Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deprecate-api validator: add checks for removed APIs on 1.25 and 1.26 #208

Merged
merged 1 commit into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 5 additions & 1 deletion pkg/validation/internal/operatorhub.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
226 changes: 164 additions & 62 deletions pkg/validation/internal/removed_apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Loading