Skip to content
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
105 changes: 105 additions & 0 deletions internal/cli/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@ import (
"context"
"encoding/base64"
"fmt"
"net/http"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"time"

"github.com/spf13/cobra"

"github.com/chainguard-dev/clog"

"chainguard.dev/apko/pkg/apk/apk"
"chainguard.dev/apko/pkg/apk/auth"
apkfs "chainguard.dev/apko/pkg/apk/fs"
"chainguard.dev/apko/pkg/build"
"chainguard.dev/apko/pkg/build/types"
Expand Down Expand Up @@ -169,6 +173,10 @@ func LockCmd(ctx context.Context, output string, archs []types.Architecture, opt
})
}

// Discover and add auto-discovered keys from repositories
discoveredKeys := discoverKeysForLock(ctx, ic, archs)
lock.Contents.Keyrings = append(lock.Contents.Keyrings, discoveredKeys...)

// TODO: If the archs can't agree on package versions (e.g., arm builds are ahead of x86) then we should fail instead of producing inconsistent locks.
for _, arch := range archs {
log := log.With("arch", arch.ToAPK())
Expand Down Expand Up @@ -236,6 +244,12 @@ func LockCmd(ctx context.Context, output string, archs []types.Architecture, opt
lock.Contents.Repositories = append(lock.Contents.Repositories, repoLock)
}
}

// Sort keyrings by name for reproducible lock files
sort.Slice(lock.Contents.Keyrings, func(i, j int) bool {
return lock.Contents.Keyrings[i].Name < lock.Contents.Keyrings[j].Name
})

return lock.SaveToFile(output)
}

Expand All @@ -262,3 +276,94 @@ func stripURLScheme(url string) string {
"http://",
)
}

// discoverKeysForLock discovers keys from repositories and returns them as LockKeyring entries
func discoverKeysForLock(ctx context.Context, ic *types.ImageConfiguration, archs []types.Architecture) []pkglock.LockKeyring {
log := clog.FromContext(ctx)

// Collect all unique repositories
repoSet := make(map[string]struct{})
for _, repo := range ic.Contents.BuildRepositories {
repoSet[repo] = struct{}{}
}
for _, repo := range ic.Contents.RuntimeOnlyRepositories {
repoSet[repo] = struct{}{}
}
for _, repo := range ic.Contents.Repositories {
repoSet[repo] = struct{}{}
}

// Map to track discovered keys by URL to avoid duplicates
discoveredKeyMap := make(map[string]pkglock.LockKeyring)

// Fetch Alpine releases once (cached by HTTP client)
client := &http.Client{}
var alpineReleases *apk.Releases

// Discover keys for each repository and architecture
for repo := range repoSet {
// Try Alpine-style key discovery
if ver, ok := apk.ParseAlpineVersion(repo); ok {
// Fetch releases.json if not already fetched
if alpineReleases == nil {
releases, err := apk.FetchAlpineReleases(ctx, client)
if err != nil {
log.Warnf("Failed to fetch Alpine releases: %v", err)
continue
}
alpineReleases = releases
}

branch := alpineReleases.GetReleaseBranch(ver)
if branch == nil {
log.Debugf("Alpine version %s not found in releases", ver)
continue
}

// Get keys for each architecture
for _, arch := range archs {
log.Debugf("Discovering Alpine keys for %s (version %s, arch %s)", repo, ver, arch.ToAPK())
urls := branch.KeysFor(arch.ToAPK(), time.Now())
if len(urls) == 0 {
log.Debugf("No keys found for arch %s and version %s", arch.ToAPK(), ver)
continue
}

// Add discovered key URLs to the map
for _, u := range urls {
discoveredKeyMap[u] = pkglock.LockKeyring{
Name: stripURLScheme(u),
URL: u,
}
}
}
}

// Try Chainguard-style key discovery
log.Debugf("Attempting Chainguard-style key discovery for %s", repo)
keys, err := apk.DiscoverKeys(ctx, client, auth.DefaultAuthenticators, repo)
if err != nil {
log.Debugf("Chainguard-style key discovery failed for %s: %v", repo, err)
} else if len(keys) > 0 {
log.Debugf("Discovered %d Chainguard-style keys for %s", len(keys), repo)
// For each JWKS key, emit a URL: repository + "/" + KeyID
repoBase := strings.TrimSuffix(repo, "/")
for _, key := range keys {
keyURL := repoBase + "/" + key.ID
discoveredKeyMap[keyURL] = pkglock.LockKeyring{
Name: stripURLScheme(keyURL),
URL: keyURL,
}
}
}
}

// Convert map to slice
discoveredKeys := make([]pkglock.LockKeyring, 0, len(discoveredKeyMap))
for _, key := range discoveredKeyMap {
discoveredKeys = append(discoveredKeys, key)
}

log.Infof("Discovered %d auto-discovered keys", len(discoveredKeys))
return discoveredKeys
}
51 changes: 32 additions & 19 deletions internal/cli/lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,38 @@ func TestLock(t *testing.T) {
ctx := context.Background()
tmp := t.TempDir()

golden := filepath.Join("testdata", "apko.lock.json")

config := "apko.yaml"
archs := types.ParseArchitectures([]string{"amd64", "arm64"})
opts := []build.Option{build.WithConfig(config, []string{"testdata"})}
outputPath := filepath.Join(tmp, "apko.lock.json")

err := cli.LockCmd(ctx, outputPath, archs, opts)
require.NoError(t, err)

want, err := os.ReadFile(golden)
require.NoError(t, err)
got, err := os.ReadFile(outputPath)
require.NoError(t, err)

if !bytes.Equal(want, got) {
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Mismatched lock files: (-%q +%q):\n%s", golden, outputPath, diff)
}
tests := []struct {
basename string
}{
{
basename: "apko",
}, {
basename: "apko-discover",
},
}
for _, tt := range tests {
t.Run(tt.basename, func(t *testing.T) {
golden := filepath.Join("testdata", tt.basename+".lock.json")

config := tt.basename + ".yaml"
archs := types.ParseArchitectures([]string{"amd64", "arm64"})
opts := []build.Option{build.WithConfig(config, []string{"testdata"})}
outputPath := filepath.Join(tmp, tt.basename+".lock.json")

err := cli.LockCmd(ctx, outputPath, archs, opts)
require.NoError(t, err)

want, err := os.ReadFile(golden)
require.NoError(t, err)
got, err := os.ReadFile(outputPath)
require.NoError(t, err)

if !bytes.Equal(want, got) {
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Mismatched lock files: (-%q +%q):\n%s", golden, outputPath, diff)
}
}
})
}
}

