Skip to content

Commit 49cecea

Browse files
committed
Add minor-update target-stage gate annotation
Introduce the core.openstack.org/minor-update-target-stage annotation on OpenStackVersion. When set, the minor update controller completes all stages up to and including the named stage, marks the next stage as blocked (FalseCondition/RequestedReason), and pauses reconciliation. Removing the annotation or advancing it to a later stage resumes the update. Includes stage-name constants, the gated-message format string, controller logic for all seven stages, functional tests for block/resume/ advance scenarios, and updated operator documentation. AI-assisted: Cursor (Claude Sonnet 4.6 by Anthropic)
1 parent c31bbb1 commit 49cecea

7 files changed

Lines changed: 807 additions & 2 deletions

File tree

api/core/v1beta1/conditions.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,4 +596,7 @@ const (
596596

597597
// OpenStackVersionMinorUpdateAvailableMessage
598598
OpenStackVersionMinorUpdateAvailableMessage = "update available"
599+
600+
// OpenStackVersionMinorUpdateReadyGatedMessage - format string; arg is the target stage name
601+
OpenStackVersionMinorUpdateReadyGatedMessage = "Minor update progression stopped after stage: %s. Set annotation to any stage after %s to resume OpenStack update"
599602
)

api/core/v1beta1/openstackversion_types.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,63 @@ const (
3434
MinorUpdateControlPlane string = "Minor Update Controlplane In Progress"
3535
// MinorUpdateComplete -
3636
MinorUpdateComplete string = "Complete"
37+
38+
// MinorUpdateTargetStageAnnotation - specifies the update stage after which the minor update
39+
// should pause. All stages up to and including the named stage will be completed; subsequent
40+
// stages will be blocked until the annotation is removed or updated to a later stage.
41+
// Valid values: "ovn-controlplane", "ovn-dataplane", "rabbitmq", "mariadb", "memcached",
42+
// "keystone", "controlplane". Remove the annotation to let the update proceed to completion.
43+
MinorUpdateTargetStageAnnotation string = "core.openstack.org/minor-update-target-stage"
44+
45+
// MinorUpdateStageOVNControlplane - stage name for OVN controlplane update
46+
MinorUpdateStageOVNControlplane string = "ovn-controlplane"
47+
// MinorUpdateStageOVNDataplane - stage name for OVN dataplane update
48+
MinorUpdateStageOVNDataplane string = "ovn-dataplane"
49+
// MinorUpdateStageRabbitMQ - stage name for RabbitMQ update
50+
MinorUpdateStageRabbitMQ string = "rabbitmq"
51+
// MinorUpdateStageMariaDB - stage name for MariaDB update
52+
MinorUpdateStageMariaDB string = "mariadb"
53+
// MinorUpdateStageMemcached - stage name for Memcached update
54+
MinorUpdateStageMemcached string = "memcached"
55+
// MinorUpdateStageKeystone - stage name for Keystone update
56+
MinorUpdateStageKeystone string = "keystone"
57+
// MinorUpdateStageControlplane - stage name for full controlplane update
58+
MinorUpdateStageControlplane string = "controlplane"
3759
)
3860

