diff --git a/go.mod b/go.mod
index 448c616fa9..38b0628e69 100644
--- a/go.mod
+++ b/go.mod
@@ -2,6 +2,10 @@ module github.com/openshift/library-go
go 1.26.0
+replace github.com/openshift/api => github.com/bhperry/openshift-api v0.0.0-20260713144529-6c01886662a4
+
+replace github.com/openshift/client-go => github.com/bhperry/openshift-client-go v0.0.0-20260716151504-c6335807bf79
+
require (
github.com/RangelReale/osincli v0.0.0-20160924135400-fababb0555f2
github.com/blang/semver/v4 v4.0.0
diff --git a/go.sum b/go.sum
index 4faf94f0d9..736ffcd18e 100644
--- a/go.sum
+++ b/go.sum
@@ -20,6 +20,10 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bhperry/openshift-api v0.0.0-20260713144529-6c01886662a4 h1:BUDKaQuVUj1zyTKsx3i8OGl7diLg3KUKvjhP3WJ1wk4=
+github.com/bhperry/openshift-api v0.0.0-20260713144529-6c01886662a4/go.mod h1:7WJ3IPaK6nmWT8bDcaNooHqd0H5WepjVqV/10VlkMEM=
+github.com/bhperry/openshift-client-go v0.0.0-20260716151504-c6335807bf79 h1:T1Ld4Rf0ZdfeaNX+HXzSCNqtvp5b774qa8Rj59wp7RE=
+github.com/bhperry/openshift-client-go v0.0.0-20260716151504-c6335807bf79/go.mod h1:Ffgh01R2/DnIbnMmnuBtg1hYIKvQBFp3J2tkISzgGhU=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -248,12 +252,8 @@ github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrB
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/selinux v1.13.0 h1:Zza88GWezyT7RLql12URvoxsbLfjFx988+LGaWfbL84=
github.com/opencontainers/selinux v1.13.0/go.mod h1:XxWTed+A/s5NNq4GmYScVy+9jzXhGBVEOAyucdRUY8s=
-github.com/openshift/api v0.0.0-20260715165912-72066cc9718b h1:gN3SihCYEwoIksD+f24wHhwiEgvaV0RxNjgmkDvBBeg=
-github.com/openshift/api v0.0.0-20260715165912-72066cc9718b/go.mod h1:k6qH5QOVa5GDln2VVm8Jz4NV3Z7R2SATHFLwGS6Wh3M=
github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee h1:+Sp5GGnjHDhT/a/nQ1xdp43UscBMr7G5wxsYotyhzJ4=
github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE=
-github.com/openshift/client-go v0.0.0-20260715172546-dac61734e0ec h1:UDjX+mot5IVLpcChyBqLXG1oSB29s4UkqFmgNb0Xsqc=
-github.com/openshift/client-go v0.0.0-20260715172546-dac61734e0ec/go.mod h1:iMHec0APKVjOH8GfL/RxddX8DuiuSvPlRe+s7KDqlyA=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
diff --git a/pkg/operator/staticpod/controller/installer/installer_controller.go b/pkg/operator/staticpod/controller/installer/installer_controller.go
index f6c4dc5538..ec5aba1470 100644
--- a/pkg/operator/staticpod/controller/installer/installer_controller.go
+++ b/pkg/operator/staticpod/controller/installer/installer_controller.go
@@ -12,6 +12,7 @@ import (
"time"
"github.com/davecgh/go-spew/spew"
+ "github.com/pkg/errors"
operatorv1 "github.com/openshift/api/operator/v1"
applyoperatorv1 "github.com/openshift/client-go/operator/applyconfigurations/operator/v1"
@@ -34,6 +35,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/informers"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
+ corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/klog/v2"
"k8s.io/utils/clock"
"k8s.io/utils/ptr"
@@ -94,6 +96,7 @@ type InstallerController struct {
configMapsGetter corev1client.ConfigMapsGetter
secretsGetter corev1client.SecretsGetter
podsGetter corev1client.PodsGetter
+ nodeLister corev1listers.NodeLister
eventRecorder events.Recorder
now func() time.Time // for test plumbing
@@ -179,6 +182,7 @@ func NewInstallerController(
secrets []revision.RevisionResource,
command []string,
kubeInformersForTargetNamespace informers.SharedInformerFactory,
+ kubeInformersClusterScoped informers.SharedInformerFactory,
operatorClient v1helpers.StaticPodOperatorClient,
configMapsGetter corev1client.ConfigMapsGetter,
secretsGetter corev1client.SecretsGetter,
@@ -197,6 +201,7 @@ func NewInstallerController(
configMapsGetter: configMapsGetter,
secretsGetter: secretsGetter,
podsGetter: podsGetter,
+ nodeLister: kubeInformersClusterScoped.Core().V1().Nodes().Lister(),
eventRecorder: eventRecorder.WithComponentSuffix("installer-controller"),
now: time.Now,
startupMonitorEnabled: func() (bool, error) {
@@ -218,7 +223,12 @@ func NewInstallerController(
// informers are needed here because their Getter are cached lister based
kubeInformersForTargetNamespace.Core().V1().Pods().Informer(),
kubeInformersForTargetNamespace.Core().V1().ConfigMaps().Informer(),
- kubeInformersForTargetNamespace.Core().V1().Secrets().Informer())
+ kubeInformersForTargetNamespace.Core().V1().Secrets().Informer(),
+ ).
+ WithBareInformers(
+ // Don't need to sync on node changes, just wait for informer cache sync
+ kubeInformersClusterScoped.Core().V1().Nodes().Informer(),
+ )
return c
}
@@ -599,6 +609,7 @@ func prepareNodeStatusApplyConfigurationFor(nodeStatuses []operatorv1.NodeStatus
func nodeStatusToNodeStatusApplyConfigurations(nodeStatus operatorv1.NodeStatus) *applyoperatorv1.NodeStatusApplyConfiguration {
nodeStatusApplyConfiguration := applyoperatorv1.NodeStatus().
WithNodeName(nodeStatus.NodeName).
+ WithNodeUID(nodeStatus.NodeUID).
WithCurrentRevision(nodeStatus.CurrentRevision).
WithTargetRevision(nodeStatus.TargetRevision).
WithLastFailedRevision(nodeStatus.LastFailedRevision).
@@ -615,6 +626,7 @@ func nodeStatusToNodeStatusApplyConfigurations(nodeStatus operatorv1.NodeStatus)
func nodeStatusApplyConfigToOperatorNodeStatus(nodeStatusApplyConfiguration *applyoperatorv1.NodeStatusApplyConfiguration) *operatorv1.NodeStatus {
return &operatorv1.NodeStatus{
NodeName: ptr.Deref(nodeStatusApplyConfiguration.NodeName, ""),
+ NodeUID: ptr.Deref(nodeStatusApplyConfiguration.NodeUID, ""),
CurrentRevision: ptr.Deref(nodeStatusApplyConfiguration.CurrentRevision, 0),
TargetRevision: ptr.Deref(nodeStatusApplyConfiguration.TargetRevision, 0),
LastFailedRevision: ptr.Deref(nodeStatusApplyConfiguration.LastFailedRevision, 0),
@@ -1123,6 +1135,25 @@ func (c InstallerController) ensureRequiredResourcesExist(ctx context.Context, r
return fmt.Errorf("missing required resources: %v", aggregatedErr)
}
+func (c *InstallerController) hasNodeUIDChanged(nodeName, nodeUid string) (bool, error) {
+ if nodeUid == "" {
+ return false, nil
+ }
+ node, err := c.nodeLister.Get(nodeName)
+ if apierrors.IsNotFound(err) {
+ klog.Warningf("Skipping node status update for %s: node not found", nodeName)
+ return true, nil
+ }
+ if err != nil {
+ return false, err
+ }
+ if string(node.UID) != nodeUid {
+ klog.Warningf("Skipping node status update for %s: node UID changed (stored=%s, actual=%s)", nodeName, nodeUid, node.UID)
+ return true, nil
+ }
+ return false, nil
+}
+
func (c *InstallerController) Sync(ctx context.Context, syncCtx factory.SyncContext) error {
operatorSpec, originalOperatorStatus, operatorResourceVersion, err := c.operatorClient.GetStaticPodOperatorState()
if err != nil {
@@ -1164,6 +1195,17 @@ func (c *InstallerController) Sync(ctx context.Context, syncCtx factory.SyncCont
return nil
}
+ // Check for stale node status entries with mismatched UID
+ for _, nodeStatus := range originalOperatorStatus.NodeStatuses {
+ if uidChanged, err := c.hasNodeUIDChanged(nodeStatus.NodeName, nodeStatus.NodeUID); err != nil {
+ return errors.Wrapf(err, "error validating uid for node %s", nodeStatus.NodeName)
+ } else if uidChanged {
+ // Avoid applying node statuses when a stale entry is found
+ // Sync will retrigger when the old node status is removed by the node controller
+ return nil
+ }
+ }
+
err = c.ensureRequiredResourcesExistFn(ctx, originalOperatorStatus.LatestAvailableRevision)
// Only manage installation pods when all required certs are present.
@@ -1204,6 +1246,7 @@ func deepCopyNodeStatusWithoutOldFailedState(ns *operatorv1.NodeStatus) *operato
if ns.TargetRevision == 0 || ns.TargetRevision != ns.LastFailedRevision {
return &operatorv1.NodeStatus{
NodeName: ns.NodeName,
+ NodeUID: ns.NodeUID,
CurrentRevision: ns.CurrentRevision,
TargetRevision: ns.TargetRevision,
}
diff --git a/pkg/operator/staticpod/controller/installer/installer_controller_test.go b/pkg/operator/staticpod/controller/installer/installer_controller_test.go
index 3b18e46e13..a59062c100 100644
--- a/pkg/operator/staticpod/controller/installer/installer_controller_test.go
+++ b/pkg/operator/staticpod/controller/installer/installer_controller_test.go
@@ -555,6 +555,7 @@ func TestNewNodeStateForInstallInProgress(t *testing.T) {
[]revision.RevisionResource{{Name: "test-secret"}},
[]string{"/bin/true", "--foo=test", "--bar"},
kubeInformers,
+ kubeInformers,
fakeStaticPodOperatorClient,
kubeClient.CoreV1(),
kubeClient.CoreV1(),
@@ -690,6 +691,7 @@ func testSync(t *testing.T, firstInstallerBehaviour testSyncInstallerBehaviour,
[]revision.RevisionResource{{Name: "test-secret"}},
podCommand,
kubeInformers,
+ kubeInformers,
fakeStaticPodOperatorClient,
kubeClient.CoreV1(),
kubeClient.CoreV1(),
@@ -1099,6 +1101,7 @@ func TestCreateInstallerPod(t *testing.T) {
[]revision.RevisionResource{{Name: "test-secret"}},
[]string{"/bin/true"},
kubeInformers,
+ kubeInformers,
fakeStaticPodOperatorClient,
kubeClient.CoreV1(),
kubeClient.CoreV1(),
@@ -1268,6 +1271,7 @@ func TestEnsureInstallerPod(t *testing.T) {
tt.secrets,
[]string{"/bin/true"},
kubeInformers,
+ kubeInformers,
fakeStaticPodOperatorClient,
kubeClient.CoreV1(),
kubeClient.CoreV1(),
@@ -2011,6 +2015,7 @@ func TestCreateInstallerPodMultiNode(t *testing.T) {
[]revision.RevisionResource{{Name: "test-secret"}},
[]string{"/bin/true"},
kubeInformers,
+ kubeInformers,
fakeStaticPodOperatorClient,
kubeClient.CoreV1(),
kubeClient.CoreV1(),
diff --git a/pkg/operator/staticpod/controller/node/node_controller.go b/pkg/operator/staticpod/controller/node/node_controller.go
index 910ff9d213..90dd9653a3 100644
--- a/pkg/operator/staticpod/controller/node/node_controller.go
+++ b/pkg/operator/staticpod/controller/node/node_controller.go
@@ -84,22 +84,24 @@ func (c *NodeController) sync(ctx context.Context, syncCtx factory.SyncContext)
nodes = append(nodes, extraNodes...)
}
+ nodeUIDs := map[string]string{}
+ for _, node := range nodes {
+ nodeUIDs[node.Name] = string(node.UID)
+ }
+
jsonPatch := jsonpatch.New()
var removedNodeStatusesCounter int
newTargetNodeStates := []*applyoperatorv1.NodeStatusApplyConfiguration{}
// remove entries for missing nodes
for i, nodeState := range originalOperatorStatus.NodeStatuses {
- found := false
- for _, node := range nodes {
- if nodeState.NodeName == node.Name {
- found = true
- }
- }
- if found {
- newTargetNodeState := applyoperatorv1.NodeStatus().WithNodeName(originalOperatorStatus.NodeStatuses[i].NodeName)
+ currentUID, found := nodeUIDs[nodeState.NodeName]
+ if found && (nodeState.NodeUID == currentUID || nodeState.NodeUID == "") {
+ newTargetNodeState := applyoperatorv1.NodeStatus().
+ WithNodeName(nodeState.NodeName).
+ WithNodeUID(currentUID)
newTargetNodeStates = append(newTargetNodeStates, newTargetNodeState)
} else {
- syncCtx.Recorder().Warningf("MasterNodeRemoved", "Observed removal of master node %s", nodeState.NodeName)
+ syncCtx.Recorder().Warningf("MasterNodeRemoved", "Observed removal of master node %s (UID: %s)", nodeState.NodeName, nodeState.NodeUID)
// each delete operation is applied to the object,
// which modifies the array. Thus, we need to
// adjust the indices to find the correct node to remove.
@@ -116,16 +118,19 @@ func (c *NodeController) sync(ctx context.Context, syncCtx factory.SyncContext)
for _, node := range nodes {
found := false
for _, nodeState := range originalOperatorStatus.NodeStatuses {
+ // NodeUID is intentionally ignored here. Nodes that are replaced with the same name will get added in the next sync
+ // to ensure previous nodeStatus from a stale cache isn't merged in with the fresh status.
if nodeState.NodeName == node.Name {
found = true
+ break
}
}
if found {
continue
}
- syncCtx.Recorder().Eventf("MasterNodeObserved", "Observed new master node %s", node.Name)
- newTargetNodeState := applyoperatorv1.NodeStatus().WithNodeName(node.Name)
+ syncCtx.Recorder().Eventf("MasterNodeObserved", "Observed new master node %s (UID: %s)", node.Name, node.UID)
+ newTargetNodeState := applyoperatorv1.NodeStatus().WithNodeName(node.Name).WithNodeUID(string(node.UID))
newTargetNodeStates = append(newTargetNodeStates, newTargetNodeState)
}
diff --git a/pkg/operator/staticpod/controller/node/node_controller_test.go b/pkg/operator/staticpod/controller/node/node_controller_test.go
index 20784fbca9..e1bd821fe1 100644
--- a/pkg/operator/staticpod/controller/node/node_controller_test.go
+++ b/pkg/operator/staticpod/controller/node/node_controller_test.go
@@ -13,6 +13,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/selection"
+ "k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/client-go/kubernetes/fake"
clientgotesting "k8s.io/client-go/testing"
clocktesting "k8s.io/utils/clock/testing"
@@ -28,6 +29,7 @@ import (
func fakeMasterNode(name string) *corev1.Node {
n := &corev1.Node{}
n.Name = name
+ n.UID = uuid.NewUUID()
n.Labels = map[string]string{
"node-role.kubernetes.io/master": "",
}
@@ -98,6 +100,21 @@ func validateJSONPatch(expected, actual *jsonpatch.PatchSet) error {
return nil
}
+func validateNodeStatuses(nodeStatuses []operatorv1.NodeStatus, nodes ...*corev1.Node) error {
+ if len(nodes) != len(nodeStatuses) {
+ return fmt.Errorf("expected %d node status, got %d", len(nodes), len(nodeStatuses))
+ }
+ for i, node := range nodes {
+ if nodeStatuses[i].NodeName != node.Name {
+ return fmt.Errorf("expected %q as node name, got %q", node.Name, nodeStatuses[i].NodeName)
+ }
+ if nodeStatuses[i].NodeUID != string(node.UID) {
+ return fmt.Errorf("expected %q as node UID, got %q", node.UID, nodeStatuses[i].NodeUID)
+ }
+ }
+ return nil
+}
+
func TestNodeControllerDegradedConditionType(t *testing.T) {
scenarios := []struct {
name string
@@ -729,3 +746,123 @@ func TestNewNodeController(t *testing.T) {
}
}
+
+func TestNodeControllerTrackNodeUIDs(t *testing.T) {
+ node1 := fakeMasterNode("test-node-1")
+ node2 := fakeMasterNode("test-node-2")
+
+ tests := []struct {
+ name string
+ syncCycles int
+ startNodes []runtime.Object
+ startNodeStatus []operatorv1.NodeStatus
+ expectedNodes [][2]string
+ evaluateNodeStatus func(s []operatorv1.NodeStatus, cycle int) error
+ }{
+ {
+ name: "node exists missing uid in status",
+ syncCycles: 1,
+ startNodes: []runtime.Object{node1},
+ startNodeStatus: []operatorv1.NodeStatus{
+ {
+ NodeName: "test-node-1",
+ },
+ },
+ evaluateNodeStatus: func(s []operatorv1.NodeStatus, cycle int) error {
+ return validateNodeStatuses(s, node1)
+ },
+ },
+ {
+ name: "node exists matching uid in status",
+ syncCycles: 1,
+ startNodes: []runtime.Object{node1},
+ startNodeStatus: []operatorv1.NodeStatus{
+ {
+ NodeName: "test-node-1",
+ NodeUID: string(node1.UID),
+ },
+ },
+ evaluateNodeStatus: func(s []operatorv1.NodeStatus, cycle int) error {
+ return validateNodeStatuses(s, node1)
+ },
+ },
+ {
+ name: "node replaced mismatched uid in status",
+ syncCycles: 2,
+ startNodes: []runtime.Object{node1},
+ startNodeStatus: []operatorv1.NodeStatus{
+ {
+ NodeName: "test-node-1",
+ NodeUID: string(uuid.NewUUID()),
+ },
+ },
+ evaluateNodeStatus: func(s []operatorv1.NodeStatus, cycle int) error {
+ if cycle == 0 {
+ // On first sync, old node status is removed
+ return validateNodeStatuses(s)
+ } else {
+ // On second sync, new node status is added
+ return validateNodeStatuses(s, node1)
+ }
+ },
+ },
+ {
+ name: "node removed",
+ syncCycles: 1,
+ startNodes: []runtime.Object{node2},
+ startNodeStatus: []operatorv1.NodeStatus{
+ {
+ NodeName: "test-node-1",
+ NodeUID: string(node1.UID),
+ },
+ },
+ evaluateNodeStatus: func(s []operatorv1.NodeStatus, cycle int) error {
+ return validateNodeStatuses(s, node2)
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ kubeClient := fake.NewSimpleClientset(test.startNodes...)
+ fakeLister := v1helpers.NewFakeNodeLister(kubeClient)
+ fakeStaticPodOperatorClient := v1helpers.NewFakeStaticPodOperatorClient(
+ &operatorv1.StaticPodOperatorSpec{
+ OperatorSpec: operatorv1.OperatorSpec{
+ ManagementState: operatorv1.Managed,
+ },
+ },
+ &operatorv1.StaticPodOperatorStatus{
+ OperatorStatus: operatorv1.OperatorStatus{
+ LatestAvailableRevision: 1,
+ },
+ NodeStatuses: test.startNodeStatus,
+ },
+ nil,
+ nil,
+ )
+
+ eventRecorder := events.NewRecorder(kubeClient.CoreV1().Events("test"), "test-operator", &corev1.ObjectReference{}, clocktesting.NewFakePassiveClock(time.Now()))
+
+ c := &NodeController{
+ operatorClient: fakeStaticPodOperatorClient,
+ nodeLister: fakeLister,
+ masterNodesSelector: masterNodesSelector(t),
+ }
+
+ // override the lister so we don't have to run the informer to list nodes
+ c.nodeLister = fakeLister
+ for i := range test.syncCycles {
+ if err := c.sync(context.TODO(), factory.NewSyncContext("NodeController", eventRecorder)); err != nil {
+ t.Fatal(err)
+ }
+
+ _, status, _, _ := fakeStaticPodOperatorClient.GetStaticPodOperatorState()
+ if err := test.evaluateNodeStatus(status.NodeStatuses, i); err != nil {
+ t.Errorf("%s: failed to evaluate node status: %v", test.name, err)
+ }
+ }
+ })
+
+ }
+}
diff --git a/pkg/operator/staticpod/controllers.go b/pkg/operator/staticpod/controllers.go
index 62722c66c3..8b2777c817 100644
--- a/pkg/operator/staticpod/controllers.go
+++ b/pkg/operator/staticpod/controllers.go
@@ -283,6 +283,7 @@ func (b *staticPodOperatorControllerBuilder) ToControllers() (manager.Controller
b.revisionSecrets,
b.installCommand,
operandInformers,
+ clusterInformers,
b.staticPodOperatorClient,
configMapClient,
secretClient,
diff --git a/pkg/operator/v1helpers/test_helpers.go b/pkg/operator/v1helpers/test_helpers.go
index 66a7b6d89c..012da38edb 100644
--- a/pkg/operator/v1helpers/test_helpers.go
+++ b/pkg/operator/v1helpers/test_helpers.go
@@ -301,7 +301,11 @@ func (n *fakeNodeLister) List(selector labels.Selector) ([]*corev1.Node, error)
}
func (n *fakeNodeLister) Get(name string) (*corev1.Node, error) {
- panic("implement me")
+ node, err := n.client.CoreV1().Nodes().Get(context.TODO(), name, metav1.GetOptions{})
+ if err != nil {
+ return nil, err
+ }
+ return node, nil
}
// NewFakeOperatorClient returns a fake operator client suitable to use in static pod controller unit tests.
@@ -499,6 +503,7 @@ func mergeStaticPodOperatorStatusApplyConfiguration(currentOperatorStatus *v1.Op
for _, nodeStatus := range applyConfiguration.NodeStatuses {
newNodeStatus := operatorv1.NodeStatus{
NodeName: ptr.Deref(nodeStatus.NodeName, ""),
+ NodeUID: ptr.Deref(nodeStatus.NodeUID, ""),
CurrentRevision: ptr.Deref(nodeStatus.CurrentRevision, 0),
TargetRevision: ptr.Deref(nodeStatus.TargetRevision, 0),
LastFailedRevision: ptr.Deref(nodeStatus.LastFailedRevision, 0),
diff --git a/vendor/github.com/openshift/api/Makefile b/vendor/github.com/openshift/api/Makefile
index b6c0de378c..e0e97ed1ac 100644
--- a/vendor/github.com/openshift/api/Makefile
+++ b/vendor/github.com/openshift/api/Makefile
@@ -4,7 +4,7 @@ all: build
update: update-non-codegen update-codegen
RUNTIME ?= podman
-RUNTIME_IMAGE_NAME ?= registry.ci.openshift.org/openshift/release:rhel-9-release-golang-1.26-openshift-5.0
+RUNTIME_IMAGE_NAME ?= registry.ci.openshift.org/openshift/release:rhel-9-release-golang-1.25-openshift-4.22
EXCLUDE_DIRS := _output/ dependencymagnet/ hack/ third_party/ tls/ tools/ vendor/ tests/
GO_PACKAGES :=$(addsuffix ...,$(addprefix ./,$(filter-out $(EXCLUDE_DIRS), $(wildcard */))))
diff --git a/vendor/github.com/openshift/api/config/v1/types_infrastructure.go b/vendor/github.com/openshift/api/config/v1/types_infrastructure.go
index 5d9f10374e..e8aaa810f5 100644
--- a/vendor/github.com/openshift/api/config/v1/types_infrastructure.go
+++ b/vendor/github.com/openshift/api/config/v1/types_infrastructure.go
@@ -660,6 +660,7 @@ type AzurePlatformStatus struct {
//
// +default={"dnsType": "PlatformDefault"}
// +kubebuilder:default={"dnsType": "PlatformDefault"}
+ // +openshift:enable:FeatureGate=AzureClusterHostedDNSInstall
// +optional
CloudLoadBalancerConfig *CloudLoadBalancerConfig `json:"cloudLoadBalancerConfig,omitempty"`
diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml b/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml
index 76f78df82d..f8a272d496 100644
--- a/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml
+++ b/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml
@@ -396,6 +396,7 @@ infrastructures.config.openshift.io:
FeatureGates:
- AWSClusterHostedDNSInstall
- AWSDualStackInstall
+ - AzureClusterHostedDNSInstall
- AzureDualStackInstall
- DualReplica
- DyanmicServiceEndpointIBMCloud
diff --git a/vendor/github.com/openshift/api/features.md b/vendor/github.com/openshift/api/features.md
index c78d402696..77737db5fc 100644
--- a/vendor/github.com/openshift/api/features.md
+++ b/vendor/github.com/openshift/api/features.md
@@ -97,6 +97,7 @@
| OSStreams| | Enabled | Enabled | Enabled | | Enabled | Enabled | Enabled |
| AWSClusterHostedDNSInstall| Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled |
| AWSServiceLBNetworkSecurityGroup| Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled |
+| AzureClusterHostedDNSInstall| Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled |
| AzureWorkloadIdentity| Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled |
| BootImageSkewEnforcement| Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled |
| BuildCSIVolumes| Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled | Enabled |
diff --git a/vendor/github.com/openshift/api/features/features.go b/vendor/github.com/openshift/api/features/features.go
index b45bef7705..0ed0d37c7b 100644
--- a/vendor/github.com/openshift/api/features/features.go
+++ b/vendor/github.com/openshift/api/features/features.go
@@ -272,6 +272,14 @@ var (
enable(inTechPreviewNoUpgrade(), inDevPreviewNoUpgrade()).
mustRegister()
+ FeatureGateAzureClusterHostedDNSInstall = newFeatureGate("AzureClusterHostedDNSInstall").
+ reportProblemsToJiraComponent("Installer").
+ contactPerson("sadasu").
+ productScope(ocpSpecific).
+ enhancementPR("https://github.com/openshift/enhancements/pull/1468").
+ enable(inDefault(), inOKD(), inTechPreviewNoUpgrade(), inDevPreviewNoUpgrade()).
+ mustRegister()
+
FeatureGateMixedCPUsAllocation = newFeatureGate("MixedCPUsAllocation").
reportProblemsToJiraComponent("NodeTuningOperator").
contactPerson("titzhak").
diff --git a/vendor/github.com/openshift/api/operator/v1/types.go b/vendor/github.com/openshift/api/operator/v1/types.go
index 3a2141abb9..599ceb03e1 100644
--- a/vendor/github.com/openshift/api/operator/v1/types.go
+++ b/vendor/github.com/openshift/api/operator/v1/types.go
@@ -266,6 +266,18 @@ type NodeStatus struct {
// +required
NodeName string `json:"nodeName"`
+ // nodeUID is the UID of the node.
+ // This field is used to detect that a node has been deleted and recreated
+ // with the same name. When the UID changes, it indicates the node is a
+ // new instance and the controller should treat this status entry as stale.
+ // When omitted, UID-based node replacement detection is not available
+ // for this entry.
+ // +kubebuilder:validation:MinLength=36
+ // +kubebuilder:validation:MaxLength=36
+ // +kubebuilder:validation:Format=uuid
+ // +optional
+ NodeUID string `json:"nodeUID,omitempty"`
+
// currentRevision is the generation of the most recently successful deployment.
// Can not be set on creation of a nodeStatus. Updates must only increase the value.
// +kubebuilder:validation:XValidation:rule="self >= oldSelf",message="must only increase"
diff --git a/vendor/github.com/openshift/api/operator/v1/zz_generated.swagger_doc_generated.go b/vendor/github.com/openshift/api/operator/v1/zz_generated.swagger_doc_generated.go
index 114b5c7a68..2825e758b5 100644
--- a/vendor/github.com/openshift/api/operator/v1/zz_generated.swagger_doc_generated.go
+++ b/vendor/github.com/openshift/api/operator/v1/zz_generated.swagger_doc_generated.go
@@ -37,6 +37,7 @@ func (MyOperatorResource) SwaggerDoc() map[string]string {
var map_NodeStatus = map[string]string{
"": "NodeStatus provides information about the current state of a particular node managed by this operator.",
"nodeName": "nodeName is the name of the node",
+ "nodeUID": "nodeUID is the UID of the node. This field is used to detect that a node has been deleted and recreated with the same name. When the UID changes, it indicates the node is a new instance and the controller should treat this status entry as stale. When omitted, UID-based node replacement detection is not available for this entry.",
"currentRevision": "currentRevision is the generation of the most recently successful deployment. Can not be set on creation of a nodeStatus. Updates must only increase the value.",
"targetRevision": "targetRevision is the generation of the deployment we're trying to apply. Can not be set on creation of a nodeStatus.",
"lastFailedRevision": "lastFailedRevision is the generation of the deployment we tried and failed to deploy.",
diff --git a/vendor/github.com/openshift/api/operator/v1alpha1/types_clusterapi.go b/vendor/github.com/openshift/api/operator/v1alpha1/types_clusterapi.go
index 291cedc145..719893f8a5 100644
--- a/vendor/github.com/openshift/api/operator/v1alpha1/types_clusterapi.go
+++ b/vendor/github.com/openshift/api/operator/v1alpha1/types_clusterapi.go
@@ -244,24 +244,13 @@ type ClusterAPIInstallerComponentSource struct {
Image ClusterAPIInstallerComponentImage `json:"image,omitzero"`
}
-// The image name validation regex is derived from the OCI distribution reference:
-// https://github.com/distribution/reference/blob/main/regexp.go
-// It matches: domainAndPort "/" remoteName
-//
-// domainAndPort = (domainName | "[" ipv6 "]") [ ":" port ]
-// domainName = label ("." label)*
-// label = alphanumeric, may contain hyphens but must start/end with [a-zA-Z0-9]
-// remoteName = pathComponent ("/" pathComponent)*
-// pathComponent = [a-z0-9]+ separated by ".", "_", "__", or "-"+
-
-// ImageDigestFormat is a type that conforms to the format host[:port][/namespace[/namespace...]]/name@sha256:.
-// The host may be a dotted domain name (including IPv4 addresses like 192.168.1.1), or an IPv6 address in brackets (e.g. [fd00::1]).
+// ImageDigestFormat is a type that conforms to the format host[:port][/namespace]/name@sha256:.
// The digest must be 64 characters long, and consist only of lowercase hexadecimal characters, a-f and 0-9.
// The length of the field must be between 1 to 447 characters.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=447
// +kubebuilder:validation:XValidation:rule=`(self.split('@').size() == 2 && self.split('@')[1].matches('^sha256:[a-f0-9]{64}$'))`,message="the OCI Image reference must end with a valid '@sha256:' suffix, where '' is 64 characters long"
-// +kubebuilder:validation:XValidation:rule=`(self.split('@')[0].matches('^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*$'))`,message="the OCI Image name should follow the host[:port][/namespace[/namespace...]]/name format, where host is a domain name, IPv4, or IPv6 address in brackets"
+// +kubebuilder:validation:XValidation:rule=`(self.split('@')[0].matches('^([a-zA-Z0-9-]+\\.)+[a-zA-Z0-9-]+(:[0-9]{2,5})?/([a-zA-Z0-9-_]{0,61}/)?[a-zA-Z0-9-_.]*?$'))`,message="the OCI Image name should follow the host[:port][/namespace]/name format, resembling a valid URL without the scheme"
type ImageDigestFormat string
// ClusterAPIInstallerComponentImage defines an image source for a component.
diff --git a/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go b/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go
index 7e41cc30bf..9c2b799bf5 100644
--- a/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go
+++ b/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go
@@ -3181,6 +3181,9 @@ var schemaYAML = typed.YAMLObject(`types:
type:
scalar: string
default: ""
+ - name: nodeUID
+ type:
+ scalar: string
- name: targetRevision
type:
scalar: numeric
diff --git a/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/nodestatus.go b/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/nodestatus.go
index f107c370d2..ab2db04ade 100644
--- a/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/nodestatus.go
+++ b/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/nodestatus.go
@@ -13,6 +13,13 @@ import (
type NodeStatusApplyConfiguration struct {
// nodeName is the name of the node
NodeName *string `json:"nodeName,omitempty"`
+ // nodeUID is the UID of the node.
+ // This field is used to detect that a node has been deleted and recreated
+ // with the same name. When the UID changes, it indicates the node is a
+ // new instance and the controller should treat this status entry as stale.
+ // When omitted, UID-based node replacement detection is not available
+ // for this entry.
+ NodeUID *string `json:"nodeUID,omitempty"`
// currentRevision is the generation of the most recently successful deployment.
// Can not be set on creation of a nodeStatus. Updates must only increase the value.
CurrentRevision *int32 `json:"currentRevision,omitempty"`
@@ -47,6 +54,14 @@ func (b *NodeStatusApplyConfiguration) WithNodeName(value string) *NodeStatusApp
return b
}
+// WithNodeUID sets the NodeUID 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 NodeUID field is set to the value of the last call.
+func (b *NodeStatusApplyConfiguration) WithNodeUID(value string) *NodeStatusApplyConfiguration {
+ b.NodeUID = &value
+ return b
+}
+
// WithCurrentRevision sets the CurrentRevision 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 CurrentRevision field is set to the value of the last call.
diff --git a/vendor/modules.txt b/vendor/modules.txt
index cb5b965766..3431c99aaf 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -314,8 +314,8 @@ github.com/opencontainers/image-spec/specs-go/v1
## explicit; go 1.19
github.com/opencontainers/selinux/go-selinux
github.com/opencontainers/selinux/pkg/pwalkdir
-# github.com/openshift/api v0.0.0-20260715165912-72066cc9718b
-## explicit; go 1.26.0
+# github.com/openshift/api v0.0.0-20260715165912-72066cc9718b => github.com/bhperry/openshift-api v0.0.0-20260713144529-6c01886662a4
+## explicit; go 1.25.0
github.com/openshift/api
github.com/openshift/api/annotations
github.com/openshift/api/apiextensions
@@ -404,7 +404,7 @@ github.com/openshift/build-machinery-go/make/targets/golang
github.com/openshift/build-machinery-go/make/targets/openshift
github.com/openshift/build-machinery-go/make/targets/openshift/operator
github.com/openshift/build-machinery-go/scripts
-# github.com/openshift/client-go v0.0.0-20260715172546-dac61734e0ec
+# github.com/openshift/client-go v0.0.0-20260715172546-dac61734e0ec => github.com/bhperry/openshift-client-go v0.0.0-20260716151504-c6335807bf79
## explicit; go 1.26.0
github.com/openshift/client-go/apps/applyconfigurations/apps/v1
github.com/openshift/client-go/apps/applyconfigurations/internal
@@ -1731,3 +1731,5 @@ sigs.k8s.io/structured-merge-diff/v6/value
# sigs.k8s.io/yaml v1.6.0
## explicit; go 1.22
sigs.k8s.io/yaml
+# github.com/openshift/api => github.com/bhperry/openshift-api v0.0.0-20260713144529-6c01886662a4
+# github.com/openshift/client-go => github.com/bhperry/openshift-client-go v0.0.0-20260716151504-c6335807bf79