Skip to content

Commit 1014561

Browse files
committed
Allow non-CSV-owned ServiceAccounts to satisfy CSV requirements.
The existing check, intended to prevent a race condition that could occur during CSV upgrades, marks a ServiceAccount as NotPresent when it has at least one owner but is not owned by the current CSV. It's expected for the ServiceAccount "default" not to be have an ownerreference to a CSV, so the check can be relaxed to only consider owners of kind ClusterServiceVersion. This also combines two ServiceAccount checks that were independently added to address the same race condition into a single check. Signed-off-by: Ben Luddy <[email protected]>
1 parent 3787874 commit 1014561

File tree

3 files changed

+197
-17
lines changed

3 files changed

+197
-17
lines changed

pkg/controller/operators/olm/requirements.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -271,18 +271,9 @@ func (a *Operator) permissionStatus(strategyDetailsDeployment *v1alpha1.Strategy
271271
}
272272
// Check SA's ownership
273273
if ownerutil.IsOwnedByKind(sa, v1alpha1.ClusterServiceVersionKind) && !ownerutil.IsOwnedBy(sa, csv) {
274-
met = false
275-
status.Status = v1alpha1.RequirementStatusReasonNotPresent
276-
status.Message = "Service account is stale"
277-
statusesSet[saName] = status
278-
continue
279-
}
280-
281-
// Check if the ServiceAccount is owned by CSV
282-
if len(sa.GetOwnerReferences()) != 0 && !ownerutil.IsOwnedBy(sa, csv) {
283274
met = false
284275
status.Status = v1alpha1.RequirementStatusReasonPresentNotSatisfied
285-
status.Message = "Service account is not owned by this ClusterServiceVersion"
276+
status.Message = "Service account is owned by another ClusterServiceVersion"
286277
statusesSet[saName] = status
287278
continue
288279
}

pkg/controller/operators/olm/requirements_test.go

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"k8s.io/apimachinery/pkg/types"
1515

1616
"github.com/operator-framework/api/pkg/operators/v1alpha1"
17+
"github.com/stretchr/testify/assert"
1718
)
1819

