-
Notifications
You must be signed in to change notification settings - Fork 1
Add support for operator environment variables #215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -321,9 +321,16 @@ func (d *Deployer) deployOperatorFromCSV(ctx context.Context, bundleDir string) | |
| d.useOperatorPullSecrets = d.config.Roxie.KonfluxImagesEnabled() && d.config.Roxie.ClusterType.NeedsPullSecrets() | ||
|
|
||
| d.logger.Info("📋 Operator deployment plan:") | ||
| d.logger.Dim(fmt.Sprintf(" • Namespace: %s", operatorNamespace)) | ||
| d.logger.Dim(fmt.Sprintf(" • ServiceAccount: %s", serviceAccountName)) | ||
| d.logger.Dim(fmt.Sprintf(" • Setting up pull secrets: %v", d.useOperatorPullSecrets)) | ||
| d.logger.Dimf(" • Namespace: %s", operatorNamespace) | ||
| d.logger.Dimf(" • ServiceAccount: %s", serviceAccountName) | ||
| d.logger.Dimf(" • Setting up pull secrets: %v", d.useOperatorPullSecrets) | ||
| if len(d.config.Operator.EnvVars) > 0 { | ||
| d.logger.Dimf(" • Custom operator env vars: %d", len(d.config.Operator.EnvVars)) | ||
| for _, envVar := range envVarsToSortedList(d.config.Operator.EnvVars) { | ||
| ev := envVar.(map[string]interface{}) | ||
| d.logger.Dimf(" %s=%s", ev["name"], ev["value"]) | ||
| } | ||
|
Comment on lines
+327
to
+332
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Avoid logging raw operator env var values. This prints user-supplied env values directly, which can leak secrets/tokens into deploy logs. Log names only (or redact values). The same pattern should be fixed in 🔧 Suggested fix- for _, envVar := range envVarsToSortedList(d.config.Operator.EnvVars) {
- ev := envVar.(map[string]interface{})
- d.logger.Dimf(" %s=%s", ev["name"], ev["value"])
- }
+ for _, envVar := range envVarsToSortedList(d.config.Operator.EnvVars) {
+ ev := envVar.(map[string]interface{})
+ d.logger.Dimf(" %s=<redacted>", ev["name"])
+ }🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| if err := d.prepareNamespace(ctx, operatorNamespace, d.useOperatorPullSecrets); err != nil { | ||
| return err | ||
|
|
@@ -530,6 +537,16 @@ func (d *Deployer) createDeploymentFromCSV(ctx context.Context, namespace string | |
| if template, ok := spec["template"].(map[string]interface{}); ok { | ||
| if podSpec, ok := template["spec"].(map[string]interface{}); ok { | ||
| podSpec["serviceAccountName"] = deploymentSpec["service_account"] | ||
|
|
||
| if len(d.config.Operator.EnvVars) > 0 { | ||
| containers, ok := podSpec["containers"].([]interface{}) | ||
| if !ok { | ||
| return errors.New("no containers found in deployment pod spec") | ||
| } | ||
| if err := d.injectEnvVarsIntoManagerContainer(containers); err != nil { | ||
| return fmt.Errorf("failed to inject operator env vars: %w", err) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -548,6 +565,45 @@ func (d *Deployer) createDeploymentFromCSV(ctx context.Context, namespace string | |
| return nil | ||
| } | ||
|
|
||
| const managerContainerName = "manager" | ||
|
|
||
| // injectEnvVarsIntoManagerContainer merges configured operator env vars into | ||
| // the manager container, overriding any existing env vars with the same name. | ||
| func (d *Deployer) injectEnvVarsIntoManagerContainer(containers []interface{}) error { | ||
| for _, c := range containers { | ||
| container, ok := c.(map[string]interface{}) | ||
| if !ok { | ||
| continue | ||
| } | ||
| if container["name"] != managerContainerName { | ||
| continue | ||
| } | ||
|
|
||
| existing := make(map[string]int) | ||
| envList, _ := container["env"].([]interface{}) | ||
| for i, item := range envList { | ||
| if envVar, ok := item.(map[string]interface{}); ok { | ||
| if name, ok := envVar["name"].(string); ok { | ||
| existing[name] = i | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for _, envVar := range envVarsToSortedList(d.config.Operator.EnvVars) { | ||
| name := envVar.(map[string]interface{})["name"].(string) | ||
| if idx, found := existing[name]; found { | ||
| envList[idx] = envVar | ||
| } else { | ||
| envList = append(envList, envVar) | ||
| } | ||
| } | ||
|
|
||
| container["env"] = envList | ||
| return nil | ||
| } | ||
| return fmt.Errorf("container %q not found in deployment", managerContainerName) | ||
| } | ||
|
|
||
| func (d *Deployer) applyBundleServiceResources(ctx context.Context, bundleDir, namespace string) error { | ||
| serviceFile := filepath.Join(bundleDir, "rhacs-operator-controller-manager-metrics-service_v1_service.yaml") | ||
| if _, err := os.Stat(serviceFile); err == nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package deployer | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "sort" | ||
| "strings" | ||
| ) | ||
|
|
||
| // ParseOperatorEnvVar parses a single KEY=VALUE environment variable string. | ||
| // Values may contain '=' characters (only the first '=' is used as the separator). | ||
| func ParseOperatorEnvVar(envExpr string) (string, string, error) { | ||
| key, value, found := strings.Cut(envExpr, "=") | ||
| if !found { | ||
| return "", "", fmt.Errorf("invalid operator env var %q: expected KEY=VALUE format", envExpr) | ||
| } | ||
| if key == "" { | ||
| return "", "", fmt.Errorf("invalid operator env var %q: empty key", envExpr) | ||
| } | ||
| return key, value, nil | ||
| } | ||
|
|
||
| // envVarsToSortedList converts a map of env vars to a sorted list of | ||
| // {name, value} maps suitable for use in Kubernetes container or OLM Subscription specs. | ||
| func envVarsToSortedList(envVars map[string]string) []interface{} { | ||
| result := make([]interface{}, 0, len(envVars)) | ||
| for name, value := range envVars { | ||
| result = append(result, map[string]interface{}{ | ||
| "name": name, | ||
| "value": value, | ||
| }) | ||
| } | ||
| sort.Slice(result, func(i, j int) bool { | ||
| return result[i].(map[string]interface{})["name"].(string) < | ||
| result[j].(map[string]interface{})["name"].(string) | ||
| }) | ||
| return result | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| package deployer | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestParseOperatorEnvVar(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| expectedKey string | ||
| expectedVal string | ||
| expectError bool | ||
| }{ | ||
| { | ||
| name: "simple KEY=VALUE", | ||
| input: "RELATED_IMAGE_MAIN=quay.io/rhacs-eng/main:4.7.0", | ||
| expectedKey: "RELATED_IMAGE_MAIN", | ||
| expectedVal: "quay.io/rhacs-eng/main:4.7.0", | ||
| }, | ||
| { | ||
| name: "value containing equals sign", | ||
| input: "MY_VAR=a=b=c", | ||
| expectedKey: "MY_VAR", | ||
| expectedVal: "a=b=c", | ||
| }, | ||
| { | ||
| name: "empty value", | ||
| input: "MY_VAR=", | ||
| expectedKey: "MY_VAR", | ||
| expectedVal: "", | ||
| }, | ||
| { | ||
| name: "value with spaces", | ||
| input: "MY_VAR= hello world ", | ||
| expectedKey: "MY_VAR", | ||
| expectedVal: " hello world ", | ||
| }, | ||
| { | ||
| name: "value with commas", | ||
| input: "MY_VAR=a,b,c", | ||
| expectedKey: "MY_VAR", | ||
| expectedVal: "a,b,c", | ||
| }, | ||
| { | ||
| name: "missing equals sign", | ||
| input: "NO_VALUE", | ||
| expectError: true, | ||
| }, | ||
| { | ||
| name: "empty key", | ||
| input: "=value", | ||
| expectError: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| key, value, err := ParseOperatorEnvVar(tt.input) | ||
| if tt.expectError { | ||
| require.Error(t, err) | ||
| return | ||
| } | ||
| require.NoError(t, err) | ||
| assert.Equal(t, tt.expectedKey, key) | ||
| assert.Equal(t, tt.expectedVal, value) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| func TestEnvVarsToSortedList(t *testing.T) { | ||
|
Comment on lines
+74
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Fix gofmt violation before merge. CI reports formatting drift at Line 71 (extra blank line before Proposed fix }
-
func TestEnvVarsToSortedList(t *testing.T) {🤖 Prompt for AI AgentsSource: Pipeline failures |
||
| input := map[string]string{ | ||
| "ZZZ": "z", | ||
| "AAA": "a", | ||
| "MMM": "m", | ||
| } | ||
| result := envVarsToSortedList(input) | ||
|
|
||
| require.Len(t, result, 3) | ||
|
|
||
| expectedOrder := []string{"AAA", "MMM", "ZZZ"} | ||
| for i, item := range result { | ||
| name := item.(map[string]interface{})["name"].(string) | ||
| assert.Equal(t, expectedOrder[i], name, "index %d", i) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🔴 Critical
🧩 Analysis chain
🏁 Script executed:
Repository: stackrox/roxie
Length of output: 1705
🏁 Script executed:
Repository: stackrox/roxie
Length of output: 2960
🏁 Script executed:
Repository: stackrox/roxie
Length of output: 565
🏁 Script executed:
Repository: stackrox/roxie
Length of output: 2021
Remove or fix the comma-separated test—comma-splitting is not implemented.
The flag plumbing does not split comma-separated values before invoking the apply function. When
--operator-env "FOO=bar,BAZ=qux"is parsed:Set()is called once with the full string"FOO=bar,BAZ=qux"ParseOperatorEnvVar()splits only on the first=, returning key="FOO", value="bar,BAZ=qux"EnvVars["FOO"] = "bar,BAZ=qux", not separateFOOandBAZentriesThe test assertion
assert.Equal(t, "qux", cfg.Operator.EnvVars["BAZ"])will fail because theBAZkey is never created. Either remove this test case or add comma-splitting logic to the apply function if that behavior is intended.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, this test is currently broken
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mclasmeier, thanks for confirming. The cleanest fix is to remove the "operator-env comma separated" test case, since comma-splitting is not implemented and would actually conflict with values that legitimately contain commas (whichParseOperatorEnvVaralready supports, per the tests inoperator_env_test.go).The two other test cases (
"operator-env single"and"operator-env multiple flags") correctly cover the intended usage patterns and can remain as-is.If comma-separated syntax is a deliberate feature you'd like to support, it would need comma-splitting logic added in the apply function in
cmd/deploy.go— but note that would break values likeFOO=bar,baz(a value containing a comma). The--operator-env KEY=VALUE --operator-env KEY2=VALUE2pattern is the unambiguous way to pass multiple variables.