Add support for configs to compose format

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
Brian Goff 2017-05-15 11:19:32 -04:00
parent 90809f8fd9
commit e574286ba2
9 changed files with 287 additions and 62 deletions

View File

@ -79,6 +79,14 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption
return err return err
} }
configs, err := convert.Configs(namespace, config.Configs)
if err != nil {
return err
}
if err := createConfigs(ctx, dockerCli, namespace, configs); err != nil {
return err
}
services, err := convert.Services(namespace, config, dockerCli.Client()) services, err := convert.Services(namespace, config, dockerCli.Client())
if err != nil { if err != nil {
return err return err
@ -208,6 +216,33 @@ func createSecrets(
return nil return nil
} }
func createConfigs(
ctx context.Context,
dockerCli command.Cli,
namespace convert.Namespace,
configs []swarm.ConfigSpec,
) error {
client := dockerCli.Client()
for _, configSpec := range configs {
config, _, err := client.ConfigInspectWithRaw(ctx, configSpec.Name)
if err == nil {
// config already exists, then we update that
if err := client.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
return err
}
} else if apiclient.IsErrConfigNotFound(err) {
// config does not exist, then we create a new one.
if _, err := client.ConfigCreate(ctx, configSpec); err != nil {
return err
}
} else {
return err
}
}
return nil
}
func createNetworks( func createNetworks(
ctx context.Context, ctx context.Context,
dockerCli command.Cli, dockerCli command.Cli,

View File

@ -116,3 +116,27 @@ func Secrets(namespace Namespace, secrets map[string]composetypes.SecretConfig)
} }
return result, nil return result, nil
} }
// Configs converts config objects from the Compose type to the engine API type
func Configs(namespace Namespace, configs map[string]composetypes.ConfigObjConfig) ([]swarm.ConfigSpec, error) {
result := []swarm.ConfigSpec{}
for name, config := range configs {
if config.External.External {
continue
}
data, err := ioutil.ReadFile(config.File)
if err != nil {
return nil, err
}
result = append(result, swarm.ConfigSpec{
Annotations: swarm.Annotations{
Name: namespace.Scope(name),
Labels: AddStackLabel(namespace, config.Labels),
},
Data: data,
})
}
return result, nil
}

View File

@ -133,3 +133,34 @@ func TestSecrets(t *testing.T) {
}, secret.Labels) }, secret.Labels)
assert.Equal(t, []byte(secretText), secret.Data) assert.Equal(t, []byte(secretText), secret.Data)
} }
func TestConfigs(t *testing.T) {
namespace := Namespace{name: "foo"}
configText := "this is the first config"
configFile := tempfile.NewTempFile(t, "convert-configs", configText)
defer configFile.Remove()
source := map[string]composetypes.ConfigObjConfig{
"one": {
File: configFile.Name(),
Labels: map[string]string{"monster": "mash"},
},
"ext": {
External: composetypes.External{
External: true,
},
},
}
specs, err := Configs(namespace, source)
assert.NoError(t, err)
require.Len(t, specs, 1)
config := specs[0]
assert.Equal(t, "foo_one", config.Name)
assert.Equal(t, map[string]string{
"monster": "mash",
LabelNamespace: "foo",
}, config.Labels)
assert.Equal(t, []byte(configText), config.Data)
}

View File

