diff --git a/crds/zz_defs.go b/crds/zz_defs.go index 8e18d6281..780ff4603 100644 --- a/crds/zz_defs.go +++ b/crds/zz_defs.go @@ -1,6 +1,6 @@ // Code generated by go-bindata. (@generated) DO NOT EDIT. - //Package crds generated by go-bindata.// sources: +//Package crds generated by go-bindata.// sources: // operators.coreos.com_catalogsources.yaml // operators.coreos.com_clusterserviceversions.yaml // operators.coreos.com_installplans.yaml diff --git a/pkg/validation/internal/community.go b/pkg/validation/internal/community.go new file mode 100644 index 000000000..3503db404 --- /dev/null +++ b/pkg/validation/internal/community.go @@ -0,0 +1,315 @@ +package internal + +import ( + "encoding/json" + "fmt" + "github.com/blang/semver" + "io/ioutil" + "os" + "strings" + + "github.com/operator-framework/api/pkg/manifests" + "github.com/operator-framework/api/pkg/validation/errors" + interfaces "github.com/operator-framework/api/pkg/validation/interfaces" +) + +// IndexImagePathKey defines the key which can be used by its consumers +// to inform where their index image path is to be checked +const IndexImagePathKey = "index-path" + +// ocpLabelindex defines the OCP label which allow configure the OCP versions +// where the bundle will be distributed +const ocpLabelindex = "com.redhat.openshift.versions" + +// CommunityOperatorValidator validates the bundle manifests against the required criteria to publish +// the projects on the community operators +// +// Note that this validator allows to receive a List of optional values as key=values. Currently, only the +// `index-path` key is allowed. If informed, it will check the labels on the image index according to its criteria. +var CommunityOperatorValidator interfaces.Validator = interfaces.ValidatorFunc(communityValidator) + +func communityValidator(objs ...interface{}) (results []errors.ManifestResult) { + + // Obtain the k8s version if informed via the objects an optional + var indexImagePath = "" + for _, obj := range objs { + switch obj.(type) { + case map[string]string: + indexImagePath = obj.(map[string]string)[IndexImagePathKey] + if len(indexImagePath) > 0 { + break + } + } + } + + for _, obj := range objs { + switch v := obj.(type) { + case *manifests.Bundle: + results = append(results, validateCommunityBundle(v, indexImagePath)) + } + } + + return results +} + +type CommunityOperatorChecks struct { + bundle manifests.Bundle + indexImagePath string + indexImage string + errs []error + warns []error +} + +// validateCommunityBundle will check the bundle against the community-operator criterias +func validateCommunityBundle(bundle *manifests.Bundle, indexImagePath string) errors.ManifestResult { + result := errors.ManifestResult{Name: bundle.Name} + if bundle == nil { + result.Add(errors.ErrInvalidBundle("Bundle is nil", nil)) + return result + } + + if bundle.CSV == nil { + result.Add(errors.ErrInvalidBundle("Bundle csv is nil", bundle.Name)) + return result + } + + checks := CommunityOperatorChecks{bundle: *bundle, indexImagePath: indexImagePath, errs: []error{}, warns: []error{}} + + deprecatedAPIs := getRemovedAPIsOn1_22From(bundle) + // Check if has deprecated apis then, check the olm.maxOpenShiftVersion property + if len(deprecatedAPIs) > 0 { + deprecatedAPIsMessage := generateMessageWithDeprecatedAPIs(deprecatedAPIs) + checks = checkMaxOpenShiftVersion(checks, deprecatedAPIsMessage) + checks = checkOCPLabelsWithHasDeprecatedAPIs(checks, deprecatedAPIsMessage) + for _, err := range checks.errs { + result.Add(errors.ErrInvalidCSV(err.Error(), bundle.CSV.GetName())) + } + for _, warn := range checks.warns { + result.Add(errors.WarnInvalidCSV(warn.Error(), bundle.CSV.GetName())) + } + } + + return result +} + +type propertiesAnnotation struct { + Type string + Value string +} + +// checkMaxOpenShiftVersion will verify if the OpenShiftVersion property was informed +func checkMaxOpenShiftVersion(checks CommunityOperatorChecks, v1beta1MsgForResourcesFound string) CommunityOperatorChecks { + // Ensure that has the OCPMaxAnnotation + const olmproperties = "olm.properties" + const olmmaxOpenShiftVersion = "olm.maxOpenShiftVersion" + semVerOCPV1beta1Unsupported, _ := semver.ParseTolerant(ocpVerV1beta1Unsupported) + + properties := checks.bundle.CSV.Annotations[olmproperties] + if len(properties) == 0 { + checks.errs = append(checks.errs, fmt.Errorf("csv.Annotations not specified %s for an "+ + "OCP version < %s. This annotation is required to prevent the user from upgrading their OCP cluster "+ + "before they have installed a version of their operator which is compatible with %s. This bundle is %s which are no "+ + "longer supported on %s. Migrate the API(s) for %s or use the annotation", + olmmaxOpenShiftVersion, + ocpVerV1beta1Unsupported, + ocpVerV1beta1Unsupported, + k8sApiDeprecatedInfo, + ocpVerV1beta1Unsupported, + v1beta1MsgForResourcesFound)) + return checks + } + + var properList []propertiesAnnotation + if err := json.Unmarshal([]byte(properties), &properList); err != nil { + checks.errs = append(checks.errs, fmt.Errorf("csv.Annotations has an invalid value specified for %s. "+ + "Please, check the value (%s) and ensure that it is an array such as: "+ + "\"olm.properties\": '[{\"type\": \"key name\", \"value\": \"key value\"}]'", + olmproperties, properties)) + return checks + } + + hasOlmMaxOpenShiftVersion := false + olmMaxOpenShiftVersionValue := "" + for _, v := range properList { + if v.Type == olmmaxOpenShiftVersion { + hasOlmMaxOpenShiftVersion = true + olmMaxOpenShiftVersionValue = v.Value + break + } + } + + if !hasOlmMaxOpenShiftVersion { + checks.errs = append(checks.errs, fmt.Errorf("csv.Annotations.%s with the "+ + "key `%s` and a value with an OCP version which is < %s is required for any operator "+ + "bundle that is %s. Migrate the API(s) for %s or use the annotation", + olmproperties, + olmmaxOpenShiftVersion, + ocpVerV1beta1Unsupported, + k8sApiDeprecatedInfo, + v1beta1MsgForResourcesFound)) + return checks + } + + semVerVersionMaxOcp, err := semver.ParseTolerant(olmMaxOpenShiftVersionValue) + if err != nil { + checks.errs = append(checks.errs, fmt.Errorf("csv.Annotations.%s has an invalid value."+ + "Unable to parse (%s) using semver : %s", + olmproperties, olmMaxOpenShiftVersionValue, err)) + return checks + } + + if semVerVersionMaxOcp.GE(semVerOCPV1beta1Unsupported) { + checks.errs = append(checks.errs, fmt.Errorf("csv.Annotations.%s with the "+ + "key and value for %s has the OCP version value %s which is >= of %s. This bundle is %s. "+ + "Migrate the API(s) for %s "+ + "or inform in this property an OCP version which is < %s", + olmproperties, + olmmaxOpenShiftVersion, + olmMaxOpenShiftVersionValue, + ocpVerV1beta1Unsupported, + k8sApiDeprecatedInfo, + v1beta1MsgForResourcesFound, + ocpVerV1beta1Unsupported)) + return checks + } + + return checks +} + +// checkOCPLabels will ensure that OCP labels are set and with a ocp target < 4.9 +func checkOCPLabelsWithHasDeprecatedAPIs(checks CommunityOperatorChecks, deprecatedAPImsg string) CommunityOperatorChecks { + // Note that we cannot make mandatory because the package format still valid + if len(checks.indexImagePath) == 0 && len(checks.indexImage) == 0 { + checks.warns = append(checks.errs, fmt.Errorf("please, inform the path of "+ + "its index image file via the the optional key values and the key %s to allow this validator check the labels "+ + "configuration or migrate the API(s) for %s. "+ + "(e.g. %s=./mypath/bundle.Dockerfile). This bundle is %s ", + IndexImagePathKey, + deprecatedAPImsg, + IndexImagePathKey, + k8sApiDeprecatedInfo)) + return checks + } + + return validateImageFile(checks, deprecatedAPImsg) +} + +func validateImageFile(checks CommunityOperatorChecks, deprecatedAPImsg string) CommunityOperatorChecks { + if len(checks.indexImagePath) == 0 { + return checks + } + + info, err := os.Stat(checks.indexImagePath) + if err != nil { + checks.errs = append(checks.errs, fmt.Errorf("the index image in the path "+ + "(%s) was not found. Please, inform the path of the bundle operator index image via the the optional key values and the key %s. "+ + "(e.g. %s=./mypath/bundle.Dockerfile). Error : %s", checks.indexImagePath, IndexImagePathKey, IndexImagePathKey, err)) + return checks + } + if info.IsDir() { + checks.errs = append(checks.errs, fmt.Errorf("the index image in the path "+ + "(%s) is not file. Please, inform the path of its index image via the the optional key values and the key %s. "+ + "(e.g. %s=./mypath/bundle.Dockerfile). The value informed is a diretory and not a file", checks.indexImagePath, IndexImagePathKey, IndexImagePathKey)) + return checks + } + + b, err := ioutil.ReadFile(checks.indexImagePath) + if err != nil { + checks.errs = append(checks.errs, fmt.Errorf("unable to read the index image in the path "+ + "(%s). Error : %s", checks.indexImagePath, err)) + return checks + } + + indexPathContent := string(b) + hasOCPLabel := strings.Contains(indexPathContent, ocpLabelindex) + if hasOCPLabel { + semVerOCPV1beta1Unsupported, _ := semver.ParseTolerant(ocpVerV1beta1Unsupported) + // the OCP range informed cannot allow carry on to OCP 4.9+ + line := strings.Split(indexPathContent, "\n") + for i := 0; i < len(line); i++ { + if strings.Contains(line[i], ocpLabelindex) { + if !strings.Contains(line[i], "=") { + checks.errs = append(checks.errs, fmt.Errorf("invalid syntax (%s) on the LABEL %s. Migrate the API(s) "+ + "for %s or use the OCP labels. (e.g. LABEL %s='4.6-4.8')", + line[i], + deprecatedAPImsg, + ocpLabelindex, + ocpLabelindex)) + return checks + } + + value := strings.Split(line[i], "=") + indexRange := value[1] + doubleCote := "\"" + singleCote := "'" + indexRange = strings.ReplaceAll(indexRange, singleCote, "") + indexRange = strings.ReplaceAll(indexRange, doubleCote, "") + if len(indexRange) > 1 { + // if has the = then, the value needs to be < 4.9 + if strings.Contains(indexRange, "=") { + version := strings.Split(indexRange, "=")[1] + verParsed, err := semver.ParseTolerant(version) + if err != nil { + checks.errs = append(checks.errs, fmt.Errorf("unable to parse the value (%s) on (%s)", + version, ocpLabelindex)) + return checks + } + + if verParsed.GE(semVerOCPV1beta1Unsupported) { + checks.errs = append(checks.errs, fmt.Errorf("this bundle is %s. Migrate the API(s) "+ + "for %s or use the OCP labels for compatible version(s). (e.g. LABEL %s='=v4.8')", + k8sApiDeprecatedInfo, + deprecatedAPImsg, + ocpLabelindex)) + return checks + } + } else { + // if not has not the = then the value needs contains - value less < 4.9 + if !strings.Contains(indexRange, "-") { + checks.errs = append(checks.errs, fmt.Errorf("this bundle is %s. "+ + "The %s allows to distribute it on >= %s. Migrate the API(s) for "+ + "%s or provide comatible version(s) via the labels. (e.g. LABEL %s='4.6-4.8')", + deprecatedAPImsg, + indexRange, + ocpVerV1beta1Unsupported, + deprecatedAPImsg, + ocpLabelindex)) + return checks + } + + version := strings.Split(indexRange, "-")[1] + verParsed, err := semver.ParseTolerant(version) + if err != nil { + checks.errs = append(checks.errs, fmt.Errorf("unable to parse the value (%s) on (%s)", + version, ocpLabelindex)) + return checks + } + + if verParsed.GE(semVerOCPV1beta1Unsupported) { + checks.errs = append(checks.errs, fmt.Errorf("this bundle is %s. Upgrade the APIs from "+ + "(v1beta1) to (v1) for %s or provide com[atible version(s) via the labels. (e.g. LABEL %s='4.6-4.8')", + k8sApiDeprecatedInfo, + deprecatedAPImsg, + ocpLabelindex)) + return checks + } + + } + } else { + checks.errs = append(checks.errs, fmt.Errorf("unable to get the range informed on %s", + ocpLabelindex)) + return checks + } + break + } + } + } else { + checks.errs = append(checks.errs, fmt.Errorf("this bundle is %s. Migrate the APIs "+ + "for %s or provide compatible version(s) via the labels. (e.g. LABEL %s='4.6-4.8')", + k8sApiDeprecatedInfo, + deprecatedAPImsg, + ocpLabelindex)) + return checks + } + return checks +} diff --git a/pkg/validation/internal/community_test.go b/pkg/validation/internal/community_test.go new file mode 100644 index 000000000..eec179919 --- /dev/null +++ b/pkg/validation/internal/community_test.go @@ -0,0 +1,205 @@ +package internal + +import ( + "fmt" + "github.com/operator-framework/api/pkg/manifests" + "github.com/stretchr/testify/require" + "testing" +) + +func Test_communityValidator(t *testing.T) { + type args struct { + annotations map[string]string + bundleDir string + imageIndexPath string + } + tests := []struct { + name string + args args + wantError bool + wantWarning bool + errStrings []string + warnStrings []string + }{ + { + name: "should work successfully when no deprecated apis are found and has not the annotations or ocp index labels", + wantError: false, + args: args{ + bundleDir: "./testdata/valid_bundle_v1", + }, + }, + { + name: "should pass when the olm annotation and index label are set with a " + + "value < 4.9 and has deprecated apis", + wantError: false, + args: args{ + bundleDir: "./testdata/valid_bundle_v1beta1", + imageIndexPath: "./testdata/dockerfile/valid_bundle.Dockerfile", + annotations: map[string]string{ + "olm.properties": fmt.Sprintf(`[{"type": "olm.maxOpenShiftVersion", "value": "4.8"}]`), + }, + }, + }, + { + name: "should fail because is missing the olm.annotation and has deprecated apis", + wantError: true, + args: args{ + bundleDir: "./testdata/valid_bundle_v1beta1", + imageIndexPath: "./testdata/dockerfile/valid_bundle.Dockerfile", + }, + errStrings: []string{"Error: Value : (etcdoperator.v0.9.4) csv.Annotations not specified " + + "olm.maxOpenShiftVersion for an OCP version < 4.9. This annotation is required to " + + "prevent the user from upgrading their OCP cluster before they have installed a " + + "version of their operator which is compatible with 4.9. " + + "This bundle is using APIs which were deprecated and removed in v1.22. " + + "More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22 which are no " + + "longer supported on 4.9. Migrate the API(s) for CRD: ([\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" \"etcdrestores.etcd.database.coreos.com\"]) or use the annotation"}, + }, + { + name: "should fail when the olm annotation is set without the properties for max ocp version and has " + + "deprecated apis", + wantError: true, + args: args{ + bundleDir: "./testdata/valid_bundle_v1beta1", + imageIndexPath: "./testdata/dockerfile/valid_bundle.Dockerfile", + annotations: map[string]string{ + "olm.properties": fmt.Sprintf(`[{"type": "olm.invalid", "value": "4.9"}]`), + }, + }, + errStrings: []string{"Error: Value : (etcdoperator.v0.9.4) csv.Annotations.olm.properties with the key " + + "`olm.maxOpenShiftVersion` and a value with an OCP version which is < 4.9 is required for any operator " + + "bundle that is using APIs which were deprecated and removed in v1.22. More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22. " + + "Migrate the API(s) for CRD: ([\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" \"etcdrestores.etcd.database.coreos.com\"]) " + + "or use the annotation"}, + }, + { + name: "should fail when the olm annotation is set with a value >= 4.9 and has deprecated apis", + wantError: true, + args: args{ + bundleDir: "./testdata/valid_bundle_v1beta1", + imageIndexPath: "./testdata/dockerfile/valid_bundle.Dockerfile", + annotations: map[string]string{ + "olm.properties": fmt.Sprintf(`[{"type": "olm.maxOpenShiftVersion", "value": "4.9"}]`), + }, + }, + errStrings: []string{"Error: Value : (etcdoperator.v0.9.4) csv.Annotations.olm.properties with the key " + + "and value for olm.maxOpenShiftVersion has the OCP version value 4.9 which is >= of 4.9. " + + "This bundle is using APIs which were deprecated and removed in v1.22. More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22." + + " Migrate the API(s) for CRD: ([\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" \"etcdrestores.etcd.database.coreos.com\"]) or inform in this property an OCP version which is < 4.9"}, + }, + { + name: "should warning because is missing the index-path and has deprecated apis", + wantWarning: true, + args: args{ + bundleDir: "./testdata/valid_bundle_v1beta1", + annotations: map[string]string{ + "olm.properties": fmt.Sprintf(`[{"type": "olm.maxOpenShiftVersion", "value": "4.8"}]`), + }, + }, + warnStrings: []string{"Warning: Value : (etcdoperator.v0.9.4) please, inform the path of its index image " + + "file via the the optional key values and the key index-path to allow this validator check the labels " + + "configuration or migrate the API(s) for CRD: " + + "([\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" " + + "\"etcdrestores.etcd.database.coreos.com\"]). (e.g. index-path=./mypath/bundle.Dockerfile). " + + "This bundle is using APIs which were deprecated and removed in v1.22. " + + "More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22 "}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + // Validate the bundle object + bundle, err := manifests.GetBundleFromDir(tt.args.bundleDir) + require.NoError(t, err) + + if len(tt.args.annotations) > 0 { + bundle.CSV.Annotations = tt.args.annotations + } + + results := validateCommunityBundle(bundle, tt.args.imageIndexPath) + require.Equal(t, tt.wantWarning, len(results.Warnings) > 0) + if tt.wantWarning { + require.Equal(t, len(tt.warnStrings), len(results.Warnings)) + for _, w := range results.Warnings { + wString := w.Error() + require.Contains(t, tt.warnStrings, wString) + } + } + + require.Equal(t, tt.wantError, len(results.Errors) > 0) + if tt.wantError { + require.Equal(t, len(tt.errStrings), len(results.Errors)) + for _, err := range results.Errors { + errString := err.Error() + require.Contains(t, tt.errStrings, errString) + } + } + }) + } +} + +func Test_checkOCPLabelsWithHasDeprecatedAPIs(t *testing.T) { + type args struct { + checks CSVChecks + indexPath string + } + tests := []struct { + name string + args args + wantError bool + wantWarning bool + }{ + { + name: "should pass when has a valid value for the OCP labels", + args: args{ + indexPath: "./testdata/dockerfile/valid_bundle.Dockerfile", + }, + }, + { + name: "should fail when the OCP label is not found", + wantError: true, + args: args{ + indexPath: "./testdata/dockerfile/bundle_without_label.Dockerfile", + }, + }, + { + name: "should fail when the the index path is an invalid path", + wantError: true, + args: args{ + indexPath: "./testdata/dockerfile/invalid", + }, + }, + { + name: "should fail when the OCP label index is => 4.9", + wantError: true, + args: args{ + indexPath: "./testdata/dockerfile/invalid_bundle_equals_upper.Dockerfile", + }, + }, + { + name: "should fail when the OCP label index range is => 4.9", + wantError: true, + args: args{ + indexPath: "./testdata/dockerfile/invalid_bundle_range_upper.Dockerfile", + }, + }, + { + name: "should fail when the OCP label index range is => 4.9 with coma e.g. v4.5,4.6", + wantError: true, + args: args{ + indexPath: "./testdata/dockerfile/invalid_bundle_range_upper.Dockerfile", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checks := CommunityOperatorChecks{bundle: manifests.Bundle{}, indexImagePath: tt.args.indexPath, errs: []error{}, warns: []error{}} + + checks = checkOCPLabelsWithHasDeprecatedAPIs(checks, "CRD: ([\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" \"etcdrestores.etcd.database.coreos.com\"])") + + require.Equal(t, tt.wantWarning, len(checks.warns) > 0) + require.Equal(t, tt.wantError, len(checks.errs) > 0) + }) + } +} diff --git a/pkg/validation/internal/operatorhub.go b/pkg/validation/internal/operatorhub.go index 26345ef88..d87ade870 100644 --- a/pkg/validation/internal/operatorhub.go +++ b/pkg/validation/internal/operatorhub.go @@ -26,9 +26,6 @@ const minKubeVersionWarnMessage = "csv.Spec.minKubeVersion is not informed. It i "Otherwise, it would mean that your operator project can be distributed and installed in any cluster version " + "available, which is not necessarily the case for all projects." -const crdv1beta1DeprecationMsg = "apiextensions.k8s.io/v1beta1, kind=CustomResourceDefinitions was deprecated in " + - "Kubernetes v1.16 and will be removed in v1.22 in favor of v1" - // OperatorHubValidator validates the bundle manifests against the required criteria to publish // the projects on OperatorHub.io. // @@ -207,21 +204,18 @@ func validateHubDeprecatedAPIS(bundle *manifests.Bundle, versionProvided string) // - if minKubeVersion any version defined it means that we are considering install // in any upper version from that where the check is always applied if !isVersionProvided || semVerVersionProvided.GE(semVerk8sVerV1betav1Deprecated) { - if len(bundle.V1beta1CRDs) > 0 { - var crdApiNames []string - for _, obj := range bundle.V1beta1CRDs { - crdApiNames = append(crdApiNames, obj.Name) - } - + deprecatedAPIs := getRemovedAPIsOn1_22From(bundle) + if len(deprecatedAPIs) > 0 { + deprecatedAPIsMessage := generateMessageWithDeprecatedAPIs(deprecatedAPIs) // isUnsupported is true only if the key/value OR minKubeVersion were informed and are >= 1.22 isUnsupported := semVerVersionProvided.GE(semVerK8sVerV1betav1Unsupported) || semverMinKube.GE(semVerK8sVerV1betav1Unsupported) // We only raise an error when the version >= 1.22 was informed via // the k8s key/value option or is specifically defined in the CSV if isUnsupported { - errs = append(errs, fmt.Errorf("%s: %+q should be migrated", crdv1beta1DeprecationMsg, crdApiNames)) + errs = append(errs, fmt.Errorf("this bundle is %s. Migrate the API(s) for %s", k8sApiDeprecatedInfo, deprecatedAPIsMessage)) } else { - warns = append(warns, fmt.Errorf("%s: %+q should be migrated", crdv1beta1DeprecationMsg, crdApiNames)) + warns = append(warns, fmt.Errorf("this bundle is %s. Migrate the API(s) for %s", k8sApiDeprecatedInfo, deprecatedAPIsMessage)) } } } diff --git a/pkg/validation/internal/operatorhub_test.go b/pkg/validation/internal/operatorhub_test.go index 57d583fab..0ea1d9b35 100644 --- a/pkg/validation/internal/operatorhub_test.go +++ b/pkg/validation/internal/operatorhub_test.go @@ -375,7 +375,7 @@ func TestValidateHubDeprecatedAPIS(t *testing.T) { directory: "./testdata/valid_bundle_v1beta1", }, wantWarning: true, - warnStrings: []string{crdv1beta1DeprecationMsg + ": [\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" \"etcdrestores.etcd.database.coreos.com\"] should be migrated"}, + warnStrings: []string{"this bundle is " + k8sApiDeprecatedInfo + ". Migrate the API(s) for CRD: ([\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" \"etcdrestores.etcd.database.coreos.com\"])"}, }, { name: "should not return a warning or error when has minKubeVersion but the k8sVersion informed is <= 1.15", @@ -395,7 +395,7 @@ func TestValidateHubDeprecatedAPIS(t *testing.T) { directory: "./testdata/valid_bundle_v1beta1", }, wantError: true, - errStrings: []string{crdv1beta1DeprecationMsg + ": [\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" \"etcdrestores.etcd.database.coreos.com\"] should be migrated"}, + errStrings: []string{"this bundle is " + k8sApiDeprecatedInfo + ". Migrate the API(s) for CRD: ([\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" \"etcdrestores.etcd.database.coreos.com\"])"}, wantWarning: true, warnStrings: []string{"checking APIs against Kubernetes version : 1.22"}, }, @@ -418,7 +418,7 @@ func TestValidateHubDeprecatedAPIS(t *testing.T) { wantError: true, wantWarning: true, errStrings: []string{"unable to use csv.Spec.MinKubeVersion to verify the CRD/Webhook apis because it has an invalid value: invalid"}, - warnStrings: []string{crdv1beta1DeprecationMsg + ": [\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" \"etcdrestores.etcd.database.coreos.com\"] should be migrated"}, + warnStrings: []string{"this bundle is " + k8sApiDeprecatedInfo + ". Migrate the API(s) for CRD: ([\"etcdbackups.etcd.database.coreos.com\" \"etcdclusters.etcd.database.coreos.com\" \"etcdrestores.etcd.database.coreos.com\"])"}, }, } for _, tt := range tests { diff --git a/pkg/validation/internal/removed_apis.go b/pkg/validation/internal/removed_apis.go new file mode 100644 index 000000000..a2d010fef --- /dev/null +++ b/pkg/validation/internal/removed_apis.go @@ -0,0 +1,98 @@ +package internal + +import ( + "fmt" + + "github.com/operator-framework/api/pkg/manifests" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// k8sApiDeprecatedInfo define the message which will appears by default when is possible to identify that the project has deprecated API(s) +const k8sApiDeprecatedInfo = "using APIs which were deprecated and removed in v1.22. More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22" + +// OCP version where the apis v1beta1 is no longer supported +const ocpVerV1beta1Unsupported = "4.9" + +// generateMessageWithDeprecatedAPIs will return a list with the kind and the name +// of the resource which were found and required to be upgraded +func generateMessageWithDeprecatedAPIs(deprecatedAPIs map[string][]string) string { + msg := "" + count := 0 + for k, v := range deprecatedAPIs { + if count == len(deprecatedAPIs)-1 { + msg = msg + fmt.Sprintf("%s: (%+q)", k, v) + } else { + msg = msg + fmt.Sprintf("%s: (%+q),", k, v) + } + } + return msg +} + +// todo: we need to improve this code since we ought to map the kinds, apis and ocp/k8s versions +// where them are no longer supported ( removed ) instead of have this fixed in this way. + +// getRemovedAPIsOn1_22From return the list of resources which were deprecated +// and are no longer be supported in 1.22. More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22 +func getRemovedAPIsOn1_22From(bundle *manifests.Bundle) map[string][]string { + deprecatedAPIs := make(map[string][]string) + if len(bundle.V1beta1CRDs) > 0 { + var crdApiNames []string + for _, obj := range bundle.V1beta1CRDs { + crdApiNames = append(crdApiNames, obj.Name) + } + deprecatedAPIs["CRD"] = crdApiNames + } + + for _, obj := range bundle.Objects { + switch u := obj.GetObjectKind().(type) { + case *unstructured.Unstructured: + switch u.GetAPIVersion() { + case "scheduling.k8s.io/v1beta1": + if u.GetKind() == PriorityClassKind { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "rbac.authorization.k8s.io/v1beta1": + if u.GetKind() == RoleKind || u.GetKind() == "ClusterRoleBinding" || u.GetKind() == "RoleBinding" || u.GetKind() == ClusterRoleKind { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "apiregistration.k8s.io/v1beta1": + if u.GetKind() == "APIService" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "authentication.k8s.io/v1beta1": + if u.GetKind() == "TokenReview" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "authorization.k8s.io/v1beta1": + if u.GetKind() == "LocalSubjectAccessReview" || u.GetKind() == "SelfSubjectAccessReview" || u.GetKind() == "SubjectAccessReview" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "admissionregistration.k8s.io/v1beta1": + if u.GetKind() == "MutatingWebhookConfiguration" || u.GetKind() == "ValidatingWebhookConfiguration" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "coordination.k8s.io/v1beta1": + if u.GetKind() == "Lease" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "extensions/v1beta1": + if u.GetKind() == "Ingress" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "networking.k8s.io/v1beta1": + if u.GetKind() == "Ingress" || u.GetKind() == "IngressClass" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "storage.k8s.io/v1beta1": + if u.GetKind() == "CSIDriver" || u.GetKind() == "CSINode" || u.GetKind() == "StorageClass" || u.GetKind() == "VolumeAttachment" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "certificates.k8s.io/v1beta1": + if u.GetKind() == "CertificateSigningRequest" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + } + } + } + return deprecatedAPIs +} diff --git a/pkg/validation/internal/removed_apis_test.go b/pkg/validation/internal/removed_apis_test.go new file mode 100644 index 000000000..a3bd71c68 --- /dev/null +++ b/pkg/validation/internal/removed_apis_test.go @@ -0,0 +1,65 @@ +package internal + +import ( + "github.com/operator-framework/api/pkg/manifests" + "github.com/stretchr/testify/require" + "reflect" + "testing" +) + +func Test_getDeprecatedAPIs(t *testing.T) { + + // Mock the expected result for ./testdata/valid_bundle_v1beta1 + crdMock := make(map[string][]string) + crdMock["CRD"] = []string{"etcdbackups.etcd.database.coreos.com", "etcdclusters.etcd.database.coreos.com", "etcdrestores.etcd.database.coreos.com"} + + // Mock the expected result for ./testdata/valid_bundle_with_v1beta1_clusterrole + otherKindsMock := make(map[string][]string) + otherKindsMock[ClusterRoleKind] = []string{"memcached-operator-metrics-reader"} + otherKindsMock[PriorityClassKind] = []string{"super-priority"} + otherKindsMock[RoleKind] = []string{"memcached-role"} + otherKindsMock["MutatingWebhookConfiguration"] = []string{"mutating-webhook-configuration"} + + type args struct { + bundleDir string + } + tests := []struct { + name string + args args + want map[string][]string + }{ + { + name: "should return an empty map when no deprecated apis are found", + args: args{ + bundleDir: "./testdata/valid_bundle_v1", + }, + want: map[string][]string{}, + }, + { + name: "should return map with CRDs when this kind of resource is deprecated", + args: args{ + bundleDir: "./testdata/valid_bundle_v1beta1", + }, + want: crdMock, + }, + { + name: "should return map with others kinds which are deprecated", + args: args{ + bundleDir: "./testdata/bundle_with_deprecated_resources", + }, + want: otherKindsMock, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + // Validate the bundle object + bundle, err := manifests.GetBundleFromDir(tt.args.bundleDir) + require.NoError(t, err) + + if got := getRemovedAPIsOn1_22From(bundle); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getRemovedAPIsOn1_22From() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/cache.example.com_memcacheds.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/cache.example.com_memcacheds.yaml new file mode 100644 index 000000000..a8ea3eb8d --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/cache.example.com_memcacheds.yaml @@ -0,0 +1,66 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: memcacheds.cache.example.com +spec: + group: cache.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Memcached is the Schema for the memcacheds API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: MemcachedSpec defines the desired state of Memcached + properties: + foo: + description: Foo is an example field of Memcached. Edit memcached_types.go + to remove/update + type: string + size: + description: Size defines the number of Memcached instances + format: int32 + type: integer + type: object + status: + description: MemcachedStatus defines the observed state of Memcached + properties: + nodes: + description: Nodes store the name of the pods which are running Memcached + instances + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager-metrics-monitor_monitoring.coreos.com_v1_servicemonitor.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager-metrics-monitor_monitoring.coreos.com_v1_servicemonitor.yaml new file mode 100644 index 000000000..2d93bb41b --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager-metrics-monitor_monitoring.coreos.com_v1_servicemonitor.yaml @@ -0,0 +1,17 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: memcached-operator-controller-manager-metrics-monitor +spec: + endpoints: + - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + path: /metrics + port: https + scheme: https + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager-metrics-service_v1_service.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager-metrics-service_v1_service.yaml new file mode 100644 index 000000000..157a0cefa --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager-metrics-service_v1_service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + control-plane: controller-manager + name: memcached-operator-controller-manager-metrics-service +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager_v1_serviceaccount.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager_v1_serviceaccount.yaml new file mode 100644 index 000000000..adc7cf0c6 --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager_v1_serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + creationTimestamp: null + name: memcached-operator-controller-manager diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-manager-config_v1_configmap.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-manager-config_v1_configmap.yaml new file mode 100644 index 000000000..ea42c5994 --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-manager-config_v1_configmap.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +data: + controller_manager_config.yaml: | + apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 + kind: ControllerManagerConfig + health: + healthProbeBindAddress: :8081 + metrics: + bindAddress: 127.0.0.1:8080 + webhook: + port: 9443 + leaderElection: + leaderElect: true + resourceName: 86f835c3.example.com +kind: ConfigMap +metadata: + name: memcached-operator-manager-config diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml new file mode 100644 index 000000000..3b5e526fd --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -0,0 +1,10 @@ +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: memcached-operator-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-webhook-service_v1_service.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-webhook-service_v1_service.yaml new file mode 100644 index 000000000..4c9ef443f --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-webhook-service_v1_service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + name: memcached-operator-webhook-service +spec: + ports: + - port: 443 + targetPort: 9443 + selector: + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator.clusterserviceversion.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator.clusterserviceversion.yaml new file mode 100644 index 000000000..8d9dd2847 --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator.clusterserviceversion.yaml @@ -0,0 +1,259 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "cache.example.com/v1alpha1", + "kind": "Memcached", + "metadata": { + "name": "memcached-sample" + }, + "spec": { + "size": 1 + } + } + ] + capabilities: Basic Install + name: memcached-operator.v0.0.1 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Memcached is the Schema for the memcacheds API + displayName: Memcached + kind: Memcached + name: memcacheds.cache.example.com + version: v1alpha1 + description: Memcached Operator description. TODO. + displayName: Memcached Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - cache.example.com + resources: + - memcacheds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - cache.example.com + resources: + - memcacheds/finalizers + verbs: + - update + - apiGroups: + - cache.example.com + resources: + - memcacheds/status + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: memcached-operator-controller-manager + deployments: + - name: memcached-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + strategy: {} + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=10 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + resources: {} + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: quay.io/example/memcached-operator:v0.0.1 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + securityContext: + allowPrivilegeEscalation: false + securityContext: + runAsNonRoot: true + serviceAccountName: memcached-operator-controller-manager + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: memcached-operator-controller-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - memcached-operator + links: + - name: Memcached Operator + url: https://memcached-operator.domain + maintainers: + - email: your@email.com + name: Maintainer Name + maturity: alpha + provider: + name: Provider Name + url: https://your.domain + version: 0.0.1 + webhookdefinitions: + - admissionReviewVersions: + - v1 + - v1beta1 + containerPort: 443 + deploymentName: memcached-operator-controller-manager + failurePolicy: Fail + generateName: vmemcached.kb.io + rules: + - apiGroups: + - cache.example.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-cache-example-com-v1alpha1-memcached + - admissionReviewVersions: + - v1 + - v1beta1 + containerPort: 443 + deploymentName: memcached-operator-controller-manager + failurePolicy: Fail + generateName: mmemcached.kb.io + rules: + - apiGroups: + - cache.example.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + sideEffects: None + targetPort: 9443 + type: MutatingAdmissionWebhook + webhookPath: /mutate-cache-example-com-v1alpha1-memcached diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/policy.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/policy.yaml new file mode 100644 index 000000000..94586ef91 --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/policy.yaml @@ -0,0 +1,6 @@ +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + name: busybox-pdb +spec: + maxUnavailable: 0 diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/priorityclass.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/priorityclass.yaml new file mode 100644 index 000000000..3bf76419e --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/priorityclass.yaml @@ -0,0 +1,6 @@ +apiVersion: scheduling.k8s.io/v1beta1 +kind: PriorityClass +metadata: + name: super-priority +value: 1000 +globalDefault: true diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/role.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/role.yaml new file mode 100644 index 000000000..34c2005b9 --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/role.yaml @@ -0,0 +1,10 @@ +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: Role +metadata: + creationTimestamp: null + name: memcached-role +rules: +- nonResourceURLs: + - /metrics + verbs: + - get diff --git a/pkg/validation/internal/testdata/bundle_with_deprecated_resources/webhook.yaml b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/webhook.yaml new file mode 100644 index 000000000..3d7f165c9 --- /dev/null +++ b/pkg/validation/internal/testdata/bundle_with_deprecated_resources/webhook.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + creationTimestamp: null + name: mutating-webhook-configuration +webhooks: + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-cache-example-com-v1alpha1-memcached + failurePolicy: Fail + name: mmemcached.kb.io + rules: + - apiGroups: + - cache.example.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-cache-example-com-v1alpha1-memcached + failurePolicy: Fail + name: vmemcached.kb.io + rules: + - apiGroups: + - cache.example.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds \ No newline at end of file diff --git a/pkg/validation/internal/testdata/dockerfile/bundle_without_label.Dockerfile b/pkg/validation/internal/testdata/dockerfile/bundle_without_label.Dockerfile new file mode 100644 index 000000000..432577b95 --- /dev/null +++ b/pkg/validation/internal/testdata/dockerfile/bundle_without_label.Dockerfile @@ -0,0 +1,17 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator +LABEL operators.operatorframework.io.bundle.channels.v1=alpha + +# Labels for testing. +LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 +LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ + +# Copy files to locations specified by labels. +COPY bundle/manifests /manifests/ +COPY bundle/metadata /metadata/ +COPY bundle/tests/scorecard /tests/scorecard/ diff --git a/pkg/validation/internal/testdata/dockerfile/invalid_bundle_equals_upper.Dockerfile b/pkg/validation/internal/testdata/dockerfile/invalid_bundle_equals_upper.Dockerfile new file mode 100644 index 000000000..46cf02769 --- /dev/null +++ b/pkg/validation/internal/testdata/dockerfile/invalid_bundle_equals_upper.Dockerfile @@ -0,0 +1,18 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator +LABEL operators.operatorframework.io.bundle.channels.v1=alpha +LABEL com.redhat.openshift.versions="=v4.9" + +# Labels for testing. +LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 +LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ + +# Copy files to locations specified by labels. +COPY bundle/manifests /manifests/ +COPY bundle/metadata /metadata/ +COPY bundle/tests/scorecard /tests/scorecard/ diff --git a/pkg/validation/internal/testdata/dockerfile/invalid_bundle_range_upper.Dockerfile b/pkg/validation/internal/testdata/dockerfile/invalid_bundle_range_upper.Dockerfile new file mode 100644 index 000000000..470d5cee3 --- /dev/null +++ b/pkg/validation/internal/testdata/dockerfile/invalid_bundle_range_upper.Dockerfile @@ -0,0 +1,18 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator +LABEL operators.operatorframework.io.bundle.channels.v1=alpha +LABEL com.redhat.openshift.versions="v4.6-v4.9" + +# Labels for testing. +LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 +LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ + +# Copy files to locations specified by labels. +COPY bundle/manifests /manifests/ +COPY bundle/metadata /metadata/ +COPY bundle/tests/scorecard /tests/scorecard/ diff --git a/pkg/validation/internal/testdata/dockerfile/invalid_bundle_range_upper_coma.Dockerfile b/pkg/validation/internal/testdata/dockerfile/invalid_bundle_range_upper_coma.Dockerfile new file mode 100644 index 000000000..5c98da76a --- /dev/null +++ b/pkg/validation/internal/testdata/dockerfile/invalid_bundle_range_upper_coma.Dockerfile @@ -0,0 +1,18 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator +LABEL operators.operatorframework.io.bundle.channels.v1=alpha +LABEL com.redhat.openshift.versions="v4.6,v4.7" + +# Labels for testing. +LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 +LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ + +# Copy files to locations specified by labels. +COPY bundle/manifests /manifests/ +COPY bundle/metadata /metadata/ +COPY bundle/tests/scorecard /tests/scorecard/ diff --git a/pkg/validation/internal/testdata/dockerfile/valid_bundle.Dockerfile b/pkg/validation/internal/testdata/dockerfile/valid_bundle.Dockerfile new file mode 100644 index 000000000..03d1ef427 --- /dev/null +++ b/pkg/validation/internal/testdata/dockerfile/valid_bundle.Dockerfile @@ -0,0 +1,18 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator +LABEL operators.operatorframework.io.bundle.channels.v1=alpha +LABEL com.redhat.openshift.versions="v4.6-v4.8" + +# Labels for testing. +LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 +LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ + +# Copy files to locations specified by labels. +COPY bundle/manifests /manifests/ +COPY bundle/metadata /metadata/ +COPY bundle/tests/scorecard /tests/scorecard/ diff --git a/pkg/validation/internal/testdata/valid_bundle_v1/cache.example.com_memcacheds.yaml b/pkg/validation/internal/testdata/valid_bundle_v1/cache.example.com_memcacheds.yaml new file mode 100644 index 000000000..a8ea3eb8d --- /dev/null +++ b/pkg/validation/internal/testdata/valid_bundle_v1/cache.example.com_memcacheds.yaml @@ -0,0 +1,66 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: memcacheds.cache.example.com +spec: + group: cache.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Memcached is the Schema for the memcacheds API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: MemcachedSpec defines the desired state of Memcached + properties: + foo: + description: Foo is an example field of Memcached. Edit memcached_types.go + to remove/update + type: string + size: + description: Size defines the number of Memcached instances + format: int32 + type: integer + type: object + status: + description: MemcachedStatus defines the observed state of Memcached + properties: + nodes: + description: Nodes store the name of the pods which are running Memcached + instances + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager-metrics-monitor_monitoring.coreos.com_v1_servicemonitor.yaml b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager-metrics-monitor_monitoring.coreos.com_v1_servicemonitor.yaml new file mode 100644 index 000000000..2d93bb41b --- /dev/null +++ b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager-metrics-monitor_monitoring.coreos.com_v1_servicemonitor.yaml @@ -0,0 +1,17 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: memcached-operator-controller-manager-metrics-monitor +spec: + endpoints: + - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + path: /metrics + port: https + scheme: https + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager-metrics-service_v1_service.yaml b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager-metrics-service_v1_service.yaml new file mode 100644 index 000000000..157a0cefa --- /dev/null +++ b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager-metrics-service_v1_service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + control-plane: controller-manager + name: memcached-operator-controller-manager-metrics-service +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager_v1_serviceaccount.yaml b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager_v1_serviceaccount.yaml new file mode 100644 index 000000000..adc7cf0c6 --- /dev/null +++ b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager_v1_serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + creationTimestamp: null + name: memcached-operator-controller-manager diff --git a/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-manager-config_v1_configmap.yaml b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-manager-config_v1_configmap.yaml new file mode 100644 index 000000000..ea42c5994 --- /dev/null +++ b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-manager-config_v1_configmap.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +data: + controller_manager_config.yaml: | + apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 + kind: ControllerManagerConfig + health: + healthProbeBindAddress: :8081 + metrics: + bindAddress: 127.0.0.1:8080 + webhook: + port: 9443 + leaderElection: + leaderElect: true + resourceName: 86f835c3.example.com +kind: ConfigMap +metadata: + name: memcached-operator-manager-config diff --git a/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml new file mode 100644 index 000000000..42a2ae6ac --- /dev/null +++ b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -0,0 +1,10 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: memcached-operator-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get diff --git a/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-webhook-service_v1_service.yaml b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-webhook-service_v1_service.yaml new file mode 100644 index 000000000..4c9ef443f --- /dev/null +++ b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-webhook-service_v1_service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + name: memcached-operator-webhook-service +spec: + ports: + - port: 443 + targetPort: 9443 + selector: + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator.clusterserviceversion.yaml b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator.clusterserviceversion.yaml new file mode 100644 index 000000000..8d9dd2847 --- /dev/null +++ b/pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator.clusterserviceversion.yaml @@ -0,0 +1,259 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "cache.example.com/v1alpha1", + "kind": "Memcached", + "metadata": { + "name": "memcached-sample" + }, + "spec": { + "size": 1 + } + } + ] + capabilities: Basic Install + name: memcached-operator.v0.0.1 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Memcached is the Schema for the memcacheds API + displayName: Memcached + kind: Memcached + name: memcacheds.cache.example.com + version: v1alpha1 + description: Memcached Operator description. TODO. + displayName: Memcached Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - cache.example.com + resources: + - memcacheds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - cache.example.com + resources: + - memcacheds/finalizers + verbs: + - update + - apiGroups: + - cache.example.com + resources: + - memcacheds/status + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: memcached-operator-controller-manager + deployments: + - name: memcached-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + strategy: {} + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=10 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + resources: {} + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: quay.io/example/memcached-operator:v0.0.1 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + securityContext: + allowPrivilegeEscalation: false + securityContext: + runAsNonRoot: true + serviceAccountName: memcached-operator-controller-manager + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: memcached-operator-controller-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - memcached-operator + links: + - name: Memcached Operator + url: https://memcached-operator.domain + maintainers: + - email: your@email.com + name: Maintainer Name + maturity: alpha + provider: + name: Provider Name + url: https://your.domain + version: 0.0.1 + webhookdefinitions: + - admissionReviewVersions: + - v1 + - v1beta1 + containerPort: 443 + deploymentName: memcached-operator-controller-manager + failurePolicy: Fail + generateName: vmemcached.kb.io + rules: + - apiGroups: + - cache.example.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-cache-example-com-v1alpha1-memcached + - admissionReviewVersions: + - v1 + - v1beta1 + containerPort: 443 + deploymentName: memcached-operator-controller-manager + failurePolicy: Fail + generateName: mmemcached.kb.io + rules: + - apiGroups: + - cache.example.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + sideEffects: None + targetPort: 9443 + type: MutatingAdmissionWebhook + webhookPath: /mutate-cache-example-com-v1alpha1-memcached diff --git a/pkg/validation/testdata/dockerfile/valid_bundle.Dockerfile b/pkg/validation/testdata/dockerfile/valid_bundle.Dockerfile new file mode 100644 index 000000000..03d1ef427 --- /dev/null +++ b/pkg/validation/testdata/dockerfile/valid_bundle.Dockerfile @@ -0,0 +1,18 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator +LABEL operators.operatorframework.io.bundle.channels.v1=alpha +LABEL com.redhat.openshift.versions="v4.6-v4.8" + +# Labels for testing. +LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 +LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ + +# Copy files to locations specified by labels. +COPY bundle/manifests /manifests/ +COPY bundle/metadata /metadata/ +COPY bundle/tests/scorecard /tests/scorecard/ diff --git a/pkg/validation/testdata/valid_bundle/etcdoperator.v0.9.4.clusterserviceversion.yaml b/pkg/validation/testdata/valid_bundle/etcdoperator.v0.9.4.clusterserviceversion.yaml index 56f414d66..3d4388aaf 100644 --- a/pkg/validation/testdata/valid_bundle/etcdoperator.v0.9.4.clusterserviceversion.yaml +++ b/pkg/validation/testdata/valid_bundle/etcdoperator.v0.9.4.clusterserviceversion.yaml @@ -22,6 +22,7 @@ metadata: description: Create and maintain highly-available etcd clusters on Kubernetes repository: https://github.com/coreos/etcd-operator tectonic-visibility: ocs + olm.properties: '[{"type": "olm.maxOpenShiftVersion", "value": "4.8"}]' name: etcdoperator.v0.9.4 namespace: placeholder spec: diff --git a/pkg/validation/testdata/valid_package/0.9.2/etcdoperator.v0.9.2.clusterserviceversion.yaml b/pkg/validation/testdata/valid_package/0.9.2/etcdoperator.v0.9.2.clusterserviceversion.yaml index efc209da6..3f8dc25af 100644 --- a/pkg/validation/testdata/valid_package/0.9.2/etcdoperator.v0.9.2.clusterserviceversion.yaml +++ b/pkg/validation/testdata/valid_package/0.9.2/etcdoperator.v0.9.2.clusterserviceversion.yaml @@ -19,6 +19,7 @@ metadata: categories: Database description: Creates and maintain highly-available etcd clusters on Kubernetes tectonic-visibility: ocs + olm.properties: '[{"type": "olm.maxOpenShiftVersion", "value": "4.8"}]' name: etcdoperator.v0.9.2 namespace: placeholder spec: diff --git a/pkg/validation/testdata/valid_package/0.9.4/etcdoperator.v0.9.4.clusterserviceversion.yaml b/pkg/validation/testdata/valid_package/0.9.4/etcdoperator.v0.9.4.clusterserviceversion.yaml index 56f414d66..3d4388aaf 100644 --- a/pkg/validation/testdata/valid_package/0.9.4/etcdoperator.v0.9.4.clusterserviceversion.yaml +++ b/pkg/validation/testdata/valid_package/0.9.4/etcdoperator.v0.9.4.clusterserviceversion.yaml @@ -22,6 +22,7 @@ metadata: description: Create and maintain highly-available etcd clusters on Kubernetes repository: https://github.com/coreos/etcd-operator tectonic-visibility: ocs + olm.properties: '[{"type": "olm.maxOpenShiftVersion", "value": "4.8"}]' name: etcdoperator.v0.9.4 namespace: placeholder spec: diff --git a/pkg/validation/validation.go b/pkg/validation/validation.go index f33a7af47..4084a8a7a 100644 --- a/pkg/validation/validation.go +++ b/pkg/validation/validation.go @@ -38,6 +38,10 @@ var ObjectValidator = internal.ObjectValidator // OperatorGroupValidator implements Validator to validate OperatorGroup manifests var OperatorGroupValidator = internal.OperatorGroupValidator +// CommunityOperatorValidator implements Validator to validate bundle objects +// for the Community Operator requirements. +var CommunityOperatorValidator = internal.CommunityOperatorValidator + // AllValidators implements Validator to validate all Operator manifest types. var AllValidators = interfaces.Validators{ PackageManifestValidator, @@ -47,6 +51,7 @@ var AllValidators = interfaces.Validators{ OperatorHubValidator, ObjectValidator, OperatorGroupValidator, + CommunityOperatorValidator, } var DefaultBundleValidators = interfaces.Validators{ diff --git a/pkg/validation/validation_test.go b/pkg/validation/validation_test.go index 281474436..56cd96a5d 100644 --- a/pkg/validation/validation_test.go +++ b/pkg/validation/validation_test.go @@ -13,7 +13,9 @@ func TestValidateSuccess(t *testing.T) { bundle, err := manifests.GetBundleFromDir("./testdata/valid_bundle") require.NoError(t, err) - results := AllValidators.Validate(bundle) + results := AllValidators.Validate(bundle, map[string]string{ + "index-path": "./testdata/dockerfile/valid_bundle.Dockerfile"}) + for _, result := range results { require.Equal(t, false, result.HasError()) }