diff --git a/cli/command/service/create.go b/cli/command/service/create.go index ae359bd687..4c709eb3a6 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -169,6 +169,9 @@ func setConfigs(apiClient client.ConfigAPIClient, service *swarm.ServiceSpec, op for _, config := range configs { if config.ConfigName == cs.Config { service.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config = config.ConfigID + // we've found the right config, no need to keep iterating + // through the rest of them. + break } } } diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 0fe40bf3a7..2da8c6dc55 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -40,7 +40,7 @@ func Services( if err != nil { return nil, errors.Wrapf(err, "service %s", service.Name) } - configs, err := convertServiceConfigObjs(client, namespace, service.Configs, config.Configs) + configs, err := convertServiceConfigObjs(client, namespace, service, config.Configs) if err != nil { return nil, errors.Wrapf(err, "service %s", service.Name) } @@ -109,7 +109,9 @@ func Service( } var privileges swarm.Privileges - privileges.CredentialSpec, err = convertCredentialSpec(service.CredentialSpec) + privileges.CredentialSpec, err = convertCredentialSpec( + namespace, service.CredentialSpec, configs, + ) if err != nil { return swarm.ServiceSpec{}, err } @@ -286,11 +288,17 @@ func convertServiceSecrets( return secrs, err } +// convertServiceConfigObjs takes an API client, a namespace, a ServiceConfig, +// and a set of compose Config specs, and creates the swarm ConfigReferences +// required by the serivce. Unlike convertServiceSecrets, this takes the whole +// ServiceConfig, because some Configs may be needed as a result of other +// fields (like CredentialSpecs). +// // TODO: fix configs API so that ConfigsAPIClient is not required here func convertServiceConfigObjs( client client.ConfigAPIClient, namespace Namespace, - configs []composetypes.ServiceConfigObjConfig, + service composetypes.ServiceConfig, configSpecs map[string]composetypes.ConfigObjConfig, ) ([]*swarm.ConfigReference, error) { refs := []*swarm.ConfigReference{} @@ -302,7 +310,7 @@ func convertServiceConfigObjs( } return composetypes.FileObjectConfig(configSpec), nil } - for _, config := range configs { + for _, config := range service.Configs { obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup) if err != nil { return nil, err @@ -315,6 +323,38 @@ func convertServiceConfigObjs( }) } + // finally, after converting all of the file objects, create any + // Runtime-type configs that are needed. these are configs that are not + // mounted into the container, but are used in some other way by the + // container runtime. Currently, this only means CredentialSpecs, but in + // the future it may be used for other fields + + // grab the CredentialSpec out of the Service + credSpec := service.CredentialSpec + // if the credSpec uses a config, then we should grab the config name, and + // create a config reference for it. A File or Registry-type CredentialSpec + // does not need this operation. + if credSpec.Config != "" { + // look up the config in the configSpecs. + obj, err := lookup(credSpec.Config) + if err != nil { + return nil, err + } + + // get the actual correct name. + name := namespace.Scope(credSpec.Config) + if obj.Name != "" { + name = obj.Name + } + + // now append a Runtime-type config. + refs = append(refs, &swarm.ConfigReference{ + ConfigName: name, + Runtime: &swarm.ConfigReferenceRuntimeTarget{}, + }) + + } + confs, err := servicecli.ParseConfigs(client, refs) if err != nil { return nil, err @@ -342,11 +382,6 @@ func convertFileObject( config composetypes.FileReferenceConfig, lookup func(key string) (composetypes.FileObjectConfig, error), ) (swarmReferenceObject, error) { - target := config.Target - if target == "" { - target = config.Source - } - obj, err := lookup(config.Source) if err != nil { return swarmReferenceObject{}, err @@ -357,6 +392,11 @@ func convertFileObject( source = obj.Name } + target := config.Target + if target == "" { + target = config.Source + } + uid := config.UID gid := config.GID if uid == "" { @@ -599,7 +639,7 @@ func convertDNSConfig(DNS []string, DNSSearch []string) (*swarm.DNSConfig, error return nil, nil } -func convertCredentialSpec(spec composetypes.CredentialSpecConfig) (*swarm.CredentialSpec, error) { +func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) { var o []string // Config was added in API v1.40 @@ -622,5 +662,23 @@ func convertCredentialSpec(spec composetypes.CredentialSpecConfig) (*swarm.Crede return nil, errors.Errorf("invalid credential spec: cannot specify both %s, and %s", strings.Join(o[:l-1], ", "), o[l-1]) } swarmCredSpec := swarm.CredentialSpec(spec) + // if we're using a swarm Config for the credential spec, over-write it + // here with the config ID + if swarmCredSpec.Config != "" { + for _, config := range refs { + if swarmCredSpec.Config == config.ConfigName { + swarmCredSpec.Config = config.ConfigID + return &swarmCredSpec, nil + } + } + // if none of the configs match, try namespacing + for _, config := range refs { + if namespace.Scope(swarmCredSpec.Config) == config.ConfigName { + swarmCredSpec.Config = config.ConfigID + return &swarmCredSpec, nil + } + } + return nil, errors.Errorf("invalid credential spec: spec specifies config %v, but no such config can be found", swarmCredSpec.Config) + } return &swarmCredSpec, nil } diff --git a/cli/compose/convert/service_test.go b/cli/compose/convert/service_test.go index f275fb874c..4205038667 100644 --- a/cli/compose/convert/service_test.go +++ b/cli/compose/convert/service_test.go @@ -318,6 +318,7 @@ func TestConvertCredentialSpec(t *testing.T) { name string in composetypes.CredentialSpecConfig out *swarm.CredentialSpec + configs []*swarm.ConfigReference expectedErr string }{ { @@ -343,10 +344,41 @@ func TestConvertCredentialSpec(t *testing.T) { 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: "0bt9dmxjvjiqermk6xrop3ekq"}, - out: &swarm.CredentialSpec{Config: "0bt9dmxjvjiqermk6xrop3ekq"}, + in: composetypes.CredentialSpecConfig{Config: "someName"}, + configs: []*swarm.ConfigReference{ + { + ConfigName: "someOtherName", + ConfigID: "someOtherID", + }, { + ConfigName: "someName", + ConfigID: "someID", + }, + }, + out: &swarm.CredentialSpec{Config: "someID"}, }, { name: "file", @@ -363,7 +395,8 @@ func TestConvertCredentialSpec(t *testing.T) { for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { - swarmSpec, err := convertCredentialSpec(tc.in) + namespace := NewNamespace(tc.name) + swarmSpec, err := convertCredentialSpec(namespace, tc.in, tc.configs) if tc.expectedErr != "" { assert.Error(t, err, tc.expectedErr) @@ -502,9 +535,14 @@ func TestConvertServiceSecrets(t *testing.T) { func TestConvertServiceConfigs(t *testing.T) { namespace := Namespace{name: "foo"} - configs := []composetypes.ServiceConfigObjConfig{ - {Source: "foo_config"}, - {Source: "bar_config"}, + 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": { @@ -513,18 +551,23 @@ func TestConvertServiceConfigs(t *testing.T) { "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, configs, configSpecs) + refs, err := convertServiceConfigObjs(client, namespace, service, configSpecs) assert.NilError(t, err) expected := []*swarm.ConfigReference{ { @@ -536,6 +579,10 @@ func TestConvertServiceConfigs(t *testing.T) { Mode: 0444, }, }, + { + ConfigName: "baz_config", + Runtime: &swarm.ConfigReferenceRuntimeTarget{}, + }, { ConfigName: "foo_config", File: &swarm.ConfigReferenceFileTarget{