Skip to content

Commit fe8cd06

Browse files
committed
Use DeploymentAvailable instead of custom test for CSV status.
CSVs can transition to Failed (reason ComponentUnhealthy) if a single pod managed by one of its deployments is deleted. This commit changes the deployment availability test from a comparison between .status.availableReplicas and .status.updatedReplicas to a check that passes unless the deployment's Available condition exists and is False. Signed-off-by: Ben Luddy <[email protected]>
1 parent 3ccd4d3 commit fe8cd06

File tree

7 files changed

+227
-35
lines changed

7 files changed

+227
-35
lines changed

deploy/chart/templates/_packageserver.deployment-spec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
spec:
33
strategy:
44
type: RollingUpdate
5+
rollingUpdate:
6+
maxUnavailable: {{ .Values.package.maxUnavailable }}
7+
maxSurge: {{ .Values.package.maxSurge }}
58
replicas: {{ .Values.package.replicaCount }}
69
selector:
710
matchLabels:

deploy/chart/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ catalog:
3838

3939
package:
4040
replicaCount: 2
41+
maxUnavailable: 1
42+
maxSurge: 1
4143
image:
4244
ref: quay.io/operator-framework/olm:master
4345
pullPolicy: Always

deploy/ocp/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ catalog:
6363
memory: 80Mi
6464
package:
6565
replicaCount: 2
66+
maxUnavailable: 1
67+
maxSurge: 1
6668
image:
6769
ref: quay.io/operator-framework/olm@sha256:e9de77ac5c08643202ad42a0311d15c98ffbfd8a1dc2eab4e9f2dcaeed7110ac
6870
pullPolicy: IfNotPresent

deploy/upstream/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ catalog:
2222
internalPort: 8080
2323
package:
2424
replicaCount: 2
25+
maxUnavailable: 1
26+
maxSurge: 1
2527
image:
2628
ref: quay.io/operator-framework/olm@sha256:e9de77ac5c08643202ad42a0311d15c98ffbfd8a1dc2eab4e9f2dcaeed7110ac
2729
pullPolicy: Always

pkg/controller/install/status_viewer.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77

88
appsv1 "k8s.io/api/apps/v1"
9+
corev1 "k8s.io/api/core/v1"
910
)
1011

