diff --git a/README.md b/README.md index fd2b6a3..51cf457 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,29 @@ Similarly, the deployment(s) can be torn down using: ./bin/roxie teardown [ ] ``` +### Multi-cluster deployments + +roxie supports hub + spoke architectures where Central and SecuredCluster run on separate clusters. + +1. Deploy Central on the hub cluster: +```bash +MAIN_IMAGE_TAG=4.9.2 ./roxie deploy central +``` + +2. Switch kubectl context to the spoke cluster and deploy SecuredCluster: +```bash +ROX_ADMIN_PASSWORD= \ +ROX_CA_CERT_FILE= \ +./roxie deploy secured-cluster \ + --set securedCluster.centralEndpoint=:443 +``` + +> **Note:** The Central endpoint is printed during deployment. If you are in the roxie subshell, +> `ROX_ADMIN_PASSWORD` and `ROX_CA_CERT_FILE` are already set, so you can run: +> ```bash +> ./roxie deploy secured-cluster --set securedCluster.centralEndpoint=$API_ENDPOINT +> ``` + ## Development Enter the dev shell: diff --git a/cmd/deploy.go b/cmd/deploy.go index 87d7caa..b03bdb4 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -299,6 +299,18 @@ func runDeploy(cmd *cobra.Command, args []string) error { } } + if deploySettings.SecuredCluster.CentralEndpoint != "" { + if !components.IncludesSensor() { + return errors.New("securedCluster.centralEndpoint can only be used when deploying secured-cluster") + } + if os.Getenv("ROX_ADMIN_PASSWORD") == "" { + return errors.New("securedCluster.centralEndpoint requires ROX_ADMIN_PASSWORD to be set") + } + if os.Getenv("ROX_CA_CERT_FILE") == "" { + return errors.New("securedCluster.centralEndpoint requires ROX_CA_CERT_FILE to be set") + } + } + d, err := deployer.New(log) if err != nil { return fmt.Errorf("failed to create deployer: %w", err) diff --git a/internal/deployer/central_endpoint_test.go b/internal/deployer/central_endpoint_test.go new file mode 100644 index 0000000..5ea93cb --- /dev/null +++ b/internal/deployer/central_endpoint_test.go @@ -0,0 +1,67 @@ +package deployer + +import ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestConfigureSpec_CentralEndpoint(t *testing.T) { + tests := []struct { + name string + centralEndpoint string + centralNamespace string + expected string + }{ + { + name: "falls back to internal endpoint", + centralEndpoint: "", + centralNamespace: "acs-central", + expected: "central.acs-central.svc:443", + }, + { + name: "falls back to internal endpoint with custom namespace", + centralEndpoint: "", + centralNamespace: "stackrox", + expected: "central.stackrox.svc:443", + }, + { + name: "uses provided central endpoint", + centralEndpoint: "central.example.com:443", + centralNamespace: "acs-central", + expected: "central.example.com:443", + }, + { + name: "provided endpoint takes precedence over namespace", + centralEndpoint: "10.0.0.1:443", + centralNamespace: "stackrox", + expected: "10.0.0.1:443", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc := &SecuredClusterConfig{ + CentralEndpoint: tt.centralEndpoint, + Spec: make(map[string]interface{}), + } + roxie := &RoxieConfig{FeatureFlags: make(map[string]bool)} + central := &CentralConfig{Namespace: tt.centralNamespace} + + if err := sc.ConfigureSpec(roxie, central); err != nil { + t.Fatalf("ConfigureSpec failed: %v", err) + } + + got, found, err := unstructured.NestedString(sc.Spec, "centralEndpoint") + if err != nil { + t.Fatalf("failed to get centralEndpoint from spec: %v", err) + } + if !found { + t.Fatal("centralEndpoint not found in spec") + } + if got != tt.expected { + t.Errorf("got %q, want %q", got, tt.expected) + } + }) + } +} diff --git a/internal/deployer/config.go b/internal/deployer/config.go index 1a6b2ca..56bc5f6 100644 --- a/internal/deployer/config.go +++ b/internal/deployer/config.go @@ -199,6 +199,7 @@ func (c *CentralConfig) CustomResource() (map[string]interface{}, error) { // SecuredClusterConfig holds deployment settings for the SecuredCluster component. type SecuredClusterConfig struct { Namespace string `yaml:"namespace,omitempty"` + CentralEndpoint string `yaml:"centralEndpoint,omitempty"` ResourceProfile types.ResourceProfile `yaml:"resourceProfile,omitempty"` PauseReconciliation bool `yaml:"pauseReconciliation,omitempty"` DeployTimeout time.Duration `yaml:"deployTimeout,omitempty"` @@ -234,8 +235,13 @@ func (s *SecuredClusterConfig) ConfigureSpec(roxieConfig *RoxieConfig, centralCo return err } + centralEndpoint := internalCentralEndpoint(centralConfig.Namespace) + if s.CentralEndpoint != "" { + centralEndpoint = s.CentralEndpoint + } + if err := helpers.DeepMerge(s.Spec, map[string]interface{}{ - "centralEndpoint": internalCentralEndpoint(centralConfig.Namespace), + "centralEndpoint": centralEndpoint, }); err != nil { return err } diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 2c602e2..36b9832 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -995,6 +995,10 @@ func (d *Deployer) PrintSecuredClusterDeploymentSummary() { log.Info(cyan.Sprint("│") + createRow("OLM", "Yes")) } + if d.config.SecuredCluster.CentralEndpoint != "" { + log.Info(cyan.Sprint("│") + createRow("Central Endpoint", d.config.SecuredCluster.CentralEndpoint)) + } + log.Info(cyan.Sprint("└" + strings.Repeat("─", boxWidth) + "┘")) log.Info("") }