Skip to content

Commit a0ffb80

Browse files
committed
Support multi-cluster deployments (Central + SecuredCluster on separate clusters)
1 parent 000f8cb commit a0ffb80

7 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
@@ -89,6 +89,23 @@ Similarly, the deployment(s) can be torn down using:
8989
./bin/roxie teardown [ <component> ]
9090
```
9191

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

94111
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().BoolVar(&singleNamespace, "single-namespace", false, "Deploy all components in a single namespace ('stackrox' by default)")
4848
cmd.Flags().StringVarP(&tag, "tag", "t", "", "Main image tag to use for deployment (takes precedence over MAIN_IMAGE_TAG environment variable)")
4949
cmd.Flags().StringSliceVar(&featureFlags, "features", []string{}, "Feature flag settings (e.g., +ROX_FOO,-ROX_BAR,ROX_BAZ=true)")
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
}
@@ -136,6 +139,20 @@ func runDeploy(cmd *cobra.Command, args []string) error {
136139
return errors.New("cannot use --deploy-operator=false with --olm (OLM requires operator deployment)")
137140
}
138141

142+
hasMultiClusterFlags := centralEndpointFlag != "" || centralPasswordFlag != "" || caCertFileFlag != ""
143+
if hasMultiClusterFlags && components != component.SecuredCluster {
144+
return errors.New("--central-endpoint, --central-password, and --ca-cert-file flags can only be used with 'secured-cluster' component")
145+
}
146+
147+
if centralEndpointFlag != "" {
148+
if centralPasswordFlag == "" && os.Getenv("ROX_ADMIN_PASSWORD") == "" {
149+
return errors.New("--central-endpoint requires a Central admin password (set --central-password or ROX_ADMIN_PASSWORD)")
150+
}
151+
if caCertFileFlag == "" && os.Getenv("ROX_CA_CERT_FILE") == "" {
152+
return errors.New("--central-endpoint requires a Central CA certificate (set --ca-cert-file or ROX_CA_CERT_FILE)")
153+
}
154+
}
155+
139156
d, err := deployer.New(log)
140157
if err != nil {
141158
return fmt.Errorf("failed to create deployer: %w", err)
@@ -169,6 +186,18 @@ func runDeploy(cmd *cobra.Command, args []string) error {
169186
}
170187
}
171188

189+
if centralEndpointFlag != "" {
190+
d.SetCentralEndpoint(centralEndpointFlag)
191+
}
192+
if centralPasswordFlag != "" {
193+
d.SetCentralPassword(centralPasswordFlag)
194+
}
195+
if caCertFileFlag != "" {
196+
if err := d.SetCACertFile(caCertFileFlag); err != nil {
197+
return err
198+
}
199+
}
200+
172201
if components.IncludesCentral() {
173202
d.PrintCentralDeploymentSummary()
174203
}

cmd/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ var (
2626
singleNamespace bool
2727
tag string
2828
featureFlags []string
29+
centralEndpointFlag string
30+
centralPasswordFlag string
31+
caCertFileFlag string
2932
)
3033

3134
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_helm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ func (d *Deployer) createCentralValues(resourcesName, exposure string) (map[stri
268268
func (d *Deployer) createSecuredClusterValues(clusterName, resources string) (map[string]interface{}, error) {
269269
base := map[string]interface{}{
270270
"clusterName": clusterName,
271-
"centralEndpoint": "https://central." + centralNamespace + ".svc:443",
271+
"centralEndpoint": "https://" + d.getCentralEndpointForSensor(),
272272
"allowNonstandardNamespace": true,
273273
}
274274

internal/deployer/deploy_via_operator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ func (d *Deployer) createSecuredClusterCR(clusterName, resources string) (map[st
669669
},
670670
"spec": map[string]interface{}{
671671
"clusterName": clusterName,
672-
"centralEndpoint": internalCentralEndpoint(d.centralNamespace),
672+
"centralEndpoint": d.getCentralEndpointForSensor(),
673673
"imagePullSecrets": []map[string]string{
674674
{"name": "stackrox"},
675675
},

internal/deployer/deployer.go

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

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

134135
type ResourceKindWithName struct {
@@ -986,6 +987,32 @@ func (d *Deployer) SetDeployOperator(deployOperator bool) {
986987
d.shouldDeployOperator = deployOperator
987988
}
988989

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

1353+
if d.userProvidedCentralEndpoint != "" {
1354+
log.Info(cyan.Sprint("│") + createRow("Central Endpoint", d.userProvidedCentralEndpoint))
1355+
}
1356+
13261357
log.Info(cyan.Sprint("└" + strings.Repeat("─", boxWidth) + "┘"))
13271358
log.Info("")
13281359
}

0 commit comments

Comments
 (0)