Skip to content

Commit 4374750

Browse files
committed
validate merge result vs individual overrides
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 8263290 commit 4374750

File tree

7 files changed

+89
-41
lines changed

7 files changed

+89
-41
lines changed

loader/extends.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,15 @@ func ApplyExtends(ctx context.Context, dict map[string]any, workingdir string, o
3131
if !ok {
3232
return nil
3333
}
34-
services := a.(map[string]any)
34+
services, ok := a.(map[string]any)
35+
if !ok {
36+
return fmt.Errorf("services must be a mapping")
37+
}
3538
for name, s := range services {
36-
service := s.(map[string]any)
39+
service, ok := s.(map[string]any)
40+
if !ok {
41+
return fmt.Errorf("services.%s must be a mapping", name)
42+
}
3743
x, ok := service["extends"]
3844
if !ok {
3945
continue

loader/loader.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,12 +312,6 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
312312

313313
fixEmptyNotNull(cfg)
314314

315-
if !opts.SkipValidation {
316-
if err := schema.Validate(cfg); err != nil {
317-
return fmt.Errorf("validating %s: %w", file.Filename, err)
318-
}
319-
}
320-
321315
if !opts.SkipExtends {
322316
err = ApplyExtends(fctx, cfg, config.WorkingDir, opts, ct, processors...)
323317
if err != nil {
@@ -333,6 +327,12 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
333327

334328
dict, err = override.Merge(dict, cfg)
335329

330+
if !opts.SkipValidation {
331+
if err := schema.Validate(dict); err != nil {
332+
return fmt.Errorf("validating %s: %w", file.Filename, err)
333+
}
334+
}
335+
336336
return err
337337
}
338338

@@ -378,8 +378,6 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
378378
}
379379
}
380380

381-
dict = groupXFieldsIntoExtensions(dict, tree.NewPath())
382-
383381
if !opts.SkipValidation {
384382
if err := validation.Validate(dict); err != nil {
385383
return nil, err
@@ -423,6 +421,8 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
423421
Environment: configDetails.Environment,
424422
}
425423
delete(dict, "name") // project name set by yaml must be identified by caller as opts.projectName
424+
425+
dict = groupXFieldsIntoExtensions(dict, tree.NewPath())
426426
err = Transform(dict, project)
427427
if err != nil {
428428
return nil, err

loader/override_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,40 @@ services:
128128
assert.NilError(t, err)
129129
assert.Check(t, p.Services["test"].DependsOn["foo"].Required == false)
130130
}
131+
132+
func TestOverridePartial(t *testing.T) {
133+
yaml := `
134+
name: test-override-networks
135+
services:
136+
test:
137+
image: test
138+
depends_on:
139+
foo:
140+
condition: service_healthy
141+
142+
foo:
143+
image: foo
144+
`
145+
146+
override := `
147+
services:
148+
test:
149+
depends_on:
150+
foo:
151+
# This is invalid according to json schema as condition is required
152+
required: false
153+
`
154+
_, err := LoadWithContext(context.Background(), types.ConfigDetails{
155+
ConfigFiles: []types.ConfigFile{
156+
{
157+
Filename: "base",
158+
Content: []byte(yaml),
159+
},
160+
{
161+
Filename: "override",
162+
Content: []byte(override),
163+
},
164+
},
165+
})
166+
assert.NilError(t, err)
167+
}

types/healthcheck.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type HealthCheckConfig struct {
3030
StartInterval *Duration `yaml:"start_interval,omitempty" json:"start_interval,omitempty"`
3131
Disable bool `yaml:"disable,omitempty" json:"disable,omitempty"`
3232

33-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
33+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
3434
}
3535

3636
// HealthCheckTest is the command run to test the health of a service

types/project.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ type Project struct {
4545
Volumes Volumes `yaml:"volumes,omitempty" json:"volumes,omitempty"`
4646
Secrets Secrets `yaml:"secrets,omitempty" json:"secrets,omitempty"`
4747
Configs Configs `yaml:"configs,omitempty" json:"configs,omitempty"`
48-
Extensions Extensions `yaml:"#extensions,inline" json:"-"` // https://github.com/golang/go/issues/6213
48+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` // https://github.com/golang/go/issues/6213
4949

5050
// IncludeReferences is keyed by Compose YAML filename and contains config for
5151
// other Compose YAML files it directly triggered a load of via `include`.

types/types.go

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ type ServiceConfig struct {
132132
VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"`
133133
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
134134

135-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
135+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
136136
}
137137

138138
// MarshalYAML makes ServiceConfig implement yaml.Marshaller
@@ -280,7 +280,7 @@ type BuildConfig struct {
280280
Platforms StringList `yaml:"platforms,omitempty" json:"platforms,omitempty"`
281281
Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"`
282282

283-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
283+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
284284
}
285285

286286
// BlkioConfig define blkio config
@@ -292,23 +292,23 @@ type BlkioConfig struct {
292292
DeviceWriteBps []ThrottleDevice `yaml:"device_write_bps,omitempty" json:"device_write_bps,omitempty"`
293293
DeviceWriteIOps []ThrottleDevice `yaml:"device_write_iops,omitempty" json:"device_write_iops,omitempty"`
294294

295-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
295+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
296296
}
297297

298298
// WeightDevice is a structure that holds device:weight pair
299299
type WeightDevice struct {
300300
Path string
301301
Weight uint16
302302

303-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
303+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
304304
}
305305

306306
// ThrottleDevice is a structure that holds device:rate_per_second pair
307307
type ThrottleDevice struct {
308308
Path string
309309
Rate UnitBytes
310310

311-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
311+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
312312
}
313313

314314
// MappingWithColon is a mapping type that can be converted from a list of
@@ -320,7 +320,7 @@ type LoggingConfig struct {
320320
Driver string `yaml:"driver,omitempty" json:"driver,omitempty"`
321321
Options Options `yaml:"options,omitempty" json:"options,omitempty"`
322322

323-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
323+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
324324
}
325325

326326
// DeployConfig the deployment configuration for a service
@@ -335,7 +335,7 @@ type DeployConfig struct {
335335
Placement Placement `yaml:"placement,omitempty" json:"placement,omitempty"`
336336
EndpointMode string `yaml:"endpoint_mode,omitempty" json:"endpoint_mode,omitempty"`
337337

338-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
338+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
339339
}
340340

341341
// UpdateConfig the service update configuration
@@ -347,15 +347,15 @@ type UpdateConfig struct {
347347
MaxFailureRatio float32 `yaml:"max_failure_ratio,omitempty" json:"max_failure_ratio,omitempty"`
348348
Order string `yaml:"order,omitempty" json:"order,omitempty"`
349349

350-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
350+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
351351
}
352352

353353
// Resources the resource limits and reservations
354354
type Resources struct {
355355
Limits *Resource `yaml:"limits,omitempty" json:"limits,omitempty"`
356356
Reservations *Resource `yaml:"reservations,omitempty" json:"reservations,omitempty"`
357357

358-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
358+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
359359
}
360360

361361
// Resource is a resource to be limited or reserved
@@ -367,15 +367,15 @@ type Resource struct {
367367
Devices []DeviceRequest `yaml:"devices,omitempty" json:"devices,omitempty"`
368368
GenericResources []GenericResource `yaml:"generic_resources,omitempty" json:"generic_resources,omitempty"`
369369

370-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
370+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
371371
}
372372

373373
// GenericResource represents a "user defined" resource which can
374374
// only be an integer (e.g: SSD=3) for a service
375375
type GenericResource struct {
376376
DiscreteResourceSpec *DiscreteGenericResource `yaml:"discrete_resource_spec,omitempty" json:"discrete_resource_spec,omitempty"`
377377

378-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
378+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
379379
}
380380

381381
// DiscreteGenericResource represents a "user defined" resource which is defined
@@ -386,7 +386,7 @@ type DiscreteGenericResource struct {
386386
Kind string `json:"kind"`
387387
Value int64 `json:"value"`
388388

389-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
389+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
390390
}
391391

392392
// RestartPolicy the service restart policy
@@ -396,7 +396,7 @@ type RestartPolicy struct {
396396
MaxAttempts *uint64 `yaml:"max_attempts,omitempty" json:"max_attempts,omitempty"`
397397
Window *Duration `yaml:"window,omitempty" json:"window,omitempty"`
398398

399-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
399+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
400400
}
401401

402402
// Placement constraints for the service
@@ -405,14 +405,14 @@ type Placement struct {
405405
Preferences []PlacementPreferences `yaml:"preferences,omitempty" json:"preferences,omitempty"`
406406
MaxReplicas uint64 `yaml:"max_replicas_per_node,omitempty" json:"max_replicas_per_node,omitempty"`
407407

408-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
408+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
409409
}
410410

411411
// PlacementPreferences is the preferences for a service placement
412412
type PlacementPreferences struct {
413413
Spread string `yaml:"spread,omitempty" json:"spread,omitempty"`
414414

415-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
415+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
416416
}
417417

418418
// ServiceNetworkConfig is the network configuration for a service
@@ -424,7 +424,7 @@ type ServiceNetworkConfig struct {
424424
LinkLocalIPs []string `yaml:"link_local_ips,omitempty" json:"link_local_ips,omitempty"`
425425
MacAddress string `yaml:"mac_address,omitempty" json:"mac_address,omitempty"`
426426

427-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
427+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
428428
}
429429

430430
// ServicePortConfig is the port configuration for a service
@@ -435,7 +435,7 @@ type ServicePortConfig struct {
435435
Published string `yaml:"published,omitempty" json:"published,omitempty"`
436436
Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"`
437437

438-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
438+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
439439
}
440440

441441
// ParsePortConfig parse short syntax for service port configuration
@@ -488,7 +488,7 @@ type ServiceVolumeConfig struct {
488488
Volume *ServiceVolumeVolume `yaml:"volume,omitempty" json:"volume,omitempty"`
489489
Tmpfs *ServiceVolumeTmpfs `yaml:"tmpfs,omitempty" json:"tmpfs,omitempty"`
490490

491-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
491+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
492492
}
493493

494494
// String render ServiceVolumeConfig as a volume string, one can parse back using loader.ParseVolume
@@ -534,7 +534,7 @@ type ServiceVolumeBind struct {
534534
Propagation string `yaml:"propagation,omitempty" json:"propagation,omitempty"`
535535
CreateHostPath bool `yaml:"create_host_path,omitempty" json:"create_host_path,omitempty"`
536536

537-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
537+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
538538
}
539539

540540
// SELinux represents the SELinux re-labeling options.
@@ -565,7 +565,7 @@ const (
565565
type ServiceVolumeVolume struct {
566566
NoCopy bool `yaml:"nocopy,omitempty" json:"nocopy,omitempty"`
567567

568-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
568+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
569569
}
570570

571571
// ServiceVolumeTmpfs are options for a service volume of type tmpfs
@@ -574,7 +574,7 @@ type ServiceVolumeTmpfs struct {
574574

575575
Mode uint32 `yaml:"mode,omitempty" json:"mode,omitempty"`
576576

577-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
577+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
578578
}
579579

580580
// FileReferenceConfig for a reference to a swarm file object
@@ -585,7 +585,7 @@ type FileReferenceConfig struct {
585585
GID string `yaml:"gid,omitempty" json:"gid,omitempty"`
586586
Mode *uint32 `yaml:"mode,omitempty" json:"mode,omitempty"`
587587

588-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
588+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
589589
}
590590

591591
// ServiceConfigObjConfig is the config obj configuration for a service
@@ -600,7 +600,7 @@ type UlimitsConfig struct {
600600
Soft int `yaml:"soft,omitempty" json:"soft,omitempty"`
601601
Hard int `yaml:"hard,omitempty" json:"hard,omitempty"`
602602

603-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
603+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
604604
}
605605

606606
// MarshalYAML makes UlimitsConfig implement yaml.Marshaller
@@ -637,14 +637,14 @@ type NetworkConfig struct {
637637
Attachable bool `yaml:"attachable,omitempty" json:"attachable,omitempty"`
638638
Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"`
639639
EnableIPv6 bool `yaml:"enable_ipv6,omitempty" json:"enable_ipv6,omitempty"`
640-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
640+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
641641
}
642642

643643
// IPAMConfig for a network
644644
type IPAMConfig struct {
645645
Driver string `yaml:"driver,omitempty" json:"driver,omitempty"`
646646
Config []*IPAMPool `yaml:"config,omitempty" json:"config,omitempty"`
647-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
647+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
648648
}
649649

650650
// IPAMPool for a network
@@ -663,7 +663,7 @@ type VolumeConfig struct {
663663
DriverOpts Options `yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
664664
External External `yaml:"external,omitempty" json:"external,omitempty"`
665665
Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"`
666-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
666+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
667667
}
668668

669669
// External identifies a Volume or Network as a reference to a resource that is
@@ -675,7 +675,7 @@ type CredentialSpecConfig struct {
675675
Config string `yaml:"config,omitempty" json:"config,omitempty"` // Config was added in API v1.40
676676
File string `yaml:"file,omitempty" json:"file,omitempty"`
677677
Registry string `yaml:"registry,omitempty" json:"registry,omitempty"`
678-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
678+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
679679
}
680680

681681
// FileObjectConfig is a config type for a file used by a service
@@ -689,7 +689,7 @@ type FileObjectConfig struct {
689689
Driver string `yaml:"driver,omitempty" json:"driver,omitempty"`
690690
DriverOpts map[string]string `yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
691691
TemplateDriver string `yaml:"template_driver,omitempty" json:"template_driver,omitempty"`
692-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
692+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
693693
}
694694

695695
const (
@@ -708,7 +708,7 @@ type DependsOnConfig map[string]ServiceDependency
708708
type ServiceDependency struct {
709709
Condition string `yaml:"condition,omitempty" json:"condition,omitempty"`
710710
Restart bool `yaml:"restart,omitempty" json:"restart,omitempty"`
711-
Extensions Extensions `yaml:"#extensions,inline" json:"-"`
711+
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
712712
Required bool `yaml:"required" json:"required"`
713713
}
714714

validation/external.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package validation
1818

1919
import (
2020
"fmt"
21+
"strings"
2122

2223
"github.com/compose-spec/compose-go/v2/consts"
2324
"github.com/compose-spec/compose-go/v2/tree"
@@ -37,6 +38,10 @@ func checkExternal(v map[string]any, p tree.Path) error {
3738
case "name", "external", consts.Extensions:
3839
continue
3940
default:
41+
if strings.HasPrefix(k, "x-") {
42+
// custom extension, ignored
43+
continue
44+
}
4045
return fmt.Errorf("%s: conflicting parameters \"external\" and %q specified", p, k)
4146
}
4247
}

0 commit comments

Comments
 (0)