Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions api/core/v1beta1/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,4 +596,7 @@ const (

// OpenStackVersionMinorUpdateAvailableMessage
OpenStackVersionMinorUpdateAvailableMessage = "update available"

// OpenStackVersionMinorUpdateReadyGatedMessage - format string; arg is the target stage name
OpenStackVersionMinorUpdateReadyGatedMessage = "Minor update progression stopped after stage: %s. Set annotation to any stage after %s to resume OpenStack update or remove the annotation to run to completion."
)
96 changes: 96 additions & 0 deletions api/core/v1beta1/openstackversion_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,104 @@ const (
MinorUpdateControlPlane string = "Minor Update Controlplane In Progress"
// MinorUpdateComplete -
MinorUpdateComplete string = "Complete"

// MinorUpdateTargetStageAnnotation - specifies the update stage after which the minor update
// should pause. All stages up to and including the named stage will be completed; subsequent
// stages will be blocked until the annotation is removed or updated to a later stage.
// During an update, the webhook rejects moving this annotation to an earlier stage.
// Valid values: "ovn-controlplane", "ovn-dataplane", "rabbitmq", "mariadb", "memcached",
// "keystone", "controlplane". Remove the annotation to let the update proceed to completion.
MinorUpdateTargetStageAnnotation string = "core.openstack.org/update-target-stage"

// MinorUpdateStageOVNControlplane - stage name for OVN controlplane update
MinorUpdateStageOVNControlplane string = "ovn-controlplane"
// MinorUpdateStageOVNDataplane - stage name for OVN dataplane update
MinorUpdateStageOVNDataplane string = "ovn-dataplane"
// MinorUpdateStageRabbitMQ - stage name for RabbitMQ update
MinorUpdateStageRabbitMQ string = "rabbitmq"
// MinorUpdateStageMariaDB - stage name for MariaDB update
MinorUpdateStageMariaDB string = "mariadb"
// MinorUpdateStageMemcached - stage name for Memcached update
MinorUpdateStageMemcached string = "memcached"
// MinorUpdateStageKeystone - stage name for Keystone update
MinorUpdateStageKeystone string = "keystone"
// MinorUpdateStageControlplane - stage name for full controlplane update
MinorUpdateStageControlplane string = "controlplane"
)

// validMinorUpdateTargetStagesOrdered is the single source of truth for allowed
// MinorUpdateTargetStageAnnotation values, listed in rollout order.
var validMinorUpdateTargetStagesOrdered = []string{
MinorUpdateStageOVNControlplane,
MinorUpdateStageOVNDataplane,
MinorUpdateStageRabbitMQ,
MinorUpdateStageMariaDB,
MinorUpdateStageMemcached,
MinorUpdateStageKeystone,
MinorUpdateStageControlplane,
}

// validMinorUpdateTargetStages is a set derived from validMinorUpdateTargetStagesOrdered for O(1) lookup.
var validMinorUpdateTargetStages = func() map[string]struct{} {
m := make(map[string]struct{}, len(validMinorUpdateTargetStagesOrdered))
for _, s := range validMinorUpdateTargetStagesOrdered {
m[s] = struct{}{}
}
return m
}()

// IsValidMinorUpdateTargetStage reports whether v is a supported minor-update target stage name.
func IsValidMinorUpdateTargetStage(v string) bool {
if v == "" {
return false
}
_, ok := validMinorUpdateTargetStages[v]
return ok
}

// ValidMinorUpdateTargetStages returns allowed annotation values in rollout order.
func ValidMinorUpdateTargetStages() []string {
return validMinorUpdateTargetStagesOrdered
}

// MinorUpdateTargetStageIndex returns the rollout order index for stage.
func MinorUpdateTargetStageIndex(stage string) (int, bool) {
for i, s := range validMinorUpdateTargetStagesOrdered {
if s == stage {
return i, true
}
}
return -1, false
}

// MinorUpdateTargetStageFromAnnotations returns the target-stage annotation value when set and valid.
func MinorUpdateTargetStageFromAnnotations(annotations map[string]string) (string, bool) {
if annotations == nil {
return "", false
}
stage, ok := annotations[MinorUpdateTargetStageAnnotation]
if !ok || !IsValidMinorUpdateTargetStage(stage) {
return "", false
}
return stage, true
}

// MinorUpdateStageAllowedForReconcile reports whether the control plane may patch resources
// for rollout stage during a minor update. When the target-stage annotation is absent, all
// stages are allowed. When set, only stages up to and including the target may be reconciled.
func MinorUpdateStageAllowedForReconcile(annotations map[string]string, stage string) bool {
target, ok := MinorUpdateTargetStageFromAnnotations(annotations)
if !ok {
return true
}
stageIdx, okStage := MinorUpdateTargetStageIndex(stage)
targetIdx, okTarget := MinorUpdateTargetStageIndex(target)
if !okStage || !okTarget {
return true
}
return stageIdx <= targetIdx
}
Comment thread
ciecierski marked this conversation as resolved.

