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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250711173707-dc2a20e5a5f8
github.com/openshift/api v0.0.0-20250901120840-a638ff2e96fb
github.com/openshift/api v0.0.0-20260605122244-2c1c5b39566d
github.com/openshift/client-go v0.0.0-20250710075018-396b36f983ee
github.com/openshift/cluster-api-actuator-pkg/testutils v0.0.0-20250718085303-e712b1ebf374
github.com/openshift/cluster-control-plane-machine-set-operator v0.0.0-20250424110138-1dbf0c7a5d51
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@ github.com/openshift-eng/openshift-tests-extension v0.0.0-20250711173707-dc2a20e
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250711173707-dc2a20e5a5f8/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M=
github.com/openshift/api v0.0.0-20250901120840-a638ff2e96fb h1:L5A3091VKSyOJb0nJto/pQyyHueoaW+4sXLO5fHrTBE=
github.com/openshift/api v0.0.0-20250901120840-a638ff2e96fb/go.mod h1:SPLf21TYPipzCO67BURkCfK6dcIIxx0oNRVWaOyRcXM=
github.com/openshift/api v0.0.0-20260605122244-2c1c5b39566d h1:Djz4aG2W0ypAHQEtubqbXpxeWGwdCds05w6dfz1GVWk=
github.com/openshift/api v0.0.0-20260605122244-2c1c5b39566d/go.mod h1:SPLf21TYPipzCO67BURkCfK6dcIIxx0oNRVWaOyRcXM=
github.com/openshift/client-go v0.0.0-20250710075018-396b36f983ee h1:tOtrrxfDEW8hK3eEsHqxsXurq/D6LcINGfprkQC3hqY=
github.com/openshift/client-go v0.0.0-20250710075018-396b36f983ee/go.mod h1:zhRiYyNMk89llof2qEuGPWPD+joQPhCRUc2IK0SB510=
github.com/openshift/cluster-api-actuator-pkg/testutils v0.0.0-20250718085303-e712b1ebf374 h1:ldUi0e64kdYJC2+ucB24GRXIXfMnI3NpSkcnalPqBGo=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,15 @@ spec:
labels of the machine template of the MachineSet.
format: int32
type: integer
labelSelector:
description: |-
labelSelector is a label selector, in string format, for Machines corresponding to the MachineSet.
It is exposed via the scale subresource as status.selector.
When omitted, the MachineSet controller has not yet reconciled spec.selector into status.labelSelector.
When present, it must not be empty and must not exceed 4096 characters.
maxLength: 4096
minLength: 1
type: string
observedGeneration:
description: observedGeneration reflects the generation of the most
recently observed MachineSet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,15 @@ spec:
labels of the machine template of the MachineSet.
format: int32
type: integer
labelSelector:
description: |-
labelSelector is a label selector, in string format, for Machines corresponding to the MachineSet.
It is exposed via the scale subresource as status.selector.
When omitted, the MachineSet controller has not yet reconciled spec.selector into status.labelSelector.
When present, it must not be empty and must not exceed 4096 characters.
maxLength: 4096
minLength: 1
type: string
observedGeneration:
description: observedGeneration reflects the generation of the most
recently observed MachineSet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,15 @@ spec:
labels of the machine template of the MachineSet.
format: int32
type: integer
labelSelector:
description: |-
labelSelector is a label selector, in string format, for Machines corresponding to the MachineSet.
It is exposed via the scale subresource as status.selector.
When omitted, the MachineSet controller has not yet reconciled spec.selector into status.labelSelector.
When present, it must not be empty and must not exceed 4096 characters.
maxLength: 4096
minLength: 1
type: string
observedGeneration:
description: observedGeneration reflects the generation of the most
recently observed MachineSet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,15 @@ spec:
labels of the machine template of the MachineSet.
format: int32
type: integer
labelSelector:
description: |-
labelSelector is a label selector, in string format, for Machines corresponding to the MachineSet.
It is exposed via the scale subresource as status.selector.
When omitted, the MachineSet controller has not yet reconciled spec.selector into status.labelSelector.
When present, it must not be empty and must not exceed 4096 characters.
maxLength: 4096
minLength: 1
type: string
observedGeneration:
description: observedGeneration reflects the generation of the most
recently observed MachineSet.
Expand Down
21 changes: 13 additions & 8 deletions pkg/controller/machineset/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package machineset
import (
"context"
"errors"
"fmt"
"reflect"

machinev1 "github.com/openshift/api/machine/v1beta1"
Expand Down Expand Up @@ -67,6 +66,7 @@ func (c *ReconcileMachineSet) calculateStatus(ms *machinev1.MachineSet, filtered
newStatus.FullyLabeledReplicas = int32(fullyLabeledReplicasCount)
newStatus.ReadyReplicas = int32(readyReplicasCount)
newStatus.AvailableReplicas = int32(availableReplicasCount)
newStatus.LabelSelector = metav1.FormatLabelSelector(&ms.Spec.Selector)
return newStatus
}

Expand All @@ -80,6 +80,7 @@ func updateMachineSetStatus(c client.Client, ms *machinev1.MachineSet, newStatus
ms.Status.FullyLabeledReplicas == newStatus.FullyLabeledReplicas &&
ms.Status.ReadyReplicas == newStatus.ReadyReplicas &&
ms.Status.AvailableReplicas == newStatus.AvailableReplicas &&
ms.Status.LabelSelector == newStatus.LabelSelector &&
reflect.DeepEqual(ms.Status.Conditions, newStatus.Conditions) &&
ms.Generation == ms.Status.ObservedGeneration {
return ms, nil
Expand All @@ -97,13 +98,17 @@ func updateMachineSetStatus(c client.Client, ms *machinev1.MachineSet, newStatus
if ms.Spec.Replicas != nil {
replicas = *ms.Spec.Replicas
}
klog.V(4).Infof("%s", fmt.Sprintf("Updating status for %v: %s/%s, ", ms.Kind, ms.Namespace, ms.Name)+
fmt.Sprintf("replicas %d->%d (need %d), ", ms.Status.Replicas, newStatus.Replicas, replicas)+
fmt.Sprintf("fullyLabeledReplicas %d->%d, ", ms.Status.FullyLabeledReplicas, newStatus.FullyLabeledReplicas)+
fmt.Sprintf("readyReplicas %d->%d, ", ms.Status.ReadyReplicas, newStatus.ReadyReplicas)+
fmt.Sprintf("availableReplicas %d->%d, ", ms.Status.AvailableReplicas, newStatus.AvailableReplicas)+
fmt.Sprintf("sequence No: %v->%v", ms.Status.ObservedGeneration, newStatus.ObservedGeneration)+
fmt.Sprintf("conditions: %v->%v", ms.Status.Conditions, newStatus.Conditions))
klog.V(4).Infof(
"Updating status for %v: %s/%s, replicas %d->%d (need %d), fullyLabeledReplicas %d->%d, readyReplicas %d->%d, availableReplicas %d->%d, labelSelector %q->%q, sequence No: %v->%v, conditions: %v->%v",
ms.Kind, ms.Namespace, ms.Name,
ms.Status.Replicas, newStatus.Replicas, replicas,
ms.Status.FullyLabeledReplicas, newStatus.FullyLabeledReplicas,
ms.Status.ReadyReplicas, newStatus.ReadyReplicas,
ms.Status.AvailableReplicas, newStatus.AvailableReplicas,
ms.Status.LabelSelector, newStatus.LabelSelector,
ms.Status.ObservedGeneration, newStatus.ObservedGeneration,
ms.Status.Conditions, newStatus.Conditions,
)

ms.Status = newStatus
patchErr = c.Status().Patch(context.Background(), ms, client.MergeFrom(machineSetCopy))
Expand Down
114 changes: 114 additions & 0 deletions pkg/controller/machineset/status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
Copyright 2026 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.
*/
package machineset

import (
"context"
"testing"

. "github.com/onsi/gomega"
machinev1 "github.com/openshift/api/machine/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

// TestMachineSetStatusLabelSelectorMatchesScaleSubresource verifies that status.labelSelector is the
// serialized label selector string expected by the CRD scale subresource (labelSelectorPath
// -> .status.labelSelector), which the apiserver maps to autoscaling/v1 Scale status.selector.
func TestMachineSetStatusLabelSelectorMatchesScaleSubresource(t *testing.T) {
tests := []struct {
name string
spec metav1.LabelSelector
}{
{
name: "matchLabels",
spec: metav1.LabelSelector{
MatchLabels: map[string]string{
"machine.openshift.io/cluster-api-cluster": "cluster-id",
"machine.openshift.io/cluster-api-machineset": "workers",
},
},
},
{
name: "matchExpressions",
spec: metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{Key: "role", Operator: metav1.LabelSelectorOpIn, Values: []string{"worker", "infra"}},
},
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)

ms := &machinev1.MachineSet{
Spec: machinev1.MachineSetSpec{
Selector: tc.spec,
},
}

got := (&ReconcileMachineSet{}).calculateStatus(ms, nil).LabelSelector
want := metav1.FormatLabelSelector(&ms.Spec.Selector)
g.Expect(got).To(Equal(want), "status.labelSelector must match Scale status.selector for HPA")
})
}
}

func TestUpdateMachineSetStatusUpdatesLabelSelectorWithoutReplicaChanges(t *testing.T) {
t.Helper()

g := NewWithT(t)

ms := &machinev1.MachineSet{
ObjectMeta: metav1.ObjectMeta{
Name: "machineset-test",
Namespace: "openshift-machine-api",
Generation: 1,
},
Status: machinev1.MachineSetStatus{
Replicas: 0,
FullyLabeledReplicas: 0,
ReadyReplicas: 0,
AvailableReplicas: 0,
ObservedGeneration: 1,
},
}

cl := fake.NewClientBuilder().
WithScheme(scheme.Scheme).
WithRuntimeObjects(ms.DeepCopy()).
WithStatusSubresource(&machinev1.MachineSet{}).
Build()

current := &machinev1.MachineSet{}
key := client.ObjectKeyFromObject(ms)
g.Expect(cl.Get(context.Background(), key, current)).To(Succeed(), "failed to fetch machineset")

newStatus := current.Status
newStatus.LabelSelector = "machine.openshift.io/cluster-api-cluster=test-cluster"

updated, err := updateMachineSetStatus(cl, current, newStatus)
g.Expect(err).NotTo(HaveOccurred(), "failed to update machineset status")
g.Expect(updated.Status.LabelSelector).To(Equal(newStatus.LabelSelector))

stored := &machinev1.MachineSet{}
g.Expect(cl.Get(context.Background(), key, stored)).To(Succeed(), "failed to refetch machineset")
g.Expect(stored.Status.LabelSelector).To(Equal(newStatus.LabelSelector))
}
Loading