@@ -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
6061func (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
114114func (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 (
@@ -135,6 +139,11 @@ func (r *OpenStackVersion) ValidateUpdate(ctx context.Context, old runtime.Objec
135139 return nil , apierrors .NewInternalError (fmt .Errorf ("failed to convert old object to OpenStackVersion" ))
136140 }
137141
142+ // Validate that the target stage annotation is not from earlier stage while a minor update is in progress
143+ if err := validateMinorUpdateTargetStageAnnotationProgress (oldVersion , r ); err != nil {
144+ return nil , err
145+ }
146+
138147 // Check if targetVersion is changing and this is a minor update
139148 if oldVersion .Spec .TargetVersion != r .Spec .TargetVersion && oldVersion .Status .DeployedVersion != nil {
140149 // Check if the skip annotation is present
@@ -174,6 +183,124 @@ func (r *OpenStackVersion) ValidateUpdate(ctx context.Context, old runtime.Objec
174183 return nil , nil
175184}
176185
186+ func validateMinorUpdateTargetStageAnnotation (annotations map [string ]string , resourceName string ) error {
187+ if annotations == nil {
188+ return nil
189+ }
190+ stage , ok := annotations [MinorUpdateTargetStageAnnotation ]
191+ if ! ok {
192+ return nil
193+ }
194+ annotationField := "metadata.annotations[" + MinorUpdateTargetStageAnnotation + "]"
195+ if stage == "" {
196+ return apierrors .NewForbidden (
197+ schema.GroupResource {
198+ Group : GroupVersion .WithKind ("OpenStackVersion" ).Group ,
199+ Resource : GroupVersion .WithKind ("OpenStackVersion" ).Kind ,
200+ }, resourceName , & field.Error {
201+ Type : field .ErrorTypeForbidden ,
202+ Field : annotationField ,
203+ BadValue : stage ,
204+ Detail : "Annotation value must not be empty. Remove the annotation or set a valid stage name" ,
205+ },
206+ )
207+ }
208+ if ! IsValidMinorUpdateTargetStage (stage ) {
209+ return apierrors .NewForbidden (
210+ schema.GroupResource {
211+ Group : GroupVersion .WithKind ("OpenStackVersion" ).Group ,
212+ Resource : GroupVersion .WithKind ("OpenStackVersion" ).Kind ,
213+ }, resourceName , & field.Error {
214+ Type : field .ErrorTypeForbidden ,
215+ Field : annotationField ,
216+ BadValue : stage ,
217+ Detail : fmt .Sprintf (
218+ "Invalid target stage %q. Must be one of: %s" ,
219+ stage ,
220+ strings .Join (ValidMinorUpdateTargetStages (), ", " ),
221+ ),
222+ },
223+ )
224+ }
225+ return nil
226+ }
227+
228+ func minorUpdateInProgress (v * OpenStackVersion ) bool {
229+ if v .Status .DeployedVersion == nil {
230+ return false
231+ }
232+ return v .Spec .TargetVersion != * v .Status .DeployedVersion
233+ }
234+
235+ func minorUpdateTargetStageFromAnnotations (annotations map [string ]string ) (string , bool ) {
236+ if annotations == nil {
237+ return "" , false
238+ }
239+ stage , ok := annotations [MinorUpdateTargetStageAnnotation ]
240+ if ! ok || ! IsValidMinorUpdateTargetStage (stage ) {
241+ return "" , false
242+ }
243+ return stage , true
244+ }
245+
246+ // validateMinorUpdateTargetStageAnnotationProgress rejects moving the target-stage
247+ // annotation to an earlier rollout stage while a minor update is in progress, and rejects
248+ // adding the annotation behind stages already completed when it was absent at update start.
249+ func validateMinorUpdateTargetStageAnnotationProgress (old , new * OpenStackVersion ) error {
250+ if ! minorUpdateInProgress (new ) {
251+ return nil
252+ }
253+ oldStage , oldOK := minorUpdateTargetStageFromAnnotations (old .Annotations )
254+ newStage , newOK := minorUpdateTargetStageFromAnnotations (new .Annotations )
255+ if ! newOK {
256+ return nil
257+ }
258+ newIdx , okNew := MinorUpdateTargetStageIndex (newStage )
259+ if ! okNew {
260+ return nil
261+ }
262+ annotationField := "metadata.annotations[" + MinorUpdateTargetStageAnnotation + "]"
263+ gr := schema.GroupResource {
264+ Group : GroupVersion .WithKind ("OpenStackVersion" ).Group ,
265+ Resource : GroupVersion .WithKind ("OpenStackVersion" ).Kind ,
266+ }
267+
268+ if ! oldOK {
269+ latest := LatestCompletedMinorUpdateTargetStageIndex (old .Status )
270+ if latest >= 0 && newIdx < latest {
271+ completedStage := validMinorUpdateTargetStagesOrdered [latest ]
272+ return apierrors .NewForbidden (
273+ gr , new .GetName (), & field.Error {
274+ Type : field .ErrorTypeForbidden ,
275+ Field : annotationField ,
276+ BadValue : newStage ,
277+ Detail : fmt .Sprintf (
278+ "Cannot set update target stage to %q while minor update is in progress: update has already completed stage %q (targetVersion %q, deployedVersion %q); choose a further stage" ,
279+ newStage , completedStage , new .Spec .TargetVersion , * new .Status .DeployedVersion ,
280+ ),
281+ },
282+ )
283+ }
284+ return nil
285+ }
286+
287+ oldIdx , _ := MinorUpdateTargetStageIndex (oldStage )
288+ if newIdx >= oldIdx {
289+ return nil
290+ }
291+ return apierrors .NewForbidden (
292+ gr , new .GetName (), & field.Error {
293+ Type : field .ErrorTypeForbidden ,
294+ Field : annotationField ,
295+ BadValue : newStage ,
296+ Detail : fmt .Sprintf (
297+ "Cannot move update target stage from %q to earlier stage %q while minor update is in progress (targetVersion %q, deployedVersion %q); remove the annotation or set a further stage" ,
298+ oldStage , newStage , new .Spec .TargetVersion , * new .Status .DeployedVersion ,
299+ ),
300+ },
301+ )
302+ }
303+
177304// hasAnyCustomImage checks if any image field in CustomContainerImages is set
178305func hasAnyCustomImage (images CustomContainerImages ) bool {
179306 // Check CinderVolumeImages map
0 commit comments