Skip to content

Commit a89a0d8

Browse files
committed
Support multi-cluster deployments (Central + SecuredCluster on separate clusters)
1 parent aa9ed67 commit a89a0d8

6 files changed

Lines changed: 219 additions & 32 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,23 @@ Similarly, the deployment(s) can be torn down using:
8484
./bin/roxie teardown [ <component> ]
8585
```
8686

87+
### Multi-cluster deployments
88+
89+
roxie supports hub + spoke architectures where Central and SecuredCluster run on separate clusters.
90+
91+
1. Deploy Central on the hub cluster:
92+
```bash
93+
MAIN_IMAGE_TAG=4.9.2 ./roxie deploy central
94+
```
95+
96+
2. Switch kubectl context to the spoke cluster and deploy SecuredCluster:
97+
```bash
98+
./roxie deploy secured-cluster \
99+
--central-endpoint=<central-loadbalancer-ip>:443 \
100+
--central-password=<admin-password> \
101+
--ca-cert-file=/tmp/roxie-ca-cert.pem
102+
```
103+
87104
## Development
88105

89106
Enter the dev shell:

cmd/deploy.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ Examples:
4747
cmd.Flags().StringSliceVar(&featureFlags, "features", []string{}, "Feature flag settings (e.g., +ROX_FOO,-ROX_BAR,ROX_BAZ=true)")
4848
cmd.Flags().StringVar(&centralWait, "central-wait", deployer.DefaultCentralWaitTimeout.String(), "Maximum wait time for Central to become ready (e.g., 5m, 10m)")
4949
cmd.Flags().StringVar(&securedClusterWait, "secured-cluster-wait", deployer.DefaultSecuredClusterWaitTimeout.String(), "Maximum wait time for SecuredCluster to become ready (e.g., 5m, 10m)")
50+
cmd.Flags().StringVar(&centralEndpointFlag, "central-endpoint", "", "Central endpoint for multi-cluster SecuredCluster deployments (e.g., central.example.com:443)")
51+
cmd.Flags().StringVar(&centralPasswordFlag, "central-password", "", "Central admin password (takes precedence over ROX_ADMIN_PASSWORD)")
52+
cmd.Flags().StringVar(&caCertFileFlag, "ca-cert-file", "", "Path to Central CA certificate file (takes precedence over ROX_CA_CERT_FILE)")
5053

5154
return cmd
5255
}
@@ -120,6 +123,20 @@ func runDeploy(cmd *cobra.Command, args []string) error {
120123
return errors.New("cannot use --deploy-operator=false with --olm (OLM requires operator deployment)")
121124
}
122125

126+
hasMultiClusterFlags := centralEndpointFlag != "" || centralPasswordFlag != "" || caCertFileFlag != ""
127+
if hasMultiClusterFlags && components != component.SecuredCluster {
128+
return errors.New("--central-endpoint, --central-password, and --ca-cert-file flags can only be used with 'secured-cluster' component")
129+
}
130+
131+
if centralEndpointFlag != "" {
132+
if centralPasswordFlag == "" && os.Getenv("ROX_ADMIN_PASSWORD") == "" {
133+
return errors.New("--central-endpoint requires a Central admin password (set --central-password or ROX_ADMIN_PASSWORD)")
134+
}
135+
if caCertFileFlag == "" && os.Getenv("ROX_CA_CERT_FILE") == "" {
136+
return errors.New("--central-endpoint requires a Central CA certificate (set --ca-cert-file or ROX_CA_CERT_FILE)")
137+
}
138+
}
139+
123140
d, err := deployer.New(log)
124141
if err != nil {
125142
return fmt.Errorf("failed to create deployer: %w", err)
@@ -153,6 +170,18 @@ func runDeploy(cmd *cobra.Command, args []string) error {
153170
}
154171
}
155172

173+
if centralEndpointFlag != "" {
174+
d.SetCentralEndpoint(centralEndpointFlag)
175+
}
176+
if centralPasswordFlag != "" {
177+
d.SetCentralPassword(centralPasswordFlag)
178+
}
179+
if caCertFileFlag != "" {
180+
if err := d.SetCACertFile(caCertFileFlag); err != nil {
181+
return err
182+
}
183+
}
184+
156185
if components.IncludesCentral() {
157186
d.PrintCentralDeploymentSummary()
158187
}

cmd/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ var (
2727
featureFlags []string
2828
centralWait string
2929
securedClusterWait string
30+
centralEndpointFlag string
31+
centralPasswordFlag string
32+
caCertFileFlag string
3033
)
3134

3235
func main() {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package deployer
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestSetCentralEndpoint(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
input string
11+
expectedCentralEndpoint string
12+
expectedUserProvidedEndpoint string
13+
}{
14+
{
15+
name: "plain host:port",
16+
input: "10.0.0.1:443",
17+
expectedCentralEndpoint: "10.0.0.1:443",
18+
expectedUserProvidedEndpoint: "10.0.0.1:443",
19+
},
20+
{
21+
name: "strips https prefix",
22+
input: "https://10.0.0.1:443",
23+
expectedCentralEndpoint: "10.0.0.1:443",
24+
expectedUserProvidedEndpoint: "10.0.0.1:443",
25+
},
26+
{
27+
name: "hostname with port",
28+
input: "central.example.com:443",
29+
expectedCentralEndpoint: "central.example.com:443",
30+
expectedUserProvidedEndpoint: "central.example.com:443",
31+
},
32+
{
33+
name: "strips https from hostname",
34+
input: "https://central.example.com:443",
35+
expectedCentralEndpoint: "central.example.com:443",
36+
expectedUserProvidedEndpoint: "central.example.com:443",
37+
},
38+
{
39+
name: "strips http prefix",
40+
input: "http://10.0.0.1:443",
41+
expectedCentralEndpoint: "10.0.0.1:443",
42+
expectedUserProvidedEndpoint: "10.0.0.1:443",
43+
},
44+
{
45+
name: "strips http from hostname",
46+
input: "http://central.example.com:443",
47+
expectedCentralEndpoint: "central.example.com:443",
48+
expectedUserProvidedEndpoint: "central.example.com:443",
49+
},
50+
}
51+
52+
for _, tt := range tests {
53+
t.Run(tt.name, func(t *testing.T) {
54+
d := &Deployer{}
55+
d.SetCentralEndpoint(tt.input)
56+
57+
if d.centralEndpoint != tt.expectedCentralEndpoint {
58+
t.Errorf("centralEndpoint: got %q, want %q", d.centralEndpoint, tt.expectedCentralEndpoint)
59+
}
60+
if d.userProvidedCentralEndpoint != tt.expectedUserProvidedEndpoint {
61+
t.Errorf("userProvidedCentralEndpoint: got %q, want %q", d.userProvidedCentralEndpoint, tt.expectedUserProvidedEndpoint)
62+
}
63+
})
64+
}
65+
}
66+
67+
func TestGetCentralEndpointForSensor(t *testing.T) {
68+
tests := []struct {
69+
name string
70+
userProvided string
71+
centralNamespace string
72+
expected string
73+
}{
74+
{
75+
name: "falls back to internal endpoint",
76+
userProvided: "",
77+
centralNamespace: "acs-central",
78+
expected: "central.acs-central.svc:443",
79+
},
80+
{
81+
name: "falls back to internal endpoint with custom namespace",
82+
userProvided: "",
83+
centralNamespace: "stackrox",
84+
expected: "central.stackrox.svc:443",
85+
},
86+
{
87+
name: "uses user-provided endpoint",
88+
userProvided: "10.0.0.1:443",
89+
centralNamespace: "acs-central",
90+
expected: "10.0.0.1:443",
91+
},
92+
}
93+
94+
for _, tt := range tests {
95+
t.Run(tt.name, func(t *testing.T) {
96+
d := &Deployer{
97+
centralNamespace: tt.centralNamespace,
98+
userProvidedCentralEndpoint: tt.userProvided,
99+
}
100+
101+
result := d.getCentralEndpointForSensor()
102+
if result != tt.expected {
103+
t.Errorf("got %q, want %q", result, tt.expected)
104+
}
105+
})
106+
}
107+
}

internal/deployer/deploy_via_operator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ func (d *Deployer) createSecuredClusterCR(resources string) (map[string]interfac
688688
},
689689
"spec": map[string]interface{}{
690690
"clusterName": generateClusterName(), // Just a default, can be overwritten.
691-
"centralEndpoint": internalCentralEndpoint(d.centralNamespace),
691+
"centralEndpoint": d.getCentralEndpointForSensor(),
692692
"imagePullSecrets": []map[string]string{
693693
{"name": "stackrox"},
694694
},

internal/deployer/deployer.go

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -102,37 +102,38 @@ var (
102102

103103
// Deployer is the base deployer for ACS
104104
type Deployer struct {
105-
logger *logger.Logger
106-
startTime time.Time
107-
dockerAuth *dockerauth.DockerAuth
108-
imageCache *imagecache.ImageCache
109-
portForward *portforward.Manager
110-
clusterDefaults *clusterdefaults.Manager
111-
roxctlVersion string
112-
centralNamespace string
113-
sensorNamespace string
114-
mainImageTag string
115-
operatorTag string
116-
centralEndpoint string
117-
centralPassword string
118-
roxCACertFile string
119-
kubeContext string
120-
portForwardEnabled bool
121-
pauseReconciliation bool
122-
exposure string
123-
centralOverrides map[string]interface{}
124-
securedClusterOverrides map[string]interface{}
125-
featureFlagOverrides map[string]interface{}
126-
envrcFile string
127-
useOLM bool
128-
useKonflux bool
129-
shouldDeployOperator bool
130-
verbose bool
131-
earlyReadiness bool
132-
centralWaitTimeout time.Duration
133-
securedClusterWaitTimeout time.Duration
134-
dockerCreds *dockerauth.Credentials
135-
clusterResourceKinds map[string]struct{}
105+
logger *logger.Logger
106+
startTime time.Time
107+
dockerAuth *dockerauth.DockerAuth
108+
imageCache *imagecache.ImageCache
109+
portForward *portforward.Manager
110+
clusterDefaults *clusterdefaults.Manager
111+
roxctlVersion string
112+
centralNamespace string
113+
sensorNamespace string
114+
mainImageTag string
115+
operatorTag string
116+
centralEndpoint string
117+
userProvidedCentralEndpoint string
118+
centralPassword string
119+
roxCACertFile string
120+
kubeContext string
121+
portForwardEnabled bool
122+
pauseReconciliation bool
123+
exposure string
124+
centralOverrides map[string]interface{}
125+
securedClusterOverrides map[string]interface{}
126+
featureFlagOverrides map[string]interface{}
127+
envrcFile string
128+
useOLM bool
129+
useKonflux bool
130+
shouldDeployOperator bool
131+
verbose bool
132+
earlyReadiness bool
133+
centralWaitTimeout time.Duration
134+
securedClusterWaitTimeout time.Duration
135+
dockerCreds *dockerauth.Credentials
136+
clusterResourceKinds map[string]struct{}
136137
}
137138

138139
type ResourceToDelete struct {
@@ -988,6 +989,32 @@ func (d *Deployer) SetDeployOperator(deployOperator bool) {
988989
d.shouldDeployOperator = deployOperator
989990
}
990991

992+
func (d *Deployer) SetCentralEndpoint(endpoint string) {
993+
endpoint = strings.TrimPrefix(endpoint, "https://")
994+
endpoint = strings.TrimPrefix(endpoint, "http://")
995+
d.centralEndpoint = endpoint
996+
d.userProvidedCentralEndpoint = endpoint
997+
}
998+
999+
func (d *Deployer) SetCentralPassword(password string) {
1000+
d.centralPassword = password
1001+
}
1002+
1003+
func (d *Deployer) SetCACertFile(path string) error {
1004+
if _, err := os.Stat(path); err != nil {
1005+
return fmt.Errorf("CA cert file not found: %w", err)
1006+
}
1007+
d.roxCACertFile = path
1008+
return nil
1009+
}
1010+
1011+
func (d *Deployer) getCentralEndpointForSensor() string {
1012+
if d.userProvidedCentralEndpoint != "" {
1013+
return d.userProvidedCentralEndpoint
1014+
}
1015+
return internalCentralEndpoint(d.centralNamespace)
1016+
}
1017+
9911018
func (d *Deployer) GetDeploymentInfo() (endpoint, password, caCertFile, kubeContext, exposure string) {
9921019
return d.centralEndpoint, d.centralPassword, d.roxCACertFile, d.kubeContext, d.exposure
9931020
}
@@ -1321,6 +1348,10 @@ func (d *Deployer) PrintSecuredClusterDeploymentSummary() {
13211348
log.Info(cyan.Sprint("│") + createRow("OLM", "Yes"))
13221349
}
13231350

1351+
if d.userProvidedCentralEndpoint != "" {
1352+
log.Info(cyan.Sprint("│") + createRow("Central Endpoint", d.userProvidedCentralEndpoint))
1353+
}
1354+
13241355
log.Info(cyan.Sprint("└" + strings.Repeat("─", boxWidth) + "┘"))
13251356
log.Info("")
13261357
}

0 commit comments

Comments
 (0)