From 963f8dcc73655d1bf882acc1fe238dd04ba9c70b Mon Sep 17 00:00:00 2001 From: decentral1se Date: Thu, 21 Oct 2021 19:40:26 +0200 Subject: [PATCH] fix: recover tests from overzealous cleanup --- pkg/client/client_test.go | 46 ++ pkg/client/context_test.go | 80 ++++ pkg/upstream/convert/compose_test.go | 171 +++++++ pkg/upstream/convert/service_test.go | 678 +++++++++++++++++++++++++++ pkg/upstream/convert/volume_test.go | 361 ++++++++++++++ 5 files changed, 1336 insertions(+) create mode 100644 pkg/client/client_test.go create mode 100644 pkg/client/context_test.go create mode 100644 pkg/upstream/convert/compose_test.go create mode 100644 pkg/upstream/convert/service_test.go create mode 100644 pkg/upstream/convert/volume_test.go diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go new file mode 100644 index 000000000..6651de73b --- /dev/null +++ b/pkg/client/client_test.go @@ -0,0 +1,46 @@ +package client_test + +import ( + "fmt" + "testing" + + "coopcloud.tech/abra/pkg/client" +) + +// use at the start to ensure testContext[0, 1, ..., amnt-1] exist and +// testContextFail[0, 1, ..., failAmnt-1] don't exist +func ensureTestState(amnt, failAmnt int) error { + for i := 0; i < amnt; i++ { + err := client.CreateContext(fmt.Sprintf("testContext%d", i), "", "") + if err != nil { + return err + } + } + for i := 0; i < failAmnt; i++ { + if _, er := client.GetContext(fmt.Sprintf("testContextFail%d", i)); er == nil { + err := client.DeleteContext(fmt.Sprintf("testContextFail%d", i)) + if err != nil { + return err + } + } + } + return nil +} + +func TestNew(t *testing.T) { + err := ensureTestState(1, 1) + if err != nil { + t.Errorf("Couldn't ensure existence/nonexistence of contexts: %s", err) + } + contextName := "testContext0" + _, err = client.New(contextName) + if err != nil { + t.Errorf("couldn't initialise a new client with context %s: %s", contextName, err) + } + contextName = "testContextFail0" + _, err = client.New(contextName) + if err == nil { + t.Errorf("client.New(\"testContextFail0\") should have failed but didn't return an error") + } + +} diff --git a/pkg/client/context_test.go b/pkg/client/context_test.go new file mode 100644 index 000000000..1ec36c641 --- /dev/null +++ b/pkg/client/context_test.go @@ -0,0 +1,80 @@ +package client_test + +import ( + "testing" + + "coopcloud.tech/abra/pkg/client" + dContext "github.com/docker/cli/cli/context" + dCliContextStore "github.com/docker/cli/cli/context/store" +) + +type TestContext struct { + context dCliContextStore.Metadata + expected_endpoint string +} + +func dockerContext(host, key string) TestContext { + dockerContext := dCliContextStore.Metadata{ + Name: "foo", + Metadata: nil, + Endpoints: map[string]interface{}{ + key: dContext.EndpointMetaBase{ + Host: host, + SkipTLSVerify: false, + }, + }, + } + return TestContext{ + context: dockerContext, + expected_endpoint: host, + } +} + +func TestCreateContext(t *testing.T) { + err := client.CreateContext("testContext0", "wronguser", "wrongport") + if err == nil { + t.Error("client.CreateContext(\"testContextCreate\", \"wronguser\", \"wrongport\") should have failed but didn't return an error") + } + err = client.CreateContext("testContext0", "", "") + if err != nil { + t.Errorf("Couldn't create context: %s", err) + } +} + +func TestDeleteContext(t *testing.T) { + ensureTestState(1, 1) + err := client.DeleteContext("default") + if err == nil { + t.Errorf("client.DeleteContext(\"default\") should have failed but didn't return an error") + } + + err = client.DeleteContext("testContext0") + if err != nil { + t.Errorf("client.DeleteContext(\"testContext0\") failed: %s", err) + } + err = client.DeleteContext("testContextFail0") + if err == nil { + t.Errorf("client.DeleteContext(\"testContextFail0\") should have failed (attempt to delete non-existent context) but didn't return an error") + } +} + +func TestGetContextEndpoint(t *testing.T) { + var testDockerContexts = []TestContext{ + dockerContext("ssh://foobar", "docker"), + dockerContext("ssh://foobar", "k8"), + } + for _, context := range testDockerContexts { + endpoint, err := client.GetContextEndpoint(context.context) + if err != nil { + if err.Error() != "context lacks Docker endpoint" { + t.Error(err) + } + } else { + if endpoint != context.expected_endpoint { + t.Errorf("did not get correct context endpoint. Expected: %s, received: %s", context.expected_endpoint, endpoint) + } + } + + } + +} diff --git a/pkg/upstream/convert/compose_test.go b/pkg/upstream/convert/compose_test.go new file mode 100644 index 000000000..0dfac14e1 --- /dev/null +++ b/pkg/upstream/convert/compose_test.go @@ -0,0 +1,171 @@ +package convert + +import ( + "testing" + + composetypes "github.com/docker/cli/cli/compose/types" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/fs" +) + +func TestNamespaceScope(t *testing.T) { + scoped := Namespace{name: "foo"}.Scope("bar") + assert.Check(t, is.Equal("foo_bar", scoped)) +} + +func TestAddStackLabel(t *testing.T) { + labels := map[string]string{ + "something": "labeled", + } + actual := AddStackLabel(Namespace{name: "foo"}, labels) + expected := map[string]string{ + "something": "labeled", + LabelNamespace: "foo", + } + assert.Check(t, is.DeepEqual(expected, actual)) +} + +func TestNetworks(t *testing.T) { + namespace := Namespace{name: "foo"} + serviceNetworks := map[string]struct{}{ + "normal": {}, + "outside": {}, + "default": {}, + "attachablenet": {}, + "named": {}, + } + source := networkMap{ + "normal": composetypes.NetworkConfig{ + Driver: "overlay", + DriverOpts: map[string]string{ + "opt": "value", + }, + Ipam: composetypes.IPAMConfig{ + Driver: "driver", + Config: []*composetypes.IPAMPool{ + { + Subnet: "10.0.0.0", + }, + }, + }, + Labels: map[string]string{ + "something": "labeled", + }, + }, + "outside": composetypes.NetworkConfig{ + External: composetypes.External{External: true}, + Name: "special", + }, + "attachablenet": composetypes.NetworkConfig{ + Driver: "overlay", + Attachable: true, + }, + "named": composetypes.NetworkConfig{ + Name: "othername", + }, + } + expected := map[string]types.NetworkCreate{ + "foo_default": { + Labels: map[string]string{ + LabelNamespace: "foo", + }, + }, + "foo_normal": { + Driver: "overlay", + IPAM: &network.IPAM{ + Driver: "driver", + Config: []network.IPAMConfig{ + { + Subnet: "10.0.0.0", + }, + }, + }, + Options: map[string]string{ + "opt": "value", + }, + Labels: map[string]string{ + LabelNamespace: "foo", + "something": "labeled", + }, + }, + "foo_attachablenet": { + Driver: "overlay", + Attachable: true, + Labels: map[string]string{ + LabelNamespace: "foo", + }, + }, + "othername": { + Labels: map[string]string{LabelNamespace: "foo"}, + }, + } + + networks, externals := Networks(namespace, source, serviceNetworks) + assert.DeepEqual(t, expected, networks) + assert.DeepEqual(t, []string{"special"}, externals) +} + +func TestSecrets(t *testing.T) { + namespace := Namespace{name: "foo"} + + secretText := "this is the first secret" + secretFile := fs.NewFile(t, "convert-secrets", fs.WithContent(secretText)) + defer secretFile.Remove() + + source := map[string]composetypes.SecretConfig{ + "one": { + File: secretFile.Path(), + Labels: map[string]string{"monster": "mash"}, + }, + "ext": { + External: composetypes.External{ + External: true, + }, + }, + } + + specs, err := Secrets(namespace, source) + assert.NilError(t, err) + assert.Assert(t, is.Len(specs, 1)) + secret := specs[0] + assert.Check(t, is.Equal("foo_one", secret.Name)) + assert.Check(t, is.DeepEqual(map[string]string{ + "monster": "mash", + LabelNamespace: "foo", + }, secret.Labels)) + assert.Check(t, is.DeepEqual([]byte(secretText), secret.Data)) +} + +func TestConfigs(t *testing.T) { + namespace := Namespace{name: "foo"} + + configText := "this is the first config" + configFile := fs.NewFile(t, "convert-configs", fs.WithContent(configText)) + defer configFile.Remove() + + source := map[string]composetypes.ConfigObjConfig{ + "one": { + File: configFile.Path(), + Labels: map[string]string{"monster": "mash"}, + }, + "ext": { + External: composetypes.External{ + External: true, + }, + }, + } + + specs, err := Configs(namespace, source) + assert.NilError(t, err) + assert.Assert(t, is.Len(specs, 1)) + config := specs[0] + assert.Check(t, is.Equal("foo_one", config.Name)) + assert.Check(t, is.DeepEqual(map[string]string{ + "monster": "mash", + LabelNamespace: "foo", + }, config.Labels)) + assert.Check(t, is.DeepEqual([]byte(configText), config.Data)) +} diff --git a/pkg/upstream/convert/service_test.go b/pkg/upstream/convert/service_test.go new file mode 100644 index 000000000..0607f3957 --- /dev/null +++ b/pkg/upstream/convert/service_test.go @@ -0,0 +1,678 @@ +package convert + +import ( + "context" + "os" + "sort" + "strings" + "testing" + "time" + + composetypes "github.com/docker/cli/cli/compose/types" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "github.com/pkg/errors" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestConvertRestartPolicyFromNone(t *testing.T) { + policy, err := convertRestartPolicy("no", nil) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual((*swarm.RestartPolicy)(nil), policy)) +} + +func TestConvertRestartPolicyFromUnknown(t *testing.T) { + _, err := convertRestartPolicy("unknown", nil) + assert.Error(t, err, "unknown restart policy: unknown") +} + +func TestConvertRestartPolicyFromAlways(t *testing.T) { + policy, err := convertRestartPolicy("always", nil) + expected := &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyConditionAny, + } + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, policy)) +} + +func TestConvertRestartPolicyFromFailure(t *testing.T) { + policy, err := convertRestartPolicy("on-failure:4", nil) + attempts := uint64(4) + expected := &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyConditionOnFailure, + MaxAttempts: &attempts, + } + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, policy)) +} + +func strPtr(val string) *string { + return &val +} + +func TestConvertEnvironment(t *testing.T) { + source := map[string]*string{ + "foo": strPtr("bar"), + "key": strPtr("value"), + } + env := convertEnvironment(source) + sort.Strings(env) + assert.Check(t, is.DeepEqual([]string{"foo=bar", "key=value"}, env)) +} + +func TestConvertExtraHosts(t *testing.T) { + source := composetypes.HostsList{ + "zulu:127.0.0.2", + "alpha:127.0.0.1", + "zulu:ff02::1", + } + assert.Check(t, is.DeepEqual([]string{"127.0.0.2 zulu", "127.0.0.1 alpha", "ff02::1 zulu"}, convertExtraHosts(source))) +} + +func TestConvertResourcesFull(t *testing.T) { + source := composetypes.Resources{ + Limits: &composetypes.ResourceLimit{ + NanoCPUs: "0.003", + MemoryBytes: composetypes.UnitBytes(300000000), + }, + Reservations: &composetypes.Resource{ + NanoCPUs: "0.002", + MemoryBytes: composetypes.UnitBytes(200000000), + }, + } + resources, err := convertResources(source) + assert.NilError(t, err) + + expected := &swarm.ResourceRequirements{ + Limits: &swarm.Limit{ + NanoCPUs: 3000000, + MemoryBytes: 300000000, + }, + Reservations: &swarm.Resources{ + NanoCPUs: 2000000, + MemoryBytes: 200000000, + }, + } + assert.Check(t, is.DeepEqual(expected, resources)) +} + +func TestConvertResourcesOnlyMemory(t *testing.T) { + source := composetypes.Resources{ + Limits: &composetypes.ResourceLimit{ + MemoryBytes: composetypes.UnitBytes(300000000), + }, + Reservations: &composetypes.Resource{ + MemoryBytes: composetypes.UnitBytes(200000000), + }, + } + resources, err := convertResources(source) + assert.NilError(t, err) + + expected := &swarm.ResourceRequirements{ + Limits: &swarm.Limit{ + MemoryBytes: 300000000, + }, + Reservations: &swarm.Resources{ + MemoryBytes: 200000000, + }, + } + assert.Check(t, is.DeepEqual(expected, resources)) +} + +func TestConvertHealthcheck(t *testing.T) { + retries := uint64(10) + timeout := composetypes.Duration(30 * time.Second) + interval := composetypes.Duration(2 * time.Millisecond) + source := &composetypes.HealthCheckConfig{ + Test: []string{"EXEC", "touch", "/foo"}, + Timeout: &timeout, + Interval: &interval, + Retries: &retries, + } + expected := &container.HealthConfig{ + Test: source.Test, + Timeout: time.Duration(timeout), + Interval: time.Duration(interval), + Retries: 10, + } + + healthcheck, err := convertHealthcheck(source) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, healthcheck)) +} + +func TestConvertHealthcheckDisable(t *testing.T) { + source := &composetypes.HealthCheckConfig{Disable: true} + expected := &container.HealthConfig{ + Test: []string{"NONE"}, + } + + healthcheck, err := convertHealthcheck(source) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, healthcheck)) +} + +func TestConvertHealthcheckDisableWithTest(t *testing.T) { + source := &composetypes.HealthCheckConfig{ + Disable: true, + Test: []string{"EXEC", "touch"}, + } + _, err := convertHealthcheck(source) + assert.Error(t, err, "test and disable can't be set at the same time") +} + +func TestConvertEndpointSpec(t *testing.T) { + source := []composetypes.ServicePortConfig{ + { + Protocol: "udp", + Target: 53, + Published: 1053, + Mode: "host", + }, + { + Target: 8080, + Published: 80, + }, + } + endpoint := convertEndpointSpec("vip", source) + + expected := swarm.EndpointSpec{ + Mode: swarm.ResolutionMode(strings.ToLower("vip")), + Ports: []swarm.PortConfig{ + { + TargetPort: 8080, + PublishedPort: 80, + }, + { + Protocol: "udp", + TargetPort: 53, + PublishedPort: 1053, + PublishMode: "host", + }, + }, + } + + assert.Check(t, is.DeepEqual(expected, *endpoint)) +} + +func TestConvertServiceNetworksOnlyDefault(t *testing.T) { + networkConfigs := networkMap{} + + configs, err := convertServiceNetworks( + nil, networkConfigs, NewNamespace("foo"), "service") + + expected := []swarm.NetworkAttachmentConfig{ + { + Target: "foo_default", + Aliases: []string{"service"}, + }, + } + + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, configs)) +} + +func TestConvertServiceNetworks(t *testing.T) { + networkConfigs := networkMap{ + "front": composetypes.NetworkConfig{ + External: composetypes.External{External: true}, + Name: "fronttier", + }, + "back": composetypes.NetworkConfig{}, + } + networks := map[string]*composetypes.ServiceNetworkConfig{ + "front": { + Aliases: []string{"something"}, + }, + "back": { + Aliases: []string{"other"}, + }, + } + + configs, err := convertServiceNetworks( + networks, networkConfigs, NewNamespace("foo"), "service") + + expected := []swarm.NetworkAttachmentConfig{ + { + Target: "foo_back", + Aliases: []string{"other", "service"}, + }, + { + Target: "fronttier", + Aliases: []string{"something", "service"}, + }, + } + + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, configs)) +} + +func TestConvertServiceNetworksCustomDefault(t *testing.T) { + networkConfigs := networkMap{ + "default": composetypes.NetworkConfig{ + External: composetypes.External{External: true}, + Name: "custom", + }, + } + networks := map[string]*composetypes.ServiceNetworkConfig{} + + configs, err := convertServiceNetworks( + networks, networkConfigs, NewNamespace("foo"), "service") + + expected := []swarm.NetworkAttachmentConfig{ + { + Target: "custom", + Aliases: []string{"service"}, + }, + } + + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, configs)) +} + +func TestConvertDNSConfigEmpty(t *testing.T) { + dnsConfig := convertDNSConfig(nil, nil) + assert.Check(t, is.DeepEqual((*swarm.DNSConfig)(nil), dnsConfig)) +} + +var ( + nameservers = []string{"8.8.8.8", "9.9.9.9"} + search = []string{"dc1.example.com", "dc2.example.com"} +) + +func TestConvertDNSConfigAll(t *testing.T) { + dnsConfig := convertDNSConfig(nameservers, search) + assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ + Nameservers: nameservers, + Search: search, + }, dnsConfig)) +} + +func TestConvertDNSConfigNameservers(t *testing.T) { + dnsConfig := convertDNSConfig(nameservers, nil) + assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ + Nameservers: nameservers, + Search: nil, + }, dnsConfig)) +} + +func TestConvertDNSConfigSearch(t *testing.T) { + dnsConfig := convertDNSConfig(nil, search) + assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ + Nameservers: nil, + Search: search, + }, dnsConfig)) +} + +func TestConvertCredentialSpec(t *testing.T) { + tests := []struct { + name string + in composetypes.CredentialSpecConfig + out *swarm.CredentialSpec + configs []*swarm.ConfigReference + expectedErr string + }{ + { + name: "empty", + }, + { + name: "config-and-file", + in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", File: "somefile.json"}, + expectedErr: `invalid credential spec: cannot specify both "Config" and "File"`, + }, + { + name: "config-and-registry", + in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", Registry: "testing"}, + expectedErr: `invalid credential spec: cannot specify both "Config" and "Registry"`, + }, + { + name: "file-and-registry", + in: composetypes.CredentialSpecConfig{File: "somefile.json", Registry: "testing"}, + expectedErr: `invalid credential spec: cannot specify both "File" and "Registry"`, + }, + { + name: "config-and-file-and-registry", + in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", File: "somefile.json", Registry: "testing"}, + expectedErr: `invalid credential spec: cannot specify both "Config", "File", and "Registry"`, + }, + { + name: "missing-config-reference", + in: composetypes.CredentialSpecConfig{Config: "missing"}, + expectedErr: "invalid credential spec: spec specifies config missing, but no such config can be found", + configs: []*swarm.ConfigReference{ + { + ConfigName: "someName", + ConfigID: "missing", + }, + }, + }, + { + name: "namespaced-config", + in: composetypes.CredentialSpecConfig{Config: "name"}, + configs: []*swarm.ConfigReference{ + { + ConfigName: "namespaced-config_name", + ConfigID: "someID", + }, + }, + out: &swarm.CredentialSpec{Config: "someID"}, + }, + { + name: "config", + in: composetypes.CredentialSpecConfig{Config: "someName"}, + configs: []*swarm.ConfigReference{ + { + ConfigName: "someOtherName", + ConfigID: "someOtherID", + }, { + ConfigName: "someName", + ConfigID: "someID", + }, + }, + out: &swarm.CredentialSpec{Config: "someID"}, + }, + { + name: "file", + in: composetypes.CredentialSpecConfig{File: "somefile.json"}, + out: &swarm.CredentialSpec{File: "somefile.json"}, + }, + { + name: "registry", + in: composetypes.CredentialSpecConfig{Registry: "testing"}, + out: &swarm.CredentialSpec{Registry: "testing"}, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + namespace := NewNamespace(tc.name) + swarmSpec, err := convertCredentialSpec(namespace, tc.in, tc.configs) + + if tc.expectedErr != "" { + assert.Error(t, err, tc.expectedErr) + } else { + assert.NilError(t, err) + } + assert.DeepEqual(t, swarmSpec, tc.out) + }) + } +} + +func TestConvertUpdateConfigOrder(t *testing.T) { + // test default behavior + updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{}) + assert.Check(t, is.Equal("", updateConfig.Order)) + + // test start-first + updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{ + Order: "start-first", + }) + assert.Check(t, is.Equal(updateConfig.Order, "start-first")) + + // test stop-first + updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{ + Order: "stop-first", + }) + assert.Check(t, is.Equal(updateConfig.Order, "stop-first")) +} + +func TestConvertFileObject(t *testing.T) { + namespace := NewNamespace("testing") + config := composetypes.FileReferenceConfig{ + Source: "source", + Target: "target", + UID: "user", + GID: "group", + Mode: uint32Ptr(0644), + } + swarmRef, err := convertFileObject(namespace, config, lookupConfig) + assert.NilError(t, err) + + expected := swarmReferenceObject{ + Name: "testing_source", + File: swarmReferenceTarget{ + Name: config.Target, + UID: config.UID, + GID: config.GID, + Mode: os.FileMode(0644), + }, + } + assert.Check(t, is.DeepEqual(expected, swarmRef)) +} + +func lookupConfig(key string) (composetypes.FileObjectConfig, error) { + if key != "source" { + return composetypes.FileObjectConfig{}, errors.New("bad key") + } + return composetypes.FileObjectConfig{}, nil +} + +func TestConvertFileObjectDefaults(t *testing.T) { + namespace := NewNamespace("testing") + config := composetypes.FileReferenceConfig{Source: "source"} + swarmRef, err := convertFileObject(namespace, config, lookupConfig) + assert.NilError(t, err) + + expected := swarmReferenceObject{ + Name: "testing_source", + File: swarmReferenceTarget{ + Name: config.Source, + UID: "0", + GID: "0", + Mode: os.FileMode(0444), + }, + } + assert.Check(t, is.DeepEqual(expected, swarmRef)) +} + +func TestServiceConvertsIsolation(t *testing.T) { + src := composetypes.ServiceConfig{ + Isolation: "hyperv", + } + result, err := Service("1.35", Namespace{name: "foo"}, src, nil, nil, nil, nil) + assert.NilError(t, err) + assert.Check(t, is.Equal(container.IsolationHyperV, result.TaskTemplate.ContainerSpec.Isolation)) +} + +func TestConvertServiceSecrets(t *testing.T) { + namespace := Namespace{name: "foo"} + secrets := []composetypes.ServiceSecretConfig{ + {Source: "foo_secret"}, + {Source: "bar_secret"}, + } + secretSpecs := map[string]composetypes.SecretConfig{ + "foo_secret": { + Name: "foo_secret", + }, + "bar_secret": { + Name: "bar_secret", + }, + } + client := &fakeClient{ + secretListFunc: func(opts types.SecretListOptions) ([]swarm.Secret, error) { + assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_secret")) + assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_secret")) + return []swarm.Secret{ + {Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo_secret"}}}, + {Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "bar_secret"}}}, + }, nil + }, + } + refs, err := convertServiceSecrets(client, namespace, secrets, secretSpecs) + assert.NilError(t, err) + expected := []*swarm.SecretReference{ + { + SecretName: "bar_secret", + File: &swarm.SecretReferenceFileTarget{ + Name: "bar_secret", + UID: "0", + GID: "0", + Mode: 0444, + }, + }, + { + SecretName: "foo_secret", + File: &swarm.SecretReferenceFileTarget{ + Name: "foo_secret", + UID: "0", + GID: "0", + Mode: 0444, + }, + }, + } + assert.DeepEqual(t, expected, refs) +} + +func TestConvertServiceConfigs(t *testing.T) { + namespace := Namespace{name: "foo"} + service := composetypes.ServiceConfig{ + Configs: []composetypes.ServiceConfigObjConfig{ + {Source: "foo_config"}, + {Source: "bar_config"}, + }, + CredentialSpec: composetypes.CredentialSpecConfig{ + Config: "baz_config", + }, + } + configSpecs := map[string]composetypes.ConfigObjConfig{ + "foo_config": { + Name: "foo_config", + }, + "bar_config": { + Name: "bar_config", + }, + "baz_config": { + Name: "baz_config", + }, + } + client := &fakeClient{ + configListFunc: func(opts types.ConfigListOptions) ([]swarm.Config, error) { + assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_config")) + assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_config")) + assert.Check(t, is.Contains(opts.Filters.Get("name"), "baz_config")) + return []swarm.Config{ + {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "foo_config"}}}, + {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "bar_config"}}}, + {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "baz_config"}}}, + }, nil + }, + } + refs, err := convertServiceConfigObjs(client, namespace, service, configSpecs) + assert.NilError(t, err) + expected := []*swarm.ConfigReference{ + { + ConfigName: "bar_config", + File: &swarm.ConfigReferenceFileTarget{ + Name: "bar_config", + UID: "0", + GID: "0", + Mode: 0444, + }, + }, + { + ConfigName: "baz_config", + Runtime: &swarm.ConfigReferenceRuntimeTarget{}, + }, + { + ConfigName: "foo_config", + File: &swarm.ConfigReferenceFileTarget{ + Name: "foo_config", + UID: "0", + GID: "0", + Mode: 0444, + }, + }, + } + assert.DeepEqual(t, expected, refs) +} + +type fakeClient struct { + client.Client + secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error) + configListFunc func(types.ConfigListOptions) ([]swarm.Config, error) +} + +func (c *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { + if c.secretListFunc != nil { + return c.secretListFunc(options) + } + return []swarm.Secret{}, nil +} + +func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) { + if c.configListFunc != nil { + return c.configListFunc(options) + } + return []swarm.Config{}, nil +} + +func TestConvertUpdateConfigParallelism(t *testing.T) { + parallel := uint64(4) + + // test default behavior + updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{}) + assert.Check(t, is.Equal(uint64(1), updateConfig.Parallelism)) + + // Non default value + updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{ + Parallelism: ¶llel, + }) + assert.Check(t, is.Equal(parallel, updateConfig.Parallelism)) +} + +func TestConvertServiceCapAddAndCapDrop(t *testing.T) { + tests := []struct { + title string + in, out composetypes.ServiceConfig + }{ + { + title: "default behavior", + }, + { + title: "some values", + in: composetypes.ServiceConfig{ + CapAdd: []string{"SYS_NICE", "CAP_NET_ADMIN"}, + CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"}, + }, + out: composetypes.ServiceConfig{ + CapAdd: []string{"CAP_NET_ADMIN", "CAP_SYS_NICE"}, + CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID"}, + }, + }, + { + title: "adding ALL capabilities", + in: composetypes.ServiceConfig{ + CapAdd: []string{"ALL", "CAP_NET_ADMIN"}, + CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"}, + }, + out: composetypes.ServiceConfig{ + CapAdd: []string{"ALL"}, + CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"}, + }, + }, + { + title: "dropping ALL capabilities", + in: composetypes.ServiceConfig{ + CapAdd: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"}, + CapDrop: []string{"ALL", "CAP_NET_ADMIN", "CAP_FOO"}, + }, + out: composetypes.ServiceConfig{ + CapAdd: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"}, + CapDrop: []string{"ALL"}, + }, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.title, func(t *testing.T) { + result, err := Service("1.41", Namespace{name: "foo"}, tc.in, nil, nil, nil, nil) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, tc.out.CapAdd)) + assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, tc.out.CapDrop)) + }) + } +} diff --git a/pkg/upstream/convert/volume_test.go b/pkg/upstream/convert/volume_test.go new file mode 100644 index 000000000..2b08357bd --- /dev/null +++ b/pkg/upstream/convert/volume_test.go @@ -0,0 +1,361 @@ +package convert + +import ( + "testing" + + composetypes "github.com/docker/cli/cli/compose/types" + "github.com/docker/docker/api/types/mount" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestConvertVolumeToMountAnonymousVolume(t *testing.T) { + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Target: "/foo/bar", + } + expected := mount.Mount{ + Type: mount.TypeVolume, + Target: "/foo/bar", + } + mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, mount)) +} + +func TestConvertVolumeToMountAnonymousBind(t *testing.T) { + config := composetypes.ServiceVolumeConfig{ + Type: "bind", + Target: "/foo/bar", + Bind: &composetypes.ServiceVolumeBind{ + Propagation: "slave", + }, + } + _, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) + assert.Error(t, err, "invalid bind source, source cannot be empty") +} + +func TestConvertVolumeToMountUnapprovedType(t *testing.T) { + config := composetypes.ServiceVolumeConfig{ + Type: "foo", + Target: "/foo/bar", + } + _, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) + assert.Error(t, err, "volume type must be volume, bind, tmpfs or npipe") +} + +func TestConvertVolumeToMountConflictingOptionsBindInVolume(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "foo", + Target: "/target", + Bind: &composetypes.ServiceVolumeBind{ + Propagation: "slave", + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.Error(t, err, "bind options are incompatible with type volume") +} + +func TestConvertVolumeToMountConflictingOptionsTmpfsInVolume(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "foo", + Target: "/target", + Tmpfs: &composetypes.ServiceVolumeTmpfs{ + Size: 1000, + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.Error(t, err, "tmpfs options are incompatible with type volume") +} + +func TestConvertVolumeToMountConflictingOptionsVolumeInBind(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "bind", + Source: "/foo", + Target: "/target", + Volume: &composetypes.ServiceVolumeVolume{ + NoCopy: true, + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.Error(t, err, "volume options are incompatible with type bind") +} + +func TestConvertVolumeToMountConflictingOptionsTmpfsInBind(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "bind", + Source: "/foo", + Target: "/target", + Tmpfs: &composetypes.ServiceVolumeTmpfs{ + Size: 1000, + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.Error(t, err, "tmpfs options are incompatible with type bind") +} + +func TestConvertVolumeToMountConflictingOptionsBindInTmpfs(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "tmpfs", + Target: "/target", + Bind: &composetypes.ServiceVolumeBind{ + Propagation: "slave", + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.Error(t, err, "bind options are incompatible with type tmpfs") +} + +func TestConvertVolumeToMountConflictingOptionsVolumeInTmpfs(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "tmpfs", + Target: "/target", + Volume: &composetypes.ServiceVolumeVolume{ + NoCopy: true, + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.Error(t, err, "volume options are incompatible with type tmpfs") +} + +func TestConvertVolumeToMountNamedVolume(t *testing.T) { + stackVolumes := volumes{ + "normal": composetypes.VolumeConfig{ + Driver: "glusterfs", + DriverOpts: map[string]string{ + "opt": "value", + }, + Labels: map[string]string{ + "something": "labeled", + }, + }, + } + namespace := NewNamespace("foo") + expected := mount.Mount{ + Type: mount.TypeVolume, + Source: "foo_normal", + Target: "/foo", + ReadOnly: true, + VolumeOptions: &mount.VolumeOptions{ + Labels: map[string]string{ + LabelNamespace: "foo", + "something": "labeled", + }, + DriverConfig: &mount.Driver{ + Name: "glusterfs", + Options: map[string]string{ + "opt": "value", + }, + }, + NoCopy: true, + }, + } + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "normal", + Target: "/foo", + ReadOnly: true, + Volume: &composetypes.ServiceVolumeVolume{ + NoCopy: true, + }, + } + mount, err := convertVolumeToMount(config, stackVolumes, namespace) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, mount)) +} + +func TestConvertVolumeToMountNamedVolumeWithNameCustomizd(t *testing.T) { + stackVolumes := volumes{ + "normal": composetypes.VolumeConfig{ + Name: "user_specified_name", + Driver: "vsphere", + DriverOpts: map[string]string{ + "opt": "value", + }, + Labels: map[string]string{ + "something": "labeled", + }, + }, + } + namespace := NewNamespace("foo") + expected := mount.Mount{ + Type: mount.TypeVolume, + Source: "user_specified_name", + Target: "/foo", + ReadOnly: true, + VolumeOptions: &mount.VolumeOptions{ + Labels: map[string]string{ + LabelNamespace: "foo", + "something": "labeled", + }, + DriverConfig: &mount.Driver{ + Name: "vsphere", + Options: map[string]string{ + "opt": "value", + }, + }, + NoCopy: true, + }, + } + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "normal", + Target: "/foo", + ReadOnly: true, + Volume: &composetypes.ServiceVolumeVolume{ + NoCopy: true, + }, + } + mount, err := convertVolumeToMount(config, stackVolumes, namespace) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, mount)) +} + +func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) { + stackVolumes := volumes{ + "outside": composetypes.VolumeConfig{ + Name: "special", + External: composetypes.External{External: true}, + }, + } + namespace := NewNamespace("foo") + expected := mount.Mount{ + Type: mount.TypeVolume, + Source: "special", + Target: "/foo", + VolumeOptions: &mount.VolumeOptions{NoCopy: false}, + } + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "outside", + Target: "/foo", + } + mount, err := convertVolumeToMount(config, stackVolumes, namespace) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, mount)) +} + +func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) { + stackVolumes := volumes{ + "outside": composetypes.VolumeConfig{ + Name: "special", + External: composetypes.External{External: true}, + }, + } + namespace := NewNamespace("foo") + expected := mount.Mount{ + Type: mount.TypeVolume, + Source: "special", + Target: "/foo", + VolumeOptions: &mount.VolumeOptions{ + NoCopy: true, + }, + } + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "outside", + Target: "/foo", + Volume: &composetypes.ServiceVolumeVolume{ + NoCopy: true, + }, + } + mount, err := convertVolumeToMount(config, stackVolumes, namespace) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, mount)) +} + +func TestConvertVolumeToMountBind(t *testing.T) { + stackVolumes := volumes{} + namespace := NewNamespace("foo") + expected := mount.Mount{ + Type: mount.TypeBind, + Source: "/bar", + Target: "/foo", + ReadOnly: true, + BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared}, + } + config := composetypes.ServiceVolumeConfig{ + Type: "bind", + Source: "/bar", + Target: "/foo", + ReadOnly: true, + Bind: &composetypes.ServiceVolumeBind{Propagation: "shared"}, + } + mount, err := convertVolumeToMount(config, stackVolumes, namespace) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, mount)) +} + +func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) { + namespace := NewNamespace("foo") + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "unknown", + Target: "/foo", + ReadOnly: true, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.Error(t, err, "undefined volume \"unknown\"") +} + +func TestConvertTmpfsToMountVolume(t *testing.T) { + config := composetypes.ServiceVolumeConfig{ + Type: "tmpfs", + Target: "/foo/bar", + Tmpfs: &composetypes.ServiceVolumeTmpfs{ + Size: 1000, + }, + } + expected := mount.Mount{ + Type: mount.TypeTmpfs, + Target: "/foo/bar", + TmpfsOptions: &mount.TmpfsOptions{SizeBytes: 1000}, + } + mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, mount)) +} + +func TestConvertTmpfsToMountVolumeWithSource(t *testing.T) { + config := composetypes.ServiceVolumeConfig{ + Type: "tmpfs", + Source: "/bar", + Target: "/foo/bar", + Tmpfs: &composetypes.ServiceVolumeTmpfs{ + Size: 1000, + }, + } + + _, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) + assert.Error(t, err, "invalid tmpfs source, source must be empty") +} + +func TestConvertVolumeToMountAnonymousNpipe(t *testing.T) { + config := composetypes.ServiceVolumeConfig{ + Type: "npipe", + Source: `\\.\pipe\foo`, + Target: `\\.\pipe\foo`, + } + expected := mount.Mount{ + Type: mount.TypeNamedPipe, + Source: `\\.\pipe\foo`, + Target: `\\.\pipe\foo`, + } + mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, mount)) +}