| package service // import "github.com/docker/docker/integration/service" | |
| import ( | |
| "context" | |
| "io" | |
| "strings" | |
| "testing" | |
| "time" | |
| "github.com/docker/docker/api/types" | |
| "github.com/docker/docker/api/types/filters" | |
| "github.com/docker/docker/api/types/strslice" | |
| swarmtypes "github.com/docker/docker/api/types/swarm" | |
| "github.com/docker/docker/api/types/versions" | |
| "github.com/docker/docker/client" | |
| "github.com/docker/docker/errdefs" | |
| "github.com/docker/docker/integration/internal/network" | |
| "github.com/docker/docker/integration/internal/swarm" | |
| "github.com/docker/docker/testutil/daemon" | |
| "gotest.tools/v3/assert" | |
| is "gotest.tools/v3/assert/cmp" | |
| "gotest.tools/v3/poll" | |
| "gotest.tools/v3/skip" | |
| ) | |
| func TestServiceCreateInit(t *testing.T) { | |
| defer setupTest(t)() | |
| t.Run("daemonInitDisabled", testServiceCreateInit(false)) | |
| t.Run("daemonInitEnabled", testServiceCreateInit(true)) | |
| } | |
| func testServiceCreateInit(daemonEnabled bool) func(t *testing.T) { | |
| return func(t *testing.T) { | |
| var ops = []daemon.Option{} | |
| if daemonEnabled { | |
| ops = append(ops, daemon.WithInit()) | |
| } | |
| d := swarm.NewSwarm(t, testEnv, ops...) | |
| defer d.Stop(t) | |
| client := d.NewClientT(t) | |
| defer client.Close() | |
| booleanTrue := true | |
| booleanFalse := false | |
| serviceID := swarm.CreateService(t, d) | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, 1), swarm.ServicePoll) | |
| i := inspectServiceContainer(t, client, serviceID) | |
| // HostConfig.Init == nil means that it delegates to daemon configuration | |
| assert.Check(t, i.HostConfig.Init == nil) | |
| serviceID = swarm.CreateService(t, d, swarm.ServiceWithInit(&booleanTrue)) | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, 1), swarm.ServicePoll) | |
| i = inspectServiceContainer(t, client, serviceID) | |
| assert.Check(t, is.Equal(true, *i.HostConfig.Init)) | |
| serviceID = swarm.CreateService(t, d, swarm.ServiceWithInit(&booleanFalse)) | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, 1), swarm.ServicePoll) | |
| i = inspectServiceContainer(t, client, serviceID) | |
| assert.Check(t, is.Equal(false, *i.HostConfig.Init)) | |
| } | |
| } | |
| func inspectServiceContainer(t *testing.T, client client.APIClient, serviceID string) types.ContainerJSON { | |
| t.Helper() | |
| containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{ | |
| Filters: filters.NewArgs(filters.Arg("label", "com.docker.swarm.service.id="+serviceID)), | |
| }) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Len(containers, 1)) | |
| i, err := client.ContainerInspect(context.Background(), containers[0].ID) | |
| assert.NilError(t, err) | |
| return i | |
| } | |
| func TestCreateServiceMultipleTimes(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows") | |
| defer setupTest(t)() | |
| d := swarm.NewSwarm(t, testEnv) | |
| defer d.Stop(t) | |
| client := d.NewClientT(t) | |
| defer client.Close() | |
| ctx := context.Background() | |
| overlayName := "overlay1_" + t.Name() | |
| overlayID := network.CreateNoError(ctx, t, client, overlayName, | |
| network.WithCheckDuplicate(), | |
| network.WithDriver("overlay"), | |
| ) | |
| var instances uint64 = 4 | |
| serviceName := "TestService_" + t.Name() | |
| serviceSpec := []swarm.ServiceSpecOpt{ | |
| swarm.ServiceWithReplicas(instances), | |
| swarm.ServiceWithName(serviceName), | |
| swarm.ServiceWithNetwork(overlayName), | |
| } | |
| serviceID := swarm.CreateService(t, d, serviceSpec...) | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, instances), swarm.ServicePoll) | |
| _, _, err := client.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{}) | |
| assert.NilError(t, err) | |
| err = client.ServiceRemove(context.Background(), serviceID) | |
| assert.NilError(t, err) | |
| poll.WaitOn(t, swarm.NoTasksForService(ctx, client, serviceID), swarm.ServicePoll) | |
| serviceID2 := swarm.CreateService(t, d, serviceSpec...) | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID2, instances), swarm.ServicePoll) | |
| err = client.ServiceRemove(context.Background(), serviceID2) | |
| assert.NilError(t, err) | |
| // we can't just wait on no tasks for the service, counter-intuitively. | |
| // Tasks may briefly exist but not show up, if they are are in the process | |
| // of being deallocated. To avoid this case, we should retry network remove | |
| // a few times, to give tasks time to be deallcoated | |
| poll.WaitOn(t, swarm.NoTasksForService(ctx, client, serviceID2), swarm.ServicePoll) | |
| for retry := 0; retry < 5; retry++ { | |
| err = client.NetworkRemove(context.Background(), overlayID) | |
| // TODO(dperny): using strings.Contains for error checking is awful, | |
| // but so is the fact that swarm functions don't return errdefs errors. | |
| // I don't have time at this moment to fix the latter, so I guess I'll | |
| // go with the former. | |
| // | |
| // The full error we're looking for is something like this: | |
| // | |
| // Error response from daemon: rpc error: code = FailedPrecondition desc = network %v is in use by task %v | |
| // | |
| // The safest way to catch this, I think, will be to match on "is in | |
| // use by", as this is an uninterrupted string that best identifies | |
| // this error. | |
| if err == nil || !strings.Contains(err.Error(), "is in use by") { | |
| // if there is no error, or the error isn't this kind of error, | |
| // then we'll break the loop body, and either fail the test or | |
| // continue. | |
| break | |
| } | |
| } | |
| assert.NilError(t, err) | |
| poll.WaitOn(t, network.IsRemoved(context.Background(), client, overlayID), poll.WithTimeout(1*time.Minute), poll.WithDelay(10*time.Second)) | |
| } | |
| func TestCreateServiceConflict(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows") | |
| defer setupTest(t)() | |
| d := swarm.NewSwarm(t, testEnv) | |
| defer d.Stop(t) | |
| c := d.NewClientT(t) | |
| defer c.Close() | |
| ctx := context.Background() | |
| serviceName := "TestService_" + t.Name() | |
| serviceSpec := []swarm.ServiceSpecOpt{ | |
| swarm.ServiceWithName(serviceName), | |
| } | |
| swarm.CreateService(t, d, serviceSpec...) | |
| spec := swarm.CreateServiceSpec(t, serviceSpec...) | |
| _, err := c.ServiceCreate(ctx, spec, types.ServiceCreateOptions{}) | |
| assert.Check(t, errdefs.IsConflict(err)) | |
| assert.ErrorContains(t, err, "service "+serviceName+" already exists") | |
| } | |
| func TestCreateServiceMaxReplicas(t *testing.T) { | |
| defer setupTest(t)() | |
| d := swarm.NewSwarm(t, testEnv) | |
| defer d.Stop(t) | |
| client := d.NewClientT(t) | |
| defer client.Close() | |
| var maxReplicas uint64 = 2 | |
| serviceSpec := []swarm.ServiceSpecOpt{ | |
| swarm.ServiceWithReplicas(maxReplicas), | |
| swarm.ServiceWithMaxReplicas(maxReplicas), | |
| } | |
| serviceID := swarm.CreateService(t, d, serviceSpec...) | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, maxReplicas), swarm.ServicePoll) | |
| _, _, err := client.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{}) | |
| assert.NilError(t, err) | |
| } | |
| func TestCreateWithDuplicateNetworkNames(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows") | |
| defer setupTest(t)() | |
| d := swarm.NewSwarm(t, testEnv) | |
| defer d.Stop(t) | |
| client := d.NewClientT(t) | |
| defer client.Close() | |
| ctx := context.Background() | |
| name := "foo_" + t.Name() | |
| n1 := network.CreateNoError(ctx, t, client, name, network.WithDriver("bridge")) | |
| n2 := network.CreateNoError(ctx, t, client, name, network.WithDriver("bridge")) | |
| // Duplicates with name but with different driver | |
| n3 := network.CreateNoError(ctx, t, client, name, network.WithDriver("overlay")) | |
| // Create Service with the same name | |
| var instances uint64 = 1 | |
| serviceName := "top_" + t.Name() | |
| serviceID := swarm.CreateService(t, d, | |
| swarm.ServiceWithReplicas(instances), | |
| swarm.ServiceWithName(serviceName), | |
| swarm.ServiceWithNetwork(name), | |
| ) | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, instances), swarm.ServicePoll) | |
| resp, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Equal(n3, resp.Spec.TaskTemplate.Networks[0].Target)) | |
| // Remove Service, and wait for its tasks to be removed | |
| err = client.ServiceRemove(ctx, serviceID) | |
| assert.NilError(t, err) | |
| poll.WaitOn(t, swarm.NoTasksForService(ctx, client, serviceID), swarm.ServicePoll) | |
| // Remove networks | |
| err = client.NetworkRemove(context.Background(), n3) | |
| assert.NilError(t, err) | |
| err = client.NetworkRemove(context.Background(), n2) | |
| assert.NilError(t, err) | |
| err = client.NetworkRemove(context.Background(), n1) | |
| assert.NilError(t, err) | |
| // Make sure networks have been destroyed. | |
| poll.WaitOn(t, network.IsRemoved(context.Background(), client, n3), poll.WithTimeout(1*time.Minute), poll.WithDelay(10*time.Second)) | |
| poll.WaitOn(t, network.IsRemoved(context.Background(), client, n2), poll.WithTimeout(1*time.Minute), poll.WithDelay(10*time.Second)) | |
| poll.WaitOn(t, network.IsRemoved(context.Background(), client, n1), poll.WithTimeout(1*time.Minute), poll.WithDelay(10*time.Second)) | |
| } | |
| func TestCreateServiceSecretFileMode(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows") | |
| defer setupTest(t)() | |
| d := swarm.NewSwarm(t, testEnv) | |
| defer d.Stop(t) | |
| client := d.NewClientT(t) | |
| defer client.Close() | |
| ctx := context.Background() | |
| secretName := "TestSecret_" + t.Name() | |
| secretResp, err := client.SecretCreate(ctx, swarmtypes.SecretSpec{ | |
| Annotations: swarmtypes.Annotations{ | |
| Name: secretName, | |
| }, | |
| Data: []byte("TESTSECRET"), | |
| }) | |
| assert.NilError(t, err) | |
| var instances uint64 = 1 | |
| serviceName := "TestService_" + t.Name() | |
| serviceID := swarm.CreateService(t, d, | |
| swarm.ServiceWithReplicas(instances), | |
| swarm.ServiceWithName(serviceName), | |
| swarm.ServiceWithCommand([]string{"/bin/sh", "-c", "ls -l /etc/secret && sleep inf"}), | |
| swarm.ServiceWithSecret(&swarmtypes.SecretReference{ | |
| File: &swarmtypes.SecretReferenceFileTarget{ | |
| Name: "/etc/secret", | |
| UID: "0", | |
| GID: "0", | |
| Mode: 0777, | |
| }, | |
| SecretID: secretResp.ID, | |
| SecretName: secretName, | |
| }), | |
| ) | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, instances), swarm.ServicePoll) | |
| body, err := client.ServiceLogs(ctx, serviceID, types.ContainerLogsOptions{ | |
| Tail: "1", | |
| ShowStdout: true, | |
| }) | |
| assert.NilError(t, err) | |
| defer body.Close() | |
| content, err := io.ReadAll(body) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Contains(string(content), "-rwxrwxrwx")) | |
| err = client.ServiceRemove(ctx, serviceID) | |
| assert.NilError(t, err) | |
| poll.WaitOn(t, swarm.NoTasksForService(ctx, client, serviceID), swarm.ServicePoll) | |
| err = client.SecretRemove(ctx, secretName) | |
| assert.NilError(t, err) | |
| } | |
| func TestCreateServiceConfigFileMode(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows") | |
| defer setupTest(t)() | |
| d := swarm.NewSwarm(t, testEnv) | |
| defer d.Stop(t) | |
| client := d.NewClientT(t) | |
| defer client.Close() | |
| ctx := context.Background() | |
| configName := "TestConfig_" + t.Name() | |
| configResp, err := client.ConfigCreate(ctx, swarmtypes.ConfigSpec{ | |
| Annotations: swarmtypes.Annotations{ | |
| Name: configName, | |
| }, | |
| Data: []byte("TESTCONFIG"), | |
| }) | |
| assert.NilError(t, err) | |
| var instances uint64 = 1 | |
| serviceName := "TestService_" + t.Name() | |
| serviceID := swarm.CreateService(t, d, | |
| swarm.ServiceWithName(serviceName), | |
| swarm.ServiceWithCommand([]string{"/bin/sh", "-c", "ls -l /etc/config && sleep inf"}), | |
| swarm.ServiceWithReplicas(instances), | |
| swarm.ServiceWithConfig(&swarmtypes.ConfigReference{ | |
| File: &swarmtypes.ConfigReferenceFileTarget{ | |
| Name: "/etc/config", | |
| UID: "0", | |
| GID: "0", | |
| Mode: 0777, | |
| }, | |
| ConfigID: configResp.ID, | |
| ConfigName: configName, | |
| }), | |
| ) | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, instances)) | |
| body, err := client.ServiceLogs(ctx, serviceID, types.ContainerLogsOptions{ | |
| Tail: "1", | |
| ShowStdout: true, | |
| }) | |
| assert.NilError(t, err) | |
| defer body.Close() | |
| content, err := io.ReadAll(body) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Contains(string(content), "-rwxrwxrwx")) | |
| err = client.ServiceRemove(ctx, serviceID) | |
| assert.NilError(t, err) | |
| poll.WaitOn(t, swarm.NoTasksForService(ctx, client, serviceID)) | |
| err = client.ConfigRemove(ctx, configName) | |
| assert.NilError(t, err) | |
| } | |
| // TestServiceCreateSysctls tests that a service created with sysctl options in | |
| // the ContainerSpec correctly applies those options. | |
| // | |
| // To test this, we're going to create a service with the sysctl option | |
| // | |
| // {"net.ipv4.ip_nonlocal_bind": "0"} | |
| // | |
| // We'll get the service's tasks to get the container ID, and then we'll | |
| // inspect the container. If the output of the container inspect contains the | |
| // sysctl option with the correct value, we can assume that the sysctl has been | |
| // plumbed correctly. | |
| // | |
| // Next, we'll remove that service and create a new service with that option | |
| // set to 1. This means that no matter what the default is, we can be confident | |
| // that the sysctl option is applying as intended. | |
| // | |
| // Additionally, we'll do service and task inspects to verify that the inspect | |
| // output includes the desired sysctl option. | |
| // | |
| // We're using net.ipv4.ip_nonlocal_bind because it's something that I'm fairly | |
| // confident won't be modified by the container runtime, and won't blow | |
| // anything up in the test environment | |
| func TestCreateServiceSysctls(t *testing.T) { | |
| skip.If( | |
| t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), | |
| "setting service sysctls is unsupported before api v1.40", | |
| ) | |
| defer setupTest(t)() | |
| d := swarm.NewSwarm(t, testEnv) | |
| defer d.Stop(t) | |
| client := d.NewClientT(t) | |
| defer client.Close() | |
| ctx := context.Background() | |
| // run thie block twice, so that no matter what the default value of | |
| // net.ipv4.ip_nonlocal_bind is, we can verify that setting the sysctl | |
| // options works | |
| for _, expected := range []string{"0", "1"} { | |
| // store the map we're going to be using everywhere. | |
| expectedSysctls := map[string]string{"net.ipv4.ip_nonlocal_bind": expected} | |
| // Create the service with the sysctl options | |
| var instances uint64 = 1 | |
| serviceID := swarm.CreateService(t, d, | |
| swarm.ServiceWithSysctls(expectedSysctls), | |
| ) | |
| // wait for the service to converge to 1 running task as expected | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, instances)) | |
| // we're going to check 3 things: | |
| // | |
| // 1. Does the container, when inspected, have the sysctl option set? | |
| // 2. Does the task have the sysctl in the spec? | |
| // 3. Does the service have the sysctl in the spec? | |
| // | |
| // if all 3 of these things are true, we know that the sysctl has been | |
| // plumbed correctly through the engine. | |
| // | |
| // We don't actually have to get inside the container and check its | |
| // logs or anything. If we see the sysctl set on the container inspect, | |
| // we know that the sysctl is plumbed correctly. everything below that | |
| // level has been tested elsewhere. (thanks @thaJeztah, because an | |
| // earlier version of this test had to get container logs and was much | |
| // more complex) | |
| // get all tasks of the service, so we can get the container | |
| tasks, err := client.TaskList(ctx, types.TaskListOptions{ | |
| Filters: filters.NewArgs(filters.Arg("service", serviceID)), | |
| }) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Equal(len(tasks), 1)) | |
| // verify that the container has the sysctl option set | |
| ctnr, err := client.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID) | |
| assert.NilError(t, err) | |
| assert.DeepEqual(t, ctnr.HostConfig.Sysctls, expectedSysctls) | |
| // verify that the task has the sysctl option set in the task object | |
| assert.DeepEqual(t, tasks[0].Spec.ContainerSpec.Sysctls, expectedSysctls) | |
| // verify that the service also has the sysctl set in the spec. | |
| service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) | |
| assert.NilError(t, err) | |
| assert.DeepEqual(t, | |
| service.Spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls, | |
| ) | |
| } | |
| } | |
| // TestServiceCreateCapabilities tests that a service created with capabilities options in | |
| // the ContainerSpec correctly applies those options. | |
| // | |
| // To test this, we're going to create a service with the capabilities option | |
| // | |
| // []string{"CAP_NET_RAW", "CAP_SYS_CHROOT"} | |
| // | |
| // We'll get the service's tasks to get the container ID, and then we'll | |
| // inspect the container. If the output of the container inspect contains the | |
| // capabilities option with the correct value, we can assume that the capabilities has been | |
| // plumbed correctly. | |
| func TestCreateServiceCapabilities(t *testing.T) { | |
| skip.If( | |
| t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.41"), | |
| "setting service capabilities is unsupported before api v1.41", | |
| ) | |
| defer setupTest(t)() | |
| d := swarm.NewSwarm(t, testEnv) | |
| defer d.Stop(t) | |
| client := d.NewClientT(t) | |
| defer client.Close() | |
| ctx := context.Background() | |
| // store the map we're going to be using everywhere. | |
| capAdd := []string{"CAP_SYS_CHROOT"} | |
| capDrop := []string{"CAP_NET_RAW"} | |
| // Create the service with the capabilities options | |
| var instances uint64 = 1 | |
| serviceID := swarm.CreateService(t, d, | |
| swarm.ServiceWithCapabilities(capAdd, capDrop), | |
| ) | |
| // wait for the service to converge to 1 running task as expected | |
| poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, instances)) | |
| // we're going to check 3 things: | |
| // | |
| // 1. Does the container, when inspected, have the capabilities option set? | |
| // 2. Does the task have the capabilities in the spec? | |
| // 3. Does the service have the capabilities in the spec? | |
| // | |
| // if all 3 of these things are true, we know that the capabilities has been | |
| // plumbed correctly through the engine. | |
| // | |
| // We don't actually have to get inside the container and check its | |
| // logs or anything. If we see the capabilities set on the container inspect, | |
| // we know that the capabilities is plumbed correctly. everything below that | |
| // level has been tested elsewhere. | |
| // get all tasks of the service, so we can get the container | |
| tasks, err := client.TaskList(ctx, types.TaskListOptions{ | |
| Filters: filters.NewArgs(filters.Arg("service", serviceID)), | |
| }) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Equal(len(tasks), 1)) | |
| // verify that the container has the capabilities option set | |
| ctnr, err := client.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID) | |
| assert.NilError(t, err) | |
| assert.DeepEqual(t, ctnr.HostConfig.CapAdd, strslice.StrSlice(capAdd)) | |
| assert.DeepEqual(t, ctnr.HostConfig.CapDrop, strslice.StrSlice(capDrop)) | |
| // verify that the task has the capabilities option set in the task object | |
| assert.DeepEqual(t, tasks[0].Spec.ContainerSpec.CapabilityAdd, capAdd) | |
| assert.DeepEqual(t, tasks[0].Spec.ContainerSpec.CapabilityDrop, capDrop) | |
| // verify that the service also has the capabilities set in the spec. | |
| service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) | |
| assert.NilError(t, err) | |
| assert.DeepEqual(t, service.Spec.TaskTemplate.ContainerSpec.CapabilityAdd, capAdd) | |
| assert.DeepEqual(t, service.Spec.TaskTemplate.ContainerSpec.CapabilityDrop, capDrop) | |
| } |