Skip to content

Conversation

@gazarenkov
Copy link
Member

Description

Introduce Flavors as specified: https://docs.google.com/document/d/1CgxFZt7ITVb8LsbxZ1YRJP5CZZw7WMIZA3I_vUL8SmE/edit?tab=t.0

Which issue(s) does this PR fix or relate to

  • Fixes #issue_number

PR acceptance criteria

  • Tests
  • Documentation

How to test changes / Special notes to the reviewer

@openshift-ci
Copy link

openshift-ci bot commented Jan 19, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign kadel for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@rhdh-qodo-merge
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🔒 Security concerns

Potential secret exposure:
pkg/model/secretenvs.go injects all key/value pairs from any Secret in the namespace labeled with rhdh.redhat.com/default-config into the Backstage pod environment. This broad mechanism can unintentionally expose sensitive values if a user labels an unrelated Secret (or a Secret contains more keys than intended). Consider scoping/allow-listing which secrets/keys can be injected, or documenting the risk clearly.

⚡ Recommended focus areas for review

Possible Issue

The controller appends pointers to range variables when building NamespacedConfig.EnvSecrets. In Go, taking the address of the loop variable can result in all pointers referencing the same memory, so the model may end up using the last secret repeatedly. Consider storing by value, or taking the address of secretList.Items[i] instead.

// discoverNamespacedConfig finds user-provided resources in the namespace via labels
// Returns secrets with label rhdh.redhat.com/default-config="" (generic) or matching flavor
func (r *BackstageReconciler) discoverNamespacedConfig(ctx context.Context, namespace string, flavor string) (model.NamespacedConfig, error) {

	namespacedConfig := model.NewNamespacedConfig()

	// List all secrets with default-config label (any value)
	secretList := &corev1.SecretList{}
	if err := r.List(ctx, secretList, namespacedConfig.ListOptions(namespace)); err != nil {
		return namespacedConfig, fmt.Errorf("failed to list default config secrets: %w", err)
	}

	for _, secret := range secretList.Items {

		labelValue := secret.Labels[model.DefaultConfigLabel]
		// Include if generic (empty value) or matches flavor
		if labelValue == "" || labelValue == flavor {
			namespacedConfig.EnvSecrets = append(namespacedConfig.EnvSecrets, &secret)
		}
	}

	return namespacedConfig, nil
}
Integration Risk

Flavor default lookup depends on LOCALBIN and assumes flavored defaults exist under <LOCALBIN>/default-config/flavors/<flavor>/<key>. In the shipped manifests, flavor files are mounted to /default-config/flavors/... (not under LOCALBIN/default-config/...). Verify LOCALBIN is set appropriately in the operator deployment (or adjust the path logic) so flavored defaults can actually be found at runtime.

func DefFile(key string, flavor string) (string, error) {
	localBin := os.Getenv("LOCALBIN")
	if localBin == "" {
		localBin = "."
	}

	if flavor != "" {
		// Check flavor directory exists
		flavorDir := filepath.Join(localBin, "default-config", "flavors", flavor)
		if _, err := os.Stat(flavorDir); err != nil {
			if os.IsNotExist(err) {
				return "", fmt.Errorf("flavor '%s' does not exist: directory not found at %s", flavor, flavorDir)
			}
			return "", fmt.Errorf("failed to check flavor directory '%s': %w", flavor, err)
		}

		// Try flavor file first
		flavorPath := filepath.Join(flavorDir, key)
		if _, err := os.Stat(flavorPath); err == nil {
			return flavorPath, nil
		}
	}

	// Fallback to default-config
	return filepath.Join(localBin, "default-config", key), nil
}
📄 References
  1. redhat-developer/rhdh-operator/tests/e2e/e2e_suite_test.go [54-61]
  2. redhat-developer/rhdh-operator/tests/e2e/e2e_test.go [58-94]
  3. redhat-developer/rhdh-operator/integration_tests/rhdh-config_test.go [23-56]
  4. redhat-developer/rhdh-operator/integration_tests/rhdh-config_test.go [226-259]
  5. redhat-developer/rhdh-operator/integration_tests/rhdh-config_test.go [261-278]
  6. redhat-developer/rhdh-operator/integration_tests/rhdh-config_test.go [142-181]
  7. redhat-developer/rhdh-operator/integration_tests/default-config_test.go [1-44]
  8. redhat-developer/rhdh-operator/integration_tests/cr-config_test.go [1-43]

