Skip to content
Merged
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
27 changes: 22 additions & 5 deletions docs/book/src/developer/providers/migrations/v1.10-to-v1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ proposal because most of the changes described below are a consequence of the wo
- v1beta2 API version has been introduced; notable changes are:
- The transition to the new K8s aligned conditions using `metav1.Conditions` types and the new condition semantic
has been completed for all Kinds:
- `status.conditions` has been replaced with `status.v1beta2.conditions`
- the old `status.conditions` will continue to exist temporarily under `status.deprecated.v1beta1` for the sake of
down conversions and to provide a temporary option for users willing to continue using old conditions.
- `status.conditions` has been replaced with `status.v1beta2.conditions` based on metav1 condition types
- the old `status.conditions` based on custom cluster API condition types will continue to exist temporarily
under `status.deprecated.v1beta1.conditions` for the sake of down conversions and to provide a temporary option
for users willing to continue using old conditions.
- Support for terminal errors has been dropped from all Kinds
- `status.failureReason` and `status.failureMessage` will continue to exist temporarily under `status.deprecated.v1beta1`
for the sake of down conversions and to provide a temporary option for users willing to continue using old conditions.
Expand All @@ -27,7 +28,16 @@ proposal because most of the changes described below are a consequence of the wo
- All the resources handling machine replicas have now a consistent set of replica counters based on corresponding
conditions defined at machine level.
- `status.readyReplicas`, `status.availableReplicas`, `status.upToDateReplicas` on `MachineDeployments`, `MachineSet`
and `KubeadmControlPlane`
and `KubeadmControlPlane`; please note that
- `status.readyReplicas` has now a new semantic based on machine's `Ready` condition
- `status.availableReplicas` has now a new semantic based on machine's `Available` condition
- `status.upToDateReplicas` has now a new semantic (and name) based on machine's `UpToDate` condition
- Temporarily, old replica counters will still be available under the `status.deprecated.v1beta1` struct; more specifically
- `status.deprecated.v1beta1.readyReplicas` with old semantic for `MachineDeployments`, `MachineSet` and `KubeadmControlPlane`
- `status.deprecated.v1beta1.availableReplicas` with old semantic for `MachineDeployments`, `MachineSet`
- `status.deprecated.v1beta1.unavailableReplicas` with old semantic for `MachineDeployments`, `KubeadmControlPlane`
- `status.deprecated.v1beta1.updatedReplicas` with old semantic (and name) for `MachineDeployments`, `KubeadmControlPlane`
- `status.deprecated.v1beta1.fullyLabeledReplicas` for `MachineSet`
- The `Cluster` resource reports replica counters for both control plane and worker machines.

### Cluster API Contract changes
Expand All @@ -51,7 +61,14 @@ See [provider contracts](../contracts/overview.md) for more details.
### Deprecation

