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/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 diff --git a/cmd/deploy.go b/cmd/deploy.go index 4999b702..98f5c616 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -51,9 +51,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 { @@ -99,9 +98,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/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/deployer/deployer.go b/internal/deployer/deployer.go index 956765a5..532e66aa 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -421,8 +421,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)) diff --git a/internal/env/env.go b/internal/env/env.go index 75026755..efdff660 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,28 +68,44 @@ 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() + panicIfNotInitialized() return currentClusterType } // GetCurrentContext returns the current kubectl context, initializing if needed func GetCurrentContext() string { - ensureInitialized() + 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 { @@ -113,13 +132,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 +201,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 +217,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 +231,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 +245,56 @@ 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) + + file, err := os.Open(kubeConfigPath) + if err != nil { + log.Warningf("Kubeconfig %s cannot be opened for reading.", kubeConfigPath) + 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 %q for reading: %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_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 1dddb904..149825a5 100644 --- a/internal/env/env_test.go +++ b/internal/env/env_test.go @@ -188,27 +188,6 @@ func TestDetectClusterType_GKE_DifferentProject(t *testing.T) { } } -func TestDetectClusterType_Integration(t *testing.T) { - // This test uses the current kubectl context - // The result will depend on the active cluster - clusterType := DetectClusterType() - - 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 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) -}