-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdeploy.go
More file actions
465 lines (406 loc) · 15.7 KB
/
Copy pathdeploy.go
File metadata and controls
465 lines (406 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
package main
import (
"context"
"errors"
"fmt"
"os"
"reflect"
"strings"
"time"
"dario.cat/mergo"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stackrox/roxie/internal/clusterdefaults"
"github.com/stackrox/roxie/internal/component"
"github.com/stackrox/roxie/internal/deployer"
"github.com/stackrox/roxie/internal/env"
"github.com/stackrox/roxie/internal/helpers"
"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/manifest"
"github.com/stackrox/roxie/internal/roxieenv"
"github.com/stackrox/roxie/internal/types"
"github.com/stackrox/roxie/internal/stackroxversions"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/utils/ptr"
)
var (
sharedNamespace = "stackrox"
)
func newDeployCmd(settings *deployer.Config) *cobra.Command {
cmd := &cobra.Command{
Use: "deploy [component]",
Short: "Deploy ACS components",
Long: `Deploy ACS components (central, secured-cluster, operator).
Examples:
roxie deploy central
roxie deploy secured-cluster
roxie deploy both
roxie deploy operator`,
ValidArgs: []string{"central", "secured-cluster", "both", "all", "operator"},
Args: cobra.MaximumNArgs(1),
RunE: runDeploy,
}
cmd.Flags().StringVar(&shell, "shell", "", "Shell to spawn after Central deployment")
cmd.Flags().StringVar(&envrc, "envrc", "", "Write environment to file instead of spawning sub-shell")
registerFlag(cmd, settings, "olm", "Deploy operator via OLM (requires OLM installed)",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Operator.DeployViaOlm = val
return nil
}),
)
registerFlag(cmd, settings, "konflux", "Use Konflux images",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Roxie.KonfluxImages = val
return nil
}),
)
registerFlag(cmd, settings, "deploy-operator", "Whether to deploy and manage the operator",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Operator.SkipDeployment = !val
return nil
}),
)
registerFlag(cmd, settings, "port-forwarding", "Enable localhost port-forward for Central",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Central.PortForwarding = ptr.To(val)
return nil
}),
)
registerFlag(cmd, settings, "pause-reconciliation", "Pause reconciliation after deployment",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Central.PauseReconciliation = val
config.SecuredCluster.PauseReconciliation = val
return nil
}),
)
registerFlag(cmd, settings, "config", "Path to YAML config file",
withShortName("c"),
withApplyFn("filename", func(config *deployer.Config, filename string) error {
if filename == "-" {
filename = "/dev/stdin"
}
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read config file %q: %w", filename, err)
}
var configFromFile deployer.Config
if err := yaml.Unmarshal(data, &configFromFile); err != nil {
return fmt.Errorf("failed to unmarshal config file %q: %w", filename, err)
}
if err := mergo.Merge(config, configFromFile, mergo.WithOverride, mergo.WithoutDereference); err != nil {
return fmt.Errorf("merging config file %q into deployer Config: %w", filename, err)
}
return nil
}),
)
registerFlag(cmd, settings, "exposure", "Central exposure backend (loadbalancer, none)",
withApplyFn("exposure", func(config *deployer.Config, val string) error {
var exposure types.Exposure
if err := yaml.Unmarshal([]byte(val), &exposure); err != nil {
return err
}
config.Central.Exposure = ptr.To(exposure)
return nil
}),
)
registerFlag(cmd, settings, "resources", fmt.Sprintf("Resource sizing preset (%s)", types.ResourceProfilesJoined()),
withApplyFn("resource-profile", func(config *deployer.Config, val string) error {
var valParsed types.ResourceProfile
if err := yaml.Unmarshal([]byte(val), &valParsed); err != nil {
return err
}
config.Central.ResourceProfile = valParsed
config.SecuredCluster.ResourceProfile = valParsed
return nil
}),
)
registerFlag(cmd, settings, "set", "Set expressions, e.g. securedCluster.spec.clusterName=sensor",
withApplyFn("set-expression", func(config *deployer.Config, expr string) error {
key, yamlValue, found := strings.Cut(expr, "=")
if !found {
return fmt.Errorf("invalid set expression '%s': expected format 'key.path=value'", expr)
}
var val interface{}
if err := yaml.Unmarshal([]byte(yamlValue), &val); err != nil {
return fmt.Errorf("failed to unmarshal value '%s' for key '%s': %w", yamlValue, key, err)
}
// SetNestedField requires JSON-compatible types: float64 for numbers, not int.
switch v := val.(type) {
case int:
val = float64(v)
case int64:
val = float64(v)
}
pathElements := strings.Split(key, ".")
if len(pathElements) > 0 && pathElements[0] == "spec" {
return errors.New("set expression begins with 'spec.' -- it must be prefixed with 'central.' or 'securedCluster.'")
}
unstructuredPatch := make(map[string]interface{})
if err := unstructured.SetNestedField(unstructuredPatch, val, pathElements...); err != nil {
return err
}
var patch deployer.Config
if err := helpers.MapToStruct(unstructuredPatch, &patch); err != nil {
return err
}
if reflect.DeepEqual(patch, deployer.Config{}) {
return fmt.Errorf("set expression %q had no effect -- typo?", expr)
}
if err := mergo.Merge(config, &patch, mergo.WithOverride, mergo.WithoutDereference); err != nil {
return fmt.Errorf("merging set-expression %q into deployer Config: %w", expr, err)
}
return nil
}),
)
registerFlag(cmd, settings, "single-namespace", "Deploy all components in a single namespace ('stackrox')",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
// We do not support --single-namespace=false as of now.
if val {
config.Central.Namespace = sharedNamespace
config.SecuredCluster.Namespace = sharedNamespace
}
return nil
}),
)
registerFlag(cmd, settings, "tag", "Main image tag to use for deployment (takes precedence over MAIN_IMAGE_TAG environment variable)",
withShortName("t"),
withApplyFn("version", func(config *deployer.Config, mainImageTag string) error {
config.Roxie.Version = mainImageTag
return nil
}),
)
registerFlag(cmd, settings, "features", "Feature flag settings (e.g., +ROX_FOO,-ROX_BAR,ROX_BAZ=true)",
withApplyFn("feature-flags", func(config *deployer.Config, featureFlagExpr string) error {
featureFlags, err := deployer.ParseFeatureFlags([]string{featureFlagExpr})
if err != nil {
return fmt.Errorf("parsing feature flags: %w", err)
}
for k, v := range featureFlags {
config.Roxie.FeatureFlags[k] = v
}
return nil
}),
)
registerFlag(cmd, settings, "central-wait", "maximum wait time for central to become ready (e.g., 5m, 10m)",
withApplyFnDuration(func(config *deployer.Config, duration time.Duration) error {
config.Central.DeployTimeout = duration
return nil
}),
)
registerFlag(cmd, settings, "secured-cluster-wait", "maximum wait time for secured cluster to become ready (e.g., 5m, 10m)",
withApplyFnDuration(func(config *deployer.Config, duration time.Duration) error {
config.SecuredCluster.DeployTimeout = duration
return nil
}),
)
registerFlag(cmd, settings, "early-readiness", "Only wait for essential workloads (central/sensor) to be ready",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Central.EarlyReadiness = new(val)
config.SecuredCluster.EarlyReadiness = new(val)
return nil
}),
)
// Make --override an alias for --config, for backwards compatibility.
cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
if name == "override" {
name = "config"
}
return pflag.NormalizedName(name)
})
return cmd
}
func runDeploy(cmd *cobra.Command, args []string) error {
log := logger.New()
if !dryRun {
if err := env.Initialize(log); err != nil {
return err
}
}
if env.RunningInteractively {
log.Dim("Running with a controlling terminal.")
} else {
log.Dim("Running without a controlling terminal.")
}
components, err := component.FromArgs(args)
if err != nil {
return err
}
if deploySettings.Roxie.Version != "" {
log.Dimf("Using main image tag %s", deploySettings.Roxie.Version)
} else {
mainImageTag, err := helpers.LookupMainImageTag(log)
if err != nil {
return fmt.Errorf("looking up main image tag: %w", err)
}
deploySettings.Roxie.Version = mainImageTag
}
if err := configureConfig(log, components, &deploySettings); err != nil {
return err
}
if err := deployValidate(components, &deploySettings); err != nil {
return err
}
if !deploySettings.Central.EarlyReadinessEnabled() || !deploySettings.SecuredCluster.EarlyReadinessEnabled() {
// Explanation on the versions involved here:
// Deploying StackRox begins with picking a "main image tag" -- this is a version identifier, which cannot be reliably parsed as a semver.
// But there is a derived version from that -- the operator version -- which can be parsed as a semver.
//
// The invocation of deploySettings.Operator.Configure() above in this function prepares the operator deployment config in the sense
// that top-level roxie configuration options are propagated to the concrete operator deployment configuration. This includes also
// storing of the derived operator version within the operator configuration.
//
// This is why we use the operator version here when checking version constraints.
hasSupport, err := stackroxversions.SupportsAdditionalPrinterColumns(deploySettings.Operator.Version)
if err != nil {
return fmt.Errorf("checking version constraint on main image tag %s: %w", deploySettings.Roxie.Version, err)
}
if !hasSupport {
return fmt.Errorf("--early-readiness=false can only be used for StackRox versions satisfying %s", stackroxversions.SupportsAdditionalPrinterColumnsConstraint.String())
}
}
d, err := deployer.New(log)
if err != nil {
return fmt.Errorf("failed to create deployer: %w", err)
}
defer d.Cleanup()
if envrc != "" {
d.SetEnvrcFile(envrc)
}
d.SetVerbose(verbose)
d.SetConfig(deploySettings)
if dryRun {
log.Info("Exiting because of enabled dry run mode.")
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
if components.IncludesCentral() {
d.PrintCentralDeploymentSummary()
}
if components.IncludesSensor() {
d.PrintSecuredClusterDeploymentSummary()
}
if err := d.Deploy(ctx, components); err != nil {
return fmt.Errorf("deployment failed: %w", err)
}
log.Success("🎉 Deployment complete!")
// If Central was deployed, wait for it to be ready before entering subshell
if components.IncludesCentral() {
d.WaitForCentral(5 * time.Minute)
}
if components.IncludesCentral() && !dryRun {
roxieEnv := roxieenv.AssembleRoxieEnvironment(d.GetCentralDeploymentInfo())
m := manifest.RoxieManifest{
RoxieEnvironment: roxieEnv,
Config: deploySettings,
}
if err := manifest.CreateManifestSecretOnCluster(ctx, log, m); err != nil {
log.Warningf("Failed to save roxie manifest: %v", err)
}
}
if components.IncludesCentral() && envrc == "" {
if err := spawnSubshellForDeployerEnv(d, log); err != nil {
return fmt.Errorf("failed to spawn subshell: %w", err)
}
}
return nil
}
func configureConfig(log *logger.Logger, components component.Component, deploySettings *deployer.Config) error {
if deploySettings.Roxie.ClusterType == types.ClusterTypeUnknown {
clusterType := env.GetAutoDetectedClusterType()
log.Dimf("Detected cluster type: %v", clusterType)
deploySettings.Roxie.ClusterType = clusterType
}
clusterType := deploySettings.Roxie.ClusterType
defaults, err := clusterdefaults.ApplyClusterDefaults(deploySettings)
if err != nil {
return err
}
if verbose {
log.Dimf("Applying the following defaults based on cluster type %v:", clusterType)
helpers.LogMultilineYaml(log, defaults)
}
// Deal with the "auto" resourceProfile.
if deploySettings.Central.ResourceProfile == types.ResourceProfileAuto {
profile := clusterdefaults.ResolveAutoResourceProfile(clusterType)
log.Dimf("Selecting resource profile %v for Central", profile)
deploySettings.Central.ResourceProfile = profile
}
if deploySettings.SecuredCluster.ResourceProfile == types.ResourceProfileAuto {
profile := clusterdefaults.ResolveAutoResourceProfile(clusterType)
log.Dimf("Selecting resource profile %v for SecuredCluster", profile)
deploySettings.SecuredCluster.ResourceProfile = profile
}
// We need to do this regardless of whether the operator is deployed or not, because
// this includes the transformation of StackRox main image tags to semver compatible versions,
// which we will make use of later for checking version constraints.
if err := deploySettings.Operator.Configure(&deploySettings.Roxie); err != nil {
return fmt.Errorf("configuring operator configuration: %w", err)
}
if components.IncludesCentral() {
if err := deploySettings.Central.ConfigureSpec(&deploySettings.Roxie); err != nil {
return fmt.Errorf("configuring Central spec: %w", err)
}
}
if components.IncludesSensor() {
if err := deploySettings.SecuredCluster.ConfigureSpec(&deploySettings.Roxie, &deploySettings.Central); err != nil {
return fmt.Errorf("configuring SecuredCluster spec: %w", err)
}
}
if verbose {
log.Dim("Deployment configuration:")
helpers.LogMultilineYaml(log, deploySettings)
}
if !deploySettings.Central.PortForwardingSet() && !deploySettings.Central.ExposureEnabled() {
log.Info("Enabling port-forwarding due to no exposure")
deploySettings.Central.PortForwarding = ptr.To(true)
}
return nil
}
func deployValidate(components component.Component, deploySettings *deployer.Config) error {
if components.IncludesCentral() && os.Getenv("ROXIE_SHELL") != "" {
return errors.New("already in a roxie sub-shell (ROXIE_SHELL environment variable is set), please exit the shell and try again")
}
if components.IncludesCentral() && !env.RunningInteractively && envrc == "" {
return errors.New("running without a controlling terminal requires --envrc to be set")
}
clusterType := deploySettings.Roxie.ClusterType
if env.RunningInRoxieContainer {
// For running containerized we have specific requirements.
if deploySettings.Central.PortForwardingEnabled() {
return errors.New("containerized mode does not support port-forwarding")
}
if !deploySettings.Central.ExposureEnabled() {
return errors.New("containerized mode requires Central exposure")
}
// On infra OpenShift we already get image pull secrets for Quay automatically.
if clusterType.NeedsPullSecrets() {
if os.Getenv("REGISTRY_USERNAME") == "" || os.Getenv("REGISTRY_PASSWORD") == "" {
return fmt.Errorf("containerized mode requires REGISTRY_USERNAME and REGISTRY_PASSWORD environment variables for clusters of type %s", clusterType)
}
if _, err := os.Stat("/kubeconfig"); err != nil {
return fmt.Errorf("containerized mode requires /kubeconfig file: %w", err)
}
}
}
if deploySettings.Operator.SkipDeployment && deploySettings.Operator.DeployViaOlm {
return errors.New("skipping operator deployment while also requesting deploying via OLM at the same time does not make sense")
}
if deploySettings.Roxie.KonfluxImages {
if deploySettings.Operator.DeployViaOlm {
return errors.New("using Konflux images while deploying operator via OLM is not supported")
}
if !clusterType.IsOpenShift() {
return fmt.Errorf("--konflux flag is only supported on OpenShift 4 clusters (current cluster type: %s)", clusterType)
}
}
return nil
}