Skip to content

Commit 73a5934

Browse files
authored
Separate Category and Capability Validation (#304)
This commit removes the default set of category and capability validation as a part of the operatorhubio validator. As we have a mechanism for custom category 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 also does the same separation for capability validation. There is no implementation of custom capability validation (as there is less churn and no explicit need for that yet) -- adding custom capability validation should be trivial in a future commit. This commit accomplishes this by deprecating the existing validator and creating a v2 version of the operatorhubio validator. Additionally, this commit adds 'Observability' to the category list.
1 parent 071829b commit 73a5934

11 files changed

+430
-74
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: 12 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"io/ioutil"
77
"net/mail"
88
"net/url"
9-
"os"
109
"path/filepath"
1110
"strings"
1211

@@ -118,15 +117,9 @@ import (
118117
// `k8s-version` key is allowed. If informed, it will perform the checks against this specific Kubernetes version where the
119118
// operator bundle is intend to be used and will raise errors instead of warnings.
120119
// Currently, this check is capable of verifying the removed APIs only for Kubernetes 1.22 version.
121-
var OperatorHubValidator interfaces.Validator = interfaces.ValidatorFunc(validateOperatorHub)
122-
123-
var validCapabilities = map[string]struct{}{
124-
"Basic Install": {},
125-
"Seamless Upgrades": {},
126-
"Full Lifecycle": {},
127-
"Deep Insights": {},
128-
"Auto Pilot": {},
129-
}
120+
//
121+
// Deprecated: Use OperatorHubV2Validator, StandardCapabilitiesValidator and StandardCategoriesValidator for equivalent validation.
122+
var OperatorHubValidator interfaces.Validator = interfaces.ValidatorFunc(validateOperatorHubDeprecated)
130123

131124
var validMediatypes = map[string]struct{}{
132125
"image/gif": {},
@@ -135,29 +128,12 @@ var validMediatypes = map[string]struct{}{
135128
"image/svg+xml": {},
136129
}
137130

138-
var validCategories = map[string]struct{}{
139-
"AI/Machine Learning": {},
140-
"Application Runtime": {},
141-
"Big Data": {},
142-
"Cloud Provider": {},
143-
"Developer Tools": {},
144-
"Database": {},
145-
"Integration & Delivery": {},
146-
"Logging & Tracing": {},
147-
"Monitoring": {},
148-
"Modernization & Migration": {},
149-
"Networking": {},
150-
"OpenShift Optional": {},
151-
"Security": {},
152-
"Storage": {},
153-
"Streaming & Messaging": {},
154-
}
155-
156131
const minKubeVersionWarnMessage = "csv.Spec.minKubeVersion is not informed. It is recommended you provide this information. " +
157132
"Otherwise, it would mean that your operator project can be distributed and installed in any cluster version " +
158133
"available, which is not necessarily the case for all projects."
159134

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

162138
// Obtain the k8s version if informed via the objects an optional
163139
k8sVersion := ""
@@ -178,6 +154,11 @@ func validateOperatorHub(objs ...interface{}) (results []errors.ManifestResult)
178154
}
179155
}
180156

157+
// Add a deprecation warning to the list so that users are aware this validator is deprecated
158+
deprecationResultWarning := errors.ManifestResult{}
159+
deprecationResultWarning.Add(errors.WarnDeprecatedValidator(`The "operatorhub" validator is deprecated; for equivalent validation use "operatorhub/v2", "standardcapabilities" and "standardcategories" validators`))
160+
results = append(results, deprecationResultWarning)
161+
181162
return results
182163
}
183164

