diff --git a/loader/loader.go b/loader/loader.go index 3843dc56..1f62e7c8 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -313,6 +313,8 @@ func parseConfig(b []byte, opts *Options) (map[string]interface{}, error) { return yml, err } +const extensions = "#extensions" // Using # prefix, we prevent risk to conflict with an actual yaml key + func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interface{} { extras := map[string]interface{}{} for key, value := range dict { @@ -325,7 +327,7 @@ func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interfac } } if len(extras) > 0 { - dict["extensions"] = extras + dict[extensions] = extras } return dict } @@ -364,7 +366,7 @@ func loadSections(filename string, config map[string]interface{}, configDetails if err != nil { return nil, err } - extensions := getSection(config, "extensions") + extensions := getSection(config, extensions) if len(extensions) > 0 { cfg.Extensions = extensions } @@ -526,7 +528,7 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error { func LoadServices(filename string, servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, opts *Options) ([]types.ServiceConfig, error) { var services []types.ServiceConfig - x, ok := servicesDict["extensions"] + x, ok := servicesDict[extensions] if ok { // as a top-level attribute, "services" doesn't support extensions, and a service can be named `x-foo` for k, v := range x.(map[string]interface{}) { diff --git a/loader/loader_test.go b/loader/loader_test.go index 4199d377..ae0c1dbb 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -308,7 +308,7 @@ services: assert.Check(t, is.Len(actual.Services, 1)) service := actual.Services[0] assert.Check(t, is.Equal("busybox", service.Image)) - extras := map[string]interface{}{ + extras := types.Extensions{ "x-foo": "bar", } assert.Check(t, is.DeepEqual(extras, service.Extensions)) @@ -2215,3 +2215,18 @@ volumes: path := project.Volumes["data"].DriverOpts["device"] assert.Check(t, filepath.IsAbs(path)) } + +func TestLoadServiceExtension(t *testing.T) { + dict := ` +services: + extension: # this name should be allowed + image: web + x-foo: bar +` + configDetails := buildConfigDetails(dict, nil) + + project, err := Load(configDetails) + assert.NilError(t, err) + assert.Equal(t, project.Services[0].Name, "extension") + assert.Equal(t, project.Services[0].Extensions["x-foo"], "bar") +} diff --git a/types/project.go b/types/project.go index b4882ab4..fdbd261a 100644 --- a/types/project.go +++ b/types/project.go @@ -41,7 +41,7 @@ type Project struct { Volumes Volumes `yaml:",omitempty" json:"volumes,omitempty"` Secrets Secrets `yaml:",omitempty" json:"secrets,omitempty"` Configs Configs `yaml:",omitempty" json:"configs,omitempty"` - Extensions Extensions `yaml:",inline" json:"-"` // https://github.com/golang/go/issues/6213 + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` // https://github.com/golang/go/issues/6213 ComposeFiles []string `yaml:"-" json:"-"` Environment Mapping `yaml:"-" json:"-"` diff --git a/types/types.go b/types/types.go index 6fce1989..9176b27c 100644 --- a/types/types.go +++ b/types/types.go @@ -186,7 +186,7 @@ type ServiceConfig struct { VolumesFrom []string `mapstructure:"volumes_from" yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"` WorkingDir string `mapstructure:"working_dir" yaml:"working_dir,omitempty" json:"working_dir,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // NetworksByPriority return the service networks IDs sorted according to Priority @@ -314,7 +314,7 @@ type BuildConfig struct { Platforms StringList `mapstructure:"platforms" yaml:"platforms,omitempty" json:"platforms,omitempty"` Privileged bool `yaml:",omitempty" json:"privileged,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // BlkioConfig define blkio config @@ -326,7 +326,7 @@ type BlkioConfig struct { DeviceWriteBps []ThrottleDevice `mapstructure:"device_write_bps" yaml:",omitempty" json:"device_write_bps,omitempty"` DeviceWriteIOps []ThrottleDevice `mapstructure:"device_write_iops" yaml:",omitempty" json:"device_write_iops,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // WeightDevice is a structure that holds device:weight pair @@ -334,7 +334,7 @@ type WeightDevice struct { Path string Weight uint16 - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // ThrottleDevice is a structure that holds device:rate_per_second pair @@ -342,7 +342,7 @@ type ThrottleDevice struct { Path string Rate uint64 - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // ShellCommand is a string or list of string args. @@ -565,7 +565,7 @@ type LoggingConfig struct { Driver string `yaml:",omitempty" json:"driver,omitempty"` Options map[string]string `yaml:",omitempty" json:"options,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // DeployConfig the deployment configuration for a service @@ -580,7 +580,7 @@ type DeployConfig struct { Placement Placement `yaml:",omitempty" json:"placement,omitempty"` EndpointMode string `mapstructure:"endpoint_mode" yaml:"endpoint_mode,omitempty" json:"endpoint_mode,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // HealthCheckConfig the healthcheck configuration for a service @@ -592,7 +592,7 @@ type HealthCheckConfig struct { StartPeriod *Duration `mapstructure:"start_period" yaml:"start_period,omitempty" json:"start_period,omitempty"` Disable bool `yaml:",omitempty" json:"disable,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // HealthCheckTest is the command run to test the health of a service @@ -607,7 +607,7 @@ type UpdateConfig struct { MaxFailureRatio float32 `mapstructure:"max_failure_ratio" yaml:"max_failure_ratio,omitempty" json:"max_failure_ratio,omitempty"` Order string `yaml:",omitempty" json:"order,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // Resources the resource limits and reservations @@ -615,7 +615,7 @@ type Resources struct { Limits *Resource `yaml:",omitempty" json:"limits,omitempty"` Reservations *Resource `yaml:",omitempty" json:"reservations,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // Resource is a resource to be limited or reserved @@ -627,7 +627,7 @@ type Resource struct { Devices []DeviceRequest `mapstructure:"devices" yaml:"devices,omitempty" json:"devices,omitempty"` GenericResources []GenericResource `mapstructure:"generic_resources" yaml:"generic_resources,omitempty" json:"generic_resources,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } type DeviceRequest struct { @@ -642,7 +642,7 @@ type DeviceRequest struct { type GenericResource struct { DiscreteResourceSpec *DiscreteGenericResource `mapstructure:"discrete_resource_spec" yaml:"discrete_resource_spec,omitempty" json:"discrete_resource_spec,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // DiscreteGenericResource represents a "user defined" resource which is defined @@ -653,7 +653,7 @@ type DiscreteGenericResource struct { Kind string `json:"kind"` Value int64 `json:"value"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // UnitBytes is the bytes type @@ -676,7 +676,7 @@ type RestartPolicy struct { MaxAttempts *uint64 `mapstructure:"max_attempts" yaml:"max_attempts,omitempty" json:"max_attempts,omitempty"` Window *Duration `yaml:",omitempty" json:"window,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // Placement constraints for the service @@ -685,14 +685,14 @@ type Placement struct { Preferences []PlacementPreferences `yaml:",omitempty" json:"preferences,omitempty"` MaxReplicas uint64 `mapstructure:"max_replicas_per_node" yaml:"max_replicas_per_node,omitempty" json:"max_replicas_per_node,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // PlacementPreferences is the preferences for a service placement type PlacementPreferences struct { Spread string `yaml:",omitempty" json:"spread,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // ServiceNetworkConfig is the network configuration for a service @@ -703,7 +703,7 @@ type ServiceNetworkConfig struct { Ipv6Address string `mapstructure:"ipv6_address" yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"` LinkLocalIPs []string `mapstructure:"link_local_ips" yaml:"link_local_ips,omitempty" json:"link_local_ips,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // ServicePortConfig is the port configuration for a service @@ -714,7 +714,7 @@ type ServicePortConfig struct { Published string `yaml:",omitempty" json:"published,omitempty"` Protocol string `yaml:",omitempty" json:"protocol,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // ParsePortConfig parse short syntax for service port configuration @@ -767,7 +767,7 @@ type ServiceVolumeConfig struct { Volume *ServiceVolumeVolume `yaml:",omitempty" json:"volume,omitempty"` Tmpfs *ServiceVolumeTmpfs `yaml:",omitempty" json:"tmpfs,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // String render ServiceVolumeConfig as a volume string, one can parse back using loader.ParseVolume @@ -811,7 +811,7 @@ type ServiceVolumeBind struct { Propagation string `yaml:",omitempty" json:"propagation,omitempty"` CreateHostPath bool `mapstructure:"create_host_path" yaml:"create_host_path,omitempty" json:"create_host_path,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // SELinux represents the SELinux re-labeling options. @@ -842,7 +842,7 @@ const ( type ServiceVolumeVolume struct { NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty" json:"nocopy,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // ServiceVolumeTmpfs are options for a service volume of type tmpfs @@ -851,7 +851,7 @@ type ServiceVolumeTmpfs struct { Mode uint32 `yaml:",omitempty" json:"mode,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // FileReferenceConfig for a reference to a swarm file object @@ -862,7 +862,7 @@ type FileReferenceConfig struct { GID string `yaml:",omitempty" json:"gid,omitempty"` Mode *uint32 `yaml:",omitempty" json:"mode,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // ServiceConfigObjConfig is the config obj configuration for a service @@ -877,7 +877,7 @@ type UlimitsConfig struct { Soft int `yaml:",omitempty" json:"soft,omitempty"` Hard int `yaml:",omitempty" json:"hard,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // MarshalYAML makes UlimitsConfig implement yaml.Marshaller @@ -905,23 +905,23 @@ func (u *UlimitsConfig) MarshalJSON() ([]byte, error) { // NetworkConfig for a network type NetworkConfig struct { - 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"` - EnableIPv6 bool `mapstructure:"enable_ipv6" yaml:"enable_ipv6,omitempty" json:"enable_ipv6,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + 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"` + EnableIPv6 bool `mapstructure:"enable_ipv6" yaml:"enable_ipv6,omitempty" json:"enable_ipv6,omitempty"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // IPAMConfig for a network type IPAMConfig struct { - Driver string `yaml:",omitempty" json:"driver,omitempty"` - Config []*IPAMPool `yaml:",omitempty" json:"config,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Driver string `yaml:",omitempty" json:"driver,omitempty"` + Config []*IPAMPool `yaml:",omitempty" json:"config,omitempty"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // IPAMPool for a network @@ -935,21 +935,21 @@ type IPAMPool struct { // VolumeConfig for a volume type VolumeConfig struct { - 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"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + 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"` + Extensions Extensions `mapstructure:"#extensions" 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" json:"name,omitempty"` - External bool `yaml:",omitempty" json:"external,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Name string `yaml:",omitempty" json:"name,omitempty"` + External bool `yaml:",omitempty" json:"external,omitempty"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // MarshalYAML makes External implement yaml.Marshaller @@ -970,23 +970,23 @@ func (e External) MarshalJSON() ([]byte, error) { // CredentialSpecConfig for credential spec on Windows type CredentialSpecConfig struct { - Config string `yaml:",omitempty" json:"config,omitempty"` // Config was added in API v1.40 - File string `yaml:",omitempty" json:"file,omitempty"` - Registry string `yaml:",omitempty" json:"registry,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Config string `yaml:",omitempty" json:"config,omitempty"` // Config was added in API v1.40 + File string `yaml:",omitempty" json:"file,omitempty"` + Registry string `yaml:",omitempty" json:"registry,omitempty"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } // FileObjectConfig is a config type for a file used by a service type FileObjectConfig struct { - Name string `yaml:",omitempty" json:"name,omitempty"` - File string `yaml:",omitempty" json:"file,omitempty"` - Environment string `yaml:",omitempty" json:"environment,omitempty"` - External External `yaml:",omitempty" json:"external,omitempty"` - Labels Labels `yaml:",omitempty" json:"labels,omitempty"` - Driver string `yaml:",omitempty" json:"driver,omitempty"` - DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"` - TemplateDriver string `mapstructure:"template_driver" yaml:"template_driver,omitempty" json:"template_driver,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Name string `yaml:",omitempty" json:"name,omitempty"` + File string `yaml:",omitempty" json:"file,omitempty"` + Environment string `yaml:",omitempty" json:"environment,omitempty"` + External External `yaml:",omitempty" json:"external,omitempty"` + Labels Labels `yaml:",omitempty" json:"labels,omitempty"` + Driver string `yaml:",omitempty" json:"driver,omitempty"` + DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"` + TemplateDriver string `mapstructure:"template_driver" yaml:"template_driver,omitempty" json:"template_driver,omitempty"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } const ( @@ -1003,9 +1003,9 @@ const ( type DependsOnConfig map[string]ServiceDependency type ServiceDependency struct { - Condition string `yaml:",omitempty" json:"condition,omitempty"` - Restart bool `yaml:",omitempty" json:"restart,omitempty"` - Extensions map[string]interface{} `yaml:",inline" json:"-"` + Condition string `yaml:",omitempty" json:"condition,omitempty"` + Restart bool `yaml:",omitempty" json:"restart,omitempty"` + Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` } type ExtendsConfig MappingWithEquals