From 0fc2b5ca8583175b4e02ea3ec3039cd7b6a75cb6 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 24 Jan 2022 16:38:33 -0800 Subject: [PATCH] bake: add named contexts keys Signed-off-by: Tonis Tiigi --- bake/bake.go | 89 ++++++++++++++- bake/bake_test.go | 202 ++++++++++++++++++++++++++++++++++ docs/reference/buildx_bake.md | 2 +- 3 files changed, 290 insertions(+), 3 deletions(-) diff --git a/bake/bake.go b/bake/bake.go index 295264f6..242b2984 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -8,6 +8,7 @@ import ( "os" "path" "regexp" + "sort" "strconv" "strings" @@ -130,6 +131,12 @@ func ReadTargets(ctx context.Context, files []File, targets, overrides []string, g = []*Group{{Targets: dedupString(gt)}} } + for name, t := range m { + if err := c.loadLinks(name, t, m, o, nil); err != nil { + return nil, nil, err + } + } + return m, g, nil } @@ -303,10 +310,45 @@ func (c Config) expandTargets(pattern string) ([]string, error) { return names, nil } +func (c Config) loadLinks(name string, t *Target, m map[string]*Target, o map[string]map[string]Override, visited []string) error { + visited = append(visited, name) + for _, v := range t.Contexts { + if strings.HasPrefix(v, "target:") { + target := strings.TrimPrefix(v, "target:") + if target == t.Name { + return errors.Errorf("target %s cannot link to itself", target) + } + for _, v := range visited { + if v == target { + return errors.Errorf("infinite loop from %s to %s", name, target) + } + } + t2, ok := m[target] + if !ok { + var err error + t2, err = c.ResolveTarget(target, o) + if err != nil { + return err + } + t2.Outputs = nil + m[target] = t2 + } + if err := c.loadLinks(target, t2, m, o, visited); err != nil { + return err + } + if len(t.Platforms) > 1 && len(t2.Platforms) > 1 { + if !sliceEqual(t.Platforms, t2.Platforms) { + return errors.Errorf("target %s can't be used by %s because it is defined for different platforms %v and %v", target, name, t2.Platforms, t.Platforms) + } + } + } + } + return nil +} + func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) { m := map[string]map[string]Override{} for _, v := range v { - parts := strings.SplitN(v, "=", 2) keys := strings.SplitN(parts[0], ".", 3) if len(keys) < 2 { @@ -351,6 +393,11 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) o.Value = v } fallthrough + case "contexts": + if len(keys) != 3 { + return nil, errors.Errorf("invalid key %s, contexts requires name", parts[0]) + } + fallthrough default: if len(parts) == 2 { o.Value = parts[1] @@ -461,6 +508,7 @@ type Target struct { Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional"` Context *string `json:"context,omitempty" hcl:"context,optional"` + Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional"` Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional"` DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional"` Args map[string]string `json:"args,omitempty" hcl:"args,optional"` @@ -488,6 +536,15 @@ func (t *Target) normalize() { t.CacheFrom = removeDupes(t.CacheFrom) t.CacheTo = removeDupes(t.CacheTo) t.Outputs = removeDupes(t.Outputs) + + for k, v := range t.Contexts { + if v == "" { + delete(t.Contexts, k) + } + } + if len(t.Contexts) == 0 { + t.Contexts = nil + } } func (t *Target) Merge(t2 *Target) { @@ -506,6 +563,12 @@ func (t *Target) Merge(t2 *Target) { } t.Args[k] = v } + for k, v := range t2.Contexts { + if t.Contexts == nil { + t.Contexts = map[string]string{} + } + t.Contexts[k] = v + } for k, v := range t2.Labels { if t.Labels == nil { t.Labels = map[string]string{} @@ -565,7 +628,14 @@ func (t *Target) AddOverrides(overrides map[string]Override) error { t.Args = map[string]string{} } t.Args[keys[1]] = value - + case "contexts": + if len(keys) != 2 { + return errors.Errorf("contexts require name") + } + if t.Contexts == nil { + t.Contexts = map[string]string{} + } + t.Contexts[keys[1]] = value case "labels": if len(keys) != 2 { return errors.Errorf("labels require name") @@ -693,6 +763,7 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { bi := build.Inputs{ ContextPath: contextPath, DockerfilePath: dockerfilePath, + NamedContexts: t.Contexts, } if t.DockerfileInline != nil { bi.DockerfileInline = *t.DockerfileInline @@ -811,3 +882,17 @@ func validateTargetName(name string) error { } return nil } + +func sliceEqual(s1, s2 []string) bool { + if len(s1) != len(s2) { + return false + } + sort.Strings(s1) + sort.Strings(s2) + for i := range s1 { + if s1[i] != s2[i] { + return false + } + } + return true +} diff --git a/bake/bake_test.go b/bake/bake_test.go index 955dd0d1..0df5440f 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -353,6 +353,208 @@ func TestOverrideMerge(t *testing.T) { require.Equal(t, "type=registry", m["app"].Outputs[0]) } +func TestReadContexts(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte(` + target "base" { + contexts = { + foo: "bar" + abc: "def" + } + } + target "app" { + inherits = ["base"] + contexts = { + foo: "baz" + } + } + `), + } + + ctx := context.TODO() + m, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, []string{}, nil) + require.NoError(t, err) + + require.Equal(t, 1, len(m)) + _, ok := m["app"] + require.True(t, ok) + + bo, err := TargetsToBuildOpt(m, &Input{}) + require.NoError(t, err) + + ctxs := bo["app"].Inputs.NamedContexts + require.Equal(t, 2, len(ctxs)) + + require.Equal(t, "baz", ctxs["foo"]) + require.Equal(t, "def", ctxs["abc"]) + + m, _, err = ReadTargets(ctx, []File{fp}, []string{"app"}, []string{"app.contexts.foo=bay", "base.contexts.ghi=jkl"}, nil) + require.NoError(t, err) + + require.Equal(t, 1, len(m)) + _, ok = m["app"] + require.True(t, ok) + + bo, err = TargetsToBuildOpt(m, &Input{}) + require.NoError(t, err) + + ctxs = bo["app"].Inputs.NamedContexts + require.Equal(t, 3, len(ctxs)) + + require.Equal(t, "bay", ctxs["foo"]) + require.Equal(t, "def", ctxs["abc"]) + require.Equal(t, "jkl", ctxs["ghi"]) + + // test resetting base values + m, _, err = ReadTargets(ctx, []File{fp}, []string{"app"}, []string{"app.contexts.foo="}, nil) + require.NoError(t, err) + + require.Equal(t, 1, len(m)) + _, ok = m["app"] + require.True(t, ok) + + bo, err = TargetsToBuildOpt(m, &Input{}) + require.NoError(t, err) + + ctxs = bo["app"].Inputs.NamedContexts + require.Equal(t, 1, len(ctxs)) + require.Equal(t, "def", ctxs["abc"]) +} + +func TestReadContextFromTargetUnknown(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte(` + target "base" { + contexts = { + foo: "bar" + abc: "def" + } + } + target "app" { + contexts = { + foo: "baz" + bar: "target:bar" + } + } + `), + } + + ctx := context.TODO() + _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, []string{}, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to find target bar") +} +func TestReadContextFromTargetChain(t *testing.T) { + ctx := context.TODO() + fp := File{ + Name: "docker-bake.hcl", + Data: []byte(` + target "base" { + } + target "mid" { + output = ["foo"] + contexts = { + parent: "target:base" + } + } + target "app" { + contexts = { + foo: "baz" + bar: "target:mid" + } + } + target "unused" {} + `), + } + + m, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, []string{}, nil) + require.NoError(t, err) + + require.Equal(t, 3, len(m)) + app, ok := m["app"] + require.True(t, ok) + + require.Equal(t, 2, len(app.Contexts)) + + mid, ok := m["mid"] + require.True(t, ok) + require.Equal(t, 0, len(mid.Outputs)) + require.Equal(t, 1, len(mid.Contexts)) + + base, ok := m["base"] + require.True(t, ok) + require.Equal(t, 0, len(base.Contexts)) +} + +func TestReadContextFromTargetInfiniteLoop(t *testing.T) { + ctx := context.TODO() + fp := File{ + Name: "docker-bake.hcl", + Data: []byte(` + target "mid" { + output = ["foo"] + contexts = { + parent: "target:app" + } + } + target "app" { + contexts = { + foo: "baz" + bar: "target:mid" + } + } + `), + } + _, _, err := ReadTargets(ctx, []File{fp}, []string{"app", "mid"}, []string{}, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "infinite loop from") +} + +func TestReadContextFromTargetMultiPlatform(t *testing.T) { + ctx := context.TODO() + fp := File{ + Name: "docker-bake.hcl", + Data: []byte(` + target "mid" { + output = ["foo"] + platforms = ["linux/amd64", "linux/arm64"] + } + target "app" { + contexts = { + bar: "target:mid" + } + platforms = ["linux/amd64", "linux/arm64"] + } + `), + } + _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, []string{}, nil) + require.NoError(t, err) +} + +func TestReadContextFromTargetInvalidPlatforms(t *testing.T) { + ctx := context.TODO() + fp := File{ + Name: "docker-bake.hcl", + Data: []byte(` + target "mid" { + output = ["foo"] + platforms = ["linux/amd64", "linux/riscv64"] + } + target "app" { + contexts = { + bar: "target:mid" + } + platforms = ["linux/amd64", "linux/arm64"] + } + `), + } + _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, []string{}, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "defined for different platforms") +} + func TestReadTargetsDefault(t *testing.T) { t.Parallel() ctx := context.TODO() diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 40f66846..c693dcb8 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -339,7 +339,7 @@ target "db" { Complete list of valid target fields: -`args`, `cache-from`, `cache-to`, `context`, `dockerfile`, `inherits`, `labels`, +`args`, `cache-from`, `cache-to`, `context`, `contexts`, `dockerfile`, `inherits`, `labels`, `no-cache`, `output`, `platform`, `pull`, `secrets`, `ssh`, `tags`, `target` ### Global scope attributes