@@ -221,7 +202,8 @@ func validateHubCSVSpec(csv v1alpha1.ClusterServiceVersion) CSVChecks {
221202
checks = checkSpecProviderName(checks)
222203
checks = checkSpecMaintainers(checks)
223204
checks = checkSpecLinks(checks)
224-
checks = checkAnnotations(checks)
205+
checks = checkCapabilities(checks)
206+
checks = checkCategories(checks)
225207
checks = checkSpecVersion(checks)
226208
checks = checkSpecIcon(checks)
227209
checks = checkSpecMinKubeVersion(checks)
@@ -256,46 +238,6 @@ func checkSpecVersion(checks CSVChecks) CSVChecks {
256238
return checks
257239
}
258240

259-
// checkAnnotations will validate the values informed via annotations such as; capabilities and categories
260-
func checkAnnotations(checks CSVChecks) CSVChecks {
261-
if checks.csv.GetAnnotations() == nil {
262-
checks.csv.SetAnnotations(make(map[string]string))
263-
}
264-
265-
if capability, ok := checks.csv.ObjectMeta.Annotations["capabilities"]; ok {
266-
if _, ok := validCapabilities[capability]; !ok {
267-
checks.errs = append(checks.errs, fmt.Errorf("csv.Metadata.Annotations.Capabilities %s is not a valid capabilities level", capability))
268-
}
269-
}
270-
271-
if categories, ok := checks.csv.ObjectMeta.Annotations["categories"]; ok {
272-
categorySlice := strings.Split(categories, ",")
273-
274-
// use custom categories for validation if provided
275-
customCategoriesPath := os.Getenv("OPERATOR_BUNDLE_CATEGORIES")
276-
if customCategoriesPath != "" {
277-
customCategories, err := extractCategories(customCategoriesPath)
278-
if err != nil {
279-
checks.errs = append(checks.errs, fmt.Errorf("could not extract custom categories from categories %#v: %s", customCategories, err))
280-
} else {
281-
for _, category := range categorySlice {
282-
if _, ok := customCategories[strings.TrimSpace(category)]; !ok {
283-
checks.errs = append(checks.errs, fmt.Errorf("csv.Metadata.Annotations[\"categories\"] value %s is not in the set of custom categories", category))
284-
}
285-
}
286-
}
287-
} else {
288-
// use default categories
289-
for _, category := range categorySlice {
290-
if _, ok := validCategories[strings.TrimSpace(category)]; !ok {
291-
checks.errs = append(checks.errs, fmt.Errorf("csv.Metadata.Annotations[\"categories\"] value %s is not in the set of default categories", category))
292-
}
293-
}
294-
}
295-
}
296-
return checks
297-
}
298-
299241
// checkSpecIcon will validate if the CSV.spec.Icon was informed and is correct
300242
func checkSpecIcon(checks CSVChecks) CSVChecks {
301243
if checks.csv.Spec.Icon != nil {

pkg/validation/internal/operatorhub_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ func TestValidateBundleOperatorHub(t *testing.T) {
3232
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Maintainers email invalidemail is invalid: mail: missing '@' or angle-addr`,
3333
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Links elements should contain both name and url`,
3434
`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`,
35-
`Error: Value : (etcdoperator.v0.9.4) csv.Metadata.Annotations.Capabilities Installs and stuff is not a valid capabilities level`,
35+
`Error: Value : (etcdoperator.v0.9.4) csv.Metadata.Annotations.Capabilities "Installs and stuff" is not a valid capabilities level`,
3636
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Icon should only have one element`,
37-
`Error: Value : (etcdoperator.v0.9.4) csv.Metadata.Annotations["categories"] value Magic is not in the set of default categories`,
37+
`Error: Value : (etcdoperator.v0.9.4) csv.Metadata.Annotations["categories"] value "Magic" is not in the set of standard categories`,
3838
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Version must be set`,
3939
},
4040
},
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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 OperatorHubV2Validator 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 = checkSpecVersion(checks)
76+
checks = checkSpecIcon(checks)
77+
checks = checkSpecMinKubeVersion(checks)
78+
79+
return checks
80+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.Spec.Icon should only have one element`,
34+
`Error: Value : (etcdoperator.v0.9.4) csv.Spec.Version must be set`,
35+
},
36+
},
37+
}
38+
39+
for _, tt := range table {
40+
// Validate the bundle object
41+
bundle, err := manifests.GetBundleFromDir(tt.directory)
42+
require.NoError(t, err)
43+
44+
results := OperatorHubV2Validator.Validate(bundle)
45+
46+
if len(results) > 0 {
47+
require.Equal(t, results[0].HasError(), tt.hasError)
48+
if results[0].HasError() {
49+
require.Equal(t, len(tt.errStrings), len(results[0].Errors))
50+
51+
for _, err := range results[0].Errors {
52+
errString := err.Error()
53+
require.Contains(t, tt.errStrings, errString)
54+
}
55+
}
56+
}
57+
}
58+
}
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+
"fmt"
5+
6+
"github.com/operator-framework/api/pkg/manifests"
7+
"github.com/operator-framework/api/pkg/validation/errors"
8+
interfaces "github.com/operator-framework/api/pkg/validation/interfaces"
9+
)
10+
11+
var StandardCapabilitiesValidator interfaces.Validator = interfaces.ValidatorFunc(validateCapabilities)
12+
13+
var validCapabilities = map[string]struct{}{
14+
"Basic Install": {},
15+
"Seamless Upgrades": {},
16+
"Full Lifecycle": {},
17+
"Deep Insights": {},
18+
"Auto Pilot": {},
19+
}
20+
21+
func validateCapabilities(objs ...interface{}) (results []errors.ManifestResult) {
22+
for _, obj := range objs {
23+
switch v := obj.(type) {
24+
case *manifests.Bundle:
25+
results = append(results, validateCapabilitiesBundle(v))
26+
}
27+
}
28+
29+
return results
30+
}
31+
32+
func validateCapabilitiesBundle(bundle *manifests.Bundle) errors.ManifestResult {
33+
result := errors.ManifestResult{Name: bundle.Name}
34+
csvCategoryCheck := CSVChecks{csv: *bundle.CSV, errs: []error{}, warns: []error{}}
35+
36+
csvChecksResult := checkCapabilities(csvCategoryCheck)
37+
for _, err := range csvChecksResult.errs {
38+
result.Add(errors.ErrInvalidCSV(err.Error(), bundle.CSV.GetName()))
39+
}
40+
for _, warn := range csvChecksResult.warns {
41+
result.Add(errors.WarnInvalidCSV(warn.Error(), bundle.CSV.GetName()))
42+
}
43+
44+
return result
45+
}
46+
47+
// checkAnnotations will validate the values informed via annotations such as; capabilities and categories
48+
func checkCapabilities(checks CSVChecks) CSVChecks {
49+
if checks.csv.GetAnnotations() == nil {
50+
checks.csv.SetAnnotations(make(map[string]string))
51+
}
52+
53+
if capability, ok := checks.csv.ObjectMeta.Annotations["capabilities"]; ok {
54+
if _, ok := validCapabilities[capability]; !ok {
55+
checks.errs = append(checks.errs, fmt.Errorf("csv.Metadata.Annotations.Capabilities %q is not a valid capabilities level", capability))
56+
}
57+
}
58+
return checks
59+
}

0 commit comments

Comments
 (0)