// OpenStackVersionSpec - defines the desired state of OpenStackVersion
type OpenStackVersionSpec struct {

Expand Down
104 changes: 103 additions & 1 deletion api/core/v1beta1/openstackversion_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"os"
"reflect"
"strings"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -59,7 +60,6 @@ func (r *OpenStackVersion) Default() {
// ValidateCreate validates the OpenStackVersion on creation
func (r *OpenStackVersion) ValidateCreate(ctx context.Context, c goClient.Client) (admission.Warnings, error) {
openstackversionlog.Info("validate create", "name", r.Name)

if r.Spec.TargetVersion != openstackVersionDefaults.AvailableVersion {
return nil, apierrors.NewForbidden(
schema.GroupResource{
Expand Down Expand Up @@ -114,6 +114,10 @@ func (r *OpenStackVersion) ValidateCreate(ctx context.Context, c goClient.Client
func (r *OpenStackVersion) ValidateUpdate(ctx context.Context, old runtime.Object, c goClient.Client) (admission.Warnings, error) {
openstackversionlog.Info("validate update", "name", r.Name)

if err := validateMinorUpdateTargetStageAnnotation(r.Annotations, r.GetName()); err != nil {
return nil, err
}

_, ok := r.Status.ContainerImageVersionDefaults[r.Spec.TargetVersion]
if r.Spec.TargetVersion != openstackVersionDefaults.AvailableVersion && !ok {
return nil, apierrors.NewForbidden(
Expand All @@ -135,6 +139,11 @@ func (r *OpenStackVersion) ValidateUpdate(ctx context.Context, old runtime.Objec
return nil, apierrors.NewInternalError(fmt.Errorf("failed to convert old object to OpenStackVersion"))
}

// Validate that the target stage annotation is not from earlier stage while a minor update is in progress
if err := validateMinorUpdateTargetStageAnnotationProgress(oldVersion, r); err != nil {
return nil, err
}

// Check if targetVersion is changing and this is a minor update
if oldVersion.Spec.TargetVersion != r.Spec.TargetVersion && oldVersion.Status.DeployedVersion != nil {
// Check if the skip annotation is present
Expand Down Expand Up @@ -174,6 +183,99 @@ func (r *OpenStackVersion) ValidateUpdate(ctx context.Context, old runtime.Objec
return nil, nil
}

func validateMinorUpdateTargetStageAnnotation(annotations map[string]string, resourceName string) error {
if annotations == nil {
return nil
}
stage, ok := annotations[MinorUpdateTargetStageAnnotation]
if !ok {
return nil
}
annotationField := "metadata.annotations[" + MinorUpdateTargetStageAnnotation + "]"
if stage == "" {
return apierrors.NewForbidden(
schema.GroupResource{
Group: GroupVersion.WithKind("OpenStackVersion").Group,
Resource: GroupVersion.WithKind("OpenStackVersion").Kind,
}, resourceName, &field.Error{
Type: field.ErrorTypeForbidden,
Field: annotationField,
BadValue: stage,
Detail: "Annotation value must not be empty. Remove the annotation or set a valid stage name",
},
)
}
if !IsValidMinorUpdateTargetStage(stage) {
return apierrors.NewForbidden(
schema.GroupResource{
Group: GroupVersion.WithKind("OpenStackVersion").Group,
Resource: GroupVersion.WithKind("OpenStackVersion").Kind,
}, resourceName, &field.Error{
Type: field.ErrorTypeForbidden,
Field: annotationField,
BadValue: stage,
Detail: fmt.Sprintf(
"Invalid target stage %q. Must be one of: %s",
stage,
strings.Join(ValidMinorUpdateTargetStages(), ", "),
),
},
)
}
return nil
}

func minorUpdateInProgress(v *OpenStackVersion) bool {
if v.Status.DeployedVersion == nil {
return false
}
return v.Spec.TargetVersion != *v.Status.DeployedVersion
}

func minorUpdateTargetStageFromAnnotations(annotations map[string]string) (string, bool) {
if annotations == nil {
return "", false
}
stage, ok := annotations[MinorUpdateTargetStageAnnotation]
if !ok || !IsValidMinorUpdateTargetStage(stage) {
return "", false
}
return stage, true
}

// validateMinorUpdateTargetStageAnnotationProgress rejects moving the target-stage
// annotation to an earlier rollout stage while a minor update is in progress.
func validateMinorUpdateTargetStageAnnotationProgress(old, new *OpenStackVersion) error {
if !minorUpdateInProgress(new) {
return nil
}
oldStage, oldOK := minorUpdateTargetStageFromAnnotations(old.Annotations)
newStage, newOK := minorUpdateTargetStageFromAnnotations(new.Annotations)
if !oldOK || !newOK {
return nil
}
oldIdx, _ := MinorUpdateTargetStageIndex(oldStage)
newIdx, _ := MinorUpdateTargetStageIndex(newStage)
if newIdx >= oldIdx {
return nil
}
annotationField := "metadata.annotations[" + MinorUpdateTargetStageAnnotation + "]"
return apierrors.NewForbidden(
schema.GroupResource{
Group: GroupVersion.WithKind("OpenStackVersion").Group,
Resource: GroupVersion.WithKind("OpenStackVersion").Kind,
}, new.GetName(), &field.Error{
Type: field.ErrorTypeForbidden,
Field: annotationField,
BadValue: newStage,
Detail: fmt.Sprintf(
"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 later stage",
oldStage, newStage, new.Spec.TargetVersion, *new.Status.DeployedVersion,
),
},
)
}

// hasAnyCustomImage checks if any image field in CustomContainerImages is set
func hasAnyCustomImage(images CustomContainerImages) bool {
// Check CinderVolumeImages map
Expand Down
Loading
Loading