Skip to content

Commit 22eb931

Browse files
committed
Separate Category Validation
This commit removes the default set of category validation as a part of the operatorhubio validator. As we have a mechanism for custom validation, and there is significantly more churn on that specific validation, this commit separates the default operatorhubio validator from a distinct default categories validator. This allows users that want to continue to use the default set of categories to still do so, and if there are custom categories they would like to include they are free to use the dynamic categories validation option instead. This commit accomplishes that by deprecating the existing validator and creating a v2 version of the operatorhubio validator. Additionally, this commit adds the 'Observability' category to the list of default categories used by the new categories validator.
1 parent 071829b commit 22eb931

File tree

8 files changed

+263
-7
lines changed

8 files changed

+263
-7
lines changed

pkg/validation/errors/error.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ const (
102102
ErrorInvalidPackageManifest ErrorType = "PackageManifestNotValid"
103103
ErrorObjectFailedValidation ErrorType = "ObjectFailedValidation"
104104
ErrorPropertiesAnnotationUsed ErrorType = "PropertiesAnnotationUsed"
105+
ErrorDeprecatedValidator ErrorType = "DeprecatedValidator"
105106
)
106107

107108
func NewError(t ErrorType, detail, field string, v interface{}) Error {
@@ -248,3 +249,7 @@ func WarnInvalidObject(detail string, value interface{}) Error {
248249
func WarnPropertiesAnnotationUsed(detail string) Error {
249250
return Error{ErrorPropertiesAnnotationUsed, LevelWarn, "", "", detail}
250251
}
252+
253+
func WarnDeprecatedValidator(detail string) Error {
254+
return Error{ErrorDeprecatedValidator, LevelWarn, "", "", detail}
255+
}

pkg/validation/internal/operatorhub.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ import (
118118
// `k8s-version` key is allowed. If informed, it will perform the checks against this specific Kubernetes version where the
119119
// operator bundle is intend to be used and will raise errors instead of warnings.
120120
// Currently, this check is capable of verifying the removed APIs only for Kubernetes 1.22 version.
121-
var OperatorHubValidator interfaces.Validator = interfaces.ValidatorFunc(validateOperatorHub)
121+
var OperatorHubValidator interfaces.Validator = interfaces.ValidatorFunc(validateOperatorHubDeprecated)
122122

123123
var validCapabilities = map[string]struct{}{
124124
"Basic Install": {},
@@ -151,13 +151,15 @@ var validCategories = map[string]struct{}{
151151
"Security": {},
152152
"Storage": {},
153153
"Streaming & Messaging": {},
154+
"Observability": {},
154155
}
155156

156157
const minKubeVersionWarnMessage = "csv.Spec.minKubeVersion is not informed. It is recommended you provide this information. " +
157158
"Otherwise, it would mean that your operator project can be distributed and installed in any cluster version " +
158159
"available, which is not necessarily the case for all projects."
159160

160-
func validateOperatorHub(objs ...interface{}) (results []errors.ManifestResult) {
161+
// Warning: this validator is deprecated in favor of validateOperatorHub()
162+
func validateOperatorHubDeprecated(objs ...interface{}) (results []errors.ManifestResult) {
161163

162164
// Obtain the k8s version if informed via the objects an optional
163165
k8sVersion := ""
@@ -178,6 +180,11 @@ func validateOperatorHub(objs ...interface{}) (results []errors.ManifestResult)
178180
}
179181
}
180182

183+
// Add a deprecation warning to the list so that users are aware this validator is deprecated
184+
deprecationResultWarning := errors.ManifestResult{}
185+
deprecationResultWarning.Add(errors.WarnDeprecatedValidator("OperatorHub Validator deprecated, use OperatorHubV2 validator"))
186+
results = append(results, deprecationResultWarning)
187+
181188
return results
182189
}
183190

@@ -221,7 +228,8 @@ func validateHubCSVSpec(csv v1alpha1.ClusterServiceVersion) CSVChecks {
221228
checks = checkSpecProviderName(checks)
222229
checks = checkSpecMaintainers(checks)
223230
checks = checkSpecLinks(checks)
224-
checks = checkAnnotations(checks)
231+
checks = checkCapabilities(checks)
232+
checks = checkCategories(checks)
225233
checks = checkSpecVersion(checks)
226234
checks = checkSpecIcon(checks)
227235
checks = checkSpecMinKubeVersion(checks)
@@ -257,7 +265,7 @@ func checkSpecVersion(checks CSVChecks) CSVChecks {
257265
}
258266

259267
// checkAnnotations will validate the values informed via annotations such as; capabilities and categories
260-
func checkAnnotations(checks CSVChecks) CSVChecks {
268+
func checkCapabilities(checks CSVChecks) CSVChecks {
261269
if checks.csv.GetAnnotations() == nil {
262270
checks.csv.SetAnnotations(make(map[string]string))
263271
}
@@ -267,6 +275,13 @@ func checkAnnotations(checks CSVChecks) CSVChecks {
267275
checks.errs = append(checks.errs, fmt.Errorf("csv.Metadata.Annotations.Capabilities %s is not a valid capabilities level", capability))
268276
}
269277
}
278+
return checks
279+
}
280+
281+
func checkCategories(checks CSVChecks) CSVChecks {
282+
if checks.csv.GetAnnotations() == nil {
283+
checks.csv.SetAnnotations(make(map[string]string))
284+
}
270285

271286
if categories, ok := checks.csv.ObjectMeta.Annotations["categories"]; ok {
272287
categorySlice := strings.Split(categories, ",")
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package internal
2+
3+
import (
4+
"github.com/operator-framework/api/pkg/manifests"
5+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
6+
"github.com/operator-framework/api/pkg/validation/errors"
7+
interfaces "github.com/operator-framework/api/pkg/validation/interfaces"
8+
)
9+
10+
var OperatorHubValidatorV2 interfaces.Validator = interfaces.ValidatorFunc(validateOperatorHubV2)
11+
12+
func validateOperatorHubV2(objs ...interface{}) (results []errors.ManifestResult) {
13+
// Obtain the k8s version if informed via the objects an optional
14+
k8sVersion := ""
15+
for _, obj := range objs {
16+
switch obj.(type) {
17+
case map[string]string:
18+
k8sVersion = obj.(map[string]string)[k8sVersionKey]
19+
if len(k8sVersion) > 0 {
20+
break
21+
}
22+
}
23+
}
24+
25+
for _, obj := range objs {
26+
switch v := obj.(type) {
27+
case *manifests.Bundle:
28+
results = append(results, validateBundleOperatorHubV2(v, k8sVersion))
29+
}
30+
}
31+
32+
return results
33+
}
34+
35+
func validateBundleOperatorHubV2(bundle *manifests.Bundle, k8sVersion string) errors.ManifestResult {
36+
result := errors.ManifestResult{Name: bundle.Name}
37+
38+
if bundle == nil {
39+
result.Add(errors.ErrInvalidBundle("Bundle is nil", nil))
40+
return result
41+
}
42+
43+
if bundle.CSV == nil {
44+
result.Add(errors.ErrInvalidBundle("Bundle csv is nil", bundle.Name))
45+
return result
46+
}
47+
48+
csvChecksResult := validateHubCSVSpecV2(*bundle.CSV)
49+
for _, err := range csvChecksResult.errs {
50+
result.Add(errors.ErrInvalidCSV(err.Error(), bundle.CSV.GetName()))
51+
}
52+
for _, warn := range csvChecksResult.warns {
53+
result.Add(errors.WarnInvalidCSV(warn.Error(), bundle.CSV.GetName()))
54+
}
55+
56+
errs, warns := validateDeprecatedAPIS(bundle, k8sVersion)
57+
for _, err := range errs {
58+
result.Add(errors.ErrFailedValidation(err.Error(), bundle.CSV.GetName()))
59+
}
60+
for _, warn := range warns {
61+
result.Add(errors.WarnFailedValidation(warn.Error(), bundle.CSV.GetName()))
62+
}
63+
64+
return result
65+
}
66+
67+
// validateHubCSVSpec will check the CSV against the criteria to publish an
68+
// operator bundle in the OperatorHub.io
69+
func validateHubCSVSpecV2(csv v1alpha1.ClusterServiceVersion) CSVChecks {
70+
checks := CSVChecks{csv: csv, errs: []error{}, warns: []error{}}
71+
72+
checks = checkSpecProviderName(checks)
73+
checks = checkSpecMaintainers(checks)
74+
checks = checkSpecLinks(checks)
75+
checks = checkCapabilities(checks)
76+
checks = checkSpecVersion(checks)
77+
checks = checkSpecIcon(checks)
78+
checks = checkSpecMinKubeVersion(checks)
79+
80+
return checks
81+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package internal
2+
3+
import (
4+
"testing"
5+
6+
"github.com/operator-framework/api/pkg/manifests"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestValidateBundleOperatorHubV2(t *testing.T) {
12+
var table = []struct {
13+
description string
14+
directory string
15+
hasError bool
16+
errStrings []string
17+
}{
18+
{
19+
description: "registryv1 bundle/valid bundle",
20+
directory: "./testdata/valid_bundle",
21+
hasError: false,
22+
},
23+
{
24+
description: "registryv1 bundle/invald bundle operatorhubio",
25+
directory: "./testdata/invalid_bundle_operatorhub",
26+
hasError: true,
27+
errStrings: []string{
28+
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Provider.Name not specified`,
29+
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Maintainers elements should contain both name and email`,
30+
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Maintainers email invalidemail is invalid: mail: missing '@' or angle-addr`,
31+
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Links elements should contain both name and url`,
32+
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Links url https//coreos.com/operators/etcd/docs/latest/ is invalid: parse "https//coreos.com/operators/etcd/docs/latest/": invalid URI for request`,
33+
`Error: Value : (etcdoperator.v0.9.4) csv.Metadata.Annotations.Capabilities Installs and stuff is not a valid capabilities level`,
34+
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Icon should only have one element`,
35+
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Version must be set`,
36+
},
37+
},
38+
}
39+
40+
for _, tt := range table {
41+
// Validate the bundle object
42+
bundle, err := manifests.GetBundleFromDir(tt.directory)
43+
require.NoError(t, err)
44+
45+
results := OperatorHubValidatorV2.Validate(bundle)
46+
47+
if len(results) > 0 {
48+
require.Equal(t, results[0].HasError(), tt.hasError)
49+
if results[0].HasError() {
50+
require.Equal(t, len(tt.errStrings), len(results[0].Errors))
51+
52+
for _, err := range results[0].Errors {
53+
errString := err.Error()
54+
require.Contains(t, tt.errStrings, errString)
55+
}
56+
}
57+
}
58+
}
59+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package internal
2+
3+
import (
4+
"github.com/operator-framework/api/pkg/manifests"
5+
"github.com/operator-framework/api/pkg/validation/errors"
6+
interfaces "github.com/operator-framework/api/pkg/validation/interfaces"
7+
)
8+
9+
var StandardCategoriesValidator interfaces.Validator = interfaces.ValidatorFunc(validateCategories)
10+
11+
func validateCategories(objs ...interface{}) (results []errors.ManifestResult) {
12+
for _, obj := range objs {
13+
switch v := obj.(type) {
14+
case *manifests.Bundle:
15+
results = append(results, validateCategoriesBundle(v))
16+
}
17+
}
18+
19+
return results
20+
}
21+
22+
func validateCategoriesBundle(bundle *manifests.Bundle) errors.ManifestResult {
23+
result := errors.ManifestResult{Name: bundle.Name}
24+
csvCategoryCheck := CSVChecks{csv: *bundle.CSV, errs: []error{}, warns: []error{}}
25+
26+
csvChecksResult := checkCategories(csvCategoryCheck)
27+
for _, err := range csvChecksResult.errs {
28+
result.Add(errors.ErrInvalidCSV(err.Error(), bundle.CSV.GetName()))
29+
}
30+
for _, warn := range csvChecksResult.warns {
31+
result.Add(errors.WarnInvalidCSV(warn.Error(), bundle.CSV.GetName()))
32+
}
33+
34+
return result
35+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package internal
2+
3+
import (
4+
"testing"
5+
6+
"github.com/operator-framework/api/pkg/manifests"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestValidateCategories(t *testing.T) {
12+
var table = []struct {
13+
description string
14+
directory string
15+
hasError bool
16+
errStrings []string
17+
}{
18+
{
19+
description: "registryv1 bundle/valid bundle",
20+
directory: "./testdata/valid_bundle",
21+
hasError: false,
22+
},
23+
{
24+
description: "registryv1 bundle/invald bundle operatorhubio",
25+
directory: "./testdata/invalid_bundle_operatorhub",
26+
hasError: true,
27+
errStrings: []string{
28+
`Error: Value : (etcdoperator.v0.9.4) csv.Metadata.Annotations["categories"] value Magic is not in the set of default categories`,
29+
},
30+
},
31+
}
32+
33+
for _, tt := range table {
34+
// Validate the bundle object
35+
bundle, err := manifests.GetBundleFromDir(tt.directory)
36+
require.NoError(t, err)
37+
38+
results := StandardCategoriesValidator.Validate(bundle)
39+
40+
if len(results) > 0 {
41+
require.Equal(t, results[0].HasError(), tt.hasError)
42+
if results[0].HasError() {
43+
require.Equal(t, len(tt.errStrings), len(results[0].Errors))
44+
45+
for _, err := range results[0].Errors {
46+
errString := err.Error()
47+
require.Contains(t, tt.errStrings, errString)
48+
}
49+
}
50+
}
51+
}
52+
}

pkg/validation/internal/testdata/valid_bundle/etcdoperator.v0.9.4.clusterserviceversion.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ metadata:
1616
],\n \"storageType\":\"S3\",\n \"s3\": {\n \"path\": \"<full-s3-path>\"\
1717
,\n \"awsSecret\": \"<aws-secret>\"\n }\n }\n }\n]\n"
1818
capabilities: Full Lifecycle
19-
categories: Database, Big Data
19+
categories: Database, Big Data, Observability
2020
containerImage: quay.io/coreos/etcd-operator@sha256:66a37fd61a06a43969854ee6d3e21087a98b93838e284a6086b13917f96b0d9b
2121
createdAt: 2019-02-28 01:03:00
2222
description: Create and maintain highly-available etcd clusters on Kubernetes

pkg/validation/validation.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,17 @@ var CustomResourceDefinitionValidator = internal.CRDValidator
2828
var BundleValidator = internal.BundleValidator
2929

3030
// OperatorHubValidator implements Validator to validate bundle objects
31-
// for OperatorHub.io requirements.
31+
// for OperatorHub.io requirements. This validator is deprecated.
3232
var OperatorHubValidator = internal.OperatorHubValidator
3333

34+
// OperatorHubValidatorV2 implements Validator to validate bundle objects
35+
// for OperatorHub.io requirements.
36+
var OperatorHubValidatorV2 = internal.OperatorHubValidatorV2
37+
38+
// StandardCategoriesValidator implements Validator to validate bundle objects
39+
// for OperatorHub.io requirements around UI category metadata
40+
var StandardCategoriesValidator = internal.StandardCategoriesValidator
41+
3442
// Object Validator validates various custom objects in the bundle like PDBs and SCCs.
3543
// Object validation is optional and not a default-level validation.
3644
var ObjectValidator = internal.ObjectValidator
@@ -69,7 +77,8 @@ var AllValidators = interfaces.Validators{
6977
ClusterServiceVersionValidator,
7078
CustomResourceDefinitionValidator,
7179
BundleValidator,
72-
OperatorHubValidator,
80+
OperatorHubValidatorV2,
81+
StandardCategoriesValidator,
7382
ObjectValidator,
7483
OperatorGroupValidator,
7584
CommunityOperatorValidator,

0 commit comments

Comments
 (0)