Skip to content
Closed
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
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/google/go-cmp v0.7.0
github.com/onsi/ginkgo/v2 v2.21.0
github.com/openshift-eng/openshift-tests-extension v0.0.0-20251205182537-ff5553e56f33
github.com/openshift/api v0.0.0-20260126183958-606bd613f9f7
github.com/openshift/api v0.0.0-20260212193555-c06ab675261f
github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13
github.com/openshift/library-go v0.0.0-20260210145149-d0e860e8d752
Expand Down Expand Up @@ -132,3 +132,6 @@ require (
)

replace github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20251001123353-fd5b1fb35db1

// workload-condition-overwrites
replace github.com/openshift/library-go => github.com/tchap/library-go v0.0.0-20260216103045-5a90edab46c3
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,12 @@ github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/openshift-eng/openshift-tests-extension v0.0.0-20251205182537-ff5553e56f33 h1:LJf6kWZQ36iako7WXRzdEa5XKrnyrAX8GBhlAcKRaZQ=
github.com/openshift-eng/openshift-tests-extension v0.0.0-20251205182537-ff5553e56f33/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M=
github.com/openshift/api v0.0.0-20260126183958-606bd613f9f7 h1:96rhgJpWlWzKEslMd6aYFMixV9vQVY32M71JcO4Gzn0=
github.com/openshift/api v0.0.0-20260126183958-606bd613f9f7/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
github.com/openshift/api v0.0.0-20260212193555-c06ab675261f h1:l1IgsK48Ym/nED30yfaCTF4MtswO1eOoyfXgh2rEmdw=
github.com/openshift/api v0.0.0-20260212193555-c06ab675261f/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
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-20260108185524-48f4ccfc4e13 h1:6rd4zSo2UaWQcAPZfHK9yzKVqH0BnMv1hqMzqXZyTds=
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13/go.mod h1:YvOmPmV7wcJxpfhTDuFqqs2Xpb3M3ovsM6Qs/i2ptq4=
github.com/openshift/library-go v0.0.0-20260210145149-d0e860e8d752 h1:KQj7j9VpMzv+gYerCgA9CbPehwGO3ARUg+B2Pt1YcWs=
github.com/openshift/library-go v0.0.0-20260210145149-d0e860e8d752/go.mod h1:DCRz1EgdayEmr9b6KXKDL+DWBN0rGHu/VYADeHzPoOk=
github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d h1:Rzx23P63JFNNz5D23ubhC0FCN5rK8CeJhKcq5QKcdyU=
github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d/go.mod h1:iVi9Bopa5cLhjG5ie9DoZVVqkH8BGb1FQVTtecOLn4I=
github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20251001123353-fd5b1fb35db1 h1:PMTgifBcBRLJJiM+LgSzPDTk9/Rx4qS09OUrfpY6GBQ=
Expand Down Expand Up @@ -205,6 +203,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tchap/library-go v0.0.0-20260216103045-5a90edab46c3 h1:ZAAnudXm3Q3NadreXfLC/rbfN9AUYMs5yaz25SSzkCQ=
github.com/tchap/library-go v0.0.0-20260216103045-5a90edab46c3/go.mod h1:K3FoNLgNBFYbFuG+Kr8usAnQxj1w84XogyUp2M8rK8k=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
Expand Down
108 changes: 108 additions & 0 deletions pkg/controllers/common/scaling/scaling.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package scaling

import (
"fmt"
"time"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/utils/clock"
"k8s.io/utils/ptr"

operatorv1 "github.com/openshift/api/operator/v1"
applyoperatorv1 "github.com/openshift/client-go/operator/applyconfigurations/operator/v1"
)

const (
replicasChangedAnnotation = "authentication.operator.openshift.io/replicas-changed"
deploymentProgressedAnnotation = "authentication.operator.openshift.io/deployment-progressed"
scalingBeginTimeout = 1 * time.Minute
)

