Skip to content

Commit

Permalink
Construct dependency graph and run in order
Browse files Browse the repository at this point in the history
Resolves #4
  • Loading branch information
svrana committed Jun 11, 2019
1 parent 5efb2f8 commit 35262e7
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 78 deletions.
32 changes: 6 additions & 26 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ func addProjectCommands(projectCmd *cobra.Command, devConfig *config.Dev, projec
Use: dev.BUILD,
Short: "Build the " + project.Name + " container (and its dependencies)",
PreRun: func(cmd *cobra.Command, args []string) {
initDeps()
if err := dev.InitDeps(appConfig, dev.BUILD, project); err != nil {
log.Fatalf("dependency initialization error: %s", err)
}
},
Run: func(cmd *cobra.Command, args []string) {
dev.RunComposeBuild(
Expand All @@ -82,7 +84,9 @@ func addProjectCommands(projectCmd *cobra.Command, devConfig *config.Dev, projec
Use: dev.UP,
Short: "Create and start the " + project.Name + " containers",
PreRun: func(cmd *cobra.Command, args []string) {
initDeps()
if err := dev.InitDeps(appConfig, dev.UP, project); err != nil {
log.Fatalf("dependency initialization error: %s", err)
}
},
Run: func(cmd *cobra.Command, args []string) {
project.Up(appConfig, true)
Expand Down Expand Up @@ -265,30 +269,6 @@ func locateConfigFile() string {
return ""
}

func createObjectMap(devConfig *config.Dev) map[string]interface{} {
objMap := make(map[string]interface{})

for name, opts := range devConfig.Projects {
objMap[name] = dev.NewProject(opts)
}

for name, opts := range devConfig.Networks {
objMap[name] = dev.NewNetwork(name, opts)
}

for name, opts := range devConfig.Registries {
objMap[name] = dev.NewRegistry(opts)
}

return objMap

}

// initDeps constructs the dag for the specified project command and initializes
// the appropriate dependencies.
func initDeps() {
}

// initConfig locates the configuration file and loads it into the Config
func initConfig() {
cfgFile := viper.GetString("CONFIG")
Expand Down
11 changes: 5 additions & 6 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

"github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus"
"github.com/wish/dev/config"
)

// A dumping ground for utilities used across commands that use exec.Command to run..
Expand Down Expand Up @@ -72,17 +71,17 @@ func RunComposeDown(project string, composePaths []string, args ...string) {
runDockerCompose("down", project, composePaths, args...)
}

// RunOnContainer runs the commands on the Project container specified in
// config.Project using the docker command.
func RunOnContainer(projectName string, project *config.Project, cmds ...string) {
cmdLine := []string{"-p", projectName}
// RunOnContainer runs the commands on the container with the specified
// name using the 'docker' command.
func RunOnContainer(containerName string, cmds ...string) {
cmdLine := []string{"exec"}

// avoid "input device is not a tty error"
if isatty.IsTerminal(os.Stdout.Fd()) {
cmdLine = append(cmdLine, "-it")
}

cmdLine = append(cmdLine, project.Name)
cmdLine = append(cmdLine, containerName)

for _, cmd := range cmds {
cmdLine = append(cmdLine, cmd)
Expand Down
6 changes: 4 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ type Project struct {
// Shell used to enter the project container with 'sh' command,
// default is /bin/bash
Shell string `mapstructure:"shell"`
// Projects, registries, networks on which this project depends.
Dependencies []string `mapstructure:"depends_on"`
}

// Registry repesents the configuration required to model a container registry.
Expand Down Expand Up @@ -164,8 +166,8 @@ func projectNameFromPath(projectPath string) string {

func newProjectConfig(projectPath, composeFilename string) *Project {
project := &Project{
Directory: projectPath,
Name: projectNameFromPath(projectPath),
Directory: projectPath,
Name: projectNameFromPath(projectPath),
DockerComposeFilenames: []string{composeFilename},
}

Expand Down
143 changes: 138 additions & 5 deletions dependency.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package dev

import (
"github.com/goombaio/dag"
d "github.com/goombaio/dag"
"github.com/pkg/errors"

c "github.com/wish/dev/config"

log "github.com/sirupsen/logrus"
)

const (
Expand All @@ -12,14 +18,14 @@ const (
// DOWN constant referring to the "down" command of this project which
// stops and removes the project container.
DOWN = "down"
// PS constant referring to the "ps" command of this project which shows
// the status of the containers used by the project.
// PS constant referring to the "ps" command of this project which
// shows the status of the containers used by the project.
PS = "ps"
// SH constant referring to the "sh" command of this project which runs
// commands on the project container.
SH = "sh"
// UP constant referring to the "up" command of this project which starts
// the project and any of the specified dependencies.
// UP constant referring to the "up" command of this project which
// starts the project and any of the specified dependencies.
UP = "up"
)

Expand All @@ -29,5 +35,132 @@ const (
type Dependency interface {
// PreRun does whatever is required of the dependency. It is run prior
// to the specified command for the given project.
PreRun(command string, appConfig *c.Dev, project *c.Project)
PreRun(command string, appConfig *c.Dev, project *Project)
// Dependencies returns the names of all the dev objects it depends on
// in order to function.
Dependencies() []string
// Name of the depencency. Maps to the name given to the object in the
// dev configuration file.
GetName() string
}

func createObjectMap(devConfig *c.Dev) map[string]Dependency {
objMap := make(map[string]Dependency)

for name, opts := range devConfig.Projects {
objMap[name] = NewProject(opts)
}

for name, opts := range devConfig.Networks {
objMap[name] = NewNetwork(name, opts)
}

for name, opts := range devConfig.Registries {
objMap[name] = NewRegistry(opts)
}

return objMap
}

func addDeps(objMap map[string]Dependency, dag *d.DAG, obj Dependency) error {
for _, depName := range obj.Dependencies() {
vertex := d.NewVertex(depName, objMap[depName])
if err := dag.AddVertex(vertex); err != nil {
return err
}
parent, err := dag.GetVertex(obj.GetName())
if err != nil {
return errors.Wrapf(err, "Unable to locate vertex for: %s", obj.GetName())
}
if err := dag.AddEdge(parent, vertex); err != nil {
return errors.Wrapf(err, "Failure adding edge from %s to %s", parent.ID, vertex.ID)
}

if err := addDeps(objMap, dag, objMap[depName]); err != nil {
return err
}
}
return nil
}

// Hmm, the libraries deleteEdge does not remove the parent from the childs
// parent list.
func deleteEdge(parent *dag.Vertex, child *dag.Vertex) error {
for _, c := range parent.Children.Values() {
if c == child {
parent.Children.Remove(child)
child.Parents.Remove(parent)
}
}

return nil
}

func topologicalSort(dag *d.DAG, vertex *d.Vertex) ([]string, error) {
sorted := []string{}
parentless := make(map[string]bool)

parentless[vertex.ID] = true

for ok := true; ok; ok = len(parentless) > 0 {
var n string
for key := range parentless {
n = key
break
}
sorted = SliceInsertString(sorted, n, 0)
delete(parentless, n)

v, err := dag.GetVertex(n)
if err != nil {
return nil, errors.Wrapf(err, "Unexpected missing vertex for: %s", n)
}

children, _ := dag.Successors(v)
for _, child := range children {
//log.Debugf("got a child %s with %d incoming edges", child.ID, child.InDegree())
if err := deleteEdge(v, child); err != nil {
return nil, err
}
if child.InDegree() == 0 {
//log.Debugf("no incoming edges for %s", child.ID)
parentless[child.ID] = true
}
}
}

if dag.Size() != 0 {
return nil, errors.Errorf("Dependency graph has a cycle")
}

// do not return the initial vertex in the final list, b/c we are only
// interested in the dependencies
return sorted[0 : len(sorted)-1], nil
}

// InitDeps runs the PreRun method on each dependency for the specified
// Project.
func InitDeps(appConfig *c.Dev, cmd string, project *Project) error {
dag := d.NewDAG()
vertex := d.NewVertex(project.Name, project)
if err := dag.AddVertex(vertex); err != nil {
return err
}

objMap := createObjectMap(appConfig)
if err := addDeps(objMap, dag, project); err != nil {
return errors.Wrap(err, "Failure mapping dependencies")
}

deps, err := topologicalSort(dag, vertex)
if err != nil {
return errors.Wrapf(err, "Failure sorting dependencies for %s", project.Name)
}

log.Debugf("Initializing dependencies for %s: %s", project.Name, deps)
for _, dep := range deps {
objMap[dep].PreRun(cmd, appConfig, project)
}

return nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/docker/go-units v0.4.0 // indirect
github.com/gogo/protobuf v1.2.1 // indirect
github.com/google/go-cmp v0.3.0 // indirect
github.com/goombaio/dag v0.0.0-20181006234417-a8874b1f72ff
github.com/gorilla/mux v1.7.2 // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/goombaio/dag v0.0.0-20181006234417-a8874b1f72ff h1:TWR7dWx09TvI7hfy3H1TXwazeOCRUC5+Gove4hefk6o=
github.com/goombaio/dag v0.0.0-20181006234417-a8874b1f72ff/go.mod h1:QulI5HOQMQJGBYLdTkWDiHWvz+E323DSypoD42v2wEU=
github.com/goombaio/orderedmap v0.0.0-20180919235155-bc5581d0235c/go.mod h1:YKu81H3RSd1cFh0d7NhvUoTtUC9IY/vBX0WUQb1/o4Y=
github.com/goombaio/orderedmap v0.0.0-20180924084748-ba921b7e2419 h1:SajEQ6tktpF9SRIuzbiPOX9AEZZ53Bvw0k9Mzrts8Lg=
github.com/goombaio/orderedmap v0.0.0-20180924084748-ba921b7e2419/go.mod h1:YKu81H3RSd1cFh0d7NhvUoTtUC9IY/vBX0WUQb1/o4Y=
github.com/goombaio/orderedset v0.0.0-20180924084730-d1b9fdd81eca h1:RiwElNGM1lrT2a3hAuQn36nM4HWI4bOgIWi077O33Yk=
github.com/goombaio/orderedset v0.0.0-20180924084730-d1b9fdd81eca/go.mod h1:6oeyMssEjbCGe1BCbSckd6C1TYxeP5Cgp8BoKejycj0=
github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
Expand Down
49 changes: 30 additions & 19 deletions network.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,38 @@ type Network struct {
Config *types.NetworkCreate
}

// NewNetwork is a Network constructor.
// NewNetwork is the Network constructor.
func NewNetwork(name string, config *types.NetworkCreate) *Network {
return &Network{
Name: name,
Config: config,
}
}

// networksCreate creates any external network configured in the dev tool if
// it does not exist already. It returns the network id used to indentify the
// network by docker.
func (r *Network) create() string {
networkID, err := docker.NetworkIDFromName(r.Name)
// create any external network configured in the dev tool if it does not exist
// already. It returns the network id used to indentify the network by docker.
func (n *Network) create() string {
networkID, err := docker.NetworkIDFromName(n.Name)
if err != nil {
err = errors.Wrapf(err, "Error checking if network %s exists", r.Name)
err = errors.Wrapf(err, "Error checking if network %s exists", n.Name)
log.Fatal(err)
}
if networkID == "" {
networkID, err = docker.NetworkCreate(r.Name, r.Config)
log.Infof("Created %s network %s", r.Name, networkID)
networkID, err = docker.NetworkCreate(n.Name, n.Config)
log.Infof("Created %s network %s", n.Name, networkID)
if err != nil {
log.Fatal(err)
}
} else {
log.Debugf("Network %s already exists with id %s", r.Name, networkID)
log.Debugf("Network %s already exists with id %s", n.Name, networkID)
}

return networkID
}

// createNetworkServiceMap creates a mapping from the networks configured by dev
// to a list of the services that use them in the projects docker-compose files.
func (r *Network) createNetworkServiceMap(devConfig *config.Dev, project *config.Project,
func (n *Network) createNetworkServiceMap(devConfig *config.Dev, project *config.Project,
networkIDMap map[string]string) map[string][]string {

serviceNetworkMap := make(map[string][]string, len(networkIDMap))
Expand All @@ -69,19 +68,19 @@ func (r *Network) createNetworkServiceMap(devConfig *config.Dev, project *config
return serviceNetworkMap
}

// updateContainers performs container operations necessary to get the
// verifyContainerConfig performs container operations necessary to get the
// containers into the state specified in the dev appConfig files.
//
// Networks do not persist reboots. Container configured with an old network id
// that no longer exists will not be able to start (docker-compose up will fail
// when it attempts to start the container). These containers must be removed
// before we attempt to start the container.
func (r *Network) verifyContainerConfig(appConfig *config.Dev, project *config.Project, networkID string) {
func (n *Network) verifyContainerConfig(appConfig *config.Dev, project *config.Project, networkID string) {
networkIDMap := map[string]string{
r.Name: networkID,
n.Name: networkID,
}

networkServiceMap := r.createNetworkServiceMap(appConfig, project, networkIDMap)
networkServiceMap := n.createNetworkServiceMap(appConfig, project, networkIDMap)
for networkName, services := range networkServiceMap {
networkID := networkIDMap[networkName]
err := docker.RemoveContainerIfRequired(networkName, networkID, services)
Expand All @@ -92,12 +91,24 @@ func (r *Network) verifyContainerConfig(appConfig *config.Dev, project *config.P
}

// PreRun implements the Dependency interface. It will destroy any containers
// that are attached to a no longer existing networ of the same name such that
// that are attached to a no longer existing network of the same name such that
// the containers can be created with the correct network.
func (r *Network) PreRun(command string, appConfig *c.Dev, project *c.Project) {
func (n *Network) PreRun(command string, appConfig *c.Dev, project *Project) {
if !SliceContainsString([]string{UP, SH}, command) {
return
}
networkID := r.create()
r.verifyContainerConfig(appConfig, project, networkID)
networkID := n.create()
n.verifyContainerConfig(appConfig, project.Config, networkID)
}

// Dependencies implements the Dependency interface. At this time a Network
// cannot have dependencies so it returns an empty slice.
func (n *Network) Dependencies() []string {
return []string{}
}

// GetName returns the name of the network as named by the user in the
// configuration file.
func (n *Network) GetName() string {
return n.Name
}
Loading

0 comments on commit 35262e7

Please sign in to comment.