diff --git a/hyperfleet-operator/internal/render/nodepool.go b/hyperfleet-operator/internal/render/nodepool.go index 880f6134..08537e8b 100644 --- a/hyperfleet-operator/internal/render/nodepool.go +++ b/hyperfleet-operator/internal/render/nodepool.go @@ -33,9 +33,7 @@ func NodePoolResource(nodePool *hyperfleetv1alpha1.NodePool, cluster *hyperfleet npSpec.Management.AutoRepair = true } - if len(nodePool.Spec.Labels) > 0 { - npSpec.NodeLabels = nodePool.Spec.Labels - } + npSpec.NodeLabels = nodePool.Spec.Labels if npSpec.Replicas == nil { npSpec.Replicas = ptr.To(int32(2)) diff --git a/platform-api/go.mod b/platform-api/go.mod index b1e7808f..2336fd9d 100644 --- a/platform-api/go.mod +++ b/platform-api/go.mod @@ -33,6 +33,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.36.0 k8s.io/apimachinery v0.36.0 + k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 sigs.k8s.io/controller-runtime v0.24.1 ) @@ -117,7 +118,6 @@ require ( k8s.io/client-go v0.36.0 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260706235625-cdb1db5517a0 // indirect - k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.4.1 // indirect diff --git a/platform-api/pkg/clients/hyperfleetdb/convert.go b/platform-api/pkg/clients/hyperfleetdb/convert.go index 7685777d..92074724 100644 --- a/platform-api/pkg/clients/hyperfleetdb/convert.go +++ b/platform-api/pkg/clients/hyperfleetdb/convert.go @@ -124,6 +124,18 @@ func NodePoolCRToPlatform(cr *hyperfleetv1alpha1.NodePool) *types.NodePool { UpdatedAt: metaTime(cr), } + // Sync top-level autoRepair → passthrough management.autoRepair so the + // response is internally consistent. The operator defaults to true when + // autoRepair is unset, so we mirror that here. + if np.Spec.AutoRepair != nil { + np.Spec.NodePool.Management.AutoRepair = *np.Spec.AutoRepair + } else { + np.Spec.NodePool.Management.AutoRepair = true + } + + // Sync top-level labels → passthrough nodeLabels (unconditional to clear stale values). + np.Spec.NodePool.NodeLabels = np.Spec.Labels + if phase := cr.Status.Phase; phase != "" { np.Status = &types.NodePoolStatusInfo{ ObservedGeneration: cr.Status.ObservedGeneration, diff --git a/platform-api/pkg/clients/hyperfleetdb/convert_test.go b/platform-api/pkg/clients/hyperfleetdb/convert_test.go index c6497aea..e1a12e78 100644 --- a/platform-api/pkg/clients/hyperfleetdb/convert_test.go +++ b/platform-api/pkg/clients/hyperfleetdb/convert_test.go @@ -5,6 +5,7 @@ import ( hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + "k8s.io/utils/ptr" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/types" ) @@ -47,6 +48,99 @@ func TestPlatformCreateToNodePoolCR_SetsAccountLabel(t *testing.T) { } } +func TestNodePoolCRToPlatform_AutoRepair(t *testing.T) { + tests := []struct { + name string + autoRepair *bool + wantTop *bool + wantMgmt bool + }{ + { + name: "nil defaults to true in passthrough", + autoRepair: nil, + wantTop: nil, + wantMgmt: true, + }, + { + name: "explicit true propagates to passthrough", + autoRepair: ptr.To(true), + wantTop: ptr.To(true), + wantMgmt: true, + }, + { + name: "explicit false propagates to passthrough", + autoRepair: ptr.To(false), + wantTop: ptr.To(false), + wantMgmt: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cr := &hyperfleetv1alpha1.NodePool{ + Spec: hyperfleetv1alpha1.NodePoolSpec{ + AutoRepair: tt.autoRepair, + }, + } + np := NodePoolCRToPlatform(cr) + + if tt.wantTop == nil { + if np.Spec.AutoRepair != nil { + t.Errorf("Spec.AutoRepair = %v, want nil", *np.Spec.AutoRepair) + } + } else { + if np.Spec.AutoRepair == nil { + t.Fatalf("Spec.AutoRepair = nil, want %v", *tt.wantTop) + } + if *np.Spec.AutoRepair != *tt.wantTop { + t.Errorf("Spec.AutoRepair = %v, want %v", *np.Spec.AutoRepair, *tt.wantTop) + } + } + + if np.Spec.NodePool.Management.AutoRepair != tt.wantMgmt { + t.Errorf("Spec.NodePool.Management.AutoRepair = %v, want %v", + np.Spec.NodePool.Management.AutoRepair, tt.wantMgmt) + } + }) + } +} + +func TestNodePoolCRToPlatform_Labels(t *testing.T) { + labels := map[string]string{"env": "staging", "team": "platform"} + + cr := &hyperfleetv1alpha1.NodePool{ + Spec: hyperfleetv1alpha1.NodePoolSpec{ + Labels: labels, + }, + } + np := NodePoolCRToPlatform(cr) + + if len(np.Spec.NodePool.NodeLabels) != len(labels) { + t.Fatalf("NodeLabels len = %d, want %d", len(np.Spec.NodePool.NodeLabels), len(labels)) + } + for k, v := range labels { + if np.Spec.NodePool.NodeLabels[k] != v { + t.Errorf("NodeLabels[%q] = %q, want %q", k, np.Spec.NodePool.NodeLabels[k], v) + } + } +} + +func TestNodePoolCRToPlatform_LabelsEmpty(t *testing.T) { + cr := &hyperfleetv1alpha1.NodePool{ + Spec: hyperfleetv1alpha1.NodePoolSpec{ + Labels: nil, + NodePool: hyperfleetv1alpha1.NodePoolSpecPassthrough{ + NodeLabels: map[string]string{"stale": "value"}, + }, + }, + } + np := NodePoolCRToPlatform(cr) + + if len(np.Spec.NodePool.NodeLabels) != 0 { + t.Errorf("NodeLabels = %v, want empty", np.Spec.NodePool.NodeLabels) + } +} + func TestPlatformCreateToClusterCR_SetsAccountLabel(t *testing.T) { req := &types.ClusterCreateRequest{ Name: "my-cluster",