61+
// validMinorUpdateTargetStages lists allowed values for MinorUpdateTargetStageAnnotation.
62+
var validMinorUpdateTargetStages = map[string]struct{}{
63+
MinorUpdateStageOVNControlplane: {},
64+
MinorUpdateStageOVNDataplane: {},
65+
MinorUpdateStageRabbitMQ: {},
66+
MinorUpdateStageMariaDB: {},
67+
MinorUpdateStageMemcached: {},
68+
MinorUpdateStageKeystone: {},
69+
MinorUpdateStageControlplane: {},
70+
}
71+
72+
// IsValidMinorUpdateTargetStage reports whether v is a supported minor-update target stage name.
73+
func IsValidMinorUpdateTargetStage(v string) bool {
74+
if v == "" {
75+
return false
76+
}
77+
_, ok := validMinorUpdateTargetStages[v]
78+
return ok
79+
}
80+
81+
// ValidMinorUpdateTargetStages returns allowed annotation values in rollout order.
82+
func ValidMinorUpdateTargetStages() []string {
83+
return []string{
84+
MinorUpdateStageOVNControlplane,
85+
MinorUpdateStageOVNDataplane,
86+
MinorUpdateStageRabbitMQ,
87+
MinorUpdateStageMariaDB,
88+
MinorUpdateStageMemcached,
89+
MinorUpdateStageKeystone,
90+
MinorUpdateStageControlplane,
91+
}
92+
}
93+
3994
// OpenStackVersionSpec - defines the desired state of OpenStackVersion
4095
type OpenStackVersionSpec struct {
4196

api/core/v1beta1/openstackversion_webhook.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"os"
2323
"reflect"
24+
"strings"
2425

2526
apierrors "k8s.io/apimachinery/pkg/api/errors"
2627
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -59,7 +60,6 @@ func (r *OpenStackVersion) Default() {
5960
// ValidateCreate validates the OpenStackVersion on creation
6061
func (r *OpenStackVersion) ValidateCreate(ctx context.Context, c goClient.Client) (admission.Warnings, error) {
6162
openstackversionlog.Info("validate create", "name", r.Name)
62-
6363
if r.Spec.TargetVersion != openstackVersionDefaults.AvailableVersion {
6464
return nil, apierrors.NewForbidden(
6565
schema.GroupResource{
@@ -114,6 +114,10 @@ func (r *OpenStackVersion) ValidateCreate(ctx context.Context, c goClient.Client
114114
func (r *OpenStackVersion) ValidateUpdate(ctx context.Context, old runtime.Object, c goClient.Client) (admission.Warnings, error) {
115115
openstackversionlog.Info("validate update", "name", r.Name)
116116

117+
if err := validateMinorUpdateTargetStageAnnotation(r.Annotations, r.GetName()); err != nil {
118+
return nil, err
119+
}
120+
117121
_, ok := r.Status.ContainerImageVersionDefaults[r.Spec.TargetVersion]
118122
if r.Spec.TargetVersion != openstackVersionDefaults.AvailableVersion && !ok {
119123
return nil, apierrors.NewForbidden(
@@ -174,6 +178,48 @@ func (r *OpenStackVersion) ValidateUpdate(ctx context.Context, old runtime.Objec
174178
return nil, nil
175179
}
176180

181+
func validateMinorUpdateTargetStageAnnotation(annotations map[string]string, resourceName string) error {
182+
if annotations == nil {
183+
return nil
184+
}
185+
stage, ok := annotations[MinorUpdateTargetStageAnnotation]
186+
if !ok {
187+
return nil
188+
}
189+
annotationField := "metadata.annotations[" + MinorUpdateTargetStageAnnotation + "]"
190+
if stage == "" {
191+
return apierrors.NewForbidden(
192+
schema.GroupResource{
193+
Group: GroupVersion.WithKind("OpenStackVersion").Group,
194+
Resource: GroupVersion.WithKind("OpenStackVersion").Kind,
195+
}, resourceName, &field.Error{
196+
Type: field.ErrorTypeForbidden,
197+
Field: annotationField,
198+
BadValue: stage,
199+
Detail: "the annotation value must not be empty; remove the annotation or set a valid stage name",
200+
},
201+
)
202+
}
203+
if !IsValidMinorUpdateTargetStage(stage) {
204+
return apierrors.NewForbidden(
205+
schema.GroupResource{
206+
Group: GroupVersion.WithKind("OpenStackVersion").Group,
207+
Resource: GroupVersion.WithKind("OpenStackVersion").Kind,
208+
}, resourceName, &field.Error{
209+
Type: field.ErrorTypeForbidden,
210+
Field: annotationField,
211+
BadValue: stage,
212+
Detail: fmt.Sprintf(
213+
"invalid target stage %q; must be one of: %s",
214+
stage,
215+
strings.Join(ValidMinorUpdateTargetStages(), ", "),
216+
),
217+
},
218+
)
219+
}
220+
return nil
221+
}
222+
177223
// hasAnyCustomImage checks if any image field in CustomContainerImages is set
178224
func hasAnyCustomImage(images CustomContainerImages) bool {
179225
// Check CinderVolumeImages map

api/core/v1beta1/openstackversion_webhook_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,82 @@ var _ = Describe("OpenStackVersion Webhook", func() {
181181
Expect(err.Error()).To(ContainSubstring("failed to convert old object to OpenStackVersion"))
182182
})
183183
})
184+
185+
Context("MinorUpdateTargetStageAnnotation validation", func() {
186+
187+
BeforeEach(func() {
188+
SetupOpenStackVersionDefaults(OpenStackVersionDefaults{
189+
AvailableVersion: "1.1.0",
190+
})
191+
})
192+
193+
It("should reject update when annotation value is invalid", func() {
194+
oldVersion := &OpenStackVersion{
195+
ObjectMeta: metav1.ObjectMeta{
196+
Name: "test-version",
197+
Namespace: "test-namespace",
198+
},
199+
Spec: OpenStackVersionSpec{TargetVersion: "1.1.0"},
200+
Status: OpenStackVersionStatus{
201+
ContainerImageVersionDefaults: map[string]*ContainerDefaults{
202+
"1.1.0": {},
203+
},
204+
},
205+
}
206+
newVersion := oldVersion.DeepCopy()
207+
newVersion.Annotations = map[string]string{
208+
MinorUpdateTargetStageAnnotation: "tyop",
209+
}
210+
211+
_, err := newVersion.ValidateUpdate(context.Background(), oldVersion, nil)
212+
Expect(err).To(HaveOccurred())
213+
Expect(err.Error()).To(ContainSubstring(`invalid target stage "tyop"`))
214+
Expect(err.Error()).To(ContainSubstring(MinorUpdateStageOVNControlplane))
215+
})
216+
217+
It("should reject update when annotation is present but empty", func() {
218+
oldVersion := &OpenStackVersion{
219+
ObjectMeta: metav1.ObjectMeta{
220+
Name: "test-version",
221+
Namespace: "test-namespace",
222+
},
223+
Spec: OpenStackVersionSpec{TargetVersion: "1.1.0"},
224+
Status: OpenStackVersionStatus{
225+
ContainerImageVersionDefaults: map[string]*ContainerDefaults{
226+
"1.1.0": {},
227+
},
228+
},
229+
}
230+
newVersion := oldVersion.DeepCopy()
231+
newVersion.Annotations = map[string]string{
232+
MinorUpdateTargetStageAnnotation: "",
233+
}
234+
235+
_, err := newVersion.ValidateUpdate(context.Background(), oldVersion, nil)
236+
Expect(err).To(HaveOccurred())
237+
Expect(err.Error()).To(ContainSubstring("annotation value must not be empty"))
238+
})
239+
240+
It("should allow update when annotation is a valid stage", func() {
241+
oldVersion := &OpenStackVersion{
242+
ObjectMeta: metav1.ObjectMeta{
243+
Name: "test-version",
244+
Namespace: "test-namespace",
245+
},
246+
Spec: OpenStackVersionSpec{TargetVersion: "1.1.0"},
247+
Status: OpenStackVersionStatus{
248+
ContainerImageVersionDefaults: map[string]*ContainerDefaults{
249+
"1.1.0": {},
250+
},
251+
},
252+
}
253+
newVersion := oldVersion.DeepCopy()
254+
newVersion.Annotations = map[string]string{
255+
MinorUpdateTargetStageAnnotation: MinorUpdateStageRabbitMQ,
256+
}
257+
258+
_, err := newVersion.ValidateUpdate(context.Background(), oldVersion, nil)
259+
Expect(err).ToNot(HaveOccurred())
260+
})
261+
})
184262
})

0 commit comments

Comments
 (0)