Skip to content

Commit 28f20de

Browse files
camilamacedo86perdasilva
authored andcommitted
✨ (GoodPracticesValidator) : new check to warning that authors should not create an operator to manage another operator by looking for RBAC permissions to create CRDs (openshift#241)
Upstream-repository: api Upstream-commit: 05acd7a906c4819f1ad08203524e14b8a8ea4094
1 parent a574769 commit 28f20de

File tree

3 files changed

+225
-11
lines changed

3 files changed

+225
-11
lines changed

staging/api/pkg/validation/internal/good_practices.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/operator-framework/api/pkg/manifests"
9+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
910
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
1011
"github.com/operator-framework/api/pkg/validation/errors"
1112
interfaces "github.com/operator-framework/api/pkg/validation/interfaces"
@@ -22,6 +23,13 @@ import (
2223
// - The channel names seems are not following the convention https://olm.operatorframework.io/docs/best-practices/channel-naming/
2324
//
2425
// - CRDs defined in the bundle have empty descriptions
26+
//
27+
// - Check if the CSV has permissions to create CRDs. Note that:
28+
// a) "Operators should own a CRD and only one Operator should control a CRD on a cluster. Two Operators managing the same CRD is not a recommended best practice. In the case where an API exists but with multiple implementations, this is typically an example of a no-op Operator because it doesn't have any deployment or reconciliation loop to define the shared API and other Operators depend on this Operator to provide one implementation of the API, e.g. similar to PVCs or Ingress."
29+
//
30+
// b) "An Operator shouldn't deploy or manage other operators (such patterns are known as meta or super operators or include CRDs in its Operands). It's the Operator Lifecycle Manager's job to manage the deployment and lifecycle of operators. For further information check Dependency Resolution: https://olm.operatorframework.io/docs/concepts/olm-architecture/dependency-resolution/"
31+
//
32+
// WARNING: if you create CRD's via the reconciliations or via the Operands then, OLM cannot handle CRDs migration and update, validation.
2533
var GoodPracticesValidator interfaces.Validator = interfaces.ValidatorFunc(goodPracticesValidator)
2634

2735
func goodPracticesValidator(objs ...interface{}) (results []errors.ManifestResult) {
@@ -51,7 +59,7 @@ func validateGoodPracticesFrom(bundle *manifests.Bundle) errors.ManifestResult {
5159
errs, warns := validateResourceRequests(bundle.CSV)
5260
warns = append(warns, validateCrdDescriptions(bundle.CSV.Spec.CustomResourceDefinitions)...)
5361
warns = append(warns, validateHubChannels(bundle))
54-
62+
warns = append(warns, validateRBACForCRDsWith(bundle.CSV))
5563
for _, err := range errs {
5664
if err != nil {
5765
result.Add(errors.ErrFailedValidation(err.Error(), bundle.CSV.GetName()))
@@ -118,6 +126,65 @@ func validateHubChannels(bundle *manifests.Bundle) error {
118126
return nil
119127
}
120128

129+
// validateRBACForCRDsWith to warning when/if permissions to create CRD are found in the rules
130+
func validateRBACForCRDsWith(csv *operatorsv1alpha1.ClusterServiceVersion) error {
131+
apiGroupResourceMap := map[string][]string{
132+
"apiextensions.k8s.io": {"customresourcedefinitions", "*", "[*]"},
133+
}
134+
verbs := []string{"create", "*", "[*]", "patch"}
135+
warning := goerrors.New("CSV contains permissions to create CRD. An Operator shouldn't deploy or manage " +
136+
"other operators (such patterns are known as meta or super operators or include CRDs in its Operands)." +
137+
" It's the Operator Lifecycle Manager's job to manage the deployment and lifecycle of operators. " +
138+
" Please, review the design of your solution and if you should not be using Dependency Resolution from OLM instead." +
139+
" More info: https://sdk.operatorframework.io/docs/best-practices/common-recommendation/")
140+
141+
for _, perm := range csv.Spec.InstallStrategy.StrategySpec.Permissions {
142+
if hasRBACFor(perm, apiGroupResourceMap, verbs) {
143+
return warning
144+
}
145+
}
146+
147+
for _, perm := range csv.Spec.InstallStrategy.StrategySpec.ClusterPermissions {
148+
if hasRBACFor(perm, apiGroupResourceMap, verbs) {
149+
return warning
150+
}
151+
}
152+
153+
return nil
154+
}
155+
156+
func hasRBACFor(perm v1alpha1.StrategyDeploymentPermissions, apiGroupResourceMap map[string][]string, verbs []string) bool {
157+
// For each APIGroup and list of resources that we are looking for
158+
for apiFromMap, resourcesFromMap := range apiGroupResourceMap {
159+
for _, rule := range perm.Rules {
160+
for _, api := range rule.APIGroups {
161+
// If we found the APIGroup
162+
if api == apiFromMap {
163+
for _, res := range rule.Resources {
164+
for _, resFromMap := range resourcesFromMap {
165+
// If we found the resource
166+
if resFromMap == res {
167+
// Check if we find the verbs:
168+
for _, verbFromList := range verbs {
169+
for _, ruleVerb := range rule.Verbs {
170+
// If we found the verb
171+
if verbFromList == ruleVerb {
172+
// stopping by returning true
173+
return true
174+
}
175+
}
176+
}
177+
}
178+
}
179+
}
180+
}
181+
}
182+
}
183+
}
184+
185+
return false
186+
}
187+
121188
// getUniqueValues return the values without duplicates
122189
func getUniqueValues(array []string) []string {
123190
var result []string

staging/api/pkg/validation/internal/good_practices_test.go

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package internal
33
import (
44
"testing"
55

6+
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
7+
68
"github.com/operator-framework/api/pkg/manifests"
79
"github.com/stretchr/testify/require"
810
)
@@ -100,15 +102,6 @@ func Test_ValidateGoodPractices(t *testing.T) {
100102
require.Contains(t, tt.warnStrings, wString)
101103
}
102104
}
103-
104-
require.Equal(t, tt.wantError, len(results.Errors) > 0)
105-
if tt.wantError {
106-
require.Equal(t, len(tt.errStrings), len(results.Errors))
107-
for _, err := range results.Errors {
108-
errString := err.Error()
109-
require.Contains(t, tt.errStrings, errString)
110-
}
111-
}
112105
})
113106
}
114107
}
@@ -162,3 +155,90 @@ func TestValidateHubChannels(t *testing.T) {
162155
})
163156
}
164157
}
158+
159+
func TestValidateRBACForCRDsWith(t *testing.T) {
160+
161+
bundle, err := manifests.GetBundleFromDir("./testdata/valid_bundle")
162+
require.NoError(t, err)
163+
164+
bundleWithPermissions, _ := manifests.GetBundleFromDir("./testdata/valid_bundle")
165+
bundleWithPermissions.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].APIGroups = []string{"apiextensions.k8s.io"}
166+
bundleWithPermissions.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].Resources = []string{"*"}
167+
bundleWithPermissions.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].Verbs = []string{"*"}
168+
169+
bundleWithPermissionsResource, _ := manifests.GetBundleFromDir("./testdata/valid_bundle")
170+
bundleWithPermissionsResource.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].APIGroups = []string{"apiextensions.k8s.io"}
171+
bundleWithPermissionsResource.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].Resources = []string{"customresourcedefinitions"}
172+
bundleWithPermissionsResource.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].Verbs = []string{"*"}
173+
174+
bundleWithPermissionsResourceCreate, _ := manifests.GetBundleFromDir("./testdata/valid_bundle")
175+
bundleWithPermissionsResourceCreate.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].APIGroups = []string{"apiextensions.k8s.io"}
176+
bundleWithPermissionsResourceCreate.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].Resources = []string{"customresourcedefinitions"}
177+
bundleWithPermissionsResourceCreate.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].Verbs = []string{"create"}
178+
179+
bundleWithPermissionsResourcePatch, _ := manifests.GetBundleFromDir("./testdata/valid_bundle")
180+
bundleWithPermissionsResourcePatch.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].APIGroups = []string{"apiextensions.k8s.io"}
181+
bundleWithPermissionsResourcePatch.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].Resources = []string{"customresourcedefinitions"}
182+
bundleWithPermissionsResourcePatch.CSV.Spec.InstallStrategy.StrategySpec.Permissions[0].Rules[0].Verbs = []string{"patch"}
183+
184+
type args struct {
185+
bundleCSV *operatorsv1alpha1.ClusterServiceVersion
186+
}
187+
tests := []struct {
188+
name string
189+
args args
190+
wantWarn bool
191+
warnStrings []string
192+
}{
193+
{
194+
name: "should not return warning when has no permissions",
195+
args: args{
196+
bundleCSV: bundle.CSV,
197+
},
198+
wantWarn: false,
199+
},
200+
{
201+
name: "should return warning when has permissions for all verbs and resources kind of the apiGroup",
202+
args: args{
203+
bundleCSV: bundleWithPermissions.CSV,
204+
},
205+
wantWarn: true,
206+
warnStrings: []string{"CSV contains permissions to create CRD. An Operator shouldn't deploy or manage other operators (such patterns are known as meta or super operators or include CRDs in its Operands). It's the Operator Lifecycle Manager's job to manage the deployment and lifecycle of operators. Please, review the design of your solution and if you should not be using Dependency Resolution from OLM instead. More info: https://sdk.operatorframework.io/docs/best-practices/common-recommendation/"},
207+
},
208+
{
209+
name: "should return warning when has permissions for all verbs with the resource specified",
210+
args: args{
211+
bundleCSV: bundleWithPermissionsResource.CSV,
212+
},
213+
wantWarn: true,
214+
warnStrings: []string{"CSV contains permissions to create CRD. An Operator shouldn't deploy or manage other operators (such patterns are known as meta or super operators or include CRDs in its Operands). It's the Operator Lifecycle Manager's job to manage the deployment and lifecycle of operators. Please, review the design of your solution and if you should not be using Dependency Resolution from OLM instead. More info: https://sdk.operatorframework.io/docs/best-practices/common-recommendation/"},
215+
},
216+
{
217+
name: "should return warning when has permissions to create a CRD",
218+
args: args{
219+
bundleCSV: bundleWithPermissionsResourceCreate.CSV,
220+
},
221+
wantWarn: true,
222+
warnStrings: []string{"CSV contains permissions to create CRD. An Operator shouldn't deploy or manage other operators (such patterns are known as meta or super operators or include CRDs in its Operands). It's the Operator Lifecycle Manager's job to manage the deployment and lifecycle of operators. Please, review the design of your solution and if you should not be using Dependency Resolution from OLM instead. More info: https://sdk.operatorframework.io/docs/best-practices/common-recommendation/"},
223+
},
224+
{
225+
name: "should return warning when has permissions to create a Patch a CRD",
226+
args: args{
227+
bundleCSV: bundleWithPermissionsResourcePatch.CSV,
228+
},
229+
wantWarn: true,
230+
warnStrings: []string{"CSV contains permissions to create CRD. An Operator shouldn't deploy or manage other operators (such patterns are known as meta or super operators or include CRDs in its Operands). It's the Operator Lifecycle Manager's job to manage the deployment and lifecycle of operators. Please, review the design of your solution and if you should not be using Dependency Resolution from OLM instead. More info: https://sdk.operatorframework.io/docs/best-practices/common-recommendation/"},
231+
},
232+
}
233+
for _, tt := range tests {
234+
t.Run(tt.name, func(t *testing.T) {
235+
err = validateRBACForCRDsWith(tt.args.bundleCSV)
236+
if (err != nil) != tt.wantWarn {
237+
t.Errorf("validateHubChannels() error = %v, wantWarn %v", err, tt.wantWarn)
238+
}
239+
if err != nil && len(tt.warnStrings) > 0 {
240+
require.Contains(t, tt.warnStrings, err.Error())
241+
}
242+
})
243+
}
244+
}

vendor/github.com/operator-framework/api/pkg/validation/internal/good_practices.go

Lines changed: 68 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)