// ProcessDeployment ensures the operator does not end up progressing on scaling.
// We define that scaling happens any time .spec.replicas is the only field that changes.
// The idea is then as follows:
//
// 1. When the replicas field is updated, store the change timestamp in a deployment annotation.
// 2. When the deployment eventually starts progressing, add another annotation so that we know it happened.
// 3. When the deployment hasn't progressing for too long, or it has finished progressing, remove all annotations.
//
// When the timestamp annotation is present, we should overwrite Progressing to be false.
//
// So, ProcessDeployment amends the expected deployment in place, also returning any conditions to set on the operator.
func ProcessDeployment(existing, expected *appsv1.Deployment, clock clock.Clock, conditionPrefix string) ([]*applyoperatorv1.OperatorConditionApplyConfiguration, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand what this is trying to solve, but the place is not correct. We cannot expect each deployment instance to have such a specific logic dedicated to progressing condition. This responsibility has to lie in the library-go or even better in the deployment controller.

The deployment controller should correctly report the progressing state via the Progressing condition. I think the best course of action should be to remove the Generation+ObservedGeneration check from library-go. As we can see in OCPBUGS-65896, not every spec update (e.g. scaling) should result in a progressing state, so generation tracking is not well equipped to do that.

Now there will be a small risk of reporting late the progressing state before the controller reacts to the real template change. This should be okay, unless the controller is down (this results in much bigger issues) or there is a long queue in the controller. Normally the reaction time should be similar to what we would have processing the annotation changes. I think it should be okay to live with this small delay.

Please also note openshift/cluster-image-registry-operator#1293.

Also, I think it might be useful to surface ProgressDeadlineExceeded reason in a degraded condition to report that the deployment controller timed out rolling out the new changes.

if !specsEqualIgnoringReplicas(existing, expected) {
return nil, nil
}

if expected.Annotations == nil {
expected.Annotations = make(map[string]string)
}

if !ptr.Equal(existing.Spec.Replicas, expected.Spec.Replicas) {
expected.Annotations[replicasChangedAnnotation] = clock.Now().UTC().Format(time.RFC3339)
return cancelProgressing(conditionPrefix), nil
}

var replicasChangedAt time.Time
if v, ok := existing.Annotations[replicasChangedAnnotation]; ok {
var err error
replicasChangedAt, err = time.Parse(time.RFC3339, v)
if err != nil {
return nil, fmt.Errorf("unable to parse annotation %q = %q: %w", replicasChangedAnnotation, v, err)
}
}
if replicasChangedAt.IsZero() {
return nil, nil
}

// Cancel scaling if we are done, or the whole process has reached the specified timeout.
startedProgressing := existing.Annotations[deploymentProgressedAnnotation] == "true"
if !isDeploymentProgressing(existing.Status) && (startedProgressing || clock.Since(replicasChangedAt) > scalingBeginTimeout) {
return nil, nil
}

expected.Annotations[replicasChangedAnnotation] = existing.Annotations[replicasChangedAnnotation]
if startedProgressing || isDeploymentProgressing(existing.Status) {
expected.Annotations[deploymentProgressedAnnotation] = "true"
}
return cancelProgressing(conditionPrefix), nil
}

// specsEqualIgnoringReplicas returns true when the deployment specs are the same or diff only in the replicas field.
// The function returns false automatically when one of the deployments is nil.
func specsEqualIgnoringReplicas(existing, expected *appsv1.Deployment) bool {
if existing == nil || expected == nil {
return false
}

s1 := &existing.Spec
s2 := &expected.Spec

if !ptr.Equal(s1.Replicas, s2.Replicas) {
s2 = s2.DeepCopy()
s2.Replicas = s1.Replicas
}
return equality.Semantic.DeepEqual(s1, s2)
}

// isDeploymentProgressing returns whether the given deployment is progressing.
func isDeploymentProgressing(status appsv1.DeploymentStatus) bool {
for _, cond := range status.Conditions {
if cond.Type == appsv1.DeploymentProgressing {
return !(cond.Status == corev1.ConditionTrue && cond.Reason == "NewReplicaSetAvailable")
}
}
return false
}

func cancelProgressing(conditionPrefix string) []*applyoperatorv1.OperatorConditionApplyConfiguration {
return []*applyoperatorv1.OperatorConditionApplyConfiguration{
applyoperatorv1.OperatorCondition().
WithType(fmt.Sprintf("%sDeploymentProgressing", conditionPrefix)).
WithStatus(operatorv1.ConditionFalse).
WithReason("AsExpected").
WithMessage("Scaling replicas only"),
}
}
Loading