From 2c9bf2718419301bc8e871b13fee959498156d50 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Wed, 11 Feb 2026 09:57:05 +0100 Subject: [PATCH 1/6] Integrate logger into env initialization, add convenience logging when running containerized --- cmd/deploy.go | 9 ++-- cmd/env.go | 14 +++++- cmd/main.go | 1 + cmd/teardown.go | 8 +++- internal/env/env.go | 92 ++++++++++++++++++++++++++++-------- internal/env/env_test.go | 4 +- internal/env/example_test.go | 14 ------ 7 files changed, 99 insertions(+), 43 deletions(-) delete mode 100644 internal/env/example_test.go diff --git a/cmd/deploy.go b/cmd/deploy.go index 6dcf6678..94018a99 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -50,9 +50,8 @@ Examples: func runDeploy(cmd *cobra.Command, args []string) error { log := logger.New() - - if env.RunningInContainer { - log.Dim("Running containerized.") + if err := env.Initialize(log); err != nil { + return err } if env.RunningInteractively { @@ -98,9 +97,9 @@ func runDeploy(cmd *cobra.Command, args []string) error { } // On infra OpenShift we already get image pull secrets for Quay automatically. - if env.GetCurrentClusterType() != env.InfraOpenShift4 { + if clusterType := env.GetCurrentClusterType(); clusterType != env.InfraOpenShift4 { if os.Getenv("REGISTRY_USERNAME") == "" || os.Getenv("REGISTRY_PASSWORD") == "" { - return errors.New("containerized mode requires REGISTRY_USERNAME and REGISTRY_PASSWORD environment variables") + return fmt.Errorf("containerized mode requires REGISTRY_USERNAME and REGISTRY_PASSWORD environment variables for clusters of type %s", clusterType) } if _, err := os.Stat("/kubeconfig"); err != nil { return fmt.Errorf("containerized mode requires /kubeconfig file: %w", err) diff --git a/cmd/env.go b/cmd/env.go index b5bc4c68..5e19ea65 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -2,9 +2,11 @@ package main import ( "fmt" + "os" "github.com/spf13/cobra" "github.com/stackrox/roxie/internal/env" + "github.com/stackrox/roxie/internal/logger" ) func newEnvCmd() *cobra.Command { @@ -13,16 +15,24 @@ func newEnvCmd() *cobra.Command { Short: "Display environment information", Long: `Display detected environment information including cluster type and container status.`, Hidden: true, // Hidden command for debugging/inspection - Run: runEnv, + RunE: runEnv, } return cmd } -func runEnv(cmd *cobra.Command, args []string) { +func runEnv(cmd *cobra.Command, args []string) error { + log := logger.New() + if err := env.Initialize(log); err != nil { + return err + } + fmt.Println("Roxie Environment Information:") fmt.Println("==============================") + fmt.Printf("Kube config: %s\n", os.Getenv("KUBECONFIG")) fmt.Printf("Running in Container: %v\n", env.RunningInContainer) fmt.Printf("Current Context: %s\n", env.GetCurrentContext()) fmt.Printf("Cluster Type: %s\n", env.GetCurrentClusterType().String()) + + return nil } diff --git a/cmd/main.go b/cmd/main.go index 07982b71..f4bf6122 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -27,6 +27,7 @@ var ( ) func main() { + if err := rootCmd.Execute(); err != nil { red := color.New(color.FgRed, color.Bold) red.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/cmd/teardown.go b/cmd/teardown.go index a5d71446..d256a1ad 100644 --- a/cmd/teardown.go +++ b/cmd/teardown.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/stackrox/roxie/internal/deployer" + "github.com/stackrox/roxie/internal/env" "github.com/stackrox/roxie/internal/logger" ) @@ -28,13 +29,16 @@ func newTeardownCmd() *cobra.Command { } func runTeardown(cmd *cobra.Command, args []string) error { + log := logger.New() + if err := env.Initialize(log); err != nil { + return err + } + component := "both" if len(args) > 0 { component = args[0] } - log := logger.New() - log.Infof("Tearing down %s", component) d, err := deployer.New(log, "", []string{}) diff --git a/internal/env/env.go b/internal/env/env.go index 75026755..0747aba2 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -2,13 +2,16 @@ package env import ( "encoding/json" + "errors" "fmt" "net/url" "os" "os/exec" + "path/filepath" "strings" "github.com/stackrox/roxie/internal/containerutil" + "github.com/stackrox/roxie/internal/logger" "golang.org/x/term" ) @@ -65,25 +68,33 @@ func isRunningInteractively() bool { // ensureInitialized performs lazy initialization of cluster information // This avoids contacting the cluster on package import -func ensureInitialized() { +func ensureInitialized(log *logger.Logger) error { if !initialized { - kubeConfig := fetchKubeConfig() + if RunningInContainer { + log.Dim("Running containerized.") + } + kubeConfig, err := fetchKubeConfig(log) + if err != nil { + return err + } currentContext = kubeConfig.CurrentContext - apiResources := fetchAPIResources() + apiResources, err := fetchAPIResources() + if err != nil { + return err + } currentClusterType = detectClusterType(kubeConfig, apiResources) initialized = true } + return nil } // GetCurrentClusterType returns the current cluster type, initializing if needed func GetCurrentClusterType() ClusterType { - ensureInitialized() return currentClusterType } // GetCurrentContext returns the current kubectl context, initializing if needed func GetCurrentContext() string { - ensureInitialized() return currentContext } @@ -113,13 +124,15 @@ type KubeCluster struct { Server string } -// DetectClusterType identifies the cluster type for the current kubectl context -// This is a convenience wrapper that fetches the kubeconfig and API resources, -// then delegates to detectClusterType for the actual detection logic -func DetectClusterType() ClusterType { - kubeConfig := fetchKubeConfig() - apiResources := fetchAPIResources() - return detectClusterType(kubeConfig, apiResources) +// Initialize performs environment initialization and sets the global variables. +func Initialize(log *logger.Logger) error { + if log == nil { + log = logger.New() + } + if err := ensureInitialized(log); err != nil { + return fmt.Errorf("failed to initialize environment: %w", err) + } + return nil } // detectClusterType implements the cluster type detection logic @@ -180,12 +193,15 @@ func isOpenShift4(apiResources []string) bool { } // fetchKubeConfig retrieves the current kubectl configuration -func fetchKubeConfig() KubeConfig { +func fetchKubeConfig(log *logger.Logger) (KubeConfig, error) { + if err := kubeconfigChecks(log); err != nil { + return KubeConfig{}, err + } // Get current context cmd := exec.Command("kubectl", "config", "current-context") output, err := cmd.Output() if err != nil { - return KubeConfig{} + return KubeConfig{}, errors.New("failed to get current context") } currentContext := strings.TrimSpace(string(output)) @@ -193,7 +209,7 @@ func fetchKubeConfig() KubeConfig { cmd = exec.Command("kubectl", "config", "view", "--minify", "-o", "json") output, err = cmd.Output() if err != nil { - return KubeConfig{CurrentContext: currentContext} + return KubeConfig{}, fmt.Errorf("failed to obtain minified kubeconfig: %w", err) } var rawConfig struct { @@ -207,7 +223,7 @@ func fetchKubeConfig() KubeConfig { } if err := json.Unmarshal(output, &rawConfig); err != nil { - return KubeConfig{CurrentContext: currentContext} + return KubeConfig{}, fmt.Errorf("failed to unmarshal kubeconfig: %w", err) } clusters := make([]KubeCluster, len(rawConfig.Clusters)) @@ -221,19 +237,57 @@ func fetchKubeConfig() KubeConfig { return KubeConfig{ CurrentContext: currentContext, Clusters: clusters, + }, nil +} + +func kubeconfigChecks(log *logger.Logger) error { + kubeConfigPath, err := getKubeConfigPath() + if err != nil { + return fmt.Errorf("getting kubeconfig path: %w", err) + } + log.Infof("Using kubeconfig %s", kubeConfigPath) + if _, err := os.Stat(kubeConfigPath); err != nil { + log.Warningf("Kubeconfig %s cannot be found.", kubeConfigPath) + if RunningInContainer { + log.Warningf("Make sure that your kubeconfig is mounted into the container, as in: -v $KUBECONFIG:/kubeconfig:U") + } + return fmt.Errorf("failed to stat kubeconfig %s: %w", kubeConfigPath, err) + } + + file, err := os.Open(kubeConfigPath) + if err != nil { + log.Warningf("Kubeconfig %s cannot be opened for reading.", kubeConfigPath) + if RunningInContainer { + log.Warningf("Make sure that your kubeconfig is mounted with the 'U' option, as in: -v $KUBECONFIG:/kubeconfig:U") + } + return fmt.Errorf("failed to open kubeconfig %s: %w", kubeConfigPath, err) + } + _ = file.Close() + return nil +} + +func getKubeConfigPath() (string, error) { + kubeConfigPath := os.Getenv("KUBECONFIG") + if kubeConfigPath == "" { + home := os.Getenv("HOME") + if home == "" { + return "", errors.New("HOME environment variable is not set") + } + kubeConfigPath = filepath.Join(home, ".kube", "config") } + return kubeConfigPath, nil } // fetchAPIResources retrieves the list of API resources from the cluster -func fetchAPIResources() []string { +func fetchAPIResources() ([]string, error) { cmd := exec.Command("kubectl", "api-resources", "-o", "name") output, err := cmd.Output() if err != nil { - return nil + return nil, fmt.Errorf("failed to retrieve API resources: %w", err) } lines := strings.Split(strings.TrimSpace(string(output)), "\n") - return lines + return lines, nil } func IsInStackroxRepository() bool { diff --git a/internal/env/env_test.go b/internal/env/env_test.go index 1dddb904..62ff59d8 100644 --- a/internal/env/env_test.go +++ b/internal/env/env_test.go @@ -189,9 +189,11 @@ func TestDetectClusterType_GKE_DifferentProject(t *testing.T) { } func TestDetectClusterType_Integration(t *testing.T) { + Initialize(nil) + // This test uses the current kubectl context // The result will depend on the active cluster - clusterType := DetectClusterType() + clusterType := GetCurrentClusterType() t.Logf("Detected cluster type: %s", clusterType) diff --git a/internal/env/example_test.go b/internal/env/example_test.go deleted file mode 100644 index c789afca..00000000 --- a/internal/env/example_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package env_test - -import ( - "fmt" - - "github.com/stackrox/roxie/internal/env" -) - -// ExampleDetectClusterType demonstrates how to use the cluster type detection -func ExampleDetectClusterType() { - // Detect the cluster type for the current kubectl context - clusterType := env.DetectClusterType() - fmt.Printf("Detected cluster type: %s\n", clusterType) -} From 9c82b5e2f20b55408bce6406cf7287af9a7a3bce Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 12 Feb 2026 17:32:03 +0100 Subject: [PATCH 2/6] Skip credentials setup in deployer when deploying to Infra OCP --- cmd/main.go | 1 - internal/deployer/deployer.go | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index f4bf6122..07982b71 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -27,7 +27,6 @@ var ( ) func main() { - if err := rootCmd.Execute(); err != nil { red := color.New(color.FgRed, color.Bold) red.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 96eaef3e..216376eb 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -420,8 +420,11 @@ func (d *Deployer) Deploy(ctx context.Context, component, resources, exposure st d.exposure = exposure // Prepare and verify credentials early to fail fast - if err := d.prepareCredentials(); err != nil { - return fmt.Errorf("failed to prepare credentials: %w", err) + + if env.GetCurrentClusterType() != env.InfraOpenShift4 { + if err := d.prepareCredentials(); err != nil { + return fmt.Errorf("failed to prepare credentials: %w", err) + } } d.logger.Infof("Initiating deployment of %s", formatComponentName(component)) From 26734edba67c3d9ac2853745f0de7cfa01ad15dd Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Fri, 13 Feb 2026 09:43:06 +0100 Subject: [PATCH 3/6] Improve the README examples --- README.md | 65 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 96d0cb84..ab71e115 100644 --- a/README.md +++ b/README.md @@ -26,39 +26,39 @@ Support for Helm charts might be dropped in the future. ## Quick start -### Option 1: Deploying using Docker image (Recommended for non-developers) +### Option 1: Deploying using image (Recommended for non-developers) **Requirements:** -* Working Docker setup +* Podman (or Docker) is set up * kubeconfig configuration file -* quay.io registry credentials in the environment variables REGISTRY_USERNAME and REGISTRY_PASSWORD. -Note that **Podman is currently not supported** for running -containerized roxie due to incomplete mapping of user IDs on macOS. This prevents the passing-in of the gcloud -configuration directory to be functional within the container, which is required for interacting with GKE clusters. +And, depending on the cluster: +* credentials for the `quay.io` registry in the environment variables `REGISTRY_USERNAME` and `REGISTRY_PASSWORD`. -Example for deploying Central and SecuredCluster to the current Kubernetes cluster context: -```bash -docker run --rm -it --privileged \ - -v ~/.config/gcloud:/.config/gcloud \ - -v $KUBECONFIG:/kubeconfig \ - -e REGISTRY_USERNAME=$REGISTRY_USERNAME \ - -e REGISTRY_PASSWORD=$REGISTRY_PASSWORD \ - ghcr.io/stackrox/roxie:latest deploy -``` - -A new roxie image for the current platform can be built using: +Infra OpenShift4 clusters come already equipped with image pull secrets for `quay.io`, so in this case +passing of `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` to the container is not required: +Example for deploying Central and SecuredCluster to an Infra OpenShift 4 cluster: ```bash -make docker-build +podman run --rm -it --privileged \ + -v $KUBECONFIG:/kubeconfig:U \ + -e MAIN_IMAGE_TAG=4.9.2 \ + quay.io/rhacs-eng/roxie:latest deploy --resources=auto ``` +Specify the `MAIN_IMAGE_TAG` as desired. -This creates two tags: -- `localhost/roxie:latest` -- `localhost/roxie:` - -Docker images can be built for the platforms `linux/amd64` and `linux/arm64`. See the `Makefile` for more -docker related targets. +Deploying to a GKE cluster requires passing of some more arguments: +``` +podman run --rm -it --privileged \ + -v ~/.config/gcloud:/.config/gcloud:U \ + -v $KUBECONFIG:/kubeconfig:U \ + -e MAIN_IMAGE_TAG=4.9.2 \ + -e REGISTRY_USERNAME=$REGISTRY_USERNAME \ + -e REGISTRY_PASSWORD=$REGISTRY_PASSWORD \ + quay.io/rhacs-eng/roxie:latest deploy --resources=auto +``` +Note that in this case we also need to pass the gcloud configuration for the authentication towards +the cluster to succeed. ### Option 2: Deploying using local build @@ -80,9 +80,10 @@ Get help: Deploy using: ```bash -./roxie deploy [ ] +MAIN_IMAGE_TAG=4.9.2 ./roxie deploy [ ] ``` where `component` can be `central` or `sensor`. If not specified, both components will be deployed. +Specify the `MAIN_IMAGE_TAG` as desired. Similarly, the deployment(s) can be torn down using: ```bash @@ -104,6 +105,20 @@ make test # Unit tests make test-e2e # E2E tests (requires a real cluster context) ``` +A new roxie image for the current platform can be built using: + +```bash +make docker-build +``` + +This creates two tags: +- `localhost/roxie:latest` +- `localhost/roxie:` + +Docker images can be built for the platforms `linux/amd64` and `linux/arm64`. See the `Makefile` for more +docker related targets. + + ## Testing (E2E) The E2E suite expects a valid `kubectl` context. \ No newline at end of file From f52de1337dcd5cde635c23e7889067f8ce4a4dcd Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 9 Mar 2026 09:33:23 +0100 Subject: [PATCH 4/6] Panic if env info not initialized --- internal/env/env.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/env/env.go b/internal/env/env.go index 0747aba2..f2ad1a59 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -90,14 +90,22 @@ func ensureInitialized(log *logger.Logger) error { // GetCurrentClusterType returns the current cluster type, initializing if needed func GetCurrentClusterType() ClusterType { + panicIfNotInitialized() return currentClusterType } // GetCurrentContext returns the current kubectl context, initializing if needed func GetCurrentContext() string { + panicIfNotInitialized() return currentContext } +func panicIfNotInitialized() { + if !initialized { + panic("environment information not initialized") + } +} + // String returns the string representation of a ClusterType func (ct ClusterType) String() string { switch ct { From 722423f78bfe6855d0160f9fbee700e0dda59fcc Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 9 Mar 2026 10:00:23 +0100 Subject: [PATCH 5/6] Mark TestDetectClusterType_Integration as integration tests, preventing it from being run as part of the unit tests (which would fail with the new panicing behaviour) --- Makefile | 15 ++++++++++++- internal/env/env_integration_test.go | 33 ++++++++++++++++++++++++++++ internal/env/env_test.go | 23 ------------------- 3 files changed, 47 insertions(+), 24 deletions(-) create mode 100644 internal/env/env_integration_test.go diff --git a/Makefile b/Makefile index 6f7330d2..916e8c6b 100644 --- a/Makefile +++ b/Makefile @@ -114,8 +114,21 @@ test-e2e: build ## Run end-to-end tests (requires kubectl context and cluster ac fi $(GOTEST) -v -tags=e2e -timeout=120m -parallel=1 ./tests/e2e/... +.PHONY: test-integration +test-integration: build ## Run integration tests (requires kubectl context and cluster access) + @echo "๐Ÿงช Running integration tests..." + @if [ -z "$(shell kubectl config current-context 2>/dev/null)" ]; then \ + echo "โŒ No kubectl context found. Please configure kubectl first."; \ + exit 1; \ + fi + @if ! command -v podman >/dev/null 2>&1; then \ + echo "โŒ podman not found. Please install podman for integration tests."; \ + exit 1; \ + fi + $(GOTEST) -v -tags=integration -run=_Integration$$ -timeout=120m -parallel=1 ./... + .PHONY: test-all -test-all: test test-e2e ## Run all tests (unit + e2e) +test-all: test test-integration test-e2e ## Run all tests (unit + integration + e2e) # Benchmarks .PHONY: bench diff --git a/internal/env/env_integration_test.go b/internal/env/env_integration_test.go new file mode 100644 index 00000000..f583eb82 --- /dev/null +++ b/internal/env/env_integration_test.go @@ -0,0 +1,33 @@ +//go:build integration + +package env + +import ( + "testing" +) + +func TestDetectClusterType_Integration(t *testing.T) { + err := Initialize(nil) + if err != nil { + t.Fatalf("Initialize() failed: %v", err) + } + + // This test uses the current kubectl context + // The result will depend on the active cluster + clusterType := GetCurrentClusterType() + + t.Logf("Detected cluster type: %s", clusterType) + + // The cluster type should never be invalid (even if Unknown) + validTypes := []ClusterType{ClusterTypeUnknown, InfraGKE, InfraOpenShift4, LocalKind} + found := false + for _, valid := range validTypes { + if clusterType == valid { + found = true + break + } + } + if !found { + t.Errorf("DetectClusterType() returned invalid type: %v", clusterType) + } +} diff --git a/internal/env/env_test.go b/internal/env/env_test.go index 62ff59d8..149825a5 100644 --- a/internal/env/env_test.go +++ b/internal/env/env_test.go @@ -188,29 +188,6 @@ func TestDetectClusterType_GKE_DifferentProject(t *testing.T) { } } -func TestDetectClusterType_Integration(t *testing.T) { - Initialize(nil) - - // This test uses the current kubectl context - // The result will depend on the active cluster - clusterType := GetCurrentClusterType() - - t.Logf("Detected cluster type: %s", clusterType) - - // The cluster type should never be invalid (even if Unknown) - validTypes := []ClusterType{ClusterTypeUnknown, InfraGKE, InfraOpenShift4, LocalKind} - found := false - for _, valid := range validTypes { - if clusterType == valid { - found = true - break - } - } - if !found { - t.Errorf("DetectClusterType() returned invalid type: %v", clusterType) - } -} - func TestIsOpenShift4(t *testing.T) { tests := []struct { name string From d9c59f39bec946a7a0e76b1899e76d06c5105ef7 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 9 Mar 2026 13:45:48 +0100 Subject: [PATCH 6/6] Review feedback: rewrite kubeconfig checks --- internal/env/env.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/internal/env/env.go b/internal/env/env.go index f2ad1a59..efdff660 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -254,21 +254,20 @@ func kubeconfigChecks(log *logger.Logger) error { return fmt.Errorf("getting kubeconfig path: %w", err) } log.Infof("Using kubeconfig %s", kubeConfigPath) - if _, err := os.Stat(kubeConfigPath); err != nil { - log.Warningf("Kubeconfig %s cannot be found.", kubeConfigPath) - if RunningInContainer { - log.Warningf("Make sure that your kubeconfig is mounted into the container, as in: -v $KUBECONFIG:/kubeconfig:U") - } - return fmt.Errorf("failed to stat kubeconfig %s: %w", kubeConfigPath, err) - } file, err := os.Open(kubeConfigPath) if err != nil { log.Warningf("Kubeconfig %s cannot be opened for reading.", kubeConfigPath) - if RunningInContainer { - log.Warningf("Make sure that your kubeconfig is mounted with the 'U' option, as in: -v $KUBECONFIG:/kubeconfig:U") + if errors.Is(err, os.ErrNotExist) { + if RunningInContainer { + log.Warningf("Make sure that your kubeconfig is mounted into the container, as in: -v $KUBECONFIG:/kubeconfig:U") + } + } else { + if RunningInContainer { + log.Warningf("Make sure that your kubeconfig is mounted with the 'U' option, as in: -v $KUBECONFIG:/kubeconfig:U") + } } - return fmt.Errorf("failed to open kubeconfig %s: %w", kubeConfigPath, err) + return fmt.Errorf("failed to open kubeconfig %q for reading: %w", kubeConfigPath, err) } _ = file.Close() return nil