diff --git a/.gitignore b/.gitignore index b205ba3d..d0736e62 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ builds/* +dist/* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index cafbf7ae..e92f3dad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ matrix: # Build - make build # Test - - go test -v + - go test -v ./... - name: Linux build w/ tests os: linux diff --git a/Makefile b/Makefile index 0d594ab0..505778df 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ build: docker run -v $(DIR):/data pygmy-go cp pygmy-go-darwin /data/builds/. docker run -v $(DIR):/data pygmy-go cp pygmy-go.exe /data/builds/. @echo "Done" - @echo "Enjoy using pygmy-go binaries in $(DIR)/data/build directory." + @echo "Enjoy using pygmy-go binaries in $(DIR)/build directory." clean: docker image rm -f pygmy-go diff --git a/README.md b/README.md index fb52a017..282c640c 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,33 @@ Content-Type: text/html ``` Thanks for testing, please post issues and successes in the queue. + +## Local development + +To run full regression tests locally, you can follow this process if you have `cmake`, `git` and `go` installed. This +will prevent a significant amount of build failures and problems after committing. + +It will use `dind` and your local daemon to walk through several tests which should pass. + +1. First clone the project: + ``` + git clone https://github.com/fubarhouse/pygmy-go.git pygmy-go && cd pygmy-go + ``` +2. Perform any updates as required. +3. Clean the environment. + ``` + go run main.go clean + ``` +4. Build the project. + ``` + make + ``` +5. Test the project prior to commiting. + ``` + go test -v + ``` ## Releasing We use GitHub Actions for simulating the automated release tagging locally. Using [Act](https://github.com/nektos/act) locally, you can simulate this process and have the same build artifacts in your `dist` folder. - This process will inject the appropriate values into the version logic. To start the process, just run `act`! \ No newline at end of file diff --git a/go.mod b/go.mod index 6a8f77d2..99036935 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/docker/go-units v0.4.0 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/ghodss/yaml v1.0.0 + github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/gorilla/mux v1.7.4 // indirect github.com/imdario/mergo v0.3.9 github.com/mitchellh/go-homedir v1.1.0 @@ -20,6 +21,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pelletier/go-toml v1.7.0 // indirect + github.com/smartystreets/assertions v1.1.1 // indirect github.com/smartystreets/goconvey v1.6.4 github.com/spf13/afero v1.2.2 // indirect github.com/spf13/cast v1.3.1 // indirect diff --git a/go.sum b/go.sum index 065a0459..4b2dd0e0 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -203,6 +205,8 @@ github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.1 h1:T/YLemO5Yp7KPzS+lVtu+WsHn8yoSwTfItdAd1r3cck= +github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= diff --git a/main_test.go b/main_test.go index c01d9be2..e3de918e 100644 --- a/main_test.go +++ b/main_test.go @@ -1,7 +1,6 @@ -package main +package main_test import ( - "context" "fmt" "os" "testing" @@ -10,13 +9,13 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" - "github.com/docker/docker/client" - model "github.com/fubarhouse/pygmy-go/service/interface" + "github.com/fubarhouse/pygmy-go/service/interface/docker" . "github.com/smartystreets/goconvey/convey" ) const ( dindContainerName = "exampleTestContainer" + binaryReference = "pygmy-go-linux-x86" ) var ( @@ -37,54 +36,46 @@ type config struct { // run to keep the consistency for as many tests as are required. func setup(t *testing.T, config *config) { - var cleanCmd = "/builds/pygmy-go-linux-x86 clean" - var statusCmd = "/builds/pygmy-go-linux-x86 status" - var upCmd = "/builds/pygmy-go-linux-x86 up" - var downCmd = "/builds/pygmy-go-linux-x86 down" + var cleanCmd = fmt.Sprintf("/builds/%v clean", binaryReference) + var statusCmd = fmt.Sprintf("/builds/%v status", binaryReference) + var upCmd = fmt.Sprintf("/builds/%v up", binaryReference) + var downCmd = fmt.Sprintf("/builds/%v down", binaryReference) if config.configpath != "" { - cleanCmd = fmt.Sprintf("/builds/pygmy-go-linux-x86 clean --config %v", config.configpath) - statusCmd = fmt.Sprintf("/builds/pygmy-go-linux-x86 status --config %v", config.configpath) - upCmd = fmt.Sprintf("/builds/pygmy-go-linux-x86 up --config %v", config.configpath) - downCmd = fmt.Sprintf("/builds/pygmy-go-linux-x86 dow --config %v", config.configpath) + cleanCmd = fmt.Sprintf("/builds/%v clean --config %v", binaryReference, config.configpath) + statusCmd = fmt.Sprintf("/builds/%v status --config %v", binaryReference, config.configpath) + upCmd = fmt.Sprintf("/builds/%v up --config %v", binaryReference, config.configpath) + downCmd = fmt.Sprintf("/builds/%v dow --config %v", binaryReference, config.configpath) } Convey("Pygmy Application Test: "+config.name, t, func() { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - Convey("Provision environment", func() { - Convey("Connection to Docker Client", func() { - So(err, ShouldEqual, nil) - }) - Convey("Image pulled", func() { - _, e := model.DockerPull("library/docker:dind") + _, e := docker.DockerPull("library/docker:dind") So(e, ShouldBeNil) }) Convey("Container created", func() { currentWorkingDirectory, err := os.Getwd() So(err, ShouldBeNil) - x, _ := cli.ContainerCreate(ctx, &container.Config{ + x, _ := docker.DockerContainerCreate(dindContainerName, container.Config{ Image: "docker:dind", - }, &container.HostConfig{ + }, container.HostConfig{ AutoRemove: false, Binds: []string{ fmt.Sprintf("%v%vbuilds%v:/builds", currentWorkingDirectory, string(os.PathSeparator), string(os.PathSeparator)), fmt.Sprintf("%v%vexamples%v:/examples", currentWorkingDirectory, string(os.PathSeparator), string(os.PathSeparator)), }, Privileged: true, - }, &network.NetworkingConfig{}, dindContainerName) + }, network.NetworkingConfig{}) dindID = x.ID So(dindID, ShouldNotEqual, "") }) Convey("Container started", func() { - err = cli.ContainerStart(ctx, dindContainerName, types.ContainerStartOptions{}) + err := docker.DockerContainerStart(dindContainerName, types.ContainerStartOptions{}) So(err, ShouldEqual, nil) }) }) @@ -92,19 +83,19 @@ func setup(t *testing.T, config *config) { Convey("Populating Daemon", func() { Convey("Container has started the daemon", func() { - _, e := model.DockerExec(dindContainerName, "dockerd") + _, e := docker.DockerExec(dindContainerName, "dockerd") So(e, ShouldEqual, nil) time.Sleep(time.Second * 2) }) - e := cli.ContainerStart(ctx, dindContainerName, types.ContainerStartOptions{}) + e := docker.DockerContainerStart(dindContainerName, types.ContainerStartOptions{}) if e != nil { fmt.Println(e) } for _, image := range config.images { Convey("Pulling "+image, func() { - _, e := model.DockerExec(dindContainerName, "docker pull "+image) + _, e := docker.DockerExec(dindContainerName, "docker pull "+image) time.Sleep(time.Second * 2) So(e, ShouldBeNil) }) @@ -114,7 +105,7 @@ func setup(t *testing.T, config *config) { Convey("Application Tests", func() { Convey("Container has configuration file ("+config.configpath+")", func() { - d, _ := model.DockerExec(dindContainerName, "stat "+config.configpath) + d, _ := docker.DockerExec(dindContainerName, "stat "+config.configpath) if config.configpath == "" { SkipSo(string(d), ShouldContainSubstring, config.configpath) } else { @@ -123,30 +114,30 @@ func setup(t *testing.T, config *config) { }) Convey("Container has compiled binary from host", func() { - d, _ := model.DockerExec(dindContainerName, "stat /builds/pygmy-go-linux-x86") - So(string(d), ShouldContainSubstring, "/builds/pygmy-go-linux-x86") + d, _ := docker.DockerExec(dindContainerName, fmt.Sprintf("stat /builds/%v", binaryReference)) + So(string(d), ShouldContainSubstring, fmt.Sprintf("/builds/%v", binaryReference)) }) - d, _ := model.DockerExec(dindContainerName, "/builds/pygmy-go-linux-x86") + d, _ := docker.DockerExec(dindContainerName, fmt.Sprintf("/builds/%v", binaryReference)) Convey("Container can run pygmy", func() { So(string(d), ShouldContainSubstring, "local containers for local development") }) // While it's safe, we should clean the environment. - _, e := model.DockerExec(dindContainerName, cleanCmd) + _, e := docker.DockerExec(dindContainerName, cleanCmd) if e != nil { fmt.Println(e) } Convey("Default ports are not allocated", func() { - g, _ := model.DockerExec(dindContainerName, statusCmd) + g, _ := docker.DockerExec(dindContainerName, statusCmd) for _, service := range config.servicewithports { So(string(g), ShouldContainSubstring, service+" is able to start") } }) Convey("Pygmy started", func() { - d, _ = model.DockerExec(dindContainerName, upCmd) + d, _ = docker.DockerExec(dindContainerName, upCmd) if config.configpath != "" { So(string(d), ShouldContainSubstring, "Using config file: "+config.configpath) } @@ -156,7 +147,7 @@ func setup(t *testing.T, config *config) { }) Convey("Endpoints are serving", func() { - d, _ = model.DockerExec(dindContainerName, statusCmd) + d, _ = docker.DockerExec(dindContainerName, statusCmd) for _, endpoint := range config.endpoints { if config.skipendpointchecks { SkipSo(string(d), ShouldNotContainSubstring, "! "+endpoint) @@ -170,11 +161,11 @@ func setup(t *testing.T, config *config) { Convey("Environment Cleanup", func() { Convey("Pygmy has cleaned the environment", func() { - _, e := model.DockerExec(dindContainerName, downCmd) + _, e := docker.DockerExec(dindContainerName, downCmd) So(e, ShouldBeNil) - _, e = model.DockerExec(dindContainerName, cleanCmd) + _, e = docker.DockerExec(dindContainerName, cleanCmd) So(e, ShouldBeNil) - d, _ := model.DockerExec(dindContainerName, statusCmd) + d, _ := docker.DockerExec(dindContainerName, statusCmd) for _, service := range config.services { So(string(d), ShouldContainSubstring, service+" is not running") } @@ -182,13 +173,9 @@ func setup(t *testing.T, config *config) { }) // System prune container... Convey("Removing DinD Container", func() { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - So(err, ShouldBeNil) - err = cli.ContainerKill(ctx, "exampleTestContainer", "") + err := docker.DockerKill("exampleTestContainer") So(err, ShouldBeNil) - err = cli.ContainerRemove(ctx, "exampleTestContainer", types.ContainerRemoveOptions{Force: true}) + err = docker.DockerRemove("exampleTestContainer") So(err, ShouldBeNil) }) }) @@ -204,7 +191,7 @@ func TestDefault(t *testing.T) { images: []string{"amazeeio/haproxy", "andyshinn/dnsmasq:2.78", "mailhog/mailhog"}, services: []string{"amazeeio-haproxy", "amazeeio-dnsmasq", "amazeeio-mailhog"}, servicewithports: []string{"amazeeio-haproxy", "amazeeio-mailhog"}, - skipendpointchecks: true, + skipendpointchecks: false, } setup(t, configuration) } diff --git a/service/dnsmasq/dnsmasq_test.go b/service/dnsmasq/dnsmasq_test.go new file mode 100644 index 00000000..d31cbf42 --- /dev/null +++ b/service/dnsmasq/dnsmasq_test.go @@ -0,0 +1,33 @@ +package dnsmasq_test + +import ( + "fmt" + "testing" + + "github.com/docker/go-connections/nat" + "github.com/fubarhouse/pygmy-go/service/dnsmasq" + . "github.com/smartystreets/goconvey/convey" +) + +func Example() { + dnsmasq.New() +} + +func Test(t *testing.T) { + Convey("DNSMasq: Field equality tests...", t, func() { + obj := dnsmasq.New() + + So(obj.Config.Image, ShouldEqual, "andyshinn/dnsmasq:2.78") + So(fmt.Sprint(obj.Config.Cmd), ShouldEqual, fmt.Sprint([]string{"-A", "/docker.amazee.io/127.0.0.1"})) + So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-dnsmasq") + So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "13") + So(obj.HostConfig.AutoRemove, ShouldBeFalse) + So(fmt.Sprint(obj.HostConfig.CapAdd), ShouldEqual, fmt.Sprint([]string{"NET_ADMIN"})) + So(obj.HostConfig.IpcMode, ShouldEqual, "private") + So(fmt.Sprint(obj.HostConfig.PortBindings), ShouldEqual, fmt.Sprint(nat.PortMap{"53/tcp": []nat.PortBinding{{HostIP: "", HostPort: "6053"}}, "53/udp": []nat.PortBinding{{HostIP: "", HostPort: "6053"}}})) + So(obj.HostConfig.RestartPolicy.Name, ShouldEqual, "on-failure") + So(obj.HostConfig.RestartPolicy.MaximumRetryCount, ShouldBeZeroValue) + }) +} diff --git a/service/endpoint/endpoint_test.go b/service/endpoint/endpoint_test.go new file mode 100644 index 00000000..bdcc5669 --- /dev/null +++ b/service/endpoint/endpoint_test.go @@ -0,0 +1,19 @@ +package endpoint_test + +import ( + "testing" + + "github.com/fubarhouse/pygmy-go/service/endpoint" + . "github.com/smartystreets/goconvey/convey" +) + +func Example() { + endpoint.Validate("http://127.0.0.1:8080") +} + +func Test(t *testing.T) { + Convey("URL Endpoint tests...", t, func() { + valid := endpoint.Validate("https://www.golang.org/") + So(valid, ShouldBeTrue) + }) +} diff --git a/service/haproxy/haproxy_test.go b/service/haproxy/haproxy_test.go new file mode 100644 index 00000000..80e9e9a8 --- /dev/null +++ b/service/haproxy/haproxy_test.go @@ -0,0 +1,35 @@ +package haproxy_test + +import ( + "fmt" + "testing" + + "github.com/docker/go-connections/nat" + "github.com/fubarhouse/pygmy-go/service/haproxy" + . "github.com/smartystreets/goconvey/convey" +) + +func Example() { + haproxy.New() + haproxy.NewDefaultPorts() +} + +func Test(t *testing.T) { + Convey("HAProxy: Field equality tests...", t, func() { + obj := haproxy.New() + objPorts := haproxy.NewDefaultPorts() + So(obj.Config.Image, ShouldEqual, "amazeeio/haproxy") + So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-haproxy") + So(obj.Config.Labels["pygmy.network"], ShouldEqual, "amazeeio-network") + So(obj.Config.Labels["pygmy.url"], ShouldEqual, "http://docker.amazee.io/stats") + So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "14") + So(obj.HostConfig.AutoRemove, ShouldBeFalse) + So(fmt.Sprint(obj.HostConfig.Binds), ShouldEqual, fmt.Sprint([]string{"/var/run/docker.sock:/tmp/docker.sock"})) + So(obj.HostConfig.PortBindings, ShouldEqual, nil) + So(obj.HostConfig.RestartPolicy.Name, ShouldEqual, "on-failure") + So(obj.HostConfig.RestartPolicy.MaximumRetryCount, ShouldEqual, 0) + So(fmt.Sprint(objPorts.HostConfig.PortBindings), ShouldEqual, fmt.Sprint(nat.PortMap{"80/tcp": []nat.PortBinding{{HostIP: "", HostPort: "80"}}})) + }) +} diff --git a/service/interface/docker/docker.go b/service/interface/docker/docker.go new file mode 100644 index 00000000..d23b2330 --- /dev/null +++ b/service/interface/docker/docker.go @@ -0,0 +1,471 @@ +package docker + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "regexp" + "strings" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + volumetypes "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/client" + "github.com/fubarhouse/pygmy-go/service/endpoint" +) + +// DockerContainerList will return a slice of containers +func DockerContainerList() ([]types.Container, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + fmt.Println(err) + } + + containers, err := cli.ContainerList(ctx, types.ContainerListOptions{ + All: true, + }) + if err != nil { + return []types.Container{}, err + } + + return containers, nil + +} + +// DockerImageList will return a slice of Docker images. +func DockerImageList() ([]types.ImageSummary, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + fmt.Println(err) + } + + images, err := cli.ImageList(ctx, types.ImageListOptions{}) + if err != nil { + return []types.ImageSummary{}, err + } + + return images, nil + +} + +// DockerPull will pull a Docker image into the daemon. +func DockerPull(image string) (string, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + fmt.Println(err) + } + + { + + // To support image references from external sources to docker.io we need to check + // and validate the image reference for all known cases of validity. + + if m, _ := regexp.MatchString("^([a-zA-Z0-9]+[/][a-zA-Z0-9:-_]+[a-zA-Z0-9:-_.]+)$", image); m { + // URL was not provided (in full), but the tag was provided. + // For this, we prepend 'docker.io/' to the reference. + // Examples: + // - amazeeio/pygmy:latest + image = fmt.Sprintf("docker.io/%v", image) + } else if m, _ := regexp.MatchString("^([a-zA-Z0-9]+[/][a-zA-Z0-9:-]+[a-zA-Z0-9:-_.]+)$", image); m { + // URL was not provided (in full), but the tag was not provided. + // For this, we prepend 'docker.io/' to the reference. + // Examples: + // - amazeeio/pygmy + image = fmt.Sprintf("docker.io/%v", image) + } else if m, _ := regexp.MatchString("^([a-zA-Z0-9.].+[a-zA-Z0-9]+[/][a-zA-Z0-9:-_]+[a-zA-Z0-9:-_.]+)$", image); m { + // URL was provided (in full), but the tag was provided. + // For this, we do not alter the value provided. + // Examples: + // - quay.io/amazeeio/pygmy:latest + } else if m, _ := regexp.MatchString("^([a-zA-Z0-9.].+[a-zA-Z0-9]+[/][a-zA-Z0-9:-_]+)$", image); m { + // URL was provided (in full), but the tag was not provided. + // For this, we do not alter the value provided. + // Examples: + // - quay.io/amazeeio/pygmy + } else if m, _ := regexp.MatchString("^([a-zA-Z0-9]+[:][a-zA-Z0-9.-_]+)$", image); m { + // Library image was provided with tag identifier. + // For this, we prepend 'docker.io/' to the reference. + // Examples: + // - pygmy:latest + image = fmt.Sprintf("docker.io/%v", image) + } else if m, _ := regexp.MatchString("^([a-zA-Z0-9-_]+)$", image); m { + // Library image was provided without tag identifier. + // For this, we prepend 'docker.io/' to the reference. + // Examples: + // - pygmy + image = fmt.Sprintf("docker.io/%v", image) + } else { + // Validation not successful + return "", fmt.Errorf("error: regexp validation for %v failed", image) + } + } + + // DockerHub Registry causes a stack trace fatal error when unavailable. + // We can check for this and report back, handling it gracefully and + // tell the user the service is down momentarily, and to try again shortly. + if strings.HasPrefix(image, "docker.io") { + if s := endpoint.Validate("https://registry-1.docker.io"); !s { + return "", fmt.Errorf("cannot reach the Docker Hub Registry, please try again in a few minutes.") + } + } + + data, err := cli.ImagePull(ctx, image, types.ImagePullOptions{}) + if err != nil { + fmt.Println(err) + } + + d := json.NewDecoder(data) + + type Event struct { + Status string `json:"status"` + Error string `json:"error"` + Progress string `json:"progress"` + ProgressDetail struct { + Current int `json:"current"` + Total int `json:"total"` + } `json:"progressDetail"` + } + + var event *Event + for { + if err := d.Decode(&event); err != nil { + if err == io.EOF { + break + } + + panic(err) + } + } + + if event != nil { + if strings.Contains(event.Status, fmt.Sprint("Downloaded newer image")) { + return fmt.Sprintf("Successfully pulled %v", image), nil + } + + if strings.Contains(event.Status, fmt.Sprint("Image is up to date")) { + return fmt.Sprintf("Image %v is up to date", image), nil + } + } + return event.Status, nil +} + +// DockerStop will stop the container. +func DockerStop(name string) error { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return err + } + timeout := time.Duration(10) + err = cli.ContainerStop(ctx, name, &timeout) + if err != nil { + return err + } + return nil +} + +// DockerKill will kill the container. +func DockerKill(name string) error { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return err + } + err = cli.ContainerKill(ctx, name, "") + if err != nil { + return err + } + return nil +} + +// DockerRemove will remove the container. +// It will not remove the image. +func DockerRemove(id string) error { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return err + } + err = cli.ContainerRemove(ctx, id, types.ContainerRemoveOptions{}) + if err != nil { + return err + } + return nil +} + +// DockerNetworkCreate is an abstraction layer on top of the Docker API call +// which will create a Docker network using a specified configuration. +func DockerNetworkCreate(network *types.NetworkResource) error { + netVal, _ := DockerNetworkStatus(network.Name) + if netVal { + return fmt.Errorf("docker network %v already exists", network.Name) + } + + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return err + } + + config := types.NetworkCreate{ + Driver: network.Driver, + EnableIPv6: network.EnableIPv6, + IPAM: &network.IPAM, + Internal: network.Internal, + Attachable: network.Attachable, + Options: network.Options, + Labels: network.Labels, + } + _, err = cli.NetworkCreate(ctx, network.Name, config) + if err != nil { + return err + } + + return nil +} + +// DockerNetworkRemove will attempt to remove a Docker network +// and will not apply force to removal. +func DockerNetworkRemove(network string) error { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return err + } + err = cli.NetworkRemove(ctx, network) + if err != nil { + return err + } + return nil +} + +// DockerNetworkStatus will identify if a network with a +// specified name is present been created and return a boolean. +func DockerNetworkStatus(network string) (bool, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return false, err + } + + networks, err := cli.NetworkList(ctx, types.NetworkListOptions{}) + if err != nil { + return false, err + } + + for _, n := range networks { + if n.Name == network { + return true, nil + } + } + + return false, nil +} + +// DockerNetworkGet will use the Docker API to retrieve a Docker network +// which has a given name. +func DockerNetworkGet(name string) (types.NetworkResource, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return types.NetworkResource{}, err + } + networks, err := cli.NetworkList(ctx, types.NetworkListOptions{}) + if err != nil { + return types.NetworkResource{}, err + } + for _, network := range networks { + if val, ok := network.Labels["pygmy.name"]; ok { + if val == name { + return network, nil + } + } + } + return types.NetworkResource{}, nil +} + +// DockerNetworkConnect will connect a container to a network. +func DockerNetworkConnect(network string, containerName string) error { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return err + } + e := cli.NetworkConnect(ctx, network, containerName, nil) + if e != nil { + return e + } + return nil +} + +// DockerNetworkConnect will check if a container is connected to a network. +func DockerNetworkConnected(network string, containerName string) (bool, error) { + // Reset network state: + c, _ := DockerContainerList() + for d := range c { + if c[d].Labels["pygmy.name"] == containerName { + for net := range c[d].NetworkSettings.Networks { + if net == network { + return true, nil + } + } + } + } + return false, fmt.Errorf("network was found without the container connected") +} + +// DockerVolumeExists will check if a Docker volume has been created. +func DockerVolumeExists(volume types.Volume) (bool, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return false, err + } + _, _, err = cli.VolumeInspectWithRaw(ctx, volume.Name) + if err != nil { + return false, err + } + + return true, nil +} + +// DockerVolumeGet will return the full contents of a types.Volume from the API. +func DockerVolumeGet(name string) (types.Volume, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + + if err != nil { + return types.Volume{ + Name: name, + }, err + } + + volumes, err := cli.VolumeList(ctx, filters.Args{}) + if err != nil { + return types.Volume{ + Name: name, + }, err + } + + for _, volume := range volumes.Volumes { + if volume.Name == name { + return *volume, nil + } + } + + return types.Volume{ + Name: name, + }, nil +} + +// DockerVolumeCreate will create a Docker Volume as configured. +func DockerVolumeCreate(volume types.Volume) (types.Volume, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return types.Volume{}, err + } + return cli.VolumeCreate(ctx, volumetypes.VolumeCreateBody{ + Driver: volume.Driver, + DriverOpts: volume.Options, + Labels: volume.Labels, + Name: volume.Name, + }) +} + +// DockerExec will run a command in a Docker container and return the output. +func DockerExec(container string, command string) ([]byte, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return []byte{}, err + } + + if rst, err := cli.ContainerExecCreate(ctx, container, types.ExecConfig{ + AttachStdout: true, + AttachStderr: true, + Cmd: strings.Split(command, " ")}); err != nil { + return []byte{}, err + } else { + if response, err := cli.ContainerExecAttach(context.Background(), rst.ID, types.ExecStartCheck{}); err != nil { + return []byte{}, err + } else { + data, _ := ioutil.ReadAll(response.Reader) + defer response.Close() + return data, nil + } + } +} + +func DockerContainerCreate(ID string, config container.Config, hostconfig container.HostConfig, networkconfig network.NetworkingConfig) (container.ContainerCreateCreatedBody, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return container.ContainerCreateCreatedBody{}, err + } + resp, err := cli.ContainerCreate(ctx, &config, &hostconfig, &networkconfig, ID) + if err != nil { + return container.ContainerCreateCreatedBody{}, err + } + return resp, err +} + +func DockerContainerStart(ID string, options types.ContainerStartOptions) error { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return err + } + if err := cli.ContainerStart(ctx, ID, types.ContainerStartOptions{}); err != nil { + return err + } + return err +} + +func DockerContainerLogs(ID string) ([]byte, error) { + ctx := context.Background() + cli, err := client.NewClientWithOpts() + cli.NegotiateAPIVersion(ctx) + if err != nil { + return []byte{}, err + } + b, _ := cli.ContainerLogs(ctx, ID, types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + }) + + buf := new(bytes.Buffer) + if _, f := buf.ReadFrom(b); f != nil { + fmt.Println(f) + } + + b.Close() + + return buf.Bytes(), nil +} diff --git a/service/interface/field.go b/service/interface/field.go new file mode 100644 index 00000000..cb3a6539 --- /dev/null +++ b/service/interface/field.go @@ -0,0 +1,100 @@ +package model + +import ( + "fmt" + "strconv" +) + +// SetField will set a pygmy label to be equal to the string equal of +// an interface{}, even if it already exists. It should not matter if +// this container is running or not. +func (Service *Service) SetField(name string, value interface{}) error { + if _, ok := Service.Config.Labels["pygmy."+fmt.Sprint(name)]; !ok { + // + } else { + old, _ := Service.GetFieldString(name) + Service.Config.Labels["pygmy."+name] = fmt.Sprint(value) + new, _ := Service.GetFieldString(name) + + if old == new { + return fmt.Errorf("tag was not set") + } + } + + return nil +} + +// GetFieldString will get and return a tag on the service using the pygmy +// convention ("pygmy.*") and return it as a string. +func (Service *Service) GetFieldString(field string) (string, error) { + + f := fmt.Sprintf("pygmy.%v", field) + + if container, running := Service.GetRunning(); running == nil { + if val, ok := container.Labels[f]; ok { + return val, nil + } + } + + if val, ok := Service.Config.Labels[f]; ok { + return val, nil + } + + return "", fmt.Errorf("could not find field 'pygmy.%v' on service using image %v?", field, Service.Config.Image) +} + +// GetFieldInt will get and return a tag on the service using the pygmy +// convention ("pygmy.*") and return it as an int. +func (Service *Service) GetFieldInt(field string) (int, error) { + + f := fmt.Sprintf("pygmy.%v", field) + + if container, running := Service.GetRunning(); running == nil { + if val, ok := container.Labels[f]; ok { + i, e := strconv.ParseInt(val, 10, 10) + if e != nil { + return 0, e + } + return int(i), nil + } + } + + if val, ok := Service.Config.Labels[f]; ok { + i, e := strconv.ParseInt(val, 10, 10) + if e != nil { + return 0, e + } + return int(i), nil + } + + return 0, fmt.Errorf("could not find field 'pygmy.%v' on service using image %v?", field, Service.Config.Image) +} + +// GetFieldBool will get and return a tag on the service using the pygmy +// convention ("pygmy.*") and return it as a bool. +func (Service *Service) GetFieldBool(field string) (bool, error) { + + f := fmt.Sprintf("pygmy.%v", field) + + if container, running := Service.GetRunning(); running == nil { + if Service.Config.Labels[f] == container.Labels[f] { + if val, ok := container.Labels[f]; ok { + if val == "true" { + return true, nil + } else if val == "false" { + return false, nil + } + } + } + } + + if val, ok := Service.Config.Labels[f]; ok { + if val == "true" || val == "1" { + return true, nil + } else if val == "false" || val == "0" { + return false, nil + } + } + + return false, fmt.Errorf("could not find field 'pygmy.%v' on service using image %v?", field, Service.Config.Image) +} diff --git a/service/interface/interface.go b/service/interface/interface.go index d0212873..192fbf54 100644 --- a/service/interface/interface.go +++ b/service/interface/interface.go @@ -1,61 +1,15 @@ package model import ( - "bytes" "context" - "encoding/json" "fmt" - "io" - "io/ioutil" - "regexp" - "strconv" "strings" - "time" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" - volumetypes "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" - "github.com/fubarhouse/pygmy-go/service/endpoint" + "github.com/fubarhouse/pygmy-go/service/interface/docker" ) -// DockerService is the requirements for a Docker container to be compatible. -// The Service struct is used to implement this interface, and individual -// variables of type Service can/have overwritten them when logic deems -// it necessary. -type DockerService interface { - Setup() error - Status() (bool, error) - Start() ([]byte, error) - Stop() error -} - -// Service is a collection of requirements for starting a container and -// provides a way for config of any container to be overridden and start -// fully compatible with Docker's API. -type Service struct { - Config container.Config - HostConfig container.HostConfig - NetworkConfig network.NetworkingConfig -} - -// Network is a struct containing the configuration of a single Docker network -// including some extra fields so that Pygmy knows how to interact with the -// desired outcome. -type Network struct { - // Name is the name of the network, it is independent of the map key which - // will be used to configure pygmy but this field should match the map key. - Name string `yaml:"name"` - // Containers is a []string which indicates the names of the containers - // that need to be connected to this network. - Containers []string `yaml:"containers"` - // Config is the actual Network configuration for the Docker Network. - // It is the Network creation configuration as provided by the Docker API. - Config types.NetworkCreate `yaml:"config"` -} - // Setup will detect if the Service's image reference exists and will // attempt to run `docker pull` on the non-canonical image if it is // not found in the daemon. @@ -64,14 +18,14 @@ func (Service *Service) Setup() error { return nil } - images, _ := DockerImageList() + images, _ := docker.DockerImageList() for _, image := range images { if strings.Contains(fmt.Sprint(image.RepoTags), Service.Config.Image) { return nil } } - msg, err := DockerPull(Service.Config.Image) + msg, err := docker.DockerPull(Service.Config.Image) if msg != "" { fmt.Println(msg) } @@ -112,21 +66,21 @@ func (Service *Service) Start() ([]byte, error) { } if purpose == "addkeys" || purpose == "showkeys" { - if e := DockerKill(name); e != nil { + if e := docker.DockerKill(name); e != nil { fmt.Sprintln(e) } - if e := DockerRemove(name); e != nil { + if e := docker.DockerRemove(name); e != nil { fmt.Sprintln(e) } } - output, err := DockerRun(Service) + output, err := Service.DockerRun() if err != nil { return []byte{}, err } - if c, err := GetRunning(Service); c.ID != "" { + if c, err := Service.GetRunning(); c.ID != "" { if !Service.HostConfig.AutoRemove && !discrete { fmt.Printf("Successfully started %v\n", name) } else if Service.HostConfig.AutoRemove && err != nil { @@ -149,18 +103,10 @@ func (Service *Service) Status() (bool, error) { if Service.HostConfig.AutoRemove { return true, nil } - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - fmt.Println(err) - } - containers, _ := cli.ContainerList(ctx, types.ContainerListOptions{ - Quiet: true, - }) + containers, _ := docker.DockerContainerList() for _, container := range containers { for _, n := range container.Names { - if strings.Contains(n, name) { + if strings.Contains(n, name) && strings.HasPrefix(container.Status, "Up") { return true, nil } } @@ -170,113 +116,10 @@ func (Service *Service) Status() (bool, error) { } -// SetField will set a pygmy label to be equal to the string equal of -// an interface{}, even if it already exists. It should not matter if -// this container is running or not. -func (Service *Service) SetField(name string, value interface{}) error { - if _, ok := Service.Config.Labels["pygmy."+fmt.Sprint(name)]; !ok { - // - } else { - old, _ := Service.GetFieldString(name) - Service.Config.Labels["pygmy."+name] = fmt.Sprint(value) - new, _ := Service.GetFieldString(name) - - if old == new { - return fmt.Errorf("tag was not set") - } - } - - return nil -} - -// GetFieldString will get and return a tag on the service using the pygmy -// convention ("pygmy.*") and return it as a string. -func (Service *Service) GetFieldString(field string) (string, error) { - - f := fmt.Sprintf("pygmy.%v", field) - - if container, running := GetRunning(Service); running == nil { - if val, ok := container.Labels[f]; ok { - return val, nil - } - } - - if val, ok := Service.Config.Labels[f]; ok { - return val, nil - } - - return "", fmt.Errorf("could not find field 'pygmy.%v' on service using image %v?", field, Service.Config.Image) -} - -// GetFieldInt will get and return a tag on the service using the pygmy -// convention ("pygmy.*") and return it as an int. -func (Service *Service) GetFieldInt(field string) (int, error) { - - f := fmt.Sprintf("pygmy.%v", field) - - if container, running := GetRunning(Service); running == nil { - if val, ok := container.Labels[f]; ok { - i, e := strconv.ParseInt(val, 10, 10) - if e != nil { - return 0, e - } - return int(i), nil - } - } - - if val, ok := Service.Config.Labels[f]; ok { - i, e := strconv.ParseInt(val, 10, 10) - if e != nil { - return 0, e - } - return int(i), nil - } - - return 0, fmt.Errorf("could not find field 'pygmy.%v' on service using image %v?", field, Service.Config.Image) -} - -// GetFieldBool will get and return a tag on the service using the pygmy -// convention ("pygmy.*") and return it as a bool. -func (Service *Service) GetFieldBool(field string) (bool, error) { - - f := fmt.Sprintf("pygmy.%v", field) - - if container, running := GetRunning(Service); running == nil { - if Service.Config.Labels[f] == container.Labels[f] { - if val, ok := container.Labels[f]; ok { - if val == "true" { - return true, nil - } else if val == "false" { - return false, nil - } - } - } - } - - if val, ok := Service.Config.Labels[f]; ok { - if val == "true" || val == "1" { - return true, nil - } else if val == "false" || val == "0" { - return false, nil - } - } - - return false, fmt.Errorf("could not find field 'pygmy.%v' on service using image %v?", field, Service.Config.Image) -} - // GetRunning will get a types.Container variable for a given running container // and it will not retrieve any information on containers that are not running. -func GetRunning(Service *Service) (types.Container, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - fmt.Println(err) - } - containers, _ := cli.ContainerList(ctx, types.ContainerListOptions{ - Quiet: true, - }) - +func (Service *Service) GetRunning() (types.Container, error) { + containers, _ := docker.DockerContainerList() for _, container := range containers { if _, ok := container.Labels["pygmy.name"]; ok { if strings.Contains(container.Names[0], Service.Config.Labels["pygmy.name"]) { @@ -296,22 +139,22 @@ func (Service *Service) Clean() error { return nil } - Containers, _ := DockerContainerList() + Containers, _ := docker.DockerContainerList() for _, container := range Containers { if container.Names[0] == name { if pygmy { name := strings.TrimLeft(container.Names[0], "/") - if e := DockerKill(container.ID); e == nil { + if e := docker.DockerKill(container.ID); e == nil { if !Service.HostConfig.AutoRemove { fmt.Printf("Successfully killed %v\n", name) } } - if e := DockerStop(container.ID); e == nil { + if e := docker.DockerStop(container.ID); e == nil { if !Service.HostConfig.AutoRemove { fmt.Printf("Successfully stopped %v\n", name) } } - if e := DockerRemove(container.ID); e != nil { + if e := docker.DockerRemove(container.ID); e != nil { if !Service.HostConfig.AutoRemove { fmt.Printf("Successfully removed %v\n", name) } @@ -332,7 +175,7 @@ func (Service *Service) Stop() error { return nil } - container, err := GetRunning(Service) + container, err := Service.GetRunning() if err != nil { if !discrete { fmt.Printf("Not running %v\n", name) @@ -341,8 +184,8 @@ func (Service *Service) Stop() error { } for _, name := range container.Names { - if e := DockerStop(container.ID); e == nil { - if e := DockerRemove(container.ID); e == nil { + if e := docker.DockerStop(container.ID); e == nil { + if e := docker.DockerRemove(container.ID); e == nil { if !discrete { containerName := strings.Trim(name, "/") fmt.Printf("Successfully removed %v\n", containerName) @@ -357,149 +200,8 @@ func (Service *Service) Stop() error { // _ will ensure DockerService is implemented by Service. var _ DockerService = (*Service)(nil) -// DockerContainerList will return a slice of containers -func DockerContainerList() ([]types.Container, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - fmt.Println(err) - } - - containers, err := cli.ContainerList(ctx, types.ContainerListOptions{ - All: true, - }) - if err != nil { - return []types.Container{}, err - } - - return containers, nil - -} - -// DockerImageList will return a slice of Docker images. -func DockerImageList() ([]types.ImageSummary, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - fmt.Println(err) - } - - images, err := cli.ImageList(ctx, types.ImageListOptions{}) - if err != nil { - return []types.ImageSummary{}, err - } - - return images, nil - -} - -// DockerPull will pull a Docker image into the daemon. -func DockerPull(image string) (string, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - fmt.Println(err) - } - - { - - // To support image references from external sources to docker.io we need to check - // and validate the image reference for all known cases of validity. - - if m, _ := regexp.MatchString("^([a-zA-Z0-9]+[/][a-zA-Z0-9:-_]+[a-zA-Z0-9:-_.]+)$", image); m { - // URL was not provided (in full), but the tag was provided. - // For this, we prepend 'docker.io/' to the reference. - // Examples: - // - amazeeio/pygmy:latest - image = fmt.Sprintf("docker.io/%v", image) - } else if m, _ := regexp.MatchString("^([a-zA-Z0-9]+[/][a-zA-Z0-9:-]+[a-zA-Z0-9:-_.]+)$", image); m { - // URL was not provided (in full), but the tag was not provided. - // For this, we prepend 'docker.io/' to the reference. - // Examples: - // - amazeeio/pygmy - image = fmt.Sprintf("docker.io/%v", image) - } else if m, _ := regexp.MatchString("^([a-zA-Z0-9.].+[a-zA-Z0-9]+[/][a-zA-Z0-9:-_]+[a-zA-Z0-9:-_.]+)$", image); m { - // URL was provided (in full), but the tag was provided. - // For this, we do not alter the value provided. - // Examples: - // - quay.io/amazeeio/pygmy:latest - } else if m, _ := regexp.MatchString("^([a-zA-Z0-9.].+[a-zA-Z0-9]+[/][a-zA-Z0-9:-_]+)$", image); m { - // URL was provided (in full), but the tag was not provided. - // For this, we do not alter the value provided. - // Examples: - // - quay.io/amazeeio/pygmy - } else if m, _ := regexp.MatchString("^([a-zA-Z0-9]+[:][a-zA-Z0-9.-_]+)$", image); m { - // Library image was provided with tag identifier. - // For this, we prepend 'docker.io/' to the reference. - // Examples: - // - pygmy:latest - image = fmt.Sprintf("docker.io/%v", image) - } else if m, _ := regexp.MatchString("^([a-zA-Z0-9-_]+)$", image); m { - // Library image was provided without tag identifier. - // For this, we prepend 'docker.io/' to the reference. - // Examples: - // - pygmy - image = fmt.Sprintf("docker.io/%v", image) - } else { - // Validation not successful - return "", fmt.Errorf("error: regexp validation for %v failed", image) - } - } - - // DockerHub Registry causes a stack trace fatal error when unavailable. - // We can check for this and report back, handling it gracefully and - // tell the user the service is down momentarily, and to try again shortly. - if strings.HasPrefix(image, "docker.io") { - if s := endpoint.Validate("https://registry-1.docker.io"); !s { - return "", fmt.Errorf("cannot reach the Docker Hub Registry, please try again in a few minutes.") - } - } - - data, err := cli.ImagePull(ctx, image, types.ImagePullOptions{}) - if err != nil { - fmt.Println(err) - } - - d := json.NewDecoder(data) - - type Event struct { - Status string `json:"status"` - Error string `json:"error"` - Progress string `json:"progress"` - ProgressDetail struct { - Current int `json:"current"` - Total int `json:"total"` - } `json:"progressDetail"` - } - - var event *Event - for { - if err := d.Decode(&event); err != nil { - if err == io.EOF { - break - } - - panic(err) - } - } - - if event != nil { - if strings.Contains(event.Status, fmt.Sprint("Downloaded newer image")) { - return fmt.Sprintf("Successfully pulled %v", image), nil - } - - if strings.Contains(event.Status, fmt.Sprint("Image is up to date")) { - return fmt.Sprintf("Image %v is up to date", image), nil - } - } - return event.Status, nil -} - // DockerRun will setup and run a given container. -func DockerRun(Service *Service) ([]byte, error) { +func (Service *Service) DockerRun() ([]byte, error) { ctx := context.Background() cli, err := client.NewClientWithOpts() @@ -509,7 +211,7 @@ func DockerRun(Service *Service) ([]byte, error) { } // Ensure we have the image available: - images, _ := DockerImageList() + images, _ := docker.DockerImageList() // Specify a false boolean which we can switch to true if the image is in the registry: imageFound := false @@ -535,7 +237,7 @@ func DockerRun(Service *Service) ([]byte, error) { } // Sanity check to ensure we don't get name conflicts. - c, _ := DockerContainerList() + c, _ := docker.DockerContainerList() for _, cn := range c { if strings.HasSuffix(cn.Names[0], Service.Config.Labels["pygmy.name"]) { return []byte{}, nil @@ -548,286 +250,15 @@ func DockerRun(Service *Service) ([]byte, error) { return []byte{}, fmt.Errorf("container config is missing label for name") } - resp, err := cli.ContainerCreate(ctx, &Service.Config, &Service.HostConfig, &Service.NetworkConfig, name) + resp, err := docker.DockerContainerCreate(name, Service.Config, Service.HostConfig, Service.NetworkConfig) if err != nil { return []byte{}, err } - if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { + if err := docker.DockerContainerStart(name, types.ContainerStartOptions{}); err != nil { return []byte{}, err } - b, _ := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - }) - - buf := new(bytes.Buffer) - if _, f := buf.ReadFrom(b); f != nil { - fmt.Println(f) - } - - b.Close() - - return buf.Bytes(), nil -} - -// DockerStop will stop the container. -func DockerStop(name string) error { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return err - } - timeout := time.Duration(10) - err = cli.ContainerStop(ctx, name, &timeout) - if err != nil { - return err - } - return nil -} - -// DockerKill will kill the container. -func DockerKill(name string) error { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return err - } - err = cli.ContainerKill(ctx, name, "") - if err != nil { - return err - } - return nil -} - -// DockerRemove will remove the container. -// It will not remove the image. -func DockerRemove(id string) error { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return err - } - err = cli.ContainerRemove(ctx, id, types.ContainerRemoveOptions{}) - if err != nil { - return err - } - return nil -} - -// DockerNetworkCreate is an abstraction layer on top of the Docker API call -// which will create a Docker network using a specified configuration. -func DockerNetworkCreate(network *types.NetworkResource) error { - netVal, _ := DockerNetworkStatus(network.Name) - if netVal { - return fmt.Errorf("docker network %v already exists", network.Name) - } - - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return err - } - - config := types.NetworkCreate{ - Driver: network.Driver, - EnableIPv6: network.EnableIPv6, - IPAM: &network.IPAM, - Internal: network.Internal, - Attachable: network.Attachable, - Options: network.Options, - Labels: network.Labels, - } - _, err = cli.NetworkCreate(ctx, network.Name, config) - if err != nil { - return err - } - - return nil -} - -// DockerNetworkRemove will attempt to remove a Docker network -// and will not apply force to removal. -func DockerNetworkRemove(network string) error { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return err - } - err = cli.NetworkRemove(ctx, network) - if err != nil { - return err - } - return nil -} - -// DockerNetworkStatus will identify if a network with a -// specified name is present been created and return a boolean. -func DockerNetworkStatus(network string) (bool, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return false, err - } - - networks, err := cli.NetworkList(ctx, types.NetworkListOptions{}) - if err != nil { - return false, err - } - - for _, n := range networks { - if n.Name == network { - return true, nil - } - } - - return false, nil -} - -// DockerNetworkGet will use the Docker API to retrieve a Docker network -// which has a given name. -func DockerNetworkGet(name string) (types.NetworkResource, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return types.NetworkResource{}, err - } - networks, err := cli.NetworkList(ctx, types.NetworkListOptions{}) - if err != nil { - return types.NetworkResource{}, err - } - for _, network := range networks { - if val, ok := network.Labels["pygmy.name"]; ok { - if val == name { - return network, nil - } - } - } - return types.NetworkResource{}, nil -} - -// DockerNetworkConnect will connect a container to a network. -func DockerNetworkConnect(network string, containerName string) error { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return err - } - e := cli.NetworkConnect(ctx, network, containerName, nil) - if e != nil { - return e - } - return nil -} - -// DockerNetworkConnect will check if a container is connected to a network. -func DockerNetworkConnected(network string, containerName string) (bool, error) { - // Reset network state: - c, _ := DockerContainerList() - for d := range c { - if c[d].Labels["pygmy.name"] == containerName { - for net := range c[d].NetworkSettings.Networks { - if net == network { - return true, nil - } - } - } - } - return false, fmt.Errorf("network was found without the container connected") -} - -// DockerVolumeExists will check if a Docker volume has been created. -func DockerVolumeExists(volume types.Volume) (bool, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return false, err - } - _, _, err = cli.VolumeInspectWithRaw(ctx, volume.Name) - if err != nil { - return false, err - } - - return true, nil -} + return docker.DockerContainerLogs(resp.ID) -// DockerVolumeGet will return the full contents of a types.Volume from the API. -func DockerVolumeGet(name string) (types.Volume, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - - if err != nil { - return types.Volume{ - Name: name, - }, err - } - - volumes, err := cli.VolumeList(ctx, filters.Args{}) - if err != nil { - return types.Volume{ - Name: name, - }, err - } - - for _, volume := range volumes.Volumes { - if volume.Name == name { - return *volume, nil - } - } - - return types.Volume{ - Name: name, - }, nil -} - -// DockerVolumeCreate will create a Docker Volume as configured. -func DockerVolumeCreate(volume types.Volume) (types.Volume, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return types.Volume{}, err - } - return cli.VolumeCreate(ctx, volumetypes.VolumeCreateBody{ - Driver: volume.Driver, - DriverOpts: volume.Options, - Labels: volume.Labels, - Name: volume.Name, - }) -} - -// DockerExec will run a command in a Docker container and return the output. -func DockerExec(container string, command string) ([]byte, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts() - cli.NegotiateAPIVersion(ctx) - if err != nil { - return []byte{}, err - } - - if rst, err := cli.ContainerExecCreate(ctx, container, types.ExecConfig{ - AttachStdout: true, - AttachStderr: true, - Cmd: strings.Split(command, " ")}); err != nil { - return []byte{}, err - } else { - if response, err := cli.ContainerExecAttach(context.Background(), rst.ID, types.ExecStartCheck{}); err != nil { - return []byte{}, err - } else { - data, _ := ioutil.ReadAll(response.Reader) - defer response.Close() - return data, nil - } - } } diff --git a/service/interface/types.go b/service/interface/types.go new file mode 100644 index 00000000..cc34c603 --- /dev/null +++ b/service/interface/types.go @@ -0,0 +1,42 @@ +package model + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" +) + +// DockerService is the requirements for a Docker container to be compatible. +// The Service struct is used to implement this interface, and individual +// variables of type Service can/have overwritten them when logic deems +// it necessary. +type DockerService interface { + Setup() error + Status() (bool, error) + Start() ([]byte, error) + Stop() error +} + +// Service is a collection of requirements for starting a container and +// provides a way for config of any container to be overridden and start +// fully compatible with Docker's API. +type Service struct { + Config container.Config + HostConfig container.HostConfig + NetworkConfig network.NetworkingConfig +} + +// Network is a struct containing the configuration of a single Docker network +// including some extra fields so that Pygmy knows how to interact with the +// desired outcome. +type Network struct { + // Name is the name of the network, it is independent of the map key which + // will be used to configure pygmy but this field should match the map key. + Name string `yaml:"name"` + // Containers is a []string which indicates the names of the containers + // that need to be connected to this network. + Containers []string `yaml:"containers"` + // Config is the actual Network configuration for the Docker Network. + // It is the Network creation configuration as provided by the Docker API. + Config types.NetworkCreate `yaml:"config"` +} diff --git a/service/library/clean.go b/service/library/clean.go index 82c6d357..fc16005e 100644 --- a/service/library/clean.go +++ b/service/library/clean.go @@ -3,14 +3,14 @@ package library import ( "fmt" - model "github.com/fubarhouse/pygmy-go/service/interface" + "github.com/fubarhouse/pygmy-go/service/interface/docker" ) // Clean will forcibly kill and remove all of pygmy's containers in the daemon func Clean(c Config) { Setup(&c) - Containers, _ := model.DockerContainerList() + Containers, _ := docker.DockerContainerList() NetworksToClean := []string{} for _, Container := range Containers { @@ -26,12 +26,12 @@ func Clean(c Config) { } if target { - err := model.DockerKill(Container.ID) + err := docker.DockerKill(Container.ID) if err == nil { fmt.Printf("Successfully killed %v.\n", Container.Names[0]) } - err = model.DockerRemove(Container.ID) + err = docker.DockerRemove(Container.ID) if err == nil { fmt.Printf("Successfully removed %v.\n", Container.Names[0]) } @@ -43,12 +43,12 @@ func Clean(c Config) { } for n := range unique(NetworksToClean) { - if s, _ := model.DockerNetworkStatus(NetworksToClean[n]); s { - e := model.DockerNetworkRemove(NetworksToClean[n]) + if s, _ := docker.DockerNetworkStatus(NetworksToClean[n]); s { + e := docker.DockerNetworkRemove(NetworksToClean[n]) if e != nil { fmt.Println(e) } - if s, _ := model.DockerNetworkStatus(NetworksToClean[n]); !s { + if s, _ := docker.DockerNetworkStatus(NetworksToClean[n]); !s { fmt.Printf("Successfully removed network %v\n", NetworksToClean[n]) } else { fmt.Printf("Network %v was not removed\n", NetworksToClean[n]) diff --git a/service/library/down.go b/service/library/down.go index d7a73213..94b1d36a 100644 --- a/service/library/down.go +++ b/service/library/down.go @@ -3,7 +3,7 @@ package library import ( "fmt" - model "github.com/fubarhouse/pygmy-go/service/interface" + "github.com/fubarhouse/pygmy-go/service/interface/docker" ) // Down will bring pygmy down safely @@ -30,11 +30,11 @@ func Down(c Config) { } for _, network := range unique(NetworksToClean) { - e := model.DockerNetworkRemove(network) + e := docker.DockerNetworkRemove(network) if e != nil { fmt.Println(e) } - if s, _ := model.DockerNetworkStatus(network); !s { + if s, _ := docker.DockerNetworkStatus(network); !s { fmt.Printf("Successfully removed network %v\n", network) } else { fmt.Printf("Network %v was not removed", network) diff --git a/service/library/library.go b/service/library/library.go index dd3979b5..6918c4d6 100644 --- a/service/library/library.go +++ b/service/library/library.go @@ -3,8 +3,8 @@ package library import ( "fmt" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types" model "github.com/fubarhouse/pygmy-go/service/interface" "github.com/fubarhouse/pygmy-go/service/resolv" "github.com/imdario/mergo" diff --git a/service/library/network.go b/service/library/network.go index bf489ae6..65d20ede 100644 --- a/service/library/network.go +++ b/service/library/network.go @@ -6,20 +6,20 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/client" - "github.com/fubarhouse/pygmy-go/service/interface" + "github.com/fubarhouse/pygmy-go/service/interface/docker" ) // NetworkCreate is part of a centralised abstraction of the Docker API // and will create a Docker network with a specified configuration. func NetworkCreate(network types.NetworkResource) error { - return model.DockerNetworkCreate(&network) + return docker.DockerNetworkCreate(&network) } // NetworkConnect is part of a centralised abstraction of the Docker API // and will connect a created container to a docker network with a // specified name. func NetworkConnect(network string, containerName string) error { - return model.DockerNetworkConnect(network, containerName) + return docker.DockerNetworkConnect(network, containerName) } // NetworkStatus will check the state of a Docker network to test if it has diff --git a/service/library/setup.go b/service/library/setup.go index f1ca5a6c..1e9056d0 100644 --- a/service/library/setup.go +++ b/service/library/setup.go @@ -11,6 +11,7 @@ import ( "github.com/fubarhouse/pygmy-go/service/dnsmasq" "github.com/fubarhouse/pygmy-go/service/haproxy" model "github.com/fubarhouse/pygmy-go/service/interface" + "github.com/fubarhouse/pygmy-go/service/interface/docker" "github.com/fubarhouse/pygmy-go/service/mailhog" "github.com/fubarhouse/pygmy-go/service/network" "github.com/fubarhouse/pygmy-go/service/resolv" @@ -155,7 +156,7 @@ func Setup(c *Config) { for _, v := range c.Volumes { // Get the potentially existing volume: - c.Volumes[v.Name], _ = model.DockerVolumeGet(v.Name) + c.Volumes[v.Name], _ = docker.DockerVolumeGet(v.Name) // Merge the volume with the provided configuration: c.Volumes[v.Name] = getVolume(c.Volumes[v.Name], c.Volumes[v.Name]) } diff --git a/service/library/status.go b/service/library/status.go index 36087ced..f787fbb8 100644 --- a/service/library/status.go +++ b/service/library/status.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/fubarhouse/pygmy-go/service/endpoint" - model "github.com/fubarhouse/pygmy-go/service/interface" + "github.com/fubarhouse/pygmy-go/service/interface/docker" "github.com/fubarhouse/pygmy-go/service/resolv" ) @@ -23,7 +23,7 @@ func Status(c Config) { fmt.Println() } - Containers, _ := model.DockerContainerList() + Containers, _ := docker.DockerContainerList() for _, Container := range Containers { if Container.Labels["pygmy.enable"] == "true" || Container.Labels["pygmy.enable"] == "1" { Service := c.Services[strings.Trim(Container.Names[0], "/")] @@ -60,7 +60,7 @@ func Status(c Config) { for _, Network := range c.Networks { for _, Container := range Network.Containers { - if x, _ := model.DockerNetworkConnected(Network.Name, Container.Name); !x { + if x, _ := docker.DockerNetworkConnected(Network.Name, Container.Name); !x { fmt.Printf("[ ] %v is not connected to network %v\n", Container.Name, Network.Name) } else { fmt.Printf("[*] %v is connected to network %v\n", Container.Name, Network.Name) @@ -78,7 +78,7 @@ func Status(c Config) { } for _, volume := range c.Volumes { - if s, _ := model.DockerVolumeExists(volume); s { + if s, _ := docker.DockerVolumeExists(volume); s { fmt.Printf("[*] Volume %v has been created\n", volume.Name) } else { fmt.Printf("[ ] Volume %v has not been created\n", volume.Name) diff --git a/service/library/up.go b/service/library/up.go index 844d40d8..4046565c 100644 --- a/service/library/up.go +++ b/service/library/up.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/fubarhouse/pygmy-go/service/endpoint" - model "github.com/fubarhouse/pygmy-go/service/interface" + "github.com/fubarhouse/pygmy-go/service/interface/docker" ) // Up will bring Pygmy up. @@ -29,8 +29,8 @@ func Up(c Config) { } for _, volume := range c.Volumes { - if s, _ := model.DockerVolumeExists(volume); !s { - _, err := model.DockerVolumeCreate(volume) + if s, _ := docker.DockerVolumeExists(volume); !s { + _, err := docker.DockerVolumeCreate(volume) if err == nil { fmt.Printf("Created volume %v\n", volume.Name) } else { @@ -56,7 +56,7 @@ func Up(c Config) { // Here we will immitate the docker command by // pulling the image if it's not in the daemon. - images, _ := model.DockerImageList() + images, _ := docker.DockerImageList() imageFound := false for _, image := range images { for _, digest := range image.RepoDigests { @@ -71,7 +71,7 @@ func Up(c Config) { // When running 'docker run', it will pull the image. // For UX it makes sense we do this here. if !imageFound { - if _, err := model.DockerPull(service.Config.Image); err != nil { + if _, err := docker.DockerPull(service.Config.Image); err != nil { continue } } @@ -91,7 +91,7 @@ func Up(c Config) { // Docker network(s) creation for _, Network := range c.Networks { if Network.Name != "" { - netVal, _ := model.DockerNetworkStatus(Network.Name) + netVal, _ := docker.DockerNetworkStatus(Network.Name) if !netVal { if err := NetworkCreate(Network); err == nil { fmt.Printf("Successfully created network %v\n", Network.Name) @@ -108,7 +108,7 @@ func Up(c Config) { name, nameErr := service.GetFieldString("name") // If the network is configured at the container level, connect it. if Network, _ := service.GetFieldString("network"); Network != "" && nameErr == nil { - if s, _ := model.DockerNetworkConnected(Network, name); !s { + if s, _ := docker.DockerNetworkConnected(Network, name); !s { if s := NetworkConnect(Network, name); s == nil { fmt.Printf("Successfully connected %v to %v\n", name, Network) } else { diff --git a/service/library/update.go b/service/library/update.go index 79c204f7..af3ddd4f 100644 --- a/service/library/update.go +++ b/service/library/update.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - model "github.com/fubarhouse/pygmy-go/service/interface" + "github.com/fubarhouse/pygmy-go/service/interface/docker" ) // Update will update the the images for all configured services. @@ -22,7 +22,7 @@ func Update(c Config) { var result string var err error if purpose == "" || purpose == "sshagent" { - result, err = model.DockerPull(service.Config.Image) + result, err = docker.DockerPull(service.Config.Image) if err == nil { fmt.Println(result) } diff --git a/service/mailhog/mailhog_test.go b/service/mailhog/mailhog_test.go new file mode 100644 index 00000000..cd7d1734 --- /dev/null +++ b/service/mailhog/mailhog_test.go @@ -0,0 +1,37 @@ +package mailhog_test + +import ( + "fmt" + "testing" + + "github.com/docker/go-connections/nat" + "github.com/fubarhouse/pygmy-go/service/mailhog" + . "github.com/smartystreets/goconvey/convey" +) + +func Example() { + mailhog.New() + mailhog.NewDefaultPorts() +} + +func Test(t *testing.T) { + Convey("MailHog: Field equality tests...", t, func() { + obj := mailhog.New() + objPorts := mailhog.NewDefaultPorts() + So(obj.Config.User, ShouldEqual, "0") + So(obj.Config.Image, ShouldEqual, "mailhog/mailhog") + So(fmt.Sprint(obj.Config.ExposedPorts), ShouldEqual, fmt.Sprint(nat.PortSet{"80/tcp": struct{}{}, "1025/tcp": struct{}{}, "8025/tcp": struct{}{}})) + So(fmt.Sprint(obj.Config.Env), ShouldEqual, fmt.Sprint([]string{"MH_UI_BIND_ADDR=0.0.0.0:80", "MH_API_BIND_ADDR=0.0.0.0:80", "AMAZEEIO=AMAZEEIO", "AMAZEEIO_URL=mailhog.docker.amazee.io"})) + So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-mailhog") + So(obj.Config.Labels["pygmy.network"], ShouldEqual, "amazeeio-network") + So(obj.Config.Labels["pygmy.url"], ShouldEqual, "http://mailhog.docker.amazee.io") + So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "15") + So(obj.HostConfig.AutoRemove, ShouldBeFalse) + So(obj.HostConfig.PortBindings, ShouldEqual, nil) + So(obj.HostConfig.RestartPolicy.Name, ShouldEqual, "on-failure") + So(obj.HostConfig.RestartPolicy.MaximumRetryCount, ShouldEqual, 0) + So(fmt.Sprint(objPorts.HostConfig.PortBindings), ShouldEqual, fmt.Sprint(nat.PortMap{"1025/tcp": []nat.PortBinding{{HostIP: "", HostPort: "1025"}}})) + }) +} diff --git a/service/network/network_test.go b/service/network/network_test.go new file mode 100644 index 00000000..7bb52aff --- /dev/null +++ b/service/network/network_test.go @@ -0,0 +1,25 @@ +package network_test + +import ( + "fmt" + "github.com/docker/docker/api/types/network" + "testing" + + n "github.com/fubarhouse/pygmy-go/service/network" + . "github.com/smartystreets/goconvey/convey" +) + +func Example() { + n.New() +} + +func Test(t *testing.T) { + Convey("Network: Field equality tests...", t, func() { + obj := n.New() + So(obj.Name, ShouldEqual, "amazeeio-network") + So(obj.IPAM.Driver, ShouldEqual, "") + So(obj.IPAM.Options, ShouldEqual, nil) + So(fmt.Sprint(obj.IPAM.Config), ShouldEqual, fmt.Sprint([]network.IPAMConfig{{Subnet: "10.99.99.0/24", Gateway: "10.99.99.1"}})) + So(fmt.Sprint(obj.Labels), ShouldEqual, fmt.Sprint(map[string]string{"pygmy.name": "amazeeio-network"})) + }) +} diff --git a/service/ssh/agent/ssh_agent.go b/service/ssh/agent/ssh_agent.go index 93be2103..269233d7 100644 --- a/service/ssh/agent/ssh_agent.go +++ b/service/ssh/agent/ssh_agent.go @@ -18,6 +18,7 @@ func New() model.Service { "pygmy.defaults": "true", "pygmy.enable": "true", "pygmy.name": "amazeeio-ssh-agent", + "pygmy.network": "amazeeio-network", "pygmy.output": "false", "pygmy.purpose": "sshagent", "pygmy.weight": "30", diff --git a/service/ssh/agent/ssh_agent_test.go b/service/ssh/agent/ssh_agent_test.go new file mode 100644 index 00000000..7fc32190 --- /dev/null +++ b/service/ssh/agent/ssh_agent_test.go @@ -0,0 +1,40 @@ +package agent_test + +import ( + "testing" + + model "github.com/fubarhouse/pygmy-go/service/interface" + "github.com/fubarhouse/pygmy-go/service/ssh/agent" + . "github.com/smartystreets/goconvey/convey" +) + +func Example() { + agent.New() +} + +func ExampleList() { + agent.List(model.Service{}) +} + +func ExampleSearch() { + agent.Search(model.Service{}, "id_rsa.pub") +} + +func Test(t *testing.T) { + Convey("SSH Agent: Field equality tests...", t, func() { + obj := agent.New() + So(obj.Config.Image, ShouldEqual, "amazeeio/ssh-agent") + So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.output"], ShouldEqual, "false") + So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-ssh-agent") + So(obj.Config.Labels["pygmy.network"], ShouldEqual, "amazeeio-network") + So(obj.Config.Labels["pygmy.purpose"], ShouldEqual, "sshagent") + So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "30") + So(obj.HostConfig.AutoRemove, ShouldBeFalse) + So(obj.HostConfig.IpcMode, ShouldEqual, "private") + So(obj.HostConfig.PortBindings, ShouldEqual, nil) + So(obj.HostConfig.RestartPolicy.Name, ShouldEqual, "on-failure") + So(obj.HostConfig.RestartPolicy.MaximumRetryCount, ShouldEqual, 0) + }) +} diff --git a/service/ssh/key/ssh_addkey.go b/service/ssh/key/ssh_addkey.go index 6bf43445..075ccabd 100644 --- a/service/ssh/key/ssh_addkey.go +++ b/service/ssh/key/ssh_addkey.go @@ -17,6 +17,7 @@ func NewAdder() model.Service { "pygmy.defaults": "true", "pygmy.enable": "true", "pygmy.name": "amazeeio-ssh-agent-add-key", + "pygmy.network": "amazeeio-network", "pygmy.discrete": "true", "pygmy.output": "false", "pygmy.purpose": "addkeys", @@ -31,30 +32,3 @@ func NewAdder() model.Service { NetworkConfig: network.NetworkingConfig{}, } } - -// NewShower will provide the standard object for the SSH key shower container. -func NewShower() model.Service { - return model.Service{ - Config: container.Config{ - Image: "amazeeio/ssh-agent", - Cmd: []string{ - "ssh-add", - "-L", - }, - Labels: map[string]string{ - "pygmy.defaults": "true", - "pygmy.enable": "true", - "pygmy.name": "amazeeio-ssh-agent-show-keys", - "pygmy.discrete": "true", - "pygmy.output": "false", - "pygmy.purpose": "showkeys", - "pygmy.weight": "32", - }, - }, - HostConfig: container.HostConfig{ - AutoRemove: true, - VolumesFrom: []string{"amazeeio-ssh-agent"}, - }, - NetworkConfig: network.NetworkingConfig{}, - } -} diff --git a/service/ssh/key/ssh_addkey_test.go b/service/ssh/key/ssh_addkey_test.go new file mode 100644 index 00000000..ae8ba8d8 --- /dev/null +++ b/service/ssh/key/ssh_addkey_test.go @@ -0,0 +1,31 @@ +package key_test + +import ( + "fmt" + "testing" + + "github.com/fubarhouse/pygmy-go/service/ssh/key" + . "github.com/smartystreets/goconvey/convey" +) + +//func ExampleAdd() { +// key.NewAdder() +//} + +func TestAdd(t *testing.T) { + Convey("SSH Key Adder: Field equality tests...", t, func() { + obj := key.NewAdder() + So(obj.Config.Image, ShouldEqual, "amazeeio/ssh-agent") + So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.output"], ShouldEqual, "false") + So(obj.Config.Labels["pygmy.discrete"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-ssh-agent-add-key") + So(obj.Config.Labels["pygmy.network"], ShouldEqual, "amazeeio-network") + So(obj.Config.Labels["pygmy.purpose"], ShouldEqual, "addkeys") + So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "31") + So(obj.HostConfig.AutoRemove, ShouldBeTrue) + So(obj.HostConfig.IpcMode, ShouldEqual, "private") + So(fmt.Sprint(obj.HostConfig.VolumesFrom), ShouldEqual, fmt.Sprint([]string{"amazeeio-ssh-agent"})) + }) +} diff --git a/service/ssh/key/ssh_addkey_win.go b/service/ssh/key/ssh_addkey_win.go index 4d158309..6a94328e 100644 --- a/service/ssh/key/ssh_addkey_win.go +++ b/service/ssh/key/ssh_addkey_win.go @@ -17,6 +17,7 @@ func NewAdder() model.Service { "pygmy.defaults": "true", "pygmy.enable": "true", "pygmy.name": "amazeeio-ssh-agent-add-key", + "pygmy.network": "amazeeio-network", "pygmy.discrete": "true", "pygmy.output": "false", "pygmy.purpose": "addkeys", @@ -31,30 +32,3 @@ func NewAdder() model.Service { NetworkConfig: network.NetworkingConfig{}, } } - -// NewShower will provide the standard object for the SSH key shower container. -func NewShower() model.Service { - return model.Service{ - Config: container.Config{ - Image: "amazeeio/ssh-agent", - Cmd: []string{ - "ssh-add", - "-L", - }, - Labels: map[string]string{ - "pygmy.defaults": "true", - "pygmy.enable": "true", - "pygmy.name": "amazeeio-ssh-agent-show-keys", - "pygmy.discrete": "true", - "pygmy.output": "false", - "pygmy.purpose": "showkeys", - "pygmy.weight": "32", - }, - }, - HostConfig: container.HostConfig{ - AutoRemove: true, - VolumesFrom: []string{"amazeeio-ssh-agent"}, - }, - NetworkConfig: network.NetworkingConfig{}, - } -} diff --git a/service/ssh/key/ssh_showkeys.go b/service/ssh/key/ssh_showkeys.go new file mode 100644 index 00000000..fe1016bc --- /dev/null +++ b/service/ssh/key/ssh_showkeys.go @@ -0,0 +1,38 @@ +// +build darwin linux + +package key + +import ( + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + model "github.com/fubarhouse/pygmy-go/service/interface" +) + +// NewShower will provide the standard object for the SSH key shower container. +func NewShower() model.Service { + return model.Service{ + Config: container.Config{ + Image: "amazeeio/ssh-agent", + Cmd: []string{ + "ssh-add", + "-L", + }, + Labels: map[string]string{ + "pygmy.defaults": "true", + "pygmy.enable": "true", + "pygmy.name": "amazeeio-ssh-agent-show-keys", + "pygmy.network": "amazeeio-network", + "pygmy.discrete": "true", + "pygmy.output": "false", + "pygmy.purpose": "showkeys", + "pygmy.weight": "32", + }, + }, + HostConfig: container.HostConfig{ + AutoRemove: true, + IpcMode: "private", + VolumesFrom: []string{"amazeeio-ssh-agent"}, + }, + NetworkConfig: network.NetworkingConfig{}, + } +} diff --git a/service/ssh/key/ssh_showkeys_test.go b/service/ssh/key/ssh_showkeys_test.go new file mode 100644 index 00000000..12a6abf0 --- /dev/null +++ b/service/ssh/key/ssh_showkeys_test.go @@ -0,0 +1,32 @@ +package key_test + +import ( + "fmt" + "testing" + + "github.com/fubarhouse/pygmy-go/service/ssh/key" + . "github.com/smartystreets/goconvey/convey" +) + +//func ExampleShow() { +// key.NewShower() +//} + +func TestShow(t *testing.T) { + Convey("SSH Key Shower: Field equality tests...", t, func() { + obj := key.NewShower() + So(obj.Config.Image, ShouldEqual, "amazeeio/ssh-agent") + So(fmt.Sprint(obj.Config.Cmd), ShouldEqual, fmt.Sprint([]string{"ssh-add", "-L"})) + So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.output"], ShouldEqual, "false") + So(obj.Config.Labels["pygmy.discrete"], ShouldEqual, "true") + So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-ssh-agent-show-keys") + So(obj.Config.Labels["pygmy.network"], ShouldEqual, "amazeeio-network") + So(obj.Config.Labels["pygmy.purpose"], ShouldEqual, "showkeys") + So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "32") + So(obj.HostConfig.AutoRemove, ShouldBeTrue) + So(obj.HostConfig.IpcMode, ShouldEqual, "private") + So(fmt.Sprint(obj.HostConfig.VolumesFrom), ShouldEqual, fmt.Sprint([]string{"amazeeio-ssh-agent"})) + }) +} diff --git a/service/ssh/key/ssh_showkeys_win.go b/service/ssh/key/ssh_showkeys_win.go new file mode 100644 index 00000000..1e10d96c --- /dev/null +++ b/service/ssh/key/ssh_showkeys_win.go @@ -0,0 +1,36 @@ +// +build windows + +package key + +import ( + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + model "github.com/fubarhouse/pygmy-go/service/interface" +) + +// NewShower will provide the standard object for the SSH key shower container. +func NewShower() model.Service { + return model.Service{ + Config: container.Config{ + Image: "amazeeio/ssh-agent", + Cmd: []string{ + "ssh-add", + "-L", + }, + Labels: map[string]string{ + "pygmy.defaults": "true", + "pygmy.enable": "true", + "pygmy.name": "amazeeio-ssh-agent-show-keys", + "pygmy.discrete": "true", + "pygmy.output": "false", + "pygmy.purpose": "showkeys", + "pygmy.weight": "32", + }, + }, + HostConfig: container.HostConfig{ + AutoRemove: true, + VolumesFrom: []string{"amazeeio-ssh-agent"}, + }, + NetworkConfig: network.NetworkingConfig{}, + } +}