Expand Down
147 changes: 147 additions & 0 deletions internal/cli/testdata/apko-discover.lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
{
"version": "v1",
"config": {
"name": "apko-discover.yaml",
"checksum": "sha256-D1mtHLoTBln0iH+VqWZMIPQOScXMWprVolB2sihPWAk="
},
"contents": {
"keyring": [
{
"name": "./testdata/melange.rsa.pub",
"url": "./testdata/melange.rsa.pub"
},
{
"name": "alpinelinux.org/keys/alpine-devel%40lists.alpinelinux.org-6165ee59.rsa.pub",
"url": "https://alpinelinux.org/keys/alpine-devel%40lists.alpinelinux.org-6165ee59.rsa.pub"
},
{
"name": "alpinelinux.org/keys/alpine-devel%40lists.alpinelinux.org-616ae350.rsa.pub",
"url": "https://alpinelinux.org/keys/alpine-devel%40lists.alpinelinux.org-616ae350.rsa.pub"
},
{
"name": "apk.cgr.dev/chainguard/chainguard-6ea100e7571978828f8455393090cc468e1f22dfc771ad2d26df14e52b73c37f.rsa.pub",
"url": "https://apk.cgr.dev/chainguard/chainguard-6ea100e7571978828f8455393090cc468e1f22dfc771ad2d26df14e52b73c37f.rsa.pub"
},
{
"name": "apk.cgr.dev/chainguard/chainguard-7fb528a64a862d44bbea6069093f1fec29fa864ba7e9754828eeceed3f487239.rsa.pub",
"url": "https://apk.cgr.dev/chainguard/chainguard-7fb528a64a862d44bbea6069093f1fec29fa864ba7e9754828eeceed3f487239.rsa.pub"
},
{
"name": "apk.cgr.dev/chainguard/chainguard-a87b67b52e9fa08414a5535a99adc7b66643536ccd52f4da66057b67adf21363.rsa.pub",
"url": "https://apk.cgr.dev/chainguard/chainguard-a87b67b52e9fa08414a5535a99adc7b66643536ccd52f4da66057b67adf21363.rsa.pub"
}
],
"build_repositories": [],
"runtime_repositories": [],
"repositories": [
{
"name": "dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64",
"url": "https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/APKINDEX.tar.gz",
"architecture": "x86_64"
},
{
"name": "apk.cgr.dev/chainguard/x86_64",
"url": "https://apk.cgr.dev/chainguard/x86_64/APKINDEX.tar.gz",
"architecture": "x86_64"
},
{
"name": "./testdata/packages/x86_64",
"url": "./testdata/packages/x86_64/APKINDEX.tar.gz",
"architecture": "x86_64"
},
{
"name": "dl-cdn.alpinelinux.org/alpine/v3.22/main/aarch64",
"url": "https://dl-cdn.alpinelinux.org/alpine/v3.22/main/aarch64/APKINDEX.tar.gz",
"architecture": "aarch64"
},
{
"name": "apk.cgr.dev/chainguard/aarch64",
"url": "https://apk.cgr.dev/chainguard/aarch64/APKINDEX.tar.gz",
"architecture": "aarch64"
},
{
"name": "./testdata/packages/aarch64",
"url": "./testdata/packages/aarch64/APKINDEX.tar.gz",
"architecture": "aarch64"
}
],
"packages": [
{
"name": "pretend-baselayout",
"url": "./testdata/packages/x86_64/pretend-baselayout-1.0.0-r0.apk",
"version": "1.0.0-r0",
"architecture": "x86_64",
"signature": {
"range": "bytes=0-648",
"checksum": "sha1-V+Htugmm+Ru2ogsWm7VgD4A1DsQ="
},
"control": {
"range": "bytes=649-1562",
"checksum": "sha1-DRtLIHolxOMB++9L4ZjkeUFaKYc="
},
"data": {
"range": "bytes=1563-2767",
"checksum": "sha256-dZB1iTdQ2sfndKG6Ohf5VwWFjqz6kjSzZbqYU17BSRM="
},
"checksum": "Q1DRtLIHolxOMB++9L4ZjkeUFaKYc="
},
{
"name": "replayout",
"url": "./testdata/packages/x86_64/replayout-1.0.0-r0.apk",
"version": "1.0.0-r0",
"architecture": "x86_64",
"signature": {
"range": "bytes=0-647",
"checksum": "sha1-ZrPCeQ4XeDjZSQw+IhJ4g4BcUlo="
},
"control": {
"range": "bytes=648-1589",
"checksum": "sha1-IvTcfj6zzLipr9akZ+YRTIyQCr8="
},
"data": {
"range": "bytes=1590-2786",
"checksum": "sha256-IIzbGjwv4H9h6N1bEbF8p4cqkV0Ex54sXEsvf6txnEo="
},
"checksum": "Q1IvTcfj6zzLipr9akZ+YRTIyQCr8="
},
{
"name": "pretend-baselayout",
"url": "./testdata/packages/aarch64/pretend-baselayout-1.0.0-r0.apk",
"version": "1.0.0-r0",
"architecture": "aarch64",
"signature": {
"range": "bytes=0-644",
"checksum": "sha1-n9SJ91H1UwE+mkVVCifh6ziTwbc="
},
"control": {
"range": "bytes=645-1555",
"checksum": "sha1-URAMn9SfiCvjs6C812GovkgRgVo="
},
"data": {
"range": "bytes=1556-2762",
"checksum": "sha256-igCqZKcxRp6yHq2IlyozkM68Z6v9PMr/PDAIuSh2W3A="
},
"checksum": "Q1URAMn9SfiCvjs6C812GovkgRgVo="
},
{
"name": "replayout",
"url": "./testdata/packages/aarch64/replayout-1.0.0-r0.apk",
"version": "1.0.0-r0",
"architecture": "aarch64",
"signature": {
"range": "bytes=0-646",
"checksum": "sha1-1ifrimC4bUlo4O6aCGXcjOKAcTo="
},
"control": {
"range": "bytes=647-1586",
"checksum": "sha1-SWYSZF3dGLrN8kebGjOBfDH6vG4="
},
"data": {
"range": "bytes=1587-2787",
"checksum": "sha256-U85iWddHApXsVosa4S0srhkr3SSlzuoBrEpZg5f1irQ="
},
"checksum": "Q1SWYSZF3dGLrN8kebGjOBfDH6vG4="
}
]
}
}
16 changes: 16 additions & 0 deletions internal/cli/testdata/apko-discover.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
contents:
keyring:
- ./testdata/melange.rsa.pub
repositories:
- https://dl-cdn.alpinelinux.org/alpine/v3.22/main
- https://apk.cgr.dev/chainguard
- ./testdata/packages
packages:
- replayout

entrypoint:
command: /bin/sh -l

archs:
- x86_64
- aarch64
Loading
Loading