1112
const TimedOutReason = "ProgressDeadlineExceeded"
@@ -26,9 +27,12 @@ func DeploymentStatus(deployment *appsv1.Deployment) (string, bool, error) {
2627
if deployment.Status.Replicas > deployment.Status.UpdatedReplicas {
2728
return fmt.Sprintf("Waiting for rollout to finish: %d old replicas are pending termination...\n", deployment.Status.Replicas-deployment.Status.UpdatedReplicas), false, nil
2829
}
29-
// waiting for new replicas to report as available
30-
if deployment.Status.AvailableReplicas < deployment.Status.UpdatedReplicas {
31-
return fmt.Sprintf("Waiting for rollout to finish: %d of %d updated replicas are available...\n", deployment.Status.AvailableReplicas, deployment.Status.UpdatedReplicas), false, nil
30+
if c := getDeploymentCondition(deployment.Status, appsv1.DeploymentAvailable); c == nil || c.Status != corev1.ConditionTrue {
31+
msg := fmt.Sprintf("deployment %q missing condition %q", deployment.Name, appsv1.DeploymentAvailable)
32+
if c != nil {
33+
msg = fmt.Sprintf("deployment %q not available: %s", deployment.Name, c.Message)
34+
}
35+
return fmt.Sprintf("Waiting for rollout to finish: %s\n", msg), false, nil
3236
}
3337
// deployment is finished
3438
return fmt.Sprintf("deployment %q successfully rolled out\n", deployment.Name), true, nil

pkg/controller/install/status_viewer_test.go

Lines changed: 87 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package install
22

33
import (
4+
"fmt"
45
"testing"
56

67
apps "k8s.io/api/apps/v1"
8+
core "k8s.io/api/core/v1"
79
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
810
)
911

@@ -15,6 +17,26 @@ func TestDeploymentStatusViewerStatus(t *testing.T) {
1517
msg string
1618
done bool
1719
}{
20+
{
21+
generation: 0,
22+
specReplicas: 1,
23+
status: apps.DeploymentStatus{
24+
ObservedGeneration: 1,
25+
Replicas: 1,
26+
UpdatedReplicas: 0,
27+
AvailableReplicas: 1,
28+
UnavailableReplicas: 0,
29+
Conditions: []apps.DeploymentCondition{
30+
{
31+
Type: apps.DeploymentProgressing,
32+
Reason: "NotTimedOut",
33+
},
34+
},
35+
},
36+
37+
msg: "Waiting for rollout to finish: 0 out of 1 new replicas have been updated...\n",
38+
done: false,
39+
},
1840
{
1941
generation: 0,
2042
specReplicas: 1,
@@ -52,9 +74,16 @@ func TestDeploymentStatusViewerStatus(t *testing.T) {
5274
UpdatedReplicas: 2,
5375
AvailableReplicas: 1,
5476
UnavailableReplicas: 1,
77+
Conditions: []apps.DeploymentCondition{
78+
{
79+
Type: apps.DeploymentAvailable,
80+
Status: core.ConditionFalse,
81+
Message: "Deployment does not have minimum availability.",
82+
},
83+
},
5584
},
5685

57-
msg: "Waiting for rollout to finish: 1 of 2 updated replicas are available...\n",
86+
msg: "Waiting for rollout to finish: deployment \"foo\" not available: Deployment does not have minimum availability.\n",
5887
done: false,
5988
},
6089
{
@@ -64,13 +93,38 @@ func TestDeploymentStatusViewerStatus(t *testing.T) {
6493
ObservedGeneration: 1,
6594
Replicas: 2,
6695
UpdatedReplicas: 2,
67-
AvailableReplicas: 2,
68-
UnavailableReplicas: 0,
96+
AvailableReplicas: 1,
97+
UnavailableReplicas: 1,
98+
Conditions: []apps.DeploymentCondition{
99+
{
100+
Type: apps.DeploymentAvailable,
101+
Status: core.ConditionTrue,
102+
},
103+
},
69104
},
70105

71106
msg: "deployment \"foo\" successfully rolled out\n",
72107
done: true,
73108
},
109+
{
110+
generation: 1,
111+
specReplicas: 2,
112+
status: apps.DeploymentStatus{
113+
ObservedGeneration: 1,
114+
Replicas: 2,
115+
UpdatedReplicas: 2,
116+
AvailableReplicas: 1,
117+
UnavailableReplicas: 1,
118+
Conditions: []apps.DeploymentCondition{
119+
{
120+
Type: "Fooing",
121+
Status: core.ConditionTrue,
122+
},
123+
},
124+
},
125+
msg: "Waiting for rollout to finish: deployment \"foo\" missing condition \"Available\"\n",
126+
done: false,
127+
},
74128
{
75129
generation: 2,
76130
specReplicas: 2,
@@ -87,33 +141,35 @@ func TestDeploymentStatusViewerStatus(t *testing.T) {
87141
},
88142
}
89143

90-
for _, test := range tests {
91-
d := &apps.Deployment{
92-
ObjectMeta: metav1.ObjectMeta{
93-
Namespace: "bar",
94-
Name: "foo",
95-
UID: "8764ae47-9092-11e4-8393-42010af018ff",
96-
Generation: test.generation,
97-
},
98-
Spec: apps.DeploymentSpec{
99-
Replicas: &test.specReplicas,
100-
},
101-
Status: test.status,
102-
}
103-
msg, done, err := DeploymentStatus(d)
104-
if err != nil {
105-
t.Fatalf("DeploymentStatusViewer.Status(): %v", err)
106-
}
107-
if done != test.done || msg != test.msg {
108-
t.Errorf("DeploymentStatusViewer.Status() for deployment with generation %d, %d replicas specified, and status %+v returned %q, %t, want %q, %t",
109-
test.generation,
110-
test.specReplicas,
111-
test.status,
112-
msg,
113-
done,
114-
test.msg,
115-
test.done,
116-
)
117-
}
144+
for i, test := range tests {
145+
t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
146+
d := &apps.Deployment{
147+
ObjectMeta: metav1.ObjectMeta{
148+
Namespace: "bar",
149+
Name: "foo",
150+
UID: "8764ae47-9092-11e4-8393-42010af018ff",
151+
Generation: test.generation,
152+
},
153+
Spec: apps.DeploymentSpec{
154+
Replicas: &test.specReplicas,
155+
},
156+
Status: test.status,
157+
}
158+
msg, done, err := DeploymentStatus(d)
159+
if err != nil {
160+
t.Fatalf("DeploymentStatusViewer.Status(): %v", err)
161+
}
162+
if done != test.done || msg != test.msg {
163+
t.Errorf("DeploymentStatusViewer.Status() for deployment with generation %d, %d replicas specified, and status %+v returned %q, %t, want %q, %t",
164+
test.generation,
165+
test.specReplicas,
166+
test.status,
167+
msg,
168+
done,
169+
test.msg,
170+
test.done,
171+
)
172+
}
173+
})
118174
}
119175
}

test/e2e/csv_e2e_test.go

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
. "github.com/onsi/ginkgo"
1111
. "github.com/onsi/gomega"
12+
"github.com/onsi/gomega/types"
13+
1214
appsv1 "k8s.io/api/apps/v1"
1315
corev1 "k8s.io/api/core/v1"
1416
rbacv1 "k8s.io/api/rbac/v1"
@@ -27,6 +29,7 @@ import (
2729

2830
v1 "github.com/operator-framework/api/pkg/operators/v1"
2931
"github.com/operator-framework/api/pkg/operators/v1alpha1"
32+
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
3033
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
3134
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
3235
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
@@ -35,19 +38,139 @@ import (
3538
)
3639

3740
var _ = Describe("ClusterServiceVersion", func() {
41+
HavePhase := func(goal operatorsv1alpha1.ClusterServiceVersionPhase) types.GomegaMatcher {
42+
return WithTransform(func(csv *operatorsv1alpha1.ClusterServiceVersion) operatorsv1alpha1.ClusterServiceVersionPhase {
43+
return csv.Status.Phase
44+
}, Equal(goal))
45+
}
46+
3847
var (
3948
c operatorclient.ClientInterface
4049
crc versioned.Interface
4150
)
42-
BeforeEach(func() {
4351

52+
BeforeEach(func() {
4453
c = newKubeClient()
4554
crc = newCRClient()
4655
})
56+
4757
AfterEach(func() {
4858
TearDown(testNamespace)
4959
})
5060

61+
When("a csv exists specifying two replicas with one max unavailable", func() {
62+
var (
63+
csv v1alpha1.ClusterServiceVersion
64+
)
65+
66+
const (
67+
TestReadinessGate = "operatorframework.io/test-readiness-gate"
68+
)
69+
70+
BeforeEach(func() {
71+
csv = v1alpha1.ClusterServiceVersion{
72+
ObjectMeta: metav1.ObjectMeta{
73+
GenerateName: "test-csv",
74+
Namespace: testNamespace,
75+
},
76+
Spec: v1alpha1.ClusterServiceVersionSpec{
77+
InstallStrategy: operatorsv1alpha1.NamedInstallStrategy{
78+
StrategyName: operatorsv1alpha1.InstallStrategyNameDeployment,
79+
StrategySpec: operatorsv1alpha1.StrategyDetailsDeployment{
80+
DeploymentSpecs: []operatorsv1alpha1.StrategyDeploymentSpec{
81+
{
82+
Name: "deployment",
83+
Spec: appsv1.DeploymentSpec{
84+
Strategy: appsv1.DeploymentStrategy{
85+
Type: appsv1.RollingUpdateDeploymentStrategyType,
86+
RollingUpdate: &appsv1.RollingUpdateDeployment{
87+
MaxUnavailable: &[]intstr.IntOrString{intstr.FromInt(1)}[0],
88+
},
89+
},
90+
Selector: &metav1.LabelSelector{
91+
MatchLabels: map[string]string{"app": "foobar"},
92+
},
93+
Replicas: &[]int32{2}[0],
94+
Template: corev1.PodTemplateSpec{
95+
ObjectMeta: metav1.ObjectMeta{
96+
Labels: map[string]string{"app": "foobar"},
97+
},
98+
Spec: corev1.PodSpec{
99+
Containers: []corev1.Container{
100+
{
101+
Name: "foobar",
102+
Image: *dummyImage,
103+
},
104+
},
105+
ReadinessGates: []corev1.PodReadinessGate{
106+
{ConditionType: TestReadinessGate},
107+
},
108+
},
109+
},
110+
},
111+
},
112+
},
113+
},
114+
},
115+
InstallModes: []v1alpha1.InstallMode{{
116+
Type: v1alpha1.InstallModeTypeAllNamespaces,
117+
Supported: true,
118+
}},
119+
},
120+
}
121+
Expect(ctx.Ctx().Client().Create(context.Background(), &csv)).To(Succeed())
122+
123+
Eventually(func() (*v1alpha1.ClusterServiceVersion, error) {
124+
var ps corev1.PodList
125+
if err := ctx.Ctx().Client().List(context.Background(), &ps, client.MatchingLabels{"app": "foobar"}); err != nil {
126+
return nil, err
127+
}
128+
129+
if len(ps.Items) != 2 {
130+
return nil, fmt.Errorf("%d pods match deployment selector, want %d", len(ps.Items), 2)
131+
}
132+
133+
for _, pod := range ps.Items {
134+
index := -1
135+
for i, c := range pod.Status.Conditions {
136+
if c.Type == TestReadinessGate {
137+
index = i
138+
break
139+
}
140+
}
141+
if index == -1 {
142+
index = len(pod.Status.Conditions)
143+
pod.Status.Conditions = append(pod.Status.Conditions, corev1.PodCondition{Type: TestReadinessGate})
144+
}
145+
if pod.Status.Conditions[index].Status == corev1.ConditionTrue {
146+
continue
147+
}
148+
pod.Status.Conditions[index].Status = corev1.ConditionTrue
149+
if err := ctx.Ctx().Client().Status().Update(context.Background(), &pod); err != nil {
150+
return nil, err
151+
}
152+
}
153+
154+
if err := ctx.Ctx().Client().Get(context.Background(), client.ObjectKeyFromObject(&csv), &csv); err != nil {
155+
return nil, err
156+
}
157+
return &csv, nil
158+
}).Should(HavePhase(operatorsv1alpha1.CSVPhaseSucceeded))
159+
})
160+
161+
It("remains in phase Succeeded when only one pod is available", func() {
162+
var ps corev1.PodList
163+
Expect(ctx.Ctx().Client().List(context.Background(), &ps, client.MatchingLabels{"app": "foobar"})).To(Succeed())
164+
Expect(ps.Items).To(Not(BeEmpty()))
165+
166+
Expect(ctx.Ctx().Client().Delete(context.Background(), &ps.Items[0])).To(Succeed())
167+
168+
Consistently(func() (*operatorsv1alpha1.ClusterServiceVersion, error) {
169+
return &csv, ctx.Ctx().Client().Get(context.Background(), client.ObjectKeyFromObject(&csv), &csv)
170+
}).Should(HavePhase(operatorsv1alpha1.CSVPhaseSucceeded))
171+
})
172+
})
173+
51174
When("a copied csv exists", func() {
52175
var (
53176
target corev1.Namespace

0 commit comments

Comments
 (0)