Skip to content

Commit 1fb1216

Browse files
stuggiclaude
andcommitted
Add openstack.org/paused annotation to pause reconciliation
When the openstack.org/paused annotation is present on a BaremetalSet or ProvisionServer resource, the controller skips reconciliation entirely with no status writes. Deletion is not affected — finalizer cleanup always proceeds. The annotation constant and IsPaused helper are provided by lib-common (modules/common/annotations). Jira: OSPRH-29529 Jira: OSPRH-22913 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Martin Schuppert <mschuppert@redhat.com>
1 parent b5adaae commit 1fb1216

8 files changed

Lines changed: 163 additions & 17 deletions

api/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ require (
77
github.com/go-playground/validator/v10 v10.30.1
88
github.com/metal3-io/baremetal-operator/apis v0.9.3
99
github.com/onsi/ginkgo/v2 v2.28.2
10-
github.com/onsi/gomega v1.39.1
11-
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981
10+
github.com/onsi/gomega v1.40.0
11+
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0
1212
k8s.io/api v0.31.14
1313
k8s.io/apimachinery v0.31.14
1414
sigs.k8s.io/controller-runtime v0.19.7

api/go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
105105
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
106106
github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps=
107107
github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
108-
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
109-
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
110-
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 h1:v1viH0gmNb+AXMg/0GxDcj8VUTdjVLotfOIGrNyMxHk=
111-
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:I/VBXZLdjk8DUGsEbB+Ha72JBFYYntP7Pm2FpEto9K8=
108+
github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc=
109+
github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
110+
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0 h1:vkFvn06Ns9qW4AbzFjFDu8ioosRmhkEZiDrO3DOQhLg=
111+
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0/go.mod h1:aIuG6lx3aS0vnXweRNdR/Q0SlfOsLIo0OzrqKK7C6xs=
112112
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
113113
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
114114
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -144,8 +144,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
144144
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
145145
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
146146
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
147-
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
148-
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
147+
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
148+
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
149149
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
150150
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
151151
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ require (
99
github.com/google/uuid v1.6.0
1010
github.com/metal3-io/baremetal-operator/apis v0.9.3
1111
github.com/onsi/ginkgo/v2 v2.28.2
12-
github.com/onsi/gomega v1.39.1
13-
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981
12+
github.com/onsi/gomega v1.40.0
13+
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0
1414
github.com/openstack-k8s-operators/openstack-baremetal-operator/api v0.0.0-00010101000000-000000000000
1515
github.com/spf13/cobra v1.10.2
1616
k8s.io/api v0.31.14
@@ -83,7 +83,7 @@ require (
8383
go.opentelemetry.io/otel/trace v1.28.0 // indirect
8484
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
8585
go.uber.org/multierr v1.11.0 // indirect
86-
go.uber.org/zap v1.27.1 // indirect
86+
go.uber.org/zap v1.28.0 // indirect
8787
go.yaml.in/yaml/v3 v3.0.4 // indirect
8888
golang.org/x/crypto v0.48.0 // indirect
8989
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
131131
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
132132
github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps=
133133
github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
134-
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
135-
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
134+
github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc=
135+
github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
136136
github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyUt0GEdoAE+r5TXy7YS21yNEo+2U=
137137
github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo=
138-
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 h1:v1viH0gmNb+AXMg/0GxDcj8VUTdjVLotfOIGrNyMxHk=
139-
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:I/VBXZLdjk8DUGsEbB+Ha72JBFYYntP7Pm2FpEto9K8=
138+
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0 h1:vkFvn06Ns9qW4AbzFjFDu8ioosRmhkEZiDrO3DOQhLg=
139+
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0/go.mod h1:aIuG6lx3aS0vnXweRNdR/Q0SlfOsLIo0OzrqKK7C6xs=
140140
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
141141
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
142142
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -196,8 +196,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
196196
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
197197
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
198198
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
199-
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
200-
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
199+
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
200+
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
201201
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
202202
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
203203
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

internal/controller/openstackbaremetalset_controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242

4343
"github.com/go-logr/logr"
4444
metal3v1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
45+
"github.com/openstack-k8s-operators/lib-common/modules/common/annotations"
4546
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
4647
"github.com/openstack-k8s-operators/lib-common/modules/common/env"
4748
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
@@ -94,6 +95,12 @@ func (r *OpenStackBaremetalSetReconciler) Reconcile(ctx context.Context, req ctr
9495
return ctrl.Result{}, err
9596
}
9697

98+
// If paused and not being deleted, return immediately — no status updates
99+
if annotations.IsPaused(instance) && instance.DeletionTimestamp.IsZero() {
100+
r.Log.Info("Reconciliation paused", "instance", instance.Name)
101+
return ctrl.Result{}, nil
102+
}
103+
97104
helper, err := helper.NewHelper(
98105
instance,
99106
r.Client,

internal/controller/openstackprovisionserver_controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343

4444
"github.com/go-logr/logr"
4545
"github.com/openstack-k8s-operators/lib-common/modules/common"
46+
"github.com/openstack-k8s-operators/lib-common/modules/common/annotations"
4647
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
4748
"github.com/openstack-k8s-operators/lib-common/modules/common/configmap"
4849
"github.com/openstack-k8s-operators/lib-common/modules/common/deployment"
@@ -114,6 +115,12 @@ func (r *OpenStackProvisionServerReconciler) Reconcile(ctx context.Context, req
114115
return ctrl.Result{}, err
115116
}
116117

118+
// If paused and not being deleted, return immediately — no status updates
119+
if annotations.IsPaused(instance) && instance.DeletionTimestamp.IsZero() {
120+
r.Log.Info("Reconciliation paused", "instance", instance.Name)
121+
return ctrl.Result{}, nil
122+
}
123+
117124
helper, err := helper.NewHelper(
118125
instance,
119126
r.Client,

tests/functional/openstackbaremetalset_controller_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,70 @@ var _ = Describe("BaremetalSet Test", func() {
11491149
})
11501150
})
11511151

1152+
When("A BaremetalSet resource is created with pause annotation", func() {
1153+
BeforeEach(func() {
1154+
DeferCleanup(th.DeleteInstance, CreateBaremetalHost(bmhName))
1155+
bmh := GetBaremetalHost(bmhName)
1156+
Eventually(func(g Gomega) {
1157+
bmh.Status.Provisioning.State = metal3v1.StateAvailable
1158+
g.Expect(th.K8sClient.Status().Update(th.Ctx, bmh)).To(Succeed())
1159+
}, th.Timeout, th.Interval).Should(Succeed())
1160+
1161+
raw := DefaultBaremetalSetTemplate(baremetalSetName, DefaultBaremetalSetSpec(bmhName, false))
1162+
raw["metadata"].(map[string]any)["annotations"] = map[string]string{
1163+
"openstack.org/paused": "",
1164+
}
1165+
DeferCleanup(th.DeleteInstance, th.CreateUnstructured(raw))
1166+
})
1167+
1168+
It("should not update status conditions while paused", func() {
1169+
Consistently(func(g Gomega) {
1170+
instance := &baremetalv1.OpenStackBaremetalSet{}
1171+
g.Expect(k8sClient.Get(ctx, baremetalSetName, instance)).To(Succeed())
1172+
g.Expect(instance.Status.Conditions).To(BeEmpty())
1173+
}, timeout, interval).Should(Succeed())
1174+
})
1175+
1176+
It("should resume reconciliation when pause annotation is removed", func() {
1177+
// Verify paused — no conditions
1178+
Consistently(func(g Gomega) {
1179+
instance := &baremetalv1.OpenStackBaremetalSet{}
1180+
g.Expect(k8sClient.Get(ctx, baremetalSetName, instance)).To(Succeed())
1181+
g.Expect(instance.Status.Conditions).To(BeEmpty())
1182+
}, timeout, interval).Should(Succeed())
1183+
1184+
// Remove annotation
1185+
Eventually(func(g Gomega) {
1186+
instance := &baremetalv1.OpenStackBaremetalSet{}
1187+
g.Expect(k8sClient.Get(ctx, baremetalSetName, instance)).To(Succeed())
1188+
instance.Annotations = map[string]string{}
1189+
g.Expect(k8sClient.Update(ctx, instance)).To(Succeed())
1190+
}, th.Timeout, th.Interval).Should(Succeed())
1191+
1192+
// Verify reconciliation resumes — conditions should appear
1193+
th.ExpectCondition(
1194+
baremetalSetName,
1195+
ConditionGetterFunc(BaremetalSetConditionGetter),
1196+
condition.ReadyCondition,
1197+
corev1.ConditionFalse,
1198+
)
1199+
})
1200+
1201+
It("should still allow deletion when paused", func() {
1202+
instance := &baremetalv1.OpenStackBaremetalSet{}
1203+
Eventually(func(g Gomega) {
1204+
g.Expect(k8sClient.Get(ctx, baremetalSetName, instance)).To(Succeed())
1205+
}, th.Timeout, th.Interval).Should(Succeed())
1206+
1207+
Expect(k8sClient.Delete(ctx, instance)).To(Succeed())
1208+
1209+
Eventually(func(g Gomega) {
1210+
err := k8sClient.Get(ctx, baremetalSetName, instance)
1211+
g.Expect(k8s_errors.IsNotFound(err)).To(BeTrue())
1212+
}, th.Timeout, th.Interval).Should(Succeed())
1213+
})
1214+
})
1215+
11521216
When("A BaremetalSet with PassThrough mode missing OSContainerImageURL", func() {
11531217
BeforeEach(func() {
11541218
DeferCleanup(th.DeleteInstance, CreateBaremetalHost(bmhName))

tests/functional/openstackprovisionserver_controller_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2525
. "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers"
2626
corev1 "k8s.io/api/core/v1"
27+
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
2728
"k8s.io/apimachinery/pkg/types"
2829
)
2930

@@ -102,4 +103,71 @@ var _ = Describe("ProvisionServer Test", func() {
102103
Expect(instance1.Spec.Port).ShouldNot(Equal(instance2.Spec.Port))
103104
})
104105
})
106+
107+
When("A ProvisionServer resource is created with pause annotation", func() {
108+
BeforeEach(func() {
109+
raw := map[string]interface{}{
110+
"apiVersion": "baremetal.openstack.org/v1beta1",
111+
"kind": "OpenStackProvisionServer",
112+
"metadata": map[string]interface{}{
113+
"name": provisionServerName.Name,
114+
"namespace": provisionServerName.Namespace,
115+
"annotations": map[string]string{
116+
"openstack.org/paused": "",
117+
},
118+
},
119+
"spec": map[string]interface{}{
120+
"osImage": "edpm-hardened-uefi.qcow2",
121+
"osContainerImageUrl": "quay.io/podified-antelope-centos9/edpm-hardened-uefi:current-podified",
122+
"apacheImageUrl": "registry.redhat.io/ubi9/httpd-24:latest",
123+
"agentImageUrl": "quay.io/openstack-k8s-operators/openstack-baremetal-operator-agent:latest",
124+
},
125+
}
126+
DeferCleanup(th.DeleteInstance, th.CreateUnstructured(raw))
127+
})
128+
129+
It("should not update status conditions while paused", func() {
130+
Consistently(func(g Gomega) {
131+
instance := &baremetalv1.OpenStackProvisionServer{}
132+
g.Expect(k8sClient.Get(ctx, provisionServerName, instance)).To(Succeed())
133+
g.Expect(instance.Status.Conditions).To(BeEmpty())
134+
}, timeout, interval).Should(Succeed())
135+
})
136+
137+
It("should resume reconciliation when pause annotation is removed", func() {
138+
Consistently(func(g Gomega) {
139+
instance := &baremetalv1.OpenStackProvisionServer{}
140+
g.Expect(k8sClient.Get(ctx, provisionServerName, instance)).To(Succeed())
141+
g.Expect(instance.Status.Conditions).To(BeEmpty())
142+
}, timeout, interval).Should(Succeed())
143+
144+
Eventually(func(g Gomega) {
145+
instance := &baremetalv1.OpenStackProvisionServer{}
146+
g.Expect(k8sClient.Get(ctx, provisionServerName, instance)).To(Succeed())
147+
instance.Annotations = map[string]string{}
148+
g.Expect(k8sClient.Update(ctx, instance)).To(Succeed())
149+
}, timeout, interval).Should(Succeed())
150+
151+
th.ExpectCondition(
152+
provisionServerName,
153+
ConditionGetterFunc(ProvisionServerConditionGetter),
154+
condition.ReadyCondition,
155+
corev1.ConditionFalse,
156+
)
157+
})
158+
159+
It("should still allow deletion when paused", func() {
160+
instance := &baremetalv1.OpenStackProvisionServer{}
161+
Eventually(func(g Gomega) {
162+
g.Expect(k8sClient.Get(ctx, provisionServerName, instance)).To(Succeed())
163+
}, timeout, interval).Should(Succeed())
164+
165+
Expect(k8sClient.Delete(ctx, instance)).To(Succeed())
166+
167+
Eventually(func(g Gomega) {
168+
err := k8sClient.Get(ctx, provisionServerName, instance)
169+
g.Expect(k8s_errors.IsNotFound(err)).To(BeTrue())
170+
}, timeout, interval).Should(Succeed())
171+
})
172+
})
105173
})

0 commit comments

Comments
 (0)