From 722610f2a7f8412f2fcd2c578b52fc4836faf8d3 Mon Sep 17 00:00:00 2001
From: Lennart Jern
Date: Mon, 17 Nov 2025 10:29:59 +0000
Subject: [PATCH] CAPI v1beta2 conditions and deprecations for OSM
Add CAPI v1beta2 conditions for the OpenStackMachine and deprecate error
and failure messages as well as the Ready condition. This also adds the
status.initialization.provisioned field.
Signed-off-by: Lennart Jern
---
.golangci.yml | 2 +-
api/v1beta1/openstackmachine_types.go | 28 +++
api/v1beta1/zz_generated.deepcopy.go | 20 ++
cmd/models-schema/zz_generated.openapi.go | 41 +++-
...re.cluster.x-k8s.io_openstackmachines.yaml | 33 ++-
controllers/openstackmachine_controller.go | 21 ++
.../openstackmachine_controller_test.go | 223 ++++++++++++++++++
docs/book/src/api/v1beta1/api.md | 59 +++++
.../api/v1beta1/machineinitialization.go | 39 +++
.../api/v1beta1/openstackmachinestatus.go | 9 +
.../applyconfiguration/internal/internal.go | 9 +
pkg/generated/applyconfiguration/utils.go | 2 +
12 files changed, 474 insertions(+), 12 deletions(-)
create mode 100644 pkg/generated/applyconfiguration/api/v1beta1/machineinitialization.go
diff --git a/.golangci.yml b/.golangci.yml
index 76cf09c675..c0aa7e39da 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -188,7 +188,7 @@ linters:
text: 'SA1019: "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" is deprecated: This package is deprecated and is going to be removed when support for v1beta1 will be dropped. Please see https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more details.'
- linters:
- staticcheck
- text: 'SA1019: .*.Status.Ready is deprecated: This field is deprecated and will be removed in a future API version. Use status.conditions to determine the ready state of the cluster.'
+ text: 'SA1019: .*.Status.Ready is deprecated: This field is deprecated and will be removed in a future API version. Use status.conditions to determine the ready state of the *'
- linters:
- staticcheck
text: 'SA1019: .*.Status.FailureReason is deprecated: This field is deprecated and will be removed in a future API version. Use status.conditions to report failures.'
diff --git a/api/v1beta1/openstackmachine_types.go b/api/v1beta1/openstackmachine_types.go
index 8b52e62a49..615a47f735 100644
--- a/api/v1beta1/openstackmachine_types.go
+++ b/api/v1beta1/openstackmachine_types.go
@@ -185,12 +185,27 @@ type ServerMetadata struct {
Value string `json:"value"`
}
+// MachineInitialization contains information about the initialization status of the machine.
+type MachineInitialization struct {
+ // Provisioned is set to true when the initial provisioning of the machine infrastructure is completed.
+ // The value of this field is never updated after provisioning is completed.
+ // +optional
+ Provisioned bool `json:"provisioned,omitempty"`
+}
+
// OpenStackMachineStatus defines the observed state of OpenStackMachine.
type OpenStackMachineStatus struct {
// Ready is true when the provider resource is ready.
+ //
+ // Deprecated: This field is deprecated and will be removed in a future API version.
+ // Use status.conditions to determine the ready state of the machine.
// +optional
Ready bool `json:"ready"`
+ // Initialization contains information about the initialization status of the machine.
+ // +optional
+ Initialization *MachineInitialization `json:"initialization,omitempty"`
+
// InstanceID is the OpenStack instance ID for this machine.
// +optional
InstanceID optional.String `json:"instanceID,omitempty"`
@@ -213,6 +228,11 @@ type OpenStackMachineStatus struct {
// +optional
Resources *MachineResources `json:"resources,omitempty"`
+ // FailureReason explains the reson behind a failure.
+ //
+ // Deprecated: This field is deprecated and will be removed in a future API version.
+ // Use status.conditions to report failures.
+ // +optional
FailureReason *capoerrors.DeprecatedCAPIMachineStatusError `json:"failureReason,omitempty"`
// FailureMessage will be set in the event that there is a terminal problem
@@ -231,9 +251,17 @@ type OpenStackMachineStatus struct {
// Any transient errors that occur during the reconciliation of Machines
// can be added as events to the Machine object and/or logged in the
// controller's output.
+ //
+ // Deprecated: This field is deprecated and will be removed in a future API version.
+ // Use status.conditions to report failures.
// +optional
FailureMessage *string `json:"failureMessage,omitempty"`
+ // Conditions defines current service state of the OpenStackMachine.
+ // This field surfaces into Machine's status.conditions[InfrastructureReady] condition.
+ // The Ready condition must surface issues during the entire lifecycle of the OpenStackMachine
+ // (both during initial provisioning and after the initial provisioning is completed).
+ // +optional
Conditions clusterv1beta1.Conditions `json:"conditions,omitempty"`
}
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 0ff6155cab..87510b42ab 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -456,6 +456,21 @@ func (in *LoadBalancer) DeepCopy() *LoadBalancer {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MachineInitialization) DeepCopyInto(out *MachineInitialization) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineInitialization.
+func (in *MachineInitialization) DeepCopy() *MachineInitialization {
+ if in == nil {
+ return nil
+ }
+ out := new(MachineInitialization)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MachineResources) DeepCopyInto(out *MachineResources) {
*out = *in
@@ -1122,6 +1137,11 @@ func (in *OpenStackMachineSpec) DeepCopy() *OpenStackMachineSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OpenStackMachineStatus) DeepCopyInto(out *OpenStackMachineStatus) {
*out = *in
+ if in.Initialization != nil {
+ in, out := &in.Initialization, &out.Initialization
+ *out = new(MachineInitialization)
+ **out = **in
+ }
if in.InstanceID != nil {
in, out := &in.InstanceID, &out.InstanceID
*out = new(string)
diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go
index 84609b7d86..757e866c16 100644
--- a/cmd/models-schema/zz_generated.openapi.go
+++ b/cmd/models-schema/zz_generated.openapi.go
@@ -346,6 +346,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ImageFilter": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_ImageFilter(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ImageParam": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_ImageParam(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.LoadBalancer": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_LoadBalancer(ref),
+ "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.MachineInitialization": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_MachineInitialization(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.MachineResources": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_MachineResources(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ManagedSecurityGroups": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_ManagedSecurityGroups(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.NetworkFilter": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_NetworkFilter(ref),
@@ -18375,6 +18376,26 @@ func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_LoadBalancer(re
}
}
+func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_MachineInitialization(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "MachineInitialization contains information about the initialization status of the machine.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "provisioned": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Provisioned is set to true when the initial provisioning of the machine infrastructure is completed. The value of this field is never updated after provisioning is completed.",
+ Type: []string{"boolean"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_MachineResources(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -19659,12 +19680,18 @@ func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackMachin
Properties: map[string]spec.Schema{
"ready": {
SchemaProps: spec.SchemaProps{
- Description: "Ready is true when the provider resource is ready.",
+ Description: "Ready is true when the provider resource is ready.\n\nDeprecated: This field is deprecated and will be removed in a future API version. Use status.conditions to determine the ready state of the machine.",
Default: false,
Type: []string{"boolean"},
Format: "",
},
},
+ "initialization": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Initialization contains information about the initialization status of the machine.",
+ Ref: ref("sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.MachineInitialization"),
+ },
+ },
"instanceID": {
SchemaProps: spec.SchemaProps{
Description: "InstanceID is the OpenStack instance ID for this machine.",
@@ -19707,20 +19734,22 @@ func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackMachin
},
"failureReason": {
SchemaProps: spec.SchemaProps{
- Type: []string{"string"},
- Format: "",
+ Description: "FailureReason explains the reson behind a failure.\n\nDeprecated: This field is deprecated and will be removed in a future API version. Use status.conditions to report failures.",
+ Type: []string{"string"},
+ Format: "",
},
},
"failureMessage": {
SchemaProps: spec.SchemaProps{
- Description: "FailureMessage will be set in the event that there is a terminal problem reconciling the Machine and will contain a more verbose string suitable for logging and human consumption.\n\nThis field should not be set for transitive errors that a controller faces that are expected to be fixed automatically over time (like service outages), but instead indicate that something is fundamentally wrong with the Machine's spec or the configuration of the controller, and that manual intervention is required. Examples of terminal errors would be invalid combinations of settings in the spec, values that are unsupported by the controller, or the responsible controller itself being critically misconfigured.\n\nAny transient errors that occur during the reconciliation of Machines can be added as events to the Machine object and/or logged in the controller's output.",
+ Description: "FailureMessage will be set in the event that there is a terminal problem reconciling the Machine and will contain a more verbose string suitable for logging and human consumption.\n\nThis field should not be set for transitive errors that a controller faces that are expected to be fixed automatically over time (like service outages), but instead indicate that something is fundamentally wrong with the Machine's spec or the configuration of the controller, and that manual intervention is required. Examples of terminal errors would be invalid combinations of settings in the spec, values that are unsupported by the controller, or the responsible controller itself being critically misconfigured.\n\nAny transient errors that occur during the reconciliation of Machines can be added as events to the Machine object and/or logged in the controller's output.\n\nDeprecated: This field is deprecated and will be removed in a future API version. Use status.conditions to report failures.",
Type: []string{"string"},
Format: "",
},
},
"conditions": {
SchemaProps: spec.SchemaProps{
- Type: []string{"array"},
+ Description: "Conditions defines current service state of the OpenStackMachine. This field surfaces into Machine's status.conditions[InfrastructureReady] condition. The Ready condition must surface issues during the entire lifecycle of the OpenStackMachine (both during initial provisioning and after the initial provisioning is completed).",
+ Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
@@ -19735,7 +19764,7 @@ func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackMachin
},
},
Dependencies: []string{
- "k8s.io/api/core/v1.NodeAddress", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.MachineResources", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ResolvedMachineSpec", "sigs.k8s.io/cluster-api/api/core/v1beta1.Condition"},
+ "k8s.io/api/core/v1.NodeAddress", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.MachineInitialization", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.MachineResources", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ResolvedMachineSpec", "sigs.k8s.io/cluster-api/api/core/v1beta1.Condition"},
}
}
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml
index 0dc88ef8c7..e752a7d2f7 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml
@@ -967,8 +967,11 @@ spec:
type: object
type: array
conditions:
- description: Conditions provide observations of the operational state
- of a Cluster API resource.
+ description: |-
+ Conditions defines current service state of the OpenStackMachine.
+ This field surfaces into Machine's status.conditions[InfrastructureReady] condition.
+ The Ready condition must surface issues during the entire lifecycle of the OpenStackMachine
+ (both during initial provisioning and after the initial provisioning is completed).
items:
description: Condition defines an observation of a Cluster API resource
operational state.
@@ -1037,11 +1040,27 @@ spec:
Any transient errors that occur during the reconciliation of Machines
can be added as events to the Machine object and/or logged in the
controller's output.
+
+ Deprecated: This field is deprecated and will be removed in a future API version.
+ Use status.conditions to report failures.
type: string
failureReason:
- description: DeprecatedCAPIMachineStatusError defines errors states
- for Machine objects.
+ description: |-
+ FailureReason explains the reson behind a failure.
+
+ Deprecated: This field is deprecated and will be removed in a future API version.
+ Use status.conditions to report failures.
type: string
+ initialization:
+ description: Initialization contains information about the initialization
+ status of the machine.
+ properties:
+ provisioned:
+ description: |-
+ Provisioned is set to true when the initial provisioning of the machine infrastructure is completed.
+ The value of this field is never updated after provisioning is completed.
+ type: boolean
+ type: object
instanceID:
description: InstanceID is the OpenStack instance ID for this machine.
type: string
@@ -1052,7 +1071,11 @@ spec:
Instead, it's set by the OpenStackServer controller.
type: string
ready:
- description: Ready is true when the provider resource is ready.
+ description: |-
+ Ready is true when the provider resource is ready.
+
+ Deprecated: This field is deprecated and will be removed in a future API version.
+ Use status.conditions to determine the ready state of the machine.
type: boolean
resolved:
description: |-
diff --git a/controllers/openstackmachine_controller.go b/controllers/openstackmachine_controller.go
index 725b24b11d..325f06d56b 100644
--- a/controllers/openstackmachine_controller.go
+++ b/controllers/openstackmachine_controller.go
@@ -382,6 +382,7 @@ func (r *OpenStackMachineReconciler) reconcileNormal(ctx context.Context, scope
if instanceStatus == nil {
v1beta1conditions.MarkFalse(openStackMachine, infrav1.InstanceReadyCondition, infrav1.InstanceDeletedReason, clusterv1beta1.ConditionSeverityError, infrav1.ServerUnexpectedDeletedMessage)
+ v1beta1conditions.MarkFalse(openStackMachine, clusterv1beta1.ReadyCondition, infrav1.InstanceDeletedReason, clusterv1beta1.ConditionSeverityError, infrav1.ServerUnexpectedDeletedMessage)
openStackMachine.SetFailure(capoerrors.DeprecatedCAPIUpdateMachineError, errors.New(infrav1.ServerUnexpectedDeletedMessage)) //nolint:staticcheck // This error is not used as an error
return ctrl.Result{}, nil
}
@@ -437,6 +438,22 @@ func (r *OpenStackMachineReconciler) reconcileMachineState(scope *scope.WithLogg
openStackMachine.Spec.ProviderID = ptr.To(fmt.Sprintf("openstack://%s/%s", region, *openStackServer.Status.InstanceID))
openStackMachine.Status.InstanceID = openStackServer.Status.InstanceID
openStackMachine.Status.Ready = true
+
+ // Set initialization.provisioned to true when initial infrastructure provisioning is complete.
+ // This field should only be set once and never changed afterward, as per CAPI v1beta2 contract.
+ // We set it here when the machine becomes ACTIVE for the first time.
+ if openStackMachine.Status.Initialization == nil {
+ openStackMachine.Status.Initialization = &infrav1.MachineInitialization{}
+ }
+ if !openStackMachine.Status.Initialization.Provisioned {
+ openStackMachine.Status.Initialization.Provisioned = true
+ scope.Logger().Info("Initial machine infrastructure provisioning completed")
+ }
+
+ // Set the Ready condition to True when infrastructure is ready.
+ // This condition surfaces into Machine's status.conditions[InfrastructureReady].
+ // It reflects the current operational state of the machine infrastructure.
+ v1beta1conditions.MarkTrue(openStackMachine, clusterv1beta1.ReadyCondition)
case infrav1.InstanceStateError:
// If the machine has a NodeRef then it must have been working at some point,
// so the error could be something temporary.
@@ -447,20 +464,24 @@ func (r *OpenStackMachineReconciler) reconcileMachineState(scope *scope.WithLogg
openStackMachine.SetFailure(capoerrors.DeprecatedCAPIUpdateMachineError, err)
}
v1beta1conditions.MarkFalse(openStackMachine, infrav1.InstanceReadyCondition, infrav1.InstanceStateErrorReason, clusterv1beta1.ConditionSeverityError, "")
+ v1beta1conditions.MarkFalse(openStackMachine, clusterv1beta1.ReadyCondition, infrav1.InstanceStateErrorReason, clusterv1beta1.ConditionSeverityError, "Instance is in ERROR state")
return &ctrl.Result{}
case infrav1.InstanceStateDeleted:
// we should avoid further actions for DELETED VM
scope.Logger().Info("Machine instance state is DELETED, no actions")
v1beta1conditions.MarkFalse(openStackMachine, infrav1.InstanceReadyCondition, infrav1.InstanceDeletedReason, clusterv1beta1.ConditionSeverityError, "")
+ v1beta1conditions.MarkFalse(openStackMachine, clusterv1beta1.ReadyCondition, infrav1.InstanceDeletedReason, clusterv1beta1.ConditionSeverityError, "Instance has been deleted")
return &ctrl.Result{}
case infrav1.InstanceStateBuild, infrav1.InstanceStateUndefined:
scope.Logger().Info("Waiting for instance to become ACTIVE", "id", openStackServer.Status.InstanceID, "status", openStackServer.Status.InstanceState)
+ v1beta1conditions.MarkFalse(openStackMachine, clusterv1beta1.ReadyCondition, infrav1.InstanceNotReadyReason, clusterv1beta1.ConditionSeverityInfo, "Instance is building")
return &ctrl.Result{RequeueAfter: waitForBuildingInstanceToReconcile}
default:
// The other state is normal (for example, migrating, shutoff) but we don't want to proceed until it's ACTIVE
// due to potential conflict or unexpected actions
scope.Logger().Info("Waiting for instance to become ACTIVE", "id", openStackServer.Status.InstanceID, "status", openStackServer.Status.InstanceState)
v1beta1conditions.MarkUnknown(openStackMachine, infrav1.InstanceReadyCondition, infrav1.InstanceNotReadyReason, "Instance state is not handled: %v", ptr.Deref(openStackServer.Status.InstanceState, infrav1.InstanceStateUndefined))
+ v1beta1conditions.MarkUnknown(openStackMachine, clusterv1beta1.ReadyCondition, infrav1.InstanceNotReadyReason, "Instance state is: %v", ptr.Deref(openStackServer.Status.InstanceState, infrav1.InstanceStateUndefined))
return &ctrl.Result{RequeueAfter: waitForInstanceBecomeActiveToReconcile}
}
diff --git a/controllers/openstackmachine_controller_test.go b/controllers/openstackmachine_controller_test.go
index 78bf6f6936..b9497c8efe 100644
--- a/controllers/openstackmachine_controller_test.go
+++ b/controllers/openstackmachine_controller_test.go
@@ -20,11 +20,17 @@ import (
"reflect"
"testing"
+ "github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
+ clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1"
+ clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
+ v1beta1conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions"
infrav1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1"
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
+ "sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
)
const (
@@ -42,6 +48,7 @@ const (
flavorName = "test-flavor"
sshKeyName = "test-ssh-key"
failureDomain = "test-failure-domain"
+ testInstanceID = "test-instance-id-12345"
)
func TestOpenStackMachineSpecToOpenStackServerSpec(t *testing.T) {
@@ -428,3 +435,219 @@ func TestGetPortIDs(t *testing.T) {
})
}
}
+
+func TestReconcileMachineState(t *testing.T) {
+ tests := []struct {
+ name string
+ instanceState infrav1.InstanceState
+ machineHasNodeRef bool
+ expectRequeue bool
+ expectedInstanceReadyCondition *clusterv1beta1.Condition
+ expectedReadyCondition *clusterv1beta1.Condition
+ expectInitializationProvisioned bool
+ expectFailureSet bool
+ }{
+ {
+ name: "Instance state ACTIVE sets conditions to True and initialization.provisioned",
+ instanceState: infrav1.InstanceStateActive,
+ expectRequeue: false,
+ expectedInstanceReadyCondition: &clusterv1beta1.Condition{
+ Type: infrav1.InstanceReadyCondition,
+ Status: corev1.ConditionTrue,
+ },
+ expectedReadyCondition: &clusterv1beta1.Condition{
+ Type: clusterv1beta1.ReadyCondition,
+ Status: corev1.ConditionTrue,
+ },
+ expectInitializationProvisioned: true,
+ },
+ {
+ name: "Instance state ERROR sets conditions to False without NodeRef",
+ instanceState: infrav1.InstanceStateError,
+ machineHasNodeRef: false,
+ expectRequeue: true,
+ expectedInstanceReadyCondition: &clusterv1beta1.Condition{
+ Type: infrav1.InstanceReadyCondition,
+ Status: corev1.ConditionFalse,
+ Severity: clusterv1beta1.ConditionSeverityError,
+ Reason: infrav1.InstanceStateErrorReason,
+ },
+ expectedReadyCondition: &clusterv1beta1.Condition{
+ Type: clusterv1beta1.ReadyCondition,
+ Status: corev1.ConditionFalse,
+ Severity: clusterv1beta1.ConditionSeverityError,
+ Reason: infrav1.InstanceStateErrorReason,
+ },
+ expectFailureSet: true,
+ },
+ {
+ name: "Instance state ERROR with NodeRef does not set failure",
+ instanceState: infrav1.InstanceStateError,
+ machineHasNodeRef: true,
+ expectRequeue: true,
+ expectedInstanceReadyCondition: &clusterv1beta1.Condition{
+ Type: infrav1.InstanceReadyCondition,
+ Status: corev1.ConditionFalse,
+ Severity: clusterv1beta1.ConditionSeverityError,
+ Reason: infrav1.InstanceStateErrorReason,
+ },
+ expectedReadyCondition: &clusterv1beta1.Condition{
+ Type: clusterv1beta1.ReadyCondition,
+ Status: corev1.ConditionFalse,
+ Severity: clusterv1beta1.ConditionSeverityError,
+ Reason: infrav1.InstanceStateErrorReason,
+ },
+ expectFailureSet: false,
+ },
+ {
+ name: "Instance state DELETED sets conditions to False",
+ instanceState: infrav1.InstanceStateDeleted,
+ expectRequeue: true,
+ expectedInstanceReadyCondition: &clusterv1beta1.Condition{
+ Type: infrav1.InstanceReadyCondition,
+ Status: corev1.ConditionFalse,
+ Severity: clusterv1beta1.ConditionSeverityError,
+ Reason: infrav1.InstanceDeletedReason,
+ },
+ expectedReadyCondition: &clusterv1beta1.Condition{
+ Type: clusterv1beta1.ReadyCondition,
+ Status: corev1.ConditionFalse,
+ Severity: clusterv1beta1.ConditionSeverityError,
+ Reason: infrav1.InstanceDeletedReason,
+ },
+ },
+ {
+ name: "Instance state BUILD sets ReadyCondition to False",
+ instanceState: infrav1.InstanceStateBuild,
+ expectRequeue: true,
+ expectedReadyCondition: &clusterv1beta1.Condition{
+ Type: clusterv1beta1.ReadyCondition,
+ Status: corev1.ConditionFalse,
+ Severity: clusterv1beta1.ConditionSeverityInfo,
+ Reason: infrav1.InstanceNotReadyReason,
+ },
+ },
+ {
+ name: "Instance state SHUTOFF sets conditions to Unknown",
+ instanceState: infrav1.InstanceStateShutoff,
+ expectRequeue: true,
+ expectedInstanceReadyCondition: &clusterv1beta1.Condition{
+ Type: infrav1.InstanceReadyCondition,
+ Status: corev1.ConditionUnknown,
+ Reason: infrav1.InstanceNotReadyReason,
+ },
+ expectedReadyCondition: &clusterv1beta1.Condition{
+ Type: clusterv1beta1.ReadyCondition,
+ Status: corev1.ConditionUnknown,
+ Reason: infrav1.InstanceNotReadyReason,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ openStackMachine := &infrav1.OpenStackMachine{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: openStackMachineName,
+ Namespace: namespace,
+ },
+ Spec: infrav1.OpenStackMachineSpec{
+ Flavor: ptr.To(flavorName),
+ Image: infrav1.ImageParam{
+ Filter: &infrav1.ImageFilter{
+ Name: ptr.To("test-image"),
+ },
+ },
+ },
+ }
+
+ machine := &clusterv1.Machine{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-machine",
+ Namespace: namespace,
+ },
+ }
+ if tt.machineHasNodeRef {
+ machine.Status.NodeRef = clusterv1.MachineNodeReference{
+ Name: "test-node",
+ }
+ }
+
+ openStackServer := &infrav1alpha1.OpenStackServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: openStackMachineName,
+ Namespace: namespace,
+ },
+ Status: infrav1alpha1.OpenStackServerStatus{
+ InstanceID: ptr.To(testInstanceID),
+ InstanceState: ptr.To(tt.instanceState),
+ },
+ }
+
+ r := &OpenStackMachineReconciler{}
+ result := r.reconcileMachineState(scope.NewWithLogger(nil, logr.Discard()), openStackMachine, machine, openStackServer)
+
+ // Check requeue
+ if tt.expectRequeue && result == nil {
+ t.Errorf("expected requeue result, got nil")
+ }
+ if !tt.expectRequeue && result != nil {
+ t.Errorf("expected no requeue, got %v", result)
+ }
+
+ // Check InstanceReadyCondition
+ if tt.expectedInstanceReadyCondition != nil {
+ condition := v1beta1conditions.Get(openStackMachine, tt.expectedInstanceReadyCondition.Type)
+ if condition == nil {
+ t.Errorf("expected %s condition to be set", tt.expectedInstanceReadyCondition.Type)
+ } else {
+ if condition.Status != tt.expectedInstanceReadyCondition.Status {
+ t.Errorf("expected %s status %s, got %s", tt.expectedInstanceReadyCondition.Type, tt.expectedInstanceReadyCondition.Status, condition.Status)
+ }
+ if tt.expectedInstanceReadyCondition.Reason != "" && condition.Reason != tt.expectedInstanceReadyCondition.Reason {
+ t.Errorf("expected %s reason %s, got %s", tt.expectedInstanceReadyCondition.Type, tt.expectedInstanceReadyCondition.Reason, condition.Reason)
+ }
+ if tt.expectedInstanceReadyCondition.Severity != "" && condition.Severity != tt.expectedInstanceReadyCondition.Severity {
+ t.Errorf("expected %s severity %s, got %s", tt.expectedInstanceReadyCondition.Type, tt.expectedInstanceReadyCondition.Severity, condition.Severity)
+ }
+ }
+ }
+
+ // Check ReadyCondition
+ if tt.expectedReadyCondition != nil {
+ condition := v1beta1conditions.Get(openStackMachine, tt.expectedReadyCondition.Type)
+ if condition == nil {
+ t.Errorf("expected %s condition to be set", tt.expectedReadyCondition.Type)
+ } else {
+ if condition.Status != tt.expectedReadyCondition.Status {
+ t.Errorf("expected %s status %s, got %s", tt.expectedReadyCondition.Type, tt.expectedReadyCondition.Status, condition.Status)
+ }
+ if tt.expectedReadyCondition.Reason != "" && condition.Reason != tt.expectedReadyCondition.Reason {
+ t.Errorf("expected %s reason %s, got %s", tt.expectedReadyCondition.Type, tt.expectedReadyCondition.Reason, condition.Reason)
+ }
+ if tt.expectedReadyCondition.Severity != "" && condition.Severity != tt.expectedReadyCondition.Severity {
+ t.Errorf("expected %s severity %s, got %s", tt.expectedReadyCondition.Type, tt.expectedReadyCondition.Severity, condition.Severity)
+ }
+ }
+ }
+
+ // Check initialization.provisioned
+ if tt.expectInitializationProvisioned {
+ if openStackMachine.Status.Initialization == nil || !openStackMachine.Status.Initialization.Provisioned {
+ t.Errorf("expected Initialization.Provisioned to be true")
+ }
+ }
+
+ // Check failure is set
+ if tt.expectFailureSet {
+ if openStackMachine.Status.FailureReason == nil || openStackMachine.Status.FailureMessage == nil {
+ t.Errorf("expected FailureReason and FailureMessage to be set")
+ }
+ } else {
+ if openStackMachine.Status.FailureReason != nil || openStackMachine.Status.FailureMessage != nil {
+ t.Errorf("expected FailureReason and FailureMessage to not be set")
+ }
+ }
+ })
+ }
+}
diff --git a/docs/book/src/api/v1beta1/api.md b/docs/book/src/api/v1beta1/api.md
index ce0334eca1..7984f01fab 100644
--- a/docs/book/src/api/v1beta1/api.md
+++ b/docs/book/src/api/v1beta1/api.md
@@ -1988,6 +1988,38 @@ subnet in the list is taken into account.
+MachineInitialization
+
+
+(Appears on:
+OpenStackMachineStatus)
+
+
+
MachineInitialization contains information about the initialization status of the machine.
+
+
+
+
+| Field |
+Description |
+
+
+
+
+
+provisioned
+
+bool
+
+ |
+
+(Optional)
+ Provisioned is set to true when the initial provisioning of the machine infrastructure is completed.
+The value of this field is never updated after provisioning is completed.
+ |
+
+
+
MachineResources
@@ -3664,6 +3696,22 @@ bool
(Optional)
Ready is true when the provider resource is ready.
+Deprecated: This field is deprecated and will be removed in a future API version.
+Use status.conditions to determine the ready state of the machine.
+ |
+
+
+
+initialization
+
+
+MachineInitialization
+
+
+ |
+
+(Optional)
+ Initialization contains information about the initialization status of the machine.
|
@@ -3742,6 +3790,10 @@ sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors.DeprecatedCAPIMachin
|
+(Optional)
+ FailureReason explains the reson behind a failure.
+Deprecated: This field is deprecated and will be removed in a future API version.
+Use status.conditions to report failures.
|
@@ -3767,6 +3819,8 @@ responsible controller itself being critically misconfigured.
Any transient errors that occur during the reconciliation of Machines
can be added as events to the Machine object and/or logged in the
controller’s output.
+Deprecated: This field is deprecated and will be removed in a future API version.
+Use status.conditions to report failures.
@@ -3777,6 +3831,11 @@ sigs.k8s.io/cluster-api/api/core/v1beta1.Conditions
|
+(Optional)
+ Conditions defines current service state of the OpenStackMachine.
+This field surfaces into Machine’s status.conditions[InfrastructureReady] condition.
+The Ready condition must surface issues during the entire lifecycle of the OpenStackMachine
+(both during initial provisioning and after the initial provisioning is completed).
|
diff --git a/pkg/generated/applyconfiguration/api/v1beta1/machineinitialization.go b/pkg/generated/applyconfiguration/api/v1beta1/machineinitialization.go
new file mode 100644
index 0000000000..5887575056
--- /dev/null
+++ b/pkg/generated/applyconfiguration/api/v1beta1/machineinitialization.go
@@ -0,0 +1,39 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1beta1
+
+// MachineInitializationApplyConfiguration represents a declarative configuration of the MachineInitialization type for use
+// with apply.
+type MachineInitializationApplyConfiguration struct {
+ Provisioned *bool `json:"provisioned,omitempty"`
+}
+
+// MachineInitializationApplyConfiguration constructs a declarative configuration of the MachineInitialization type for use with
+// apply.
+func MachineInitialization() *MachineInitializationApplyConfiguration {
+ return &MachineInitializationApplyConfiguration{}
+}
+
+// WithProvisioned sets the Provisioned field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Provisioned field is set to the value of the last call.
+func (b *MachineInitializationApplyConfiguration) WithProvisioned(value bool) *MachineInitializationApplyConfiguration {
+ b.Provisioned = &value
+ return b
+}
diff --git a/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinestatus.go b/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinestatus.go
index 03db4df1c8..cba090d317 100644
--- a/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinestatus.go
+++ b/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinestatus.go
@@ -29,6 +29,7 @@ import (
// with apply.
type OpenStackMachineStatusApplyConfiguration struct {
Ready *bool `json:"ready,omitempty"`
+ Initialization *MachineInitializationApplyConfiguration `json:"initialization,omitempty"`
InstanceID *string `json:"instanceID,omitempty"`
Addresses []v1.NodeAddress `json:"addresses,omitempty"`
InstanceState *apiv1beta1.InstanceState `json:"instanceState,omitempty"`
@@ -53,6 +54,14 @@ func (b *OpenStackMachineStatusApplyConfiguration) WithReady(value bool) *OpenSt
return b
}
+// WithInitialization sets the Initialization field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Initialization field is set to the value of the last call.
+func (b *OpenStackMachineStatusApplyConfiguration) WithInitialization(value *MachineInitializationApplyConfiguration) *OpenStackMachineStatusApplyConfiguration {
+ b.Initialization = value
+ return b
+}
+
// WithInstanceID sets the InstanceID field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the InstanceID field is set to the value of the last call.
diff --git a/pkg/generated/applyconfiguration/internal/internal.go b/pkg/generated/applyconfiguration/internal/internal.go
index deadb79120..01d6b118c9 100644
--- a/pkg/generated/applyconfiguration/internal/internal.go
+++ b/pkg/generated/applyconfiguration/internal/internal.go
@@ -681,6 +681,12 @@ var schemaYAML = typed.YAMLObject(`types:
elementType:
scalar: string
elementRelationship: atomic
+- name: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.MachineInitialization
+ map:
+ fields:
+ - name: provisioned
+ type:
+ scalar: boolean
- name: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.MachineResources
map:
fields:
@@ -1129,6 +1135,9 @@ var schemaYAML = typed.YAMLObject(`types:
- name: failureReason
type:
scalar: string
+ - name: initialization
+ type:
+ namedType: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.MachineInitialization
- name: instanceID
type:
scalar: string
diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go
index b0ba50dcaa..e927bafe5b 100644
--- a/pkg/generated/applyconfiguration/utils.go
+++ b/pkg/generated/applyconfiguration/utils.go
@@ -86,6 +86,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &apiv1beta1.ImageParamApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("LoadBalancer"):
return &apiv1beta1.LoadBalancerApplyConfiguration{}
+ case v1beta1.SchemeGroupVersion.WithKind("MachineInitialization"):
+ return &apiv1beta1.MachineInitializationApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("MachineResources"):
return &apiv1beta1.MachineResourcesApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("ManagedSecurityGroups"):