@ -37,7 +37,12 @@ func Services(
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name) return nil, errors.Wrapf(err, "service %s", service.Name)
} }
serviceSpec, err := convertService(client.ClientVersion(), namespace, service, networks, volumes, secrets) configs, err := convertServiceConfigObjs(client, namespace, service.Configs, config.Configs)
if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name)
}
serviceSpec, err := convertService(client.ClientVersion(), namespace, service, networks, volumes, secrets, configs)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name) return nil, errors.Wrapf(err, "service %s", service.Name)
} }
@ -54,6 +59,7 @@ func convertService(
networkConfigs map[string]composetypes.NetworkConfig, networkConfigs map[string]composetypes.NetworkConfig,
volumes map[string]composetypes.VolumeConfig, volumes map[string]composetypes.VolumeConfig,
secrets []*swarm.SecretReference, secrets []*swarm.SecretReference,
configs []*swarm.ConfigReference,
) (swarm.ServiceSpec, error) { ) (swarm.ServiceSpec, error) {
name := namespace.Scope(service.Name) name := namespace.Scope(service.Name)
@ -277,6 +283,57 @@ func convertServiceSecrets(
return servicecli.ParseSecrets(client, refs) return servicecli.ParseSecrets(client, refs)
} }
// TODO: fix configs API so that ConfigsAPIClient is not required here
func convertServiceConfigObjs(
client client.ConfigAPIClient,
namespace Namespace,
configs []composetypes.ServiceConfigObjConfig,
configSpecs map[string]composetypes.ConfigObjConfig,
) ([]*swarm.ConfigReference, error) {
refs := []*swarm.ConfigReference{}
for _, config := range configs {
target := config.Target
if target == "" {
target = config.Source
}
configSpec, exists := configSpecs[config.Source]
if !exists {
return nil, errors.Errorf("undefined config %q", config.Source)
}
source := namespace.Scope(config.Source)
if configSpec.External.External {
source = configSpec.External.Name
}
uid := config.UID
gid := config.GID
if uid == "" {
uid = "0"
}
if gid == "" {
gid = "0"
}
mode := config.Mode
if mode == nil {
mode = uint32Ptr(0444)
}
refs = append(refs, &swarm.ConfigReference{
File: &swarm.ConfigReferenceFileTarget{
Name: target,
UID: uid,
GID: gid,
Mode: os.FileMode(*mode),
},
ConfigName: source,
})
}
return servicecli.ParseConfigs(client, refs)
}
func uint32Ptr(value uint32) *uint32 { func uint32Ptr(value uint32) *uint32 {
return &value return &value
} }

View File

