Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down
34 changes: 12 additions & 22 deletions loader/full-struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ services:
FOO: foo_from_env_file
QUX: qux_from_environment
env_file:
- %s
- path: %s
- path: %s
required: false
expose:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1370,7 +1366,9 @@ func fullExampleJSON(workingDir, homeDir string) string {
"QUX": "qux_from_environment"
},
"env_file": [
"%s",
{
"path": "%s"
},
{
"path": "%s",
"required": false
Expand Down Expand Up @@ -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",
Expand Down
21 changes: 18 additions & 3 deletions schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package schema
import (
// Enable support for embedded static resources
_ "embed"
"encoding/json"
"errors"
"fmt"
"slices"
Expand Down Expand Up @@ -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
}
Expand All @@ -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)}
Expand Down
26 changes: 1 addition & 25 deletions types/envfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
10 changes: 9 additions & 1 deletion types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions types/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
`)
}