@rhdh-qodo-merge rhdh-qodo-merge bot added the enhancement New feature or request label Jan 19, 2026
@rhdh-qodo-merge
Copy link

PR Type

Enhancement


Description

  • Add Flavor support to enable pre-configured Backstage deployment templates

  • Discover namespaced config via labeled secrets for shared defaults

  • Implement flavor-specific configuration file loading with fallback mechanism

  • Add AI and Orchestrator flavor templates with dynamic plugins and app configs


File Walkthrough

Relevant files
Enhancement
6 files
backstage_types.go
Add Flavor field to BackstageSpec                                               
+6/-0     
backstage_controller.go
Add namespaced config discovery and flavor support             
+34/-4   
namespacedconfig.go
New file for namespaced config discovery via labels           
+34/-0   
runtime.go
Add flavor support and namespaced config to model initialization
+27/-8   
secretenvs.go
Inject namespaced config secrets as environment variables
+10/-0   
utils.go
Add flavor-aware config file path resolution with fallback
+25/-2   
Formatting
3 files
zz_generated.deepcopy.go
Fix import alias for apiextensions v1                                       
+1/-1     
zz_generated.deepcopy.go
Fix import alias for apiextensions v1                                       
+1/-1     
zz_generated.deepcopy.go
Fix import alias for apiextensions v1                                       
+1/-1     
Tests
13 files
model_tests.go
Add namespacedConfig to test helper object                             
+7/-4     
appconfig_test.go
Update InitObjects calls with namespacedConfig parameter 
+3/-4     
configmapenvs_test.go
Update InitObjects calls with namespacedConfig parameter 
+6/-6     
configmapfiles_test.go
Update InitObjects calls with namespacedConfig parameter 
+7/-7     
db-secret_test.go
Update InitObjects calls with namespacedConfig parameter 
+3/-3     
db-statefulset_test.go
Update InitObjects calls and remove commented test code   
+2/-41   
deployment_test.go
Update InitObjects calls with namespacedConfig parameter 
+11/-11 
dynamic-plugins_test.go
Update InitObjects calls with namespacedConfig parameter 
+12/-12 
pvcs_test.go
Update InitObjects calls with namespacedConfig parameter 
+5/-5     
route_test.go
Update InitObjects calls with namespacedConfig parameter 
+9/-9     
runtime_test.go
Update InitObjects calls with namespacedConfig parameter 
+9/-9     
secretenvs_test.go
Update InitObjects calls with namespacedConfig parameter 
+6/-6     
secretfiles_test.go
Update InitObjects calls with namespacedConfig parameter 
+9/-9     
Configuration changes
6 files
rhdh.redhat.com_backstages.yaml
Add Flavor field to CRD schema definition                               
+6/-0     
app-config.yaml
Add AI flavor app configuration template                                 
+204/-0 
dynamic-plugins.yaml
Add AI flavor dynamic plugins configuration                           
+322/-0 
dynamic-plugins.yaml
Add Orchestrator flavor dynamic plugins configuration       
+61/-0   
kustomization.yaml
Add flavor configMap generators and update operator image
+9/-2     
deployment-patch.yaml
Add flavor volume mounts to operator deployment                   
+16/-1   
Miscellaneous
1 files
bs1.yaml
Minor formatting adjustment to example                                     
+1/-0     

Co-authored-by: gazarenkov <gazarenkov@users.noreply.github.com>
@github-actions
Copy link
Contributor

⚠️ Files changed in bundle and installer generation!

Those changes to the operator bundle/installer manifests should have been pushed automatically to your PR branch.

NOTE: If the PR checks are stuck after this additional commit, manually close the PR and immediately reopen it to trigger the checks again.

@sonarqubecloud
Copy link

@rhdh-qodo-merge
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix incorrect secret reference in loop

