Skip to content

Commit 0d12d44

Browse files
committed
Support multi-cluster deployments (Central + SecuredCluster on separate clusters)
1 parent f7cb07b commit 0d12d44

5 files changed

Lines changed: 192 additions & 1 deletion

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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ Examples:
240240
return pflag.NormalizedName(name)
241241
})
242242

243+
cmd.Flags().StringVar(&centralEndpointFlag, "central-endpoint", "", "Central endpoint for multi-cluster SecuredCluster deployments (e.g., central.example.com:443)")
244+
cmd.Flags().StringVar(&centralPasswordFlag, "central-password", "", "Central admin password (takes precedence over ROX_ADMIN_PASSWORD)")
245+
cmd.Flags().StringVar(&caCertFileFlag, "ca-cert-file", "", "Path to Central CA certificate file (takes precedence over ROX_CA_CERT_FILE)")
246+
247+
243248
return cmd
244249
}
245250

@@ -299,6 +304,20 @@ func runDeploy(cmd *cobra.Command, args []string) error {
299304
}
300305
}
301306

307+
hasMultiClusterFlags := centralEndpointFlag != "" || centralPasswordFlag != "" || caCertFileFlag != ""
308+
if hasMultiClusterFlags && components != component.SecuredCluster {
309+
return errors.New("--central-endpoint, --central-password, and --ca-cert-file flags can only be used with 'secured-cluster' component")
310+
}
311+
312+
if centralEndpointFlag != "" {
313+
if centralPasswordFlag == "" && os.Getenv("ROX_ADMIN_PASSWORD") == "" {
314+
return errors.New("--central-endpoint requires a Central admin password (set --central-password or ROX_ADMIN_PASSWORD)")
315+
}
316+
if caCertFileFlag == "" && os.Getenv("ROX_CA_CERT_FILE") == "" {
317+
return errors.New("--central-endpoint requires a Central CA certificate (set --ca-cert-file or ROX_CA_CERT_FILE)")
318+
}
319+
}
320+
302321
d, err := deployer.New(log)
303322
if err != nil {
304323
return fmt.Errorf("failed to create deployer: %w", err)
@@ -319,6 +338,18 @@ func runDeploy(cmd *cobra.Command, args []string) error {
319338
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
320339
defer cancel()
321340

341+
if centralEndpointFlag != "" {
342+
d.SetCentralEndpoint(centralEndpointFlag)
343+
}
344+
if centralPasswordFlag != "" {
345+
d.SetCentralPassword(centralPasswordFlag)
346+
}
347+
if caCertFileFlag != "" {
348+
if err := d.SetCACertFile(caCertFileFlag); err != nil {
349+
return err
350+
}
351+
}
352+
322353
if components.IncludesCentral() {
323354
d.PrintCentralDeploymentSummary()
324355
}

cmd/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ var (
1515
envrc string
1616
dryRun bool
1717

18+
// Multi-cluster flags
19+
centralEndpointFlag string
20+
centralPasswordFlag string
21+
caCertFileFlag string
22+
1823
// We need this set up before command line flags are parsed.
1924
deploySettings = deployer.NewConfig()
2025
)
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/deployer.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ type Deployer struct {
5151
config Config
5252

5353
// State
54-
centralEndpoint string
54+
centralEndpoint string
55+
userProvidedCentralEndpoint string
5556
centralPassword string
5657
roxCACertFile string
5758
tempDir string
@@ -653,6 +654,32 @@ func (d *Deployer) removePauseReconcileAnnotation(ctx context.Context, resourceT
653654
return nil
654655
}
655656

657+
func (d *Deployer) SetCentralEndpoint(endpoint string) {
658+
endpoint = strings.TrimPrefix(endpoint, "https://")
659+
endpoint = strings.TrimPrefix(endpoint, "http://")
660+
d.centralEndpoint = endpoint
661+
d.userProvidedCentralEndpoint = endpoint
662+
}
663+
664+
func (d *Deployer) SetCentralPassword(password string) {
665+
d.centralPassword = password
666+
}
667+
668+
func (d *Deployer) SetCACertFile(path string) error {
669+
if _, err := os.Stat(path); err != nil {
670+
return fmt.Errorf("CA cert file not found: %w", err)
671+
}
672+
d.roxCACertFile = path
673+
return nil
674+
}
675+
676+
func (d *Deployer) getCentralEndpointForSensor() string {
677+
if d.userProvidedCentralEndpoint != "" {
678+
return d.userProvidedCentralEndpoint
679+
}
680+
return internalCentralEndpoint(d.config.Central.Namespace)
681+
}
682+
656683
// WaitForCentral waits for Central to be ready and responding on its endpoint
657684
// Returns true if Central is ready, false if timeout occurs
658685
func (d *Deployer) WaitForCentral(timeout time.Duration) bool {
@@ -995,6 +1022,10 @@ func (d *Deployer) PrintSecuredClusterDeploymentSummary() {
9951022
log.Info(cyan.Sprint("│") + createRow("OLM", "Yes"))
9961023
}
9971024

1025+
if d.userProvidedCentralEndpoint != "" {
1026+
log.Info(cyan.Sprint("│") + createRow("Central Endpoint", d.userProvidedCentralEndpoint))
1027+
}
1028+
9981029
log.Info(cyan.Sprint("└" + strings.Repeat("─", boxWidth) + "┘"))
9991030
log.Info("")
10001031
}

0 commit comments

Comments
 (0)