Skip to content

Commit c5d75a3

Browse files
smoserclaude
andcommitted
fix(apko-as-apk): avoid re-resolving already-installed packages when adding new packages
This fixes a bug where apko-as-apk add would fail when the system had packages installed from repositories that are no longer accessible. Problem: When running `apko-as-apk add <package>` in a container with packages from private or unavailable repositories (e.g., chainguard-baselayout from cgr.dev/chainguard-private), the command would fail with: "failed to resolve world: nothing provides <unavailable-package>" This occurred because the add command would: 1. Read all packages from /etc/apk/world (including already-installed ones) 2. Add the new package to world 3. Call ResolveWorld() which tried to re-resolve ALL packages 4. Fail when it couldn't find already-installed packages in current repos Example failure: docker run cgr.dev/chainguard-private/chainguard-base:latest \ apko-as-apk add grep # Error: nothing provides "chainguard-baselayout" Solution: Modified internal/cli/apkcompat/add.go to: 1. Get the list of already-installed packages before resolving 2. Filter the world file to only include packages NOT already installed 3. Only resolve and install packages that are actually new 4. Call InstallPackages() directly instead of FixateWorld() to avoid re-resolving the entire world This matches the behavior of the real `apk` command, which doesn't attempt to re-resolve packages that are already installed. Testing: - Added comprehensive unit tests in add_test.go - Verified fix works with both chainguard-base and wolfi-base containers - All existing tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 1b08539 commit c5d75a3

2 files changed

Lines changed: 365 additions & 9 deletions

File tree

internal/cli/apkcompat/add.go

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,59 @@ func runAdd(ctx context.Context, opts *addOptions, packages []string) error {
160160
return fmt.Errorf("virtual packages not yet implemented")
161161
}
162162

163-
// Add packages to world
163+
// Get list of installed packages to avoid re-resolving them
164+
installed, err := apkClient.GetInstalled()
165+
if err != nil {
166+
return fmt.Errorf("failed to get installed packages: %w", err)
167+
}
168+
169+
installedMap := make(map[string]bool)
170+
for _, pkg := range installed {
171+
// Store package name without version for comparison
172+
installedMap[pkg.Name] = true
173+
}
174+
175+
// Add packages to world and track which are new
176+
newPackages := []string{}
164177
for _, pkg := range packages {
165178
// Check if it's a local .apk file
166179
if strings.HasSuffix(pkg, ".apk") {
167180
return fmt.Errorf("local .apk files not yet fully supported: %s", pkg)
168181
}
169182

170-
// Add to world
171-
world = append(world, pkg)
183+
// Extract package name (remove version constraint if present)
184+
pkgName := pkg
185+
if idx := strings.IndexAny(pkg, "=<>~"); idx != -1 {
186+
pkgName = pkg[:idx]
187+
}
188+
189+
// Check if already in world
190+
alreadyInWorld := false
191+
for _, w := range world {
192+
wName := w
193+
if idx := strings.IndexAny(w, "=<>~"); idx != -1 {
194+
wName = w[:idx]
195+
}
196+
if wName == pkgName {
197+
alreadyInWorld = true
198+
break
199+
}
200+
}
201+
202+
if !alreadyInWorld {
203+
world = append(world, pkg)
204+
}
205+
206+
// Track as new package if not already installed
207+
if !installedMap[pkgName] {
208+
newPackages = append(newPackages, pkg)
209+
}
210+
}
211+
212+
// If all requested packages are already installed, nothing to do
213+
if len(newPackages) == 0 {
214+
slog.Info("All requested packages are already installed")
215+
return nil
172216
}
173217

174218
slog.Info("Setting world", "packages", world)
@@ -177,21 +221,60 @@ func runAdd(ctx context.Context, opts *addOptions, packages []string) error {
177221
}
178222

179223
if opts.simulate {
180-
slog.Info("Simulation mode - would resolve and install packages")
224+
slog.Info("Simulation mode - would resolve and install packages", "new_packages", newPackages)
181225
// TODO: show what would be installed
182226
return nil
183227
}
184228

185-
// Resolve dependencies
186-
slog.Info("Resolving world")
187-
if _, _, err := apkClient.ResolveWorld(ctx); err != nil {
229+
// To avoid re-resolving already installed packages that may not be in current repos,
230+
// we create a temporary world with only packages that need to be installed
231+
worldToResolve := []string{}
232+
for _, w := range world {
233+
wName := w
234+
if idx := strings.IndexAny(w, "=<>~"); idx != -1 {
235+
wName = w[:idx]
236+
}
237+
// Only include packages that are not already installed
238+
if !installedMap[wName] {
239+
worldToResolve = append(worldToResolve, w)
240+
}
241+
}
242+
243+
// Temporarily set world to only uninstalled packages for resolution
244+
if err := apkClient.SetWorld(ctx, worldToResolve); err != nil {
245+
return fmt.Errorf("failed to set temporary world for resolution: %w", err)
246+
}
247+
248+
// Resolve dependencies for new packages only
249+
slog.Info("Resolving new packages", "packages", worldToResolve)
250+
toInstall, conflicts, err := apkClient.ResolveWorld(ctx)
251+
if err != nil {
188252
return fmt.Errorf("failed to resolve world: %w", err)
189253
}
190254

191-
// Install packages
255+
// Check for conflicts with already installed packages
256+
for _, conflict := range conflicts {
257+
// Check if conflict package is already installed
258+
if installedMap[conflict] {
259+
return fmt.Errorf("cannot install due to conflict with %s", conflict)
260+
}
261+
}
262+
263+
// Restore full world before installation
264+
if err := apkClient.SetWorld(ctx, world); err != nil {
265+
return fmt.Errorf("failed to restore world: %w", err)
266+
}
267+
268+
// Convert to InstallablePackage slice
269+
allInstPkgs := make([]apk.InstallablePackage, len(toInstall))
270+
for i, pkg := range toInstall {
271+
allInstPkgs[i] = pkg
272+
}
273+
274+
// Install packages directly without calling FixateWorld (which would re-resolve everything)
192275
slog.Info("Installing packages")
193276
var sourceDateEpoch *time.Time
194-
diffs, err := apkClient.FixateWorld(ctx, sourceDateEpoch)
277+
diffs, err := apkClient.InstallPackages(ctx, sourceDateEpoch, allInstPkgs)
195278
if err != nil {
196279
return fmt.Errorf("failed to install packages: %w", err)
197280
}

0 commit comments

Comments
 (0)