From f3abc292913788bdba794d1b030e3d9263b71b0e Mon Sep 17 00:00:00 2001 From: John Ewart Date: Thu, 31 Oct 2019 16:34:34 -0700 Subject: [PATCH] Remove YB CLI container logic to use Narwhal; go 1.13 (#63) * Remove YB CLI container logic to use Narwhal -- updates in Narwhal support Darwin port checks properly --- .yourbase.yml | 2 +- cli/build.go | 77 +-- cli/exec.go | 19 +- go.mod | 4 +- go.sum | 4 + types/types.go | 97 ++-- workspace/containers.go | 1016 ---------------------------------- workspace/containers_test.go | 26 - 8 files changed, 94 insertions(+), 1151 deletions(-) delete mode 100644 workspace/containers.go delete mode 100644 workspace/containers_test.go diff --git a/.yourbase.yml b/.yourbase.yml index 123fdb00..e58b3be4 100644 --- a/.yourbase.yml +++ b/.yourbase.yml @@ -1,6 +1,6 @@ dependencies: build: - - go:1.12.10 + - go:1.13 - python:3.6 build_targets: diff --git a/cli/build.go b/cli/build.go index 8ef096c3..504e1457 100644 --- a/cli/build.go +++ b/cli/build.go @@ -19,6 +19,7 @@ import ( "time" "github.com/johnewart/archiver" + "github.com/johnewart/narwhal" "github.com/johnewart/subcommands" ybconfig "github.com/yourbase/yb/config" @@ -26,7 +27,6 @@ import ( . "github.com/yourbase/yb/plumbing" "github.com/yourbase/yb/plumbing/log" . "github.com/yourbase/yb/types" - . "github.com/yourbase/yb/workspace" ) const TIME_FORMAT = "15:04:05 MST" @@ -93,6 +93,7 @@ func (b *BuildCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) } manifest := targetPackage.Manifest + workDir := targetPackage.BuildRoot() // Determine build target buildTargetName := "default" @@ -123,17 +124,17 @@ func (b *BuildCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) // Setup dependencies containers := target.Dependencies.Containers - if len(containers) > 0 && !b.NoSideContainer { - contextId := fmt.Sprintf("%s-%s", targetPackage.Name, target.Name) - log.Infof("Starting %d containers with context id %s...", len(containers), contextId) - sc, err := NewServiceContextWithId(contextId, targetPackage, target.Dependencies.ContainerList()) - if err != nil { - log.Errorf("Couldn't create service context for dependencies: %v", err) - return subcommands.ExitFailure - } + contextId := fmt.Sprintf("%s-%s", targetPackage.Name, target.Name) + sc, err := narwhal.NewServiceContextWithId(contextId, workDir) + if err != nil { + log.Errorf("Couldn't create service context for dependencies: %v", err) + return subcommands.ExitFailure + } - buildData.Containers.ServiceContext = sc + buildData.Containers.ServiceContext = sc + if len(containers) > 0 && !b.NoSideContainer { + log.Infof("Starting %d containers with context id %s...", len(containers), contextId) if !b.ReuseContainers { log.Infof("Not reusing containers, will teardown existing ones and clean up after ourselves") defer Cleanup(buildData) @@ -143,9 +144,12 @@ func (b *BuildCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) } } - if err = sc.StandUp(); err != nil { - log.Errorf("Couldn't start dependencies: %v", err) - return subcommands.ExitFailure + for _, c := range containers { + _, err := sc.StartContainer(c) + if err != nil { + log.Errorf("Couldn't start dependencies: %v", err) + return subcommands.ExitFailure + } } } @@ -171,29 +175,30 @@ func (b *BuildCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) log.Infof("Executing build steps in container") target := primaryTarget - container := target.Container - container.Command = "/usr/bin/tail -f /dev/null" + containerDef := target.Container + containerDef.Command = "/usr/bin/tail -f /dev/null" // Append build environment variables //container.Environment = append(container.Environment, buildData.EnvironmentVariables()...) - container.Environment = []string{} + containerDef.Environment = []string{} + + // Add package to mounts @ /workspace + sourceMapDir := "/workspace" + if containerDef.WorkDir != "" { + sourceMapDir = containerDef.WorkDir + } + + log.Infof("Will mount package %s at %s in container", targetPackage.Path, sourceMapDir) + mount := fmt.Sprintf("%s:%s", targetPackage.Path, sourceMapDir) + containerDef.Mounts = append(containerDef.Mounts, mount) if false { u, _ := user.Current() fmt.Printf("U! %s", u.Uid) } - containerOpts := BuildContainerOpts{ - ContainerOpts: container, - Package: targetPackage, - Target: target.Name, - //ExecUserId: u.Uid, - //ExecGroupId: u.Gid, - MountPackage: true, - } - // Do our build in a container - don't do the build locally - buildErr := b.BuildInsideContainer(target, containerOpts, buildData, b.ExecPrefix, b.ReuseContainers) + buildErr := b.BuildInsideContainer(target, containerDef, buildData, b.ExecPrefix, b.ReuseContainers) if buildErr != nil { log.Errorf("Unable to build %s inside container: %v", target.Name, buildErr) return subcommands.ExitFailure @@ -354,11 +359,11 @@ func Cleanup(b BuildData) { } } -func (b *BuildCmd) BuildInsideContainer(target BuildTarget, containerOpts BuildContainerOpts, buildData BuildData, execPrefix string, reuseOldContainer bool) error { +func (b *BuildCmd) BuildInsideContainer(target BuildTarget, containerDef narwhal.ContainerDefinition, buildData BuildData, execPrefix string, reuseOldContainer bool) error { // Perform build inside a container image := target.Container.Image log.Infof("Using container image: %s", image) - buildContainer, err := FindContainer(containerOpts) + buildContainer, err := buildData.Containers.ServiceContext.StartContainer(containerDef) if err != nil { return fmt.Errorf("Failed trying to find container: %v", err) @@ -372,7 +377,7 @@ func (b *BuildCmd) BuildInsideContainer(target BuildTarget, containerOpts BuildC if !reuseOldContainer { log.Infof("Elected to not re-use containers, will remove the container") // Try stopping it first if needed - if err = RemoveContainerById(buildContainer.Id); err != nil { + if err = narwhal.RemoveContainerById(buildContainer.Id); err != nil { return fmt.Errorf("Unable to remove existing container: %v", err) } } @@ -382,10 +387,10 @@ func (b *BuildCmd) BuildInsideContainer(target BuildTarget, containerOpts BuildC log.Infof("Reusing existing build container %s", buildContainer.Id[0:12]) } else { log.Infof("Creating a new build container...") - if c, err := NewContainer(containerOpts); err != nil { + if c, err := buildData.Containers.ServiceContext.StartContainer(containerDef); err != nil { return fmt.Errorf("Couldn't create a new build container: %v", err) } else { - buildContainer = &c + buildContainer = c } } @@ -409,8 +414,8 @@ func (b *BuildCmd) BuildInsideContainer(target BuildTarget, containerOpts BuildC // Set container work dir containerWorkDir := "/workspace" - if containerOpts.ContainerOpts.WorkDir != "" { - containerWorkDir = containerOpts.ContainerOpts.WorkDir + if containerDef.WorkDir != "" { + containerWorkDir = containerDef.WorkDir } log.Infof("Building from %s in container: %s", containerWorkDir, buildContainer.Id) @@ -618,13 +623,13 @@ func SetupBuildDependencies(pkg Package) (TargetTimer, error) { // Build metadata; used for things like interpolation in environment variables type ContainerData struct { - ServiceContext *ServiceContext + ServiceContext *narwhal.ServiceContext } func (c ContainerData) IP(label string) string { // Check service context if c.ServiceContext != nil { - if buildContainer, ok := c.ServiceContext.BuildContainers[label]; ok { + if buildContainer, ok := c.ServiceContext.Containers[label]; ok { if ipv4, err := buildContainer.IPv4Address(); err == nil { return ipv4 } @@ -643,7 +648,7 @@ func (c ContainerData) IP(label string) string { func (c ContainerData) Environment() map[string]string { result := make(map[string]string) if c.ServiceContext != nil { - for label, container := range c.ServiceContext.BuildContainers { + for label, container := range c.ServiceContext.Containers { if ipv4, err := container.IPv4Address(); err == nil { key := fmt.Sprintf("YB_CONTAINER_%s_IP", strings.ToUpper(label)) result[key] = ipv4 diff --git a/cli/exec.go b/cli/exec.go index 60809826..ee1908fb 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -3,13 +3,14 @@ package cli import ( "context" "flag" + "path/filepath" "strings" + "github.com/johnewart/narwhal" "github.com/johnewart/subcommands" . "github.com/yourbase/yb/plumbing" "github.com/yourbase/yb/plumbing/log" - . "github.com/yourbase/yb/workspace" ) type ExecCmd struct { @@ -55,16 +56,24 @@ func (b *ExecCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) if len(containers) > 0 { log.ActiveSection("Containers") + localContainerWorkDir := filepath.Join(targetPackage.BuildRoot(), "containers") + MkdirAsNeeded(localContainerWorkDir) + + log.Infof("Will use %s as the dependency work dir", localContainerWorkDir) log.Infof("Starting %d dependencies...", len(containers)) - sc, err := NewServiceContextWithId("exec", targetPackage, containers) + sc, err := narwhal.NewServiceContextWithId("exec", targetPackage.BuildRoot()) if err != nil { log.Errorf("Couldn't create service context for dependencies: %v", err) return subcommands.ExitFailure } - if err = sc.StandUp(); err != nil { - log.Infof("Couldn't start dependencies: %v\n", err) - return subcommands.ExitFailure + for _, c := range containers { + // TODO: avoid setting these here + c.LocalWorkDir = localContainerWorkDir + if _, err = sc.StartContainer(c); err != nil { + log.Infof("Couldn't start dependencies: %v\n", err) + return subcommands.ExitFailure + } } buildData.Containers.ServiceContext = sc diff --git a/go.mod b/go.mod index bc6689d0..acb3cf01 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/gopherjs/gopherjs v0.0.0-20190915194858-d3ddacdb130f // indirect github.com/johnewart/archiver v3.1.4+incompatible github.com/johnewart/go-dockerclient v1.3.7 + github.com/johnewart/narwhal v0.0.0-20191030181313-9828f6c923b0 github.com/johnewart/subcommands v0.0.0-20181012225330-46f0354f6315 github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c // indirect github.com/juju/errors v0.0.0-20190930114154-d42613fe1ab9 // indirect @@ -41,7 +42,6 @@ require ( golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 // indirect golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect - google.golang.org/appengine v1.4.0 // indirect google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 // indirect google.golang.org/grpc v1.24.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect @@ -51,3 +51,5 @@ require ( gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/yaml.v2 v2.2.4 ) + +go 1.13 diff --git a/go.sum b/go.sum index a3917a39..c884ab65 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,10 @@ github.com/johnewart/archiver v3.1.4+incompatible h1:509GRUpPDmAvOLIeqRa1LnbhGvz github.com/johnewart/archiver v3.1.4+incompatible/go.mod h1:RZZSHcya+2fNoD31xh/kmPmwCa7hDaxCm3dFNlPDmaY= github.com/johnewart/go-dockerclient v1.3.7 h1:oyTFh8XsnQ/XdnK8D6RGHQYQSZmZ05sg2BbHbVzwIKQ= github.com/johnewart/go-dockerclient v1.3.7/go.mod h1:FCtHjONdIZH2BKTKbXFJ2Qp8h31iQkiXNboLI3M/TYg= +github.com/johnewart/narwhal v0.0.0-20191030172955-5a51cfc5963a h1:CMttmUMYzeqbuex8Zk+hGIzJ1qbeEFfI54Z/f4ahsYM= +github.com/johnewart/narwhal v0.0.0-20191030172955-5a51cfc5963a/go.mod h1:EsUUI8gi1HFwS6NoGEocjpErZEK2ER+b+D0b1qxKM/g= +github.com/johnewart/narwhal v0.0.0-20191030181313-9828f6c923b0 h1:Hrt19BwfJaNsEK29v32epT4+ZPDfL7dZ2vHbkAKvBIQ= +github.com/johnewart/narwhal v0.0.0-20191030181313-9828f6c923b0/go.mod h1:EsUUI8gi1HFwS6NoGEocjpErZEK2ER+b+D0b1qxKM/g= github.com/johnewart/subcommands v0.0.0-20181012225330-46f0354f6315 h1:IHd01G1+fdwW9G/AraiEZzx6GNbGdntaICZjP5PI+oM= github.com/johnewart/subcommands v0.0.0-20181012225330-46f0354f6315/go.mod h1:P9HfiwoEAZ8bQktefnfAuKGlAYDuwfmBqt/cgIBGt+M= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= diff --git a/types/types.go b/types/types.go index 47e5ad2a..69b2d522 100644 --- a/types/types.go +++ b/types/types.go @@ -1,9 +1,9 @@ package types import ( - "fmt" - "strings" "time" + + "github.com/johnewart/narwhal" ) const ( @@ -42,24 +42,24 @@ type DependencySet struct { } type ExecPhase struct { - Name string `yaml:"name"` - Dependencies ExecDependencies `yaml:"dependencies"` - Container ContainerDefinition `yaml:"container"` - Commands []string `yaml:"commands"` - Ports []string `yaml:"ports"` - Environment map[string][]string `yaml:"environment"` - LogFiles []string `yaml:"logfiles"` - Sandbox bool `yaml:"sandbox"` - HostOnly bool `yaml:"host_only"` - BuildFirst []string `yaml:"build_first"` + Name string `yaml:"name"` + Dependencies ExecDependencies `yaml:"dependencies"` + Container narwhal.ContainerDefinition `yaml:"container"` + Commands []string `yaml:"commands"` + Ports []string `yaml:"ports"` + Environment map[string][]string `yaml:"environment"` + LogFiles []string `yaml:"logfiles"` + Sandbox bool `yaml:"sandbox"` + HostOnly bool `yaml:"host_only"` + BuildFirst []string `yaml:"build_first"` } type BuildDependencies struct { - Containers map[string]ContainerDefinition `yaml:"containers"` + Containers map[string]narwhal.ContainerDefinition `yaml:"containers"` } -func (b BuildDependencies) ContainerList() []ContainerDefinition { - containers := make([]ContainerDefinition, 0) +func (b BuildDependencies) ContainerList() []narwhal.ContainerDefinition { + containers := make([]narwhal.ContainerDefinition, 0) for label, c := range b.Containers { c.Label = label containers = append(containers, c) @@ -68,11 +68,11 @@ func (b BuildDependencies) ContainerList() []ContainerDefinition { } type ExecDependencies struct { - Containers map[string]ContainerDefinition `yaml:"containers"` + Containers map[string]narwhal.ContainerDefinition `yaml:"containers"` } -func (b ExecDependencies) ContainerList() []ContainerDefinition { - containers := make([]ContainerDefinition, 0) +func (b ExecDependencies) ContainerList() []narwhal.ContainerDefinition { + containers := make([]narwhal.ContainerDefinition, 0) for label, c := range b.Containers { c.Label = label containers = append(containers, c) @@ -80,55 +80,20 @@ func (b ExecDependencies) ContainerList() []ContainerDefinition { return containers } -type ContainerDefinition struct { - Image string `yaml:"image"` - Mounts []string `yaml:"mounts"` - Ports []string `yaml:"ports"` - Environment []string `yaml:"environment"` - Command string `yaml:"command"` - WorkDir string `yaml:"workdir"` - Privileged bool - PortWaitCheck PortWaitCheck `yaml:"port_check"` - Label string `yaml:"label"` -} - -func (c ContainerDefinition) ImageNameWithTag() string { - return fmt.Sprintf("%s:%s", c.ImageName(), c.ImageTag()) -} - -func (c ContainerDefinition) ImageName() string { - parts := strings.Split(c.Image, ":") - return parts[0] -} - -func (c ContainerDefinition) ImageTag() string { - parts := strings.Split(c.Image, ":") - if len(parts) != 2 { - return "latest" - } else { - return parts[1] - } -} - -type PortWaitCheck struct { - Port int `yaml:"port"` - Timeout int `yaml:"timeout"` -} - type BuildTarget struct { - Name string `yaml:"name"` - Container ContainerDefinition `yaml:"container"` - Tools []string `yaml:"tools"` - Commands []string `yaml:"commands"` - Artifacts []string `yaml:"artifacts"` - CachePaths []string `yaml:"cache_paths"` - Sandbox bool `yaml:"sandbox"` - HostOnly bool `yaml:"host_only"` - Root string `yaml:"root"` - Environment []string `yaml:"environment"` - Tags map[string]string `yaml:"tags"` - BuildAfter []string `yaml:"build_after"` - Dependencies BuildDependencies `yaml:"dependencies"` + Name string `yaml:"name"` + Container narwhal.ContainerDefinition `yaml:"container"` + Tools []string `yaml:"tools"` + Commands []string `yaml:"commands"` + Artifacts []string `yaml:"artifacts"` + CachePaths []string `yaml:"cache_paths"` + Sandbox bool `yaml:"sandbox"` + HostOnly bool `yaml:"host_only"` + Root string `yaml:"root"` + Environment []string `yaml:"environment"` + Tags map[string]string `yaml:"tags"` + BuildAfter []string `yaml:"build_after"` + Dependencies BuildDependencies `yaml:"dependencies"` } // API Responses -- TODO use Swagger instead, this is silly diff --git a/workspace/containers.go b/workspace/containers.go deleted file mode 100644 index 1436df37..00000000 --- a/workspace/containers.go +++ /dev/null @@ -1,1016 +0,0 @@ -package workspace - -import ( - "archive/tar" - "bytes" - "fmt" - "io" - "io/ioutil" - "net" - "os" - "path/filepath" - "regexp" - "strings" - "time" - - docker "github.com/johnewart/go-dockerclient" - "github.com/nu7hatch/gouuid" - - . "github.com/yourbase/yb/packages" - . "github.com/yourbase/yb/plumbing" - "github.com/yourbase/yb/plumbing/log" - . "github.com/yourbase/yb/types" -) - -type ServiceContext struct { - DockerClient *docker.Client - Id string - Containers []ContainerDefinition - Package Package - NetworkId string - BuildContainers map[string]*BuildContainer -} - -type BuildContainerOpts struct { - ContainerOpts ContainerDefinition - Package Package - Target string - ExecUserId string // who to run exec as (useful for local container builds which map the source) - ExecGroupId string - MountPackage bool - Namespace string // A namespace for prefixing container names -} - -func (opts BuildContainerOpts) containerName() string { - cd := opts.ContainerOpts - s := strings.Split(cd.Image, ":") - imageName := s[0] - containerImageName := strings.Replace(imageName, "/", "_", -1) - - containerName := fmt.Sprintf("%s-%s", opts.Package.Name, containerImageName) - - if opts.Target != "" { - containerName = fmt.Sprintf("%s-%s", containerName, opts.Target) - } - - if cd.Label != "" { - containerName = fmt.Sprintf("%s-%s", containerName, cd.Label) - } - - // Prefix container name with the namespace - if opts.Namespace != "" { - containerName = fmt.Sprintf("%s-%s", opts.Namespace, containerName) - } - - return sanitizeContainerName(containerName) -} - -type BuildContainer struct { - Id string - Name string - Options BuildContainerOpts - IPv4 string -} - -func sanitizeContainerName(proposed string) string { - // Remove unusable characters from the container name - // Must match: [a-zA-Z0-9][a-zA-Z0-9_.-] - re := regexp.MustCompile(`^([a-zA-Z0-9])([a-zA-Z0-9_.-]+)$`) - - if re.MatchString(proposed) { - return proposed - } - - badChars := regexp.MustCompile(`[^a-zA-Z0-9_.-]`) - result := badChars.ReplaceAllString(proposed, "") - - firstCharRe := regexp.MustCompile(`[a-zA-Z0-9]`) - if !firstCharRe.MatchString(string(result[0])) { - result = result[1:] - } - - return result -} - -func (sc *ServiceContext) FindContainer(cd ContainerDefinition) (*BuildContainer, error) { - return FindContainer(BuildContainerOpts{ - Package: sc.Package, - ContainerOpts: cd, - Namespace: sc.Id, - }) -} - -// TODO: make sure the opts match the existing container -func FindContainer(opts BuildContainerOpts) (*BuildContainer, error) { - - containerName := opts.containerName() - - client := NewDockerClient() - log.Debugf("Looking for container: %s", containerName) - - filters := make(map[string][]string) - filters["name"] = append(filters["name"], containerName) - - result, err := client.ListContainers(docker.ListContainersOptions{ - Filters: filters, - All: true, - }) - - if err == nil && len(result) > 0 { - for _, c := range result { - log.Debugf("ID: %s -- NAME: %s", c.ID, c.Names) - } - c := result[0] - log.Debugf("Found container %s with ID %s", containerName, c.ID) - _, err := client.InspectContainer(c.ID) - if err != nil { - return nil, err - } else { - bc := BuildContainer{ - Id: c.ID, - Name: containerName, - Options: opts, - } - return &bc, nil - } - } else { - return nil, err - } - -} - -func StopContainerById(id string, timeout uint) error { - client := NewDockerClient() - log.Infof("Stopping container %s with a %d second timeout...", id, timeout) - return client.StopContainer(id, timeout) -} - -func RemoveContainerById(id string) error { - client := NewDockerClient() - return client.RemoveContainer(docker.RemoveContainerOptions{ - ID: id, - }) -} - -func (sc *ServiceContext) NewContainer(c ContainerDefinition) (BuildContainer, error) { - opts := BuildContainerOpts{ - ContainerOpts: c, - Package: sc.Package, - Namespace: sc.Id, - } - return NewContainer(opts) -} - -func NewDockerClient() *docker.Client { - // TODO: Do something smarter... - endpoint := "unix:///var/run/docker.sock" - client, err := docker.NewVersionedClient(endpoint, "1.39") - if err != nil { - panic(err) - } - return client - -} - -func PullImage(c ContainerDefinition) error { - client := NewDockerClient() - imageName := c.ImageName() - imageTag := c.ImageTag() - - pullOpts := docker.PullImageOptions{ - Repository: imageName, - Tag: imageTag, - OutputStream: os.Stdout, - } - - authConfig := docker.AuthConfiguration{} - - if err := client.PullImage(pullOpts, authConfig); err != nil { - return fmt.Errorf("Unable to pull image '%s:%s': %v", imageName, imageTag, err) - } - - return nil -} - -func PullImageIfNotHere(c ContainerDefinition) error { - client := NewDockerClient() - filters := make(map[string][]string) - - imageLabel := c.ImageNameWithTag() - log.Debugf("Pulling %s if needed...", imageLabel) - - opts := docker.ListImagesOptions{ - Filters: filters, - } - - imgs, err := client.ListImages(opts) - if err != nil { - return fmt.Errorf("Error getting image list: %v", err) - } - - foundImage := false - if len(imgs) > 0 { - for _, img := range imgs { - for _, tag := range img.RepoTags { - if tag == imageLabel { - log.Debugf("Found image: %s with tags: %s", img.ID, strings.Join(img.RepoTags, ",")) - foundImage = true - } - } - } - } - - if !foundImage { - log.Infof("Image %s not found, pulling", imageLabel) - return PullImage(c) - } - - return nil - -} - -func (b BuildContainer) Destroy() error { - client := NewDockerClient() - opts := docker.RemoveContainerOptions{ - ID: b.Id, - } - return client.RemoveContainer(opts) -} - -func (b BuildContainer) ListNetworkIDs() ([]string, error) { - client := NewDockerClient() - c, err := client.InspectContainer(b.Id) - - networkIds := make([]string, 0) - - if err != nil { - return networkIds, fmt.Errorf("Couldn't get networks for container %s: %v", b.Id, err) - } - - for _, network := range c.NetworkSettings.Networks { - networkIds = append(networkIds, network.NetworkID) - } - return networkIds, nil -} - -func (b BuildContainer) DisconnectFromNetworks() error { - - dockerClient := NewDockerClient() - if networkIds, err := b.ListNetworkIDs(); err != nil { - return fmt.Errorf("Can't get listing of networks: %v", err) - } else { - for _, networkId := range networkIds { - opts := docker.NetworkConnectionOptions{ - Container: b.Id, - EndpointConfig: &docker.EndpointConfig{ - NetworkID: networkId, - }, - Force: true, - } - - if err := dockerClient.DisconnectNetwork(networkId, opts); err != nil { - log.Warnf("Couldn't disconnect container %s from network %s: %v", b.Id, networkId, err) - } - } - } - - return nil -} - -func (b BuildContainer) EnsureRunning(uptime int) error { - - sleepTime := time.Duration(uptime) * time.Second - time.Sleep(sleepTime) - - running, err := b.IsRunning() - if err != nil { - return fmt.Errorf("Couldn't wait for running state: %v", err) - } - - if !running { - return fmt.Errorf("Container stopped running before %d seconds", uptime) - } - - return nil -} - -func (b BuildContainer) WaitForTcpPort(port int, timeout int) error { - address, err := b.IPv4Address() - if err != nil { - return fmt.Errorf("Couldn't wait for TCP port %d: %v", port, err) - } - - hostPort := fmt.Sprintf("%s:%d", address, port) - - timeWaited := 0 - secondsToSleep := 1 - sleepTime := time.Duration(secondsToSleep) * time.Second - - for timeWaited < timeout { - conn, err := net.Dial("tcp", hostPort) - if err != nil { - // Pass for now - timeWaited = timeWaited + secondsToSleep - time.Sleep(sleepTime) - } else { - conn.Close() - return nil - } - } - - return fmt.Errorf("Couldn't connect to service before specified timeout (%d sec.)", timeout) -} - -func (b BuildContainer) IPv4Address() (string, error) { - client := NewDockerClient() - c, err := client.InspectContainer(b.Id) - - if err != nil { - return "", fmt.Errorf("Couldn't determine IP of container %s: %v", b.Id, err) - } - - ipv4 := c.NetworkSettings.IPAddress - return ipv4, nil -} - -func (b BuildContainer) IsRunning() (bool, error) { - client := NewDockerClient() - c, err := client.InspectContainer(b.Id) - if err != nil { - return false, fmt.Errorf("Couldn't determine state of container %s: %v", b.Id, err) - } - - return c.State.Running, nil -} - -func (b BuildContainer) Stop(timeout uint) error { - client := NewDockerClient() - log.Infof("Stopping container %s with a %d timeout...", b.Id, timeout) - return client.StopContainer(b.Id, timeout) -} - -func (b BuildContainer) Start() error { - client := NewDockerClient() - - if running, err := b.IsRunning(); err != nil { - return fmt.Errorf("Couldn't determine if container %s is running: %v", b.Id, err) - } else { - if running { - // Nothing to do - return nil - } - } - - hostConfig := &docker.HostConfig{} - - return client.StartContainer(b.Id, hostConfig) -} - -func (b BuildContainer) DownloadDirectoryToWriter(remotePath string, sink io.Writer) error { - client := NewDockerClient() - downloadOpts := docker.DownloadFromContainerOptions{ - OutputStream: sink, - Path: remotePath, - } - - err := client.DownloadFromContainer(b.Id, downloadOpts) - if err != nil { - return fmt.Errorf("Unable to download %s: %v", remotePath, err) - } - - return nil -} - -func (b BuildContainer) DownloadDirectoryToFile(remotePath string, localFile string) error { - outputFile, err := os.OpenFile(localFile, os.O_CREATE|os.O_RDWR, 0660) - if err != nil { - return fmt.Errorf("Can't create local file: %s: %v", localFile, err) - } - - defer outputFile.Close() - - log.Infof("Downloading %s to %s...", remotePath, localFile) - - return b.DownloadDirectoryToWriter(remotePath, outputFile) -} - -func (b BuildContainer) DownloadDirectory(remotePath string) (string, error) { - - dir, err := ioutil.TempDir("", "yb-container-download") - - if err != nil { - return "", fmt.Errorf("Can't create temporary download location: %s: %v", dir, err) - } - - fileParts := strings.Split(remotePath, "/") - filename := fileParts[len(fileParts)-1] - outfileName := fmt.Sprintf("%s.tar", filename) - outfilePath := filepath.Join(dir, outfileName) - - err = b.DownloadDirectoryToFile(remotePath, outfilePath) - - if err != nil { - return "", err - } - - return outfilePath, nil -} - -func (b BuildContainer) UploadStream(source io.Reader, remotePath string) error { - client := NewDockerClient() - - uploadOpts := docker.UploadToContainerOptions{ - InputStream: source, - Path: remotePath, - NoOverwriteDirNonDir: true, - } - - err := client.UploadToContainer(b.Id, uploadOpts) - - return err -} - -func (b BuildContainer) UploadArchive(localFile string, remotePath string) error { - client := NewDockerClient() - - file, err := os.Open(localFile) - if err != nil { - return err - } - - defer file.Close() - - uploadOpts := docker.UploadToContainerOptions{ - InputStream: file, - Path: remotePath, - NoOverwriteDirNonDir: true, - } - - err = client.UploadToContainer(b.Id, uploadOpts) - - return err -} - -func (b BuildContainer) UploadFile(localFile string, fileName string, remotePath string) error { - client := NewDockerClient() - - dir, err := ioutil.TempDir("", "yb") - if err != nil { - return err - } - - defer os.RemoveAll(dir) // clean up - tmpfile, err := os.OpenFile(fmt.Sprintf("%s/%s.tar", dir, fileName), os.O_CREATE|os.O_RDWR|os.O_APPEND, 0660) - if err != nil { - return err - } - - err = archiveFile(localFile, fileName, tmpfile.Name()) - - if err != nil { - return err - } - - uploadOpts := docker.UploadToContainerOptions{ - InputStream: tmpfile, - Path: remotePath, - NoOverwriteDirNonDir: true, - } - - err = client.UploadToContainer(b.Id, uploadOpts) - - return err -} - -func (b BuildContainer) CommitImage(repository string, tag string) (string, error) { - client := NewDockerClient() - - commitOpts := docker.CommitContainerOptions{ - Container: b.Id, - Repository: repository, - Tag: tag, - } - - img, err := client.CommitContainer(commitOpts) - - if err != nil { - return "", err - } - - log.Infof("Committed container %s as image %s:%s with id %s", b.Id, repository, tag, img.ID) - - return img.ID, nil -} - -func (b BuildContainer) MakeDirectoryInContainer(path string) error { - client := NewDockerClient() - - cmdArray := strings.Split(fmt.Sprintf("mkdir -p %s", path), " ") - - execOpts := docker.CreateExecOptions{ - Env: b.Options.ContainerOpts.Environment, - Cmd: cmdArray, - AttachStdout: true, - AttachStderr: true, - Container: b.Id, - } - - exec, err := client.CreateExec(execOpts) - - if err != nil { - log.Infof("Can't create exec: %v", err) - return err - } - - startOpts := docker.StartExecOptions{ - OutputStream: os.Stdout, - ErrorStream: os.Stdout, - } - - err = client.StartExec(exec.ID, startOpts) - - if err != nil { - log.Infof("Unable to run exec %s: %v", exec.ID, err) - return err - } - - return nil - -} - -func (b BuildContainer) ExecToStdoutWithEnv(cmdString string, targetDir string, env []string) error { - return b.ExecToWriterWithEnv(cmdString, targetDir, os.Stdout, env) -} - -func (b BuildContainer) ExecToStdout(cmdString string, targetDir string) error { - return b.ExecToWriter(cmdString, targetDir, os.Stdout) -} - -func (b BuildContainer) ExecToWriter(cmdString string, targetDir string, outputSink io.Writer) error { - return b.ExecToWriterWithEnv(cmdString, targetDir, outputSink, []string{}) -} - -func (b BuildContainer) ExecToWriterWithEnv(cmdString string, targetDir string, outputSink io.Writer, env []string) error { - client := NewDockerClient() - - shellCmd := []string{"bash", "-c", cmdString} - - execOpts := docker.CreateExecOptions{ - Env: env, - Cmd: shellCmd, - AttachStdout: true, - AttachStderr: true, - Container: b.Id, - WorkingDir: targetDir, - } - - if b.Options.ExecUserId != "" || b.Options.ExecGroupId != "" { - uidGid := fmt.Sprintf("%s:%s", b.Options.ExecUserId, b.Options.ExecGroupId) - execOpts.User = uidGid - } - - exec, err := client.CreateExec(execOpts) - - if err != nil { - return fmt.Errorf("Can't create exec: %v", err) - } - - startOpts := docker.StartExecOptions{ - OutputStream: outputSink, - ErrorStream: outputSink, - } - - err = client.StartExec(exec.ID, startOpts) - - if err != nil { - return fmt.Errorf("Unable to run exec %s: %v", exec.ID, err) - } - - results, err := client.InspectExec(exec.ID) - if err != nil { - return fmt.Errorf("Unable to get exec results %s: %v", exec.ID, err) - } - - if results.ExitCode != 0 { - return fmt.Errorf("Command failed in container with status code %d", results.ExitCode) - } - - return nil - -} - -func NewContainer(opts BuildContainerOpts) (BuildContainer, error) { - containerDef := opts.ContainerOpts - - client := NewDockerClient() - - if containerDef.Image == "" { - containerDef.Image = DEFAULT_YB_CONTAINER - } - - containerName := opts.containerName() - log.Infof("Creating container '%s'", containerName) - - PullImage(containerDef) - - var mounts = make([]docker.HostMount, 0) - - buildRoot := opts.Package.BuildRoot() - pkgWorkdir := filepath.Join(buildRoot, opts.Package.Name) - - for _, mountSpec := range containerDef.Mounts { - s := strings.Split(mountSpec, ":") - src := s[0] - - if !strings.HasPrefix(src, "/") { - src = filepath.Join(pkgWorkdir, src) - MkdirAsNeeded(src) - } - - dst := s[1] - - log.Infof("Will mount %s as %s in container", src, dst) - mounts = append(mounts, docker.HostMount{ - Source: src, - Target: dst, - Type: "bind", - }) - } - - if opts.MountPackage { - sourceMapDir := "/workspace" - if containerDef.WorkDir != "" { - sourceMapDir = containerDef.WorkDir - } - - log.Infof("Will mount package %s at %s in container", opts.Package.Path, sourceMapDir) - mounts = append(mounts, docker.HostMount{ - Source: opts.Package.Path, - Target: sourceMapDir, - Type: "bind", - }) - } - - var ports = make([]string, 0) - for _, portSpec := range containerDef.Ports { - ports = append(ports, portSpec) - } - - var bindings = make(map[docker.Port][]docker.PortBinding) - - if len(ports) > 0 { - log.Infof("Will map the following ports: ") - - for _, portSpec := range containerDef.Ports { - parts := strings.Split(portSpec, ":") - externalPort := parts[0] - internalPort := parts[1] - log.Infof(" * %s -> %s in container", externalPort, internalPort) - portKey := docker.Port(fmt.Sprintf("%s/tcp", internalPort)) - var pb = make([]docker.PortBinding, 0) - pb = append(pb, docker.PortBinding{HostIP: "0.0.0.0", HostPort: externalPort}) - bindings[portKey] = pb - } - } - - hostConfig := docker.HostConfig{ - Mounts: mounts, - PortBindings: bindings, - Privileged: containerDef.Privileged, - } - - config := docker.Config{ - Env: opts.ContainerOpts.Environment, - AttachStdout: false, - AttachStdin: false, - Image: containerDef.Image, - PortSpecs: ports, - } - - if len(opts.ContainerOpts.Command) > 0 { - cmd := opts.ContainerOpts.Command - log.Debugf("Will run %s in the container", cmd) - cmdParts := strings.Split(cmd, " ") - config.Cmd = cmdParts - } - - container, err := client.CreateContainer( - docker.CreateContainerOptions{ - Name: containerName, - Config: &config, - HostConfig: &hostConfig, - }) - - if err != nil { - return BuildContainer{}, fmt.Errorf("Failed to create container: %v", err) - } - - log.Debugf("Found container ID: %s", container.ID) - - return BuildContainer{ - Name: containerName, - Id: container.ID, - Options: opts, - }, nil -} - -func FindNetworkByName(name string) (*docker.Network, error) { - dockerClient := NewDockerClient() - log.Debugf("Finding network by name %s", name) - filters := make(map[string]map[string]bool) - filter := make(map[string]bool) - filter[name] = true - filters["name"] = filter - networks, err := dockerClient.FilteredListNetworks(filters) - - if err != nil { - return nil, fmt.Errorf("Can't filter networks by name %s: %v", name, err) - } - - if len(networks) == 0 { - return nil, nil - } - - network := networks[0] - return &network, nil -} - -func NewServiceContextWithId(ctxId string, pkg Package, containers []ContainerDefinition) (*ServiceContext, error) { - dockerClient := NewDockerClient() - log.Infof("Creating service context '%s'...", ctxId) - - // Find network by context Id - sc := &ServiceContext{ - Package: pkg, - Id: ctxId, - DockerClient: dockerClient, - Containers: containers, - NetworkId: "", - BuildContainers: make(map[string]*BuildContainer), - } - - return sc, nil -} - -func NewServiceContext(pkg Package, containers []ContainerDefinition) (*ServiceContext, error) { - ctxId, _ := uuid.NewV4() - return NewServiceContextWithId(ctxId.String(), pkg, containers) -} - -func (sc *ServiceContext) CreateNetwork() error { - dockerClient := NewDockerClient() - - network, err := FindNetworkByName(sc.Id) - - if err != nil { - return fmt.Errorf("Error trying to find existing network: %v", err) - } - - if err == nil && network == nil { - opts := docker.CreateNetworkOptions{ - Name: sc.Id, - Driver: "bridge", - } - - network, err = dockerClient.CreateNetwork(opts) - - if err != nil { - return fmt.Errorf("Unable to create Docker network: %v", err) - } - - } - - sc.NetworkId = network.ID - - return nil -} - -func (sc *ServiceContext) TearDown() error { - log.Infof("Terminating services...") - - for _, c := range sc.Containers { - log.Infof(" %s...", c.Image) - - container, err := sc.FindContainer(c) - - if err != nil { - log.Infof("Problem searching for container: %v", err) - } - - if container != nil { - log.Infof(" %s", container.Id) - container.Stop(0) - if err := container.Destroy(); err != nil { - log.Warnf("Unable to destroy container: %v", err) - } - } - } - - client := sc.DockerClient - if sc.NetworkId != "" { - log.Infof("Removing network...") - err := client.RemoveNetwork(sc.NetworkId) - if err != nil { - log.Warnf("Unable to remove network %s: %v", sc.NetworkId, err) - } - } - - return nil -} - -func (sc *ServiceContext) StandUp() error { - dockerClient := sc.DockerClient - - if err := sc.CreateNetwork(); err != nil { - return fmt.Errorf("Problem establishing service network: %v", err) - } - - buildRoot := sc.Package.BuildRoot() - pkgWorkdir := filepath.Join(buildRoot, sc.Package.Name) - MkdirAsNeeded(pkgWorkdir) - logDir := filepath.Join(pkgWorkdir, "logs") - MkdirAsNeeded(logDir) - - log.Infof("Starting up dependent containers and network...") - - // TODO: Move away from this dict and just have people use an array - for _, c := range sc.Containers { - - log.Infof(" === %s (using %s:%s) ===", c.Label, c.ImageName(), c.ImageTag()) - - container, err := sc.FindContainer(c) - - if err != nil { - return fmt.Errorf("Problem searching for container %s: %v", c.Image, err) - } - - if container != nil { - log.Infof("Container for %s already exists, not re-creating...", c.Image) - } else { - c, err := sc.NewContainer(c) - if err != nil { - return err - } - container = &c - log.Infof("Created container: %s", container.Id) - - // Attach to network - log.Infof("Attaching container to network ... ") - opts := docker.NetworkConnectionOptions{ - Container: container.Id, - EndpointConfig: &docker.EndpointConfig{ - NetworkID: sc.NetworkId, - }, - } - - if err = dockerClient.ConnectNetwork(sc.NetworkId, opts); err != nil { - return fmt.Errorf("Couldn't connect container %s to network %s: %v", container.Id, sc.NetworkId, err) - } - - } - - // Add to list of build containers - sc.BuildContainers[c.Label] = container - - running, err := container.IsRunning() - if err != nil { - return fmt.Errorf("Couldn't determine if container is running: %v", err) - } - - if !running { - log.Infof("Starting container for %s...", c.Image) - if err = container.Start(); err != nil { - return fmt.Errorf("Couldn't start container %s: %v", container.Id, err) - } - } - - ipv4, err := container.IPv4Address() - if err != nil { - return fmt.Errorf("Couldn't determine IP of container dependency %s (%s): %v", c.Label, container.Id, err) - } - - if ipv4 == "" { - return fmt.Errorf("Container didn't get an IP address -- check the logs for container %s", container.Id[0:12]) - } - log.Infof("Container IP: %s", ipv4) - - if c.PortWaitCheck.Port != 0 { - check := c.PortWaitCheck - log.Infof("Waiting up to %ds for %s:%d to be ready... ", check.Timeout, ipv4, check.Port) - if err := container.WaitForTcpPort(check.Port, check.Timeout); err != nil { - log.Warnf("Timed out!") - return fmt.Errorf("Timeout occured waiting for container '%s' to be ready", c.Label) - } - } - - /*//TODO: stream logs from each dependency to the build dir - containerLogFile := filepath.Join(logDir, fmt.Sprintf("%s.log", imageName)) - Name: - f, err := os.Create(containerLogFile) - - if err != nil { - log.Infof("Unable to write to log file %s: %v", containerLogFile, err) - return err - } - - out, err := dockerClient.ContainerLogs(ctx, dependencyContainer.ID, types.ContainerLogsOptions{ - ShowStderr: true, - ShowStdout: true, - Timestamps: false, - Follow: true, - Tail: "40", - }) - if err != nil { - log.Infof("Can't get log handle for container %s: %v", dependencyContainer.ID, err) - return err - } - go func() { - for { - io.Copy(f, out) - time.Sleep(300 * time.Millisecond) - } - }() - */ - } - - return nil -} - -func archiveFileInMemory(source string, target string) (*tar.Reader, error) { - var buf bytes.Buffer - - tarball := tar.NewWriter(&buf) - defer tarball.Close() - - info, err := os.Stat(source) - if err != nil { - return nil, err - } - - header, err := tar.FileInfoHeader(info, info.Name()) - if err != nil { - return nil, err - } - - header.Name = target - - log.Infof("Adding %s as %s...", info.Name(), header.Name) - - if err := tarball.WriteHeader(header); err != nil { - return nil, err - } - - fh, err := os.Open(source) - if err != nil { - return nil, err - } - defer fh.Close() - _, err = io.Copy(tarball, fh) - - tarball.Close() - - tr := tar.NewReader(&buf) - return tr, nil - -} - -func archiveFile(source string, target string, tarfile string) error { - tf, err := os.Create(tarfile) - if err != nil { - return err - } - defer tf.Close() - - tarball := tar.NewWriter(tf) - defer tarball.Close() - - info, err := os.Stat(source) - if err != nil { - return err - } - - header, err := tar.FileInfoHeader(info, info.Name()) - if err != nil { - return err - } - - header.Name = target - - log.Infof("Adding %s as %s...", info.Name(), header.Name) - - if err := tarball.WriteHeader(header); err != nil { - return err - } - - fh, err := os.Open(source) - if err != nil { - return err - } - defer fh.Close() - _, err = io.Copy(tarball, fh) - - tarball.Close() - - return nil - -} diff --git a/workspace/containers_test.go b/workspace/containers_test.go deleted file mode 100644 index faca5fda..00000000 --- a/workspace/containers_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package workspace - -import "testing" - -func TestSanitizeContainerName(t *testing.T) { - bogus := []string{ - "_1234ABcd", - "123$@!#$%4567", - "abc.d.e.rfg*()1234", - "aaa-bbb-ccc-1234-dd-ff/postgresql", - } - expected := []string{ - "1234ABcd", - "1234567", - "abc.d.e.rfg1234", - "aaa-bbb-ccc-1234-dd-ffpostgresql", - } - - for i, input := range bogus { - result := sanitizeContainerName(input) - wanted := expected[i] - if result != wanted { - t.Errorf("sanitized name was incorrect, got: '%s', want: '%s'", result, wanted) - } - } -}