mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 09:17:49 +08:00
Merge pull request #928 from tonistiigi/bake-named-contexts
bake: add named contexts keys
This commit is contained in:
commit
60a025b227
89
bake/bake.go
89
bake/bake.go
@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -130,6 +131,12 @@ func ReadTargets(ctx context.Context, files []File, targets, overrides []string,
|
|||||||
g = []*Group{{Targets: dedupString(gt)}}
|
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
|
return m, g, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,10 +310,45 @@ func (c Config) expandTargets(pattern string) ([]string, error) {
|
|||||||
return names, nil
|
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) {
|
func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) {
|
||||||
m := map[string]map[string]Override{}
|
m := map[string]map[string]Override{}
|
||||||
for _, v := range v {
|
for _, v := range v {
|
||||||
|
|
||||||
parts := strings.SplitN(v, "=", 2)
|
parts := strings.SplitN(v, "=", 2)
|
||||||
keys := strings.SplitN(parts[0], ".", 3)
|
keys := strings.SplitN(parts[0], ".", 3)
|
||||||
if len(keys) < 2 {
|
if len(keys) < 2 {
|
||||||
@ -351,6 +393,11 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error)
|
|||||||
o.Value = v
|
o.Value = v
|
||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
|
case "contexts":
|
||||||
|
if len(keys) != 3 {
|
||||||
|
return nil, errors.Errorf("invalid key %s, contexts requires name", parts[0])
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
default:
|
default:
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
o.Value = parts[1]
|
o.Value = parts[1]
|
||||||
@ -461,6 +508,7 @@ type Target struct {
|
|||||||
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional"`
|
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional"`
|
||||||
|
|
||||||
Context *string `json:"context,omitempty" hcl:"context,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"`
|
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional"`
|
||||||
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional"`
|
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional"`
|
||||||
Args map[string]string `json:"args,omitempty" hcl:"args,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.CacheFrom = removeDupes(t.CacheFrom)
|
||||||
t.CacheTo = removeDupes(t.CacheTo)
|
t.CacheTo = removeDupes(t.CacheTo)
|
||||||
t.Outputs = removeDupes(t.Outputs)
|
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) {
|
func (t *Target) Merge(t2 *Target) {
|
||||||
@ -506,6 +563,12 @@ func (t *Target) Merge(t2 *Target) {
|
|||||||
}
|
}
|
||||||
t.Args[k] = v
|
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 {
|
for k, v := range t2.Labels {
|
||||||
if t.Labels == nil {
|
if t.Labels == nil {
|
||||||
t.Labels = map[string]string{}
|
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 = map[string]string{}
|
||||||
}
|
}
|
||||||
t.Args[keys[1]] = value
|
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":
|
case "labels":
|
||||||
if len(keys) != 2 {
|
if len(keys) != 2 {
|
||||||
return errors.Errorf("labels require name")
|
return errors.Errorf("labels require name")
|
||||||
@ -693,6 +763,7 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
|||||||
bi := build.Inputs{
|
bi := build.Inputs{
|
||||||
ContextPath: contextPath,
|
ContextPath: contextPath,
|
||||||
DockerfilePath: dockerfilePath,
|
DockerfilePath: dockerfilePath,
|
||||||
|
NamedContexts: t.Contexts,
|
||||||
}
|
}
|
||||||
if t.DockerfileInline != nil {
|
if t.DockerfileInline != nil {
|
||||||
bi.DockerfileInline = *t.DockerfileInline
|
bi.DockerfileInline = *t.DockerfileInline
|
||||||
@ -811,3 +882,17 @@ func validateTargetName(name string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@ -353,6 +353,208 @@ func TestOverrideMerge(t *testing.T) {
|
|||||||
require.Equal(t, "type=registry", m["app"].Outputs[0])
|
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) {
|
func TestReadTargetsDefault(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
|
145
build/build.go
145
build/build.go
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/docker/buildx/util/imagetools"
|
"github.com/docker/buildx/util/imagetools"
|
||||||
"github.com/docker/buildx/util/progress"
|
"github.com/docker/buildx/util/progress"
|
||||||
"github.com/docker/buildx/util/resolver"
|
"github.com/docker/buildx/util/resolver"
|
||||||
|
"github.com/docker/buildx/util/waitmap"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
@ -34,6 +35,7 @@ import (
|
|||||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||||
"github.com/moby/buildkit/session"
|
"github.com/moby/buildkit/session"
|
||||||
"github.com/moby/buildkit/session/upload/uploadprovider"
|
"github.com/moby/buildkit/session/upload/uploadprovider"
|
||||||
|
"github.com/moby/buildkit/solver/pb"
|
||||||
"github.com/moby/buildkit/util/apicaps"
|
"github.com/moby/buildkit/util/apicaps"
|
||||||
"github.com/moby/buildkit/util/entitlements"
|
"github.com/moby/buildkit/util/entitlements"
|
||||||
"github.com/moby/buildkit/util/progress/progresswriter"
|
"github.com/moby/buildkit/util/progress/progresswriter"
|
||||||
@ -667,8 +669,35 @@ func Build(ctx context.Context, drivers []DriverInfo, opt map[string]Options, do
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate that all links between targets use same drivers
|
||||||
|
for name := range opt {
|
||||||
|
dps := m[name]
|
||||||
|
for _, dp := range dps {
|
||||||
|
for k, v := range dp.so.FrontendAttrs {
|
||||||
|
if strings.HasPrefix(k, "context:") && strings.HasPrefix(v, "target:") {
|
||||||
|
k2 := strings.TrimPrefix(v, "target:")
|
||||||
|
dps2, ok := m[k2]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("failed to find target %s for context %s", k2, strings.TrimPrefix(k, "context:")) // should be validated before already
|
||||||
|
}
|
||||||
|
var found bool
|
||||||
|
for _, dp2 := range dps2 {
|
||||||
|
if dp2.driverIndex == dp.driverIndex {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, errors.Errorf("failed to use %s as context %s for %s because targets build with different drivers", k2, strings.TrimPrefix(k, "context:"), name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp = map[string]*client.SolveResponse{}
|
resp = map[string]*client.SolveResponse{}
|
||||||
var respMu sync.Mutex
|
var respMu sync.Mutex
|
||||||
|
results := waitmap.New()
|
||||||
|
|
||||||
multiTarget := len(opt) > 1
|
multiTarget := len(opt) > 1
|
||||||
|
|
||||||
@ -793,7 +822,6 @@ func Build(ctx context.Context, drivers []DriverInfo, opt map[string]Options, do
|
|||||||
|
|
||||||
for i, dp := range dps {
|
for i, dp := range dps {
|
||||||
so := *dp.so
|
so := *dp.so
|
||||||
|
|
||||||
if multiDriver {
|
if multiDriver {
|
||||||
for i, e := range so.Exports {
|
for i, e := range so.Exports {
|
||||||
switch e.Type {
|
switch e.Type {
|
||||||
@ -826,14 +854,42 @@ func Build(ctx context.Context, drivers []DriverInfo, opt map[string]Options, do
|
|||||||
pw := progress.WithPrefix(w, k, multiTarget)
|
pw := progress.WithPrefix(w, k, multiTarget)
|
||||||
|
|
||||||
c := clients[dp.driverIndex]
|
c := clients[dp.driverIndex]
|
||||||
|
|
||||||
pw = progress.ResetTime(pw)
|
|
||||||
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
|
if err := waitContextDeps(ctx, dp.driverIndex, results, &so); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pw = progress.ResetTime(pw)
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
ch, done := progress.NewChannel(pw)
|
ch, done := progress.NewChannel(pw)
|
||||||
defer func() { <-done }()
|
defer func() { <-done }()
|
||||||
rr, err := c.Solve(ctx, nil, so, ch)
|
|
||||||
|
frontendInputs := make(map[string]*pb.Definition)
|
||||||
|
for key, st := range so.FrontendInputs {
|
||||||
|
def, err := st.Marshal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
frontendInputs[key] = def.ToPB()
|
||||||
|
}
|
||||||
|
|
||||||
|
req := gateway.SolveRequest{
|
||||||
|
Frontend: so.Frontend,
|
||||||
|
FrontendOpt: so.FrontendAttrs,
|
||||||
|
FrontendInputs: frontendInputs,
|
||||||
|
}
|
||||||
|
so.Frontend = ""
|
||||||
|
so.FrontendAttrs = nil
|
||||||
|
so.FrontendInputs = nil
|
||||||
|
|
||||||
|
rr, err := c.Build(ctx, so, "buildx", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||||
|
res, err := c.Solve(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results.Set(resultKey(dp.driverIndex, k), res)
|
||||||
|
return res, nil
|
||||||
|
}, ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -1084,7 +1140,7 @@ func LoadInputs(ctx context.Context, d driver.Driver, inp Inputs, pw progress.Wr
|
|||||||
|
|
||||||
for k, v := range inp.NamedContexts {
|
for k, v := range inp.NamedContexts {
|
||||||
target.FrontendAttrs["frontend.caps"] = "moby.buildkit.frontend.contexts+forward"
|
target.FrontendAttrs["frontend.caps"] = "moby.buildkit.frontend.contexts+forward"
|
||||||
if urlutil.IsGitURL(v) || urlutil.IsURL(v) || strings.HasPrefix(v, "docker-image://") {
|
if urlutil.IsGitURL(v) || urlutil.IsURL(v) || strings.HasPrefix(v, "docker-image://") || strings.HasPrefix(v, "target:") {
|
||||||
target.FrontendAttrs["context:"+k] = v
|
target.FrontendAttrs["context:"+k] = v
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -1111,6 +1167,83 @@ func LoadInputs(ctx context.Context, d driver.Driver, inp Inputs, pw progress.Wr
|
|||||||
return release, nil
|
return release, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resultKey(index int, name string) string {
|
||||||
|
return fmt.Sprintf("%d-%s", index, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitContextDeps(ctx context.Context, index int, results *waitmap.Map, so *client.SolveOpt) error {
|
||||||
|
m := map[string]string{}
|
||||||
|
for k, v := range so.FrontendAttrs {
|
||||||
|
if strings.HasPrefix(k, "context:") && strings.HasPrefix(v, "target:") {
|
||||||
|
target := resultKey(index, strings.TrimPrefix(v, "target:"))
|
||||||
|
m[target] = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
res, err := results.Get(ctx, keys...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range m {
|
||||||
|
r, ok := res[k]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rr, ok := r.(*gateway.Result)
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("invalid result type %T", rr)
|
||||||
|
}
|
||||||
|
if so.FrontendAttrs == nil {
|
||||||
|
so.FrontendAttrs = map[string]string{}
|
||||||
|
}
|
||||||
|
if so.FrontendInputs == nil {
|
||||||
|
so.FrontendInputs = map[string]llb.State{}
|
||||||
|
}
|
||||||
|
if len(rr.Refs) > 0 {
|
||||||
|
for platform, r := range rr.Refs {
|
||||||
|
st, err := r.ToState()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
so.FrontendInputs[k+"::"+platform] = st
|
||||||
|
so.FrontendAttrs[v+"::"+platform] = "input:" + k + "::" + platform
|
||||||
|
dt, ok := rr.Metadata["containerimage.config/"+platform]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dt, err = json.Marshal(map[string][]byte{"containerimage.config": dt})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
so.FrontendAttrs["input-metadata:"+k+"::"+platform] = string(dt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rr.Ref != nil {
|
||||||
|
st, err := rr.Ref.ToState()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
so.FrontendInputs[k] = st
|
||||||
|
so.FrontendAttrs[v] = "input:" + k
|
||||||
|
if dt, ok := rr.Metadata["containerimage.config"]; ok {
|
||||||
|
dt, err = json.Marshal(map[string][]byte{"containerimage.config": dt})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
so.FrontendAttrs["input-metadata:"+k] = string(dt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func notSupported(d driver.Driver, f driver.Feature) error {
|
func notSupported(d driver.Driver, f driver.Feature) error {
|
||||||
return errors.Errorf("%s feature is currently not supported for %s driver. Please switch to a different driver (eg. \"docker buildx create --use\")", f, d.Factory().Name())
|
return errors.Errorf("%s feature is currently not supported for %s driver. Please switch to a different driver (eg. \"docker buildx create --use\")", f, d.Factory().Name())
|
||||||
}
|
}
|
||||||
|
@ -339,7 +339,7 @@ target "db" {
|
|||||||
|
|
||||||
Complete list of valid target fields:
|
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`
|
`no-cache`, `output`, `platform`, `pull`, `secrets`, `ssh`, `tags`, `target`
|
||||||
|
|
||||||
### Global scope attributes
|
### Global scope attributes
|
||||||
@ -799,6 +799,78 @@ $ docker buildx bake --print app
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Defining additional build contexts and linking targets
|
||||||
|
|
||||||
|
In addition to the main `context` key that defines the build context each target can also define additional named contexts with a map defined with key `contexts`. These values map to the `--build-context` flag in the [build command](buildx_build.md#build-context).
|
||||||
|
|
||||||
|
Inside the Dockerfile these contexts can be used with the `FROM` instruction or `--from` flag.
|
||||||
|
|
||||||
|
The value can be a local source directory, container image (with docker-image:// prefix), Git URL, HTTP URL or a name of another target in the Bake file (with target: prefix).
|
||||||
|
|
||||||
|
#### Pinning alpine image
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
# Dockerfile
|
||||||
|
FROM alpine
|
||||||
|
RUN echo "Hello world"
|
||||||
|
```
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# docker-bake.hcl
|
||||||
|
target "app" {
|
||||||
|
contexts = {
|
||||||
|
alpine = "docker-image://alpine:3.13"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using a secondary source directory
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
# Dockerfile
|
||||||
|
|
||||||
|
FROM scratch AS src
|
||||||
|
|
||||||
|
FROM golang
|
||||||
|
COPY --from=src . .
|
||||||
|
```
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# docker-bake.hcl
|
||||||
|
target "app" {
|
||||||
|
contexts = {
|
||||||
|
src = "../path/to/source"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using a result of one target as a base image in another target
|
||||||
|
|
||||||
|
To use a result of one target as a build context of another, specity the target name with `target:` prefix.
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
# Dockerfile
|
||||||
|
FROM baseapp
|
||||||
|
RUN echo "Hello world"
|
||||||
|
```
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# docker-bake.hcl
|
||||||
|
|
||||||
|
target "base" {
|
||||||
|
dockerfile = "baseapp.Dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
target "app" {
|
||||||
|
contexts = {
|
||||||
|
baseapp = "target:base"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that in most cases you should just use a single multi-stage Dockerfile with multiple targets for similar behavior. This case is recommended when you have multiple Dockerfiles that can't be easily merged into one.
|
||||||
|
|
||||||
|
|
||||||
### Extension field with Compose
|
### Extension field with Compose
|
||||||
|
|
||||||
[Special extension](https://github.com/compose-spec/compose-spec/blob/master/spec.md#extension)
|
[Special extension](https://github.com/compose-spec/compose-spec/blob/master/spec.md#extension)
|
||||||
|
74
util/waitmap/waitmap.go
Normal file
74
util/waitmap/waitmap.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package waitmap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Map struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
m map[string]interface{}
|
||||||
|
ch map[string]chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Map {
|
||||||
|
return &Map{
|
||||||
|
m: make(map[string]interface{}),
|
||||||
|
ch: make(map[string]chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Map) Set(key string, value interface{}) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
m.m[key] = value
|
||||||
|
|
||||||
|
if ch, ok := m.ch[key]; ok {
|
||||||
|
if ch != nil {
|
||||||
|
close(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.ch[key] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Map) Get(ctx context.Context, keys ...string) (map[string]interface{}, error) {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return map[string]interface{}{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keys) > 1 {
|
||||||
|
out := make(map[string]interface{})
|
||||||
|
for _, key := range keys {
|
||||||
|
mm, err := m.Get(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out[key] = mm[key]
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key := keys[0]
|
||||||
|
m.mu.Lock()
|
||||||
|
ch, ok := m.ch[key]
|
||||||
|
if !ok {
|
||||||
|
ch = make(chan struct{})
|
||||||
|
m.ch[key] = ch
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch != nil {
|
||||||
|
m.mu.Unlock()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-ch:
|
||||||
|
m.mu.Lock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := m.m[key]
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
return map[string]interface{}{key: res}, nil
|
||||||
|
}
|
64
util/waitmap/waitmap_test.go
Normal file
64
util/waitmap/waitmap_test.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package waitmap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetAfter(t *testing.T) {
|
||||||
|
m := New()
|
||||||
|
|
||||||
|
m.Set("foo", "bar")
|
||||||
|
m.Set("bar", "baz")
|
||||||
|
|
||||||
|
ctx := context.TODO()
|
||||||
|
v, err := m.Get(ctx, "foo", "bar")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, 2, len(v))
|
||||||
|
require.Equal(t, "bar", v["foo"])
|
||||||
|
require.Equal(t, "baz", v["bar"])
|
||||||
|
|
||||||
|
v, err = m.Get(ctx, "foo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(v))
|
||||||
|
require.Equal(t, "bar", v["foo"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeout(t *testing.T) {
|
||||||
|
m := New()
|
||||||
|
|
||||||
|
m.Set("foo", "bar")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := m.Get(ctx, "bar")
|
||||||
|
require.Error(t, err)
|
||||||
|
require.True(t, errors.Is(err, context.DeadlineExceeded))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlocking(t *testing.T) {
|
||||||
|
m := New()
|
||||||
|
|
||||||
|
m.Set("foo", "bar")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
m.Set("bar", "baz")
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
m.Set("baz", "abc")
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx := context.TODO()
|
||||||
|
v, err := m.Get(ctx, "foo", "bar", "baz")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 3, len(v))
|
||||||
|
require.Equal(t, "bar", v["foo"])
|
||||||
|
require.Equal(t, "baz", v["bar"])
|
||||||
|
require.Equal(t, "abc", v["baz"])
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user