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"
)
55 changes: 55 additions & 0 deletions api/core/v1beta1/openstackversion_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,63 @@ 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.
// 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/minor-update-target-stage"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if we could shorten it to 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"
)

// validMinorUpdateTargetStages lists allowed values for MinorUpdateTargetStageAnnotation.
var validMinorUpdateTargetStages = map[string]struct{}{
MinorUpdateStageOVNControlplane: {},
MinorUpdateStageOVNDataplane: {},
MinorUpdateStageRabbitMQ: {},
MinorUpdateStageMariaDB: {},
MinorUpdateStageMemcached: {},
MinorUpdateStageKeystone: {},
MinorUpdateStageControlplane: {},
}

// 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 []string{
MinorUpdateStageOVNControlplane,
MinorUpdateStageOVNDataplane,
MinorUpdateStageRabbitMQ,
MinorUpdateStageMariaDB,
MinorUpdateStageMemcached,
MinorUpdateStageKeystone,
MinorUpdateStageControlplane,
}
}

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

Expand Down
48 changes: 47 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 Down Expand Up @@ -174,6 +178,48 @@ 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: "the 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
}

// hasAnyCustomImage checks if any image field in CustomContainerImages is set
func hasAnyCustomImage(images CustomContainerImages) bool {
// Check CinderVolumeImages map
Expand Down
78 changes: 78 additions & 0 deletions api/core/v1beta1/openstackversion_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,82 @@ var _ = Describe("OpenStackVersion Webhook", func() {
Expect(err.Error()).To(ContainSubstring("failed to convert old object to OpenStackVersion"))
})
})

Context("MinorUpdateTargetStageAnnotation validation", func() {

BeforeEach(func() {
SetupOpenStackVersionDefaults(OpenStackVersionDefaults{
AvailableVersion: "1.1.0",
})
})

It("should reject update when annotation value is invalid", func() {
oldVersion := &OpenStackVersion{
ObjectMeta: metav1.ObjectMeta{
Name: "test-version",
Namespace: "test-namespace",
},
Spec: OpenStackVersionSpec{TargetVersion: "1.1.0"},
Status: OpenStackVersionStatus{
ContainerImageVersionDefaults: map[string]*ContainerDefaults{
"1.1.0": {},
},
},
}
newVersion := oldVersion.DeepCopy()
newVersion.Annotations = map[string]string{
MinorUpdateTargetStageAnnotation: "tyop",
}

_, err := newVersion.ValidateUpdate(context.Background(), oldVersion, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(`invalid target stage "tyop"`))
Expect(err.Error()).To(ContainSubstring(MinorUpdateStageOVNControlplane))
})

It("should reject update when annotation is present but empty", func() {
oldVersion := &OpenStackVersion{
ObjectMeta: metav1.ObjectMeta{
Name: "test-version",
Namespace: "test-namespace",
},
Spec: OpenStackVersionSpec{TargetVersion: "1.1.0"},
Status: OpenStackVersionStatus{
ContainerImageVersionDefaults: map[string]*ContainerDefaults{
"1.1.0": {},
},
},
}
newVersion := oldVersion.DeepCopy()
newVersion.Annotations = map[string]string{
MinorUpdateTargetStageAnnotation: "",
}

_, err := newVersion.ValidateUpdate(context.Background(), oldVersion, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("annotation value must not be empty"))
})

It("should allow update when annotation is a valid stage", func() {
oldVersion := &OpenStackVersion{
ObjectMeta: metav1.ObjectMeta{
Name: "test-version",
Namespace: "test-namespace",
},
Spec: OpenStackVersionSpec{TargetVersion: "1.1.0"},
Status: OpenStackVersionStatus{
ContainerImageVersionDefaults: map[string]*ContainerDefaults{
"1.1.0": {},
},
},
}
newVersion := oldVersion.DeepCopy()
newVersion.Annotations = map[string]string{
MinorUpdateTargetStageAnnotation: MinorUpdateStageRabbitMQ,
}

_, err := newVersion.ValidateUpdate(context.Background(), oldVersion, nil)
Expect(err).ToNot(HaveOccurred())
})
})
})
Loading
Loading