1920
func TestRequirementAndPermissionStatus(t *testing.T) {
@@ -297,7 +298,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) {
297298
Version: "v1",
298299
Kind: "ServiceAccount",
299300
Name: "sa",
300-
Status: v1alpha1.RequirementStatusReasonPresent,
301+
Status: v1alpha1.RequirementStatusReasonPresentNotSatisfied,
301302
Dependents: []v1alpha1.DependentStatus{
302303
{
303304
Group: "rbac.authorization.k8s.io",
@@ -641,7 +642,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) {
641642
Version: "v1",
642643
Kind: "CustomResourceDefinition",
643644
Name: "c1.g1",
644-
Status: v1alpha1.RequirementStatusReasonNotAvailable,
645+
Status: v1alpha1.RequirementStatusReasonNotPresent,
645646
},
646647
{"operators.coreos.com", "v1alpha1", "ClusterServiceVersion", "csv1"}: {
647648
Group: "operators.coreos.com",
@@ -814,7 +815,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) {
814815
Version: "v1",
815816
Kind: "ServiceAccount",
816817
Name: "sa",
817-
Status: v1alpha1.RequirementStatusReasonNotPresent,
818+
Status: v1alpha1.RequirementStatusReasonPresentNotSatisfied,
818819
Dependents: []v1alpha1.DependentStatus{},
819820
},
820821
{"operators.coreos.com", "v1alpha1", "ClusterServiceVersion", "csv1"}: {
@@ -827,6 +828,85 @@ func TestRequirementAndPermissionStatus(t *testing.T) {
827828
},
828829
expectedError: nil,
829830
},
831+
{
832+
description: "RequirementMet/ServiceAccountOwnedByNonCSV",
833+
csv: csvWithUID(csv("csv",
834+
namespace,
835+
"0.0.0",
836+
"",
837+
installStrategy(
838+
"csv-dep",
839+
[]v1alpha1.StrategyDeploymentPermissions{
840+
{
841+
ServiceAccountName: "sa",
842+
},
843+
},
844+
nil,
845+
),
846+
nil,
847+
nil,
848+
v1alpha1.CSVPhasePending,
849+
), types.UID("csv-uid")),
850+
existingObjs: []runtime.Object{
851+
&corev1.ServiceAccount{
852+
ObjectMeta: metav1.ObjectMeta{
853+
Name: "sa",
854+
Namespace: namespace,
855+
UID: types.UID("sa"),
856+
OwnerReferences: []metav1.OwnerReference{
857+
{
858+
Kind: v1alpha1.SubscriptionKind, // arbitrary non-CSV kind
859+
UID: "non-csv",
860+
},
861+
},
862+
},
863+
},
864+
&rbacv1.Role{
865+
ObjectMeta: metav1.ObjectMeta{
866+
Name: "role",
867+
Namespace: namespace,
868+
},
869+
},
870+
&rbacv1.RoleBinding{
871+
ObjectMeta: metav1.ObjectMeta{
872+
Name: "roleBinding",
873+
Namespace: namespace,
874+
},
875+
Subjects: []rbacv1.Subject{
876+
{
877+
Kind: "ServiceAccount",
878+
Name: "sa",
879+
Namespace: namespace,
880+
},
881+
},
882+
RoleRef: rbacv1.RoleRef{
883+
APIGroup: "rbac.authorization.k8s.io",
884+
Kind: "Role",
885+
Name: "role",
886+
},
887+
},
888+
},
889+
existingExtObjs: nil,
890+
met: true,
891+
expectedRequirementStatuses: map[gvkn]v1alpha1.RequirementStatus{
892+
{"", "v1", "ServiceAccount", "sa"}: {
893+
Group: "",
894+
Version: "v1",
895+
Kind: "ServiceAccount",
896+
Name: "sa",
897+
Status: v1alpha1.RequirementStatusReasonPresent,
898+
Dependents: []v1alpha1.DependentStatus{},
899+
},
900+
{"operators.coreos.com", "v1alpha1", "ClusterServiceVersion", "csv"}: {
901+
Group: "operators.coreos.com",
902+
Version: "v1alpha1",
903+
Kind: "ClusterServiceVersion",
904+
Name: "csv",
905+
Status: v1alpha1.RequirementStatusReasonPresent,
906+
},
907+
},
908+
expectedError: nil,
909+
},
830910
}
831911

832912
for _, test := range tests {
@@ -843,7 +923,8 @@ func TestRequirementAndPermissionStatus(t *testing.T) {
843923
require.Error(t, err)
844924
require.EqualError(t, test.expectedError, err.Error())
845925
}
846-
require.Equal(t, test.met, met)
926+
assert := assert.New(t)
927+
assert.Equal(test.met, met)
847928

848929
for _, status := range statuses {
849930
key := gvkn{
@@ -854,14 +935,15 @@ func TestRequirementAndPermissionStatus(t *testing.T) {
854935
}
855936

856937
expected, ok := test.expectedRequirementStatuses[key]
857-
require.True(t, ok, fmt.Sprintf("permission requirement status %+v found but not expected", key))
858-
require.Len(t, status.Dependents, len(expected.Dependents), "number of dependents is not what was expected")
938+
assert.True(ok, fmt.Sprintf("permission requirement status %+v found but not expected", key))
939+
assert.Equal(expected.Status, status.Status)
940+
assert.Len(status.Dependents, len(expected.Dependents), "number of dependents is not what was expected")
859941

860942
// Delete the requirement status to mark as found
861943
delete(test.expectedRequirementStatuses, key)
862944
}
863945

864-
require.Len(t, test.expectedRequirementStatuses, 0, "not all expected permission requirement statuses were found")
946+
assert.Len(test.expectedRequirementStatuses, 0, "not all expected permission requirement statuses were found")
865947
})
866948
}
867949
}

test/e2e/csv_e2e_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,113 @@ var _ = Describe("ClusterServiceVersion", func() {
127127
})
128128
})
129129

