diff --git a/integration/config/config_test.go b/integration/config/config_test.go index fa2a205953..6a96986e4a 100644 --- a/integration/config/config_test.go +++ b/integration/config/config_test.go @@ -1,8 +1,10 @@ package config import ( + "bytes" "sort" "testing" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -10,6 +12,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/integration/internal/swarm" "github.com/docker/docker/internal/testutil" + "github.com/docker/docker/pkg/stdcopy" "github.com/gotestyourself/gotestyourself/skip" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -188,3 +191,130 @@ func TestConfigsUpdate(t *testing.T) { err = client.ConfigUpdate(ctx, configID, insp.Version, insp.Spec) testutil.ErrorContains(t, err, "only updates to Labels are allowed") } + +func TestTemplatedConfig(t *testing.T) { + d := swarm.NewSwarm(t, testEnv) + defer d.Stop(t) + + ctx := context.Background() + client := swarm.GetClient(t, d) + + referencedSecretSpec := swarmtypes.SecretSpec{ + Annotations: swarmtypes.Annotations{ + Name: "referencedsecret", + }, + Data: []byte("this is a secret"), + } + referencedSecret, err := client.SecretCreate(ctx, referencedSecretSpec) + assert.NoError(t, err) + + referencedConfigSpec := swarmtypes.ConfigSpec{ + Annotations: swarmtypes.Annotations{ + Name: "referencedconfig", + }, + Data: []byte("this is a config"), + } + referencedConfig, err := client.ConfigCreate(ctx, referencedConfigSpec) + assert.NoError(t, err) + + configSpec := swarmtypes.ConfigSpec{ + Annotations: swarmtypes.Annotations{ + Name: "templated_config", + }, + Templating: &swarmtypes.Driver{ + Name: "golang", + }, + Data: []byte("SERVICE_NAME={{.Service.Name}}\n" + + "{{secret \"referencedsecrettarget\"}}\n" + + "{{config \"referencedconfigtarget\"}}\n"), + } + + templatedConfig, err := client.ConfigCreate(ctx, configSpec) + assert.NoError(t, err) + + serviceID := swarm.CreateService(t, d, + swarm.ServiceWithConfig( + &swarmtypes.ConfigReference{ + File: &swarmtypes.ConfigReferenceFileTarget{ + Name: "/templated_config", + UID: "0", + GID: "0", + Mode: 0600, + }, + ConfigID: templatedConfig.ID, + ConfigName: "templated_config", + }, + ), + swarm.ServiceWithConfig( + &swarmtypes.ConfigReference{ + File: &swarmtypes.ConfigReferenceFileTarget{ + Name: "referencedconfigtarget", + UID: "0", + GID: "0", + Mode: 0600, + }, + ConfigID: referencedConfig.ID, + ConfigName: "referencedconfig", + }, + ), + swarm.ServiceWithSecret( + &swarmtypes.SecretReference{ + File: &swarmtypes.SecretReferenceFileTarget{ + Name: "referencedsecrettarget", + UID: "0", + GID: "0", + Mode: 0600, + }, + SecretID: referencedSecret.ID, + SecretName: "referencedsecret", + }, + ), + swarm.ServiceWithName("svc"), + ) + + var tasks []swarmtypes.Task + waitAndAssert(t, 60*time.Second, func(t *testing.T) bool { + tasks = swarm.GetRunningTasks(t, d, serviceID) + return len(tasks) > 0 + }) + + task := tasks[0] + waitAndAssert(t, 60*time.Second, func(t *testing.T) bool { + if task.NodeID == "" || (task.Status.ContainerStatus == nil || task.Status.ContainerStatus.ContainerID == "") { + task, _, _ = client.TaskInspectWithRaw(context.Background(), task.ID) + } + return task.NodeID != "" && task.Status.ContainerStatus != nil && task.Status.ContainerStatus.ContainerID != "" + }) + + attach := swarm.ExecTask(t, d, task, types.ExecConfig{ + Cmd: []string{"/bin/cat", "/templated_config"}, + AttachStdout: true, + AttachStderr: true, + }) + + buf := bytes.NewBuffer(nil) + _, err = stdcopy.StdCopy(buf, buf, attach.Reader) + require.NoError(t, err) + + expect := "SERVICE_NAME=svc\n" + + "this is a secret\n" + + "this is a config\n" + + assert.Equal(t, expect, buf.String()) +} + +func waitAndAssert(t *testing.T, timeout time.Duration, f func(*testing.T) bool) { + t.Helper() + after := time.After(timeout) + for { + select { + case <-after: + t.Fatalf("timed out waiting for condition") + default: + } + if f(t) { + return + } + time.Sleep(100 * time.Millisecond) + } +} diff --git a/integration/internal/swarm/service.go b/integration/internal/swarm/service.go index bd001ba0b4..a46b02e146 100644 --- a/integration/internal/swarm/service.go +++ b/integration/internal/swarm/service.go @@ -1,10 +1,14 @@ package swarm import ( + "context" "fmt" "testing" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" swarmtypes "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" "github.com/docker/docker/integration-cli/daemon" "github.com/docker/docker/internal/test/environment" "github.com/stretchr/testify/require" @@ -34,3 +38,121 @@ func NewSwarm(t *testing.T, testEnv *environment.Execution) *daemon.Swarm { require.NoError(t, d.Init(swarmtypes.InitRequest{})) return d } + +// ServiceSpecOpt is used with `CreateService` to pass in service spec modifiers +type ServiceSpecOpt func(*swarmtypes.ServiceSpec) + +// CreateService creates a service on the passed in swarm daemon. +func CreateService(t *testing.T, d *daemon.Swarm, opts ...ServiceSpecOpt) string { + spec := defaultServiceSpec() + for _, o := range opts { + o(&spec) + } + + client := GetClient(t, d) + + resp, err := client.ServiceCreate(context.Background(), spec, types.ServiceCreateOptions{}) + require.NoError(t, err, "error creating service") + return resp.ID +} + +func defaultServiceSpec() swarmtypes.ServiceSpec { + var spec swarmtypes.ServiceSpec + ServiceWithImage("busybox:latest")(&spec) + ServiceWithCommand([]string{"/bin/top"})(&spec) + ServiceWithReplicas(1)(&spec) + return spec +} + +// ServiceWithImage sets the image to use for the service +func ServiceWithImage(image string) func(*swarmtypes.ServiceSpec) { + return func(spec *swarmtypes.ServiceSpec) { + ensureContainerSpec(spec) + spec.TaskTemplate.ContainerSpec.Image = image + } +} + +// ServiceWithCommand sets the command to use for the service +func ServiceWithCommand(cmd []string) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + ensureContainerSpec(spec) + spec.TaskTemplate.ContainerSpec.Command = cmd + } +} + +// ServiceWithConfig adds the config reference to the service +func ServiceWithConfig(configRef *swarmtypes.ConfigReference) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + ensureContainerSpec(spec) + spec.TaskTemplate.ContainerSpec.Configs = append(spec.TaskTemplate.ContainerSpec.Configs, configRef) + } +} + +// ServiceWithSecret adds the secret reference to the service +func ServiceWithSecret(secretRef *swarmtypes.SecretReference) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + ensureContainerSpec(spec) + spec.TaskTemplate.ContainerSpec.Secrets = append(spec.TaskTemplate.ContainerSpec.Secrets, secretRef) + } +} + +// ServiceWithReplicas sets the replicas for the service +func ServiceWithReplicas(n uint64) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + spec.Mode = swarmtypes.ServiceMode{ + Replicated: &swarmtypes.ReplicatedService{ + Replicas: &n, + }, + } + } +} + +// ServiceWithName sets the name of the service +func ServiceWithName(name string) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + spec.Annotations.Name = name + } +} + +// GetRunningTasks gets the list of running tasks for a service +func GetRunningTasks(t *testing.T, d *daemon.Swarm, serviceID string) []swarmtypes.Task { + client := GetClient(t, d) + + filterArgs := filters.NewArgs() + filterArgs.Add("desired-state", "running") + filterArgs.Add("service", serviceID) + + options := types.TaskListOptions{ + Filters: filterArgs, + } + tasks, err := client.TaskList(context.Background(), options) + require.NoError(t, err) + return tasks +} + +// ExecTask runs the passed in exec config on the given task +func ExecTask(t *testing.T, d *daemon.Swarm, task swarmtypes.Task, config types.ExecConfig) types.HijackedResponse { + client := GetClient(t, d) + + ctx := context.Background() + resp, err := client.ContainerExecCreate(ctx, task.Status.ContainerStatus.ContainerID, config) + require.NoError(t, err, "error creating exec") + + startCheck := types.ExecStartCheck{} + attach, err := client.ContainerExecAttach(ctx, resp.ID, startCheck) + require.NoError(t, err, "error attaching to exec") + return attach +} + +func ensureContainerSpec(spec *swarmtypes.ServiceSpec) { + if spec.TaskTemplate.ContainerSpec == nil { + spec.TaskTemplate.ContainerSpec = &swarmtypes.ContainerSpec{} + } +} + +// GetClient creates a new client for the passed in swarm daemon. +func GetClient(t *testing.T, d *daemon.Swarm) client.APIClient { + client, err := client.NewClientWithOpts(client.WithHost((d.Sock()))) + require.NoError(t, err) + return client +} diff --git a/integration/secret/secret_test.go b/integration/secret/secret_test.go index a6e9983e23..e26d8a698d 100644 --- a/integration/secret/secret_test.go +++ b/integration/secret/secret_test.go @@ -1,8 +1,10 @@ package secret import ( + "bytes" "sort" "testing" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -10,6 +12,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/integration/internal/swarm" "github.com/docker/docker/internal/testutil" + "github.com/docker/docker/pkg/stdcopy" "github.com/gotestyourself/gotestyourself/skip" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -232,3 +235,130 @@ func TestSecretsUpdate(t *testing.T) { err = client.SecretUpdate(ctx, secretID, insp.Version, insp.Spec) testutil.ErrorContains(t, err, "only updates to Labels are allowed") } + +func TestTemplatedSecret(t *testing.T) { + d := swarm.NewSwarm(t, testEnv) + defer d.Stop(t) + + ctx := context.Background() + client := swarm.GetClient(t, d) + + referencedSecretSpec := swarmtypes.SecretSpec{ + Annotations: swarmtypes.Annotations{ + Name: "referencedsecret", + }, + Data: []byte("this is a secret"), + } + referencedSecret, err := client.SecretCreate(ctx, referencedSecretSpec) + assert.NoError(t, err) + + referencedConfigSpec := swarmtypes.ConfigSpec{ + Annotations: swarmtypes.Annotations{ + Name: "referencedconfig", + }, + Data: []byte("this is a config"), + } + referencedConfig, err := client.ConfigCreate(ctx, referencedConfigSpec) + assert.NoError(t, err) + + secretSpec := swarmtypes.SecretSpec{ + Annotations: swarmtypes.Annotations{ + Name: "templated_secret", + }, + Templating: &swarmtypes.Driver{ + Name: "golang", + }, + Data: []byte("SERVICE_NAME={{.Service.Name}}\n" + + "{{secret \"referencedsecrettarget\"}}\n" + + "{{config \"referencedconfigtarget\"}}\n"), + } + + templatedSecret, err := client.SecretCreate(ctx, secretSpec) + assert.NoError(t, err) + + serviceID := swarm.CreateService(t, d, + swarm.ServiceWithSecret( + &swarmtypes.SecretReference{ + File: &swarmtypes.SecretReferenceFileTarget{ + Name: "templated_secret", + UID: "0", + GID: "0", + Mode: 0600, + }, + SecretID: templatedSecret.ID, + SecretName: "templated_secret", + }, + ), + swarm.ServiceWithConfig( + &swarmtypes.ConfigReference{ + File: &swarmtypes.ConfigReferenceFileTarget{ + Name: "referencedconfigtarget", + UID: "0", + GID: "0", + Mode: 0600, + }, + ConfigID: referencedConfig.ID, + ConfigName: "referencedconfig", + }, + ), + swarm.ServiceWithSecret( + &swarmtypes.SecretReference{ + File: &swarmtypes.SecretReferenceFileTarget{ + Name: "referencedsecrettarget", + UID: "0", + GID: "0", + Mode: 0600, + }, + SecretID: referencedSecret.ID, + SecretName: "referencedsecret", + }, + ), + swarm.ServiceWithName("svc"), + ) + + var tasks []swarmtypes.Task + waitAndAssert(t, 60*time.Second, func(t *testing.T) bool { + tasks = swarm.GetRunningTasks(t, d, serviceID) + return len(tasks) > 0 + }) + + task := tasks[0] + waitAndAssert(t, 60*time.Second, func(t *testing.T) bool { + if task.NodeID == "" || (task.Status.ContainerStatus == nil || task.Status.ContainerStatus.ContainerID == "") { + task, _, _ = client.TaskInspectWithRaw(context.Background(), task.ID) + } + return task.NodeID != "" && task.Status.ContainerStatus != nil && task.Status.ContainerStatus.ContainerID != "" + }) + + attach := swarm.ExecTask(t, d, task, types.ExecConfig{ + Cmd: []string{"/bin/cat", "/run/secrets/templated_secret"}, + AttachStdout: true, + AttachStderr: true, + }) + + buf := bytes.NewBuffer(nil) + _, err = stdcopy.StdCopy(buf, buf, attach.Reader) + require.NoError(t, err) + + expect := "SERVICE_NAME=svc\n" + + "this is a secret\n" + + "this is a config\n" + + assert.Equal(t, expect, buf.String()) +} + +func waitAndAssert(t *testing.T, timeout time.Duration, f func(*testing.T) bool) { + t.Helper() + after := time.After(timeout) + for { + select { + case <-after: + t.Fatalf("timed out waiting for condition") + default: + } + if f(t) { + return + } + time.Sleep(100 * time.Millisecond) + } +}