Skip to content

Commit 509008e

Browse files
authored
fix: add registry authentication for platform discovery (project-copacetic#1420)
1 parent 3747056 commit 509008e

File tree

5 files changed

+210
-5
lines changed

5 files changed

+210
-5
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
name: "[Blocking] Private Registry E2E Test"
2+
3+
# This workflow tests Copa's ability to work with private container registries.
4+
# It runs only on merge to main (not on PRs) because it requires access to
5+
# private images in GHCR using the repository's GITHUB_TOKEN.
6+
7+
on:
8+
push:
9+
branches:
10+
- main
11+
paths-ignore:
12+
- "**.md"
13+
- "website/**"
14+
- "docs/**"
15+
- "demo/**"
16+
workflow_dispatch:
17+
18+
env:
19+
TRIVY_VERSION: 0.59.1
20+
BUILDKIT_VERSION: 0.19.0
21+
22+
permissions:
23+
contents: read
24+
packages: read
25+
26+
jobs:
27+
build:
28+
name: Build Copa
29+
runs-on: oracle-vm-16cpu-64gb-x86-64
30+
timeout-minutes: 5
31+
steps:
32+
- name: Harden Runner
33+
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.3.1
34+
with:
35+
egress-policy: audit
36+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
37+
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
38+
with:
39+
go-version: "1.25"
40+
check-latest: true
41+
- name: Build copa
42+
shell: bash
43+
run: |
44+
make build
45+
make archive
46+
- name: Upload copa to build artifacts
47+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
48+
with:
49+
name: copa_edge_linux_amd64.tar.gz
50+
path: dist/linux_amd64/release/copa_edge_linux_amd64.tar.gz
51+
52+
test-private-registry:
53+
needs: build
54+
name: Test Private Registry Authentication
55+
runs-on: oracle-vm-16cpu-64gb-x86-64
56+
timeout-minutes: 15
57+
steps:
58+
- name: Harden Runner
59+
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.3.1
60+
with:
61+
egress-policy: audit
62+
63+
- name: Check out code
64+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
65+
66+
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
67+
with:
68+
go-version: "1.25"
69+
check-latest: true
70+
71+
- name: Add containerd-snapshotter to docker daemon
72+
run: |
73+
echo '{"features": { "containerd-snapshotter": true }}' | sudo tee /etc/docker/daemon.json
74+
sudo systemctl restart docker
75+
76+
- name: Download copa from build artifacts
77+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
78+
with:
79+
name: copa_edge_linux_amd64.tar.gz
80+
81+
- name: Extract copa
82+
shell: bash
83+
run: |
84+
tar xzf copa_edge_linux_amd64.tar.gz
85+
./copa --version
86+
87+
- name: Login to GitHub Container Registry
88+
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
89+
with:
90+
registry: ghcr.io
91+
username: ${{ github.actor }}
92+
password: ${{ secrets.GITHUB_TOKEN }}
93+
94+
- name: Run private registry e2e tests
95+
shell: bash
96+
run: |
97+
set -eu -o pipefail
98+
go test -v ./test/e2e/private-registry --addr="docker://" --copa="$(pwd)/copa" -timeout 0
99+
100+
- name: Test Summary
101+
if: always()
102+
run: |
103+
echo "## Private Registry Test Results" >> $GITHUB_STEP_SUMMARY
104+
echo "" >> $GITHUB_STEP_SUMMARY
105+
echo "This test validates that Copa can authenticate to private registries" >> $GITHUB_STEP_SUMMARY
106+
echo "using credentials from Docker config (via docker login)." >> $GITHUB_STEP_SUMMARY
107+
echo "" >> $GITHUB_STEP_SUMMARY
108+
echo "Test image: \`ghcr.io/project-copacetic/copa-action/test/docker.io/library/nginx-private:1.21.0\`" >> $GITHUB_STEP_SUMMARY

pkg/buildkit/buildkit.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/project-copacetic/copacetic/pkg/utils"
3030
log "github.com/sirupsen/logrus"
3131

32+
"github.com/google/go-containerregistry/pkg/authn"
3233
"github.com/google/go-containerregistry/pkg/name"
3334
v1 "github.com/google/go-containerregistry/pkg/v1"
3435
"github.com/google/go-containerregistry/pkg/v1/daemon"
@@ -304,7 +305,7 @@ func DiscoverPlatformsFromReference(manifestRef string) ([]types.PatchPlatform,
304305
desc, err := TryGetManifestFromLocal(ref)
305306
if err != nil {
306307
log.Debugf("Failed to get descriptor from local daemon: %v, trying remote registry", err)
307-
desc, err = remote.Get(ref)
308+
desc, err = remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
308309
if err != nil {
309310
return nil, fmt.Errorf("error fetching descriptor for %q from both local daemon and remote registry: %w", manifestRef, err)
310311
}
@@ -1445,7 +1446,7 @@ func exportPreservedPlatformsToOutput(outputDir string, originalRef reference.Na
14451446
isLocal := (err == nil)
14461447
if err != nil {
14471448
log.Debugf("Failed to get descriptor from local daemon: %v, trying remote registry", err)
1448-
desc, err = remote.Get(ref)
1449+
desc, err = remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
14491450
if err != nil {
14501451
return nil, fmt.Errorf("failed to get remote descriptor: %w", err)
14511452
}
@@ -1552,7 +1553,7 @@ func exportPreservedPlatformsToOutput(outputDir string, originalRef reference.Na
15521553
platformDesc, err := TryGetManifestFromLocal(platformRef)
15531554
if err != nil {
15541555
// Fall back to remote if local fails
1555-
img, err = remote.Image(platformRef)
1556+
img, err = remote.Image(platformRef, remote.WithAuthFromKeychain(authn.DefaultKeychain))
15561557
if err != nil {
15571558
return nil, fmt.Errorf("failed to get image for preserved platform %s/%s: %w", platformSpec.OS, platformSpec.Architecture, err)
15581559
}
@@ -1788,7 +1789,7 @@ func exportOriginalImagePlatformsAsOCI(outputDir string, originalRef reference.N
17881789
}
17891790

17901791
// Get the remote descriptor
1791-
desc, err := remote.Get(ref)
1792+
desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
17921793
if err != nil {
17931794
return fmt.Errorf("failed to get remote descriptor: %w", err)
17941795
}

pkg/patch/platform.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66

77
"github.com/containerd/platforms"
8+
"github.com/google/go-containerregistry/pkg/authn"
89
"github.com/google/go-containerregistry/pkg/name"
910
"github.com/google/go-containerregistry/pkg/v1/remote"
1011
"github.com/opencontainers/go-digest"
@@ -111,7 +112,7 @@ func getPlatformDescriptorFromManifest(
111112
desc, err := buildkit.TryGetManifestFromLocal(ref)
112113
if err != nil {
113114
log.Debugf("Failed to get descriptor from local daemon: %v, trying remote registry", err)
114-
desc, err = remote.Get(ref)
115+
desc, err = remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
115116
if err != nil {
116117
return nil, fmt.Errorf("error fetching descriptor for %q from both local daemon and remote registry: %w", imageRef, err)
117118
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package privateregistry
2+
3+
import (
4+
"flag"
5+
"os"
6+
"testing"
7+
)
8+
9+
var (
10+
copaPath string
11+
buildkitAddr string
12+
)
13+
14+
func TestMain(m *testing.M) {
15+
flag.StringVar(&buildkitAddr, "addr", "docker://", "buildkit address to pass through to copa binary")
16+
flag.StringVar(&copaPath, "copa", "./copa", "path to copa binary")
17+
flag.Parse()
18+
19+
if copaPath == "" {
20+
panic("missing --copa")
21+
}
22+
23+
ec := m.Run()
24+
os.Exit(ec)
25+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package privateregistry
2+
3+
import (
4+
"bytes"
5+
"os/exec"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestPrivateRegistryPlatformDiscovery tests that Copa can correctly discover
13+
// platforms from a private registry image when authenticated via docker login.
14+
// This test validates the fix for UNAUTHORIZED errors during platform discovery.
15+
func TestPrivateRegistryPlatformDiscovery(t *testing.T) {
16+
if testing.Short() {
17+
t.Skip("skipping test in short mode")
18+
}
19+
20+
// This test requires authentication to GHCR which should be set up
21+
// via docker login before running this test.
22+
// In CI, this is done via the workflow using GITHUB_TOKEN.
23+
24+
// Test image in private GHCR registry
25+
testImage := "ghcr.io/project-copacetic/copa-action/test/docker.io/library/nginx-private:1.21.0"
26+
27+
// Run copa patch without a report (comprehensive patching)
28+
// This exercises the DiscoverPlatformsFromReference code path
29+
patchCmd := exec.Command(
30+
copaPath,
31+
"patch",
32+
"--image", testImage,
33+
"--tag", "patched-test",
34+
"-a="+buildkitAddr,
35+
"--debug",
36+
)
37+
38+
var stdout, stderr bytes.Buffer
39+
patchCmd.Stdout = &stdout
40+
patchCmd.Stderr = &stderr
41+
42+
err := patchCmd.Run()
43+
44+
// Combine output for error reporting
45+
combinedOutput := stdout.String() + stderr.String()
46+
47+
// Check that the UNAUTHORIZED error does not appear in the output
48+
require.NotContains(t, combinedOutput, "UNAUTHORIZED",
49+
"Platform discovery should not fail with UNAUTHORIZED error when authenticated. Output:\n%s", combinedOutput)
50+
51+
require.NotContains(t, combinedOutput, "authentication required",
52+
"Platform discovery should not fail with authentication required error. Output:\n%s", combinedOutput)
53+
54+
// The patch command should succeed or fail for reasons other than authentication
55+
if err != nil {
56+
// If it failed, make sure it's not due to authentication issues
57+
require.NotContains(t, err.Error(), "UNAUTHORIZED",
58+
"Patch command failed with UNAUTHORIZED error: %v", err)
59+
60+
// Check if it's failing because the image doesn't exist or other valid reasons
61+
// Authentication errors should never occur if we're properly logged in
62+
if strings.Contains(combinedOutput, "Failed to discover platforms") &&
63+
strings.Contains(combinedOutput, "UNAUTHORIZED") {
64+
t.Fatalf("Platform discovery failed with authentication error despite being logged in.\nOutput:\n%s", combinedOutput)
65+
}
66+
}
67+
68+
// If we got here without UNAUTHORIZED errors, the authentication fix is working
69+
t.Logf("Platform discovery completed without authentication errors")
70+
}

0 commit comments

Comments
 (0)