Skip to content

Commit b53ff7b

Browse files
committed
feat: transparent buildx support for remote buildkit (#6732)
* detect-buildx global config option for backward compatibility * cache-tag global config option to customize cache destination * new CacheTo in DockerArtifact in configuration yaml (for docker build --cache-to) * export LoadDockerConfig to read ~/.docker/config.json for buildx detection * fix avoid loading image via buildx if no docker daemon is accessible * fix remote lookup / import missing in buildx workaround * fix image import if no docker daemon is available (under buildx) * adjust cache reference preserving tag and default cacheTo if not given * parse buildx metadata to extract ImageID digest Initially based on ebekebe's #8172 patch ebekebe@1c1fdeb Signed-off-by: [email protected]
1 parent 71bbee7 commit b53ff7b

File tree

13 files changed

+234
-34
lines changed

13 files changed

+234
-34
lines changed

pkg/skaffold/build/cache/cache.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type cache struct {
5858
isLocalImage func(imageName string) (bool, error)
5959
importMissingImage func(imageName string) (bool, error)
6060
lister DependencyLister
61+
buildx bool
6162
}
6263

6364
// DependencyLister fetches a list of dependencies for an artifact
@@ -69,6 +70,7 @@ type Config interface {
6970
GetPipelines() []latest.Pipeline
7071
DefaultPipeline() latest.Pipeline
7172
GetCluster() config.Cluster
73+
DetectBuildX() bool
7274
CacheArtifacts() bool
7375
CacheFile() string
7476
Mode() config.RunMode
@@ -118,6 +120,9 @@ func NewCache(ctx context.Context, cfg Config, isLocalImage func(imageName strin
118120
return pipeline.Build.LocalBuild.TryImportMissing, nil
119121
}
120122

123+
// for backward compatibility, extended build capabilities with BuildKit are disabled by default
124+
buildx := cfg.DetectBuildX()
125+
121126
return &cache{
122127
artifactCache: artifactCache,
123128
hashByName: hashByName,
@@ -129,6 +134,7 @@ func NewCache(ctx context.Context, cfg Config, isLocalImage func(imageName strin
129134
isLocalImage: isLocalImage,
130135
importMissingImage: importMissingImage,
131136
lister: dependencies,
137+
buildx: buildx,
132138
}, nil
133139
}
134140

pkg/skaffold/build/cache/lookup.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ func (c *cache) lookup(ctx context.Context, out io.Writer, a *latest.Artifact, t
8787
}
8888

8989
if isLocal, err := c.isLocalImage(a.ImageName); err != nil {
90+
log.Entry(ctx).Debugf("isLocalImage failed %w", err)
9091
return failed{err}
9192
} else if isLocal {
9293
return c.lookupLocal(ctx, hash, tag, entry)
@@ -122,9 +123,12 @@ func (c *cache) lookupLocal(ctx context.Context, hash, tag string, entry ImageDe
122123
func (c *cache) lookupRemote(ctx context.Context, hash, tag string, platforms []specs.Platform, entry ImageDetails) cacheDetails {
123124
if remoteDigest, err := docker.RemoteDigest(tag, c.cfg, nil); err == nil {
124125
// Image exists remotely with the same tag and digest
126+
log.Entry(ctx).Debugf("RemoteDigest: %s entry.Digest %s", remoteDigest, entry.Digest)
125127
if remoteDigest == entry.Digest {
126128
return found{hash: hash}
127129
}
130+
} else {
131+
log.Entry(ctx).Debugf("RemoteDigest error %w", err)
128132
}
129133

130134
// Image exists remotely with a different tag
@@ -152,23 +156,32 @@ func (c *cache) tryImport(ctx context.Context, a *latest.Artifact, tag string, h
152156
return ImageDetails{}, fmt.Errorf("import of missing images disabled")
153157
}
154158

155-
if !c.client.ImageExists(ctx, tag) {
156-
log.Entry(ctx).Debugf("Importing artifact %s from docker registry", tag)
157-
err := c.client.Pull(ctx, io.Discard, tag, pl)
159+
// under buildx, docker daemon is not really needed and could be disabled
160+
load := true
161+
if c.buildx {
162+
_, err := c.client.ServerVersion(ctx)
163+
load = err == nil
164+
if !load {
165+
log.Entry(ctx).Debugf("Docker client error, disabling image load as using buildx: %w", err)
166+
}
167+
}
168+
if load {
169+
if !c.client.ImageExists(ctx, tag) {
170+
log.Entry(ctx).Debugf("Importing artifact %s from docker registry", tag)
171+
err := c.client.Pull(ctx, io.Discard, tag, pl)
172+
if err != nil {
173+
return entry, err
174+
}
175+
} else {
176+
log.Entry(ctx).Debugf("Importing artifact %s from local docker", tag)
177+
}
178+
imageID, err := c.client.ImageID(ctx, tag)
158179
if err != nil {
159180
return entry, err
160181
}
161-
} else {
162-
log.Entry(ctx).Debugf("Importing artifact %s from local docker", tag)
163-
}
164-
165-
imageID, err := c.client.ImageID(ctx, tag)
166-
if err != nil {
167-
return entry, err
168-
}
169-
170-
if imageID != "" {
171-
entry.ID = imageID
182+
if imageID != "" {
183+
entry.ID = imageID
184+
}
172185
}
173186

174187
if digest, err := docker.RemoteDigest(tag, c.cfg, nil); err == nil {

pkg/skaffold/build/docker/docker.go

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ package docker
1919
import (
2020
"bytes"
2121
"context"
22+
"encoding/json"
2223
"fmt"
2324
"io"
2425
"os"
2526
"os/exec"
2627

2728
v1 "github.com/google/go-containerregistry/pkg/v1"
2829

30+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/config"
2931
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/docker"
3032
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/instrumentation"
3133
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/output"
@@ -46,7 +48,7 @@ func (b *Builder) Build(ctx context.Context, out io.Writer, a *latest.Artifact,
4648
if len(matcher.Platforms) == 1 {
4749
pl = util.ConvertToV1Platform(matcher.Platforms[0])
4850
}
49-
a = adjustCacheFrom(a, tag)
51+
a = b.adjustCache(ctx, a, tag)
5052
instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{
5153
"BuildType": "docker",
5254
"Context": instrumentation.PII(a.Workspace),
@@ -82,7 +84,7 @@ func (b *Builder) Build(ctx context.Context, out io.Writer, a *latest.Artifact,
8284
return "", newBuildError(err, b.cfg)
8385
}
8486

85-
if b.pushImages {
87+
if !b.useCLI && b.pushImages {
8688
// TODO (tejaldesai) Remove https://github.com/GoogleContainerTools/skaffold/blob/main/pkg/skaffold/errors/err_map.go#L56
8789
// and instead define a pushErr() method here.
8890
return b.localDocker.Push(ctx, out, tag)
@@ -120,10 +122,31 @@ func (b *Builder) dockerCLIBuild(ctx context.Context, out io.Writer, name string
120122
args = append(args, "--platform", pl.String())
121123
}
122124

123-
if b.useBuildKit != nil && *b.useBuildKit && !b.pushImages {
124-
args = append(args, "--load")
125+
if b.useBuildKit != nil && *b.useBuildKit {
126+
if !b.pushImages {
127+
load := true
128+
if b.buildx {
129+
// if docker daemon is not used, do not try to load the image (a buildx warning will be logged)
130+
_, err := b.localDocker.ServerVersion(ctx)
131+
load = err == nil
132+
}
133+
if load {
134+
args = append(args, "--load")
135+
}
136+
} else if b.buildx {
137+
// with buildx, push the image directly to the registry (not using the docker daemon)
138+
args = append(args, "--push")
139+
}
125140
}
126141

142+
// temporary file for buildx metadata containing the image digest:
143+
var metadata *os.File
144+
if b.buildx {
145+
metadata, err = os.CreateTemp("", "metadata.json")
146+
metadata.Close()
147+
defer os.Remove(metadata.Name())
148+
args = append(args, "--metadata-file", metadata.Name())
149+
}
127150
cmd := exec.CommandContext(ctx, "docker", args...)
128151
cmd.Env = append(util.OSEnviron(), b.localDocker.ExtraEnv()...)
129152
if b.useBuildKit != nil {
@@ -146,11 +169,16 @@ func (b *Builder) dockerCLIBuild(ctx context.Context, out io.Writer, name string
146169
return "", tryExecFormatErr(fmt.Errorf("running build: %w", err), errBuffer)
147170
}
148171

149-
return b.localDocker.ImageID(ctx, opts.Tag)
172+
if !b.buildx {
173+
return b.localDocker.ImageID(ctx, opts.Tag)
174+
} else {
175+
return getBuildxDigest(ctx, metadata.Name())
176+
}
150177
}
151178

152179
func (b *Builder) pullCacheFromImages(ctx context.Context, out io.Writer, a *latest.DockerArtifact, pl v1.Platform) error {
153-
if len(a.CacheFrom) == 0 {
180+
// when using buildx, avoid pulling as the builder not necessarily uses the local docker daemon
181+
if len(a.CacheFrom) == 0 || b.buildx {
154182
return nil
155183
}
156184

@@ -172,26 +200,81 @@ func (b *Builder) pullCacheFromImages(ctx context.Context, out io.Writer, a *lat
172200
return nil
173201
}
174202

175-
// adjustCacheFrom returns an artifact where any cache references from the artifactImage is changed to the tagged built image name instead.
176-
func adjustCacheFrom(a *latest.Artifact, artifactTag string) *latest.Artifact {
203+
// adjustCache returns an artifact where any cache references from the artifactImage is changed to the tagged built image name instead.
204+
// Under buildx, if cache-tag is configured, it will be used instead of the generated artifact tag (registry preserved)
205+
// if no cacheTo was specified in the skaffold yaml, it will add a tagged destination using the same cache source reference
206+
func (b *Builder) adjustCache(ctx context.Context, a *latest.Artifact, artifactTag string) *latest.Artifact {
207+
cacheRef := a.ImageName // lookup value to be replaced
208+
cacheTag := artifactTag // full reference to be used
177209
if os.Getenv("SKAFFOLD_DISABLE_DOCKER_CACHE_ADJUSTMENT") != "" {
178210
// allow this behaviour to be disabled
179211
return a
180212
}
181-
182-
if !stringslice.Contains(a.DockerArtifact.CacheFrom, a.ImageName) {
213+
if b.buildx {
214+
// compute the full cache reference (including registry, preserving tag)
215+
tag, _ := config.GetCacheTag(b.cfg.GlobalConfig())
216+
imgRef, err := docker.ParseReference(artifactTag)
217+
if err != nil {
218+
log.Entry(ctx).Errorf("couldn't parse image tag: %w", err)
219+
} else if tag != "" {
220+
cacheTag = fmt.Sprintf("%s/%s:%s", imgRef.Repo, cacheRef, tag)
221+
}
222+
}
223+
if !stringslice.Contains(a.DockerArtifact.CacheFrom, cacheRef) {
183224
return a
184225
}
185226

186227
cf := make([]string, 0, len(a.DockerArtifact.CacheFrom))
228+
ct := make([]string, 0, len(a.DockerArtifact.CacheTo))
187229
for _, image := range a.DockerArtifact.CacheFrom {
188-
if image == a.ImageName {
189-
cf = append(cf, artifactTag)
230+
if image == cacheRef {
231+
// change cache reference to to the tagged image name (built or given, including registry)
232+
log.Entry(ctx).Debugf("Adjusting cache source image ref: %s", cacheTag)
233+
cf = append(cf, cacheTag)
234+
if b.buildx {
235+
// add cache destination reference, only if we're pushing to a registry and not given in config
236+
if len(a.DockerArtifact.CacheTo) == 0 && b.pushImages {
237+
log.Entry(ctx).Debugf("Adjusting cache destination image ref: %s", cacheTag)
238+
ct = append(ct, fmt.Sprintf("type=registry,ref=%s,mode=max", cacheTag))
239+
}
240+
}
190241
} else {
191242
cf = append(cf, image)
192243
}
193244
}
245+
// just copy any other cache destination given in the config file:
246+
for _, image := range a.DockerArtifact.CacheTo {
247+
ct = append(cf, image)
248+
}
194249
copy := *a
195250
copy.DockerArtifact.CacheFrom = cf
251+
copy.DockerArtifact.CacheTo = ct
196252
return &copy
197253
}
254+
255+
func getBuildxDigest(ctx context.Context, filename string) (string, error) {
256+
var metadata map[string]interface{}
257+
data, err := os.ReadFile(filename)
258+
if err == nil {
259+
err = json.Unmarshal(data, &metadata)
260+
}
261+
if err == nil {
262+
// avoid panic: interface conversion: interface {} is nil, not string (if keys don't exists)
263+
var digest string
264+
if value := metadata["containerimage.digest"]; value != nil {
265+
digest = value.(string)
266+
}
267+
var name string
268+
if value := metadata["image.name"]; value != nil {
269+
name = value.(string)
270+
}
271+
if digest != "" {
272+
log.Entry(ctx).Debugf("Image digest found in buildx metadata: %s for %s", digest, name)
273+
return digest, nil
274+
}
275+
}
276+
log.Entry(ctx).Warnf("No digest found in buildx metadata: %w", err)
277+
// if image is not pushed, it could not contain the digest log for debugging:
278+
log.Entry(ctx).Debugf("Full buildx metadata: %s", data)
279+
return "", err
280+
}

pkg/skaffold/build/docker/types.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Builder struct {
3030
pushImages bool
3131
useCLI bool
3232
useBuildKit *bool
33+
buildx bool
3334
artifacts ArtifactResolver
3435
sourceDependencies TransitiveSourceDependenciesResolver
3536
}
@@ -45,13 +46,14 @@ type TransitiveSourceDependenciesResolver interface {
4546
}
4647

4748
// NewBuilder returns an new instance of a docker builder
48-
func NewArtifactBuilder(localDocker docker.LocalDaemon, cfg docker.Config, useCLI bool, useBuildKit *bool, pushImages bool, ar ArtifactResolver, dr TransitiveSourceDependenciesResolver) *Builder {
49+
func NewArtifactBuilder(localDocker docker.LocalDaemon, cfg docker.Config, useCLI bool, useBuildKit *bool, buildx bool, pushImages bool, ar ArtifactResolver, dr TransitiveSourceDependenciesResolver) *Builder {
4950
return &Builder{
5051
localDocker: localDocker,
5152
pushImages: pushImages,
5253
cfg: cfg,
5354
useCLI: useCLI,
5455
useBuildKit: useBuildKit,
56+
buildx: buildx,
5557
artifacts: ar,
5658
sourceDependencies: dr,
5759
}

pkg/skaffold/build/local/local.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ func (b *Builder) buildArtifact(ctx context.Context, out io.Writer, a *latest.Ar
8989
if b.pushImages {
9090
// only track images for pruning when building with docker
9191
// if we're pushing a bazel image, it was built directly to the registry
92+
// buildx also has its own build cache, and image load will not be attempted if no docker daemon is accessible
9293
if a.DockerArtifact != nil {
9394
imageID, err := b.getImageIDForTag(ctx, tag)
94-
if err != nil {
95+
if err != nil && !b.buildx {
9596
log.Entry(ctx).Warn("unable to inspect image: built images may not be cleaned up correctly by skaffold")
9697
}
9798
if imageID != "" {
@@ -103,6 +104,8 @@ func (b *Builder) buildArtifact(ctx context.Context, out io.Writer, a *latest.Ar
103104
return build.TagWithDigest(tag, digest), nil
104105
}
105106

107+
// TODO: prune buildx cache using digest?
108+
106109
imageID := digestOrImageID
107110
if b.mode == config.RunModes.Dev {
108111
artifacts, err := b.artifactStore.GetArtifacts([]*latest.Artifact{a})
@@ -123,8 +126,8 @@ func (b *Builder) buildArtifact(ctx context.Context, out io.Writer, a *latest.Ar
123126
}
124127

125128
func (b *Builder) runBuildForArtifact(ctx context.Context, out io.Writer, a *latest.Artifact, tag string, platforms platform.Matcher) (string, error) {
126-
if !b.pushImages {
127-
// All of the builders will rely on a local Docker:
129+
if !b.buildx && !b.pushImages {
130+
// Most builders will rely on a local Docker (except when using a remote buildkit via buildx):
128131
// + Either to build the image,
129132
// + Or to docker load it.
130133
// Let's fail fast if Docker is not available

pkg/skaffold/build/local/types.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type Builder struct {
5252
skipTests bool
5353
mode config.RunMode
5454
kubeContext string
55+
buildx bool
5556
builtImages []string
5657
insecureRegistries map[string]bool
5758
muted build.Muted
@@ -66,6 +67,7 @@ type Config interface {
6667
GlobalConfig() string
6768
GetKubeContext() string
6869
GetCluster() config.Cluster
70+
DetectBuildX() bool
6971
SkipTests() bool
7072
Mode() config.RunMode
7173
NoPruneChildren() bool
@@ -103,6 +105,9 @@ func NewBuilder(ctx context.Context, bCtx BuilderContext, buildCfg *latest.Local
103105

104106
tryImportMissing := buildCfg.TryImportMissing
105107

108+
// for backward compatibility, extended build capabilities with BuildKit are disabled by default
109+
buildx := bCtx.DetectBuildX()
110+
106111
return &Builder{
107112
local: *buildCfg,
108113
cfg: bCtx,
@@ -111,6 +116,7 @@ func NewBuilder(ctx context.Context, bCtx BuilderContext, buildCfg *latest.Local
111116
localCluster: cluster.Local,
112117
pushImages: pushImages,
113118
tryImportMissing: tryImportMissing,
119+
buildx: buildx,
114120
skipTests: bCtx.SkipTests(),
115121
mode: bCtx.Mode(),
116122
prune: bCtx.Prune(),
@@ -133,7 +139,7 @@ type artifactBuilder interface {
133139
func newPerArtifactBuilder(b *Builder, a *latest.Artifact) (artifactBuilder, error) {
134140
switch {
135141
case a.DockerArtifact != nil:
136-
return dockerbuilder.NewArtifactBuilder(b.localDocker, b.cfg, b.local.UseDockerCLI, b.local.UseBuildkit, b.pushImages, b.artifactStore, b.sourceDependencies), nil
142+
return dockerbuilder.NewArtifactBuilder(b.localDocker, b.cfg, b.local.UseDockerCLI, b.local.UseBuildkit, b.buildx, b.pushImages, b.artifactStore, b.sourceDependencies), nil
137143

138144
case a.BazelArtifact != nil:
139145
return bazel.NewArtifactBuilder(b.localDocker, b.cfg, b.pushImages), nil

pkg/skaffold/config/global_config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ type ContextConfig struct {
3333
InsecureRegistries []string `yaml:"insecure-registries,omitempty"`
3434
// DebugHelpersRegistry is the registry from which the debug helper images are used.
3535
DebugHelpersRegistry string `yaml:"debug-helpers-registry,omitempty"`
36+
CacheTag string `yaml:"cache-tag,omitempty"`
37+
DetectBuildX *bool `yaml:"detect-buildx,omitempty"`
3638
UpdateCheck *bool `yaml:"update-check,omitempty"`
3739
Survey *SurveyConfig `yaml:"survey,omitempty"`
3840
KindDisableLoad *bool `yaml:"kind-disable-load,omitempty"`

0 commit comments

Comments
 (0)