@ -66,69 +66,58 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
} }
cfg := types.Config{} cfg := types.Config{}
lookupEnv := func(k string) (string, bool) {
v, ok := configDetails.Environment[k]
return v, ok
}
if services, ok := configDict["services"]; ok {
servicesConfig, err := interpolation.Interpolate(services.(map[string]interface{}), "service", lookupEnv)
if err != nil {
return nil, err
}
servicesList, err := LoadServices(servicesConfig, configDetails.WorkingDir, lookupEnv) config, err := interpolateConfig(configDict, configDetails.LookupEnv)
if err != nil { if err != nil {
return nil, err return nil, err
}
cfg.Services = servicesList
} }
if networks, ok := configDict["networks"]; ok { cfg.Services, err = LoadServices(config["services"], configDetails.WorkingDir, configDetails.LookupEnv)
networksConfig, err := interpolation.Interpolate(networks.(map[string]interface{}), "network", lookupEnv) if err != nil {
if err != nil { return nil, err
return nil, err
}
networksMapping, err := LoadNetworks(networksConfig)
if err != nil {
return nil, err
}
cfg.Networks = networksMapping
} }
if volumes, ok := configDict["volumes"]; ok { cfg.Networks, err = LoadNetworks(config["networks"])
volumesConfig, err := interpolation.Interpolate(volumes.(map[string]interface{}), "volume", lookupEnv) if err != nil {
if err != nil { return nil, err
return nil, err
}
volumesMapping, err := LoadVolumes(volumesConfig)
if err != nil {
return nil, err
}
cfg.Volumes = volumesMapping
} }
if secrets, ok := configDict["secrets"]; ok { cfg.Volumes, err = LoadVolumes(config["volumes"])
secretsConfig, err := interpolation.Interpolate(secrets.(map[string]interface{}), "secret", lookupEnv) if err != nil {
if err != nil { return nil, err
return nil, err }
}
secretsMapping, err := LoadSecrets(secretsConfig, configDetails.WorkingDir) cfg.Secrets, err = LoadSecrets(config["secrets"], configDetails.WorkingDir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg.Secrets = secretsMapping cfg.Configs, err = LoadConfigObjs(config["configs"], configDetails.WorkingDir)
if err != nil {
return nil, err
} }
return &cfg, nil return &cfg, nil
} }
func interpolateConfig(configDict map[string]interface{}, lookupEnv template.Mapping) (map[string]map[string]interface{}, error) {
config := make(map[string]map[string]interface{})
for _, key := range []string{"services", "networks", "volumes", "secrets", "configs"} {
section, ok := configDict[key]
if !ok {
config[key] = make(map[string]interface{})
continue
}
var err error
config[key], err = interpolation.Interpolate(section.(map[string]interface{}), key, lookupEnv)
if err != nil {
return nil, err
}
}
return config, nil
}
// GetUnsupportedProperties returns the list of any unsupported properties that are // GetUnsupportedProperties returns the list of any unsupported properties that are
// used in the Compose files. // used in the Compose files.
func GetUnsupportedProperties(configDetails types.ConfigDetails) []string { func GetUnsupportedProperties(configDetails types.ConfigDetails) []string {
@ -241,7 +230,9 @@ func transformHook(
case reflect.TypeOf([]types.ServicePortConfig{}): case reflect.TypeOf([]types.ServicePortConfig{}):
return transformServicePort(data) return transformServicePort(data)
case reflect.TypeOf(types.ServiceSecretConfig{}): case reflect.TypeOf(types.ServiceSecretConfig{}):
return transformServiceSecret(data) return transformStringSourceMap(data)
case reflect.TypeOf(types.ServiceConfigObjConfig{}):
return transformStringSourceMap(data)
case reflect.TypeOf(types.StringOrNumberList{}): case reflect.TypeOf(types.StringOrNumberList{}):
return transformStringOrNumberList(data) return transformStringOrNumberList(data)
case reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): case reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}):
@ -482,6 +473,25 @@ func LoadSecrets(source map[string]interface{}, workingDir string) (map[string]t
return secrets, nil return secrets, nil
} }
// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadConfigObjs(source map[string]interface{}, workingDir string) (map[string]types.ConfigObjConfig, error) {
configs := make(map[string]types.ConfigObjConfig)
if err := transform(source, &configs); err != nil {
return configs, err
}
for name, config := range configs {
if config.External.External && config.External.Name == "" {
config.External.Name = name
configs[name] = config
}
if config.File != "" {
config.File = absPath(workingDir, config.File)
}
}
return configs, nil
}
func absPath(workingDir string, filepath string) string { func absPath(workingDir string, filepath string) string {
if path.IsAbs(filepath) { if path.IsAbs(filepath) {
return filepath return filepath
@ -544,7 +554,7 @@ func transformServicePort(data interface{}) (interface{}, error) {
} }
} }
func transformServiceSecret(data interface{}) (interface{}, error) { func transformStringSourceMap(data interface{}) (interface{}, error) {
switch value := data.(type) { switch value := data.(type) {
case string: case string:
return map[string]interface{}{"source": value}, nil return map[string]interface{}{"source": value}, nil

View File

@ -206,12 +206,17 @@ services:
image: busybox image: busybox
credential_spec: credential_spec:
File: "/foo" File: "/foo"
configs: [super]
configs:
super:
external: true
`) `)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
return return
} }
assert.Equal(t, len(actual.Services), 1) assert.Equal(t, len(actual.Services), 1)
assert.Equal(t, actual.Services[0].CredentialSpec.File, "/foo") assert.Equal(t, actual.Services[0].CredentialSpec.File, "/foo")
assert.Equal(t, len(actual.Configs), 1)
} }
func TestParseAndLoad(t *testing.T) { func TestParseAndLoad(t *testing.T) {

File diff suppressed because one or more lines are too long

View File

@ -50,6 +50,17 @@
} }
}, },
"additionalProperties": false "additionalProperties": false
},
"configs": {
"id": "#/properties/configs",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/config"
}
},
"additionalProperties": false
} }
}, },
@ -88,6 +99,24 @@
{"type": "array", "items": {"type": "string"}} {"type": "array", "items": {"type": "string"}}
] ]
}, },
"configs": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"source": {"type": "string"},
"target": {"type": "string"},
"uid": {"type": "string"},
"gid": {"type": "string"},
"mode": {"type": "number"}
}
}
]
}
},
"container_name": {"type": "string"}, "container_name": {"type": "string"},
"credential_spec": {"type": "object", "properties": { "credential_spec": {"type": "object", "properties": {
"file": {"type": "string"}, "file": {"type": "string"},
@ -443,6 +472,22 @@
"additionalProperties": false "additionalProperties": false
}, },
"config": {
"id": "#/definitions/config",
"type": "object",
"properties": {
"file": {"type": "string"},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
}
},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"string_or_list": { "string_or_list": {
"oneOf": [ "oneOf": [
{"type": "string"}, {"type": "string"},

View File

@ -60,12 +60,19 @@ type ConfigDetails struct {
Environment map[string]string Environment map[string]string
} }
// LookupEnv provides a lookup function for environment variables
func (cd ConfigDetails) LookupEnv(key string) (string, bool) {
v, ok := cd.Environment[key]
return v, ok
}
// Config is a full compose file configuration // Config is a full compose file configuration
type Config struct { type Config struct {
Services []ServiceConfig Services []ServiceConfig
Networks map[string]NetworkConfig Networks map[string]NetworkConfig
Volumes map[string]VolumeConfig Volumes map[string]VolumeConfig
Secrets map[string]SecretConfig Secrets map[string]SecretConfig
Configs map[string]ConfigObjConfig
} }
// ServiceConfig is the configuration of one service // ServiceConfig is the configuration of one service
@ -76,6 +83,7 @@ type ServiceConfig struct {
CapDrop []string `mapstructure:"cap_drop"` CapDrop []string `mapstructure:"cap_drop"`
CgroupParent string `mapstructure:"cgroup_parent"` CgroupParent string `mapstructure:"cgroup_parent"`
Command ShellCommand Command ShellCommand
Configs []ServiceConfigObjConfig
ContainerName string `mapstructure:"container_name"` ContainerName string `mapstructure:"container_name"`
CredentialSpec CredentialSpecConfig `mapstructure:"credential_spec"` CredentialSpec CredentialSpecConfig `mapstructure:"credential_spec"`
DependsOn []string `mapstructure:"depends_on"` DependsOn []string `mapstructure:"depends_on"`
@ -252,8 +260,7 @@ type ServiceVolumeVolume struct {
NoCopy bool `mapstructure:"nocopy"` NoCopy bool `mapstructure:"nocopy"`
} }
// ServiceSecretConfig is the secret configuration for a service type fileReferenceConfig struct {
type ServiceSecretConfig struct {
Source string Source string
Target string Target string
UID string UID string
@ -261,6 +268,12 @@ type ServiceSecretConfig struct {
Mode *uint32 Mode *uint32
} }
// ServiceConfigObjConfig is the config obj configuration for a service
type ServiceConfigObjConfig fileReferenceConfig
// ServiceSecretConfig is the secret configuration for a service
type ServiceSecretConfig fileReferenceConfig
// UlimitsConfig the ulimit configuration // UlimitsConfig the ulimit configuration
type UlimitsConfig struct { type UlimitsConfig struct {
Single int Single int
@ -305,15 +318,20 @@ type External struct {
External bool External bool
} }
// SecretConfig for a secret
type SecretConfig struct {
File string
External External
Labels Labels
}
// CredentialSpecConfig for credential spec on Windows // CredentialSpecConfig for credential spec on Windows
type CredentialSpecConfig struct { type CredentialSpecConfig struct {
File string File string
Registry string Registry string
} }
type fileObjectConfig struct {
File string
External External
Labels Labels
}
// SecretConfig for a secret
type SecretConfig fileObjectConfig
// ConfigObjConfig is the config for the swarm "Config" object
type ConfigObjConfig fileObjectConfig