Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a4ca065
feat: RHIDP-9762-consume-catalog-index
Fortune-Ndlovu Dec 10, 2025
d12045f
typo
Fortune-Ndlovu Dec 10, 2025
1747427
Add RHIDP-9762-consume-catalog-index
Fortune-Ndlovu Dec 11, 2025
b9362ae
Update docs
Fortune-Ndlovu Dec 11, 2025
575cbf8
Update test values and docs
Fortune-Ndlovu Dec 12, 2025
e6f8659
the extraEnvs now just appends env vars as before. Since Kubernetes u…
Fortune-Ndlovu Dec 12, 2025
db8cf61
fix syntax
Fortune-Ndlovu Dec 12, 2025
9856567
Check if existingEnv exists if so replace it with a new value, if it …
Fortune-Ndlovu Dec 16, 2025
4fdd377
Look at operator's own env var RELATED_IMAGE_catalog_index, If it exi…
Fortune-Ndlovu Dec 16, 2025
17baa0c
Update docs
Fortune-Ndlovu Dec 16, 2025
13d7a64
Update TestCatalogIndexImageEnvVar to use janus-deployment.yaml inste…
Fortune-Ndlovu Dec 17, 2025
bf6f2dd
Fix: Added a check to only set CATALOG_INDEX_IMAGE from the operator …
Fortune-Ndlovu Dec 17, 2025
0dc45a8
update catalog index documentation and move configuration to examples…
Fortune-Ndlovu Dec 18, 2025
bf35403
fixup: RELATED_IMAGE_catalog_index should override the default config…
Fortune-Ndlovu Dec 18, 2025
fdc93be
Regenerate bundle/installer manifests
github-actions[bot] Dec 18, 2025
0f53b2a
fix(deps): update all patch dependencies to v0.34.3 (#1999)
renovate[bot] Dec 11, 2025
b660892
chore(deps): update tj-actions/changed-files action to v47.0.1 (#2001)
renovate[bot] Dec 11, 2025
7a5ca5b
chore(deps): update redhat-plumbers-in-action/differential-shellcheck…
renovate[bot] Dec 11, 2025
d1f66d9
chore(deps): update github/codeql-action digest to 1b168cd (#2006)
renovate[bot] Dec 12, 2025
987596d
fix(deps): update github.com/openshift/api digest to cafbd60 (#2009)
renovate[bot] Dec 13, 2025
d5a51bd
chore(deps): update actions/upload-artifact action to v6 (#2011)
renovate[bot] Dec 13, 2025
46e02e6
fix(deps): update github.com/openshift/api digest to bfa868a (#2015)
renovate[bot] Dec 14, 2025
ef549ac
chore(deps): update github/codeql-action digest to 5d4e8d1 (#2018)
renovate[bot] Dec 16, 2025
c789fd4
fix(deps): update k8s.io/utils digest to 61b37f7 (#2021)
renovate[bot] Dec 18, 2025
79d7c2d
chore(deps): update registry.access.redhat.com/ubi9/go-toolset docker…
renovate[bot] Dec 19, 2025
a66ff72
Merge branch 'redhat-developer:main' into RHIDP-9762-consume-catalog-…
Fortune-Ndlovu Dec 19, 2025
c459e8b
Apply suggestions from code review
rm3l Dec 23, 2025
521c74a
Update pkg/model/deployment_test.go
rm3l Dec 23, 2025
c6ab3e3
Update pkg/model/dynamic-plugins.go
rm3l Dec 23, 2025
f058548
Update pkg/model/dynamic-plugins.go
rm3l Dec 23, 2025
b1db665
Update pkg/model/deployment_test.go
rm3l Dec 23, 2025
7a12dff
Update pkg/model/dynamic-plugins.go
rm3l Dec 23, 2025
336a136
Apply suggestion from @rm3l
rm3l Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/v1alpha3/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/v1alpha4/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/v1alpha5/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ metadata:
}
}
]
createdAt: "2025-12-02T11:27:28Z"
createdAt: "2025-12-18T16:30:38Z"
description: Backstage Operator
operators.operatorframework.io/builder: operator-sdk-v1.37.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ metadata:
categories: Developer Tools
certified: "true"
containerImage: registry.redhat.io/rhdh/rhdh-rhel9-operator:1.9
createdAt: "2025-12-08T08:32:34Z"
createdAt: "2025-12-18T16:30:39Z"
description: Red Hat Developer Hub is a Red Hat supported version of Backstage.
It comes with pre-built plug-ins and configuration settings, supports use of
an external database, and can help streamline the process of setting up a self-managed
Expand Down Expand Up @@ -338,6 +338,8 @@ spec:
value: quay.io/fedora/postgresql-15:latest
- name: RELATED_IMAGE_backstage
value: quay.io/rhdh-community/rhdh:next
- name: RELATED_IMAGE_catalog_index
value: quay.io/rhdh/plugin-catalog-index:1.9
image: quay.io/rhdh/rhdh-rhel9-operator:1.9
livenessProbe:
httpGet:
Expand Down Expand Up @@ -459,5 +461,7 @@ spec:
name: postgresql
- image: quay.io/rhdh-community/rhdh:next
name: backstage
- image: quay.io/rhdh/plugin-catalog-index:1.9
name: catalog-index
replaces: rhdh-operator.v1.8.0
version: 1.9.0
3 changes: 3 additions & 0 deletions bundle/rhdh/manifests/rhdh-default-config_v1_configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ data:
value: /opt/app-root/src/.npmrc.dynamic-plugins/.npmrc
- name: MAX_ENTRY_SIZE
value: "30000000"
# CATALOG_INDEX_IMAGE will be replaced by the value of the `RELATED_IMAGE_catalog_index` env var, if set
- name: CATALOG_INDEX_IMAGE
value: "quay.io/rhdh/plugin-catalog-index:1.9"
volumeMounts:
- mountPath: /dynamic-plugins-root
name: dynamic-plugins-root
Expand Down
3 changes: 3 additions & 0 deletions config/profile/rhdh/default-config/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ spec:
value: /opt/app-root/src/.npmrc.dynamic-plugins/.npmrc
- name: MAX_ENTRY_SIZE
value: "30000000"
# CATALOG_INDEX_IMAGE will be replaced by the value of the `RELATED_IMAGE_catalog_index` env var, if set
- name: CATALOG_INDEX_IMAGE
value: "quay.io/rhdh/plugin-catalog-index:1.9"
volumeMounts:
- mountPath: /dynamic-plugins-root
name: dynamic-plugins-root
Expand Down
2 changes: 2 additions & 0 deletions config/profile/rhdh/patches/deployment-patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ spec:
value: quay.io/fedora/postgresql-15:latest
- name: RELATED_IMAGE_backstage
value: quay.io/rhdh-community/rhdh:next
- name: RELATED_IMAGE_catalog_index
Comment thread
Fortune-Ndlovu marked this conversation as resolved.
value: quay.io/rhdh/plugin-catalog-index:1.9

5 changes: 5 additions & 0 deletions dist/rhdh/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2088,6 +2088,9 @@ data:
value: /opt/app-root/src/.npmrc.dynamic-plugins/.npmrc
- name: MAX_ENTRY_SIZE
value: "30000000"
# CATALOG_INDEX_IMAGE will be replaced by the value of the `RELATED_IMAGE_catalog_index` env var, if set
- name: CATALOG_INDEX_IMAGE
value: "quay.io/rhdh/plugin-catalog-index:1.9"
volumeMounts:
- mountPath: /dynamic-plugins-root
name: dynamic-plugins-root
Expand Down Expand Up @@ -2948,6 +2951,8 @@ spec:
value: quay.io/fedora/postgresql-15:latest
- name: RELATED_IMAGE_backstage
value: quay.io/rhdh-community/rhdh:next
- name: RELATED_IMAGE_catalog_index
value: quay.io/rhdh/plugin-catalog-index:1.9
image: quay.io/rhdh/rhdh-rhel9-operator:1.9
livenessProbe:
httpGet:
Expand Down
7 changes: 7 additions & 0 deletions docs/dynamic-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ spec:

TODO: Dynamic plugins can be configured to use container registries for authentication and image pulling. This section should cover the configuration options available for container registry integration with dynamic plugins.

## Catalog Index Configuration

The operator supports loading default plugin configurations from an OCI container image (catalog index). For general information about how the catalog index works, see [Using a Catalog Index Image for Default Plugin Configurations](https://github.com/redhat-developer/rhdh/blob/main/docs/dynamic-plugins/installing-plugins.md#using-a-catalog-index-image-for-default-plugin-configurations).

By default, the `rhdh` profile of operator [injects](../config/profile/rhdh/patches/deployment-patch.yaml#L31-L32) the `CATALOG_INDEX_IMAGE` environment variable in the RHDH `install-dynamic-plugins` init containers.
To use a different catalog index image, such as a newer version or a mirrored image, use the `extraEnvs` field in your Backstage CR. See [examples/catalog-index.yaml](../examples/catalog-index.yaml) for a complete example.

## Dynamic plugins dependency management

### Overview
Expand Down
13 changes: 13 additions & 0 deletions examples/catalog-index.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: rhdh.redhat.com/v1alpha5
kind: Backstage
metadata:
name: my-backstage
spec:
application:
extraEnvs:
envs:
- name: CATALOG_INDEX_IMAGE
value: "quay.io/rhdh/plugin-catalog-index:1.9"
containers:
- install-dynamic-plugins

26 changes: 21 additions & 5 deletions pkg/model/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
)

const BackstageImageEnvVar = "RELATED_IMAGE_backstage"
const CatalogIndexImageEnvVar = "RELATED_IMAGE_catalog_index"
const DefaultMountDir = "/opt/app-root/src"
const ExtConfigHashAnnotation = "rhdh.redhat.com/ext-config-hash"

Expand Down Expand Up @@ -111,6 +112,13 @@ func (b *BackstageDeployment) addToModel(model *BackstageModel, backstage bsv1.B
b.setImage(ptr.To(os.Getenv(BackstageImageEnvVar)))
}

// Set CATALOG_INDEX_IMAGE from operator env var BEFORE extraEnvs are applied, so user-specified extraEnvs can still override this value
if catalogIndexImage := os.Getenv(CatalogIndexImageEnvVar); catalogIndexImage != "" {
if i, _ := DynamicPluginsInitContainer(b.podSpec().InitContainers); i >= 0 {
b.setOrAppendEnvVar(&b.podSpec().InitContainers[i], "CATALOG_INDEX_IMAGE", catalogIndexImage)
}
}

if err := b.setDeployment(backstage); err != nil {
return false, err
}
Expand Down Expand Up @@ -285,6 +293,7 @@ func (b *BackstageDeployment) setImage(image *string) {
}

// adds environment from source to the Backstage Container
// If an env var with the same name already exists, it will be replaced (not duplicated)
func (b *BackstageDeployment) addExtraEnvs(extraEnvs *bsv1.ExtraEnvs) error {
if extraEnvs == nil {
return nil
Expand All @@ -296,16 +305,23 @@ func (b *BackstageDeployment) addExtraEnvs(extraEnvs *bsv1.ExtraEnvs) error {
return fmt.Errorf("can not get containers to add env %s: %w", env.Name, err)
}
for _, container := range containers {
container.Env =
append(container.Env, corev1.EnvVar{
Name: env.Name,
Value: env.Value,
})
b.setOrAppendEnvVar(container, env.Name, env.Value)
}
}
return nil
}

// setOrAppendEnvVar sets an env var on a container, replacing if it exists or appending if not
func (b *BackstageDeployment) setOrAppendEnvVar(container *corev1.Container, name, value string) {
for i, existingEnv := range container.Env {
if existingEnv.Name == name {
container.Env[i] = corev1.EnvVar{Name: name, Value: value}
return
}
}
container.Env = append(container.Env, corev1.EnvVar{Name: name, Value: value})
}

// MountFilesFrom adds Volume to specified podSpec and related VolumeMounts to specified belonging to this podSpec container
// from ConfigMap or Secret volume source
// containers - array of containers to add VolumeMount(s) to
Expand Down
123 changes: 123 additions & 0 deletions pkg/model/dynamic-plugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha5"

corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -247,6 +248,128 @@ func initContainer(model *BackstageModel) *corev1.Container {
return nil
}


// TestCatalogIndexImageFromDefaultConfig verifies that the operator sets CATALOG_INDEX_IMAGE
// on the install-dynamic-plugins init container from the default config by default
func TestCatalogIndexImageFromDefaultConfig(t *testing.T) {
bs := testDynamicPluginsBackstage.DeepCopy()

testObj := createBackstageTest(*bs).withDefaultConfig(true).
addToDefaultConfig("dynamic-plugins.yaml", "raw-dynamic-plugins.yaml").
addToDefaultConfig("deployment.yaml", "janus-deployment.yaml")
Comment thread
Fortune-Ndlovu marked this conversation as resolved.

model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme)
assert.NoError(t, err)
assert.NotNil(t, model.backstageDeployment)

ic := initContainer(model)
assert.NotNil(t, ic)

assert.Len(t, ic.Env, 2)
assert.Equal(t, "NPM_CONFIG_USERCONFIG", ic.Env[0].Name)
assert.Equal(t, "CATALOG_INDEX_IMAGE", ic.Env[1].Name)
assert.Equal(t, "quay.io/rhdh/plugin-catalog-index:1.9", ic.Env[1].Value, "CATALOG_INDEX_IMAGE should be set from the default config")
}

// TestCatalogIndexImageOverridesDefaultConfig verifies that RELATED_IMAGE_catalog_index
// overrides the CATALOG_INDEX_IMAGE value that comes from the default config.
// This is the critical test case: the default-config deployment.yaml has CATALOG_INDEX_IMAGE
// set to one value, but RELATED_IMAGE_catalog_index should override it.
func TestCatalogIndexImageOverridesDefaultConfig(t *testing.T) {
bs := testDynamicPluginsBackstage.DeepCopy()

// janus-deployment.yaml has CATALOG_INDEX_IMAGE set (like the real default-config)
testObj := createBackstageTest(*bs).withDefaultConfig(true).
addToDefaultConfig("dynamic-plugins.yaml", "raw-dynamic-plugins.yaml").
addToDefaultConfig("deployment.yaml", "janus-deployment.yaml")

// Set RELATED_IMAGE_catalog_index to a DIFFERENT value - this should override the default config
t.Setenv(CatalogIndexImageEnvVar, "quay.io/fake-reg/img:1.2.3")

model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme)
assert.NoError(t, err)
assert.NotNil(t, model.backstageDeployment)

ic := initContainer(model)
assert.NotNil(t, ic)

assert.Len(t, ic.Env, 2)
assert.Equal(t, "NPM_CONFIG_USERCONFIG", ic.Env[0].Name)
assert.Equal(t, "CATALOG_INDEX_IMAGE", ic.Env[1].Name)
assert.Equal(t, "quay.io/fake-reg/img:1.2.3", ic.Env[1].Value, "RELATED_IMAGE_catalog_index should override the default config value")
}

// TestCatalogIndexImageUserPatchTakesPrecedence verifies that user-specified deployment patch
// takes precedence over the operator's RELATED_IMAGE_catalog_index env var
func TestCatalogIndexImageUserPatchTakesPrecedence(t *testing.T) {
bs := testDynamicPluginsBackstage.DeepCopy()

// User specifies CATALOG_INDEX_IMAGE via deployment patch
bs.Spec.Deployment = &bsv1.BackstageDeployment{}
bs.Spec.Deployment.Patch = &apiextensionsv1.JSON{
Raw: []byte(`
spec:
template:
spec:
initContainers:
- name: install-dynamic-plugins
env:
- name: CATALOG_INDEX_IMAGE
value: "quay.io/user-specified/image:2.0"
`),
}

testObj := createBackstageTest(*bs).withDefaultConfig(true).
addToDefaultConfig("dynamic-plugins.yaml", "raw-dynamic-plugins.yaml").
addToDefaultConfig("deployment.yaml", "janus-deployment.yaml")

// Set RELATED_IMAGE_catalog_index - but user's patch should take precedence
t.Setenv(CatalogIndexImageEnvVar, "quay.io/rhdh/plugin-catalog-index:related-image")

model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme)
assert.NoError(t, err)
assert.NotNil(t, model.backstageDeployment)

ic := initContainer(model)
assert.NotNil(t, ic)
assert.Len(t, ic.Env, 2)
assert.Equal(t, "NPM_CONFIG_USERCONFIG", ic.Env[0].Name)
assert.Equal(t, "CATALOG_INDEX_IMAGE", ic.Env[1].Name)
assert.Equal(t, "quay.io/user-specified/image:2.0", ic.Env[1].Value, "user's deployment patch should override RELATED_IMAGE_catalog_index")
}

// TestCatalogIndexImageExtraEnvsOverride verifies that user-specified extraEnvs
// takes precedence over the operator's RELATED_IMAGE_catalog_index env var
func TestCatalogIndexImageExtraEnvsOverride(t *testing.T) {
bs := testDynamicPluginsBackstage.DeepCopy()

// User specifies a different catalog index image via extraEnvs
bs.Spec.Application.ExtraEnvs = &bsv1.ExtraEnvs{
Envs: []bsv1.Env{
{Name: "CATALOG_INDEX_IMAGE", Value: "quay.io/rhdh/plugin-catalog-index:extra-env", Containers: []string{"install-dynamic-plugins"}},
},
}

testObj := createBackstageTest(*bs).withDefaultConfig(true).
addToDefaultConfig("dynamic-plugins.yaml", "raw-dynamic-plugins.yaml").
addToDefaultConfig("deployment.yaml", "janus-deployment.yaml")

// Set the RELATED_IMAGE_catalog_index env var (simulating operator environment)
// This should NOT override the user's extraEnvs value
t.Setenv(CatalogIndexImageEnvVar, "quay.io/rhdh/plugin-catalog-index:related-image")

model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme)
assert.NoError(t, err)
assert.NotNil(t, model.backstageDeployment)

ic := initContainer(model)
assert.NotNil(t, ic)
assert.Len(t, ic.Env, 2)
assert.Equal(t, "NPM_CONFIG_USERCONFIG", ic.Env[0].Name)
assert.Equal(t, "CATALOG_INDEX_IMAGE", ic.Env[1].Name)
assert.Equal(t, "quay.io/rhdh/plugin-catalog-index:extra-env", ic.Env[1].Value, "extraEnvs value should override the operator's RELATED_IMAGE_catalog_index")
}

func TestUnmarshalDynaPluginsConfig(t *testing.T) {
yamlData := `
plugins:
Expand Down
3 changes: 3 additions & 0 deletions pkg/model/testdata/janus-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ spec:
env:
- name: NPM_CONFIG_USERCONFIG
value: /opt/app-root/src/.npmrc.dynamic-plugins
# CATALOG_INDEX_IMAGE will be replaced by the value of the `RELATED_IMAGE_catalog_index` env var, if set
- name: CATALOG_INDEX_IMAGE
value: "quay.io/rhdh/plugin-catalog-index:1.9"
image: 'quay.io/rhdh/rhdh-hub-rhel9:next'
imagePullPolicy: IfNotPresent
name: install-dynamic-plugins
Expand Down