- v1beta1 API version is deprecated and it will be removed tentatively in August 2026
- All the fields under `status.deprecated.v1beta1` in the new v1beta2 API are deprecated and whey will be removed
- All the fields under `status.deprecated.v1beta1` in the new v1beta2 API are deprecated and whey will be removed. This includes:
- `status.deprecated.v1beta1.conditions` based on custom cluster API condition types
- `status.deprecated.v1beta1.failureReason` and `status.failureMessage`
- `status.deprecated.v1beta1.readyReplicas` with old semantic for `MachineDeployments`, `MachineSet` and `KubeadmControlPlane`
- `status.deprecated.v1beta1.availableReplicas` with old semantic for `MachineDeployments`, `MachineSet`
- `status.deprecated.v1beta1.unavailableReplicas` with old semantic for `MachineDeployments`, `KubeadmControlPlane`
- `status.deprecated.v1beta1.updatedReplicas` with old semantic (and name) for `MachineDeployments`, `KubeadmControlPlane`
- `status.deprecated.v1beta1.fullyLabeledReplicas` for `MachineSet`
- v1beta1 conditions utils are now deprecated, and will removed as soon as v1beta1 API will be removed
- v1beta1 support in the patch helper is now deprecated, and will removed as soon as v1beta1 API will be removed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ type ControlPlaneBuiltins struct {

// replicas is the value of the replicas field of the ControlPlane object.
// +optional
Replicas *int64 `json:"replicas,omitempty"`
Replicas *int32 `json:"replicas,omitempty"`

// machineTemplate is the value of the .spec.machineTemplate field of the ControlPlane object.
// +optional
Expand Down
2 changes: 1 addition & 1 deletion exp/runtime/hooks/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion exp/topology/desiredstate/desired_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ func (g *generator) computeControlPlane(ctx context.Context, s *scope.Scope, inf
// NOTE: If the Topology.ControlPlane.replicas value is nil, it is assumed that the control plane controller
// does not implement support for this field and the ControlPlane object is generated without the number of Replicas.
if s.Blueprint.Topology.ControlPlane.Replicas != nil {
if err := contract.ControlPlane().Replicas().Set(controlPlane, int64(*s.Blueprint.Topology.ControlPlane.Replicas)); err != nil {
if err := contract.ControlPlane().Replicas().Set(controlPlane, *s.Blueprint.Topology.ControlPlane.Replicas); err != nil {
return nil, errors.Wrapf(err, "failed to set %s in the ControlPlane object", contract.ControlPlane().Replicas().Path())
}
}
Expand Down
10 changes: 3 additions & 7 deletions exp/topology/desiredstate/desired_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1859,13 +1859,9 @@ func TestComputeMachineDeployment(t *testing.T) {
WithStatus(clusterv1.MachineDeploymentStatus{
ObservedGeneration: 2,
Replicas: 2,
Deprecated: &clusterv1.MachineDeploymentDeprecatedStatus{
V1Beta1: &clusterv1.MachineDeploymentV1Beta1DeprecatedStatus{
ReadyReplicas: 2,
UpdatedReplicas: 2,
AvailableReplicas: 2,
},
},
ReadyReplicas: ptr.To[int32](2),
UpToDateReplicas: ptr.To[int32](2),
AvailableReplicas: ptr.To[int32](2),
}).
Build()
mdsState = duplicateMachineDeploymentsState(mdsState)
Expand Down
157 changes: 61 additions & 96 deletions internal/contract/controlplane.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,95 +93,49 @@ func (c *ControlPlaneContract) ControlPlaneEndpoint() *ControlPlaneEndpoint {
// NOTE: When working with unstructured there is no way to understand if the ControlPlane provider
// do support a field in the type definition from the fact that a field is not set in a given instance.
// This is why in we are deriving if replicas is required from the ClusterClass in the topology reconciler code.
func (c *ControlPlaneContract) Replicas() *Int64 {
return &Int64{
func (c *ControlPlaneContract) Replicas() *Int32 {
return &Int32{
path: []string{"spec", "replicas"},
}
}

// StatusReplicas provide access to the status.replicas field in a ControlPlane object, if any. Applies to implementations using replicas.
func (c *ControlPlaneContract) StatusReplicas() *Int64 {
return &Int64{
func (c *ControlPlaneContract) StatusReplicas() *Int32 {
return &Int32{
path: []string{"status", "replicas"},
}
}

// UpdatedReplicas provide access to the status.updatedReplicas field in a ControlPlane object, if any. Applies to implementations using replicas.
// TODO (v1beta2): Rename to V1Beta1DeprecatedUpdatedReplicas and make sure we are only using this method for compatibility with old contracts.
func (c *ControlPlaneContract) UpdatedReplicas(contractVersion string) *Int64 {
if contractVersion == "v1beta1" {
return &Int64{
path: []string{"status", "updatedReplicas"},
}
}

return &Int64{
path: []string{"status", "deprecated", "v1beta1", "updatedReplicas"},
}
}

// ReadyReplicas provide access to the status.readyReplicas field in a ControlPlane object, if any. Applies to implementations using replicas.
// TODO (v1beta2): Rename to V1Beta1DeprecatedReadyReplicas and make sure we are only using this method for compatibility with old contracts.
func (c *ControlPlaneContract) ReadyReplicas(contractVersion string) *Int64 {
if contractVersion == "v1beta1" {
return &Int64{
path: []string{"status", "readyReplicas"},
}
}

return &Int64{
path: []string{"status", "deprecated", "v1beta1", "readyReplicas"},
}
}

// UnavailableReplicas provide access to the status.unavailableReplicas field in a ControlPlane object, if any. Applies to implementations using replicas.
// TODO (v1beta2): Rename to V1Beta1DeprecatedUnavailableReplicas and make sure we are only using this method for compatibility with old contracts.
func (c *ControlPlaneContract) UnavailableReplicas(contractVersion string) *Int64 {
if contractVersion == "v1beta1" {
return &Int64{
path: []string{"status", "unavailableReplicas"},
}
}

return &Int64{
path: []string{"status", "deprecated", "v1beta1", "unavailableReplicas"},
}
}

// V1Beta2ReadyReplicas provide access to the status.readyReplicas field in a ControlPlane object, if any. Applies to implementations using replicas.
// TODO (v1beta2): Drop V1Beta2 prefix..
func (c *ControlPlaneContract) V1Beta2ReadyReplicas(contractVersion string) *Int32 {
if contractVersion == "v1beta1" {
return &Int32{
path: []string{"status", "v1beta2", "readyReplicas"},
}
}

// NOTE: readyReplicas changed semantic in v1beta2 contract.
func (c *ControlPlaneContract) ReadyReplicas() *Int32 {
return &Int32{
path: []string{"status", "readyReplicas"},
}
}

// V1Beta2AvailableReplicas provide access to the status.availableReplicas field in a ControlPlane object, if any. Applies to implementations using replicas.
// TODO (v1beta2): Drop V1Beta2 prefix.x.
func (c *ControlPlaneContract) V1Beta2AvailableReplicas(contractVersion string) *Int32 {
if contractVersion == "v1beta1" {
return &Int32{
path: []string{"status", "v1beta2", "availableReplicas"},
}
}

// AvailableReplicas provide access to the status.availableReplicas field in a ControlPlane object, if any. Applies to implementations using replicas.
// NOTE: availableReplicas was introduced by the v1beta2 contract; use unavailableReplicas for the v1beta1 contract.
func (c *ControlPlaneContract) AvailableReplicas() *Int32 {
return &Int32{
path: []string{"status", "availableReplicas"},
}
}

// V1Beta2UpToDateReplicas provide access to the status.upToDateReplicas field in a ControlPlane object, if any. Applies to implementations using replicas.
// TODO (v1beta2): Drop V1Beta2 prefix.ix.
func (c *ControlPlaneContract) V1Beta2UpToDateReplicas(contractVersion string) *Int32 {
// V1Beta1UnavailableReplicas provide access to the status.unavailableReplicas field in a ControlPlane object, if any. Applies to implementations using replicas.
// NOTE: use availableReplicas when working with the v1beta2 contract.
func (c *ControlPlaneContract) V1Beta1UnavailableReplicas() *Int64 {
return &Int64{
path: []string{"status", "unavailableReplicas"},
}
}

// UpToDateReplicas provide access to the status.upToDateReplicas field in a ControlPlane object, if any. Applies to implementations using replicas.
// NOTE: upToDateReplicas was introduced by the v1beta2 contract; code will fall back to updatedReplicas for the v1beta1 contract.
func (c *ControlPlaneContract) UpToDateReplicas(contractVersion string) *Int32 {
if contractVersion == "v1beta1" {
return &Int32{
path: []string{"status", "v1beta2", "upToDateReplicas"},
path: []string{"status", "updatedReplicas"},
}
}

Expand Down Expand Up @@ -285,19 +239,16 @@ func (c *ControlPlaneContract) IsUpgrading(obj *unstructured.Unstructured) (bool
// A control plane is considered scaling if:
// - status.replicas is not yet set.
// - spec.replicas != status.replicas.
// - spec.replicas != status.updatedReplicas.
// - spec.replicas != status.upToDateReplicas.
// - spec.replicas != status.readyReplicas.
// - status.unavailableReplicas > 0.
// - spec.replicas != status.availableReplicas.
// NOTE: this function is used only in E2E tests.
func (c *ControlPlaneContract) IsScaling(obj *unstructured.Unstructured, contractVersion string) (bool, error) {
desiredReplicas, err := c.Replicas().Get(obj)
if err != nil {
return false, errors.Wrap(err, "failed to get control plane spec replicas")
return false, errors.Wrapf(err, "failed to get control plane %s", c.Replicas().Path().String())
}

// TODO (v1beta2): Add a new code path using v1beta2 replica counters
// note: currently we are still always using v1beta1 counters no matter if they are moved under deprecated
// but we should stop doing this ASAP

statusReplicas, err := c.StatusReplicas().Get(obj)
if err != nil {
if errors.Is(err, ErrFieldNotFound) {
Expand All @@ -306,54 +257,68 @@ func (c *ControlPlaneContract) IsScaling(obj *unstructured.Unstructured, contrac
// so that we can block any operations that expect control plane to be stable.
return true, nil
}
return false, errors.Wrap(err, "failed to get control plane status replicas")
return false, errors.Wrapf(err, "failed to get control plane %s", c.StatusReplicas().Path().String())
}

updatedReplicas, err := c.UpdatedReplicas(contractVersion).Get(obj)
upToDateReplicas, err := c.UpToDateReplicas(contractVersion).Get(obj)
if err != nil {
if errors.Is(err, ErrFieldNotFound) {
// If updatedReplicas is not set on the control plane
// we should consider the control plane to be scaling so that
// we block any operation that expect the control plane to be stable.
return true, nil
}
return false, errors.Wrap(err, "failed to get control plane status updatedReplicas")
return false, errors.Wrapf(err, "failed to get control plane %s", c.UpToDateReplicas(contractVersion).Path().String())
}

readyReplicas, err := c.ReadyReplicas(contractVersion).Get(obj)
readyReplicas, err := c.ReadyReplicas().Get(obj)
if err != nil {
if errors.Is(err, ErrFieldNotFound) {
// If readyReplicas is not set on the control plane
// we should consider the control plane to be scaling so that
// we block any operation that expect the control plane to be stable.
return true, nil
}
return false, errors.Wrap(err, "failed to get control plane status readyReplicas")
return false, errors.Wrapf(err, "failed to get control plane %s", c.ReadyReplicas().Path().String())
}

unavailableReplicas, err := c.UnavailableReplicas(contractVersion).Get(obj)
if err != nil {
if !errors.Is(err, ErrFieldNotFound) {
return false, errors.Wrap(err, "failed to get control plane status unavailableReplicas")
var availableReplicas *int32
if contractVersion == "v1beta1" {
unavailableReplicas, err := c.V1Beta1UnavailableReplicas().Get(obj)
if err != nil {
if !errors.Is(err, ErrFieldNotFound) {
return false, errors.Wrapf(err, "failed to get control plane %s", c.V1Beta1UnavailableReplicas().Path().String())
}
// If unavailableReplicas is not set on the control plane we assume it is 0.
// We have to do this as the following happens after clusterctl move with KCP:
// * clusterctl move creates the KCP object without status
// * the KCP controller won't patch the field to 0 if it doesn't exist
// * This is because the patchHelper marshals before/after object to JSON to calculate a diff
// and as the unavailableReplicas field is not a pointer, not set and 0 are both rendered as 0.
// If before/after of the field is the same (i.e. 0), there is no diff and thus also no patch to set it to 0.
unavailableReplicas = ptr.To[int64](0)
}
availableReplicas = ptr.To(*desiredReplicas - int32(*unavailableReplicas))
} else {
availableReplicas, err = c.AvailableReplicas().Get(obj)
if err != nil {
if errors.Is(err, ErrFieldNotFound) {
// If availableReplicas is not set on the control plane
// we should consider the control plane to be scaling so that
// we block any operation that expect the control plane to be stable.
return true, nil
}
return false, errors.Wrapf(err, "failed to get control plane %s", c.AvailableReplicas().Path().String())
}
// If unavailableReplicas is not set on the control plane we assume it is 0.
// We have to do this as the following happens after clusterctl move with KCP:
// * clusterctl move creates the KCP object without status
// * the KCP controller won't patch the field to 0 if it doesn't exist
// * This is because the patchHelper marshals before/after object to JSON to calculate a diff
// and as the unavailableReplicas field is not a pointer, not set and 0 are both rendered as 0.
// If before/after of the field is the same (i.e. 0), there is no diff and thus also no patch to set it to 0.
unavailableReplicas = ptr.To[int64](0)
}

// Control plane is still scaling if:
// * .spec.replicas, .status.replicas, .status.updatedReplicas,
// .status.readyReplicas are not equal and
// * unavailableReplicas > 0
// * .spec.replicas, .status.replicas, .status.upToDateReplicas,
// .status.readyReplicas, .status.availableReplicas are not equal.
if *statusReplicas != *desiredReplicas ||
*updatedReplicas != *desiredReplicas ||
*upToDateReplicas != *desiredReplicas ||
*readyReplicas != *desiredReplicas ||
*unavailableReplicas > 0 {
*availableReplicas != *desiredReplicas {
return true, nil
}
return false, nil
Expand Down
Loading