Skip to content

Commit 513966a

Browse files
committed
Support multi-cluster deployments (Central + SecuredCluster on separate clusters)
1 parent 2a60f6b commit 513966a

6 files changed

Lines changed: 220 additions & 33 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)
@@ -154,6 +171,18 @@ func runDeploy(cmd *cobra.Command, args []string) error {
154171
}
155172
}
156173

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

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
@@ -692,7 +692,7 @@ func (d *Deployer) createSecuredClusterCR(resources string) (map[string]interfac
692692
},
693693
"spec": map[string]interface{}{
694694
"clusterName": generateClusterName(), // Just a default, can be overwritten.
695-
"centralEndpoint": internalCentralEndpoint(d.centralNamespace),
695+
"centralEndpoint": d.getCentralEndpointForSensor(),
696696
"imagePullSecrets": []map[string]string{
697697
{"name": "stackrox"},
698698
},

internal/deployer/deployer.go

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -102,38 +102,39 @@ 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{}
136-
tempDir string
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{}
137+
tempDir string
137138
}
138139

139140
type ResourceToDelete struct {
@@ -1009,6 +1010,32 @@ func (d *Deployer) SetDeployOperator(deployOperator bool) {
10091010
d.shouldDeployOperator = deployOperator
10101011
}
10111012

1013+
func (d *Deployer) SetCentralEndpoint(endpoint string) {
1014+
endpoint = strings.TrimPrefix(endpoint, "https://")
1015+
endpoint = strings.TrimPrefix(endpoint, "http://")
1016+
d.centralEndpoint = endpoint
1017+
d.userProvidedCentralEndpoint = endpoint
1018+
}
1019+
1020+
func (d *Deployer) SetCentralPassword(password string) {
1021+
d.centralPassword = password
1022+
}
1023+
1024+
func (d *Deployer) SetCACertFile(path string) error {
1025+
if _, err := os.Stat(path); err != nil {
1026+
return fmt.Errorf("CA cert file not found: %w", err)
1027+
}
1028+
d.roxCACertFile = path
1029+
return nil
1030+
}
1031+
1032+
func (d *Deployer) getCentralEndpointForSensor() string {
1033+
if d.userProvidedCentralEndpoint != "" {
1034+
return d.userProvidedCentralEndpoint
1035+
}
1036+
return internalCentralEndpoint(d.centralNamespace)
1037+
}
1038+
10121039
func (d *Deployer) GetDeploymentInfo() (endpoint, password, caCertFile, kubeContext, exposure string) {
10131040
return d.centralEndpoint, d.centralPassword, d.roxCACertFile, d.kubeContext, d.exposure
10141041
}
@@ -1340,6 +1367,10 @@ func (d *Deployer) PrintSecuredClusterDeploymentSummary() {
13401367
log.Info(cyan.Sprint("│") + createRow("OLM", "Yes"))
13411368
}
13421369

1370+
if d.userProvidedCentralEndpoint != "" {
1371+
log.Info(cyan.Sprint("│") + createRow("Central Endpoint", d.userProvidedCentralEndpoint))
1372+
}
1373+
13431374
log.Info(cyan.Sprint("└" + strings.Repeat("─", boxWidth) + "┘"))
13441375
log.Info("")
13451376
}

0 commit comments

Comments
 (0)