bake: add named contexts keys

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi 2022-01-24 16:38:33 -08:00
parent 9a1267cd02
commit 0fc2b5ca85
3 changed files with 290 additions and 3 deletions

View File

@ -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
}

View File

@ -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()

View File

@ -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