From 002aaa3d4b59bef8910c0fd4294d9d627ef39444 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 6 Nov 2025 12:31:01 +0100 Subject: [PATCH] create_host_path must not be omitted when set to false Signed-off-by: Nicolas De Loof --- .github/workflows/ci.yml | 2 +- go.mod | 2 +- loader/full-struct_test.go | 34 ++++++++++++---------------------- schema/schema.go | 21 ++++++++++++++++++--- types/envfile.go | 26 +------------------------- types/types.go | 10 +++++++++- types/types_test.go | 34 ++++++++++++++++++++++++++++++++++ 7 files changed, 76 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d71ddc3a..ab0f2eae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: test: strategy: matrix: - go-version: ['1.23', '1.24'] + go-version: ['1.24', '1.25'] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} timeout-minutes: 10 diff --git a/go.mod b/go.mod index f2e1f95b..99836cde 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/compose-spec/compose-go/v2 -go 1.23 +go 1.24 require ( github.com/distribution/reference v0.5.0 diff --git a/loader/full-struct_test.go b/loader/full-struct_test.go index 07e258d6..77e05f08 100644 --- a/loader/full-struct_test.go +++ b/loader/full-struct_test.go @@ -755,7 +755,7 @@ services: FOO: foo_from_env_file QUX: qux_from_environment env_file: - - %s + - path: %s - path: %s required: false expose: @@ -949,24 +949,20 @@ services: - type: bind source: /opt/data target: /var/lib/data - bind: - create_host_path: true + bind: {} - type: bind source: %s target: /code - bind: - create_host_path: true + bind: {} - type: bind source: %s target: /var/www/html - bind: - create_host_path: true + bind: {} - type: bind source: %s target: /etc/configs read_only: true - bind: - create_host_path: true + bind: {} - type: volume source: datavolume target: /var/lib/volume @@ -1370,7 +1366,9 @@ func fullExampleJSON(workingDir, homeDir string) string { "QUX": "qux_from_environment" }, "env_file": [ - "%s", + { + "path": "%s" + }, { "path": "%s", "required": false @@ -1640,34 +1638,26 @@ func fullExampleJSON(workingDir, homeDir string) string { "type": "bind", "source": "/opt/data", "target": "/var/lib/data", - "bind": { - "create_host_path": true - } + "bind": {} }, { "type": "bind", "source": "%s", "target": "/code", - "bind": { - "create_host_path": true - } + "bind": {} }, { "type": "bind", "source": "%s", "target": "/var/www/html", - "bind": { - "create_host_path": true - } + "bind": {} }, { "type": "bind", "source": "%s", "target": "/etc/configs", "read_only": true, - "bind": { - "create_host_path": true - } + "bind": {} }, { "type": "volume", diff --git a/schema/schema.go b/schema/schema.go index 8aa7ccf6..a73eda24 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -19,6 +19,7 @@ package schema import ( // Enable support for embedded static resources _ "embed" + "encoding/json" "errors" "fmt" "slices" @@ -48,11 +49,11 @@ var Schema string // Validate uses the jsonschema to validate the configuration func Validate(config map[string]interface{}) error { compiler := jsonschema.NewCompiler() - json, err := jsonschema.UnmarshalJSON(strings.NewReader(Schema)) + shema, err := jsonschema.UnmarshalJSON(strings.NewReader(Schema)) if err != nil { return err } - err = compiler.AddResource("compose-spec.json", json) + err = compiler.AddResource("compose-spec.json", shema) if err != nil { return err } @@ -61,7 +62,21 @@ func Validate(config map[string]interface{}) error { Validate: durationFormatChecker, }) schema := compiler.MustCompile("compose-spec.json") - err = schema.Validate(config) + + // santhosh-tekuri doesn't allow derived types + // see https://github.com/santhosh-tekuri/jsonschema/pull/240 + marshaled, err := json.Marshal(config) + if err != nil { + return err + } + + var raw map[string]interface{} + err = json.Unmarshal(marshaled, &raw) + if err != nil { + return err + } + + err = schema.Validate(raw) var verr *jsonschema.ValidationError if ok := errors.As(err, &verr); ok { return validationError{getMostSpecificError(verr)} diff --git a/types/envfile.go b/types/envfile.go index 1348f132..a7d239ee 100644 --- a/types/envfile.go +++ b/types/envfile.go @@ -16,32 +16,8 @@ package types -import ( - "encoding/json" -) - type EnvFile struct { Path string `yaml:"path,omitempty" json:"path,omitempty"` - Required bool `yaml:"required" json:"required"` + Required OptOut `yaml:"required,omitempty" json:"required,omitzero"` Format string `yaml:"format,omitempty" json:"format,omitempty"` } - -// MarshalYAML makes EnvFile implement yaml.Marshaler -func (e EnvFile) MarshalYAML() (interface{}, error) { - if e.Required { - return e.Path, nil - } - return map[string]any{ - "path": e.Path, - "required": e.Required, - }, nil -} - -// MarshalJSON makes EnvFile implement json.Marshaler -func (e *EnvFile) MarshalJSON() ([]byte, error) { - if e.Required { - return json.Marshal(e.Path) - } - // Pass as a value to avoid re-entering this method and use the default implementation - return json.Marshal(*e) -} diff --git a/types/types.go b/types/types.go index b8630f48..fd4f3513 100644 --- a/types/types.go +++ b/types/types.go @@ -564,12 +564,20 @@ const ( type ServiceVolumeBind struct { SELinux string `yaml:"selinux,omitempty" json:"selinux,omitempty"` Propagation string `yaml:"propagation,omitempty" json:"propagation,omitempty"` - CreateHostPath bool `yaml:"create_host_path,omitempty" json:"create_host_path,omitempty"` + CreateHostPath OptOut `yaml:"create_host_path,omitempty" json:"create_host_path,omitzero"` Recursive string `yaml:"recursive,omitempty" json:"recursive,omitempty"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } +// OptOut is a boolean which default value is 'true' +type OptOut bool + +func (o OptOut) IsZero() bool { + // Attribute can be omitted if value is true + return bool(o) +} + // SELinux represents the SELinux re-labeling options. const ( // SELinuxShared option indicates that the bind mount content is shared among multiple containers diff --git a/types/types_test.go b/types/types_test.go index f79ceac0..874079df 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -380,3 +380,37 @@ func TestMappingValues(t *testing.T) { }) assert.DeepEqual(t, mapping.Values(), values) } + +func TestMarhsall(t *testing.T) { + p := Project{ + Services: Services{ + "test": ServiceConfig{ + Volumes: []ServiceVolumeConfig{ + { + Type: "bind", + Bind: &ServiceVolumeBind{ + CreateHostPath: true, // default + }, + }, + { + Type: "bind", + Bind: &ServiceVolumeBind{ + CreateHostPath: false, + }, + }, + }, + }, + }, + } + marshalYAML, err := p.MarshalYAML() + assert.NilError(t, err) + assert.Equal(t, string(marshalYAML), `services: + test: + volumes: + - type: bind + bind: {} + - type: bind + bind: + create_host_path: false +`) +}