Skip to content

Flavours#2415

Open
gazarenkov wants to merge 57 commits intoredhat-developer:mainfrom
gazarenkov:flavour2
Open

Flavours#2415
gazarenkov wants to merge 57 commits intoredhat-developer:mainfrom
gazarenkov:flavour2

Conversation

@gazarenkov
Copy link
Copy Markdown
Member

Description

Introduce Flavors as [specified in ADR](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

https://issues.redhat.com/browse/RHIDP-11505

PR acceptance criteria

  • Tests
  • Documentation

How to test changes / Special notes to the reviewer

@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge bot commented Mar 2, 2026

PR Reviewer Guide 🔍

(Review updated until commit 1f788ec)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Spec Mismatch

The documented spec.flavours behavior for an explicitly empty array conflicts with the behavior asserted in tests. The type comment says an empty array still uses default flavours, but the new flavour tests expect an empty array to disable all flavours. This needs clarification and alignment (either adjust code/logic or fix the comment/tests) to avoid breaking user expectations and API contract.

	// Flavours specifies which pre-configured templates to enable.
	// Flavours are variations of the default configuration that extend and override
	// base defaults with domain-specific customizations.
	//
	// If not specified (nil), flavours with enabledByDefault: true in their metadata are used.
	// If specified as empty array ([]), default flavours are still used.
	// To disable a default flavour, explicitly list it with enabled: false.
	//
	// Multiple flavours can be enabled - configs are merged in the order specified.
	// +optional
	Flavours []Flavour `json:"flavours,omitempty"`
}
Test Logic

TestGetEnabledFlavours describes expected flavour names “in order”, and includes a case asserting spec order preservation, but it uses assert.ElementsMatch, which ignores ordering. This can let ordering regressions slip through. Consider using assert.Equal (or require.Equal) for the gotNames vs wantFlavours comparison in cases where order matters.

