From e3872a65a7ffd9e396ea96c2e76bc9a6c082d3ff Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 11 Oct 2024 11:35:52 +0200 Subject: [PATCH] introduce ability to define image reference as {name,tag,digest} Signed-off-by: Nicolas De Loof --- cli/options_test.go | 2 +- loader/include_test.go | 2 +- loader/loader_test.go | 20 +++++++++++++++++--- schema/compose-spec.json | 20 +++++++++++++++++++- types/project.go | 4 ++-- types/project_test.go | 11 ++++++----- types/types.go | 34 +++++++++++++++++++++++++++++++++- 7 files changed, 79 insertions(+), 14 deletions(-) diff --git a/cli/options_test.go b/cli/options_test.go index 31edf9b9..db82868d 100644 --- a/cli/options_test.go +++ b/cli/options_test.go @@ -211,7 +211,7 @@ func TestProjectFromSetOfFiles(t *testing.T) { assert.NilError(t, err) service, err := p.GetService("simple") assert.NilError(t, err) - assert.Equal(t, service.Image, "haproxy") + assert.Equal(t, service.Image, types.Image("haproxy")) } func TestProjectComposefilesFromSetOfFiles(t *testing.T) { diff --git a/loader/include_test.go b/loader/include_test.go index f44d417b..8c7037d6 100644 --- a/loader/include_test.go +++ b/loader/include_test.go @@ -221,7 +221,7 @@ services: options.SetProjectName("project", true) }) assert.NilError(t, err) - assert.Equal(t, p.Services["included"].Image, "alpine") + assert.Equal(t, p.Services["included"].Image, types.Image("alpine")) } func createFile(t *testing.T, rootDir, content, fileName string) string { diff --git a/loader/loader_test.go b/loader/loader_test.go index 554ae9f6..502baa8d 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -333,7 +333,7 @@ services: }, actual.Extensions)) assert.Check(t, is.Len(actual.Services, 2)) service := actual.Services["foo"] - assert.Check(t, is.Equal("busybox", service.Image)) + assert.Check(t, is.Equal(service.Image, types.Image("busybox"))) assert.Check(t, is.DeepEqual(types.Extensions{ "x-foo": "bar", @@ -2720,7 +2720,7 @@ services: options.ResolvePaths = true }) assert.NilError(t, err) - assert.Equal(t, p.Services["imported"].Image, "overridden") + assert.Equal(t, p.Services["imported"].Image, types.Image("overridden")) } func TestLoadDependsOnCycle(t *testing.T) { @@ -2921,7 +2921,7 @@ services: `) assert.NilError(t, err) - assert.Equal(t, project.Services["test"].Image, "nginx:override") + assert.Equal(t, project.Services["test"].Image, types.Image("nginx:override")) } func TestLoadDevelopConfig(t *testing.T) { @@ -3555,3 +3555,17 @@ services: }, }) } + +func TestLoadImageReference(t *testing.T) { + p, err := loadYAML(` +name: load-image-reference +services: + test: + image: + name: registry.com/foo + tag: bar + digest: sha256:1234567890123456789012345678901234567890123456789012345678901234 +`) + assert.NilError(t, err) + assert.Equal(t, p.Services["test"].Image, types.Image("registry.com/foo:bar@sha256:1234567890123456789012345678901234567890123456789012345678901234")) +} diff --git a/schema/compose-spec.json b/schema/compose-spec.json index 95326f31..54f15bab 100644 --- a/schema/compose-spec.json +++ b/schema/compose-spec.json @@ -276,7 +276,7 @@ }, "healthcheck": {"$ref": "#/definitions/healthcheck"}, "hostname": {"type": "string"}, - "image": {"type": "string"}, + "image": {"$ref": "#/definitions/imageReference"}, "init": {"type": ["boolean", "string"]}, "ipc": {"type": "string"}, "isolation": {"type": "string"}, @@ -465,6 +465,24 @@ "additionalProperties": false }, + "imageReference": { + "id": "#/definitions/imageReference", + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "name": {"type": "string"}, + "tag": {"type": "string"}, + "digest": {"type": "string"} + }, + "required": ["name"], + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + ] + }, + "healthcheck": { "id": "#/definitions/healthcheck", "type": "object", diff --git a/types/project.go b/types/project.go index 19d6e32b..3ff0cc5b 100644 --- a/types/project.go +++ b/types/project.go @@ -539,7 +539,7 @@ func (p *Project) WithImagesResolved(resolver func(named reference.Named) (godig if service.Image == "" { return service, nil } - named, err := reference.ParseDockerRef(service.Image) + named, err := reference.ParseDockerRef(string(service.Image)) if err != nil { return service, err } @@ -555,7 +555,7 @@ func (p *Project) WithImagesResolved(resolver func(named reference.Named) (godig return service, err } } - service.Image = named.String() + service.Image = Image(named.String()) return service, nil }) } diff --git a/types/project_test.go b/types/project_test.go index c2c0fbfe..e232dc26 100644 --- a/types/project_test.go +++ b/types/project_test.go @@ -200,11 +200,11 @@ func Test_ResolveImages(t *testing.T) { for _, test := range tests { service := p.Services["service_1"] - service.Image = test.image + service.Image = Image(test.image) p.Services["service_1"] = service p, err := p.WithImagesResolved(resolver) assert.NilError(t, err) - assert.Equal(t, p.Services["service_1"].Image, test.resolved) + assert.Equal(t, p.Services["service_1"].Image, Image(test.resolved)) } } @@ -218,14 +218,15 @@ func Test_ResolveImages_concurrent(t *testing.T) { } for i := 0; i < 1000; i++ { p.Services[fmt.Sprintf("service_%d", i)] = ServiceConfig{ - Image: fmt.Sprintf("image_%d", i), + Image: Image(fmt.Sprintf("image_%d", i)), } } p, err := p.WithImagesResolved(resolver) assert.NilError(t, err) for i := 0; i < 1000; i++ { + expected := fmt.Sprintf("docker.io/library/image_%d:latest@%s", i, garfield) assert.Equal(t, p.Services[fmt.Sprintf("service_%d", i)].Image, - fmt.Sprintf("docker.io/library/image_%d:latest@%s", i, garfield)) + Image(expected)) } } @@ -238,7 +239,7 @@ func Test_ResolveImages_concurrent_interrupted(t *testing.T) { } for i := 0; i < 10; i++ { p.Services[fmt.Sprintf("service_%d", i)] = ServiceConfig{ - Image: fmt.Sprintf("image_%d", i), + Image: Image(fmt.Sprintf("image_%d", i)), } } _, err := p.WithImagesResolved(resolver) diff --git a/types/types.go b/types/types.go index dd9705f4..d67bb501 100644 --- a/types/types.go +++ b/types/types.go @@ -23,7 +23,9 @@ import ( "strconv" "strings" + "github.com/distribution/reference" "github.com/docker/go-connections/nat" + "github.com/opencontainers/go-digest" ) // ServiceConfig is the configuration of one service @@ -84,7 +86,7 @@ type ServiceConfig struct { GroupAdd []string `yaml:"group_add,omitempty" json:"group_add,omitempty"` Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"` HealthCheck *HealthCheckConfig `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"` - Image string `yaml:"image,omitempty" json:"image,omitempty"` + Image Image `yaml:"image,omitempty" json:"image,omitempty"` Init *bool `yaml:"init,omitempty" json:"init,omitempty"` Ipc string `yaml:"ipc,omitempty" json:"ipc,omitempty"` Isolation string `yaml:"isolation,omitempty" json:"isolation,omitempty"` @@ -267,6 +269,36 @@ func (s ServiceConfig) GetDependents(p *Project) []string { return dependent } +type Image string + +func (i *Image) DecodeMapstructure(value interface{}) error { + switch v := value.(type) { + case string: + *i = Image(v) + case map[string]any: + r := v["name"].(string) + name, err := reference.WithName(r) + if err != nil { + return err + } + if t, ok := v["tag"].(string); ok { + name, err = reference.WithTag(name, t) + if err != nil { + return err + } + } + if d, ok := v["digest"].(string); ok { + name, err = reference.WithDigest(name, digest.Digest(d)) + if err != nil { + return err + } + } + *i = Image(reference.FamiliarString(name)) + } + return nil + +} + // BuildConfig is a type for build type BuildConfig struct { Context string `yaml:"context,omitempty" json:"context,omitempty"`