130+
When("a csv requires a serviceaccount solely owned by a non-csv", func() {
131+
var (
132+
cm corev1.ConfigMap
133+
sa corev1.ServiceAccount
134+
csv v1alpha1.ClusterServiceVersion
135+
)
136+
137+
BeforeEach(func() {
138+
cm = corev1.ConfigMap{
139+
ObjectMeta: metav1.ObjectMeta{
140+
GenerateName: "cm-",
141+
Namespace: testNamespace,
142+
},
143+
}
144+
Expect(ctx.Ctx().Client().Create(context.Background(), &cm)).To(Succeed())
145+
146+
sa = corev1.ServiceAccount{
147+
ObjectMeta: metav1.ObjectMeta{
148+
GenerateName: "sa-",
149+
Namespace: testNamespace,
150+
OwnerReferences: []metav1.OwnerReference{
151+
{
152+
Name: cm.GetName(),
153+
APIVersion: corev1.SchemeGroupVersion.String(),
154+
Kind: "ConfigMap",
155+
UID: cm.GetUID(),
156+
},
157+
},
158+
},
159+
}
160+
Expect(ctx.Ctx().Client().Create(context.Background(), &sa)).To(Succeed())
161+
162+
csv = v1alpha1.ClusterServiceVersion{
163+
TypeMeta: metav1.TypeMeta{
164+
Kind: v1alpha1.ClusterServiceVersionKind,
165+
APIVersion: v1alpha1.ClusterServiceVersionAPIVersion,
166+
},
167+
ObjectMeta: metav1.ObjectMeta{
168+
GenerateName: "csv-",
169+
Namespace: testNamespace,
170+
},
171+
Spec: v1alpha1.ClusterServiceVersionSpec{
172+
InstallStrategy: v1alpha1.NamedInstallStrategy{
173+
StrategyName: v1alpha1.InstallStrategyNameDeployment,
174+
StrategySpec: v1alpha1.StrategyDetailsDeployment{
175+
DeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{
176+
{
177+
Name: "foo",
178+
Spec: appsv1.DeploymentSpec{
179+
Selector: &metav1.LabelSelector{
180+
MatchLabels: map[string]string{"app": "foo"},
181+
},
182+
Template: corev1.PodTemplateSpec{
183+
ObjectMeta: metav1.ObjectMeta{
184+
Labels: map[string]string{"app": "foo"},
185+
},
186+
Spec: corev1.PodSpec{Containers: []corev1.Container{
187+
{
188+
Name: genName("foo"),
189+
Image: *dummyImage,
190+
},
191+
}},
192+
},
193+
},
194+
},
195+
},
196+
Permissions: []v1alpha1.StrategyDeploymentPermissions{
197+
{
198+
ServiceAccountName: sa.GetName(),
199+
Rules: []rbacv1.PolicyRule{},
200+
},
201+
},
202+
},
203+
},
204+
InstallModes: []v1alpha1.InstallMode{
205+
{
206+
Type: v1alpha1.InstallModeTypeAllNamespaces,
207+
Supported: true,
208+
},
209+
},
210+
},
211+
}
212+
Expect(ctx.Ctx().Client().Create(context.Background(), &csv)).To(Succeed())
213+
})
214+
215+
AfterEach(func() {
216+
if cm.GetName() != "" {
217+
Expect(ctx.Ctx().Client().Delete(context.Background(), &cm)).To(Succeed())
218+
}
219+
})
220+
221+
It("considers the serviceaccount requirement satisfied", func() {
222+
Eventually(func() (v1alpha1.StatusReason, error) {
223+
if err := ctx.Ctx().Client().Get(context.Background(), client.ObjectKeyFromObject(&csv), &csv); err != nil {
224+
return "", err
225+
}
226+
for _, requirement := range csv.Status.RequirementStatus {
227+
if requirement.Name != sa.GetName() {
228+
continue
229+
}
230+
return requirement.Status, nil
231+
}
232+
return "", fmt.Errorf("missing expected requirement %q", sa.GetName())
233+
}).Should(Equal(v1alpha1.RequirementStatusReasonPresent))
234+
})
235+
})
236+
130237
It("create with unmet requirements min kube version", func() {
131238

132239
depName := genName("dep-")

0 commit comments

Comments
 (0)