Repository: chainguard-dev/apko (go)

Issue:
Problems with python finding itself
Opening for @deitch  (feel free to retitle and/or correct the below!)

When debugging failing E2E tests in Melange (e.g. https://github.com/chainguard-dev/melange/actions/runs/4743961439/jobs/8424331419?pr=395) we noticed two issues:

1. the apk implementation doesn't have support for using tilde (`~`) to specify a version for a dependency
2. the apk implementation doesn't have support a package depending directly on itself (i.e. not on a provides, but the package itself) — e.g. `python3` depending on `python3~3.11`

Pull request description:
add optional support for self-package resolution
Fixes #627 

Since apk normally does allow self-package resolution (if I fulfill one of my dependencies, then look no further), but other places sometimes do not (e.g. wolfictl, as a policy choice, see [this comment](https://github.com/chainguard-dev/apko/issues/627#issuecomment-1517042175) by @kaniini), we make it optional in the resolver.

We always make it true here, but given the overlap between apko logic and wolfictl graph, and the plans for merging them going forward, making this optional only can help.

Patch:
diff --git a/examples/alpine-python3.yaml b/examples/alpine-python3.yaml
new file mode 100644
index 0000000..cf1109a
--- /dev/null
+++ b/examples/alpine-python3.yaml
@@ -0,0 +1,12 @@
+contents:
+  repositories:
+    - https://dl-cdn.alpinelinux.org/alpine/edge/main
+  packages:
+    - alpine-base
+    - python3
+
+cmd: /bin/sh -l
+
+# optional environment configuration
+environment:
+  PATH: /usr/sbin:/sbin:/usr/bin:/bin
diff --git a/pkg/apk/impl/repo.go b/pkg/apk/impl/repo.go
index 213d797..da7fdac 100644
--- a/pkg/apk/impl/repo.go
+++ b/pkg/apk/impl/repo.go
@@ -277,7 +277,7 @@ func (p *PkgResolver) GetPackageWithDependencies(pkgName string, existing map[st
 	pkg = pkgs[0]
 
 	_, _, _, pin := resolvePackageNameVersionPin(pkgName)
-	deps, conflicts, err := p.getPackageDependencies(pkg, pin, parents, localExisting)
+	deps, conflicts, err := p.getPackageDependencies(pkg, pin, true, parents, localExisting)
 	if err != nil {
 		return
 	}
@@ -389,7 +389,7 @@ func (p *PkgResolver) ResolvePackage(pkgName string) (pkgs []*repository.Reposit
 // It might change the order of install.
 // In other words, this _should_ be a DAG (acyclical), but because the packages
 // are just listing dependencies in text, it might be cyclical. We need to be careful of that.
-func (p *PkgResolver) getPackageDependencies(pkg *repository.RepositoryPackage, allowPin string, parents map[string]bool, existing map[string]*repository.RepositoryPackage) (dependencies []*repository.RepositoryPackage, conflicts []string, err error) {
+func (p *PkgResolver) getPackageDependencies(pkg *repository.RepositoryPackage, allowPin string, allowSelfFulfill bool, parents map[string]bool, existing map[string]*repository.RepositoryPackage) (dependencies []*repository.RepositoryPackage, conflicts []string, err error) {
 	// check if the package we are checking is one of our parents, avoid cyclical graphs
 	if _, ok := parents[pkg.Name]; ok {
 		return nil, nil, nil
@@ -423,6 +423,24 @@ func (p *PkgResolver) getPackageDependencies(pkg *repository.RepositoryPackage,
 			continue
 		}
 
+		if allowSelfFulfill && pkg.Name == name {
+			var (
+				actualVersion, requiredVersion packageVersion
+				err1, err2                     error
+			)
+			actualVersion, err1 = parseVersion(pkg.Version)
+			if compare != versionNone {
+				requiredVersion, err2 = parseVersion(version)
+			}
+			// we accept invalid versions for ourself, but do not try to use it to fulfill
+			if err1 == nil && err2 == nil {
+				if compare.satisfies(actualVersion, requiredVersion) {
+					// we provide it, so skip looking elsewhere
+					continue
+				}
+			}
+		}
+
 		// first see if it is a name of a package
 		depPkgWithVersions, ok := p.nameMap[name]
 		if ok {
@@ -474,7 +492,7 @@ func (p *PkgResolver) getPackageDependencies(pkg *repository.RepositoryPackage,
 			childParents[k] = true
 		}
 		childParents[pkg.Name] = true
-		subDeps, confs, err := p.getPackageDependencies(depPkg, allowPin, childParents, existing)
+		subDeps, confs, err := p.getPackageDependencies(depPkg, allowPin, true, childParents, existing)
 		if err != nil {
 			return nil, nil, err
 		}
diff --git a/pkg/apk/impl/version.go b/pkg/apk/impl/version.go
index c014d1d..7cb2e01 100644
--- a/pkg/apk/impl/version.go
+++ b/pkg/apk/impl/version.go
@@ -37,7 +37,7 @@ import (
 
 var (
 	versionRegex     = regexp.MustCompile(`^([0-9]+)((\.[0-9]+)*)([a-z]?)((_alpha|_beta|_pre|_rc)([0-9]*))?((_cvs|_svn|_git|_hg|_p)([0-9]*))?((-r)([0-9]+))?$`)
-	packageNameRegex = regexp.MustCompile(`^([^@=><~]+)(([=><]+)([^@]+))?(@([a-zA-Z0-9]+))?$`)
+	packageNameRegex = regexp.MustCompile(`^([^@=><~]+)(([=><~]+)([^@]+))?(@([a-zA-Z0-9]+))?$`)
 )
 
 func init() {


Test patch:
diff --git a/pkg/apk/impl/repo_test.go b/pkg/apk/impl/repo_test.go
index 8ad6dc1..d1ebb1d 100644
--- a/pkg/apk/impl/repo_test.go
+++ b/pkg/apk/impl/repo_test.go
@@ -139,6 +139,8 @@ func testGetPackagesAndIndex() ([]*repository.RepositoryPackage, []*repository.R
 			{Name: "package5", Version: "1.5.0"},
 			{Name: "package5", Version: "1.5.1"},
 			{Name: "package5", Version: "2.0.0"},
+			{Name: "package6", Version: "1.5.1"},
+			{Name: "package6", Version: "2.0.0", Dependencies: []string{"package6", "package5"}},
 		}
 		repoPackages = make([]*repository.RepositoryPackage, 0, len(packages))
 	)
@@ -206,6 +208,34 @@ func TestGetPackageDependencies(t *testing.T) {
 		}
 		require.True(t, reflect.DeepEqual(expected, actual), "dependencies mismatch:\nactual %v\nexpect %v", actual, expected)
 	})
+	t.Run("self-fulfill", func(t *testing.T) {
+		_, index := testGetPackagesAndIndex()
+
+		resolver := NewPkgResolver(testNamedRepositoryFromIndexes(index))
+		pkg6, err := resolver.ResolvePackage("package6")
+		require.NoErrorf(t, err, "unable to resolve package6")
+		require.GreaterOrEqual(t, len(pkg6), 1, "package6 should have at least one match")
+		tests := []struct {
+			name     string
+			expected []string
+			allow    bool
+		}{
+			{"allowed", []string{"package5"}, true},
+			{"not allowed", []string{"package6", "package5"}, false},
+		}
+		for _, tt := range tests {
+			t.Run(tt.name, func(t *testing.T) {
+				deps, _, err := resolver.getPackageDependencies(pkg6[0], "", tt.allow, nil, nil)
+				require.NoErrorf(t, err, "unable to get dependencies")
+
+				var actual = make([]string, 0, len(deps))
+				for _, p := range deps {
+					actual = append(actual, p.Name)
+				}
+				require.True(t, reflect.DeepEqual(tt.expected, actual), "dependencies mismatch:\nactual %v\nexpect %v", actual, tt.expected)
+			})
+		}
+	})
 }
 
 func TestResolvePackage(t *testing.T) {