From e7788d6f9a61d67566518efe11e394c414a21173 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 29 Aug 2018 14:29:39 -0700 Subject: [PATCH] Allow marshalling of Compose config to JSON Signed-off-by: Joffrey F --- cli/command/stack/kubernetes/convert.go | 6 +- cli/command/stack/kubernetes/warnings_test.go | 6 +- cli/compose/convert/service.go | 17 +- cli/compose/convert/service_test.go | 8 +- cli/compose/loader/full-struct_test.go | 558 +++++++++++++++++- cli/compose/loader/loader.go | 15 + cli/compose/loader/loader_test.go | 11 +- cli/compose/loader/types_test.go | 17 + cli/compose/types/types.go | 401 ++++++++----- 9 files changed, 852 insertions(+), 187 deletions(-) diff --git a/cli/command/stack/kubernetes/convert.go b/cli/command/stack/kubernetes/convert.go index b122090fb6..6c842df2b7 100644 --- a/cli/command/stack/kubernetes/convert.go +++ b/cli/command/stack/kubernetes/convert.go @@ -222,7 +222,7 @@ func fromComposeServiceConfig(s composeTypes.ServiceConfig) v1beta2.ServiceConfi ReadOnly: s.ReadOnly, Secrets: fromComposeServiceSecrets(s.Secrets), StdinOpen: s.StdinOpen, - StopGracePeriod: s.StopGracePeriod, + StopGracePeriod: composetypes.ConvertDurationPtr(s.StopGracePeriod), Tmpfs: s.Tmpfs, Tty: s.Tty, User: userID, @@ -285,8 +285,8 @@ func fromComposeHealthcheck(h *composeTypes.HealthCheckConfig) *v1beta2.HealthCh } return &v1beta2.HealthCheckConfig{ Test: h.Test, - Timeout: h.Timeout, - Interval: h.Interval, + Timeout: composetypes.ConvertDurationPtr(h.Timeout), + Interval: composetypes.ConvertDurationPtr(h.Interval), Retries: h.Retries, } } diff --git a/cli/command/stack/kubernetes/warnings_test.go b/cli/command/stack/kubernetes/warnings_test.go index bdb7bf9de7..111fa7bd5e 100644 --- a/cli/command/stack/kubernetes/warnings_test.go +++ b/cli/command/stack/kubernetes/warnings_test.go @@ -10,7 +10,7 @@ import ( ) func TestWarnings(t *testing.T) { - duration := 5 * time.Second + duration := composetypes.Duration(5 * time.Second) attempts := uint64(3) config := &composetypes.Config{ Version: "3.4", @@ -26,9 +26,9 @@ func TestWarnings(t *testing.T) { DependsOn: []string{"ignored"}, Deploy: composetypes.DeployConfig{ UpdateConfig: &composetypes.UpdateConfig{ - Delay: 5 * time.Second, + Delay: composetypes.Duration(5 * time.Second), FailureAction: "rollback", - Monitor: 10 * time.Second, + Monitor: composetypes.Duration(10 * time.Second), MaxFailureRatio: 0.5, }, RestartPolicy: &composetypes.RestartPolicy{ diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 06d9dea9ae..d2bfcbaa55 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -141,7 +141,7 @@ func Service( Dir: service.WorkingDir, User: service.User, Mounts: mounts, - StopGracePeriod: service.StopGracePeriod, + StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod), StopSignal: service.StopSignal, TTY: service.Tty, OpenStdin: service.StdinOpen, @@ -414,13 +414,13 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container } if healthcheck.Timeout != nil { - timeout = *healthcheck.Timeout + timeout = time.Duration(*healthcheck.Timeout) } if healthcheck.Interval != nil { - interval = *healthcheck.Interval + interval = time.Duration(*healthcheck.Interval) } if healthcheck.StartPeriod != nil { - startPeriod = *healthcheck.StartPeriod + startPeriod = time.Duration(*healthcheck.StartPeriod) } if healthcheck.Retries != nil { retries = int(*healthcheck.Retries) @@ -458,11 +458,12 @@ func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (* return nil, errors.Errorf("unknown restart policy: %s", restart) } } + return &swarm.RestartPolicy{ Condition: swarm.RestartPolicyCondition(source.Condition), - Delay: source.Delay, + Delay: composetypes.ConvertDurationPtr(source.Delay), MaxAttempts: source.MaxAttempts, - Window: source.Window, + Window: composetypes.ConvertDurationPtr(source.Window), }, nil } @@ -476,9 +477,9 @@ func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig } return &swarm.UpdateConfig{ Parallelism: parallel, - Delay: source.Delay, + Delay: time.Duration(source.Delay), FailureAction: source.FailureAction, - Monitor: source.Monitor, + Monitor: time.Duration(source.Monitor), MaxFailureRatio: source.MaxFailureRatio, Order: source.Order, } diff --git a/cli/compose/convert/service_test.go b/cli/compose/convert/service_test.go index be8e24b634..d281e0e452 100644 --- a/cli/compose/convert/service_test.go +++ b/cli/compose/convert/service_test.go @@ -124,8 +124,8 @@ func TestConvertResourcesOnlyMemory(t *testing.T) { func TestConvertHealthcheck(t *testing.T) { retries := uint64(10) - timeout := 30 * time.Second - interval := 2 * time.Millisecond + timeout := composetypes.Duration(30 * time.Second) + interval := composetypes.Duration(2 * time.Millisecond) source := &composetypes.HealthCheckConfig{ Test: []string{"EXEC", "touch", "/foo"}, Timeout: &timeout, @@ -134,8 +134,8 @@ func TestConvertHealthcheck(t *testing.T) { } expected := &container.HealthConfig{ Test: source.Test, - Timeout: timeout, - Interval: interval, + Timeout: time.Duration(timeout), + Interval: time.Duration(interval), Retries: 10, } diff --git a/cli/compose/loader/full-struct_test.go b/cli/compose/loader/full-struct_test.go index 4ee7798f42..758557fcf4 100644 --- a/cli/compose/loader/full-struct_test.go +++ b/cli/compose/loader/full-struct_test.go @@ -65,17 +65,17 @@ func services(workingDir, homeDir string) []types.ServiceConfig { Labels: map[string]string{"FOO": "BAR"}, RollbackConfig: &types.UpdateConfig{ Parallelism: uint64Ptr(3), - Delay: time.Duration(10 * time.Second), + Delay: types.Duration(10 * time.Second), FailureAction: "continue", - Monitor: time.Duration(60 * time.Second), + Monitor: types.Duration(60 * time.Second), MaxFailureRatio: 0.3, Order: "start-first", }, UpdateConfig: &types.UpdateConfig{ Parallelism: uint64Ptr(3), - Delay: time.Duration(10 * time.Second), + Delay: types.Duration(10 * time.Second), FailureAction: "continue", - Monitor: time.Duration(60 * time.Second), + Monitor: types.Duration(60 * time.Second), MaxFailureRatio: 0.3, Order: "start-first", }, @@ -344,7 +344,7 @@ func services(workingDir, homeDir string) []types.ServiceConfig { }, StdinOpen: true, StopSignal: "SIGUSR1", - StopGracePeriod: durationPtr(time.Duration(20 * time.Second)), + StopGracePeriod: durationPtr(20 * time.Second), Tmpfs: []string{"/run", "/tmp"}, Tty: true, Ulimits: map[string]*types.UlimitsConfig{ @@ -887,3 +887,551 @@ x-nested: workingDir, workingDir) } + +func fullExampleJSON(workingDir string) string { + return fmt.Sprintf(`{ + "configs": { + "config1": { + "file": "%s/config_data", + "external": false, + "labels": { + "foo": "bar" + } + }, + "config2": { + "name": "my_config", + "external": true + }, + "config3": { + "name": "config3", + "external": true + }, + "config4": { + "name": "foo", + "file": "%s", + "external": false + } + }, + "networks": { + "external-network": { + "name": "external-network", + "ipam": {}, + "external": true + }, + "other-external-network": { + "name": "my-cool-network", + "ipam": {}, + "external": true + }, + "other-network": { + "driver": "overlay", + "driver_opts": { + "baz": "1", + "foo": "bar" + }, + "ipam": { + "driver": "overlay", + "config": [ + { + "subnet": "172.16.238.0/24" + }, + { + "subnet": "2001:3984:3989::/64" + } + ] + }, + "external": false, + "labels": { + "foo": "bar" + } + }, + "some-network": { + "ipam": {}, + "external": false + } + }, + "secrets": { + "secret1": { + "file": "%s/secret_data", + "external": false, + "labels": { + "foo": "bar" + } + }, + "secret2": { + "name": "my_secret", + "external": true + }, + "secret3": { + "name": "secret3", + "external": true + }, + "secret4": { + "name": "bar", + "file": "%s", + "external": false + } + }, + "services": { + "foo": { + "build": { + "context": "./dir", + "dockerfile": "Dockerfile", + "args": { + "foo": "bar" + }, + "labels": { + "FOO": "BAR" + }, + "cache_from": [ + "foo", + "bar" + ], + "network": "foo", + "target": "foo" + }, + "cap_add": [ + "ALL" + ], + "cap_drop": [ + "NET_ADMIN", + "SYS_ADMIN" + ], + "cgroup_parent": "m-executor-abcd", + "command": [ + "bundle", + "exec", + "thin", + "-p", + "3000" + ], + "configs": [ + { + "source": "config1" + }, + { + "source": "config2", + "target": "/my_config", + "uid": "103", + "gid": "103", + "mode": 288 + } + ], + "container_name": "my-web-container", + "credential_spec": {}, + "depends_on": [ + "db", + "redis" + ], + "deploy": { + "mode": "replicated", + "replicas": 6, + "labels": { + "FOO": "BAR" + }, + "update_config": { + "parallelism": 3, + "delay": "10s", + "failure_action": "continue", + "monitor": "1m0s", + "max_failure_ratio": 0.3, + "order": "start-first" + }, + "rollback_config": { + "parallelism": 3, + "delay": "10s", + "failure_action": "continue", + "monitor": "1m0s", + "max_failure_ratio": 0.3, + "order": "start-first" + }, + "resources": { + "limits": { + "cpus": "0.001", + "memory": "52428800" + }, + "reservations": { + "cpus": "0.0001", + "memory": "20971520", + "generic_resources": [ + { + "discrete_resource_spec": { + "kind": "gpu", + "value": 2 + } + }, + { + "discrete_resource_spec": { + "kind": "ssd", + "value": 1 + } + } + ] + } + }, + "restart_policy": { + "condition": "on-failure", + "delay": "5s", + "max_attempts": 3, + "window": "2m0s" + }, + "placement": { + "constraints": [ + "node=foo" + ], + "preferences": [ + { + "spread": "node.labels.az" + } + ] + }, + "endpoint_mode": "dnsrr" + }, + "devices": [ + "/dev/ttyUSB0:/dev/ttyUSB0" + ], + "dns": [ + "8.8.8.8", + "9.9.9.9" + ], + "dns_search": [ + "dc1.example.com", + "dc2.example.com" + ], + "domainname": "foo.com", + "entrypoint": [ + "/code/entrypoint.sh", + "-p", + "3000" + ], + "environment": { + "BAR": "bar_from_env_file_2", + "BAZ": "baz_from_service_def", + "FOO": "foo_from_env_file", + "QUX": "qux_from_environment" + }, + "env_file": [ + "./example1.env", + "./example2.env" + ], + "expose": [ + "3000", + "8000" + ], + "external_links": [ + "redis_1", + "project_db_1:mysql", + "project_db_1:postgresql" + ], + "extra_hosts": [ + "somehost:162.242.195.82", + "otherhost:50.31.209.229" + ], + "hostname": "foo", + "healthcheck": { + "test": [ + "CMD-SHELL", + "echo \"hello world\"" + ], + "timeout": "1s", + "interval": "10s", + "retries": 5, + "start_period": "15s" + }, + "image": "redis", + "ipc": "host", + "labels": { + "com.example.description": "Accounting webapp", + "com.example.empty-label": "", + "com.example.number": "42" + }, + "links": [ + "db", + "db:database", + "redis" + ], + "logging": { + "driver": "syslog", + "options": { + "syslog-address": "tcp://192.168.0.42:123" + } + }, + "mac_address": "02:42:ac:11:65:43", + "network_mode": "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b", + "networks": { + "other-network": { + "ipv4_address": "172.16.238.10", + "ipv6_address": "2001:3984:3989::10" + }, + "other-other-network": null, + "some-network": { + "aliases": [ + "alias1", + "alias3" + ] + } + }, + "pid": "host", + "ports": [ + { + "mode": "ingress", + "target": 3000, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 3001, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 3002, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 3003, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 3004, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 3005, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 8000, + "published": 8000, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 8080, + "published": 9090, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 8081, + "published": 9091, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 22, + "published": 49100, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 8001, + "published": 8001, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5000, + "published": 5000, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5001, + "published": 5001, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5002, + "published": 5002, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5003, + "published": 5003, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5004, + "published": 5004, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5005, + "published": 5005, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5006, + "published": 5006, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5007, + "published": 5007, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5008, + "published": 5008, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5009, + "published": 5009, + "protocol": "tcp" + }, + { + "mode": "ingress", + "target": 5010, + "published": 5010, + "protocol": "tcp" + } + ], + "privileged": true, + "read_only": true, + "restart": "always", + "secrets": [ + { + "source": "secret1" + }, + { + "source": "secret2", + "target": "my_secret", + "uid": "103", + "gid": "103", + "mode": 288 + } + ], + "security_opt": [ + "label=level:s0:c100,c200", + "label=type:svirt_apache_t" + ], + "stdin_open": true, + "stop_grace_period": "20s", + "stop_signal": "SIGUSR1", + "tmpfs": [ + "/run", + "/tmp" + ], + "tty": true, + "ulimits": { + "nofile": { + "soft": 20000, + "hard": 40000 + }, + "nproc": 65535 + }, + "user": "someone", + "volumes": [ + { + "type": "volume", + "target": "/var/lib/mysql" + }, + { + "type": "bind", + "source": "/opt/data", + "target": "/var/lib/mysql" + }, + { + "type": "bind", + "source": "/foo", + "target": "/code" + }, + { + "type": "bind", + "source": "%s", + "target": "/var/www/html" + }, + { + "type": "bind", + "source": "/bar/configs", + "target": "/etc/configs/", + "read_only": true + }, + { + "type": "volume", + "source": "datavolume", + "target": "/var/lib/mysql" + }, + { + "type": "bind", + "source": "%s", + "target": "/opt", + "consistency": "cached" + }, + { + "type": "tmpfs", + "target": "/opt", + "tmpfs": { + "size": 10000 + } + } + ], + "working_dir": "/code" + } + }, + "version": "3.7", + "volumes": { + "another-volume": { + "name": "user_specified_name", + "driver": "vsphere", + "driver_opts": { + "baz": "1", + "foo": "bar" + }, + "external": false + }, + "external-volume": { + "name": "external-volume", + "external": true + }, + "external-volume3": { + "name": "this-is-volume3", + "external": true + }, + "other-external-volume": { + "name": "my-cool-volume", + "external": true + }, + "other-volume": { + "driver": "flocker", + "driver_opts": { + "baz": "1", + "foo": "bar" + }, + "external": false, + "labels": { + "foo": "bar" + } + }, + "some-volume": { + "external": false + } + }, + "x-bar": "baz", + "x-foo": "bar", + "x-nested": { + "bar": "baz", + "foo": "bar" + } +}`, + workingDir, + workingDir, + workingDir, + workingDir, + filepath.Join(workingDir, "static"), + filepath.Join(workingDir, "opt")) +} diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index cea5d109c0..741f560f29 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -7,6 +7,7 @@ import ( "reflect" "sort" "strings" + "time" interp "github.com/docker/cli/cli/compose/interpolation" "github.com/docker/cli/cli/compose/schema" @@ -309,6 +310,7 @@ func createTransformHook(additionalTransformers ...Transformer) mapstructure.Dec reflect.TypeOf(types.HostsList{}): transformListOrMappingFunc(":", false), reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig, reflect.TypeOf(types.BuildConfig{}): transformBuildConfig, + reflect.TypeOf(types.Duration(0)): transformStringToDuration, } for _, transformer := range additionalTransformers { @@ -854,6 +856,19 @@ func transformSize(value interface{}) (interface{}, error) { panic(errors.Errorf("invalid type for size %T", value)) } +func transformStringToDuration(value interface{}) (interface{}, error) { + switch value := value.(type) { + case string: + d, err := time.ParseDuration(value) + if err != nil { + return value, err + } + return types.Duration(d), nil + default: + return value, errors.Errorf("invalid type %T for duration", value) + } +} + func toServicePortConfigs(value string) ([]interface{}, error) { var portConfigs []interface{} diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index c2c84bbcc1..64dc55f8bc 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -806,15 +806,16 @@ volumes: external_volume: name: user_specified_name external: - name: external_name + name: external_name `) assert.ErrorContains(t, err, "volume.external.name and volume.name conflict; only use volume.name") assert.ErrorContains(t, err, "external_volume") } -func durationPtr(value time.Duration) *time.Duration { - return &value +func durationPtr(value time.Duration) *types.Duration { + result := types.Duration(value) + return &result } func uint64Ptr(value uint64) *uint64 { @@ -1281,7 +1282,7 @@ secrets: external_secret: name: user_specified_name external: - name: external_name + name: external_name `) assert.ErrorContains(t, err, "secret.external.name and secret.name conflict; only use secret.name") @@ -1368,7 +1369,7 @@ networks: foo: name: user_specified_name external: - name: external_name + name: external_name `) assert.ErrorContains(t, err, "network.external.name and network.name conflict; only use network.name") diff --git a/cli/compose/loader/types_test.go b/cli/compose/loader/types_test.go index 0d604d8cea..32a92db3f8 100644 --- a/cli/compose/loader/types_test.go +++ b/cli/compose/loader/types_test.go @@ -1,6 +1,7 @@ package loader import ( + "encoding/json" "testing" yaml "gopkg.in/yaml.v2" @@ -24,3 +25,19 @@ func TestMarshallConfig(t *testing.T) { _, err = Load(buildConfigDetails(dict, map[string]string{})) assert.NilError(t, err) } + +func TestJSONMarshallConfig(t *testing.T) { + workingDir := "/foo" + homeDir := "/bar" + cfg := fullExampleConfig(workingDir, homeDir) + expected := fullExampleJSON(workingDir) + + actual, err := json.MarshalIndent(cfg, "", " ") + assert.NilError(t, err) + assert.Check(t, is.Equal(expected, string(actual))) + + dict, err := ParseYAML([]byte(expected)) + assert.NilError(t, err) + _, err = Load(buildConfigDetails(dict, map[string]string{})) + assert.NilError(t, err) +} diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index 0f60fd9ec9..e427cdba42 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -1,6 +1,7 @@ package types import ( + "encoding/json" "fmt" "time" ) @@ -62,6 +63,32 @@ type ConfigDetails struct { Environment map[string]string } +// Duration is a thin wrapper around time.Duration with improved JSON marshalling +type Duration time.Duration + +func (d Duration) String() string { + return time.Duration(d).String() +} + +// ConvertDurationPtr converts a typedefined Duration pointer to a time.Duration pointer with the same value. +func ConvertDurationPtr(d *Duration) *time.Duration { + if d == nil { + return nil + } + res := time.Duration(*d) + return &res +} + +// MarshalJSON makes Duration implement json.Marshaler +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +// MarshalYAML makes Duration implement yaml.Marshaler +func (d Duration) MarshalYAML() (interface{}, error) { + return d.String(), nil +} + // LookupEnv provides a lookup function for environment variables func (cd ConfigDetails) LookupEnv(key string) (string, bool) { v, ok := cd.Environment[key] @@ -70,14 +97,39 @@ func (cd ConfigDetails) LookupEnv(key string) (string, bool) { // Config is a full compose file configuration type Config struct { - Filename string `yaml:"-"` - Version string - Services Services - Networks map[string]NetworkConfig `yaml:",omitempty"` - Volumes map[string]VolumeConfig `yaml:",omitempty"` - Secrets map[string]SecretConfig `yaml:",omitempty"` - Configs map[string]ConfigObjConfig `yaml:",omitempty"` - Extras map[string]interface{} `yaml:",inline"` + Filename string `yaml:"-" json:"-"` + Version string `json:"version"` + Services Services `json:"services"` + Networks map[string]NetworkConfig `yaml:",omitempty" json:"networks,omitempty"` + Volumes map[string]VolumeConfig `yaml:",omitempty" json:"volumes,omitempty"` + Secrets map[string]SecretConfig `yaml:",omitempty" json:"secrets,omitempty"` + Configs map[string]ConfigObjConfig `yaml:",omitempty" json:"configs,omitempty"` + Extras map[string]interface{} `yaml:",inline", json:"-"` +} + +// MarshalJSON makes Config implement json.Marshaler +func (c Config) MarshalJSON() ([]byte, error) { + m := map[string]interface{}{ + "version": c.Version, + "services": c.Services, + } + + if len(c.Networks) > 0 { + m["networks"] = c.Networks + } + if len(c.Volumes) > 0 { + m["volumes"] = c.Volumes + } + if len(c.Secrets) > 0 { + m["secrets"] = c.Secrets + } + if len(c.Configs) > 0 { + m["configs"] = c.Configs + } + for k, v := range c.Extras { + m[k] = v + } + return json.Marshal(m) } // Services is a list of ServiceConfig @@ -92,75 +144,84 @@ func (s Services) MarshalYAML() (interface{}, error) { return services, nil } +// MarshalJSON makes Services implement json.Marshaler +func (s Services) MarshalJSON() ([]byte, error) { + data, err := s.MarshalYAML() + if err != nil { + return nil, err + } + return json.MarshalIndent(data, "", " ") +} + // ServiceConfig is the configuration of one service type ServiceConfig struct { - Name string `yaml:"-"` + Name string `yaml:"-" json:"-"` - Build BuildConfig `yaml:",omitempty"` - CapAdd []string `mapstructure:"cap_add" yaml:"cap_add,omitempty"` - CapDrop []string `mapstructure:"cap_drop" yaml:"cap_drop,omitempty"` - CgroupParent string `mapstructure:"cgroup_parent" yaml:"cgroup_parent,omitempty"` - Command ShellCommand `yaml:",omitempty"` - Configs []ServiceConfigObjConfig `yaml:",omitempty"` - ContainerName string `mapstructure:"container_name" yaml:"container_name,omitempty"` - CredentialSpec CredentialSpecConfig `mapstructure:"credential_spec" yaml:"credential_spec,omitempty"` - DependsOn []string `mapstructure:"depends_on" yaml:"depends_on,omitempty"` - Deploy DeployConfig `yaml:",omitempty"` - Devices []string `yaml:",omitempty"` - DNS StringList `yaml:",omitempty"` - DNSSearch StringList `mapstructure:"dns_search" yaml:"dns_search,omitempty"` - DomainName string `mapstructure:"domainname" yaml:"domainname,omitempty"` - Entrypoint ShellCommand `yaml:",omitempty"` - Environment MappingWithEquals `yaml:",omitempty"` - EnvFile StringList `mapstructure:"env_file" yaml:"env_file,omitempty"` - Expose StringOrNumberList `yaml:",omitempty"` - ExternalLinks []string `mapstructure:"external_links" yaml:"external_links,omitempty"` - ExtraHosts HostsList `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty"` - Hostname string `yaml:",omitempty"` - HealthCheck *HealthCheckConfig `yaml:",omitempty"` - Image string `yaml:",omitempty"` - Init *bool `yaml:",omitempty"` - Ipc string `yaml:",omitempty"` - Isolation string `mapstructure:"isolation" yaml:"isolation,omitempty"` - Labels Labels `yaml:",omitempty"` - Links []string `yaml:",omitempty"` - Logging *LoggingConfig `yaml:",omitempty"` - MacAddress string `mapstructure:"mac_address" yaml:"mac_address,omitempty"` - NetworkMode string `mapstructure:"network_mode" yaml:"network_mode,omitempty"` - Networks map[string]*ServiceNetworkConfig `yaml:",omitempty"` - Pid string `yaml:",omitempty"` - Ports []ServicePortConfig `yaml:",omitempty"` - Privileged bool `yaml:",omitempty"` - ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty"` - Restart string `yaml:",omitempty"` - Secrets []ServiceSecretConfig `yaml:",omitempty"` - SecurityOpt []string `mapstructure:"security_opt" yaml:"security_opt,omitempty"` - ShmSize string `mapstructure:"shm_size" yaml:"shm_size,omitempty"` - StdinOpen bool `mapstructure:"stdin_open" yaml:"stdin_open,omitempty"` - StopGracePeriod *time.Duration `mapstructure:"stop_grace_period" yaml:"stop_grace_period,omitempty"` - StopSignal string `mapstructure:"stop_signal" yaml:"stop_signal,omitempty"` - Sysctls StringList `yaml:",omitempty"` - Tmpfs StringList `yaml:",omitempty"` - Tty bool `mapstructure:"tty" yaml:"tty,omitempty"` - Ulimits map[string]*UlimitsConfig `yaml:",omitempty"` - User string `yaml:",omitempty"` - UserNSMode string `mapstructure:"userns_mode" yaml:"userns_mode,omitempty"` - Volumes []ServiceVolumeConfig `yaml:",omitempty"` - WorkingDir string `mapstructure:"working_dir" yaml:"working_dir,omitempty"` + Build BuildConfig `yaml:",omitempty" json:"build,omitempty"` + CapAdd []string `mapstructure:"cap_add" yaml:"cap_add,omitempty" json:"cap_add,omitempty"` + CapDrop []string `mapstructure:"cap_drop" yaml:"cap_drop,omitempty" json:"cap_drop,omitempty"` + CgroupParent string `mapstructure:"cgroup_parent" yaml:"cgroup_parent,omitempty" json:"cgroup_parent,omitempty"` + Command ShellCommand `yaml:",omitempty" json:"command,omitempty"` + Configs []ServiceConfigObjConfig `yaml:",omitempty" json:"configs,omitempty"` + ContainerName string `mapstructure:"container_name" yaml:"container_name,omitempty" json:"container_name,omitempty"` + CredentialSpec CredentialSpecConfig `mapstructure:"credential_spec" yaml:"credential_spec,omitempty" json:"credential_spec,omitempty"` + DependsOn []string `mapstructure:"depends_on" yaml:"depends_on,omitempty" json:"depends_on,omitempty"` + Deploy DeployConfig `yaml:",omitempty" json:"deploy,omitempty"` + Devices []string `yaml:",omitempty" json:"devices,omitempty"` + DNS StringList `yaml:",omitempty" json:"dns,omitempty"` + DNSSearch StringList `mapstructure:"dns_search" yaml:"dns_search,omitempty" json:"dns_search,omitempty"` + DomainName string `mapstructure:"domainname" yaml:"domainname,omitempty" json:"domainname,omitempty"` + Entrypoint ShellCommand `yaml:",omitempty" json:"entrypoint,omitempty"` + Environment MappingWithEquals `yaml:",omitempty" json:"environment,omitempty"` + EnvFile StringList `mapstructure:"env_file" yaml:"env_file,omitempty" json:"env_file,omitempty"` + Expose StringOrNumberList `yaml:",omitempty" json:"expose,omitempty"` + ExternalLinks []string `mapstructure:"external_links" yaml:"external_links,omitempty" json:"external_links,omitempty"` + ExtraHosts HostsList `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"` + Hostname string `yaml:",omitempty" json:"hostname,omitempty"` + HealthCheck *HealthCheckConfig `yaml:",omitempty" json:"healthcheck,omitempty"` + Image string `yaml:",omitempty" json:"image,omitempty"` + Init *bool `yaml:",omitempty" json:"init,omitempty"` + Ipc string `yaml:",omitempty" json:"ipc,omitempty"` + Isolation string `mapstructure:"isolation" yaml:"isolation,omitempty" json:"isolation,omitempty"` + Labels Labels `yaml:",omitempty" json:"labels,omitempty"` + Links []string `yaml:",omitempty" json:"links,omitempty"` + Logging *LoggingConfig `yaml:",omitempty" json:"logging,omitempty"` + MacAddress string `mapstructure:"mac_address" yaml:"mac_address,omitempty" json:"mac_address,omitempty"` + NetworkMode string `mapstructure:"network_mode" yaml:"network_mode,omitempty" json:"network_mode,omitempty"` + Networks map[string]*ServiceNetworkConfig `yaml:",omitempty" json:"networks,omitempty"` + Pid string `yaml:",omitempty" json:"pid,omitempty"` + Ports []ServicePortConfig `yaml:",omitempty" json:"ports,omitempty"` + Privileged bool `yaml:",omitempty" json:"privileged,omitempty"` + ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"` + Restart string `yaml:",omitempty" json:"restart,omitempty"` + Secrets []ServiceSecretConfig `yaml:",omitempty" json:"secrets,omitempty"` + SecurityOpt []string `mapstructure:"security_opt" yaml:"security_opt,omitempty" json:"security_opt,omitempty"` + ShmSize string `mapstructure:"shm_size" yaml:"shm_size,omitempty" json:"shm_size,omitempty"` + StdinOpen bool `mapstructure:"stdin_open" yaml:"stdin_open,omitempty" json:"stdin_open,omitempty"` + StopGracePeriod *Duration `mapstructure:"stop_grace_period" yaml:"stop_grace_period,omitempty" json:"stop_grace_period,omitempty"` + StopSignal string `mapstructure:"stop_signal" yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"` + Sysctls StringList `yaml:",omitempty" json:"sysctls,omitempty"` + Tmpfs StringList `yaml:",omitempty" json:"tmpfs,omitempty"` + Tty bool `mapstructure:"tty" yaml:"tty,omitempty" json:"tty,omitempty"` + Ulimits map[string]*UlimitsConfig `yaml:",omitempty" json:"ulimits,omitempty"` + User string `yaml:",omitempty" json:"user,omitempty"` + UserNSMode string `mapstructure:"userns_mode" yaml:"userns_mode,omitempty" json:"userns_mode,omitempty"` + Volumes []ServiceVolumeConfig `yaml:",omitempty" json:"volumes,omitempty"` + WorkingDir string `mapstructure:"working_dir" yaml:"working_dir,omitempty" json:"working_dir,omitempty"` - Extras map[string]interface{} `yaml:",inline"` + Extras map[string]interface{} `yaml:",inline" json:"-"` } // BuildConfig is a type for build // using the same format at libcompose: https://github.com/docker/libcompose/blob/master/yaml/build.go#L12 type BuildConfig struct { - Context string `yaml:",omitempty"` - Dockerfile string `yaml:",omitempty"` - Args MappingWithEquals `yaml:",omitempty"` - Labels Labels `yaml:",omitempty"` - CacheFrom StringList `mapstructure:"cache_from" yaml:"cache_from,omitempty"` - Network string `yaml:",omitempty"` - Target string `yaml:",omitempty"` + Context string `yaml:",omitempty" json:"context,omitempty"` + Dockerfile string `yaml:",omitempty" json:"dockerfile,omitempty"` + Args MappingWithEquals `yaml:",omitempty" json:"args,omitempty"` + Labels Labels `yaml:",omitempty" json:"labels,omitempty"` + CacheFrom StringList `mapstructure:"cache_from" yaml:"cache_from,omitempty" json:"cache_from,omitempty"` + Network string `yaml:",omitempty" json:"network,omitempty"` + Target string `yaml:",omitempty" json:"target,omitempty"` } // ShellCommand is a string or list of string args @@ -191,31 +252,31 @@ type HostsList []string // LoggingConfig the logging configuration for a service type LoggingConfig struct { - Driver string `yaml:",omitempty"` - Options map[string]string `yaml:",omitempty"` + Driver string `yaml:",omitempty" json:"driver,omitempty"` + Options map[string]string `yaml:",omitempty" json:"options,omitempty"` } // DeployConfig the deployment configuration for a service type DeployConfig struct { - Mode string `yaml:",omitempty"` - Replicas *uint64 `yaml:",omitempty"` - Labels Labels `yaml:",omitempty"` - UpdateConfig *UpdateConfig `mapstructure:"update_config" yaml:"update_config,omitempty"` - RollbackConfig *UpdateConfig `mapstructure:"rollback_config" yaml:"rollback_config,omitempty"` - Resources Resources `yaml:",omitempty"` - RestartPolicy *RestartPolicy `mapstructure:"restart_policy" yaml:"restart_policy,omitempty"` - Placement Placement `yaml:",omitempty"` - EndpointMode string `mapstructure:"endpoint_mode" yaml:"endpoint_mode,omitempty"` + Mode string `yaml:",omitempty" json:"mode,omitempty"` + Replicas *uint64 `yaml:",omitempty" json:"replicas,omitempty"` + Labels Labels `yaml:",omitempty" json:"labels,omitempty"` + UpdateConfig *UpdateConfig `mapstructure:"update_config" yaml:"update_config,omitempty" json:"update_config,omitempty"` + RollbackConfig *UpdateConfig `mapstructure:"rollback_config" yaml:"rollback_config,omitempty" json:"rollback_config,omitempty"` + Resources Resources `yaml:",omitempty" json:"resources,omitempty"` + RestartPolicy *RestartPolicy `mapstructure:"restart_policy" yaml:"restart_policy,omitempty" json:"restart_policy,omitempty"` + Placement Placement `yaml:",omitempty" json:"placement,omitempty"` + EndpointMode string `mapstructure:"endpoint_mode" yaml:"endpoint_mode,omitempty" json:"endpoint_mode,omitempty"` } // HealthCheckConfig the healthcheck configuration for a service type HealthCheckConfig struct { - Test HealthCheckTest `yaml:",omitempty"` - Timeout *time.Duration `yaml:",omitempty"` - Interval *time.Duration `yaml:",omitempty"` - Retries *uint64 `yaml:",omitempty"` - StartPeriod *time.Duration `mapstructure:"start_period" yaml:"start_period,omitempty"` - Disable bool `yaml:",omitempty"` + Test HealthCheckTest `yaml:",omitempty" json:"test,omitempty"` + Timeout *Duration `yaml:",omitempty" json:"timeout,omitempty"` + Interval *Duration `yaml:",omitempty" json:"interval,omitempty"` + Retries *uint64 `yaml:",omitempty" json:"retries,omitempty"` + StartPeriod *Duration `mapstructure:"start_period" yaml:"start_period,omitempty" json:"start_period,omitempty"` + Disable bool `yaml:",omitempty" json:"disable,omitempty"` } // HealthCheckTest is the command run to test the health of a service @@ -223,32 +284,32 @@ type HealthCheckTest []string // UpdateConfig the service update configuration type UpdateConfig struct { - Parallelism *uint64 `yaml:",omitempty"` - Delay time.Duration `yaml:",omitempty"` - FailureAction string `mapstructure:"failure_action" yaml:"failure_action,omitempty"` - Monitor time.Duration `yaml:",omitempty"` - MaxFailureRatio float32 `mapstructure:"max_failure_ratio" yaml:"max_failure_ratio,omitempty"` - Order string `yaml:",omitempty"` + Parallelism *uint64 `yaml:",omitempty" json:"parallelism,omitempty"` + Delay Duration `yaml:",omitempty" json:"delay,omitempty"` + FailureAction string `mapstructure:"failure_action" yaml:"failure_action,omitempty" json:"failure_action,omitempty"` + Monitor Duration `yaml:",omitempty" json:"monitor,omitempty"` + MaxFailureRatio float32 `mapstructure:"max_failure_ratio" yaml:"max_failure_ratio,omitempty" json:"max_failure_ratio,omitempty"` + Order string `yaml:",omitempty" json:"order,omitempty"` } // Resources the resource limits and reservations type Resources struct { - Limits *Resource `yaml:",omitempty"` - Reservations *Resource `yaml:",omitempty"` + Limits *Resource `yaml:",omitempty" json:"limits,omitempty"` + Reservations *Resource `yaml:",omitempty" json:"reservations,omitempty"` } // Resource is a resource to be limited or reserved type Resource struct { // TODO: types to convert from units and ratios - NanoCPUs string `mapstructure:"cpus" yaml:"cpus,omitempty"` - MemoryBytes UnitBytes `mapstructure:"memory" yaml:"memory,omitempty"` - GenericResources []GenericResource `mapstructure:"generic_resources" yaml:"generic_resources,omitempty"` + NanoCPUs string `mapstructure:"cpus" yaml:"cpus,omitempty" json:"cpus,omitempty"` + MemoryBytes UnitBytes `mapstructure:"memory" yaml:"memory,omitempty" json:"memory,omitempty"` + GenericResources []GenericResource `mapstructure:"generic_resources" yaml:"generic_resources,omitempty" json:"generic_resources,omitempty"` } // GenericResource represents a "user defined" resource which can // only be an integer (e.g: SSD=3) for a service type GenericResource struct { - DiscreteResourceSpec *DiscreteGenericResource `mapstructure:"discrete_resource_spec" yaml:"discrete_resource_spec,omitempty"` + DiscreteResourceSpec *DiscreteGenericResource `mapstructure:"discrete_resource_spec" yaml:"discrete_resource_spec,omitempty" json:"discrete_resource_spec,omitempty"` } // DiscreteGenericResource represents a "user defined" resource which is defined @@ -256,8 +317,8 @@ type GenericResource struct { // "Kind" is used to describe the Kind of a resource (e.g: "GPU", "FPGA", "SSD", ...) // Value is used to count the resource (SSD=5, HDD=3, ...) type DiscreteGenericResource struct { - Kind string - Value int64 + Kind string `json:"kind"` + Value int64 `json:"value"` } // UnitBytes is the bytes type @@ -268,74 +329,79 @@ func (u UnitBytes) MarshalYAML() (interface{}, error) { return fmt.Sprintf("%d", u), nil } +// MarshalJSON makes UnitBytes implement json.Marshaler +func (u UnitBytes) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%d"`, u)), nil +} + // RestartPolicy the service restart policy type RestartPolicy struct { - Condition string `yaml:",omitempty"` - Delay *time.Duration `yaml:",omitempty"` - MaxAttempts *uint64 `mapstructure:"max_attempts" yaml:"max_attempts,omitempty"` - Window *time.Duration `yaml:",omitempty"` + Condition string `yaml:",omitempty" json:"condition,omitempty"` + Delay *Duration `yaml:",omitempty" json:"delay,omitempty"` + MaxAttempts *uint64 `mapstructure:"max_attempts" yaml:"max_attempts,omitempty" json:"max_attempts,omitempty"` + Window *Duration `yaml:",omitempty" json:"window,omitempty"` } // Placement constraints for the service type Placement struct { - Constraints []string `yaml:",omitempty"` - Preferences []PlacementPreferences `yaml:",omitempty"` + Constraints []string `yaml:",omitempty" json:"constraints,omitempty"` + Preferences []PlacementPreferences `yaml:",omitempty" json:"preferences,omitempty"` } // PlacementPreferences is the preferences for a service placement type PlacementPreferences struct { - Spread string `yaml:",omitempty"` + Spread string `yaml:",omitempty" json:"spread,omitempty"` } // ServiceNetworkConfig is the network configuration for a service type ServiceNetworkConfig struct { - Aliases []string `yaml:",omitempty"` - Ipv4Address string `mapstructure:"ipv4_address" yaml:"ipv4_address,omitempty"` - Ipv6Address string `mapstructure:"ipv6_address" yaml:"ipv6_address,omitempty"` + Aliases []string `yaml:",omitempty" json:"aliases,omitempty"` + Ipv4Address string `mapstructure:"ipv4_address" yaml:"ipv4_address,omitempty" json:"ipv4_address,omitempty"` + Ipv6Address string `mapstructure:"ipv6_address" yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"` } // ServicePortConfig is the port configuration for a service type ServicePortConfig struct { - Mode string `yaml:",omitempty"` - Target uint32 `yaml:",omitempty"` - Published uint32 `yaml:",omitempty"` - Protocol string `yaml:",omitempty"` + Mode string `yaml:",omitempty" json:"mode,omitempty"` + Target uint32 `yaml:",omitempty" json:"target,omitempty"` + Published uint32 `yaml:",omitempty" json:"published,omitempty"` + Protocol string `yaml:",omitempty" json:"protocol,omitempty"` } // ServiceVolumeConfig are references to a volume used by a service type ServiceVolumeConfig struct { - Type string `yaml:",omitempty"` - Source string `yaml:",omitempty"` - Target string `yaml:",omitempty"` - ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty"` - Consistency string `yaml:",omitempty"` - Bind *ServiceVolumeBind `yaml:",omitempty"` - Volume *ServiceVolumeVolume `yaml:",omitempty"` - Tmpfs *ServiceVolumeTmpfs `yaml:",omitempty"` + Type string `yaml:",omitempty" json:"type,omitempty"` + Source string `yaml:",omitempty" json:"source,omitempty"` + Target string `yaml:",omitempty" json:"target,omitempty"` + ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"` + Consistency string `yaml:",omitempty" json:"consistency,omitempty"` + Bind *ServiceVolumeBind `yaml:",omitempty" json:"bind,omitempty"` + Volume *ServiceVolumeVolume `yaml:",omitempty" json:"volume,omitempty"` + Tmpfs *ServiceVolumeTmpfs `yaml:",omitempty" json:"tmpfs,omitempty"` } // ServiceVolumeBind are options for a service volume of type bind type ServiceVolumeBind struct { - Propagation string `yaml:",omitempty"` + Propagation string `yaml:",omitempty" json:"propagation,omitempty"` } // ServiceVolumeVolume are options for a service volume of type volume type ServiceVolumeVolume struct { - NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty"` + NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty" json:"nocopy,omitempty"` } // ServiceVolumeTmpfs are options for a service volume of type tmpfs type ServiceVolumeTmpfs struct { - Size int64 `yaml:",omitempty"` + Size int64 `yaml:",omitempty" json:"size,omitempty"` } // FileReferenceConfig for a reference to a swarm file object type FileReferenceConfig struct { - Source string `yaml:",omitempty"` - Target string `yaml:",omitempty"` - UID string `yaml:",omitempty"` - GID string `yaml:",omitempty"` - Mode *uint32 `yaml:",omitempty"` + Source string `yaml:",omitempty" json:"source,omitempty"` + Target string `yaml:",omitempty" json:"target,omitempty"` + UID string `yaml:",omitempty" json:"uid,omitempty"` + GID string `yaml:",omitempty" json:"gid,omitempty"` + Mode *uint32 `yaml:",omitempty" json:"mode,omitempty"` } // ServiceConfigObjConfig is the config obj configuration for a service @@ -346,9 +412,9 @@ type ServiceSecretConfig FileReferenceConfig // UlimitsConfig the ulimit configuration type UlimitsConfig struct { - Single int `yaml:",omitempty"` - Soft int `yaml:",omitempty"` - Hard int `yaml:",omitempty"` + Single int `yaml:",omitempty" json:"single,omitempty"` + Soft int `yaml:",omitempty" json:"soft,omitempty"` + Hard int `yaml:",omitempty" json:"hard,omitempty"` } // MarshalYAML makes UlimitsConfig implement yaml.Marshaller @@ -359,46 +425,55 @@ func (u *UlimitsConfig) MarshalYAML() (interface{}, error) { return u, nil } +// MarshalJSON makes UlimitsConfig implement json.Marshaller +func (u *UlimitsConfig) MarshalJSON() ([]byte, error) { + if u.Single != 0 { + return json.Marshal(u.Single) + } + // Pass as a value to avoid re-entering this method and use the default implementation + return json.Marshal(*u) +} + // NetworkConfig for a network type NetworkConfig struct { - Name string `yaml:",omitempty"` - Driver string `yaml:",omitempty"` - DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty"` - Ipam IPAMConfig `yaml:",omitempty"` - External External `yaml:",omitempty"` - Internal bool `yaml:",omitempty"` - Attachable bool `yaml:",omitempty"` - Labels Labels `yaml:",omitempty"` - Extras map[string]interface{} `yaml:",inline"` + Name string `yaml:",omitempty" json:"name,omitempty"` + Driver string `yaml:",omitempty" json:"driver,omitempty"` + DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"` + Ipam IPAMConfig `yaml:",omitempty" json:"ipam,omitempty"` + External External `yaml:",omitempty" json:"external,omitempty"` + Internal bool `yaml:",omitempty" json:"internal,omitempty"` + Attachable bool `yaml:",omitempty" json:"attachable,omitempty"` + Labels Labels `yaml:",omitempty" json:"labels,omitempty"` + Extras map[string]interface{} `yaml:",inline" json:"-"` } // IPAMConfig for a network type IPAMConfig struct { - Driver string `yaml:",omitempty"` - Config []*IPAMPool `yaml:",omitempty"` + Driver string `yaml:",omitempty" json:"driver,omitempty"` + Config []*IPAMPool `yaml:",omitempty" json:"config,omitempty"` } // IPAMPool for a network type IPAMPool struct { - Subnet string `yaml:",omitempty"` + Subnet string `yaml:",omitempty" json:"subnet,omitempty"` } // VolumeConfig for a volume type VolumeConfig struct { - Name string `yaml:",omitempty"` - Driver string `yaml:",omitempty"` - DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty"` - External External `yaml:",omitempty"` - Labels Labels `yaml:",omitempty"` - Extras map[string]interface{} `yaml:",inline"` + Name string `yaml:",omitempty" json:"name,omitempty"` + Driver string `yaml:",omitempty" json:"driver,omitempty"` + DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"` + External External `yaml:",omitempty" json:"external,omitempty"` + Labels Labels `yaml:",omitempty" json:"labels,omitempty"` + Extras map[string]interface{} `yaml:",inline" json:"-"` } // External identifies a Volume or Network as a reference to a resource that is // not managed, and should already exist. // External.name is deprecated and replaced by Volume.name type External struct { - Name string `yaml:",omitempty"` - External bool `yaml:",omitempty"` + Name string `yaml:",omitempty" json:"name,omitempty"` + External bool `yaml:",omitempty" json:"external,omitempty"` } // MarshalYAML makes External implement yaml.Marshaller @@ -409,19 +484,27 @@ func (e External) MarshalYAML() (interface{}, error) { return External{Name: e.Name}, nil } +// MarshalJSON makes External implement json.Marshaller +func (e External) MarshalJSON() ([]byte, error) { + if e.Name == "" { + return []byte(fmt.Sprintf("%v", e.External)), nil + } + return []byte(fmt.Sprintf(`{"name": %q}`, e.Name)), nil +} + // CredentialSpecConfig for credential spec on Windows type CredentialSpecConfig struct { - File string `yaml:",omitempty"` - Registry string `yaml:",omitempty"` + File string `yaml:",omitempty" json:"file,omitempty"` + Registry string `yaml:",omitempty" json:"registry,omitempty"` } // FileObjectConfig is a config type for a file used by a service type FileObjectConfig struct { - Name string `yaml:",omitempty"` - File string `yaml:",omitempty"` - External External `yaml:",omitempty"` - Labels Labels `yaml:",omitempty"` - Extras map[string]interface{} `yaml:",inline"` + Name string `yaml:",omitempty" json:"name,omitempty"` + File string `yaml:",omitempty" json:"file,omitempty"` + External External `yaml:",omitempty" json:"external,omitempty"` + Labels Labels `yaml:",omitempty" json:"labels,omitempty"` + Extras map[string]interface{} `yaml:",inline" json:"-"` } // SecretConfig for a secret