Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more strict verifications when user provides a platform #336

Merged
merged 2 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ bin/
/snapshot
/.tool
/.task
.mise.toml

# changelog generation
CHANGELOG.md
Expand Down
29 changes: 18 additions & 11 deletions pkg/image/containerd/daemon_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,37 +341,44 @@ func (p *daemonImageProvider) pullImageIfMissing(ctx context.Context, client *co
}
}

if err := p.validatePlatform(resolvedPlatform); err != nil {
if err := validatePlatform(p.platform, resolvedPlatform); err != nil {
return "", nil, fmt.Errorf("platform validation failed: %w", err)
}

return resolvedImage, resolvedPlatform, nil
}

func (p *daemonImageProvider) validatePlatform(platform *platforms.Platform) error {
if p.platform == nil {
func validatePlatform(expected *image.Platform, given *platforms.Platform) error {
if expected == nil {
return nil
}

if platform == nil {
return fmt.Errorf("image has no platform information (might be a manifest list)")
if given == nil {
return newErrPlatformMismatch(expected, fmt.Errorf("image has no platform information (might be a manifest list)"))
}

if platform.OS != p.platform.OS {
return fmt.Errorf("image has unexpected OS %q, which differs from the user specified PS %q", platform.OS, p.platform.OS)
if given.OS != expected.OS {
return newErrPlatformMismatch(expected, fmt.Errorf("image has unexpected OS %q, which differs from the user specified PS %q", given.OS, expected.OS))
}

if platform.Architecture != p.platform.Architecture {
return fmt.Errorf("image has unexpected architecture %q, which differs from the user specified architecture %q", platform.Architecture, p.platform.Architecture)
if given.Architecture != expected.Architecture {
return newErrPlatformMismatch(expected, fmt.Errorf("image has unexpected architecture %q, which differs from the user specified architecture %q", given.Architecture, expected.Architecture))
}

if platform.Variant != p.platform.Variant {
return fmt.Errorf("image has unexpected architecture %q, which differs from the user specified architecture %q", platform.Variant, p.platform.Variant)
if given.Variant != expected.Variant {
return newErrPlatformMismatch(expected, fmt.Errorf("image has unexpected architecture %q, which differs from the user specified architecture %q", given.Variant, expected.Variant))
}

return nil
}

func newErrPlatformMismatch(expected *image.Platform, err error) *image.ErrPlatformMismatch {
return &image.ErrPlatformMismatch{
ExpectedPlatform: expected.String(),
Err: err,
}
}

// save the image from the containerd daemon to a tar file
func (p *daemonImageProvider) saveImage(ctx context.Context, client *containerd.Client, resolvedImage string) (string, error) {
imageTempDir, err := p.tmpDirGen.NewDirectory("containerd-daemon-image")
Expand Down
66 changes: 66 additions & 0 deletions pkg/image/containerd/daemon_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,69 @@ func Test_exportPlatformComparer(t *testing.T) {
})
}
}

func TestValidatePlatform(t *testing.T) {
isFetchError := func(t require.TestingT, err error, args ...interface{}) {
var pErr *image.ErrPlatformMismatch
require.ErrorAs(t, err, &pErr)
}

tests := []struct {
name string
expected *image.Platform
given *platforms.Platform
expectedErrMsg string
expectedErr require.ErrorAssertionFunc
}{
{
name: "nil expected platform",
expected: nil,
given: &platforms.Platform{OS: "linux", Architecture: "amd64"},
expectedErr: require.NoError,
},
{
name: "nil given platform",
expected: &image.Platform{OS: "linux", Architecture: "amd64"},
given: nil,
expectedErr: isFetchError,
expectedErrMsg: "image has no platform information",
},
{
name: "OS mismatch",
expected: &image.Platform{OS: "linux", Architecture: "amd64"},
given: &platforms.Platform{OS: "windows", Architecture: "amd64"},
expectedErr: isFetchError,
expectedErrMsg: `image has unexpected OS "windows", which differs from the user specified PS "linux"`,
},
{
name: "architecture mismatch",
expected: &image.Platform{OS: "linux", Architecture: "amd64"},
given: &platforms.Platform{OS: "linux", Architecture: "arm64"},
expectedErr: isFetchError,
expectedErrMsg: `image has unexpected architecture "arm64", which differs from the user specified architecture "amd64"`,
},
{
name: "variant mismatch",
expected: &image.Platform{OS: "linux", Architecture: "arm64", Variant: "v8"},
given: &platforms.Platform{OS: "linux", Architecture: "arm64", Variant: "v7"},
expectedErr: isFetchError,
expectedErrMsg: `image has unexpected architecture "v7", which differs from the user specified architecture "v8"`,
},
{
name: "matching platform",
expected: &image.Platform{OS: "linux", Architecture: "amd64", Variant: ""},
given: &platforms.Platform{OS: "linux", Architecture: "amd64", Variant: ""},
expectedErr: require.NoError,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validatePlatform(tt.expected, tt.given)
tt.expectedErr(t, err)
if err != nil {
assert.ErrorContains(t, err, tt.expectedErrMsg)
}
})
}
}
28 changes: 24 additions & 4 deletions pkg/image/docker/daemon_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,35 @@ func (p *daemonImageProvider) pull(ctx context.Context, client client.APIClient,

return fmt.Errorf("failed to pull image: %w", err)
}
if err := handlePullEvent(status, thePullEvent); err != nil {
return err
}
}

return nil
}

// check for the last two events indicating the pull is complete
if strings.HasPrefix(thePullEvent.Status, "Digest:") || strings.HasPrefix(thePullEvent.Status, "Status:") {
continue
type emitter interface {
onEvent(event *pullEvent)
}

func handlePullEvent(status emitter, event *pullEvent) error {
if event.Error != "" {
if strings.Contains(event.Error, "does not match the specified platform") {
return &image.ErrPlatformMismatch{
Err: errors.New(event.Error),
}
}
return errors.New(event.Error)
}

status.onEvent(thePullEvent)
// check for the last two events indicating the pull is complete
if strings.HasPrefix(event.Status, "Digest:") || strings.HasPrefix(event.Status, "Status:") {
return nil
}

status.onEvent(event)

return nil
}

Expand Down
83 changes: 83 additions & 0 deletions pkg/image/docker/daemon_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
configTypes "github.com/docker/cli/cli/config/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/anchore/stereoscope/pkg/image"
)

func TestEncodeCredentials(t *testing.T) {
Expand Down Expand Up @@ -85,3 +87,84 @@ func Test_authURL(t *testing.T) {
})
}
}

