Skip to content

Commit f15dc7a

Browse files
committed
Fix docker_runner on mac.
I have no idea if this breaks otehr stuff, but it fixed the issue for me. Signed-off-by: Dan Lorenc <dlorenc@chainguard.dev>
1 parent 8f00e84 commit f15dc7a

File tree

2 files changed

+252
-2
lines changed

2 files changed

+252
-2
lines changed

pkg/container/docker/docker_runner.go

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@
1515
package docker
1616

1717
import (
18+
"archive/tar"
19+
"bytes"
1820
"context"
1921
"fmt"
2022
"io"
2123
"os"
24+
"runtime"
25+
"strings"
2226

2327
"go.opentelemetry.io/otel"
2428
"golang.org/x/sync/errgroup"
@@ -38,6 +42,7 @@ import (
3842
"github.com/docker/docker/pkg/stdcopy"
3943
v1 "github.com/google/go-containerregistry/pkg/v1"
4044
"github.com/google/go-containerregistry/pkg/v1/empty"
45+
"github.com/google/go-containerregistry/pkg/v1/tarball"
4146
image_spec "github.com/opencontainers/image-spec/specs-go/v1"
4247
)
4348

@@ -366,9 +371,106 @@ type dockerLoader struct {
366371
cli *client.Client
367372
}
368373

374+
// filterXattrsForMacOS creates a wrapped layer that filters xattrs
375+
// mode can be "problematic" (only filter known problematic xattrs) or "all" (filter all xattrs)
376+
func filterXattrsForMacOS(ctx context.Context, originalLayer v1.Layer, mode string) (v1.Layer, error) {
377+
log := clog.FromContext(ctx)
378+
log.Debugf("Filtering %s xattrs for MacOS compatibility", mode)
379+
380+
rc, err := originalLayer.Uncompressed()
381+
if err != nil {
382+
return nil, err
383+
}
384+
defer rc.Close()
385+
386+
// Create a buffer for the new layer content
387+
var buf bytes.Buffer
388+
389+
// Process the tar file, filtering xattrs
390+
tr := tar.NewReader(rc)
391+
tw := tar.NewWriter(&buf)
392+
393+
for {
394+
hdr, err := tr.Next()
395+
if err == io.EOF {
396+
break
397+
}
398+
if err != nil {
399+
return nil, err
400+
}
401+
402+
// Filter out xattrs based on mode
403+
if hdr.PAXRecords != nil {
404+
filteredPAXRecords := make(map[string]string)
405+
for k, v := range hdr.PAXRecords {
406+
if mode == "all" {
407+
// In "all" mode, strip all xattrs
408+
if strings.HasPrefix(k, "SCHILY.xattr.") {
409+
log.Debugf("Filtering xattr %s for file %s", k, hdr.Name)
410+
continue
411+
}
412+
} else {
413+
// In "problematic" mode (default), only strip problematic xattrs
414+
if strings.HasPrefix(k, "SCHILY.xattr.com.apple.") ||
415+
strings.HasPrefix(k, "SCHILY.xattr.com.docker.") {
416+
log.Debugf("Filtering xattr %s for file %s", k, hdr.Name)
417+
continue
418+
}
419+
}
420+
filteredPAXRecords[k] = v
421+
}
422+
hdr.PAXRecords = filteredPAXRecords
423+
}
424+
425+
if err := tw.WriteHeader(hdr); err != nil {
426+
return nil, err
427+
}
428+
429+
if hdr.Typeflag == tar.TypeReg {
430+
if _, err := io.Copy(tw, tr); err != nil {
431+
return nil, err
432+
}
433+
}
434+
}
435+
436+
if err := tw.Close(); err != nil {
437+
return nil, err
438+
}
439+
440+
// Create a new layer from the filtered content
441+
layerReader := func() (io.ReadCloser, error) {
442+
return io.NopCloser(bytes.NewReader(buf.Bytes())), nil
443+
}
444+
445+
// Create a new layer from the opener function
446+
layer, err := tarball.LayerFromOpener(layerReader)
447+
if err != nil {
448+
return nil, err
449+
}
450+
451+
return layer, nil
452+
}
453+
369454
func (d *dockerLoader) LoadImage(ctx context.Context, layer v1.Layer, arch apko_types.Architecture, bc *apko_build.Context) (string, error) {
370455
ctx, span := otel.Tracer("melange").Start(ctx, "docker.LoadImage")
371456
defer span.End()
457+
458+
log := clog.FromContext(ctx)
459+
460+
// Detect MacOS platform
461+
isMacOS := runtime.GOOS == "darwin"
462+
if isMacOS {
463+
log.Debug("Detected MacOS platform, using modified image loading approach")
464+
465+
// First try: Filter only known problematic xattrs on MacOS
466+
filteredLayer, err := filterXattrsForMacOS(ctx, layer, "problematic")
467+
if err != nil {
468+
log.Warnf("Failed to filter xattrs for MacOS compatibility: %v", err)
469+
log.Warn("Continuing with original layer, but this may cause errors")
470+
} else {
471+
layer = filteredLayer
472+
}
473+
}
372474

373475
creationTime, err := bc.GetBuildDateEpoch()
374476
if err != nil {
@@ -380,10 +482,49 @@ func (d *dockerLoader) LoadImage(ctx context.Context, layer v1.Layer, arch apko_
380482
return "", err
381483
}
382484

485+
// Try to load the image
383486
ref, err := apko_oci.LoadImage(ctx, img, []string{"melange:latest"})
384-
if err != nil {
487+
if err != nil && isMacOS {
488+
// On MacOS, if loading fails, we might still have xattr errors
489+
log.Warnf("Initial image load failed on MacOS: %v", err)
490+
491+
// If we're on MacOS and got an error related to xattrs, try again with more aggressive filtering
492+
if strings.Contains(err.Error(), "xattr") {
493+
log.Info("Attempting more aggressive xattr filtering...")
494+
495+
// Try second approach: Filter ALL xattrs from the layer
496+
filteredLayer, err := filterXattrsForMacOS(ctx, layer, "all")
497+
if err != nil {
498+
log.Warnf("Failed to perform aggressive xattr filtering: %v", err)
499+
return "", fmt.Errorf("failed to create MacOS-compatible layer: %w", err)
500+
}
501+
502+
// Build a new image with the aggressively filtered layer
503+
img, err = apko_oci.BuildImageFromLayer(ctx, empty.Image, filteredLayer, bc.ImageConfiguration(), creationTime, arch)
504+
if err != nil {
505+
return "", err
506+
}
507+
508+
// Try again with the fully filtered image
509+
ref, err = apko_oci.LoadImage(ctx, img, []string{"melange:latest"})
510+
if err != nil {
511+
log.Warnf("Failed even with aggressive xattr filtering: %v", err)
512+
513+
// Last resort - add explicit error handling advising the user
514+
if strings.Contains(err.Error(), "xattr") {
515+
return "", fmt.Errorf("unable to handle MacOS xattr issues: %w\n"+
516+
"Consider using the QEMU runner instead with MELANGE_EXTRA_OPTS=\"--runner=qemu\"", err)
517+
}
518+
return "", err
519+
}
520+
log.Info("Successfully loaded image with aggressive xattr filtering")
521+
} else {
522+
return "", err
523+
}
524+
} else if err != nil {
385525
return "", err
386526
}
527+
387528
return ref.String(), nil
388529
}
389530

@@ -408,4 +549,4 @@ func (d *dockerLoader) RemoveImage(ctx context.Context, ref string) error {
408549
}
409550

410551
return nil
411-
}
552+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2022 Chainguard, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package docker
16+
17+
import (
18+
"archive/tar"
19+
"strings"
20+
"testing"
21+
22+
"github.com/chainguard-dev/clog/slogtest"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
// Test the filterXattrsForMacOS function directly with simple input
27+
func TestXattrFiltering(t *testing.T) {
28+
// No need to use ctx in this simplified test
29+
_ = slogtest.Context(t)
30+
31+
tests := []struct {
32+
name string
33+
mode string
34+
inputPAXRecords map[string]string
35+
wantPAXRecords map[string]string
36+
}{
37+
{
38+
name: "problematic mode removes apple and docker xattrs",
39+
mode: "problematic",
40+
inputPAXRecords: map[string]string{
41+
"SCHILY.xattr.com.apple.provenance": "apple-data",
42+
"SCHILY.xattr.com.docker.grpcfuse.ownership": "docker-data",
43+
"SCHILY.xattr.user.normal": "should-keep",
44+
"APK-TOOLS.checksum.SHA1": "checksum-value",
45+
},
46+
wantPAXRecords: map[string]string{
47+
"SCHILY.xattr.user.normal": "should-keep",
48+
"APK-TOOLS.checksum.SHA1": "checksum-value",
49+
},
50+
},
51+
{
52+
name: "all mode removes all xattrs",
53+
mode: "all",
54+
inputPAXRecords: map[string]string{
55+
"SCHILY.xattr.com.apple.provenance": "apple-data",
56+
"SCHILY.xattr.user.normal": "should-remove",
57+
"APK-TOOLS.checksum.SHA1": "checksum-value",
58+
},
59+
wantPAXRecords: map[string]string{
60+
"APK-TOOLS.checksum.SHA1": "checksum-value",
61+
},
62+
},
63+
{
64+
name: "preserves non-xattr records",
65+
mode: "all",
66+
inputPAXRecords: map[string]string{
67+
"SCHILY.xattr.any.attr": "xattr-data",
68+
"uid": "1000",
69+
"APK-TOOLS.checksum.SHA1": "checksum-value",
70+
},
71+
wantPAXRecords: map[string]string{
72+
"uid": "1000",
73+
"APK-TOOLS.checksum.SHA1": "checksum-value",
74+
},
75+
},
76+
}
77+
78+
for _, tt := range tests {
79+
t.Run(tt.name, func(t *testing.T) {
80+
// Create a tar header with the input PAX records
81+
hdr := &tar.Header{
82+
Name: "test.txt",
83+
PAXRecords: tt.inputPAXRecords,
84+
}
85+
86+
// Create filtered PAX records directly using our filtering logic
87+
filteredPAXRecords := make(map[string]string)
88+
for k, v := range hdr.PAXRecords {
89+
if tt.mode == "all" {
90+
// In "all" mode, strip all xattrs
91+
if strings.HasPrefix(k, "SCHILY.xattr.") {
92+
continue
93+
}
94+
} else {
95+
// In "problematic" mode, only strip problematic xattrs
96+
if strings.HasPrefix(k, "SCHILY.xattr.com.apple.") ||
97+
strings.HasPrefix(k, "SCHILY.xattr.com.docker.") {
98+
continue
99+
}
100+
}
101+
filteredPAXRecords[k] = v
102+
}
103+
104+
// Verify results
105+
require.Equal(t, tt.wantPAXRecords, filteredPAXRecords)
106+
})
107+
}
108+
}
109+

0 commit comments

Comments
 (0)