func TestGetEnabledFlavours(t *testing.T) {
	// Setup: Set LOCALBIN to testflavours directory for tests
	originalLocalBin := os.Getenv("LOCALBIN")
	testDataDir, err := filepath.Abs("testdata/testflavours")
	require.NoError(t, err)
	err = os.Setenv("LOCALBIN", testDataDir)
	require.NoError(t, err)
	defer func() {
		_ = os.Setenv("LOCALBIN", originalLocalBin)
	}()

	tests := []struct {
		name         string
		spec         api.BackstageSpec
		wantFlavours []string // expected flavour names in order
		wantErr      bool
		errContains  string
	}{
		{
			name:         "nil spec.Flavours returns all defaults",
			spec:         api.BackstageSpec{Flavours: nil},
			wantFlavours: []string{"flavor1", "flavor3"},
			wantErr:      false,
		},
		{
			name: "empty spec.Flavours returns no defaults",
			spec: api.BackstageSpec{
				Flavours: []api.Flavour{},
			},
			wantFlavours: []string{},
			wantErr:      false,
		},
		{
			name: "explicit enabled flavour",
			spec: api.BackstageSpec{
				Flavours: []api.Flavour{
					{Name: "flavor2", Enabled: true},
				},
			},
			// flavor2 (explicit) + defaults (flavor1, flavor3)
			wantFlavours: []string{"flavor2", "flavor1", "flavor3"},
			wantErr:      false,
		},
		{
			name: "default enabled flavour disabled explicitly",
			spec: api.BackstageSpec{
				Flavours: []api.Flavour{
					{Name: "flavor1", Enabled: false},
				},
			},
			// flavor1 disabled, only flavor3 (default) remains
			wantFlavours: []string{"flavor3"},
			wantErr:      false,
		},
		{
			name: "default disabled flavour enabled explicitly",
			spec: api.BackstageSpec{
				Flavours: []api.Flavour{
					{Name: "flavor2", Enabled: true},
				},
			},
			// flavor2 (explicit enabled) + defaults (flavor1, flavor3)
			wantFlavours: []string{"flavor2", "flavor1", "flavor3"},
			wantErr:      false,
		},
		{
			name: "mix of explicit enabled, disabled, and defaults",
			spec: api.BackstageSpec{
				Flavours: []api.Flavour{
					{Name: "flavor2", Enabled: true},  // default=false, spec=enabled
					{Name: "flavor1", Enabled: false}, // default=true, spec=disabled
					{Name: "flavor3", Enabled: true},  // default=true, spec=enabled
				},
			},
			// flavor2 (explicit), flavor3 (explicit), no flavor1 (disabled)
			wantFlavours: []string{"flavor2", "flavor3"},
			wantErr:      false,
		},
		{
			name: "spec order is preserved for explicit flavours",
			spec: api.BackstageSpec{
				Flavours: []api.Flavour{
					{Name: "flavor3", Enabled: true},
					{Name: "flavor2", Enabled: true},
					{Name: "flavor1", Enabled: true},
				},
			},
			// Should be in spec order since all are explicit
			wantFlavours: []string{"flavor3", "flavor2", "flavor1"},
			wantErr:      false,
		},
		{
			name: "nonexistent flavour returns error",
			spec: api.BackstageSpec{
				Flavours: []api.Flavour{
					{Name: "nonexistent", Enabled: true},
				},
			},
			wantErr:     true,
			errContains: "flavour 'nonexistent' not found",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			flavours, err := GetEnabledFlavours(tt.spec)

			if tt.wantErr {
				assert.Error(t, err)
				if tt.errContains != "" {
					assert.Contains(t, err.Error(), tt.errContains)
				}
			} else {
				require.NoError(t, err)

				// Extract flavour names
				gotNames := make([]string, len(flavours))
				for i, f := range flavours {
					gotNames[i] = f.name
				}

				if !assert.ElementsMatch(t, tt.wantFlavours, gotNames) {
					t.Logf("Expected flavours: %v", tt.wantFlavours)
					t.Logf("Got flavours: %v", gotNames)
				}
			}
		})
📄 References
  1. redhat-developer/rhdh/e2e-tests/playwright/utils/authentication-providers/yamls/configmap.yaml [1-42]
  2. redhat-developer/rhdh-chart/charts/backstage/values.yaml [342-356]
  3. redhat-developer/rhdh/scripts/rhdh-openshift-setup/values.yaml [29-55]
  4. redhat-developer/rhdh/scripts/rhdh-openshift-setup/values.yaml [271-310]
  5. redhat-developer/rhdh-operator/config/crd/bases/rhdh.redhat.com_backstages.yaml [325-337]
  6. redhat-developer/rhdh-operator/bundle/rhdh/manifests/rhdh.redhat.com_backstages.yaml [325-337]
  7. redhat-developer/rhdh-chart/charts/backstage/templates/tests/test-secret.yaml [1-3]
  8. redhat-developer/rhdh-operator/config/scorecard/patches/olm.config.yaml [1-40]

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

github-actions bot commented Mar 2, 2026

⚠️ 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.

@rhdh-qodo-merge rhdh-qodo-merge bot added enhancement New feature or request Tests labels Mar 2, 2026
@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge bot commented Mar 2, 2026

Review Summary by Qodo

Introduce Flavours system with v1alpha6 API and API abstraction layer

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Introduces v1alpha6 API version with comprehensive Flavours support for pre-configured
  domain-specific customizations
• Implements flavour discovery, loading, and merging system with metadata-based enablement control
• Adds API abstraction layer (api/current-types.go) to decouple code from specific API versions
• Migrates entire codebase from v1alpha5 to generic api package imports for future API version
  flexibility
• Extends configuration model to support multiple ConfigMaps for app-config, configmap-files, and
  configmap-envs through flavour merging
• Implements deployment and dynamic-plugins merging strategies for flavour-based customizations
• Adds DefaultMultiObjectName() helper for consistent multi-object naming across flavours
• Introduces MergeConfigFunc interface for pluggable configuration merging strategies
• Includes comprehensive test coverage for flavour functionality, default config resolution, and
  multi-object handling
• Updates CRD manifests across all bundles (rhdh, backstage.io) with v1alpha6 version and flavours
  field
• Adds lightspeed and orchestrator flavour configurations with deployment patches, dynamic plugins,
  and ConfigMaps
• Updates all integration tests and examples to use v1alpha6 API version
Diagram
flowchart LR
  A["v1alpha5 API"] -->|"Abstraction Layer"| B["api Package"]
  B --> C["v1alpha6 API"]
  D["Base Config"] -->|"Flavour Merging"| E["Enabled Flavours"]
  E -->|"Merge Strategies"| F["Deployment"]
  E -->|"Merge Strategies"| G["Dynamic Plugins"]
  E -->|"Merge Strategies"| H["ConfigMaps"]
  F --> I["Final Configuration"]
  G --> I
  H --> I
  J["Flavour Discovery"] -->|"Metadata.yaml"| K["FlavourMetadata"]
  K -->|"EnabledByDefault"| E
Loading

Grey Divider

File Changes

1. api/v1alpha6/backstage_types.go ✨ Enhancement +397/-0

New v1alpha6 API with Flavours support

• Introduces new v1alpha6 API version with Flavour type for pre-configured templates
• Adds Flavours field to BackstageSpec to enable/disable domain-specific customizations
• Defines condition types and reasons for deployment status tracking
• Includes helper methods like IsLocalDbEnabled(), IsRouteEnabled(), IsMonitoringEnabled()

api/v1alpha6/backstage_types.go


2. api/v1alpha6/zz_generated.deepcopy.go Code generation +467/-0

Auto-generated deepcopy implementations for v1alpha6

• Auto-generated deepcopy functions for all v1alpha6 types
• Implements DeepCopyInto() and DeepCopy() methods for proper object cloning
• Handles nested structures including Flavour, BackstageSpec, and deployment configurations

api/v1alpha6/zz_generated.deepcopy.go


3. pkg/model/flavour_test.go 🧪 Tests +216/-0

Flavour functionality test suite

• Comprehensive test suite for flavour functionality with 5 test cases
• Tests default flavour enablement, explicit enabling/disabling, and mixed scenarios
• Validates app-config, configmap-files, configmap-envs, deployment labels, and dynamic-plugins
 merging
• Tests edge cases like empty flavour arrays and flavours without base configuration

pkg/model/flavour_test.go


View more (142)
4. pkg/model/default-config_test.go 🧪 Tests +278/-0

Default config and flavour resolution tests

• Tests for dynamic plugins merging with overlay configurations
• Tests for GetEnabledFlavours() function with various spec scenarios
• Validates flavour ordering, default enablement, and error handling for nonexistent flavours

pkg/model/default-config_test.go


5. pkg/model/route_test.go 🧪 Tests +58/-47

Route tests updated for multi-object app-config

• Updates test imports from v1alpha5 to generic api package
• Refactors appConfig field from single ConfigMap to ConfigMaps multi-object structure
• Updates test assertions to work with new multi-object ConfigMap handling

pkg/model/route_test.go


6. integration_tests/pvcs_test.go 🧪 Tests +26/-34

PVC integration tests updated for new naming scheme

• Updates imports from v1alpha5 to generic api package
• Replaces model.PvcsName() calls with model.DefaultMultiObjectName() for consistent naming
• Updates PVC mount path assertions to use explicit paths instead of generated names
• Adds PVC bound status verification

integration_tests/pvcs_test.go


7. pkg/model/secretfiles_test.go 🧪 Tests +26/-26

Secret files tests updated to use generic API

• Updates imports from v1alpha5 to generic api package
• Refactors test setup to use new api types for Backstage and BackstageSpec
• Maintains existing test logic for secret file mounting and container filtering

pkg/model/secretfiles_test.go


8. integration_tests/rhdh-config_test.go 🧪 Tests +77/-14

RHDH config tests with Lightspeed flavour support

• Updates imports from v1alpha5 to generic api package
• Adds new test for default Lightspeed flavour with validation of sidecars and ConfigMaps
• Updates ConfigMap naming to use DefaultMultiObjectName() helper
• Adjusts volume and container count assertions for flavour-based configurations

integration_tests/rhdh-config_test.go


9. pkg/model/deployment.go ✨ Enhancement +69/-20

Deployment merging and mount path handling

• Registers mergeDeployments function for deployment configuration merging
• Implements mergeDeployments() to merge deployment patches from base and flavours using YAML
 merge
• Updates getDefConfigMountPath() to handle mount paths, subpaths, and file names separately
• Refactors imports from v1alpha5 to generic api package

pkg/model/deployment.go


10. pkg/model/dynamic-plugins.go ✨ Enhancement +54/-45

Dynamic plugins merging refactoring

• Registers mergeDynamicPlugins function for plugin configuration merging
• Extracts MergePluginsData() as reusable function for merging plugin configurations
• Refactors mergeWith() to use new MergePluginsData() helper
• Updates imports from v1alpha5 to generic api package

pkg/model/dynamic-plugins.go


11. pkg/model/configmapfiles_test.go 🧪 Tests +35/-27

ConfigMap files tests updated for subpath handling

• Updates imports from v1alpha5 to generic api package
• Renames TestReplaceFiles() to TestDefaultSubpath() with updated assertions
• Tests volume mount paths and subpaths for configmap files with multiple mount scenarios

pkg/model/configmapfiles_test.go


12. pkg/model/default-config.go ✨ Enhancement +173/-0

Flavour-aware default configuration loading

• New file implementing flavour-aware default configuration reading
• Introduces ReadDefaultConfig() as main entry point for reading configs with flavour merging
• Implements collectConfigSources() to gather base and flavour config files
• Provides mergeDynamicPlugins() and mergeMultiObjectConfigs() merge strategies
• Adds DefaultMultiObjectName() helper for consistent object naming across flavours

pkg/model/default-config.go


13. cmd/main.go ⚙️ Configuration changes +1/-1

Main command updated to v1alpha6 API

• Updates API version import from v1alpha5 to v1alpha6

cmd/main.go


14. examples/bs-existing-secret.yaml 📝 Documentation +1/-1

Example updated to v1alpha6 API version

• Updates example Backstage resource API version from v1alpha5 to v1alpha6

examples/bs-existing-secret.yaml


15. internal/controller/backstage_status.go Refactoring +18/-18

Update API imports to use abstraction layer

• Updated import from api/v1alpha5 to api package for API type abstraction
• Replaced all bs. prefixed type references with api. equivalents throughout the file
• No functional changes to the status reconciliation logic

internal/controller/backstage_status.go


16. integration_tests/cr-config_test.go Refactoring +28/-28

Migrate integration tests to API abstraction

• Changed import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. for Backstage spec components
• Maintains all existing test functionality and assertions

integration_tests/cr-config_test.go


17. integration_tests/default-config_test.go Refactoring +23/-21

Update default config tests with API abstraction

• Updated import from api/v1alpha5 to api package
• Removed unused utils import
• Commented out app-config validation tests (configuration-specific)
• Adjusted timeout values in Eventually blocks
• Updated all type references to use api. prefix

integration_tests/default-config_test.go


18. pkg/model/runtime.go ✨ Enhancement +27/-9

Introduce flavours support in runtime initialization

• Updated import from api/v1alpha5 to api package
• Added new annotation constants: DefaultSubPathAnnotation and SourceAnnotation
• Modified registerConfig function to accept MergeConfigFunc parameter for flavour merging
• Added GetEnabledFlavours call to determine enabled flavours during initialization
• Replaced utils.ReadYamlFiles with ReadDefaultConfig to support flavour-based config loading

pkg/model/runtime.go


19. pkg/model/pvcs_test.go Refactoring +21/-20

Update PVC tests with new naming convention

• Updated import from api/v1alpha5 to api package
• Changed PVC naming to use DefaultMultiObjectName function instead of PvcsName
• Updated mount path assertions to reflect new mount path handling

pkg/model/pvcs_test.go


20. pkg/model/route.go ✨ Enhancement +21/-16

Support multi-object app-config in route handling

• Updated import from api/v1alpha5 to api package
• Added corev1 import for ConfigMap type handling
• Updated registerConfig call to include nil merge function parameter
• Changed m.appConfig.ConfigMap to m.appConfig.ConfigMaps to support multiple ConfigMaps
• Updated base URL population logic to work with multi-object ConfigMap structure

pkg/model/route.go


21. pkg/model/flavour.go ✨ Enhancement +135/-0

Add flavours discovery and management system

• New file implementing flavour support functionality
• Defines FlavourMetadata struct for flavour configuration with EnabledByDefault flag
• Implements GetEnabledFlavours function to determine enabled flavours based on spec
• Provides flavour discovery and loading from filesystem with metadata.yaml parsing
• Supports enabling/disabling flavours via spec overrides

pkg/model/flavour.go


22. pkg/model/deployment_test.go Refactoring +15/-15

Update deployment tests to use API abstraction

• Updated import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. throughout test cases
• No changes to test logic or assertions

pkg/model/deployment_test.go


23. pkg/model/configmapenvs.go ✨ Enhancement +28/-25

Support multiple ConfigMaps for environment variables

• Updated import from api/v1alpha5 to api package
• Changed from single ConfigMap to multiobject.MultiObject for multiple ConfigMaps support
• Updated registerConfig to enable multiple objects and add merge function
• Modified updateAndValidate to iterate over multiple ConfigMap items
• Updated setMetaInfo to use setMultiObjectConfigMetaInfo helper

pkg/model/configmapenvs.go


24. pkg/model/model_tests.go 🧪 Tests +66/-7

Enhance test utilities with helper functions

• Updated import from api/v1alpha5 to api package
• Added client import from controller-runtime
• Modified withLocalDb method to accept boolean parameter for flexibility
• Added withConfigPath method to set custom LOCALBIN path for testing
• Added helper functions for test assertions: findConfigMapByName, findConfigMapBySource,
 findPluginByPackage, findEnvVar, findVolumeMountByPath

pkg/model/model_tests.go


25. pkg/model/configmapenvs_test.go Refactoring +23/-23

Update ConfigMap environment tests to use API abstraction

• Updated import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. throughout test cases
• No changes to test logic or assertions

pkg/model/configmapenvs_test.go


26. pkg/model/configmapfiles.go ✨ Enhancement +26/-25

Support multiple ConfigMaps for file mounting

• Updated import from api/v1alpha5 to api package
• Changed from single ConfigMap to multiobject.MultiObject for multiple ConfigMaps
• Updated registerConfig to enable multiple objects and add merge function
• Modified updateAndValidate to iterate over multiple ConfigMap items with mount path handling
• Updated setMetaInfo to use setMultiObjectConfigMetaInfo helper

pkg/model/configmapfiles.go


27. pkg/model/appconfig.go ✨ Enhancement +25/-29

Support multiple app-config ConfigMaps from flavours

• Updated import from api/v1alpha5 to api package
• Changed from single ConfigMap to multiobject.MultiObject for multiple app-config ConfigMaps
• Updated registerConfig to enable multiple objects and add merge function
• Removed AppConfigDefaultName function (now uses generic naming)
• Modified updateAndValidate to iterate over multiple ConfigMap items
• Updated setMetaInfo to use setMultiObjectConfigMetaInfo helper

pkg/model/appconfig.go


28. pkg/model/runtime_test.go Refactoring +15/-15

Update runtime tests to use API abstraction

• Updated import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. throughout test cases
• No changes to test logic or assertions

pkg/model/runtime_test.go


29. pkg/model/secretfiles.go ✨ Enhancement +9/-29

Update secret files handling for multi-object support

• Updated import from api/v1alpha5 to api package
• Updated registerConfig to include nil merge function parameter
• Modified updateAndValidate to handle mount path with three return values (mountPath, subPath,
 fileName)
• Updated setMetaInfo to use setMultiObjectConfigMetaInfo helper

pkg/model/secretfiles.go


30. pkg/model/pvcs.go ✨ Enhancement +8/-21

Update PVC handling with generic naming convention

• Updated import from api/v1alpha5 to api package
• Updated registerConfig to include nil merge function parameter
• Removed PvcsName function (replaced with generic DefaultMultiObjectName)
• Updated updateAndValidate to handle three-value return from getDefConfigMountPath
• Updated setMetaInfo to use setMultiObjectConfigMetaInfo helper

pkg/model/pvcs.go


31. pkg/utils/utils.go Refactoring +3/-64

Clean up utility functions and improve API

• Removed commented-out legacy ReadYamls function implementation
• Renamed readPlatformPatch to ReadPlatformPatch (public) with documentation
• Removed unused template parameter references from comments

pkg/utils/utils.go


32. pkg/model/appconfig_test.go Refactoring +14/-15

Update app-config tests with new naming convention

• Updated import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. throughout test cases
• Updated test backstage name and added Database field
• Updated assertions to use DefaultMultiObjectName for ConfigMap naming

pkg/model/appconfig_test.go


33. internal/controller/backstage_controller.go Refactoring +8/-8

Update controller to use API abstraction layer

• Updated import from api/v1alpha5 to api package
• Replaced all bs. prefixed type references with api. equivalents
• Updated status condition constants to use api. prefix
• No functional changes to reconciliation logic

internal/controller/backstage_controller.go


34. integration_tests/config-refresh_test.go Refactoring +15/-15

Update config refresh tests to use API abstraction

• Updated import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. throughout test cases
• No changes to test logic or assertions

integration_tests/config-refresh_test.go


35. pkg/model/secretenvs.go ✨ Enhancement +7/-24

Update secret environment handling with generic metadata

• Updated import from api/v1alpha5 to api package
• Updated registerConfig to include nil merge function parameter
• Updated setMetaInfo to use setMultiObjectConfigMetaInfo helper instead of inline logic

pkg/model/secretenvs.go


36. integration_tests/route_test.go Refactoring +9/-9

Update route tests with API abstraction

• Updated import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. throughout test cases
• Updated app-config ConfigMap name lookup to use DefaultMultiObjectName

integration_tests/route_test.go


37. pkg/model/secretenvs_test.go Refactoring +14/-14

Update secret environment tests to use API abstraction

• Updated import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. throughout test cases
• No changes to test logic or assertions

pkg/model/secretenvs_test.go


38. pkg/model/db-secret_test.go Refactoring +7/-7

Update database secret tests with API abstraction

• Updated import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. throughout test cases
• Updated withLocalDb() calls to pass boolean parameter true

pkg/model/db-secret_test.go


39. pkg/model/db-statefulset.go Refactoring +5/-10

Update database StatefulSet with API abstraction

• Updated import from api/v1alpha5 to api package
• Updated registerConfig to include nil merge function parameter
• Updated all type references from bsv1. to api. in method signatures

pkg/model/db-statefulset.go


40. pkg/model/dynamic-plugins_test.go Refactoring +8/-17

Update dynamic plugins tests to use API abstraction

• Updated import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. throughout test cases
• No changes to test logic or assertions

pkg/model/dynamic-plugins_test.go


41. integration_tests/db_test.go Refactoring +8/-8

Update database tests with API abstraction

• Updated import from api/v1alpha5 to api package
• Updated all type references from bsv1. to api. throughout test cases
• No changes to test logic or assertions

integration_tests/db_test.go


42. api/current-types.go ✨ Enhancement +60/-0

Add API abstraction layer with type aliases

• New file providing type aliases for API abstraction layer
• Imports from api/v1alpha6 and re-exports as unversioned types
• Includes type aliases for core types, conditions, spec components, and references
• Provides AddToScheme function delegation to underlying versioned API

api/current-types.go


43. integration_tests/suite_test.go Refactoring +6/-6

Update test suite to use API abstraction

• Updated import to use api package instead of api/v1alpha5
• Updated AddToScheme call to use api.AddToScheme instead of bsv1.AddToScheme
• Updated function signatures to use api.BackstageSpec and api.Backstage types

integration_tests/suite_test.go


44. pkg/model/service.go Refactoring +5/-10

Update service handling with API abstraction

• Updated import from api/v1alpha5 to api package
• Updated registerConfig to include nil merge function parameter
• Updated all type references from bsv1. to api. in method signatures

pkg/model/service.go


45. pkg/model/interfaces.go ✨ Enhancement +13/-5

Add flavour merging interface and update type references

• Updated import from api/v1alpha5 to api package
• Added client import from controller-runtime
• Added MergeConfigFunc type definition for flavour config merging
• Added MergeFunc field to ObjectConfig struct for flavour support
• Updated all method signatures to use api. prefixed types

pkg/model/interfaces.go


46. pkg/model/db-secret.go Refactoring +5/-5

Update database secret with API abstraction

• Updated import from api/v1alpha5 to api package
• Updated registerConfig to include nil merge function parameter
• Updated all type references from bsv1. to api. in method signatures

pkg/model/db-secret.go


47. pkg/model/db-service.go Refactoring +5/-5

Update database service with API abstraction

• Updated import from api/v1alpha5 to api package
• Updated registerConfig to include nil merge function parameter
• Updated all type references from bsv1. to api. in method signatures

pkg/model/db-service.go


48. integration_tests/cr-compatibility_test.go Refactoring +7/-7

Update API compatibility tests with abstraction

• Updated import to use api package instead of api/v1alpha5
• Updated all type references from bsv1. to api. throughout test cases
• Maintains backward compatibility testing with v1alpha3

integration_tests/cr-compatibility_test.go


49. integration_tests/testdata/rhdh-replace-dynaplugin-root.yaml ⚙️ Configuration changes +1/-1

Update test data to use v1alpha6 API version

• Updated API version from v1alpha5 to v1alpha6 in test data

integration_tests/testdata/rhdh-replace-dynaplugin-root.yaml


50. internal/controller/monitor_test.go ✨ Enhancement +7/-7

Update API imports and types to use generic api package

• Updated import from github.com/redhat-developer/rhdh-operator/api/v1alpha5 to
 github.com/redhat-developer/rhdh-operator/api
• Changed all type references from bs.Backstage, bs.BackstageSpec, and bs.Monitoring to
 api.Backstage, api.BackstageSpec, and api.Monitoring
• Updated scheme registration calls from bs.AddToScheme to api.AddToScheme

internal/controller/monitor_test.go


51. internal/controller/spec_preprocessor.go ✨ Enhancement +4/-4

Migrate spec preprocessor to use generic api package

• Changed import from bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha5" to
 "github.com/redhat-developer/rhdh-operator/api"
• Updated function signatures and type references from bsv1.Backstage, bsv1.Application, and
 bsv1.FileObjectRef to api.Backstage, api.Application, and api.FileObjectRef

internal/controller/spec_preprocessor.go


52. pkg/model/db-statefulset_test.go ✨ Enhancement +6/-6

Update db-statefulset tests to use generic api package

• Updated import from bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha5" to
 "github.com/redhat-developer/rhdh-operator/api"
• Changed all type references from bsv1.Backstage, bsv1.BackstageSpec, bsv1.Database, and
 bsv1.Application to api.* equivalents
• Fixed test method call from .withLocalDb() to .withLocalDb(true) with boolean parameter

pkg/model/db-statefulset_test.go


53. pkg/model/plugin_deps_test.go ✨ Enhancement +3/-3

Migrate plugin deps tests to generic api package

• Changed import from bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha5" to
 "github.com/redhat-developer/rhdh-operator/api"
• Updated type references from bsv1.Backstage to api.Backstage and bsv1.AddToScheme to
 api.AddToScheme

pkg/model/plugin_deps_test.go


54. integration_tests/plugin-deps_test.go ✨ Enhancement +3/-3

Update integration tests to use generic api package

• Updated import from bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha5" to
 "github.com/redhat-developer/rhdh-operator/api"
• Changed type references from bsv1.BackstageSpec and bsv1.RuntimeConfig to api.BackstageSpec
 and api.RuntimeConfig

integration_tests/plugin-deps_test.go


55. api/v1alpha6/groupversion_info.go ✨ Enhancement +20/-0

Add v1alpha6 API group version definition

• Created new file defining the v1alpha6 API group version
• Establishes GroupVersion schema with group rhdh.redhat.com and version v1alpha6
• Provides SchemeBuilder and AddToScheme function for registering v1alpha6 types

api/v1alpha6/groupversion_info.go


56. internal/controller/watchers.go ✨ Enhancement +2/-2

Update watchers to use generic api package

• Updated import from bs "github.com/redhat-developer/rhdh-operator/api/v1alpha5" to
 "github.com/redhat-developer/rhdh-operator/api"
• Changed type reference from bs.Backstage{} to api.Backstage{}

internal/controller/watchers.go


57. pkg/model/plugin_deps.go ✨ Enhancement +2/-2

Migrate plugin deps to generic api package

• Updated import from bs "github.com/redhat-developer/rhdh-operator/api/v1alpha5" to
 "github.com/redhat-developer/rhdh-operator/api"
• Changed function parameter type from bs.Backstage to api.Backstage in GetPluginDeps function

pkg/model/plugin_deps.go


58. internal/controller/monitor.go ✨ Enhancement +2/-2

Update monitor to use generic api package

• Updated import from bs "github.com/redhat-developer/rhdh-operator/api/v1alpha5" to
 "github.com/redhat-developer/rhdh-operator/api"
• Changed function parameter type from *bs.Backstage to *api.Backstage in applyServiceMonitor
 method

internal/controller/monitor.go


59. internal/controller/plugin-deps.go ✨ Enhancement +2/-2

Update plugin-deps controller to use generic api package

• Updated import from bs "github.com/redhat-developer/rhdh-operator/api/v1alpha5" to
 "github.com/redhat-developer/rhdh-operator/api"
• Changed function parameter type from bs.Backstage to api.Backstage in applyPluginDeps method

internal/controller/plugin-deps.go


60. internal/controller/preprocessor_test.go ✨ Enhancement +1/-1

Update preprocessor tests to v1alpha6 API version

• Updated import from bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha5" to `bsv1
 "github.com/redhat-developer/rhdh-operator/api/v1alpha6"`
• Migrated test to use v1alpha6 API version

internal/controller/preprocessor_test.go


61. .rhdh/scripts/prepare-restricted-environment.sh ⚙️ Configuration changes +1/-1

Update script CR example to v1alpha6

• Updated API version in CR example from rhdh.redhat.com/v1alpha5 to rhdh.redhat.com/v1alpha6

.rhdh/scripts/prepare-restricted-environment.sh


62. .rhdh/scripts/install-rhdh-catalog-source.sh ⚙️ Configuration changes +1/-1

Update catalog source script to v1alpha6

• Updated API version in CR example from rhdh.redhat.com/v1alpha5 to rhdh.redhat.com/v1alpha6

.rhdh/scripts/install-rhdh-catalog-source.sh


63. dist/rhdh/install.yaml ✨ Enhancement +720/-12

Add v1alpha6 CRD with flavours support and update deployment

• Added v1alpha6 CRD version with complete OpenAPI schema including new flavours field
• Marked v1alpha5 as non-storage version (storage: false)
• Added two new ConfigMaps for flavour configurations (lightspeed and orchestrator)
• Updated deployment volumes to mount flavour configuration directories
• Removed orchestrator plugins from default dynamic-plugins configuration

dist/rhdh/install.yaml


64. dist/backstage.io/install.yaml ✨ Enhancement +518/-1

Add v1alpha6 CRD with flavours support

• Added v1alpha6 CRD version with complete OpenAPI schema including new flavours field
• Marked v1alpha5 as non-storage version (storage: false)
• Updated ConfigMap name from my-backstage-config-cm1 to default-appconfig

dist/backstage.io/install.yaml


65. bundle/backstage.io/manifests/rhdh.redhat.com_backstages.yaml ✨ Enhancement +517/-0

Add v1alpha6 CRD to backstage.io bundle

• Added v1alpha6 CRD version with complete OpenAPI schema including new flavours field
• Marked v1alpha5 as non-storage version (storage: false)

bundle/backstage.io/manifests/rhdh.redhat.com_backstages.yaml


66. bundle/rhdh/manifests/rhdh.redhat.com_backstages.yaml ✨ Enhancement +517/-0

Add v1alpha6 CRD to rhdh bundle

• Added v1alpha6 CRD version with complete OpenAPI schema including new flavours field
• Marked v1alpha5 as non-storage version (storage: false)

bundle/rhdh/manifests/rhdh.redhat.com_backstages.yaml


67. bundle/rhdh/manifests/rhdh-flavour-lightspeed-config_v1_configmap.yaml ✨ Enhancement +160/-0

Add lightspeed flavour configuration ConfigMap

• Created new ConfigMap for lightspeed flavour configuration
• Includes llama-stack configuration, deployment specifications, and dynamic plugins
• Provides lightspeed-stack configuration with MCP server integration
• Marked as enabled by default in metadata

bundle/rhdh/manifests/rhdh-flavour-lightspeed-config_v1_configmap.yaml


68. config/profile/rhdh/default-config/flavours/lightspeed/dynamic-plugins.yaml ✨ Enhancement +45/-0

Add lightspeed flavour dynamic plugins configuration

• Created new dynamic plugins configuration for lightspeed flavour
• Includes lightspeed frontend and backend plugins with configuration
• Adds MCP actions and software catalog/techdocs MCP tools

config/profile/rhdh/default-config/flavours/lightspeed/dynamic-plugins.yaml


69. pkg/model/testdata/testflavours-nobase/default-config/flavours/flavor3/metadata.yaml 🧪 Tests +1/-0

Add test flavour metadata configuration

• Created new test flavour metadata file marking flavor3 as disabled by default

pkg/model/testdata/testflavours-nobase/default-config/flavours/flavor3/metadata.yaml


70. .rhdh/docs/airgap.adoc Additional files +1/-1

...

.rhdh/docs/airgap.adoc


71. .sonarcloud.properties Additional files +3/-0

...

.sonarcloud.properties


72. PROJECT Additional files +2/-2

...

PROJECT


73. api/v1alpha5/backstage_types.go Additional files +0/-1

...

api/v1alpha5/backstage_types.go


74. bundle/backstage.io/manifests/backstage-default-config_v1_configmap.yaml Additional files +1/-1

...

bundle/backstage.io/manifests/backstage-default-config_v1_configmap.yaml


75. bundle/backstage.io/manifests/backstage-operator.clusterserviceversion.yaml Additional files +20/-1

...

bundle/backstage.io/manifests/backstage-operator.clusterserviceversion.yaml


76. bundle/rhdh/manifests/backstage-operator.clusterserviceversion.yaml Additional files +30/-1

...

bundle/rhdh/manifests/backstage-operator.clusterserviceversion.yaml


77. bundle/rhdh/manifests/rhdh-default-config_v1_configmap.yaml Additional files +1/-12

...

bundle/rhdh/manifests/rhdh-default-config_v1_configmap.yaml


78. bundle/rhdh/manifests/rhdh-flavour-orchestrator-config_v1_configmap.yaml Additional files +28/-0

...

bundle/rhdh/manifests/rhdh-flavour-orchestrator-config_v1_configmap.yaml


79. config/crd/bases/rhdh.redhat.com_backstages.yaml Additional files +517/-0

...

config/crd/bases/rhdh.redhat.com_backstages.yaml


80. config/manifests/backstage.io/bases/backstage-operator.clusterserviceversion.yaml Additional files +9/-0

...

config/manifests/backstage.io/bases/backstage-operator.clusterserviceversion.yaml


81. config/manifests/rhdh/bases/backstage-operator.clusterserviceversion.yaml Additional files +9/-0

...

config/manifests/rhdh/bases/backstage-operator.clusterserviceversion.yaml


82. config/profile/backstage.io/default-config/app-config.yaml Additional files +1/-1

...

config/profile/backstage.io/default-config/app-config.yaml


83. config/profile/rhdh/default-config/app-config.yaml Additional files +1/-1

...

config/profile/rhdh/default-config/app-config.yaml


84. config/profile/rhdh/default-config/dynamic-plugins.yaml Additional files +0/-11

...

config/profile/rhdh/default-config/dynamic-plugins.yaml


85. config/profile/rhdh/default-config/flavours/lightspeed/configmap-files.yaml Additional files +169/-0

...

config/profile/rhdh/default-config/flavours/lightspeed/configmap-files.yaml


86. config/profile/rhdh/default-config/flavours/lightspeed/deployment.yaml Additional files +46/-0

...

config/profile/rhdh/default-config/flavours/lightspeed/deployment.yaml


87. config/profile/rhdh/default-config/flavours/lightspeed/metadata.yaml Additional files +3/-0

...

config/profile/rhdh/default-config/flavours/lightspeed/metadata.yaml


88. config/profile/rhdh/default-config/flavours/orchestrator/dynamic-plugins.yaml Additional files +17/-0

...

config/profile/rhdh/default-config/flavours/orchestrator/dynamic-plugins.yaml


89. config/profile/rhdh/default-config/flavours/orchestrator/metadata.yaml Additional files +4/-0

...

config/profile/rhdh/default-config/flavours/orchestrator/metadata.yaml


90. config/profile/rhdh/kustomization.yaml Additional files +17/-0

...

config/profile/rhdh/kustomization.yaml


91. config/profile/rhdh/patches/deployment-patch.yaml Additional files +12/-1

...

config/profile/rhdh/patches/deployment-patch.yaml


92. config/samples/_v1alpha6_backstage.yaml Additional files +8/-0

...

config/samples/_v1alpha6_backstage.yaml


93. config/samples/kustomization.yaml Additional files +1/-0

...

config/samples/kustomization.yaml


94. docs/configuration.md Additional files +17/-17

...

docs/configuration.md


95. docs/dynamic-plugins.md Additional files +1/-1

...

docs/dynamic-plugins.md


96. docs/external-db.md Additional files +1/-1

...

docs/external-db.md


97. docs/monitoring.md Additional files +5/-5

...

docs/monitoring.md


98. examples/bs-route-disabled.yaml Additional files +1/-1

...

examples/bs-route-disabled.yaml


99. examples/bs-route.yaml Additional files +1/-1

...

examples/bs-route.yaml


100. examples/bs1.yaml Additional files +1/-1

...

examples/bs1.yaml


101. examples/catalog-index.yaml Additional files +1/-1

...

examples/catalog-index.yaml


102. examples/envvars.yaml Additional files +1/-1

...

examples/envvars.yaml


103. examples/filemounts.yaml Additional files +1/-1

...

examples/filemounts.yaml


104. examples/lightspeed-disabled.yaml Additional files +8/-0

...

examples/lightspeed-disabled.yaml


105. examples/lightspeed.yaml Additional files +31/-0

...

examples/lightspeed.yaml


106. examples/local-bs.yaml Additional files +27/-0

...

examples/local-bs.yaml


107. examples/orchestrator-cicd.yaml Additional files +1/-1

...

<a href='https://github.com/redhat-developer/rhdh-operator/pull/2415/files#di...

@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge bot commented Mar 2, 2026

PR Code Suggestions ✨

Latest suggestions up to 1f788ec

CategorySuggestion                                                                                                                                    Impact
Incremental [*]
Persist preprocessing spec mutations

Modify the preprocessSpec function to accept a pointer to api.Backstage instead
of a value. This ensures that any mutations made to the backstage.Spec within
the function are persisted and visible to the caller.

internal/controller/spec_preprocessor.go [37-79]

-func (r *BackstageReconciler) preprocessSpec(ctx context.Context, backstage api.Backstage) (model.ExternalConfig, error) {
+func (r *BackstageReconciler) preprocessSpec(ctx context.Context, backstage *api.Backstage) (model.ExternalConfig, error) {
 	//lg := log.FromContext(ctx)
 
-	bsSpec := backstage.Spec
+	bsSpec := &backstage.Spec
 ...
 	if bsSpec.Application == nil {
 		bsSpec.Application = &api.Application{}
 	}

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a critical bug where spec modifications are lost due to pass-by-value, which would lead to incorrect runtime behavior or panics.

High
Pass scheme by pointer

*Update the MergeConfigFunc type definition to accept a pointer to runtime.Scheme
(runtime.Scheme) instead of passing it by value. This aligns with standard
Kubernetes/controller-runtime practices and prevents potential bugs.

pkg/model/interfaces.go [12]

-type MergeConfigFunc func(sources []configSource, scheme runtime.Scheme, platformExt string) ([]client.Object, error)
+type MergeConfigFunc func(sources []configSource, scheme *runtime.Scheme, platformExt string) ([]client.Object, error)

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that passing runtime.Scheme by value is a bug that can lead to runtime errors and performance issues, and proposes the idiomatic fix.

High
Preserve unset vs false semantics

*Change the Enabled field in the Flavour struct to a pointer (bool) to correctly
distinguish between an unset value (defaulting to true) and an explicitly set
false value.

api/v1alpha6/backstage_types.go [355-366]

 type Flavour struct {
 	// Name of the flavour to enable (e.g., "orchestrator", "lightspeed")
 	// +kubebuilder:validation:Required
 	Name string `json:"name"`
 
 	// Enabled controls whether this flavour is active.
 	// Defaults to true when not specified.
 	// Set to false to explicitly disable a flavour (including default flavours).
 	// +optional
 	// +kubebuilder:default=true
-	Enabled bool `json:"enabled,omitempty"`
+	Enabled *bool `json:"enabled,omitempty"`
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential for bugs due to using a non-pointer bool for an optional field, which can lead to misinterpretation of unset values. Proposing a change to *bool aligns with idiomatic Kubernetes API design and enhances robustness.

Medium
Make merge order deterministic

Ensure a deterministic order when collecting enabled flavours to prevent
inconsistent configuration merges. Iterate based on spec.Flavours if provided;
otherwise, sort by flavour name.

pkg/model/flavour.go [61-69]

-// Step 3: Collect enabled flavours
+// Step 3: Collect enabled flavours (deterministic order)
 var result []enabledFlavour
-for name, flavour := range allFlavours {
-	if flavour.enabled {
-		result = append(result, enabledFlavour{
-			name:     name,
-			basePath: flavour.basePath,
-		})
+
+if spec.Flavours != nil {
+	for _, f := range spec.Flavours {
+		fi, exists := allFlavours[f.Name]
+		if !exists {
+			return nil, fmt.Errorf("flavour '%s' not found in %s", f.Name, flavoursDir)
+		}
+		if fi.enabled {
+			result = append(result, enabledFlavour{name: f.Name, basePath: fi.basePath})
+		}
+	}
+	return result, nil
+}
+
+names := make([]string, 0, len(allFlavours))
+for name := range allFlavours {
+	names = append(names, name)
+}
+sort.Strings(names)
+
+for _, name := range names {
+	fi := allFlavours[name]
+	if fi.enabled {
+		result = append(result, enabledFlavour{name: name, basePath: fi.basePath})
 	}
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that iterating over a map (allFlavours) results in a non-deterministic order for flavours, which can lead to inconsistent configuration merging and unpredictable behavior. This is a significant correctness issue.

Medium
Avoid unsafe type assertions

*Replace the unsafe type assertion in the setObject function with a safe, checked
value, ok assertion. This will prevent a potential panic if the runtime object
is not of the expected multiobject.MultiObject type.

pkg/model/appconfig.go [52-57]

 func (b *AppConfig) setObject(obj runtime.Object) {
 	b.ConfigMaps = nil
-	if obj != nil {
-		b.ConfigMaps = obj.(*multiobject.MultiObject)
+	if obj == nil {
+		return
+	}
+	if mo, ok := obj.(*multiobject.MultiObject); ok {
+		b.ConfigMaps = mo
 	}
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies an unsafe type assertion that could cause the operator to panic. Replacing it with a safe, checked assertion is a critical improvement for the robustness of the controller.

Medium
Fail fast on read errors

Modify collectConfigSources to return an error if os.ReadFile fails. This change
prevents silently ignoring file read errors, ensuring that issues like incorrect
permissions cause the reconciliation to fail fast.

pkg/model/default-config.go [56-85]

-func collectConfigSources(key string, basePath string, flavours []enabledFlavour) []configSource {
+func collectConfigSources(key string, basePath string, flavours []enabledFlavour) ([]configSource, error) {
 	var sources []configSource
 
 	// Read base config if it exists
 	if _, err := os.Stat(basePath); err == nil {
-		if content, err := os.ReadFile(basePath); err == nil {
-			sources = append(sources, configSource{
-				path:        basePath,
-				flavourName: "", // empty for base
-				content:     content,
-			})
+		content, err := os.ReadFile(basePath)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read base config %s: %w", basePath, err)
 		}
+		sources = append(sources, configSource{
+			path:        basePath,
+			flavourName: "", // empty for base
+			content:     content,
+		})
 	}
 
 	// Read each flavour config if it exists
 	for _, flavour := range flavours {
 		flavourConfigPath := filepath.Join(flavour.basePath, key)
 		if _, err := os.Stat(flavourConfigPath); err == nil {
-			if content, err := os.ReadFile(flavourConfigPath); err == nil {
-				sources = append(sources, configSource{
-					path:        flavourConfigPath,
-					flavourName: flavour.name,
-					content:     content,
-				})
+			content, err := os.ReadFile(flavourConfigPath)
+			if err != nil {
+				return nil, fmt.Errorf("failed to read flavour config %s: %w", flavourConfigPath, err)
 			}
+			sources = append(sources, configSource{
+				path:        flavourConfigPath,
+				flavourName: flavour.name,
+				content:     content,
+			})
 		}
 	}
 
-	return sources
+	return sources, nil
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: This is a valid and important improvement to error handling. Silently ignoring file read errors can lead to hard-to-debug issues with configuration, and failing fast is a much more robust approach.

Medium
Fix empty array behaviour docs
Suggestion Impact:The documentation comment for spec.flavours was updated to indicate that an empty array disables default flavours, partially aligning with the suggested wording.

code diff:

 	// If not specified (nil), flavours with enabledByDefault: true in their metadata are used.
-	// If specified as empty array ([]), default flavours are still used.
+	// If specified as empty array ([]), default flavours are disabled.
 	// To disable a default flavour, explicitly list it with enabled: false.

Update the comment for spec.flavours to correctly document that an empty array
disables all flavours, aligning the documentation with the tested behavior.

api/v1alpha6/backstage_types.go [47-49]

 // If not specified (nil), flavours with enabledByDefault: true in their metadata are used.
-// If specified as empty array ([]), default flavours are still used.
-// To disable a default flavour, explicitly list it with enabled: false.
+// If specified as an empty array ([]), no flavours are enabled (including defaults).
+// To disable a default flavour while still using other defaults/explicit flavours, explicitly list it with enabled: false.

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a discrepancy between the API documentation comment and the implementation's behavior as verified by the new tests, which is crucial for preventing user confusion.

Low
Possible issue
Apply platform patches during merges

Modify configSource to include platform-specific patch content. Update
collectConfigSources to populate this field and ensure merge functions like
mergeDynamicPlugins and mergeMultiObjectConfigs apply these patches when parsing
YAMLs.

pkg/model/default-config.go [98-147]

 type configSource struct {
-	path        string
-	flavourName string // empty string for base config
-	content     []byte // pre-read YAML content
+	path          string
+	flavourName   string // empty string for base config
+	content       []byte // pre-read YAML content
+	platformPatch []byte // optional platform-specific patch content
+}
+
+func collectConfigSources(key string, basePath string, flavours []enabledFlavour, platformExt string) []configSource {
+	var sources []configSource
+
+	// Read base config if it exists
+	if _, err := os.Stat(basePath); err == nil {
+		if content, err := os.ReadFile(basePath); err == nil {
+			pp, _ := utils.ReadPlatformPatch(basePath, platformExt)
+			sources = append(sources, configSource{
+				path:          basePath,
+				flavourName:   "", // empty for base
+				content:       content,
+				platformPatch: pp,
+			})
+		}
+	}
+
+	// Read each flavour config if it exists
+	for _, flavour := range flavours {
+		flavourConfigPath := filepath.Join(flavour.basePath, key)
+		if _, err := os.Stat(flavourConfigPath); err == nil {
+			if content, err := os.ReadFile(flavourConfigPath); err == nil {
+				pp, _ := utils.ReadPlatformPatch(flavourConfigPath, platformExt)
+				sources = append(sources, configSource{
+					path:          flavourConfigPath,
+					flavourName:   flavour.name,
+					content:       content,
+					platformPatch: pp,
+				})
+			}
+		}
+	}
+
+	return sources
 }
 
 func mergeDynamicPlugins(sources []configSource, scheme runtime.Scheme, _ string) ([]client.Object, error) {
 ...
 	for _, src := range sources {
-		objs, err := utils.ReadYamls(src.content, nil, scheme)
+		objs, err := utils.ReadYamls(src.content, src.platformPatch, scheme)
 		if err != nil {
 			return nil, fmt.Errorf("failed to parse dynamic-plugins.yaml from %s: %w", src.path, err)
 		}
 ...
 	}
 ...
 }
 
 func mergeMultiObjectConfigs(sources []configSource, scheme runtime.Scheme, _ string) ([]client.Object, error) {
 ...
 	for _, src := range sources {
-		objs, err := utils.ReadYamls(src.content, nil, scheme)
+		objs, err := utils.ReadYamls(src.content, src.platformPatch, scheme)
 		if err != nil {
 			return nil, fmt.Errorf("failed to parse config from %s: %w", src.path, err)
 		}
 ...
 	}
 
 	return allObjects, nil
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a regression bug where platform-specific configurations are ignored during the new flavour-based config merging, which could lead to incorrect deployments.

High
Prevent cross-flavour name collisions

Update setMultiObjectConfigMetaInfo to include the flavour name from the
SourceAnnotation in the generated Kubernetes object name. This will prevent name
collisions between objects that are defined in different flavours.

pkg/model/default-config.go [167-172]

 func DefaultMultiObjectName(objectType, backstageName, objectName string) string {
 	return "backstage-" + objectType + "-" + backstageName + "-" + objectName
 }
 
 func setMultiObjectConfigMetaInfo(mo *multiobject.MultiObject, objectType string, backstage api.Backstage, scheme *runtime.Scheme) {
 	for _, item := range mo.Items {
-		utils.AddAnnotation(item, ConfiguredNameAnnotation, item.GetName())
-		item.SetName(DefaultMultiObjectName(objectType, backstage.Name, item.GetName()))
+		origName := item.GetName()
+		utils.AddAnnotation(item, ConfiguredNameAnnotation, origName)
+
+		name := DefaultMultiObjectName(objectType, backstage.Name, origName)
+		if src := item.GetAnnotations()[SourceAnnotation]; src != "" && src != "default" {
+			name = name + "-" + src
+		}
+
+		item.SetName(name)
 		setMetaInfo(item, backstage, scheme)
 	}
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical bug where objects from different flavours can have name collisions, leading to one overwriting the other, which undermines the new flavour feature.

High
Avoid panic on type mismatch

*In updateAppConfigWithBaseUrls, replace the unsafe type assertion for
corev1.ConfigMap with a safe one (cm, ok := ...) and add a check to handle
cases where the type is incorrect, preventing a potential panic.

pkg/model/route.go [199-208]

 // Update only the first ConfigMap (base config) with baseUrl
-cm := m.appConfig.ConfigMaps.Items[0].(*corev1.ConfigMap)
+cm, ok := m.appConfig.ConfigMaps.Items[0].(*corev1.ConfigMap)
+if !ok || cm == nil {
+	klog.V(1).Infof(
+		"First app-config item is not a ConfigMap - skipping automatic population of base URLS in the default app-config for Backstage %s",
+		backstage.Name)
+	return
+}
+
 for k, v := range cm.Data {
 	updated, err := updateFn(v)
 	if err != nil {
 		klog.V(1).Infof("[warn] could not update base url in default app-config %q for backstage %s: %v",
 			k, backstage.Name, err)
 		continue
 	}
 	cm.Data[k] = updated
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion prevents a potential panic from an unsafe type assertion, which would crash the controller, thus making the operator more robust against malformed configuration.

Medium
  • Update

Previous suggestions

✅ Suggestions up to commit 9523adc
CategorySuggestion                                                                                                                                    Impact
High-level
Flavour discovery relies on filesystem path

The flavour discovery mechanism should not rely on the LOCALBIN environment
variable and a filesystem path. Instead, it should use a more robust method like
embedding flavour configurations into the operator binary using Go's embed
feature.

Examples:

pkg/model/flavour.go [36-37]

Solution Walkthrough:

Before:

// pkg/model/flavour.go

func GetEnabledFlavours(spec api.BackstageSpec) ([]enabledFlavour, error) {
    // Relies on an environment variable to find the flavours directory.
    localBin := os.Getenv("LOCALBIN")
    flavoursDir := filepath.Join(localBin, "default-config", "flavours")

    // Reads from the filesystem path, which might fail silently if LOCALBIN is wrong.
    defaults, err := getDefaultFlavours(flavoursDir)
    if err != nil {
        return nil, err
    }
    // ... more logic using flavoursDir
    return mergeWithDefaults(spec.Flavours, defaults, flavoursDir)
}

After:

// pkg/model/flavour.go
import "embed"

//go:embed default-config/flavours
var flavoursFS embed.FS

func GetEnabledFlavours(spec api.BackstageSpec) ([]enabledFlavour, error) {
    // Path is now relative to the embedded filesystem, not an external variable.
    flavoursDir := "default-config/flavours"

    // Reads from the embedded filesystem, making it self-contained and reliable.
    defaults, err := getDefaultFlavours(flavoursDir)
    if err != nil {
        return nil, err
    }
    // ...
    return mergeWithDefaults(spec.Flavours, defaults, flavoursDir)
}
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a significant design flaw in the new flavour discovery mechanism, which relies on a fragile environment variable (LOCALBIN) and filesystem path, proposing robust, standard alternatives.

Medium
General
Enable flavour‐aware plugin merging

Change the flavour merge policy for dynamic-plugins.yaml from
FlavourMergePolicyNoFlavour to FlavourMergePolicyArrayMerge to enable merging of
plugins across different flavours.

pkg/model/runtime.go [64]

 func init() {
-    registerConfig("dynamic-plugins.yaml", DynamicPluginsFactory{}, false, FlavourMergePolicyNoFlavour)
+    registerConfig("dynamic-plugins.yaml", DynamicPluginsFactory{}, false, FlavourMergePolicyArrayMerge)
 }
Suggestion importance[1-10]: 8

__

Why: This suggestion corrects a significant configuration error, enabling the newly introduced "flavours" feature to correctly apply to dynamic plugins, which is a key part of the PR's intent.

Medium
Use version-agnostic API import alias
Suggestion Impact:The commit updated the import from api/v1alpha6 to api and adjusted related type references from bsv1.Backstage to api.Backstage accordingly.

code diff:

@@ -5,7 +5,7 @@
 
 	"k8s.io/apimachinery/pkg/runtime"
 
-	bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha6"
+	"github.com/redhat-developer/rhdh-operator/api"
 	"github.com/redhat-developer/rhdh-operator/pkg/utils"
 
 	corev1 "k8s.io/api/core/v1"
@@ -43,7 +43,7 @@
 }
 
 // implementation of RuntimeObject interface
-func (b *BackstageService) addToModel(model *BackstageModel, _ bsv1.Backstage) (bool, error) {
+func (b *BackstageService) addToModel(model *BackstageModel, _ api.Backstage) (bool, error) {
 	b.model = model
 	if b.service == nil {
 		return false, fmt.Errorf("backstage Service is not initialized, make sure there is service.yaml in default or raw configuration")
@@ -56,11 +56,11 @@
 }
 
 // implementation of RuntimeObject interface
-func (b *BackstageService) updateAndValidate(_ bsv1.Backstage) error {
+func (b *BackstageService) updateAndValidate(_ api.Backstage) error {
 	return nil
 }
 
-func (b *BackstageService) setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) {
+func (b *BackstageService) setMetaInfo(backstage api.Backstage, scheme *runtime.Scheme) {
 	b.service.SetName(ServiceName(backstage.Name))
 	utils.GenerateLabel(&b.service.Spec.Selector, BackstageAppLabel, utils.BackstageAppLabelValue(backstage.Name))
 	setMetaInfo(b.service, backstage, scheme)

Replace the version-specific import bsv1
"github.com/redhat-developer/rhdh-operator/api/v1alpha6" with the
version-agnostic alias api "github.com/redhat-developer/rhdh-operator/api".

pkg/model/service.go [6-12]

 import (
 	"k8s.io/apimachinery/pkg/runtime"
 
-	bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha6"
+	"github.com/redhat-developer/rhdh-operator/api"
 	"github.com/redhat-developer/rhdh-operator/pkg/utils"
 
 	corev1 "k8s.io/api/core/v1"
 )
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly points out an inconsistent import that goes against the PR's goal of using a version-agnostic API alias, improving code consistency and maintainability.

Low
Switch to alias API import
Suggestion Impact:Updated the import from github.com/redhat-developer/rhdh-operator/api/v1alpha6 to github.com/redhat-developer/rhdh-operator/api and adjusted the Backstage type reference in GetPluginDeps to use api.Backstage.

code diff:

@@ -6,7 +6,7 @@
 	"path/filepath"
 	"strings"
 
-	bs "github.com/redhat-developer/rhdh-operator/api/v1alpha6"
+	"github.com/redhat-developer/rhdh-operator/api"
 	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
@@ -15,7 +15,7 @@
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 )
 
-func GetPluginDeps(backstage bs.Backstage, plugins DynamicPlugins, scheme *runtime.Scheme) ([]*unstructured.Unstructured, error) {
+func GetPluginDeps(backstage api.Backstage, plugins DynamicPlugins, scheme *runtime.Scheme) ([]*unstructured.Unstructured, error) {
 

Replace the direct import of the versioned API api/v1alpha6 with the central
alias package github.com/redhat-developer/rhdh-operator/api.

pkg/model/plugin_deps.go [6-12]

 import (
 	"path/filepath"
 	"strings"
 
-	bs "github.com/redhat-developer/rhdh-operator/api/v1alpha6"
+	api "github.com/redhat-developer/rhdh-operator/api"
 	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 )
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies an inconsistent import that deviates from the PR's refactoring pattern, and fixing it improves code consistency and maintainability.

Low
Import central API alias
Suggestion Impact:The commit switched the import from the versioned v1alpha6 API to the central api package and updated type references from bsv1.* to api.* (e.g., Backstage, Application, FileObjectRef).

code diff:

@@ -24,7 +24,7 @@
 
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha6"
+	"github.com/redhat-developer/rhdh-operator/api"
 
 	"github.com/redhat-developer/rhdh-operator/pkg/model"
 
@@ -34,7 +34,7 @@
 
 // Add additional details to the Backstage Spec helping in making Backstage RuntimeObjects Model
 // Validates Backstage Spec and fails fast if something not correct
-func (r *BackstageReconciler) preprocessSpec(ctx context.Context, backstage bsv1.Backstage) (model.ExternalConfig, error) {
+func (r *BackstageReconciler) preprocessSpec(ctx context.Context, backstage api.Backstage) (model.ExternalConfig, error) {
 	//lg := log.FromContext(ctx)
 
 	bsSpec := backstage.Spec
@@ -75,7 +75,7 @@
 	}
 
 	if bsSpec.Application == nil {
-		bsSpec.Application = &bsv1.Application{}
+		bsSpec.Application = &api.Application{}
 	}
 
 	// Process AppConfigs
@@ -222,7 +222,7 @@
 	return nil
 }
 
-func addToWatch(fileObjectRef bsv1.FileObjectRef) bool {
+func addToWatch(fileObjectRef api.FileObjectRef) bool {
 	// it will contain subPath either as specified key or as a list of all keys if only mountPath specified

Replace the versioned API import bsv1
"github.com/redhat-developer/rhdh-operator/api/v1alpha6" with the central alias
api "github.com/redhat-developer/rhdh-operator/api".

internal/controller/spec_preprocessor.go [24-30]

 import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha6"
+	api "github.com/redhat-developer/rhdh-operator/api"
 
 	"github.com/redhat-developer/rhdh-operator/pkg/model"
 )
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies an inconsistent import that deviates from the PR's refactoring pattern, and fixing it improves code consistency and maintainability.

Low
Possible issue
Align implementation with API documentation
Suggestion Impact:GetEnabledFlavours was refactored, and now explicitly returns an empty enabled flavour list when spec.Flavours is provided but has length 0, allowing users to disable all flavours as documented.

code diff:

+	if spec.Flavours != nil {
 
-// mergeWithDefaults merges spec.Flavours with default flavours
-// Order: spec flavours (if enabled) first, then defaults not in spec
-func mergeWithDefaults(specFlavours []api.Flavour, defaults []enabledFlavour, flavoursDir string) ([]enabledFlavour, error) {
-	var result []enabledFlavour
-	seen := make(map[string]bool)
-
-	// First, process spec flavours in order
-	for _, f := range specFlavours {
-		seen[f.Name] = true
-
-		// Skip disabled flavours (even if they're default)
-		if !f.Enabled {
-			continue
+		// No specified returns empty list
+		if len(spec.Flavours) == 0 {
+			return make([]enabledFlavour, 0), nil
 		}

Modify GetEnabledFlavours to handle an empty spec.Flavours slice by returning no
flavours, aligning the function's behavior with its API documentation.

pkg/model/flavour.go [35-49]

 func GetEnabledFlavours(spec api.BackstageSpec) ([]enabledFlavour, error) {
 	localBin := os.Getenv("LOCALBIN")
 	flavoursDir := filepath.Join(localBin, "default-config", "flavours")
 
 	// Get all default flavours
 	defaults, err := getDefaultFlavours(flavoursDir)
 	if err != nil {
 		return nil, err
-	} else if spec.Flavours == nil {
+	}
+
+	if spec.Flavours == nil {
 		return defaults, nil
+	}
+
+	// Empty array disables all flavours
+	if len(spec.Flavours) == 0 {
+		return []enabledFlavour{}, nil
 	}
 
 	// Merge spec.Flavours with defaults
 	return mergeWithDefaults(spec.Flavours, defaults, flavoursDir)
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a functional bug where the implementation contradicts the API documentation, preventing users from disabling all flavours as intended.

Medium
Prevent panic and incorrect metadata
Suggestion Impact:mergeDynamicPlugins was refactored and now explicitly handles the “no objects found” case by returning an error instead of risking a panic when indexing objs[0]. However, it does not implement the suggested behavior of skipping empty files, does not preserve base ConfigMap metadata (still overwrites resultConfigMap each iteration), and still errors on missing DynamicPluginsFile key rather than skipping.

code diff:

 // mergeDynamicPlugins merges dynamic-plugins.yaml files by package name
 // Later entries override earlier entries with the same package name
-func mergeDynamicPlugins(paths []string, scheme runtime.Scheme, platformExt string) ([]client.Object, error) {
+func mergeDynamicPlugins(sources []configSource, scheme runtime.Scheme, platformExt string) ([]client.Object, error) {
 
-	if len(paths) == 0 {
+	if len(sources) == 0 {
 		return []client.Object{}, nil
 	}
 
 	var resultConfigMap *corev1.ConfigMap
 	var mergedData string
 
-	for i := 0; i < len(paths); i++ {
-		path := paths[i]
+	for _, src := range sources {
+		objs, err := utils.ReadYamls(src.content, nil, scheme)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse dynamic-plugins.yaml from %s: %w", src.path, err)
+		}
 
-		objs, err := utils.ReadYamlFiles(path, scheme, platformExt)
-		if err != nil {
-			return nil, fmt.Errorf("failed to read dynamic-plugins.yaml from %s: %w", path, err)
+		if len(objs) == 0 {
+			return nil, fmt.Errorf("no objects found in %s", src.path)
 		}
 
 		// single object expected
 		configMap, ok := objs[0].(*corev1.ConfigMap)
 		if !ok {
-			return nil, fmt.Errorf("no ConfigMap found in %s", path)
+			return nil, fmt.Errorf("no ConfigMap found in %s", src.path)
 		}
 
 		data, ok := configMap.Data[DynamicPluginsFile]
 		if !ok {
-			return nil, fmt.Errorf("no %s key found in ConfigMap from %s", DynamicPluginsFile, path)
+			return nil, fmt.Errorf("no %s key found in ConfigMap from %s", DynamicPluginsFile, src.path)
 		}
 
 		mergedData, err = MergePluginsData(mergedData, data)
 		if err != nil {
-			return nil, fmt.Errorf("failed to merge dynamic-plugins from %s: %w", path, err)
+			return nil, fmt.Errorf("failed to merge dynamic-plugins from %s: %w", src.path, err)
 		}
 		resultConfigMap = configMap
 	}
@@ -152,21 +139,16 @@

Refactor mergeDynamicPlugins to prevent a panic on empty YAML files and to
correctly preserve the metadata from the base ConfigMap during a merge.

pkg/model/default-config.go [102-141]

 func mergeDynamicPlugins(paths []string, scheme runtime.Scheme, platformExt string) ([]client.Object, error) {
 
 	if len(paths) == 0 {
 		return []client.Object{}, nil
 	}
 
 	var resultConfigMap *corev1.ConfigMap
 	var mergedData string
 
-	for i := 0; i < len(paths); i++ {
-		path := paths[i]
-
+	for i, path := range paths {
 		objs, err := utils.ReadYamlFiles(path, scheme, platformExt)
 		if err != nil {
 			return nil, fmt.Errorf("failed to read dynamic-plugins.yaml from %s: %w", path, err)
+		}
+
+		if len(objs) == 0 {
+			continue // Skip empty files
 		}
 
 		// single object expected
 		configMap, ok := objs[0].(*corev1.ConfigMap)
 		if !ok {
 			return nil, fmt.Errorf("no ConfigMap found in %s", path)
 		}
 
+		if i == 0 {
+			resultConfigMap = configMap
+		}
+
 		data, ok := configMap.Data[DynamicPluginsFile]
 		if !ok {
-			return nil, fmt.Errorf("no %s key found in ConfigMap from %s", DynamicPluginsFile, path)
+			// It's valid for a configmap to not have the key, just skip it
+			continue
 		}
 
 		mergedData, err = MergePluginsData(mergedData, data)
 		if err != nil {
 			return nil, fmt.Errorf("failed to merge dynamic-plugins from %s: %w", path, err)
 		}
-		resultConfigMap = configMap
+	}
+
+	if resultConfigMap == nil {
+		return []client.Object{}, nil
 	}
 
 	// Update the ConfigMap with merged data
 	resultConfigMap.Data[DynamicPluginsFile] = mergedData
 
 	return []client.Object{resultConfigMap}, nil
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential panic if a YAML file is empty and a logic error where the final ConfigMap metadata would be incorrect, improving both robustness and correctness.

Medium
Use correct flavour merge policy
Suggestion Impact:The commit changed the app-config.yaml registration away from FlavourMergePolicyNoFlavour and refactored AppConfig to use a multi-object approach (MultiObject with a multi-object merge function), aligning with the intended multi-object flavour merge behavior, though implemented via a different API/signature than the suggested constant swap.

code diff:

 func init() {
-	registerConfig("app-config.yaml", AppConfigFactory{}, false, FlavourMergePolicyNoFlavour)
+	registerConfig("app-config.yaml", AppConfigFactory{}, true, mergeMultiObjectConfigs)
 }

Change the flavour merge policy for app-config.yaml from
FlavourMergePolicyNoFlavour to FlavourMergePolicyMultiObject to align with the
design specified in pkg/model/interfaces.go.

pkg/model/appconfig.go [29-31]

 func init() {
-	registerConfig("app-config.yaml", AppConfigFactory{}, false, FlavourMergePolicyNoFlavour)
+	registerConfig("app-config.yaml", AppConfigFactory{}, false, FlavourMergePolicyMultiObject)
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies an inconsistency between the implementation and the design comments for the new 'Flavours' feature, which could be a potential bug.

Medium

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

github-actions bot commented Mar 3, 2026

⚠️ 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.

gazarenkov and others added 2 commits March 25, 2026 16:44
Co-authored-by: gazarenkov <gazarenkov@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
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.

# Conflicts:
#	integration_tests/default-config_test.go
#	internal/controller/backstage_status.go
Copy link
Copy Markdown
Contributor

@gabemontero gabemontero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool deal @gazarenkov thanks for removing the kube-api-access item !!

@thepetk @Jdubrick FYI

@gabemontero
Copy link
Copy Markdown
Contributor

cool deal @gazarenkov thanks for removing the kube-api-access item !!

@thepetk @Jdubrick FYI

I'm booting up a cluster and will sanity check this shortly

@gabemontero
Copy link
Copy Markdown
Contributor

gabemontero commented Mar 25, 2026

cool deal @gazarenkov thanks for removing the kube-api-access item !!
@thepetk @Jdubrick FYI

I'm booting up a cluster and will sanity check this shortly

testing looks good @gazarenkov @thepetk @Jdubrick

  • default / empty backstage instance: sidecars up OK, essentially inert
  • with llama stack secret, sidecars up OK, in theory ready to go
  • flavor disabled, no sidecars

install dynamic plugins also does what is expected in all 3 scenarios ^^

@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge bot commented Mar 26, 2026

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (1) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. deployment.yaml omits spec.replicas 📘 Rule violation ⚙ Maintainability
Description
This new Deployment manifest omits spec.replicas without an adjacent comment stating replicas
are managed by autoscaling/another controller. This violates the requirement to document intentional
replicas omission to avoid ambiguous scaling behavior.
Code

pkg/model/testdata/testflavours/default-config/deployment.yaml[R1-5]

+apiVersion: apps/v1
+kind: Deployment
+spec:
+  template:
+    spec:
Evidence
PR Compliance ID 9 requires that when a workload supporting spec.replicas omits it, there must be
a nearby comment explaining that autoscaling/automatic replica management is responsible. The added
Deployment YAML contains spec: and template: but no replicas: and no such comment.

Rule 9: Document omitted spec.replicas when relying on autoscaling
pkg/model/testdata/testflavours/default-config/deployment.yaml[1-5]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A newly added `Deployment` manifest omits `spec.replicas` but does not include an adjacent comment stating replicas are intentionally omitted because autoscaling (or another controller) manages them.

## Issue Context
Compliance requires an explicit indication when `spec.replicas` is omitted for autoscaling/automatic replica management, otherwise readers cannot tell whether the omission is intentional.

## Fix Focus Areas
- pkg/model/testdata/testflavours/default-config/deployment.yaml[1-5]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Flavour merge order unstable 🐞 Bug ✓ Correctness
Description
GetEnabledFlavours builds the enabled flavour list by iterating a Go map, producing
nondeterministic ordering. Since default configs are merged in flavour slice order and the API/CRD
promises “merged in the order specified”, override precedence can change between reconciles.
Code

pkg/model/flavour.go[R60-69]

+	// Step 3: Collect enabled flavours
+	var result []enabledFlavour
+	for name, flavour := range allFlavours {
+		if flavour.enabled {
+			result = append(result, enabledFlavour{
+				name:     name,
+				basePath: flavour.basePath,
+			})
+		}
+	}
Evidence
The API/CRD says flavour configs are merged in the order specified, and collectConfigSources
claims flavours are in spec order, but GetEnabledFlavours ranges over allFlavours map when
collecting enabled flavours, which has undefined iteration order in Go.

api/v1alpha6/backstage_types.go[47-53]
pkg/model/default-config.go[54-56]
pkg/model/flavour.go[60-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Enabled flavour ordering is nondeterministic because the code ranges over a map. This breaks the “merge in order specified” contract and can flip override precedence between reconciles.

### Issue Context
Config merging in `ReadDefaultConfig` depends on the order of the `flavours []enabledFlavour` slice.

### Fix Focus Areas
- pkg/model/flavour.go[50-71]
- pkg/model/default-config.go[54-85]
- pkg/model/default-config_test.go[229-246]

### What to change
- Build `[]enabledFlavour` in a deterministic order:
 - When spec provides an explicit list, emit enabled flavours in **spec order**.
 - Decide where “default-enabled but not mentioned in spec” flavours belong in the sequence (e.g., append after explicit list in sorted order), and document it.
 - When spec is nil (and/or empty, depending on intended semantics), return default-enabled flavours in a stable order (e.g., sorted by name).
- Update tests to assert **order** (avoid `assert.ElementsMatch` when order matters).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Plugins merge nondeterministic 🐞 Bug ⛯ Reliability
Description
MergePluginsData iterates over maps (pluginMap and includeSet) when building slices, producing
nondeterministic YAML output. With flavours, mergeDynamicPlugins merges multiple sources, so the
dynamic-plugins ConfigMap can change between reconciles even when inputs are identical, triggering
repeated apply/rollouts.
Code

pkg/model/dynamic-plugins.go[R255-263]

	// Merge Plugins by package field
	pluginMap := make(map[string]DynaPlugin)
-	for _, plugin := range modelPluginsConfig.Plugins {
+	for _, plugin := range firstPluginsConfig.Plugins {
		pluginMap[plugin.Package] = plugin
	}
-	for _, plugin := range specPluginsConfig.Plugins {
+	for _, plugin := range secondPluginsConfig.Plugins {

		if existingPlugin, found := pluginMap[plugin.Package]; found {
			if plugin.PluginConfig != nil {
Evidence
mergeDynamicPlugins merges successive sources using MergePluginsData, which constructs output
slices via map iteration. Map iteration order is undefined, so the marshaled YAML can vary across
reconciles, causing unnecessary updates to the dynamic-plugins ConfigMap.

pkg/model/default-config.go[89-135]
pkg/model/dynamic-plugins.go[255-304]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Dynamic plugins merging can be nondeterministic because maps are iterated to build `Plugins` and `Includes` slices, leading to unstable YAML output and repeated reconciles/rollouts.

### Issue Context
With flavours enabled, the operator merges multiple `dynamic-plugins.yaml` sources (base + flavours). Any nondeterminism in merge output becomes much more visible.

### Fix Focus Areas
- pkg/model/dynamic-plugins.go[255-304]
- pkg/model/default-config.go[95-129]

### What to change
- Ensure deterministic ordering before marshaling:
 - For plugins: collect keys (`package`), `sort.Strings`, then append plugins in that order.
 - For includes: build slice, `sort.Strings`.
- Consider preserving a stable “base order then new packages” policy if desired, but **do not** rely on map iteration.
- Add/adjust tests to validate deterministic output across multiple runs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Empty flavours semantics broken🐞 Bug ✓ Correctness
Description
The CRD/API documentation states that an explicitly empty spec.flavours: [] should still use
default flavours, but GetEnabledFlavours returns an empty list when the slice is non-nil and
length 0. This breaks the published API contract and can unexpectedly disable flavours depending on
whether YAML omits the field vs sets it empty.
Code

pkg/model/flavour.go[R43-48]

+	if spec.Flavours != nil {
+
+		// No specified returns empty list
+		if len(spec.Flavours) == 0 {
+			return make([]enabledFlavour, 0), nil
+		}
Evidence
Both the Go API doc comment and the generated CRD description state that an empty array should still
use default flavours, but the controller logic returns no flavours for a non-nil empty slice (and
the unit test currently codifies that behavior), creating an API/implementation mismatch.

api/v1alpha6/backstage_types.go[43-53]
config/crd/bases/rhdh.redhat.com_backstages.yaml[1794-1804]
pkg/model/flavour.go[42-48]
pkg/model/default-config_test.go[174-181]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`spec.flavours: []` behavior is inconsistent: CRD/API docs say defaults are still used, but controller code treats empty list as “disable all flavours”. This is a user-facing API contract break.

### Issue Context
- Docs in API types + CRD describe empty-array semantics.
- Controller logic in `GetEnabledFlavours` returns an empty enabled flavour list for a non-nil empty slice.
- Tests currently expect the implementation behavior, worsening the mismatch.

### Fix Focus Areas
- pkg/model/flavour.go[42-48]
- api/v1alpha6/backstage_types.go[43-53]
- config/crd/bases/rhdh.redhat.com_backstages.yaml[1794-1804]
- pkg/model/default-config_test.go[174-181]

### What to change
- Decide the intended semantics and make **implementation + API docs + CRD + tests** consistent.
 - If following docs: remove the `len(spec.Flavours)==0` early-return and treat empty slice like nil (use enabledByDefault).
 - If keeping current behavior: update the API comment and CRD description to state that `[]` disables all flavours, and consider documenting an explicit way to keep defaults if needed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Mount path collisions possible 🐞 Bug ✓ Correctness
Description
getDefConfigMountPath now defaults to b.defaultMountPath() when rhdh.redhat.com/mount-path is
absent, and mountFilesFrom replaces mounts that share the same MountPath. When multiple
ConfigMaps in configmap-files.yaml omit mount-path, later items can replace earlier mounts at the
same path, silently dropping earlier mounted files.
Code

pkg/model/deployment.go[R267-275]

+	mountPath = obj.GetAnnotations()[DefaultMountPathAnnotation]
+	if mountPath == "" {
+		mountPath = b.defaultMountPath()
	}
+	subPath = obj.GetAnnotations()[DefaultSubPathAnnotation]
+	fileName = ""
+	if subPath != "" && subPath != "*" {
+		fileName = subPath
+	}
Evidence
ConfigMapFiles iterates over multiple items and calls getDefConfigMountPath then mountFilesFrom
for each. If mount-path is missing, all items resolve to the same default mount path;
mountFilesFrom then performs replacement-by-MountPath, so subsequent mounts can overwrite earlier
ones rather than coexisting.

pkg/model/deployment.go[260-275]
pkg/model/configmapfiles.go[71-86]
pkg/model/deployment.go[352-418]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When mount-path annotation is missing, multiple file-sources can resolve to the same mountPath, and `mountFilesFrom` will replace mounts with matching `MountPath`, causing silent loss of earlier mounts.

### Issue Context
This is most visible for `configmap-files.yaml` because multiple ConfigMaps are processed sequentially.

### Fix Focus Areas
- pkg/model/deployment.go[260-282]
- pkg/model/configmapfiles.go[71-86]
- pkg/model/deployment.go[399-417]

### What to change
- Restore a collision-resistant default when `rhdh.redhat.com/mount-path` is absent, e.g.:
 - default to `filepath.Join(b.defaultMountPath(), utils.ToRFC1123Label(obj.GetName()))`, mounting as a directory, OR
 - default to mounting file-by-file under a unique subdir.
- Optionally: detect and error/log when multiple mounts target the same `MountPath` without subPath semantics.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Config read errors hidden 🐞 Bug ⛯ Reliability
Description
collectConfigSources silently ignores os.Stat/os.ReadFile failures, causing default/flavour
config files to be skipped without any error. This can lead to reconciles succeeding with missing
configuration and makes diagnosing filesystem/packaging issues difficult.
Code

pkg/model/default-config.go[R60-82]

+	if _, err := os.Stat(basePath); err == nil {
+		if content, err := os.ReadFile(basePath); err == nil {
+			sources = append(sources, configSource{
+				path:        basePath,
+				flavourName: "", // empty for base
+				content:     content,
+			})
+		}
+	}
+
+	// Read each flavour config if it exists
+	for _, flavour := range flavours {
+		flavourConfigPath := filepath.Join(flavour.basePath, key)
+		if _, err := os.Stat(flavourConfigPath); err == nil {
+			if content, err := os.ReadFile(flavourConfigPath); err == nil {
+				sources = append(sources, configSource{
+					path:        flavourConfigPath,
+					flavourName: flavour.name,
+					content:     content,
+				})
+			}
+		}
+	}
Evidence
The code only appends config sources when both Stat and ReadFile return nil; any other error
(permissions, transient I/O, etc.) results in the source being dropped with no returned error or
log.

pkg/model/default-config.go[54-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Default/flavour config files can be skipped silently on I/O errors, producing incomplete configs without surfacing failures.

### Issue Context
This affects all merged config types (deployment, dynamic-plugins, app-config, configmap-*), because all use `collectConfigSources`.

### Fix Focus Areas
- pkg/model/default-config.go[54-85]
- pkg/model/default-config.go[16-51]

### What to change
- Change `collectConfigSources` to return `(sources []configSource, err error)`.
- Treat non-NotExist errors from `Stat`/`ReadFile` as fatal (return error), and optionally log when files are absent.
- Update `ReadDefaultConfig` to propagate the error.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. mergeDeployments may panic 🐞 Bug ⛯ Reliability
Description
mergeDeployments returns objs[0] without checking that parsing produced any objects, which can
panic if the merged YAML is empty/whitespace-only. It also silently discards additional objects if
multiple documents are present.
Code

pkg/model/deployment.go[R501-507]

+	// Parse with platform patch applied
+	objs, err := utils.ReadYamls(mergedYAML, platformPatch, scheme)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse merged deployment: %w", err)
+	}
+
+	return []client.Object{objs[0]}, nil
Evidence
After parsing merged YAML, the code unconditionally indexes the first element without validating
len(objs), creating an out-of-range panic risk and masking multi-document errors.

pkg/model/deployment.go[478-507]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`mergeDeployments` can panic on `objs[0]` if parsing yields 0 objects, and it ignores extra objects if >1.

### Issue Context
This function is used for base+flavour deployment merging, where YAML merging could yield empty output (e.g., if an overlay replaces content) or multi-doc YAML.

### Fix Focus Areas
- pkg/model/deployment.go[480-507]

### What to change
- After `ReadYamls`, validate:
 - if `len(objs)==0`: return a descriptive error including involved source paths
 - if `len(objs)>1`: return an error (deployment.yaml must be a single object)
 - else return the single object.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@gazarenkov
Copy link
Copy Markdown
Member Author

/review

@rhdh-qodo-merge
Copy link
Copy Markdown

Persistent review updated to latest commit 1f788ec

Copy link
Copy Markdown
Contributor

@gabemontero gabemontero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

with the understanding that lgtm's from @rm3l or @kadel are more critical

@openshift-ci
Copy link
Copy Markdown

openshift-ci bot commented Mar 26, 2026

New changes are detected. LGTM label has been removed.

Co-authored-by: gazarenkov <gazarenkov@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
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.

@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge bot commented Mar 27, 2026

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (1) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. Empty flavours semantics broken🐞 Bug ✓ Correctness
Description
The CRD/API documentation states that an explicitly empty spec.flavours: [] should still use
default flavours, but GetEnabledFlavours returns an empty list when the slice is non-nil and
length 0. This breaks the published API contract and can unexpectedly disable flavours depending on
whether YAML omits the field vs sets it empty.
Code

pkg/model/flavour.go[R43-48]

+	if spec.Flavours != nil {
+
+		// No specified returns empty list
+		if len(spec.Flavours) == 0 {
+			return make([]enabledFlavour, 0), nil
+		}
Evidence
Both the Go API doc comment and the generated CRD description state that an empty array should still
use default flavours, but the controller logic returns no flavours for a non-nil empty slice (and
the unit test currently codifies that behavior), creating an API/implementation mismatch.

api/v1alpha6/backstage_types.go[43-53]
config/crd/bases/rhdh.redhat.com_backstages.yaml[1794-1804]
pkg/model/flavour.go[42-48]
pkg/model/default-config_test.go[174-181]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`spec.flavours: []` behavior is inconsistent: CRD/API docs say defaults are still used, but controller code treats empty list as “disable all flavours”. This is a user-facing API contract break.

### Issue Context
- Docs in API types + CRD describe empty-array semantics.
- Controller logic in `GetEnabledFlavours` returns an empty enabled flavour list for a non-nil empty slice.
- Tests currently expect the implementation behavior, worsening the mismatch.

### Fix Focus Areas
- pkg/model/flavour.go[42-48]
- api/v1alpha6/backstage_types.go[43-53]
- config/crd/bases/rhdh.redhat.com_backstages.yaml[1794-1804]
- pkg/model/default-config_test.go[174-181]

### What to change
- Decide the intended semantics and make **implementation + API docs + CRD + tests** consistent.
 - If following docs: remove the `len(spec.Flavours)==0` early-return and treat empty slice like nil (use enabledByDefault).
 - If keeping current behavior: update the API comment and CRD description to state that `[]` disables all flavours, and consider documenting an explicit way to keep defaults if needed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. deployment.yaml omits spec.replicas 📘 Rule violation ⚙ Maintainability
Description
This new Deployment manifest omits spec.replicas without an adjacent comment stating replicas
are managed by autoscaling/another controller. This violates the requirement to document intentional
replicas omission to avoid ambiguous scaling behavior.
Code

pkg/model/testdata/testflavours/default-config/deployment.yaml[R1-5]

+apiVersion: apps/v1
+kind: Deployment
+spec:
+  template:
+    spec:
Evidence
PR Compliance ID 9 requires that when a workload supporting spec.replicas omits it, there must be
a nearby comment explaining that autoscaling/automatic replica management is responsible. The added
Deployment YAML contains spec: and template: but no replicas: and no such comment.

Rule 9: Document omitted spec.replicas when relying on autoscaling
pkg/model/testdata/testflavours/default-config/deployment.yaml[1-5]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A newly added `Deployment` manifest omits `spec.replicas` but does not include an adjacent comment stating replicas are intentionally omitted because autoscaling (or another controller) manages them.

## Issue Context
Compliance requires an explicit indication when `spec.replicas` is omitted for autoscaling/automatic replica management, otherwise readers cannot tell whether the omission is intentional.

## Fix Focus Areas
- pkg/model/testdata/testflavours/default-config/deployment.yaml[1-5]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Flavour merge order unstable 🐞 Bug ✓ Correctness
Description
GetEnabledFlavours builds the enabled flavour list by iterating a Go map, producing
nondeterministic ordering. Since default configs are merged in flavour slice order and the API/CRD
promises “merged in the order specified”, override precedence can change between reconciles.
Code

pkg/model/flavour.go[R60-69]

+	// Step 3: Collect enabled flavours
+	var result []enabledFlavour
+	for name, flavour := range allFlavours {
+		if flavour.enabled {
+			result = append(result, enabledFlavour{
+				name:     name,
+				basePath: flavour.basePath,
+			})
+		}
+	}
Evidence
The API/CRD says flavour configs are merged in the order specified, and collectConfigSources
claims flavours are in spec order, but GetEnabledFlavours ranges over allFlavours map when
collecting enabled flavours, which has undefined iteration order in Go.

api/v1alpha6/backstage_types.go[47-53]
pkg/model/default-config.go[54-56]
pkg/model/flavour.go[60-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Enabled flavour ordering is nondeterministic because the code ranges over a map. This breaks the “merge in order specified” contract and can flip override precedence between reconciles.

### Issue Context
Config merging in `ReadDefaultConfig` depends on the order of the `flavours []enabledFlavour` slice.

### Fix Focus Areas
- pkg/model/flavour.go[50-71]
- pkg/model/default-config.go[54-85]
- pkg/model/default-config_test.go[229-246]

### What to change
- Build `[]enabledFlavour` in a deterministic order:
 - When spec provides an explicit list, emit enabled flavours in **spec order**.
 - Decide where “default-enabled but not mentioned in spec” flavours belong in the sequence (e.g., append after explicit list in sorted order), and document it.
 - When spec is nil (and/or empty, depending on intended semantics), return default-enabled flavours in a stable order (e.g., sorted by name).
- Update tests to assert **order** (avoid `assert.ElementsMatch` when order matters).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Plugins merge nondeterministic 🐞 Bug ⛯ Reliability
Description
MergePluginsData iterates over maps (pluginMap and includeSet) when building slices, producing
nondeterministic YAML output. With flavours, mergeDynamicPlugins merges multiple sources, so the
dynamic-plugins ConfigMap can change between reconciles even when inputs are identical, triggering
repeated apply/rollouts.
Code

pkg/model/dynamic-plugins.go[R255-263]

	// Merge Plugins by package field
	pluginMap := make(map[string]DynaPlugin)
-	for _, plugin := range modelPluginsConfig.Plugins {
+	for _, plugin := range firstPluginsConfig.Plugins {
		pluginMap[plugin.Package] = plugin
	}
-	for _, plugin := range specPluginsConfig.Plugins {
+	for _, plugin := range secondPluginsConfig.Plugins {

		if existingPlugin, found := pluginMap[plugin.Package]; found {
			if plugin.PluginConfig != nil {
Evidence
mergeDynamicPlugins merges successive sources using MergePluginsData, which constructs output
slices via map iteration. Map iteration order is undefined, so the marshaled YAML can vary across
reconciles, causing unnecessary updates to the dynamic-plugins ConfigMap.

pkg/model/default-config.go[89-135]
pkg/model/dynamic-plugins.go[255-304]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Dynamic plugins merging can be nondeterministic because maps are iterated to build `Plugins` and `Includes` slices, leading to unstable YAML output and repeated reconciles/rollouts.

### Issue Context
With flavours enabled, the operator merges multiple `dynamic-plugins.yaml` sources (base + flavours). Any nondeterminism in merge output becomes much more visible.

### Fix Focus Areas
- pkg/model/dynamic-plugins.go[255-304]
- pkg/model/default-config.go[95-129]

### What to change
- Ensure deterministic ordering before marshaling:
 - For plugins: collect keys (`package`), `sort.Strings`, then append plugins in that order.
 - For includes: build slice, `sort.Strings`.
- Consider preserving a stable “base order then new packages” policy if desired, but **do not** rely on map iteration.
- Add/adjust tests to validate deterministic output across multiple runs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Mount path collisions possible 🐞 Bug ✓ Correctness
Description
getDefConfigMountPath now defaults to b.defaultMountPath() when rhdh.redhat.com/mount-path is
absent, and mountFilesFrom replaces mounts that share the same MountPath. When multiple
ConfigMaps in configmap-files.yaml omit mount-path, later items can replace earlier mounts at the
same path, silently dropping earlier mounted files.
Code

pkg/model/deployment.go[R267-275]

+	mountPath = obj.GetAnnotations()[DefaultMountPathAnnotation]
+	if mountPath == "" {
+		mountPath = b.defaultMountPath()
	}
+	subPath = obj.GetAnnotations()[DefaultSubPathAnnotation]
+	fileName = ""
+	if subPath != "" && subPath != "*" {
+		fileName = subPath
+	}
Evidence
ConfigMapFiles iterates over multiple items and calls getDefConfigMountPath then mountFilesFrom
for each. If mount-path is missing, all items resolve to the same default mount path;
mountFilesFrom then performs replacement-by-MountPath, so subsequent mounts can overwrite earlier
ones rather than coexisting.

pkg/model/deployment.go[260-275]
pkg/model/configmapfiles.go[71-86]
pkg/model/deployment.go[352-418]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When mount-path annotation is missing, multiple file-sources can resolve to the same mountPath, and `mountFilesFrom` will replace mounts with matching `MountPath`, causing silent loss of earlier mounts.

### Issue Context
This is most visible for `configmap-files.yaml` because multiple ConfigMaps are processed sequentially.

### Fix Focus Areas
- pkg/model/deployment.go[260-282]
- pkg/model/configmapfiles.go[71-86]
- pkg/model/deployment.go[399-417]

### What to change
- Restore a collision-resistant default when `rhdh.redhat.com/mount-path` is absent, e.g.:
 - default to `filepath.Join(b.defaultMountPath(), utils.ToRFC1123Label(obj.GetName()))`, mounting as a directory, OR
 - default to mounting file-by-file under a unique subdir.
- Optionally: detect and error/log when multiple mounts target the same `MountPath` without subPath semantics.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Config read errors hidden 🐞 Bug ⛯ Reliability
Description
collectConfigSources silently ignores os.Stat/os.ReadFile failures, causing default/flavour
config files to be skipped without any error. This can lead to reconciles succeeding with missing
configuration and makes diagnosing filesystem/packaging issues difficult.
Code

pkg/model/default-config.go[R60-82]

+	if _, err := os.Stat(basePath); err == nil {
+		if content, err := os.ReadFile(basePath); err == nil {
+			sources = append(sources, configSource{
+				path:        basePath,
+				flavourName: "", // empty for base
+				content:     content,
+			})
+		}
+	}
+
+	// Read each flavour config if it exists
+	for _, flavour := range flavours {
+		flavourConfigPath := filepath.Join(flavour.basePath, key)
+		if _, err := os.Stat(flavourConfigPath); err == nil {
+			if content, err := os.ReadFile(flavourConfigPath); err == nil {
+				sources = append(sources, configSource{
+					path:        flavourConfigPath,
+					flavourName: flavour.name,
+					content:     content,
+				})
+			}
+		}
+	}
Evidence
The code only appends config sources when both Stat and ReadFile return nil; any other error
(permissions, transient I/O, etc.) results in the source being dropped with no returned error or
log.

pkg/model/default-config.go[54-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Default/flavour config files can be skipped silently on I/O errors, producing incomplete configs without surfacing failures.

### Issue Context
This affects all merged config types (deployment, dynamic-plugins, app-config, configmap-*), because all use `collectConfigSources`.

### Fix Focus Areas
- pkg/model/default-config.go[54-85]
- pkg/model/default-config.go[16-51]

### What to change
- Change `collectConfigSources` to return `(sources []configSource, err error)`.
- Treat non-NotExist errors from `Stat`/`ReadFile` as fatal (return error), and optionally log when files are absent.
- Update `ReadDefaultConfig` to propagate the error.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. mergeDeployments may panic 🐞 Bug ⛯ Reliability
Description
mergeDeployments returns objs[0] without checking that parsing produced any objects, which can
panic if the merged YAML is empty/whitespace-only. It also silently discards additional objects if
multiple documents are present.
Code

pkg/model/deployment.go[R501-507]

+	// Parse with platform patch applied
+	objs, err := utils.ReadYamls(mergedYAML, platformPatch, scheme)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse merged deployment: %w", err)
+	}
+
+	return []client.Object{objs[0]}, nil
Evidence
After parsing merged YAML, the code unconditionally indexes the first element without validating
len(objs), creating an out-of-range panic risk and masking multi-document errors.

pkg/model/deployment.go[478-507]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`mergeDeployments` can panic on `objs[0]` if parsing yields 0 objects, and it ignores extra objects if >1.

### Issue Context
This function is used for base+flavour deployment merging, where YAML merging could yield empty output (e.g., if an overlay replaces content) or multi-doc YAML.

### Fix Focus Areas
- pkg/model/deployment.go[480-507]

### What to change
- After `ReadYamls`, validate:
 - if `len(objs)==0`: return a descriptive error including involved source paths
 - if `len(objs)>1`: return an error (deployment.yaml must be a single object)
 - else return the single object.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@sonarqubecloud
Copy link
Copy Markdown

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants