diff --git a/README.md b/README.md index 611e36872..086021c17 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,50 @@ Following an example. return nonEmptyResults ``` +#### Passing optional key/values to the validators + +Validators may accept pass optional key/values which will be used in the checks made. +These values are global and if the key/value pair provided is not used for 1 or more +validators called then, it is ignored. + +The following example calls `AlphaDeprecatedAPIsValidator`, which allows us to inform +the K8s version intended to publish the OLM Bundle: + +```go + validators := apivalidation.DefaultBundleValidators + validators = validators.WithValidators(apivalidation.OperatorHubValidator) + validators = validators.WithValidators(apivalidation.ObjectValidator) + validators = validators.WithValidators(apivalidation.AlphaDeprecatedAPIsValidator) + validators = validators.WithValidators(apivalidation.GoodPracticesValidator) + + objs := auditBundle.Bundle.ObjectsToValidate() + + // Pass the --optional-values. e.g. --optional-values="k8s-version=1.22" + // or --optional-values="image-path=bundle.Dockerfile" + var optionalValues = map[string]string{ + "k8s-version":"1.22", + } + objs = append(objs, optionalValues) + + results := validators.Validate(objs...) + nonEmptyResults := []errors.ManifestResult{} + + for _, result := range results { + if result.HasError() || result.HasWarn() { + nonEmptyResults = append(nonEmptyResults, result) + } + } +``` + +**How the optional key/values are informed via the CLI?** + +By using [Operator-SDK][sdk] you can pass a list of key/values via the flag `--optional-values`, for example, +to validate that your manifests can work with a Kubernetes cluster of a particular version using the `k8s-version`: + +```sh +$ operator-sdk bundle validate ./bundle --select-optional suite=operatorframework --optional-values=k8s-version=1.22 +``` + ## API CLI Usage You can install the `operator-verify` tool from source using: diff --git a/pkg/validation/internal/operatorhub.go b/pkg/validation/internal/operatorhub.go index f265b0c13..48456b308 100644 --- a/pkg/validation/internal/operatorhub.go +++ b/pkg/validation/internal/operatorhub.go @@ -11,7 +11,7 @@ import ( "regexp" "strings" - semver "github.com/blang/semver/v4" + "github.com/blang/semver/v4" "github.com/operator-framework/api/pkg/manifests" "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/operator-framework/api/pkg/validation/errors" @@ -158,6 +158,10 @@ var validCategories = map[string]struct{}{ "Streaming & Messaging": {}, } +const minKubeVersionWarnMessage = "csv.Spec.minKubeVersion is not informed. It is recommended you provide this information. " + + "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." + func validateOperatorHub(objs ...interface{}) (results []errors.ManifestResult) { // Obtain the k8s version if informed via the objects an optional diff --git a/pkg/validation/internal/removed_apis.go b/pkg/validation/internal/removed_apis.go index 9377b50b0..6cefe95b2 100644 --- a/pkg/validation/internal/removed_apis.go +++ b/pkg/validation/internal/removed_apis.go @@ -3,30 +3,43 @@ package internal import ( "fmt" "github.com/blang/semver" - "github.com/operator-framework/api/pkg/manifests" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/operator-framework/api/pkg/validation/errors" interfaces "github.com/operator-framework/api/pkg/validation/interfaces" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sort" ) // k8sVersionKey defines the key which can be used by its consumers // to inform what is the K8S version that should be used to do the tests against. const k8sVersionKey = "k8s-version" -const minKubeVersionWarnMessage = "csv.Spec.minKubeVersion is not informed. It is recommended you provide this information. " + - "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." - -// K8s version where the apis v1betav1 is no longer supported -const k8sVerV1betav1Unsupported = "1.22.0" +// DeprecateMessage defines the content of the message that will be raised as an error or warning +// when the removed apis are found +const DeprecateMessage = "this bundle is using APIs which were deprecated and removed in v%v.%v. " + + "More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v%v-%v. " + + "Migrate the API(s) for %s" -// K8s version where the apis v1betav1 was deprecated -const k8sVerV1betav1Deprecated = "1.16.0" +// K8sVersionsSupportedByValidator defines the k8s versions which this validator is implemented to +// perform the checks +var K8sVersionsSupportedByValidator = []string{"1.22.0", "1.25.0", "1.26.0"} -// AlphaDeprecatedAPIsValidator validates if the bundles is using versions API version which are deprecate or -// removed in specific Kubernetes versions informed via optional key value `k8s-version`. +// AlphaDeprecatedAPIsValidator implements Validator to validate bundle objects +// for API deprecation requirements. +// +// Note that this validator looks at the manifests. If any removed APIs for the mapped k8s versions are found, +// it raises a warning. +// +// This validator only raises an error when the deprecated API found is removed in the specified k8s +// version informed via the optional key `k8s-version`. +// +// The K8s versions supported and checks are: +// +// - 1.22 : https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22 +// +// - 1.25 : https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-25 +// +// - 1.26 : https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-26 var AlphaDeprecatedAPIsValidator interfaces.Validator = interfaces.ValidatorFunc(validateDeprecatedAPIsValidator) func validateDeprecatedAPIsValidator(objs ...interface{}) (results []errors.ManifestResult) { @@ -54,13 +67,14 @@ func validateDeprecatedAPIsValidator(objs ...interface{}) (results []errors.Mani } func validateDeprecatedAPIs(bundle *manifests.Bundle, k8sVersion string) errors.ManifestResult { - result := errors.ManifestResult{Name: bundle.Name} - + result := errors.ManifestResult{} if bundle == nil { result.Add(errors.ErrInvalidBundle("Bundle is nil", nil)) return result } + result.Name = bundle.Name + if bundle.CSV == nil { result.Add(errors.ErrInvalidBundle("Bundle csv is nil", bundle.Name)) return result @@ -78,34 +92,22 @@ func validateDeprecatedAPIs(bundle *manifests.Bundle, k8sVersion string) errors. } // validateDeprecatedAPIS will check if the operator bundle is using a deprecated or no longer supported k8s api -// Note if the k8s was informed via "k8s=1.22" it will be used. Otherwise, we will use the minKubeVersion in -// the CSV to do the checks. So, the criteria is >=minKubeVersion. By last, if the minKubeVersion is not provided +// Note if the k8s version was informed via "k8s-version" optional key it will be used. Otherwise, we will use the minKubeVersion in +// the CSV to do the checks. So, the criteria is >=minKubeVersion. Lastly, if the minKubeVersion is not provided // then, we should consider the operator bundle is intend to work well in any Kubernetes version. // Then, it means that: -//--optional-values="k8s-version=value" flag with a value => 1.16 <= 1.22 the validator will return result as warning. -//--optional-values="k8s-version=value" flag with a value => 1.22 the validator will return result as error. -//minKubeVersion >= 1.22 return the error result. -//minKubeVersion empty returns a warning since it would mean the same of allow install in any supported version +// - --optional-values="k8s-version=value" flag with a value <= unsupportedAPIVersion the validator will return result as warning. +// - --optional-values="k8s-version=value" flag with a value => unsupportedAPIVersion the validator will return result as error. +// - minKubeVersion >= unsupportedAPIVersion return the error result. +// - minKubeVersion empty returns a warning since it would mean the same of allow in any supported version func validateDeprecatedAPIS(bundle *manifests.Bundle, versionProvided string) (errs, warns []error) { - - // semver of the K8s version where the apis v1betav1 is no longer supported to allow us compare - semVerK8sVerV1betav1Unsupported := semver.MustParse(k8sVerV1betav1Unsupported) - // semver of the K8s version where the apis v1betav1 is deprecated to allow us compare - semVerk8sVerV1betav1Deprecated := semver.MustParse(k8sVerV1betav1Deprecated) // isVersionProvided defines if the k8s version to test against was or not informed isVersionProvided := len(versionProvided) > 0 + // semVerVersionProvided -- converts the k8s version informed in semver + semVerVersionProvided, _ := semver.ParseTolerant(versionProvided) - // Transform the key/option versionProvided in semver Version to compare - var semVerVersionProvided semver.Version - if isVersionProvided { - var err error - semVerVersionProvided, err = semver.ParseTolerant(versionProvided) - if err != nil { - errs = append(errs, fmt.Errorf("invalid value informed via the k8s key option : %s", versionProvided)) - } else { - // we might want to return it as info instead of warning in the future. - warns = append(warns, fmt.Errorf("checking APIs against Kubernetes version : %s", versionProvided)) - } + if err := verifyK8sVersionInformed(versionProvided); err != nil && isVersionProvided { + errs = append(errs, err) } // Transform the spec minKubeVersion in semver Version to compare @@ -118,39 +120,86 @@ func validateDeprecatedAPIS(bundle *manifests.Bundle, versionProvided string) (e } } - // if the k8s value was informed and it is >=1.16 we should check - // if the k8s value was not informed we also should check since the - // check should occurs with any minKubeVersion value: - // - if minKubeVersion empty then means that the project can be installed in any version - // - 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) { - 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 + // Check the bundle with all k8s versions implemented + for _, v := range K8sVersionsSupportedByValidator { + k8sVersionToCheck := semver.MustParse(v) + errs, warns = checkRemovedAPIsForVersion(bundle, + k8sVersionToCheck, + semVerVersionProvided, + semverMinKube, + errs, + warns) + } + + return errs, warns +} + +// checkRemovedAPIsForVersion will check if the bundle is using the removed APIs +// for the version informed (k8sVersionToCheck) +func checkRemovedAPIsForVersion( + bundle *manifests.Bundle, + k8sVersionToCheck, semVerVersionProvided, semverMinKube semver.Version, + errs []error, warns []error) ([]error, []error) { + + found := map[string][]string{} + switch k8sVersionToCheck.String() { + case "1.22.0": + found = getRemovedAPIsOn1_22From(bundle) + case "1.25.0": + found = getRemovedAPIsOn1_25From(bundle) + case "1.26.0": + found = getRemovedAPIsOn1_26From(bundle) + default: + panic(fmt.Errorf("invalid internal call to check the removed apis with the version (%s) which is not supported", k8sVersionToCheck.String())) + } + + if len(found) > 0 { + deprecatedAPIsMessage := generateMessageWithDeprecatedAPIs(found) + msg := fmt.Errorf(DeprecateMessage, + k8sVersionToCheck.Major, k8sVersionToCheck.Minor, + k8sVersionToCheck.Major, k8sVersionToCheck.Minor, + deprecatedAPIsMessage) + if isK8sVersionInformedEQ(semVerVersionProvided, k8sVersionToCheck, semverMinKube) { + // We only raise an error when the version >= 1.26 was informed via // the k8s key/value option or is specifically defined in the CSV - msg := fmt.Errorf("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 %s", deprecatedAPIsMessage) - if isUnsupported { - errs = append(errs, msg) - } else { - warns = append(warns, msg) - } + errs = append(errs, msg) + } else { + warns = append(warns, msg) } } - return errs, warns } +// isK8sVersionInformedEQ returns true only if the key/value OR minKubeVersion were informed and are >= semVerAPIUnsupported +func isK8sVersionInformedEQ(semVerVersionProvided semver.Version, semVerAPIUnsupported semver.Version, semverMinKube semver.Version) bool { + return semVerVersionProvided.GE(semVerAPIUnsupported) || semverMinKube.GE(semVerAPIUnsupported) +} + +func verifyK8sVersionInformed(versionProvided string) error { + if _, err := semver.ParseTolerant(versionProvided); err != nil { + return fmt.Errorf("invalid value informed via the k8s key option : %s", versionProvided) + } + return nil +} + // 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 { + + keys := make([]string, 0, len(deprecatedAPIs)) + for k := range deprecatedAPIs { + keys = append(keys, k) + } + sort.Strings(keys) + + deprecatedAPIsSorted := make(map[string][]string) + for _, key := range keys { + deprecatedAPIsSorted[key] = deprecatedAPIs[key] + } + + for k, v := range deprecatedAPIsSorted { if count == len(deprecatedAPIs)-1 { msg = msg + fmt.Sprintf("%s: (%+q)", k, v) } else { @@ -160,9 +209,6 @@ func generateMessageWithDeprecatedAPIs(deprecatedAPIs map[string][]string) strin 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 { @@ -228,3 +274,59 @@ func getRemovedAPIsOn1_22From(bundle *manifests.Bundle) map[string][]string { } return deprecatedAPIs } + +// getRemovedAPIsOn1_25From return the list of resources which were deprecated +// and are no longer be supported in 1.25. More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-25 +func getRemovedAPIsOn1_25From(bundle *manifests.Bundle) map[string][]string { + deprecatedAPIs := make(map[string][]string) + for _, obj := range bundle.Objects { + switch u := obj.GetObjectKind().(type) { + case *unstructured.Unstructured: + switch u.GetAPIVersion() { + case "batch/v1beta1": + if u.GetKind() == "CronJob" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "discovery.k8s.io/v1beta1": + if u.GetKind() == "EndpointSlice" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "events.k8s.io/v1beta1": + if u.GetKind() == "Event" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "autoscaling/v2beta1": + if u.GetKind() == "HorizontalPodAutoscaler" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "policy/v1beta1": + if u.GetKind() == "PodDisruptionBudget" || u.GetKind() == "PodSecurityPolicy" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + case "node.k8s.io/v1beta1": + if u.GetKind() == "RuntimeClass" { + deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName()) + } + } + } + } + return deprecatedAPIs +} + +// getRemovedAPIsOn1_26From return the list of resources which were deprecated +// and are no longer be supported in 1.26. More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-26 +func getRemovedAPIsOn1_26From(bundle *manifests.Bundle) map[string][]string { + deprecatedAPIs := make(map[string][]string) + for _, obj := range bundle.Objects { + switch u := obj.GetObjectKind().(type) { + case *unstructured.Unstructured: + switch u.GetAPIVersion() { + case "autoscaling/v2beta2": + if u.GetKind() == "HorizontalPodAutoscaler" { + 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 index 440b34a69..1b1aabdab 100644 --- a/pkg/validation/internal/removed_apis_test.go +++ b/pkg/validation/internal/removed_apis_test.go @@ -7,8 +7,7 @@ import ( "testing" ) -func Test_getDeprecatedAPIs(t *testing.T) { - +func Test_GetRemovedAPIsOn1_22From(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"} @@ -64,6 +63,89 @@ func Test_getDeprecatedAPIs(t *testing.T) { } } +func Test_GetRemovedAPIsOn1_25From(t *testing.T) { + mock := make(map[string][]string) + mock["HorizontalPodAutoscaler"] = []string{"memcached-operator-hpa"} + mock["PodDisruptionBudget"] = []string{"memcached-operator-policy-manager"} + + 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 fail return the removed APIs in 1.25", + args: args{ + bundleDir: "./testdata/removed_api_1_25", + }, + want: mock, + }, + } + 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_25From(bundle); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getRemovedAPIsOn1_25From() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_GetRemovedAPIsOn1_26From(t *testing.T) { + mock := make(map[string][]string) + mock["HorizontalPodAutoscaler"] = []string{"memcached-operator-hpa"} + + 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 fail return the removed APIs in 1.26", + args: args{ + bundleDir: "./testdata/removed_api_1_26", + }, + want: mock, + }, + } + 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_26From(bundle); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getRemovedAPIsOn1_26From() = %v, want %v", got, tt.want) + } + }) + } +} + func TestValidateDeprecatedAPIS(t *testing.T) { type args struct { minKubeVersion string @@ -78,16 +160,6 @@ func TestValidateDeprecatedAPIS(t *testing.T) { errStrings []string warnStrings []string }{ - { - name: "should not return error or warning when the k8sVersion is <= 1.15", - args: args{ - k8sVersion: "1.15", - minKubeVersion: "", - directory: "./testdata/valid_bundle_v1beta1", - }, - wantWarning: true, - warnStrings: []string{"checking APIs against Kubernetes version : 1.15"}, - }, { name: "should return a warning when has the CRD v1beta1 and minKubeVersion is informed", args: args{ @@ -101,16 +173,6 @@ func TestValidateDeprecatedAPIS(t *testing.T) { "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", - args: args{ - k8sVersion: "1.15", - minKubeVersion: "1.11.3", - directory: "./testdata/valid_bundle_v1beta1", - }, - wantWarning: true, - warnStrings: []string{"checking APIs against Kubernetes version : 1.15"}, - }, { name: "should return an error when the k8sVersion is >= 1.22 and has the deprecated API", args: args{ @@ -123,8 +185,56 @@ func TestValidateDeprecatedAPIS(t *testing.T) { "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\"])"}, + }, + { + name: "should return an error when the k8sVersion is >= 1.25 and found removed APIs on 1.25", + args: args{ + k8sVersion: "1.25", + minKubeVersion: "", + directory: "./testdata/removed_api_1_25", + }, + wantError: true, + errStrings: []string{"this bundle is using APIs which were deprecated and removed in v1.25. " + + "More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-25. " + + "Migrate the API(s) for HorizontalPodAutoscaler: ([\"memcached-operator-hpa\"])," + + "PodDisruptionBudget: ([\"memcached-operator-policy-manager\"]),"}, + }, + { + name: "should return a warning if the k8sVersion is empty and found removed APIs on 1.25", + args: args{ + k8sVersion: "", + minKubeVersion: "", + directory: "./testdata/removed_api_1_25", + }, + wantWarning: true, + warnStrings: []string{"this bundle is using APIs which were deprecated and removed in v1.25. " + + "More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-25. " + + "Migrate the API(s) for HorizontalPodAutoscaler: ([\"memcached-operator-hpa\"])," + + "PodDisruptionBudget: ([\"memcached-operator-policy-manager\"]),"}, + }, + { + name: "should return an error when the k8sVersion is >= 1.26 and found removed APIs on 1.26", + args: args{ + k8sVersion: "1.26", + minKubeVersion: "", + directory: "./testdata/removed_api_1_26", + }, + wantError: true, + errStrings: []string{"this bundle is using APIs which were deprecated and removed in v1.26. " + + "More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-26. " + + "Migrate the API(s) for HorizontalPodAutoscaler: ([\"memcached-operator-hpa\"])"}, + }, + { + name: "should return a warning when the k8sVersion is empty and found removed APIs on 1.26", + args: args{ + k8sVersion: "", + minKubeVersion: "", + directory: "./testdata/removed_api_1_26", + }, wantWarning: true, - warnStrings: []string{"checking APIs against Kubernetes version : 1.22"}, + warnStrings: []string{"this bundle is using APIs which were deprecated and removed in v1.26. " + + "More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-26. " + + "Migrate the API(s) for HorizontalPodAutoscaler: ([\"memcached-operator-hpa\"])"}, }, { name: "should return an error when the k8sVersion informed is invalid", @@ -133,8 +243,13 @@ func TestValidateDeprecatedAPIS(t *testing.T) { minKubeVersion: "", directory: "./testdata/valid_bundle_v1beta1", }, - wantError: true, - errStrings: []string{"invalid value informed via the k8s key option : invalid"}, + wantError: true, + errStrings: []string{"invalid value informed via the k8s key option : invalid"}, + wantWarning: true, + warnStrings: []string{"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\"])"}, }, { name: "should return an error when the csv.spec.minKubeVersion informed is invalid", diff --git a/pkg/validation/internal/testdata/removed_api_1_25/cache.example.com_memcacheds.yaml b/pkg/validation/internal/testdata/removed_api_1_25/cache.example.com_memcacheds.yaml new file mode 100644 index 000000000..a8ea3eb8d --- /dev/null +++ b/pkg/validation/internal/testdata/removed_api_1_25/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/removed_api_1_25/horizontal-pod.yaml b/pkg/validation/internal/testdata/removed_api_1_25/horizontal-pod.yaml new file mode 100644 index 000000000..69f77b5f4 --- /dev/null +++ b/pkg/validation/internal/testdata/removed_api_1_25/horizontal-pod.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: memcached-operator-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: memcached-operator-controller-manager + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 \ No newline at end of file diff --git a/pkg/validation/internal/testdata/removed_api_1_25/memcached-operator.clusterserviceversion.yaml b/pkg/validation/internal/testdata/removed_api_1_25/memcached-operator.clusterserviceversion.yaml new file mode 100644 index 000000000..bddc80769 --- /dev/null +++ b/pkg/validation/internal/testdata/removed_api_1_25/memcached-operator.clusterserviceversion.yaml @@ -0,0 +1,252 @@ +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 + 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/removed_api_1_25/policy.yaml b/pkg/validation/internal/testdata/removed_api_1_25/policy.yaml new file mode 100644 index 000000000..15714a60c --- /dev/null +++ b/pkg/validation/internal/testdata/removed_api_1_25/policy.yaml @@ -0,0 +1,9 @@ +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + name: memcached-operator-policy-manager +spec: + minAvailable: 2 + selector: + matchLabels: + app: memcached-operator \ No newline at end of file diff --git a/pkg/validation/internal/testdata/removed_api_1_26/cache.example.com_memcacheds.yaml b/pkg/validation/internal/testdata/removed_api_1_26/cache.example.com_memcacheds.yaml new file mode 100644 index 000000000..a8ea3eb8d --- /dev/null +++ b/pkg/validation/internal/testdata/removed_api_1_26/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/removed_api_1_26/horizontal-pod.yaml b/pkg/validation/internal/testdata/removed_api_1_26/horizontal-pod.yaml new file mode 100644 index 000000000..33d6e16fa --- /dev/null +++ b/pkg/validation/internal/testdata/removed_api_1_26/horizontal-pod.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: memcached-operator-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: memcached-operator-controller-manager + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 \ No newline at end of file diff --git a/pkg/validation/internal/testdata/removed_api_1_26/memcached-operator.clusterserviceversion.yaml b/pkg/validation/internal/testdata/removed_api_1_26/memcached-operator.clusterserviceversion.yaml new file mode 100644 index 000000000..bddc80769 --- /dev/null +++ b/pkg/validation/internal/testdata/removed_api_1_26/memcached-operator.clusterserviceversion.yaml @@ -0,0 +1,252 @@ +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 + 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/validation.go b/pkg/validation/validation.go index d84a5ace7..1d87737fa 100644 --- a/pkg/validation/validation.go +++ b/pkg/validation/validation.go @@ -48,6 +48,12 @@ var CommunityOperatorValidator = internal.CommunityOperatorValidator // AlphaDeprecatedAPIsValidator implements Validator to validate bundle objects // for API deprecation requirements. +// +// Note that this validator looks at the manifests. If any removed APIs for the mapped k8s versions are found, +// it raises a warning. +// +// This validator only raises an error when the deprecated API found is removed in the specified k8s +// version informed via the optional key `k8s-version`. var AlphaDeprecatedAPIsValidator = internal.AlphaDeprecatedAPIsValidator // GoodPracticesValidator implements Validator to validate the criteria defined as good practices