From 6ce40326ceffef16b3fc57b343456e74ffc3eb44 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 May 2026 18:41:11 +0200 Subject: [PATCH 01/10] fix: honor init container order in spec.deployment patch [RHDHBUGS-2900] Use ListIncreaseDirection: MergeOptionsListPrepend so user-specified init containers are prepended rather than appended, allowing them to run before install-dynamic-plugins. Assisted-by: Claude --- pkg/model/deployment.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/deployment.go b/pkg/model/deployment.go index 0989a1bce..611ea84d3 100644 --- a/pkg/model/deployment.go +++ b/pkg/model/deployment.go @@ -227,7 +227,7 @@ func (b *BackstageDeployment) setDeployment(backstage api.Backstage) error { return fmt.Errorf("can not marshal deployment object: %w", err) } - merged, err := merge2.MergeStrings(string(conf.Raw), string(deplStr), false, kyaml.MergeOptions{}) + merged, err := merge2.MergeStrings(string(conf.Raw), string(deplStr), false, kyaml.MergeOptions{ListIncreaseDirection: kyaml.MergeOptionsListPrepend}) if err != nil { return fmt.Errorf("can not merge spec.deployment: %w", err) } From 0dd864cd659b691070800d47850f88b3974584d0 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 11 May 2026 18:52:05 +0200 Subject: [PATCH 02/10] test/docs: add init container ordering test and documentation [RHDHBUGS-2900] Add TestInitContainerOrderInSpecDeployment to verify user-specified init containers are prepended before install-dynamic-plugins. Update existing TestMergeFromSpecDeployment assertions to match prepend behavior. Document list ordering semantics in docs/configuration.md. Assisted-by: Claude --- docs/configuration.md | 21 ++++++++++++++++++ pkg/model/deployment_test.go | 41 ++++++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e774c5b53..3cf4aee01 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -837,6 +837,27 @@ spec: claimName: dynamic-plugins-root ``` +##### List ordering + +When the patch introduces **new** list items (containers, init containers, volumes, etc.), they are **prepended** before the existing items in the default configuration. Items that match an existing entry by name are **merged in-place** and retain their original position. + +This is particularly relevant for init containers, where execution order [matters](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#detailed-behavior). For example, if the default configuration defines an `install-dynamic-plugins` init container, patching in a custom init container will place it **before** `install-dynamic-plugins`: + +```yaml +spec: + deployment: + patch: + spec: + template: + spec: + initContainers: + - name: my-init + image: busybox + command: ["sh", "-c", "echo preparing"] +``` + +The resulting init container order will be: `my-init`, then `install-dynamic-plugins`. + ##### Handling Discriminated Unions When patching Kubernetes resources that contain **discriminated unions** (fields where one field determines which other fields are valid), you may need to use the `$patch: delete` directive to remove conflicting fields. diff --git a/pkg/model/deployment_test.go b/pkg/model/deployment_test.go index c8e3f6efb..0784f27eb 100644 --- a/pkg/model/deployment_test.go +++ b/pkg/model/deployment_test.go @@ -140,23 +140,50 @@ spec: assert.Equal(t, "java", model.backstageDeployment.deployable.GetObject().GetLabels()["mylabel"]) assert.Equal(t, "backstage", model.backstageDeployment.deployable.PodObjectMeta().GetLabels()["pod"]) - // sidecar added + // sidecar prepended assert.Equal(t, 2, len(model.backstageDeployment.podSpec().Containers)) - assert.Equal(t, "sidecar", model.backstageDeployment.podSpec().Containers[1].Name) - assert.Equal(t, "my-image:1.0.0", model.backstageDeployment.podSpec().Containers[1].Image) + assert.Equal(t, "sidecar", model.backstageDeployment.podSpec().Containers[0].Name) + assert.Equal(t, "my-image:1.0.0", model.backstageDeployment.podSpec().Containers[0].Image) // backstage container resources updated assert.Equal(t, "backstage-backend", model.backstageDeployment.container().Name) assert.Equal(t, "257Mi", model.backstageDeployment.container().Resources.Requests.Memory().String()) - // volumes - // dynamic-plugins-root, dynamic-plugins-npmrc, dynamic-plugins-auth, my-vol + // volumes: dynamic-plugins-root (merged in-place), my-vol (new), dynamic-plugins-npmrc, dynamic-plugins-registry-auth assert.Equal(t, 4, len(model.backstageDeployment.podSpec().Volumes)) assert.Equal(t, "dynamic-plugins-root", model.backstageDeployment.podSpec().Volumes[0].Name) // overrides StorageClassName assert.Equal(t, "special", *model.backstageDeployment.podSpec().Volumes[0].Ephemeral.VolumeClaimTemplate.Spec.StorageClassName) - // adds new volume - assert.Equal(t, "my-vol", model.backstageDeployment.podSpec().Volumes[3].Name) + // new volume added + assert.Equal(t, "my-vol", model.backstageDeployment.podSpec().Volumes[1].Name) +} + +// https://redhat.atlassian.net/browse/RHDHBUGS-2900 +func TestInitContainerOrderInSpecDeployment(t *testing.T) { + bs := *deploymentTestBackstage.DeepCopy() + bs.Spec.Deployment = &api.BackstageDeployment{} + bs.Spec.Deployment.Patch = &apiextensionsv1.JSON{ + Raw: []byte(` +spec: + template: + spec: + initContainers: + - name: my-init + image: busybox + command: ["sh", "-c", "echo init"] +`), + } + + testObj := createBackstageTest(bs).withDefaultConfig(true). + addToDefaultConfig("deployment.yaml", "rhdh-deployment.yaml") + + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + assert.NoError(t, err) + + initContainers := model.backstageDeployment.podSpec().InitContainers + assert.Equal(t, 2, len(initContainers)) + assert.Equal(t, "my-init", initContainers[0].Name) + assert.Equal(t, "install-dynamic-plugins", initContainers[1].Name) } func TestImageInCRPrevailsOnEnvVar(t *testing.T) { From 08e506ba14b99a4549119f667426ff510307a67b Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 12 May 2026 09:26:36 +0200 Subject: [PATCH 03/10] docs: document append workaround for init container ordering [RHDHBUGS-2900] Show how to place a custom init container after install-dynamic-plugins by referencing the existing container by name first in the patch. Assisted-by: Claude --- docs/configuration.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 3cf4aee01..0354a85a3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -858,6 +858,24 @@ spec: The resulting init container order will be: `my-init`, then `install-dynamic-plugins`. +To place a custom init container **after** an existing one, reference the existing container by name in the patch before your custom entry: + +```yaml +spec: + deployment: + patch: + spec: + template: + spec: + initContainers: + - name: install-dynamic-plugins + - name: my-init + image: busybox + command: ["sh", "-c", "echo preparing"] +``` + +The resulting init container order will be: `install-dynamic-plugins`, then `my-init`. Listing `install-dynamic-plugins` by name (without any other fields) anchors it in position, and new items listed after it are placed accordingly. + ##### Handling Discriminated Unions When patching Kubernetes resources that contain **discriminated unions** (fields where one field determines which other fields are valid), you may need to use the `$patch: delete` directive to remove conflicting fields. From 5b2b84b3b25fba21630ba5cf075bc6ce09461103 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 12 May 2026 10:06:09 +0200 Subject: [PATCH 04/10] test: add subtest for placing init container after existing one [RHDHBUGS-2900] Cover both ordering directions in TestInitContainerOrderInSpecDeployment: prepending before and anchoring after install-dynamic-plugins. Assisted-by: Claude --- pkg/model/deployment_test.go | 92 ++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/pkg/model/deployment_test.go b/pkg/model/deployment_test.go index 0784f27eb..f4d0ac6d7 100644 --- a/pkg/model/deployment_test.go +++ b/pkg/model/deployment_test.go @@ -160,10 +160,14 @@ spec: // https://redhat.atlassian.net/browse/RHDHBUGS-2900 func TestInitContainerOrderInSpecDeployment(t *testing.T) { - bs := *deploymentTestBackstage.DeepCopy() - bs.Spec.Deployment = &api.BackstageDeployment{} - bs.Spec.Deployment.Patch = &apiextensionsv1.JSON{ - Raw: []byte(` + tests := []struct { + name string + patch string + expected []string + }{ + { + name: "new init container runs before existing", + patch: ` spec: template: spec: @@ -171,19 +175,77 @@ spec: - name: my-init image: busybox command: ["sh", "-c", "echo init"] -`), +`, + expected: []string{"my-init", "install-dynamic-plugins"}, + }, + { + name: "new init container runs before existing by anchoring", + patch: ` +spec: + template: + spec: + initContainers: + - name: my-init + image: busybox + command: ["sh", "-c", "echo init"] + - name: install-dynamic-plugins +`, + expected: []string{"my-init", "install-dynamic-plugins"}, + }, + { + name: "new init container runs after existing by anchoring", + patch: ` +spec: + template: + spec: + initContainers: + - name: install-dynamic-plugins + - name: my-init + image: busybox + command: ["sh", "-c", "echo init"] +`, + expected: []string{"install-dynamic-plugins", "my-init"}, + }, + { + name: "multiple new init containers with mixed ordering", + patch: ` +spec: + template: + spec: + initContainers: + - name: pre-init + image: busybox + command: ["sh", "-c", "echo pre"] + - name: install-dynamic-plugins + - name: post-init + image: busybox + command: ["sh", "-c", "echo post"] +`, + expected: []string{"pre-init", "install-dynamic-plugins", "post-init"}, + }, } - testObj := createBackstageTest(bs).withDefaultConfig(true). - addToDefaultConfig("deployment.yaml", "rhdh-deployment.yaml") - - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) - assert.NoError(t, err) - - initContainers := model.backstageDeployment.podSpec().InitContainers - assert.Equal(t, 2, len(initContainers)) - assert.Equal(t, "my-init", initContainers[0].Name) - assert.Equal(t, "install-dynamic-plugins", initContainers[1].Name) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bs := *deploymentTestBackstage.DeepCopy() + bs.Spec.Deployment = &api.BackstageDeployment{} + bs.Spec.Deployment.Patch = &apiextensionsv1.JSON{ + Raw: []byte(tt.patch), + } + + testObj := createBackstageTest(bs).withDefaultConfig(true). + addToDefaultConfig("deployment.yaml", "rhdh-deployment.yaml") + + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + assert.NoError(t, err) + + initContainers := model.backstageDeployment.podSpec().InitContainers + assert.Equal(t, len(tt.expected), len(initContainers)) + for i, name := range tt.expected { + assert.Equal(t, name, initContainers[i].Name) + } + }) + } } func TestImageInCRPrevailsOnEnvVar(t *testing.T) { From d89236f25da05796ef227287b82c3cd9feffb4b1 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 12 May 2026 12:43:45 +0200 Subject: [PATCH 05/10] fix: two-pass merge to preserve $patch directives with ListPrepend [RHDHBUGS-2900] kyaml's merge2 silently ignores $patch directives (e.g. $patch: replace) when ListIncreaseDirection is set to ListPrepend. Work around this by doing a second merge pass with default options to apply $patch directives, after the first pass has already established correct list item positions. Assisted-by: Claude --- pkg/model/deployment.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/model/deployment.go b/pkg/model/deployment.go index 611ea84d3..6190b9176 100644 --- a/pkg/model/deployment.go +++ b/pkg/model/deployment.go @@ -227,11 +227,24 @@ func (b *BackstageDeployment) setDeployment(backstage api.Backstage) error { return fmt.Errorf("can not marshal deployment object: %w", err) } + // TODO(asoro): once https://github.com/kubernetes-sigs/kustomize/issues/6146 is resolved, + // remove this two-pass merge and use only ListPrepend. + // + // RHDHBUGS-2900: Two-pass merge to work around a kyaml bug where ListPrepend + // silently breaks $patch directives (e.g. $patch: replace). + // Pass 1: ListPrepend to get correct ordering of new list items. + // Pass 2: default options to apply $patch directives. All items + // already exist from pass 1 so positions are preserved. merged, err := merge2.MergeStrings(string(conf.Raw), string(deplStr), false, kyaml.MergeOptions{ListIncreaseDirection: kyaml.MergeOptionsListPrepend}) if err != nil { return fmt.Errorf("can not merge spec.deployment: %w", err) } + merged, err = merge2.MergeStrings(string(conf.Raw), merged, false, kyaml.MergeOptions{}) + if err != nil { + return fmt.Errorf("can not merge spec.deployment: %w", err) + } + b.deployable.SetEmpty() err = yaml.Unmarshal([]byte(merged), b.deployable.GetObject()) if err != nil { From 81f1512f01b51b5d58e08d44b51ade3e6deec16c Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Mon, 18 May 2026 13:50:58 +0200 Subject: [PATCH 06/10] docs: clarify init container ordering and $setElementOrder limitation [RHDHBUGS-2900] Recommend listing all init containers explicitly in the desired order, and note that $setElementOrder is not supported by the kyaml library. Co-authored-by: Gennady Azarenkov Assisted-by: Claude --- docs/configuration.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0354a85a3..f6613e649 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -839,9 +839,11 @@ spec: ##### List ordering -When the patch introduces **new** list items (containers, init containers, volumes, etc.), they are **prepended** before the existing items in the default configuration. Items that match an existing entry by name are **merged in-place** and retain their original position. +If the ordering of list items matters (e.g., init containers, where execution order is [significant](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#detailed-behavior)), include **all** init containers in the patch in the desired order — including any existing ones like `install-dynamic-plugins`. -This is particularly relevant for init containers, where execution order [matters](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#detailed-behavior). For example, if the default configuration defines an `install-dynamic-plugins` init container, patching in a custom init container will place it **before** `install-dynamic-plugins`: +> **Note:** The `$setElementOrder` directive from the Kubernetes Strategic Merge Patch specification is **not supported** by the [kyaml](https://github.com/kubernetes-sigs/kustomize/tree/master/kyaml) (kustomize) library used by this operator. Listing all items explicitly in the patch is the recommended way to control ordering. + +For example, to run a custom init container **before** `install-dynamic-plugins`: ```yaml spec: @@ -854,11 +856,12 @@ spec: - name: my-init image: busybox command: ["sh", "-c", "echo preparing"] + - name: install-dynamic-plugins ``` The resulting init container order will be: `my-init`, then `install-dynamic-plugins`. -To place a custom init container **after** an existing one, reference the existing container by name in the patch before your custom entry: +To run a custom init container **after** `install-dynamic-plugins`: ```yaml spec: From 3d7b40747b3648a2292b0128b637a3cf97d63b01 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 19 May 2026 16:01:39 +0200 Subject: [PATCH 07/10] feat: make ListPrepend merge opt-in via Backstage CR annotation [RHDHBUGS-2900] Introduce rhdh.redhat.com/deployment-patch-list-merge-mode annotation to control list merge behavior in spec.deployment.patch. Default behavior (append) is preserved; setting the annotation to "prepend" enables ListPrepend with a two-pass merge workaround for $patch directives. Co-authored-by: Gennady Azarenkov Assisted-by: Claude --- docs/configuration.md | 16 +++++++++++++++- pkg/model/deployment.go | 29 +++++++++++++++++------------ pkg/model/deployment_test.go | 16 ++++++++++------ 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f6613e649..deeb721a8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -839,13 +839,21 @@ spec: ##### List ordering -If the ordering of list items matters (e.g., init containers, where execution order is [significant](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#detailed-behavior)), include **all** init containers in the patch in the desired order — including any existing ones like `install-dynamic-plugins`. +By default, new list items in the patch (containers, init containers, volumes, etc.) are **appended** after existing items. The position of new items in the patch does not affect their placement in the result. + +If the ordering of list items matters (e.g., init containers, where execution order is [significant](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#detailed-behavior)), you can opt in to **prepend** mode by setting the `rhdh.redhat.com/deployment-patch-list-merge-mode` annotation on the Backstage CR. In prepend mode, you can control the exact ordering by listing all items (including existing ones) explicitly in the patch in the desired order. > **Note:** The `$setElementOrder` directive from the Kubernetes Strategic Merge Patch specification is **not supported** by the [kyaml](https://github.com/kubernetes-sigs/kustomize/tree/master/kyaml) (kustomize) library used by this operator. Listing all items explicitly in the patch is the recommended way to control ordering. For example, to run a custom init container **before** `install-dynamic-plugins`: ```yaml +apiVersion: rhdh.redhat.com/v1alpha5 +kind: Backstage +metadata: + name: my-rhdh + annotations: + rhdh.redhat.com/deployment-patch-list-merge-mode: prepend spec: deployment: patch: @@ -864,6 +872,12 @@ The resulting init container order will be: `my-init`, then `install-dynamic-plu To run a custom init container **after** `install-dynamic-plugins`: ```yaml +apiVersion: rhdh.redhat.com/v1alpha5 +kind: Backstage +metadata: + name: my-rhdh + annotations: + rhdh.redhat.com/deployment-patch-list-merge-mode: prepend spec: deployment: patch: diff --git a/pkg/model/deployment.go b/pkg/model/deployment.go index 6190b9176..dcf378220 100644 --- a/pkg/model/deployment.go +++ b/pkg/model/deployment.go @@ -31,6 +31,7 @@ const ( const BackstageImageEnvVar = "RELATED_IMAGE_backstage" const DefaultMountDir = "/opt/app-root/src" const ExtConfigHashAnnotation = "rhdh.redhat.com/ext-config-hash" +const ListMergeAnnotation = "rhdh.redhat.com/deployment-patch-list-merge-mode" type BackstageDeploymentFactory struct{} @@ -227,24 +228,28 @@ func (b *BackstageDeployment) setDeployment(backstage api.Backstage) error { return fmt.Errorf("can not marshal deployment object: %w", err) } - // TODO(asoro): once https://github.com/kubernetes-sigs/kustomize/issues/6146 is resolved, - // remove this two-pass merge and use only ListPrepend. - // - // RHDHBUGS-2900: Two-pass merge to work around a kyaml bug where ListPrepend - // silently breaks $patch directives (e.g. $patch: replace). - // Pass 1: ListPrepend to get correct ordering of new list items. - // Pass 2: default options to apply $patch directives. All items - // already exist from pass 1 so positions are preserved. - merged, err := merge2.MergeStrings(string(conf.Raw), string(deplStr), false, kyaml.MergeOptions{ListIncreaseDirection: kyaml.MergeOptionsListPrepend}) - if err != nil { - return fmt.Errorf("can not merge spec.deployment: %w", err) + mergeOpts := kyaml.MergeOptions{} + switch backstage.GetAnnotations()[ListMergeAnnotation] { + case "prepend": + mergeOpts.ListIncreaseDirection = kyaml.MergeOptionsListPrepend + case "append": + mergeOpts.ListIncreaseDirection = kyaml.MergeOptionsListAppend } - merged, err = merge2.MergeStrings(string(conf.Raw), merged, false, kyaml.MergeOptions{}) + merged, err := merge2.MergeStrings(string(conf.Raw), string(deplStr), false, mergeOpts) if err != nil { return fmt.Errorf("can not merge spec.deployment: %w", err) } + // TODO(asoro): once https://github.com/kubernetes-sigs/kustomize/issues/6146 is resolved, + // remove this second pass and use only ListPrepend above. + if mergeOpts.ListIncreaseDirection == kyaml.MergeOptionsListPrepend { + merged, err = merge2.MergeStrings(string(conf.Raw), merged, false, kyaml.MergeOptions{}) + if err != nil { + return fmt.Errorf("can not merge spec.deployment: %w", err) + } + } + b.deployable.SetEmpty() err = yaml.Unmarshal([]byte(merged), b.deployable.GetObject()) if err != nil { diff --git a/pkg/model/deployment_test.go b/pkg/model/deployment_test.go index f4d0ac6d7..67b0ee530 100644 --- a/pkg/model/deployment_test.go +++ b/pkg/model/deployment_test.go @@ -140,22 +140,23 @@ spec: assert.Equal(t, "java", model.backstageDeployment.deployable.GetObject().GetLabels()["mylabel"]) assert.Equal(t, "backstage", model.backstageDeployment.deployable.PodObjectMeta().GetLabels()["pod"]) - // sidecar prepended + // sidecar appended (default merge behavior — no prepend annotation) assert.Equal(t, 2, len(model.backstageDeployment.podSpec().Containers)) - assert.Equal(t, "sidecar", model.backstageDeployment.podSpec().Containers[0].Name) - assert.Equal(t, "my-image:1.0.0", model.backstageDeployment.podSpec().Containers[0].Image) + assert.Equal(t, "backstage-backend", model.backstageDeployment.podSpec().Containers[0].Name) + assert.Equal(t, "sidecar", model.backstageDeployment.podSpec().Containers[1].Name) + assert.Equal(t, "my-image:1.0.0", model.backstageDeployment.podSpec().Containers[1].Image) // backstage container resources updated assert.Equal(t, "backstage-backend", model.backstageDeployment.container().Name) assert.Equal(t, "257Mi", model.backstageDeployment.container().Resources.Requests.Memory().String()) - // volumes: dynamic-plugins-root (merged in-place), my-vol (new), dynamic-plugins-npmrc, dynamic-plugins-registry-auth + // volumes: dynamic-plugins-root (merged in-place), dynamic-plugins-npmrc, dynamic-plugins-registry-auth, my-vol (appended) assert.Equal(t, 4, len(model.backstageDeployment.podSpec().Volumes)) assert.Equal(t, "dynamic-plugins-root", model.backstageDeployment.podSpec().Volumes[0].Name) // overrides StorageClassName assert.Equal(t, "special", *model.backstageDeployment.podSpec().Volumes[0].Ephemeral.VolumeClaimTemplate.Spec.StorageClassName) - // new volume added - assert.Equal(t, "my-vol", model.backstageDeployment.podSpec().Volumes[1].Name) + // new volume appended at end + assert.Equal(t, "my-vol", model.backstageDeployment.podSpec().Volumes[3].Name) } // https://redhat.atlassian.net/browse/RHDHBUGS-2900 @@ -228,6 +229,9 @@ spec: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bs := *deploymentTestBackstage.DeepCopy() + bs.ObjectMeta.Annotations = map[string]string{ + ListMergeAnnotation: "prepend", + } bs.Spec.Deployment = &api.BackstageDeployment{} bs.Spec.Deployment.Patch = &apiextensionsv1.JSON{ Raw: []byte(tt.patch), From 8ec632ebdbfcd9d66f45d4da90910cc7576cde9d Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 19 May 2026 16:11:10 +0200 Subject: [PATCH 08/10] docs: clarify default merge behavior for matching list items [RHDHBUGS-2900] Assisted-by: Claude --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index deeb721a8..e4c80c1ad 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -839,7 +839,7 @@ spec: ##### List ordering -By default, new list items in the patch (containers, init containers, volumes, etc.) are **appended** after existing items. The position of new items in the patch does not affect their placement in the result. +By default, new list items in the patch (containers, init containers, volumes, etc.) are **appended** after existing items. Items that match an existing entry by name are merged in-place and retain their original position — their placement in the patch does not reorder them in the result. If the ordering of list items matters (e.g., init containers, where execution order is [significant](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#detailed-behavior)), you can opt in to **prepend** mode by setting the `rhdh.redhat.com/deployment-patch-list-merge-mode` annotation on the Backstage CR. In prepend mode, you can control the exact ordering by listing all items (including existing ones) explicitly in the patch in the desired order. From 5928c49addd124608202e642c053d9e5c63271ca Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 19 May 2026 16:15:26 +0200 Subject: [PATCH 09/10] fix: remove embedded field from selector to fix lint error [RHDHBUGS-2900] Assisted-by: Claude --- pkg/model/deployment_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/deployment_test.go b/pkg/model/deployment_test.go index 67b0ee530..e956f31b0 100644 --- a/pkg/model/deployment_test.go +++ b/pkg/model/deployment_test.go @@ -229,7 +229,7 @@ spec: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bs := *deploymentTestBackstage.DeepCopy() - bs.ObjectMeta.Annotations = map[string]string{ + bs.Annotations = map[string]string{ ListMergeAnnotation: "prepend", } bs.Spec.Deployment = &api.BackstageDeployment{} From dc23f1a05319d31a53580ceab19ef1fe644962a3 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Tue, 19 May 2026 16:33:29 +0200 Subject: [PATCH 10/10] docs: mention unsupported SMP directives ($deleteFromPrimitiveList, $retainKeys) [RHDHBUGS-2900] Assisted-by: Claude --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index e4c80c1ad..284a50a3a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -843,7 +843,7 @@ By default, new list items in the patch (containers, init containers, volumes, e If the ordering of list items matters (e.g., init containers, where execution order is [significant](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#detailed-behavior)), you can opt in to **prepend** mode by setting the `rhdh.redhat.com/deployment-patch-list-merge-mode` annotation on the Backstage CR. In prepend mode, you can control the exact ordering by listing all items (including existing ones) explicitly in the patch in the desired order. -> **Note:** The `$setElementOrder` directive from the Kubernetes Strategic Merge Patch specification is **not supported** by the [kyaml](https://github.com/kubernetes-sigs/kustomize/tree/master/kyaml) (kustomize) library used by this operator. Listing all items explicitly in the patch is the recommended way to control ordering. +> **Note:** The `$setElementOrder`, `$deleteFromPrimitiveList`, and `$retainKeys` directives from the Kubernetes Strategic Merge Patch specification are **not supported** by the [kyaml](https://github.com/kubernetes-sigs/kustomize/tree/master/kyaml) (kustomize) library used by this operator. Listing all items explicitly in the patch is the recommended way to control ordering. For example, to run a custom init container **before** `install-dynamic-plugins`: