From f91ab73a18282b9f097dab1eae5d4902924dde45 Mon Sep 17 00:00:00 2001 From: Marcelo Pereira Date: Sun, 8 Jul 2018 22:16:01 +0200 Subject: [PATCH] Unit tests for service spec creation Signed-off-by: Marcelo Pereira --- .travis.yml | 3 +- Makefile | 4 ++ cmd/run.go | 117 +++++++++++++++++++++++----------------- cmd/run_test.go | 139 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 214 insertions(+), 49 deletions(-) create mode 100644 cmd/run_test.go diff --git a/.travis.yml b/.travis.yml index 6cf2b1c..9173e54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ sudo: required services: - - docker + - docker before_install: - docker pull golang:1.9.2 script: + - make test - make docker diff --git a/Makefile b/Makefile index 5964110..e97e788 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ +.PHONY: test linux: CGO_ENABLED=0 GOOS=linux go build -a -ldflags "-s -w" -installsuffix cgo -o ./jaas @@ -7,3 +8,6 @@ darwin: docker: docker build -t alexellis2/jaas:latest . + +test: + go test ./... diff --git a/cmd/run.go b/cmd/run.go index e4d85c8..32ca64a 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -27,6 +27,10 @@ var ( verbose bool ) +type clientInterface interface { + client.CommonAPIClient +} + func init() { rootCmd.AddCommand(runCmd) @@ -78,6 +82,7 @@ func runTask(taskRequest TaskRequest) error { fmt.Printf("Connected to.. OK %s\n", taskRequest.Networks) fmt.Printf("Constraints: %s\n", taskRequest.Constraints) fmt.Printf("envVars: %s\n", taskRequest.EnvVars) + fmt.Printf("envFiles: %s\n", taskRequest.EnvFiles) fmt.Printf("Secrets: %s\n", taskRequest.Secrets) } @@ -94,7 +99,6 @@ func runTask(taskRequest TaskRequest) error { var err error c, err = client.NewEnvClient() if err != nil { - return fmt.Errorf("is the Docker Daemon running? Error: %s", err.Error()) } @@ -116,13 +120,7 @@ func runTask(taskRequest TaskRequest) error { } } - spec := makeSpec(taskRequest.Image, taskRequest.EnvVars) - if len(taskRequest.Networks) > 0 { - nets := []swarm.NetworkAttachmentConfig{ - swarm.NetworkAttachmentConfig{Target: taskRequest.Networks[0]}, - } - spec.Networks = nets - } + spec := makeServiceSpec(taskRequest, c) createOptions := types.ServiceCreateOptions{} @@ -131,18 +129,52 @@ func runTask(taskRequest TaskRequest) error { fmt.Println("Using RegistryAuth") } - placement := &swarm.Placement{} - if len(taskRequest.Constraints) > 0 { - placement.Constraints = taskRequest.Constraints - spec.TaskTemplate.Placement = placement + createResponse, _ := c.ServiceCreate(context.Background(), spec, createOptions) + opts := types.ServiceInspectOptions{InsertDefaults: true} + + service, _, _ := c.ServiceInspectWithRaw(context.Background(), createResponse.ID, opts) + fmt.Printf("Service created: %s (%s)\n", service.Spec.Name, createResponse.ID) + + taskExitCode := pollTask(c, createResponse.ID, timeoutVal, taskRequest.ShowLogs, taskRequest.RemoveService) + os.Exit(taskExitCode) + return nil +} + +func makeServiceSpec(tr TaskRequest, c clientInterface) swarm.ServiceSpec { + max := uint64(1) + spec := swarm.ServiceSpec{ + TaskTemplate: swarm.TaskSpec{ + RestartPolicy: &swarm.RestartPolicy{ + MaxAttempts: &max, + Condition: swarm.RestartPolicyConditionNone, + }, + ContainerSpec: &swarm.ContainerSpec{ + Image: tr.Image, + Env: tr.EnvVars, + }, + }, } + attachNetworks(&spec, tr.Networks) + readEnvFiles(&spec, tr.EnvFiles) + setConstraints(&spec, tr.Constraints) + setCommand(&spec, tr.Command) + attachMounts(&spec, tr.Mounts) + attachSecrets(&spec, tr.Secrets, c) + return spec +} - if len(taskRequest.Command) > 0 { - spec.TaskTemplate.ContainerSpec.Command = strings.Split(taskRequest.Command, " ") +func attachNetworks(spec *swarm.ServiceSpec, networks []string) { + nets := []swarm.NetworkAttachmentConfig{} + for _, n := range networks { + nets = append(nets, swarm.NetworkAttachmentConfig{Target: n}) } - if len(taskRequest.EnvFiles) > 0 { - for _, file := range taskRequest.EnvFiles { + spec.Networks = nets +} + +func readEnvFiles(spec *swarm.ServiceSpec, files []string) { + if len(files) > 0 { + for _, file := range files { envs, err := readEnvs(file) if err != nil { fmt.Fprintf(os.Stderr, "%s", err) @@ -154,11 +186,26 @@ func runTask(taskRequest TaskRequest) error { } } } +} +func setConstraints(spec *swarm.ServiceSpec, constraints []string) { + if len(constraints) > 0 { + placement := &swarm.Placement{Constraints: constraints} + spec.TaskTemplate.Placement = placement + } +} + +func setCommand(spec *swarm.ServiceSpec, command string) { + if len(command) > 0 { + spec.TaskTemplate.ContainerSpec.Command = strings.Split(command, " ") + } +} + +func attachMounts(spec *swarm.ServiceSpec, mounts []string) { spec.TaskTemplate.ContainerSpec.Mounts = []mount.Mount{} - for _, bindMount := range taskRequest.Mounts { + for _, bindMount := range mounts { parts := strings.Split(bindMount, "=") - if len(parts) < 2 || len(parts) > 2 { + if len(parts) != 2 { fmt.Fprintf(os.Stderr, "Bind-mounts must be specified as: src=dest, i.e. --mount /home/alex/tmp/=/tmp/\n") os.Exit(1) } @@ -172,11 +219,13 @@ func runTask(taskRequest TaskRequest) error { spec.TaskTemplate.ContainerSpec.Mounts = append(spec.TaskTemplate.ContainerSpec.Mounts, mountVal) } } +} - secretList, err := c.SecretList(context.Background(), types.SecretListOptions{}) +func attachSecrets(spec *swarm.ServiceSpec, secrets []string, c clientInterface) { + secretList, _ := c.SecretList(context.Background(), types.SecretListOptions{}) spec.TaskTemplate.ContainerSpec.Secrets = []*swarm.SecretReference{} - for _, serviceSecret := range taskRequest.Secrets { + for _, serviceSecret := range secrets { var secretID string for _, s := range secretList { if serviceSecret == s.Spec.Annotations.Name { @@ -202,34 +251,6 @@ func runTask(taskRequest TaskRequest) error { spec.TaskTemplate.ContainerSpec.Secrets = append(spec.TaskTemplate.ContainerSpec.Secrets, &secretVal) } - - createResponse, _ := c.ServiceCreate(context.Background(), spec, createOptions) - opts := types.ServiceInspectOptions{InsertDefaults: true} - - service, _, _ := c.ServiceInspectWithRaw(context.Background(), createResponse.ID, opts) - fmt.Printf("Service created: %s (%s)\n", service.Spec.Name, createResponse.ID) - - taskExitCode := pollTask(c, createResponse.ID, timeoutVal, taskRequest.ShowLogs, taskRequest.RemoveService) - os.Exit(taskExitCode) - return nil -} - -func makeSpec(image string, envVars []string) swarm.ServiceSpec { - max := uint64(1) - - spec := swarm.ServiceSpec{ - TaskTemplate: swarm.TaskSpec{ - RestartPolicy: &swarm.RestartPolicy{ - MaxAttempts: &max, - Condition: swarm.RestartPolicyConditionNone, - }, - ContainerSpec: &swarm.ContainerSpec{ - Image: image, - Env: envVars, - }, - }, - } - return spec } func readEnvs(file string) ([]string, error) { diff --git a/cmd/run_test.go b/cmd/run_test.go new file mode 100644 index 0000000..caa471a --- /dev/null +++ b/cmd/run_test.go @@ -0,0 +1,139 @@ +// Copyright (c) Alex Ellis 2017-2018. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package cmd + +import ( + "context" + "io/ioutil" + "os" + "reflect" + "strconv" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" +) + +var validRequest = TaskRequest{ + Image: "input-image", + Networks: []string{"net1", "net2"}, + Constraints: []string{"node.id=2ivku8v2gvtg4", "engine.labels.operatingsystem==ubuntu 14.04"}, + EnvVars: []string{"ev1=val1", "ev2=val2"}, + Mounts: []string{"hostVol1=taskVol1", "hostVol2=taskVol2"}, + Secrets: []string{"secret1", "secret2"}, + ShowLogs: true, + Timeout: "12", + RemoveService: true, + RegistryAuth: "true", + Command: "echo 'some output'", +} + +type fakeClient struct { + client.CommonAPIClient + secrets []string +} + +func (fk fakeClient) SecretList(ctx context.Context, sopt types.SecretListOptions) ([]swarm.Secret, error) { + slist := []swarm.Secret{} + for i, secret := range fk.secrets { + a := swarm.Annotations{Name: secret} + sspec := swarm.SecretSpec{Annotations: a} + s := swarm.Secret{ + ID: strconv.Itoa(i), + Meta: swarm.Meta{}, + Spec: sspec, + } + + slist = append(slist, s) + } + return slist, nil +} + +func newClient(secrets []string) fakeClient { + return fakeClient{secrets: secrets} +} + +func contains(el string, array []string) bool { + for _, e := range array { + if el == e { + return true + } + } + return false +} + +func TestMakeServiceSpecValid(t *testing.T) { + c := newClient([]string{"secret1", "secret2", "secret3"}) + + f1, _ := ioutil.TempFile("", "jaas_env") + f1body := []byte("f1var1=val11\nf1var2=val12\n") + f1.Write(f1body) + f1.Sync() + f2, _ := ioutil.TempFile("", "jaas_env") + f2body := []byte("f2var1=val21\nf2var2=val22\n") + f2.Write(f2body) + f2.Sync() + defer os.Remove(f1.Name()) + defer os.Remove(f2.Name()) + validRequest.EnvFiles = []string{f1.Name(), f2.Name()} + + spec := makeServiceSpec(validRequest, c) + if spec.TaskTemplate.ContainerSpec.Image != "input-image" { + t.Errorf("Container spec image should be %s, was %s", validRequest.Image, spec.TaskTemplate.ContainerSpec.Image) + } + + // Test networks + networkTargets := []string{} + for _, n := range spec.Networks { + networkTargets = append(networkTargets, n.Target) + } + if !reflect.DeepEqual(networkTargets, []string{"net1", "net2"}) { + t.Errorf("Container spec networks should be %s, was %s", validRequest.Networks, networkTargets) + } + + // Test env vars from input and env files + inputEnv := []string{"ev1=val1", "ev2=val2", "f1var1=val11", "f1var2=val12", "f2var1=val21", "f2var2=val22"} + for _, ev := range inputEnv { + if !contains(ev, spec.TaskTemplate.ContainerSpec.Env) { + t.Errorf("Container spec env should contain %s, but only has %s", ev, spec.TaskTemplate.ContainerSpec.Env) + } + } + + // Test constraints + if !reflect.DeepEqual(spec.TaskTemplate.Placement.Constraints, []string{"node.id=2ivku8v2gvtg4", "engine.labels.operatingsystem==ubuntu 14.04"}) { + t.Errorf("Container spec constraints should be %s, was %s", validRequest.Constraints, spec.TaskTemplate.Placement.Constraints) + } + + // Test mounts + expectedMounts := []mount.Mount{ + {Source: "hostVol1", Target: "taskVol1"}, + {Source: "hostVol2", Target: "taskVol2"}, + } + if !reflect.DeepEqual(spec.TaskTemplate.ContainerSpec.Mounts, expectedMounts) { + t.Error("Container spec mounts should include:") + for _, m := range expectedMounts { + t.Errorf("{Source: %s, Target: %s}", m.Source, m.Target) + } + t.Error("But contained instead:") + for _, m := range spec.TaskTemplate.ContainerSpec.Mounts { + t.Errorf("{Source: %s, Target: %s}", m.Source, m.Target) + } + } + + // Test secrets + secretNames := []string{} + for _, s := range spec.TaskTemplate.ContainerSpec.Secrets { + secretNames = append(secretNames, s.SecretName) + } + if !reflect.DeepEqual(secretNames, []string{"secret1", "secret2"}) { + t.Errorf("Container spec secrets should be %s, was %s", validRequest.Secrets, secretNames) + } + + // Test command + if !reflect.DeepEqual(spec.TaskTemplate.ContainerSpec.Command, []string{"echo", "'some", "output'"}) { + t.Errorf("Container spec command should be %s, was %s", []string{"echo", "'some", "output'"}, spec.TaskTemplate.ContainerSpec.Command) + } +}