type mockEmitter struct {
calledEvents []*pullEvent
}

func (m *mockEmitter) onEvent(event *pullEvent) {
m.calledEvents = append(m.calledEvents, event)
}

func TestHandlePullEventWithMockEmitter(t *testing.T) {
tests := []struct {
name string
event *pullEvent
expectOnEvent bool
assertFunc require.ErrorAssertionFunc
}{
{
name: "error in event",
event: &pullEvent{
Error: "fetch failed",
},
expectOnEvent: false,
assertFunc: func(t require.TestingT, err error, args ...interface{}) {
require.Error(t, err)
var pErr *image.ErrPlatformMismatch
require.NotErrorAs(t, err, &pErr)
},
},
{
name: "platform error in event",
event: &pullEvent{
Error: "image with reference anchore/test_images:golang was found but its platform (linux/amd64) does not match the specified platform (linux/arm64)",
},
expectOnEvent: false,
assertFunc: func(t require.TestingT, err error, args ...interface{}) {
require.Error(t, err)
var pErr *image.ErrPlatformMismatch
require.ErrorAs(t, err, &pErr)
},
},
{
name: "digest event",
event: &pullEvent{
Status: "Digest: abc123",
},
expectOnEvent: false,
assertFunc: require.NoError,
},
{
name: "status event",
event: &pullEvent{
Status: "Status: Downloaded",
},
expectOnEvent: false,
assertFunc: require.NoError,
},
{
name: "non-terminal event",
event: &pullEvent{
Status: "Downloading layer",
},
expectOnEvent: true,
assertFunc: require.NoError,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := &mockEmitter{}
err := handlePullEvent(mock, tt.event)
tt.assertFunc(t, err)

if tt.expectOnEvent {
require.Len(t, mock.calledEvents, 1)
require.Equal(t, tt.event, mock.calledEvents[0])
} else {
require.Empty(t, mock.calledEvents)
}
})
}
}
Loading
Loading