From b6c12327766c91d0defa3575ef20a2dc54df4131 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:14:56 +0200 Subject: [PATCH 01/20] Add new function LookupLatestReleaseTagsViaGitHub --- internal/stackroxversions/release_tag.go | 91 +++++++++++++++++++ .../release_tag_integration_test.go | 26 ++++++ 2 files changed, 117 insertions(+) create mode 100644 internal/stackroxversions/release_tag.go create mode 100644 internal/stackroxversions/release_tag_integration_test.go diff --git a/internal/stackroxversions/release_tag.go b/internal/stackroxversions/release_tag.go new file mode 100644 index 00000000..5e452c27 --- /dev/null +++ b/internal/stackroxversions/release_tag.go @@ -0,0 +1,91 @@ +package stackroxversions + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "sort" + + "github.com/Masterminds/semver/v3" + "github.com/stackrox/roxie/internal/constants" +) + +const ( + gitHubAPIBase = "https://api.github.com/repos/" + constants.GitHubStackroxRepo +) + +type ghRelease struct { + TagName string `json:"tag_name"` +} + +// LookupLatestReleaseTagsViaGitHub queries the GitHub releases API for stackrox/stackrox +// and attempts to return the newest atMost stable release tags (e.g. ["4.11.0", "4.10.4", "4.9.8"]), +// sorted by descending semver. Pre-release and RC tags are excluded. +// It is not guaranteed that the function returns exactly atMost tags, though this should be +// the case for reasonable values of atMost (0 < atMost < 10). +// The code is deliberately kept simple, i.e. it is not using pagination, which would +// smell like over-engineering for the use-case we need this for. +func LookupLatestReleaseTagsViaGitHub(ctx context.Context, atMost int) ([]string, error) { + if atMost <= 0 { + return nil, fmt.Errorf("atMost must be positive, got %d", atMost) + } + + tags, err := fetchLatestGitHubReleases(ctx, atMost) + if err != nil { + return nil, err + } + + var versions semver.Collection + for _, tag := range tags { + version, err := semver.NewVersion(tag) + if err != nil || version.Prerelease() != "" { + continue + } + versions = append(versions, version) + } + sort.Sort(sort.Reverse(versions)) + + n := min(atMost, len(versions)) + if n == 0 { + return nil, errors.New("failed to obtain any release tags parsing as semantic versions") + } + + sortedReleaseTags := make([]string, n) + for i := range n { + sortedReleaseTags[i] = versions[i].Original() + } + + return sortedReleaseTags, nil +} + +func fetchLatestGitHubReleases(ctx context.Context, atMost int) ([]string, error) { + releasesToFetch := min(100, atMost*10) // Reasonable estimate for how many tags we intend to fetch. + url := fmt.Sprintf("%s?per_page=%d", gitHubAPIBase+"/releases", releasesToFetch) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + req.Header.Set("Accept", "application/vnd.github+json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("fetching releases: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode) + } + + var releasesResponse []ghRelease + if err := json.NewDecoder(resp.Body).Decode(&releasesResponse); err != nil { + return nil, fmt.Errorf("decoding releases response: %w", err) + } + + // Convert to string slice. + tags := make([]string, len(releasesResponse)) + for i, release := range releasesResponse { + tags[i] = release.TagName + } + return tags, nil +} diff --git a/internal/stackroxversions/release_tag_integration_test.go b/internal/stackroxversions/release_tag_integration_test.go new file mode 100644 index 00000000..14ab2c67 --- /dev/null +++ b/internal/stackroxversions/release_tag_integration_test.go @@ -0,0 +1,26 @@ +//go:build integration + +package stackroxversions + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLookupLatestReleaseTags_Integration(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + tags, err := LookupLatestReleaseTagsViaGitHub(ctx, 3) + require.NoError(t, err) + assert.NotEmpty(t, tags, "no tags") + + t.Log("Latest release tags") + for i, tag := range tags { + t.Logf("%v.: %s", i, tag) + } +} From b6c2f7818eba9fa34f926bacb892530c879b1d73 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:15:43 +0200 Subject: [PATCH 02/20] Add function LookupLatestTag, remove hard-coded fallback --- internal/helpers/tag.go | 41 +++++++++++++++++++----- internal/helpers/tag_integration_test.go | 36 +++++++++++++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 internal/helpers/tag_integration_test.go diff --git a/internal/helpers/tag.go b/internal/helpers/tag.go index 39a3cbb3..9a664f52 100644 --- a/internal/helpers/tag.go +++ b/internal/helpers/tag.go @@ -1,19 +1,19 @@ package helpers import ( + "context" + "fmt" "os" "strings" + "github.com/stackrox/roxie/internal/constants" "github.com/stackrox/roxie/internal/env" "github.com/stackrox/roxie/internal/logger" + "github.com/stackrox/roxie/internal/ocihelper" + "github.com/stackrox/roxie/internal/stackroxversions" ) -const ( - // TODO(#91): Is the plan to keep bumping this on new ACS releases? - defaultMainImageTag = "4.9.2" -) - -func LookupMainImageTag(log *logger.Logger) (string, error) { +func LookupMainImageTag(ctx context.Context, log *logger.Logger) (string, error) { log.Info("Looking up main image tag") if tag := os.Getenv("MAIN_IMAGE_TAG"); tag != "" { log.Dimf("Using MAIN_IMAGE_TAG from environment: %s", tag) @@ -29,11 +29,36 @@ func LookupMainImageTag(log *logger.Logger) (string, error) { return tag, nil } - log.Warningf("No MAIN_IMAGE_TAG found in the environment, using default main image tag %s for deployment", defaultMainImageTag) + log.Warningf("No MAIN_IMAGE_TAG found in the environment, looking up latest release tag on registry") log.Warning("To use a different tag, set the MAIN_IMAGE_TAG environment variable") log.Warning("Alternatively, execute roxie from within the stackrox repository, in which case the currently checked out stackrox tag will be used") - return defaultMainImageTag, nil + latestTag, err := LookupLatestTag(ctx, log) + if err != nil { + return "", fmt.Errorf("looking up latest release tag: %w", err) + } + + return latestTag, nil +} + +// Computes the latest image tag for a pullable, released main image. +func LookupLatestTag(ctx context.Context, log *logger.Logger) (string, error) { + const atMost = 5 + + tags, err := stackroxversions.LookupLatestReleaseTagsViaGitHub(ctx, atMost) + if err != nil { + return "", fmt.Errorf("looking up latest release tags: %w", err) + } + + // Verify we have a pullable main image. + for _, tag := range tags { + mainImage := fmt.Sprintf("%s/main:%s", constants.DefaultRegistry, tag) + if err := ocihelper.VerifyImageExistence(ctx, log, mainImage); err == nil { + return tag, nil + } + } + + return "", fmt.Errorf("failed to verify main image existence for tags %s", strings.Join(tags, ", ")) } func ConvertMainTagToOperatorTag(mainTag string) string { diff --git a/internal/helpers/tag_integration_test.go b/internal/helpers/tag_integration_test.go new file mode 100644 index 00000000..4821bb20 --- /dev/null +++ b/internal/helpers/tag_integration_test.go @@ -0,0 +1,36 @@ +//go:build integration + +package helpers + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/stackrox/roxie/internal/constants" + "github.com/stackrox/roxie/internal/logger" + "github.com/stretchr/testify/require" +) + +func TestLookupLatestTag_Integration(t *testing.T) { + log := logger.New() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + tag, err := LookupLatestTag(ctx, log) + require.NoError(t, err) + require.NotEmpty(t, tag) + + imageRef := fmt.Sprintf("%s/main:%s", constants.DefaultRegistry, tag) + ref, err := name.ParseReference(imageRef) + require.NoError(t, err) + + _, err = remote.Head(ref, remote.WithContext(ctx), remote.WithAuthFromKeychain(authn.DefaultKeychain)) + require.NoError(t, err, "image %s is not pullable", imageRef) + + t.Logf("Latest pullable tag: %s (%s)", tag, imageRef) +} From 379b02d985746da05e23da9d0b1894480c4861d0 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:16:46 +0200 Subject: [PATCH 03/20] Adjust to new LookupMainImageTag API --- cmd/deploy.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 638278a3..f1b9e044 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -279,10 +279,13 @@ func runDeploy(cmd *cobra.Command, args []string) error { return fmt.Errorf("applying config patches from command line argument: %w", err) } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + if deploySettings.Roxie.Version != "" { log.Dimf("Using main image tag %s", deploySettings.Roxie.Version) } else { - mainImageTag, err := helpers.LookupMainImageTag(log) + mainImageTag, err := helpers.LookupMainImageTag(ctx, log) if err != nil { return fmt.Errorf("looking up main image tag: %w", err) } @@ -333,9 +336,6 @@ func runDeploy(cmd *cobra.Command, args []string) error { return nil } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) - defer cancel() - // If we are deploying to a local cluster and the images exist locally, then we transfer them // to the local cluster. if deploySettings.Roxie.ClusterType.IsLocal() && !deploySettings.Roxie.KonfluxImagesEnabled() { From d31062c16de45bb29123af1143cb35e44a4c5db5 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:17:17 +0200 Subject: [PATCH 04/20] New constants package --- internal/constants/registries.go | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 internal/constants/registries.go diff --git a/internal/constants/registries.go b/internal/constants/registries.go new file mode 100644 index 00000000..cdb93338 --- /dev/null +++ b/internal/constants/registries.go @@ -0,0 +1,6 @@ +package constants + +const ( + DefaultRegistry = "quay.io/rhacs-eng" + GitHubStackroxRepo = "stackrox/stackrox" +) From 2628d0bc5829049419d035d5de5aacb9c659900c Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:17:36 +0200 Subject: [PATCH 05/20] Use new constants --- internal/deployer/acs_images.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/deployer/acs_images.go b/internal/deployer/acs_images.go index 0a66c15a..1d2a340c 100644 --- a/internal/deployer/acs_images.go +++ b/internal/deployer/acs_images.go @@ -1,9 +1,9 @@ package deployer -import "fmt" +import ( + "fmt" -const ( - imageRegistry = "quay.io/rhacs-eng" + "github.com/stackrox/roxie/internal/constants" ) func imagesForConfig(config Config) []string { @@ -13,6 +13,7 @@ func imagesForConfig(config Config) []string { prefix = "release-" } + imageRegistry := constants.DefaultRegistry images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "main", config.Roxie.Version)) images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "central-db", config.Roxie.Version)) images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "scanner-v4-db", config.Roxie.Version)) @@ -27,6 +28,7 @@ func imagesForConfig(config Config) []string { } func OperatorBundleImage(config Config) string { + imageRegistry := constants.DefaultRegistry if config.Roxie.KonfluxImagesEnabled() { return fmt.Sprintf("%s/release-operator-bundle:v%s", imageRegistry, config.Operator.Version) } From b77eeb6f83f37f550e06a2f3d8cfeb2eae0e3a11 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:17:58 +0200 Subject: [PATCH 06/20] Use new constants package --- internal/deployer/operator.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/deployer/operator.go b/internal/deployer/operator.go index f309dde2..7f0bba32 100644 --- a/internal/deployer/operator.go +++ b/internal/deployer/operator.go @@ -14,6 +14,7 @@ import ( "gopkg.in/yaml.v3" + "github.com/stackrox/roxie/internal/constants" "github.com/stackrox/roxie/internal/k8s" "github.com/stackrox/roxie/internal/ocihelper" ) @@ -203,7 +204,7 @@ func (d *Deployer) applyImageContentSourcePolicy(ctx context.Context) error { // Define repository digest mirrors as Go data structures rewrite := func(from, to string) map[string]interface{} { source := fmt.Sprintf("registry.redhat.io/advanced-cluster-security/%s", from) - mirror := fmt.Sprintf("quay.io/rhacs-eng/%s", to) + mirror := fmt.Sprintf("%s/%s", constants.DefaultRegistry, to) if d.verbose { d.logger.Dimf("Image rewriting rule: %s -> %s", source, mirror) } From 4296af1e958c72026c79097cb0229fa9c4479689 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:18:15 +0200 Subject: [PATCH 07/20] Use new constants package --- internal/deployer/operator_olm.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/deployer/operator_olm.go b/internal/deployer/operator_olm.go index c55c0fca..1dfd6833 100644 --- a/internal/deployer/operator_olm.go +++ b/internal/deployer/operator_olm.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/stackrox/roxie/internal/constants" "github.com/stackrox/roxie/internal/k8s" "gopkg.in/yaml.v3" ) @@ -17,7 +18,7 @@ const ( subscriptionName = "stackrox-operator-subscription" operatorGroupName = "all-namespaces-operator-group" operatorChannel = "latest" - operatorIndexImage = "quay.io/rhacs-eng/stackrox-operator-index" + operatorIndexImage = constants.DefaultRegistry + "/stackrox-operator-index" namespacedSubscriptionName = operatorNamespace + "/" + subscriptionName ) From 6cc02e9581905b93d699b5378e070f99a685ca1d Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:31:40 +0200 Subject: [PATCH 08/20] Increase context timeout in integration test --- internal/helpers/tag_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/helpers/tag_integration_test.go b/internal/helpers/tag_integration_test.go index 4821bb20..8a66eb51 100644 --- a/internal/helpers/tag_integration_test.go +++ b/internal/helpers/tag_integration_test.go @@ -18,7 +18,7 @@ import ( func TestLookupLatestTag_Integration(t *testing.T) { log := logger.New() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() tag, err := LookupLatestTag(ctx, log) From 3c2fd19ad9cb42a93af0e940d93403e5171e4bfd Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:39:11 +0200 Subject: [PATCH 09/20] LookupLatestTag --- internal/helpers/tag.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/helpers/tag.go b/internal/helpers/tag.go index 9a664f52..c7b8f692 100644 --- a/internal/helpers/tag.go +++ b/internal/helpers/tag.go @@ -2,10 +2,13 @@ package helpers import ( "context" + "errors" "fmt" + "net/http" "os" "strings" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/stackrox/roxie/internal/constants" "github.com/stackrox/roxie/internal/env" "github.com/stackrox/roxie/internal/logger" @@ -53,9 +56,14 @@ func LookupLatestTag(ctx context.Context, log *logger.Logger) (string, error) { // Verify we have a pullable main image. for _, tag := range tags { mainImage := fmt.Sprintf("%s/main:%s", constants.DefaultRegistry, tag) - if err := ocihelper.VerifyImageExistence(ctx, log, mainImage); err == nil { - return tag, nil + if err := ocihelper.VerifyImageExistence(ctx, log, mainImage); err != nil { + var te *transport.Error + if errors.As(err, &te) && te.StatusCode == http.StatusNotFound { + continue + } + return "", fmt.Errorf("verifying image %s: %w", mainImage, err) } + return tag, nil } return "", fmt.Errorf("failed to verify main image existence for tags %s", strings.Join(tags, ", ")) From 2151a15e90edca32658bea578ffa9556c5cc4584 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:39:57 +0200 Subject: [PATCH 10/20] VerifyImageExistence: Retry also on rate limiting --- internal/ocihelper/ocihelper.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index f2ab93ad..81d1d890 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "net/http" "os" "path/filepath" @@ -28,10 +29,10 @@ func VerifyImageExistence(ctx context.Context, log *logger.Logger, imageRef stri return fmt.Errorf("invalid image reference: %w", err) } - // Use HEAD request to verify image exists without downloading _, err = remote.Head(ref, remote.WithContext(ctx), - remote.WithAuthFromKeychain(authn.DefaultKeychain)) + remote.WithAuthFromKeychain(authn.DefaultKeychain), + remote.WithRetryStatusCodes(http.StatusTooManyRequests, http.StatusServiceUnavailable)) if err != nil { return fmt.Errorf("image inspection failed: %w", err) } From cf9d374f67398d3fb3fd47be74036594c09957ca Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:40:22 +0200 Subject: [PATCH 11/20] Add test for VerifyImageExistence NotFound behavior --- internal/helpers/tag_integration_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/helpers/tag_integration_test.go b/internal/helpers/tag_integration_test.go index 8a66eb51..0b3d0364 100644 --- a/internal/helpers/tag_integration_test.go +++ b/internal/helpers/tag_integration_test.go @@ -4,15 +4,20 @@ package helpers import ( "context" + "errors" "fmt" + "net/http" "testing" "time" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/stackrox/roxie/internal/constants" "github.com/stackrox/roxie/internal/logger" + "github.com/stackrox/roxie/internal/ocihelper" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -34,3 +39,17 @@ func TestLookupLatestTag_Integration(t *testing.T) { t.Logf("Latest pullable tag: %s (%s)", tag, imageRef) } + +func TestVerifyImageExistence_NotFound_Integration(t *testing.T) { + log := logger.New() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + madeUpImage := fmt.Sprintf("%s/main:99.99.99", constants.DefaultRegistry) + err := ocihelper.VerifyImageExistence(ctx, log, madeUpImage) + require.Error(t, err) + + var te *transport.Error + require.True(t, errors.As(err, &te), "expected transport.Error, got %T", err) + assert.Equal(t, http.StatusNotFound, te.StatusCode) +} From aad4bedbae98da8991a210bf44047a10f9993450 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Tue, 30 Jun 2026 09:46:52 +0200 Subject: [PATCH 12/20] Add REGISTRY_* env var names to constants package --- internal/constants/registries.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/constants/registries.go b/internal/constants/registries.go index cdb93338..8affd99b 100644 --- a/internal/constants/registries.go +++ b/internal/constants/registries.go @@ -3,4 +3,7 @@ package constants const ( DefaultRegistry = "quay.io/rhacs-eng" GitHubStackroxRepo = "stackrox/stackrox" + + EnvRegistryUsername = "REGISTRY_USERNAME" + EnvRegistryPassword = "REGISTRY_PASSWORD" ) From bc46961652e6a2515fa3e8c19cfb26b52092d642 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Tue, 30 Jun 2026 09:47:41 +0200 Subject: [PATCH 13/20] Use new constants in dockerauth package --- internal/dockerauth/dockerauth.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/dockerauth/dockerauth.go b/internal/dockerauth/dockerauth.go index f6f0284d..fde6bd07 100644 --- a/internal/dockerauth/dockerauth.go +++ b/internal/dockerauth/dockerauth.go @@ -10,6 +10,7 @@ import ( "os/exec" "path/filepath" + "github.com/stackrox/roxie/internal/constants" "github.com/stackrox/roxie/internal/logger" ) @@ -61,14 +62,14 @@ func (d *DockerAuth) GetAndVerifyCredentials() (*Credentials, error) { var username, password string // Try environment variables first. - username = os.Getenv("REGISTRY_USERNAME") - password = os.Getenv("REGISTRY_PASSWORD") + username = os.Getenv(constants.EnvRegistryUsername) + password = os.Getenv(constants.EnvRegistryPassword) if username != "" && password == "" { - return nil, errors.New("REGISTRY_USERNAME set but REGISTRY_PASSWORD is empty") + return nil, fmt.Errorf("%s set but %s is empty", constants.EnvRegistryUsername, constants.EnvRegistryPassword) } if username == "" && password != "" { - return nil, errors.New("REGISTRY_PASSWORD set but REGISTRY_USERNAME is empty") + return nil, fmt.Errorf("%s set but %s is empty", constants.EnvRegistryPassword, constants.EnvRegistryUsername) } if username == "" { From bb1a3c89786e7db656f49bff38edbc86801b718a Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Tue, 30 Jun 2026 09:48:17 +0200 Subject: [PATCH 14/20] New keychain respecting the REGISTRY_* env vars for go-containerregistry access --- internal/ocihelper/keychain.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 internal/ocihelper/keychain.go diff --git a/internal/ocihelper/keychain.go b/internal/ocihelper/keychain.go new file mode 100644 index 00000000..c54f5101 --- /dev/null +++ b/internal/ocihelper/keychain.go @@ -0,0 +1,27 @@ +package ocihelper + +import ( + "os" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/stackrox/roxie/internal/constants" +) + +// Keychain resolves registry credentials by checking REGISTRY_USERNAME/REGISTRY_PASSWORD +// environment variables first, then falling back to the default Docker keychain +// (~/.docker/config.json, credential helpers, etc.). +var Keychain = authn.NewMultiKeychain(&envKeychain{}, authn.DefaultKeychain) + +type envKeychain struct{} + +func (e *envKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) { + username := os.Getenv(constants.EnvRegistryUsername) + password := os.Getenv(constants.EnvRegistryPassword) + if username == "" || password == "" { + return authn.Anonymous, nil + } + return authn.FromConfig(authn.AuthConfig{ + Username: username, + Password: password, + }), nil +} From dbd725e93b285f35f910a6ddf728cf8cf91c688d Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Tue, 30 Jun 2026 09:48:31 +0200 Subject: [PATCH 15/20] Use custom keychain in ocihelper package --- internal/ocihelper/ocihelper.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index 81d1d890..81856bab 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -9,7 +9,6 @@ import ( "os" "path/filepath" - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/daemon" @@ -31,7 +30,7 @@ func VerifyImageExistence(ctx context.Context, log *logger.Logger, imageRef stri _, err = remote.Head(ref, remote.WithContext(ctx), - remote.WithAuthFromKeychain(authn.DefaultKeychain), + remote.WithAuthFromKeychain(Keychain), remote.WithRetryStatusCodes(http.StatusTooManyRequests, http.StatusServiceUnavailable)) if err != nil { return fmt.Errorf("image inspection failed: %w", err) @@ -101,7 +100,7 @@ func assureImageExistsLocally(ctx context.Context, log *logger.Logger, imageRef, img, err = remote.Image(ref, remote.WithContext(ctx), - remote.WithAuthFromKeychain(authn.DefaultKeychain), + remote.WithAuthFromKeychain(Keychain), remote.WithPlatform(platform)) if err != nil { return nil, fmt.Errorf("failed to fetch image: %w", err) From c3f1d5d330ee558b10026f913368147d873a8104 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 29 Jun 2026 17:18:31 +0200 Subject: [PATCH 16/20] Use new helpers.LookupLatestTag in tests --- tests/e2e/e2e_test.go | 25 +++++++++++++++++++++++-- tests/e2e/helpers.go | 7 ++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index fa68225b..1f9434a5 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -3,9 +3,14 @@ package e2e import ( + "context" "fmt" "os" "testing" + "time" + + "github.com/stackrox/roxie/internal/helpers" + "github.com/stackrox/roxie/internal/logger" ) func TestMain(m *testing.M) { @@ -15,9 +20,14 @@ func TestMain(m *testing.M) { os.Exit(1) } - // Set default MAIN_IMAGE_TAG if not set + // Use the most recent released ACS version if MAIN_IMAGE_TAG is not set. if os.Getenv("MAIN_IMAGE_TAG") == "" { - os.Setenv("MAIN_IMAGE_TAG", defaultMainImageTag) + mainImageTag, err := lookupLatestTag() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to lookup latest tag: %v\n", err) + os.Exit(1) + } + os.Setenv("MAIN_IMAGE_TAG", mainImageTag) } // Verify kubectl context @@ -38,6 +48,17 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +func lookupLatestTag() (string, error) { + log := logger.New() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + tag, err := helpers.LookupLatestTag(ctx, log) + if err != nil { + return "", err + } + return tag, nil +} + func TestDeployBothComponentsTogetherInSingleNamespace(t *testing.T) { dumpClusterStateOnFailure(t) diff --git a/tests/e2e/helpers.go b/tests/e2e/helpers.go index d7489dd3..a4471704 100644 --- a/tests/e2e/helpers.go +++ b/tests/e2e/helpers.go @@ -21,11 +21,8 @@ import ( ) const ( - // TODO(#91): We should come up with some auto-updating of this on ACS releases. - // Don't think we should directly inject nightlies here. - defaultMainImageTag = "4.10.1" - deployTimeout = 30 * time.Minute - teardownTimeout = 10 * time.Minute + deployTimeout = 30 * time.Minute + teardownTimeout = 10 * time.Minute ) var ( From 5702216e683d6fc99d99c01039115df9336786d0 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Wed, 1 Jul 2026 09:26:18 +0200 Subject: [PATCH 17/20] Fix integration test --- internal/helpers/tag_integration_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/helpers/tag_integration_test.go b/internal/helpers/tag_integration_test.go index 0b3d0364..3f474113 100644 --- a/internal/helpers/tag_integration_test.go +++ b/internal/helpers/tag_integration_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote/transport" @@ -34,7 +33,7 @@ func TestLookupLatestTag_Integration(t *testing.T) { ref, err := name.ParseReference(imageRef) require.NoError(t, err) - _, err = remote.Head(ref, remote.WithContext(ctx), remote.WithAuthFromKeychain(authn.DefaultKeychain)) + _, err = remote.Head(ref, remote.WithContext(ctx), remote.WithAuthFromKeychain(ocihelper.Keychain)) require.NoError(t, err, "image %s is not pullable", imageRef) t.Logf("Latest pullable tag: %s (%s)", tag, imageRef) From 1efe853e0f1113a1cc74e5c0780e468e48dbf86c Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Wed, 1 Jul 2026 09:29:36 +0200 Subject: [PATCH 18/20] Use t.Context() --- internal/helpers/tag_integration_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/helpers/tag_integration_test.go b/internal/helpers/tag_integration_test.go index 3f474113..990ddcb2 100644 --- a/internal/helpers/tag_integration_test.go +++ b/internal/helpers/tag_integration_test.go @@ -22,7 +22,7 @@ import ( func TestLookupLatestTag_Integration(t *testing.T) { log := logger.New() - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute) defer cancel() tag, err := LookupLatestTag(ctx, log) @@ -41,7 +41,7 @@ func TestLookupLatestTag_Integration(t *testing.T) { func TestVerifyImageExistence_NotFound_Integration(t *testing.T) { log := logger.New() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) defer cancel() madeUpImage := fmt.Sprintf("%s/main:99.99.99", constants.DefaultRegistry) From a86a3c49c77fed689ac1a15c4262b5e58d9c4827 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Wed, 1 Jul 2026 09:29:44 +0200 Subject: [PATCH 19/20] Prettier test log output --- internal/stackroxversions/release_tag_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/stackroxversions/release_tag_integration_test.go b/internal/stackroxversions/release_tag_integration_test.go index 14ab2c67..b08cbea5 100644 --- a/internal/stackroxversions/release_tag_integration_test.go +++ b/internal/stackroxversions/release_tag_integration_test.go @@ -21,6 +21,6 @@ func TestLookupLatestReleaseTags_Integration(t *testing.T) { t.Log("Latest release tags") for i, tag := range tags { - t.Logf("%v.: %s", i, tag) + t.Logf("%v. %s", i+1, tag) } } From e93409e772f79c215613064ed45ec483827741ef Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Wed, 1 Jul 2026 09:30:36 +0200 Subject: [PATCH 20/20] Use t.Context() --- internal/stackroxversions/release_tag_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/stackroxversions/release_tag_integration_test.go b/internal/stackroxversions/release_tag_integration_test.go index b08cbea5..c54a5511 100644 --- a/internal/stackroxversions/release_tag_integration_test.go +++ b/internal/stackroxversions/release_tag_integration_test.go @@ -12,7 +12,7 @@ import ( ) func TestLookupLatestReleaseTags_Integration(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) defer cancel() tags, err := LookupLatestReleaseTagsViaGitHub(ctx, 3)