From d6edb226e2a33644adb670468899a1ea308865ce Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 17 Nov 2025 13:43:02 -0500 Subject: [PATCH 1/2] Fix offline mode caching for Alpine APK keys Alpine APK keys were not being cached properly in offline mode due to three issues: 1. fetchAlpineKeys() did not wrap the HTTP client with cache transport, bypassing offline cache entirely 2. cachePathFromURL() did not handle top-level URLs correctly (e.g., releases.json at the root path) 3. fetchOffline() could mistakenly select directories as cached files, causing "is a directory" errors This commit fixes all three issues by: - Wrapping the client with cache transport in fetchAlpineKeys() - Simplifying cachePathFromURL() to use full URL path structure uniformly - Filtering out directories in fetchOffline() when searching for cached files Note: The cache path structure change is not backwards compatible. Existing caches will need to be rebuilt, but old and new cache structures can coexist in the same cache directory. --- pkg/apk/apk/cache.go | 34 +++++++++++++++++++++------------- pkg/apk/apk/implementation.go | 4 ++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/pkg/apk/apk/cache.go b/pkg/apk/apk/cache.go index 5029494e8..1d28e666e 100644 --- a/pkg/apk/apk/cache.go +++ b/pkg/apk/apk/cache.go @@ -323,16 +323,24 @@ func (t *cacheTransport) fetchOffline(cacheFile string) (*http.Response, error) return nil, fmt.Errorf("listing %q for offline cache: %w", cacheDir, err) } - if len(des) == 0 { + // Filter out directories, only consider files + var files []os.DirEntry + for _, de := range des { + if !de.IsDir() { + files = append(files, de) + } + } + + if len(files) == 0 { return nil, fmt.Errorf("no offline cached entries for %s", cacheDir) } - newest, err := des[0].Info() + newest, err := files[0].Info() if err != nil { return nil, err } - for _, de := range des[1:] { + for _, de := range files[1:] { fi, err := de.Info() if err != nil { return nil, err @@ -484,16 +492,16 @@ func cachePathFromURL(root string, u url.URL) (string, error) { u2.ForceQuery = false u2.RawFragment = "" u2.RawQuery = "" - filename := filepath.Base(u2.Path) - archDir := filepath.Dir(u2.Path) - dir := filepath.Base(archDir) - repoDir := filepath.Dir(archDir) - // include the hostname - u2.Path = repoDir - - // url encode it so it can be a single directory - repoDir = url.QueryEscape(u2.String()) - cacheFile := filepath.Join(root, repoDir, dir, filename) + + // Use the full path as the cache structure to handle both top-level and nested files uniformly + cachePath := strings.TrimPrefix(filepath.Clean(u2.Path), "/") + baseURL := u2 + baseURL.Path = "" + + // url encode the base URL so it can be a single directory + baseDir := url.QueryEscape(baseURL.String()) + cacheFile := filepath.Join(root, baseDir, cachePath) + // validate it is within root cacheFile = filepath.Clean(cacheFile) cleanroot := filepath.Clean(root) diff --git a/pkg/apk/apk/implementation.go b/pkg/apk/apk/implementation.go index 67d26db2e..d6440193a 100644 --- a/pkg/apk/apk/implementation.go +++ b/pkg/apk/apk/implementation.go @@ -880,6 +880,10 @@ func (a *APK) fetchAlpineKeys(ctx context.Context, alpineVersions ...string) err u := alpineReleasesURL client := a.client + // Wrap client with cache transport to support offline mode + if a.cache != nil { + client = a.cache.client(client, true) + } req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return err From 450e48c2a10614cfdec32b4e86f5c5a2bc8cae50 Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 17 Nov 2025 13:54:28 -0500 Subject: [PATCH 2/2] Update tests to align with new cache path structure The cache path structure was changed to use the full URL path instead of just the last two path components. This better handles top-level files like releases.json while maintaining proper cache organization. Updated test expectations to use the new structure: - Old: cache_root/encoded_full_repo_url/arch/file - New: cache_root/encoded_base_url/path/to/arch/file --- pkg/apk/apk/implementation_test.go | 17 +++++++++++++---- pkg/apk/apk/repo_test.go | 4 +++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pkg/apk/apk/implementation_test.go b/pkg/apk/apk/implementation_test.go index 163098b9d..dfdd184c4 100644 --- a/pkg/apk/apk/implementation_test.go +++ b/pkg/apk/apk/implementation_test.go @@ -715,7 +715,10 @@ func TestFetchPackage(t *testing.T) { tmpDir := t.TempDir() a := prepLayout(t, tmpDir) // fill the cache - repoDir := filepath.Join(tmpDir, url.QueryEscape(testAlpineRepos), testArch) + // New cache structure uses full path: https%3A%2F%2Fhost/path/to/arch/file + repoURL, _ := url.Parse(testAlpineRepos) + baseDir := url.QueryEscape(repoURL.Scheme + "://" + repoURL.Host) + repoDir := filepath.Join(tmpDir, baseDir, strings.TrimPrefix(repoURL.Path, "/"), testArch) err := os.MkdirAll(repoDir, 0o755) require.NoError(t, err, "unable to mkdir cache") @@ -757,7 +760,9 @@ func TestFetchPackage(t *testing.T) { tmpDir := t.TempDir() a := prepLayout(t, tmpDir) // fill the cache - repoDir := filepath.Join(tmpDir, url.QueryEscape(testAlpineRepos), testArch) + repoURL, _ := url.Parse(testAlpineRepos) + baseDir := url.QueryEscape(repoURL.Scheme + "://" + repoURL.Host) + repoDir := filepath.Join(tmpDir, baseDir, strings.TrimPrefix(repoURL.Path, "/"), testArch) err := os.MkdirAll(repoDir, 0o755) require.NoError(t, err, "unable to mkdir cache") @@ -785,7 +790,9 @@ func TestFetchPackage(t *testing.T) { tmpDir := t.TempDir() a := prepLayout(t, tmpDir) // fill the cache - repoDir := filepath.Join(tmpDir, url.QueryEscape(testAlpineRepos), testArch) + repoURL, _ := url.Parse(testAlpineRepos) + baseDir := url.QueryEscape(repoURL.Scheme + "://" + repoURL.Host) + repoDir := filepath.Join(tmpDir, baseDir, strings.TrimPrefix(repoURL.Path, "/"), testArch) err := os.MkdirAll(repoDir, 0o755) require.NoError(t, err, "unable to mkdir cache") @@ -815,7 +822,9 @@ func TestFetchPackage(t *testing.T) { tmpDir := t.TempDir() a := prepLayout(t, tmpDir) // fill the cache - repoDir := filepath.Join(tmpDir, url.QueryEscape(testAlpineRepos), testArch) + repoURL, _ := url.Parse(testAlpineRepos) + baseDir := url.QueryEscape(repoURL.Scheme + "://" + repoURL.Host) + repoDir := filepath.Join(tmpDir, baseDir, strings.TrimPrefix(repoURL.Path, "/"), testArch) err := os.MkdirAll(repoDir, 0o755) require.NoError(t, err, "unable to mkdir cache") diff --git a/pkg/apk/apk/repo_test.go b/pkg/apk/apk/repo_test.go index 7dae913cc..eb0fc109f 100644 --- a/pkg/apk/apk/repo_test.go +++ b/pkg/apk/apk/repo_test.go @@ -178,7 +178,9 @@ func TestGetRepositoryIndexes(t *testing.T) { tmpDir := t.TempDir() a := prepLayout(t, tmpDir, []string{testAlpineRepos}) // fill the cache - repoDir := filepath.Join(tmpDir, url.QueryEscape(testAlpineRepos), testArch) + repoURL, _ := url.Parse(testAlpineRepos) + baseDir := url.QueryEscape(repoURL.Scheme + "://" + repoURL.Host) + repoDir := filepath.Join(tmpDir, baseDir, strings.TrimPrefix(repoURL.Path, "/"), testArch) a.SetClient(&http.Client{ Transport: &testLocalTransport{