In discoverNamespacedConfig, fix the loop over secretList.Items to correctly
capture the secret variable before taking its address. This prevents all
elements in namespacedConfig.EnvSecrets from incorrectly pointing to the last
secret in the list.

internal/controller/backstage_controller.go [252-259]

-	for _, secret := range secretList.Items {
-
+	for i := range secretList.Items {
+		secret := secretList.Items[i]
 		labelValue := secret.Labels[model.DefaultConfigLabel]
 		// Include if generic (empty value) or matches flavor
 		if labelValue == "" || labelValue == flavor {
 			namespacedConfig.EnvSecrets = append(namespacedConfig.EnvSecrets, &secret)
 		}
 	}
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical bug where taking the address of a loop variable would cause all appended pointers to reference the last element, leading to incorrect secret configurations.

High
Skip missing flavor directory

Update DefFile to silently fall back to the default configuration if a specified
flavor directory does not exist, preventing startup failures for invalid or
unused flavors.

pkg/utils/utils.go [242-267]

 func DefFile(key string, flavor string) (string, error) {
 	localBin := os.Getenv("LOCALBIN")
 	if localBin == "" {
 		localBin = "."
 	}
 
 	if flavor != "" {
-		// Check flavor directory exists
 		flavorDir := filepath.Join(localBin, "default-config", "flavors", flavor)
-		if _, err := os.Stat(flavorDir); err != nil {
-			if os.IsNotExist(err) {
-				return "", fmt.Errorf("flavor '%s' does not exist: directory not found at %s", flavor, flavorDir)
+		if info, err := os.Stat(flavorDir); err == nil && info.IsDir() {
+			flavorPath := filepath.Join(flavorDir, key)
+			if _, err := os.Stat(flavorPath); err == nil {
+				return flavorPath, nil
 			}
-			return "", fmt.Errorf("failed to check flavor directory '%s': %w", flavor, err)
 		}
-
-		// Try flavor file first
-		flavorPath := filepath.Join(flavorDir, key)
-		if _, err := os.Stat(flavorPath); err == nil {
-			return flavorPath, nil
-		}
+		// silently skip missing or invalid flavorDir and fall back
 	}
 
-	// Fallback to default-config
 	return filepath.Join(localBin, "default-config", key), nil
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly proposes to make the flavor discovery more robust by silently falling back to default configurations if a flavor directory is missing, which aligns with the feature's design of flavors being optional overrides.

Medium
High-level
Re-evaluate the implicit namespace-level configuration

The "Namespaced Config" feature implicitly discovers secrets using labels. This
should be changed to an explicit configuration within the BackstageSpec to
improve clarity and maintainability.

Examples:

internal/controller/backstage_controller.go [241-262]
// Returns secrets with label rhdh.redhat.com/default-config="" (generic) or matching flavor
func (r *BackstageReconciler) discoverNamespacedConfig(ctx context.Context, namespace string, flavor string) (model.NamespacedConfig, error) {

	namespacedConfig := model.NewNamespacedConfig()

	// List all secrets with default-config label (any value)
	secretList := &corev1.SecretList{}
	if err := r.List(ctx, secretList, namespacedConfig.ListOptions(namespace)); err != nil {
		return namespacedConfig, fmt.Errorf("failed to list default config secrets: %w", err)
	}

 ... (clipped 12 lines)
pkg/model/secretenvs.go [35-40]
	for _, secret := range p.model.NamespacedConfig.EnvSecrets {
		err := p.model.backstageDeployment.addEnvVarsFrom(containersFilter{}, SecretObjectKind, secret.Name, "")
		if err != nil {
			return fmt.Errorf("failed to add namespaced config secret env vars %s: %w", secret.Name, err)
		}
	}

Solution Walkthrough:

Before:

// In backstage_controller.go
func (r *BackstageReconciler) Reconcile(ctx, req) {
  // ...
  // Implicitly discover secrets in the namespace via labels
  namespacedConfig, err := r.discoverNamespacedConfig(ctx, backstage.Namespace, backstage.Spec.Flavor)
  // ...
  // Pass discovered config to the model
  bsModel, err := model.InitObjects(ctx, backstage, externalConfig, namespacedConfig, ...)
  // ...
}

// In model/secretenvs.go
func (p *SecretEnvs) addExternalConfig(spec) {
  // Inject env vars from discovered secrets
  for _, secret := range p.model.NamespacedConfig.EnvSecrets {
    p.model.backstageDeployment.addEnvVarsFrom(..., secret.Name, "")
  }
  // ...
}

After:

// In api/v1alpha5/backstage_types.go
type BackstageSpec struct {
  // ...
  // Explicitly list secrets to use for namespaced config
  // +optional
  NamespacedEnvFromSecrets []string `json:"namespacedEnvFromSecrets,omitempty"`
  // ...
}

// In backstage_controller.go
func (r *BackstageReconciler) Reconcile(ctx, req) {
  // ...
  // No implicit discovery. Controller fetches secrets listed in the spec.
  namespacedConfig := model.NewNamespacedConfig()
  for _, secretName := range backstage.Spec.NamespacedEnvFromSecrets {
    // fetch secret by name and add to namespacedConfig.EnvSecrets
  }
  bsModel, err := model.InitObjects(ctx, backstage, externalConfig, namespacedConfig, ...)
  // ...
}
Suggestion importance[1-10]: 8

__

Why: This is a significant design suggestion that correctly identifies a move towards implicit, label-based configuration, which can reduce clarity and make debugging harder.

Medium
Organization
best practice
Provide runnable flavor example

Update the example to demonstrate spec.flavor usage and include placeholder
Secrets (and labels) needed for the flavor so the manifest can be applied
without missing dependencies.

examples/bs1.yaml [1-4]

+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: ai-flavor-env
+  labels:
+    rhdh.redhat.com/default-config: ai
+stringData:
+  RHDH_BASE_URL: "https://example.invalid"
+  RHDH_CALLBACK_URL: "https://example.invalid/api/auth/oidc/handler/frame"
+  BACKEND_SECRET: "dummy-not-secret"
+  ADMIN_TOKEN: "dummy-token"
+  MCP_TOKEN: "dummy-token"
+  KEYCLOAK_BASE_URL: "https://example.invalid"
+  KEYCLOAK_METADATA_URL: "https://example.invalid/realms/dummy/.well-known/openid-configuration"
+  KEYCLOAK_REALM: "dummy"
+  KEYCLOAK_CLIENT_ID: "dummy"
+  KEYCLOAK_CLIENT_SECRET: "dummy"
+  POSTGRESQL_ADMIN_PASSWORD: "dummy"
+  ARGOCD_USER: "dummy"
+  ARGOCD_PASSWORD: "dummy"
+  ARGOCD_HOSTNAME: "example.invalid"
+  ARGOCD_API_TOKEN: "dummy"
+  K8S_CLUSTER_TOKEN: "dummy"
+  GITHUB_APP_APP_ID: "0"
+  GITHUB_APP_CLIENT_ID: "dummy"
+  GITHUB_APP_CLIENT_SECRET: "dummy"
+  GITHUB_APP_WEBHOOK_URL: "https://example.invalid/webhook"
+  GITHUB_APP_WEBHOOK_SECRET: "dummy"
+  GITHUB_APP_PRIVATE_KEY: "dummy"
+---
 apiVersion: rhdh.redhat.com/v1alpha5
 kind: Backstage
 metadata:
   name: bs1
+spec:
+  flavor: ai

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Add complete, runnable examples by including required dependent resources (e.g., Secrets/ConfigMaps) with safe placeholder values so users can apply them out-of-the-box.

Low
Validate the flavor field

Add kubebuilder validation (e.g., enum) and clarify behavior for unknown flavors
to prevent reconcile loops due to typos and make the feature self-describing in
the CRD.

api/v1alpha5/backstage_types.go [26-30]

 // Flavor specifies a pre-configured template for Backstage deployment (e.g., "orchestrator", "ai").
 // When specified, operator loads default configuration from the flavor template instead of standard defaults.
-// Optional.
+// +kubebuilder:validation:Enum=ai;orchestrator
 // +optional
 Flavor string `json:"flavor,omitempty"`
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - When introducing optional features controlled by CR fields, document and validate the field to avoid confusing failures and improve lifecycle management.

Low
  • More

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant