From b6117c76c9da4713fa08c75fede620f4ec8d83f5 Mon Sep 17 00:00:00 2001 From: Christopher Junk Date: Wed, 25 Feb 2026 18:29:55 +0100 Subject: [PATCH 1/6] fix: cache key for local images with xp v2.2 On-behalf-of: @SAP christopher.junk@sap.com Signed-off-by: Christopher Junk --- pkg/xpenvfuncs/xpenvfuncs.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pkg/xpenvfuncs/xpenvfuncs.go b/pkg/xpenvfuncs/xpenvfuncs.go index d65754e..172f23f 100644 --- a/pkg/xpenvfuncs/xpenvfuncs.go +++ b/pkg/xpenvfuncs/xpenvfuncs.go @@ -312,19 +312,24 @@ func loadCrossplanePackageToCluster(clusterName string, pkg string) env.Func { return ctx, err } - ref, err := name.ParseReference(pkg) - if err != nil { - return ctx, err - } - digest, err := retrieveDigest(ctx, pkg) if err != nil { return ctx, err } + var friendlyIdentifier string + if digest == localImageDigest { + friendlyIdentifier = friendlyID(pkg, digest) + } else { + ref, err := name.ParseReference(pkg) + if err != nil { + return ctx, err + } + friendlyIdentifier = friendlyID(parsePackageSourceFromReference(ref), digest) + } cacheKeys := []string{ fullyQualifiedPathName("/cache/xpkg/", pkg, ".gz"), - fullyQualifiedPathName("/cache/xpkg/", friendlyID(parsePackageSourceFromReference(ref), digest), ".gz"), + fullyQualifiedPathName("/cache/xpkg/", friendlyIdentifier, ".gz"), } for _, key := range cacheKeys { @@ -429,6 +434,11 @@ func installCrossplaneProviderEnvFunc(_ string, opts InstallCrossplaneProviderOp return func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { klog.V(4).Infof("Installing crossplane provider %s: %s", opts.Name, opts.Package) + digest, err := retrieveDigest(ctx, opts.Package) + if err != nil { + return ctx, err + } + data := struct { Name string Package string @@ -436,7 +446,7 @@ func installCrossplaneProviderEnvFunc(_ string, opts InstallCrossplaneProviderOp RuntimeConfig string }{ Name: opts.Name, - Package: opts.Package, + Package: fmt.Sprintf("%s@%s", opts.Package, digest), } if opts.ControllerConfig != nil { From c15c5cfb425b728608234f5b97368a31206e69ca Mon Sep 17 00:00:00 2001 From: Christopher Junk Date: Wed, 25 Feb 2026 18:35:39 +0100 Subject: [PATCH 2/6] fix: cache key for local images with xp v2.2 On-behalf-of: @SAP christopher.junk@sap.com Signed-off-by: Christopher Junk --- pkg/xpenvfuncs/xpenvfuncs.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/xpenvfuncs/xpenvfuncs.go b/pkg/xpenvfuncs/xpenvfuncs.go index 172f23f..685d25c 100644 --- a/pkg/xpenvfuncs/xpenvfuncs.go +++ b/pkg/xpenvfuncs/xpenvfuncs.go @@ -438,6 +438,9 @@ func installCrossplaneProviderEnvFunc(_ string, opts InstallCrossplaneProviderOp if err != nil { return ctx, err } + if digest == localImageDigest { + opts.Package += localImageDigest + } data := struct { Name string @@ -446,7 +449,7 @@ func installCrossplaneProviderEnvFunc(_ string, opts InstallCrossplaneProviderOp RuntimeConfig string }{ Name: opts.Name, - Package: fmt.Sprintf("%s@%s", opts.Package, digest), + Package: opts.Package, } if opts.ControllerConfig != nil { From c557d9ae47b1bf451b2266ab1397e654b7aecb76 Mon Sep 17 00:00:00 2001 From: Christopher Junk Date: Thu, 26 Feb 2026 17:18:19 +0100 Subject: [PATCH 3/6] test: package cache keys On-behalf-of: @SAP christopher.junk@sap.com Signed-off-by: Christopher Junk --- pkg/xpenvfuncs/xpenvfuncs.go | 40 +++++++++++-------- pkg/xpenvfuncs/xpenvfuncs_test.go | 65 +++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/pkg/xpenvfuncs/xpenvfuncs.go b/pkg/xpenvfuncs/xpenvfuncs.go index 685d25c..7bec952 100644 --- a/pkg/xpenvfuncs/xpenvfuncs.go +++ b/pkg/xpenvfuncs/xpenvfuncs.go @@ -312,25 +312,10 @@ func loadCrossplanePackageToCluster(clusterName string, pkg string) env.Func { return ctx, err } - digest, err := retrieveDigest(ctx, pkg) + cacheKeys, err := generatePackageCacheKeys(ctx, pkg, retrieveDigest) if err != nil { return ctx, err } - var friendlyIdentifier string - if digest == localImageDigest { - friendlyIdentifier = friendlyID(pkg, digest) - } else { - ref, err := name.ParseReference(pkg) - if err != nil { - return ctx, err - } - friendlyIdentifier = friendlyID(parsePackageSourceFromReference(ref), digest) - } - - cacheKeys := []string{ - fullyQualifiedPathName("/cache/xpkg/", pkg, ".gz"), - fullyQualifiedPathName("/cache/xpkg/", friendlyIdentifier, ".gz"), - } for _, key := range cacheKeys { if err := docker.Exec(clusterControlPlaneName, "mkdir", "-m", "777", "-p", filepath.Dir(key)); err != nil { @@ -348,6 +333,29 @@ func loadCrossplanePackageToCluster(clusterName string, pkg string) env.Func { } } +type retrieveDigestFunc func(context.Context, string) (string, error) + +func generatePackageCacheKeys(ctx context.Context, pkg string, digestFunc retrieveDigestFunc) ([]string, error) { + digest, err := digestFunc(ctx, pkg) + if err != nil { + return nil, err + } + var friendlyIdentifier string + if digest == localImageDigest { + friendlyIdentifier = friendlyID(pkg, digest) + } else { + ref, err := name.ParseReference(pkg) + if err != nil { + return nil, err + } + friendlyIdentifier = friendlyID(parsePackageSourceFromReference(ref), digest) + } + return []string{ + fullyQualifiedPathName("/cache/xpkg/", pkg, ".gz"), + fullyQualifiedPathName("/cache/xpkg/", friendlyIdentifier, ".gz"), + }, nil +} + // (from crossplane internal/xpkg) func fullyQualifiedPathName(cacheDir, packageName, ext string) string { full := filepath.Join(cacheDir, packageName) diff --git a/pkg/xpenvfuncs/xpenvfuncs_test.go b/pkg/xpenvfuncs/xpenvfuncs_test.go index 375313f..611b94d 100644 --- a/pkg/xpenvfuncs/xpenvfuncs_test.go +++ b/pkg/xpenvfuncs/xpenvfuncs_test.go @@ -7,6 +7,7 @@ import ( "github.com/crossplane-contrib/xp-testing/pkg/vendored" "github.com/pkg/errors" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sigs.k8s.io/e2e-framework/pkg/env" "sigs.k8s.io/e2e-framework/pkg/envconf" @@ -404,3 +405,67 @@ func Test_ValidateTestSetup(t *testing.T) { }) } } + +func Test_generatePackageCacheKeys(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + pkg string + digestFunc retrieveDigestFunc + want []string + wantErr bool + }{ + { + name: "remote image", + pkg: "xpkg.upbound.io/crossplane-contrib/provider-nop:v0.2.0", + digestFunc: func(ctx context.Context, s string) (string, error) { + return "sha256:552a394a8accd2b4d37fc5858abe93d311e727eafb3c00636e11c72572873e48", nil + }, + want: []string{ + // note that the pkg repo name .0 is interpreted as file extension pre v2.2 and is replaced with gz + "/cache/xpkg/xpkg.upbound.io/crossplane-contrib/provider-nop:v0.2.gz", // < v2.2 + // note that the image tag is cut off + "/cache/xpkg/xpkg-upbound-io-crossplane-contrib-provider-nop-sha256-552a3.gz", // >= v2.2 + }, + wantErr: false, + }, + { + name: "local image", + pkg: "index.docker.io/build-908b1e2d/provider-nop:5eaddce-dirty", + digestFunc: func(ctx context.Context, s string) (string, error) { + return localImageDigest, nil + }, + want: []string{ + // note that pkg repo name does not contain a file extension and remains completely the same + "/cache/xpkg/index.docker.io/build-908b1e2d/provider-nop:5eaddce-dirty.gz", // < v2.2 + // note that the image tag is not cut off but truncated + "/cache/xpkg/index-docker-io-build-908b1e2d-provider-nop-5eaddc-sha256-00000.gz", // >= v2.2 + }, + wantErr: false, + }, + { + name: "retrieve digest error", + pkg: "xpkg.upbound.io/crossplane-contrib/provider-nop:v0.2.0", + digestFunc: func(ctx context.Context, s string) (string, error) { + return "", errors.Errorf("error") + }, + want: []string{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := generatePackageCacheKeys(context.Background(), tt.pkg, tt.digestFunc) + if gotErr != nil { + if !tt.wantErr { + t.Errorf("generatePackageCacheKeys() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("generatePackageCacheKeys() succeeded unexpectedly") + } + assert.ElementsMatch(t, tt.want, got) + }) + } +} From 4074b3b66f4847c1f3805c82062e2efeae2e9e71 Mon Sep 17 00:00:00 2001 From: Christopher Junk Date: Mon, 2 Mar 2026 09:32:01 +0100 Subject: [PATCH 4/6] fix: package ref On-behalf-of: @SAP christopher.junk@sap.com Signed-off-by: Christopher Junk --- pkg/xpenvfuncs/xpenvfuncs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/xpenvfuncs/xpenvfuncs.go b/pkg/xpenvfuncs/xpenvfuncs.go index 7bec952..20e681e 100644 --- a/pkg/xpenvfuncs/xpenvfuncs.go +++ b/pkg/xpenvfuncs/xpenvfuncs.go @@ -447,7 +447,7 @@ func installCrossplaneProviderEnvFunc(_ string, opts InstallCrossplaneProviderOp return ctx, err } if digest == localImageDigest { - opts.Package += localImageDigest + opts.Package = fmt.Sprintf("%s@%s", opts.Package, localImageDigest) } data := struct { From ef65d22ccc52e445834b852f674b98e3babbdbf4 Mon Sep 17 00:00:00 2001 From: Christopher Junk Date: Wed, 4 Mar 2026 17:51:30 +0100 Subject: [PATCH 5/6] refactor: localImage flag On-behalf-of: @SAP christopher.junk@sap.com Signed-off-by: Christopher Junk --- pkg/images/lookup.go | 1 + pkg/setup/setup.go | 1 + pkg/xpenvfuncs/xpenvfuncs.go | 23 ++++++++++++----------- pkg/xpenvfuncs/xpenvfuncs_test.go | 22 ++++++++++++++++++++-- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/pkg/images/lookup.go b/pkg/images/lookup.go index 6fec77c..04d1292 100644 --- a/pkg/images/lookup.go +++ b/pkg/images/lookup.go @@ -17,6 +17,7 @@ const ( type ProviderImages struct { Package string ControllerImage *string + LocalImage bool } // GetImagesFromEnvironmentOrPanic retrieves image information from the environment and panics if `E2E_IMAGES` is not set diff --git a/pkg/setup/setup.go b/pkg/setup/setup.go index d47e4ac..ac9ca00 100644 --- a/pkg/setup/setup.go +++ b/pkg/setup/setup.go @@ -109,6 +109,7 @@ func (s *ClusterSetup) Configure(testEnv env.Environment, cluster *kind.Cluster) ControllerImage: s.Images.ControllerImage, ControllerConfig: s.ControllerConfig, DeploymentRuntimeConfig: s.DeploymentRuntimeConfig, + LocalImage: s.Images.LocalImage, }), ), firstSetup), setupProviderCredentials(s), diff --git a/pkg/xpenvfuncs/xpenvfuncs.go b/pkg/xpenvfuncs/xpenvfuncs.go index 20e681e..829b19b 100644 --- a/pkg/xpenvfuncs/xpenvfuncs.go +++ b/pkg/xpenvfuncs/xpenvfuncs.go @@ -210,13 +210,14 @@ type InstallCrossplaneProviderOptions struct { ControllerImage *string // TODO read from package ControllerConfig *vendored.ControllerConfig DeploymentRuntimeConfig *vendored.DeploymentRuntimeConfig + LocalImage bool } // InstallCrossplaneProvider returns an env.Func that is used to // install a crossplane provider into the active cluster func InstallCrossplaneProvider(clusterName string, opts InstallCrossplaneProviderOptions) env.Func { return Compose( - loadCrossplanePackageToCluster(clusterName, opts.Package), + loadCrossplanePackageToCluster(clusterName, opts.LocalImage, opts.Package), loadCrossplaneControllerImageToCluster(clusterName, opts.ControllerImage), installCrossplaneProviderEnvFunc(clusterName, opts), awaitProviderHealthy(opts.Name), @@ -296,7 +297,7 @@ func setupCrossplanePackageCache(clusterName string, cacheName string) env.Func } // loadCrossplanePackageToCluster loads the crossplane config package into the given clusters package cache folder (/cache) -func loadCrossplanePackageToCluster(clusterName string, pkg string) env.Func { +func loadCrossplanePackageToCluster(clusterName string, localImage bool, pkg string) env.Func { return func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { f, err := os.CreateTemp("", "xpkg") if err != nil { @@ -312,7 +313,7 @@ func loadCrossplanePackageToCluster(clusterName string, pkg string) env.Func { return ctx, err } - cacheKeys, err := generatePackageCacheKeys(ctx, pkg, retrieveDigest) + cacheKeys, err := generatePackageCacheKeys(ctx, pkg, localImage, retrieveDigest) if err != nil { return ctx, err } @@ -335,13 +336,13 @@ func loadCrossplanePackageToCluster(clusterName string, pkg string) env.Func { type retrieveDigestFunc func(context.Context, string) (string, error) -func generatePackageCacheKeys(ctx context.Context, pkg string, digestFunc retrieveDigestFunc) ([]string, error) { +func generatePackageCacheKeys(ctx context.Context, pkg string, localImage bool, digestFunc retrieveDigestFunc) ([]string, error) { digest, err := digestFunc(ctx, pkg) if err != nil { return nil, err } var friendlyIdentifier string - if digest == localImageDigest { + if localImage { friendlyIdentifier = friendlyID(pkg, digest) } else { ref, err := name.ParseReference(pkg) @@ -442,12 +443,12 @@ func installCrossplaneProviderEnvFunc(_ string, opts InstallCrossplaneProviderOp return func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { klog.V(4).Infof("Installing crossplane provider %s: %s", opts.Name, opts.Package) - digest, err := retrieveDigest(ctx, opts.Package) - if err != nil { - return ctx, err - } - if digest == localImageDigest { - opts.Package = fmt.Sprintf("%s@%s", opts.Package, localImageDigest) + if opts.LocalImage { + digest, err := retrieveDigest(ctx, opts.Package) + if err != nil { + return ctx, err + } + opts.Package = fmt.Sprintf("%s@%s", opts.Package, digest) } data := struct { diff --git a/pkg/xpenvfuncs/xpenvfuncs_test.go b/pkg/xpenvfuncs/xpenvfuncs_test.go index 611b94d..aa86928 100644 --- a/pkg/xpenvfuncs/xpenvfuncs_test.go +++ b/pkg/xpenvfuncs/xpenvfuncs_test.go @@ -411,6 +411,7 @@ func Test_generatePackageCacheKeys(t *testing.T) { name string // description of this test case // Named input parameters for target function. pkg string + localImage bool digestFunc retrieveDigestFunc want []string wantErr bool @@ -418,6 +419,7 @@ func Test_generatePackageCacheKeys(t *testing.T) { { name: "remote image", pkg: "xpkg.upbound.io/crossplane-contrib/provider-nop:v0.2.0", + localImage: false, digestFunc: func(ctx context.Context, s string) (string, error) { return "sha256:552a394a8accd2b4d37fc5858abe93d311e727eafb3c00636e11c72572873e48", nil }, @@ -430,8 +432,9 @@ func Test_generatePackageCacheKeys(t *testing.T) { wantErr: false, }, { - name: "local image", + name: "local image without repo digest", pkg: "index.docker.io/build-908b1e2d/provider-nop:5eaddce-dirty", + localImage: true, digestFunc: func(ctx context.Context, s string) (string, error) { return localImageDigest, nil }, @@ -443,6 +446,21 @@ func Test_generatePackageCacheKeys(t *testing.T) { }, wantErr: false, }, + { + name: "local image with repo digest", + pkg: "index.docker.io/build-908b1e2d/provider-nop:5eaddce-dirty", + localImage: true, + digestFunc: func(ctx context.Context, s string) (string, error) { + return "sha256:2605848b12fdd3a0f2b21e3b36acb8beaa0b01f93db834584956c58b0569157d", nil + }, + want: []string{ + // note that pkg repo name does not contain a file extension and remains completely the same + "/cache/xpkg/index.docker.io/build-908b1e2d/provider-nop:5eaddce-dirty.gz", // < v2.2 + // note that the image tag is not cut off but truncated + "/cache/xpkg/index-docker-io-build-908b1e2d-provider-nop-5eaddc-sha256-26058.gz", // >= v2.2 + }, + wantErr: false, + }, { name: "retrieve digest error", pkg: "xpkg.upbound.io/crossplane-contrib/provider-nop:v0.2.0", @@ -455,7 +473,7 @@ func Test_generatePackageCacheKeys(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, gotErr := generatePackageCacheKeys(context.Background(), tt.pkg, tt.digestFunc) + got, gotErr := generatePackageCacheKeys(context.Background(), tt.pkg, tt.localImage, tt.digestFunc) if gotErr != nil { if !tt.wantErr { t.Errorf("generatePackageCacheKeys() failed: %v", gotErr) From 1a7d449d74b28b7aa466302525ec08714fd181e0 Mon Sep 17 00:00:00 2001 From: Christopher Junk Date: Wed, 4 Mar 2026 17:55:56 +0100 Subject: [PATCH 6/6] refactor: localImage flag On-behalf-of: @SAP christopher.junk@sap.com Signed-off-by: Christopher Junk --- pkg/xpenvfuncs/xpenvfuncs_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/xpenvfuncs/xpenvfuncs_test.go b/pkg/xpenvfuncs/xpenvfuncs_test.go index aa86928..2cff942 100644 --- a/pkg/xpenvfuncs/xpenvfuncs_test.go +++ b/pkg/xpenvfuncs/xpenvfuncs_test.go @@ -417,8 +417,8 @@ func Test_generatePackageCacheKeys(t *testing.T) { wantErr bool }{ { - name: "remote image", - pkg: "xpkg.upbound.io/crossplane-contrib/provider-nop:v0.2.0", + name: "remote image", + pkg: "xpkg.upbound.io/crossplane-contrib/provider-nop:v0.2.0", localImage: false, digestFunc: func(ctx context.Context, s string) (string, error) { return "sha256:552a394a8accd2b4d37fc5858abe93d311e727eafb3c00636e11c72572873e48", nil @@ -432,8 +432,8 @@ func Test_generatePackageCacheKeys(t *testing.T) { wantErr: false, }, { - name: "local image without repo digest", - pkg: "index.docker.io/build-908b1e2d/provider-nop:5eaddce-dirty", + name: "local image without repo digest", + pkg: "index.docker.io/build-908b1e2d/provider-nop:5eaddce-dirty", localImage: true, digestFunc: func(ctx context.Context, s string) (string, error) { return localImageDigest, nil @@ -447,8 +447,8 @@ func Test_generatePackageCacheKeys(t *testing.T) { wantErr: false, }, { - name: "local image with repo digest", - pkg: "index.docker.io/build-908b1e2d/provider-nop:5eaddce-dirty", + name: "local image with repo digest", + pkg: "index.docker.io/build-908b1e2d/provider-nop:5eaddce-dirty", localImage: true, digestFunc: func(ctx context.Context, s string) (string, error) { return "sha256:2605848b12fdd3a0f2b21e3b36acb8beaa0b01f93db834584956c58b0569157d", nil