Skip to content

Commit bfa93d1

Browse files
deprecate-api validator: add checks for removed APIs on 1.25 and 1.26 (#208)
1 parent fc492d5 commit bfa93d1

12 files changed

+1040
-88
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,50 @@ Following an example.
7070
return nonEmptyResults
7171
```
7272

73+
#### Passing optional key/values to the validators
74+
75+
Validators may accept pass optional key/values which will be used in the checks made.
76+
These values are global and if the key/value pair provided is not used for 1 or more
77+
validators called then, it is ignored.
78+
79+
The following example calls `AlphaDeprecatedAPIsValidator`, which allows us to inform
80+
the K8s version intended to publish the OLM Bundle:
81+
82+
```go
83+
validators := apivalidation.DefaultBundleValidators
84+
validators = validators.WithValidators(apivalidation.OperatorHubValidator)
85+
validators = validators.WithValidators(apivalidation.ObjectValidator)
86+
validators = validators.WithValidators(apivalidation.AlphaDeprecatedAPIsValidator)
87+
validators = validators.WithValidators(apivalidation.GoodPracticesValidator)
88+
89+
objs := auditBundle.Bundle.ObjectsToValidate()
90+
91+
// Pass the --optional-values. e.g. --optional-values="k8s-version=1.22"
92+
// or --optional-values="image-path=bundle.Dockerfile"
93+
var optionalValues = map[string]string{
94+
"k8s-version":"1.22",
95+
}
96+
objs = append(objs, optionalValues)
97+
98+
results := validators.Validate(objs...)
99+
nonEmptyResults := []errors.ManifestResult{}
100+
101+
for _, result := range results {
102+
if result.HasError() || result.HasWarn() {
103+
nonEmptyResults = append(nonEmptyResults, result)
104+
}
105+
}
106+
```
107+
108+
**How the optional key/values are informed via the CLI?**
109+
110+
By using [Operator-SDK][sdk] you can pass a list of key/values via the flag `--optional-values`, for example,
111+
to validate that your manifests can work with a Kubernetes cluster of a particular version using the `k8s-version`:
112+
113+
```sh
114+
$ operator-sdk bundle validate ./bundle --select-optional suite=operatorframework --optional-values=k8s-version=1.22
115+
```
116+
73117
## API CLI Usage
74118

75119
You can install the `operator-verify` tool from source using:

pkg/validation/internal/operatorhub.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"regexp"
1212
"strings"
1313

14-
semver "github.com/blang/semver/v4"
14+
"github.com/blang/semver/v4"
1515
"github.com/operator-framework/api/pkg/manifests"
1616
"github.com/operator-framework/api/pkg/operators/v1alpha1"
1717
"github.com/operator-framework/api/pkg/validation/errors"
@@ -158,6 +158,10 @@ var validCategories = map[string]struct{}{
158158
"Streaming & Messaging": {},
159159
}
160160

161+
const minKubeVersionWarnMessage = "csv.Spec.minKubeVersion is not informed. It is recommended you provide this information. " +
162+
"Otherwise, it would mean that your operator project can be distributed and installed in any cluster version " +
163+
"available, which is not necessarily the case for all projects."
164+
161165
func validateOperatorHub(objs ...interface{}) (results []errors.ManifestResult) {
162166

163167
// Obtain the k8s version if informed via the objects an optional

pkg/validation/internal/removed_apis.go

Lines changed: 164 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,43 @@ package internal
33
import (
44
"fmt"
55
"github.com/blang/semver"
6-
76
"github.com/operator-framework/api/pkg/manifests"
8-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9-
107
"github.com/operator-framework/api/pkg/validation/errors"
118
interfaces "github.com/operator-framework/api/pkg/validation/interfaces"
9+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
10+
"sort"
1211
)
1312

1413
// k8sVersionKey defines the key which can be used by its consumers
1514
// to inform what is the K8S version that should be used to do the tests against.
1615
const k8sVersionKey = "k8s-version"
1716

18-
const minKubeVersionWarnMessage = "csv.Spec.minKubeVersion is not informed. It is recommended you provide this information. " +
19-
"Otherwise, it would mean that your operator project can be distributed and installed in any cluster version " +
20-
"available, which is not necessarily the case for all projects."
21-
22-
// K8s version where the apis v1betav1 is no longer supported
23-
const k8sVerV1betav1Unsupported = "1.22.0"
17+
// DeprecateMessage defines the content of the message that will be raised as an error or warning
18+
// when the removed apis are found
19+
const DeprecateMessage = "this bundle is using APIs which were deprecated and removed in v%v.%v. " +
20+
"More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v%v-%v. " +
21+
"Migrate the API(s) for %s"
2422

25-
// K8s version where the apis v1betav1 was deprecated
26-
const k8sVerV1betav1Deprecated = "1.16.0"
23+
// K8sVersionsSupportedByValidator defines the k8s versions which this validator is implemented to
24+
// perform the checks
25+
var K8sVersionsSupportedByValidator = []string{"1.22.0", "1.25.0", "1.26.0"}
2726

28-
// AlphaDeprecatedAPIsValidator validates if the bundles is using versions API version which are deprecate or
29-
// removed in specific Kubernetes versions informed via optional key value `k8s-version`.
27+
// AlphaDeprecatedAPIsValidator implements Validator to validate bundle objects
28+
// for API deprecation requirements.
29+
//
30+
// Note that this validator looks at the manifests. If any removed APIs for the mapped k8s versions are found,
31+
// it raises a warning.
32+
//
33+
// This validator only raises an error when the deprecated API found is removed in the specified k8s
34+
// version informed via the optional key `k8s-version`.
35+
//
36+
// The K8s versions supported and checks are:
37+
//
38+
// - 1.22 : https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22
39+
//
40+
// - 1.25 : https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-25
41+
//
42+
// - 1.26 : https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-26
3043
var AlphaDeprecatedAPIsValidator interfaces.Validator = interfaces.ValidatorFunc(validateDeprecatedAPIsValidator)
3144

3245
func validateDeprecatedAPIsValidator(objs ...interface{}) (results []errors.ManifestResult) {
@@ -54,13 +67,14 @@ func validateDeprecatedAPIsValidator(objs ...interface{}) (results []errors.Mani
5467
}
5568

5669
func validateDeprecatedAPIs(bundle *manifests.Bundle, k8sVersion string) errors.ManifestResult {
57-
result := errors.ManifestResult{Name: bundle.Name}
58-
70+
result := errors.ManifestResult{}
5971
if bundle == nil {
6072
result.Add(errors.ErrInvalidBundle("Bundle is nil", nil))
6173
return result
6274
}
6375

76+
result.Name = bundle.Name
77+
6478
if bundle.CSV == nil {
6579
result.Add(errors.ErrInvalidBundle("Bundle csv is nil", bundle.Name))
6680
return result
@@ -78,34 +92,22 @@ func validateDeprecatedAPIs(bundle *manifests.Bundle, k8sVersion string) errors.
7892
}
7993

8094
// validateDeprecatedAPIS will check if the operator bundle is using a deprecated or no longer supported k8s api
81-
// Note if the k8s was informed via "k8s=1.22" it will be used. Otherwise, we will use the minKubeVersion in
82-
// the CSV to do the checks. So, the criteria is >=minKubeVersion. By last, if the minKubeVersion is not provided
95+
// Note if the k8s version was informed via "k8s-version" optional key it will be used. Otherwise, we will use the minKubeVersion in
96+
// the CSV to do the checks. So, the criteria is >=minKubeVersion. Lastly, if the minKubeVersion is not provided
8397
// then, we should consider the operator bundle is intend to work well in any Kubernetes version.
8498
// Then, it means that:
85-
//--optional-values="k8s-version=value" flag with a value => 1.16 <= 1.22 the validator will return result as warning.
86-
//--optional-values="k8s-version=value" flag with a value => 1.22 the validator will return result as error.
87-
//minKubeVersion >= 1.22 return the error result.
88-
//minKubeVersion empty returns a warning since it would mean the same of allow install in any supported version
99+
// - --optional-values="k8s-version=value" flag with a value <= unsupportedAPIVersion the validator will return result as warning.
100+
// - --optional-values="k8s-version=value" flag with a value => unsupportedAPIVersion the validator will return result as error.
101+
// - minKubeVersion >= unsupportedAPIVersion return the error result.
102+
// - minKubeVersion empty returns a warning since it would mean the same of allow in any supported version
89103
func validateDeprecatedAPIS(bundle *manifests.Bundle, versionProvided string) (errs, warns []error) {
90-
91-
// semver of the K8s version where the apis v1betav1 is no longer supported to allow us compare
92-
semVerK8sVerV1betav1Unsupported := semver.MustParse(k8sVerV1betav1Unsupported)
93-
// semver of the K8s version where the apis v1betav1 is deprecated to allow us compare
94-
semVerk8sVerV1betav1Deprecated := semver.MustParse(k8sVerV1betav1Deprecated)
95104
// isVersionProvided defines if the k8s version to test against was or not informed
96105
isVersionProvided := len(versionProvided) > 0
106+
// semVerVersionProvided -- converts the k8s version informed in semver
107+
semVerVersionProvided, _ := semver.ParseTolerant(versionProvided)
97108

98-
// Transform the key/option versionProvided in semver Version to compare
99-
var semVerVersionProvided semver.Version
100-
if isVersionProvided {
101-
var err error
102-
semVerVersionProvided, err = semver.ParseTolerant(versionProvided)
103-
if err != nil {
104-
errs = append(errs, fmt.Errorf("invalid value informed via the k8s key option : %s", versionProvided))
105-
} else {
106-
// we might want to return it as info instead of warning in the future.
107-
warns = append(warns, fmt.Errorf("checking APIs against Kubernetes version : %s", versionProvided))
108-
}
109+
if err := verifyK8sVersionInformed(versionProvided); err != nil && isVersionProvided {
110+
errs = append(errs, err)
109111
}
110112

111113
// Transform the spec minKubeVersion in semver Version to compare
@@ -118,39 +120,86 @@ func validateDeprecatedAPIS(bundle *manifests.Bundle, versionProvided string) (e
118120
}
119121
}
120122

121-
// if the k8s value was informed and it is >=1.16 we should check
122-
// if the k8s value was not informed we also should check since the
123-
// check should occurs with any minKubeVersion value:
124-
// - if minKubeVersion empty then means that the project can be installed in any version
125-
// - if minKubeVersion any version defined it means that we are considering install
126-
// in any upper version from that where the check is always applied
127-
if !isVersionProvided || semVerVersionProvided.GE(semVerk8sVerV1betav1Deprecated) {
128-
deprecatedAPIs := getRemovedAPIsOn1_22From(bundle)
129-
if len(deprecatedAPIs) > 0 {
130-
deprecatedAPIsMessage := generateMessageWithDeprecatedAPIs(deprecatedAPIs)
131-
// isUnsupported is true only if the key/value OR minKubeVersion were informed and are >= 1.22
132-
isUnsupported := semVerVersionProvided.GE(semVerK8sVerV1betav1Unsupported) ||
133-
semverMinKube.GE(semVerK8sVerV1betav1Unsupported)
134-
// We only raise an error when the version >= 1.22 was informed via
123+
// Check the bundle with all k8s versions implemented
124+
for _, v := range K8sVersionsSupportedByValidator {
125+
k8sVersionToCheck := semver.MustParse(v)
126+
errs, warns = checkRemovedAPIsForVersion(bundle,
127+
k8sVersionToCheck,
128+
semVerVersionProvided,
129+
semverMinKube,
130+
errs,
131+
warns)
132+
}
133+
134+
return errs, warns
135+
}
136+
137+
// checkRemovedAPIsForVersion will check if the bundle is using the removed APIs
138+
// for the version informed (k8sVersionToCheck)
139+
func checkRemovedAPIsForVersion(
140+
bundle *manifests.Bundle,
141+
k8sVersionToCheck, semVerVersionProvided, semverMinKube semver.Version,
142+
errs []error, warns []error) ([]error, []error) {
143+
144+
found := map[string][]string{}
145+
switch k8sVersionToCheck.String() {
146+
case "1.22.0":
147+
found = getRemovedAPIsOn1_22From(bundle)
148+
case "1.25.0":
149+
found = getRemovedAPIsOn1_25From(bundle)
150+
case "1.26.0":
151+
found = getRemovedAPIsOn1_26From(bundle)
152+
default:
153+
panic(fmt.Errorf("invalid internal call to check the removed apis with the version (%s) which is not supported", k8sVersionToCheck.String()))
154+
}
155+
156+
if len(found) > 0 {
157+
deprecatedAPIsMessage := generateMessageWithDeprecatedAPIs(found)
158+
msg := fmt.Errorf(DeprecateMessage,
159+
k8sVersionToCheck.Major, k8sVersionToCheck.Minor,
160+
k8sVersionToCheck.Major, k8sVersionToCheck.Minor,
161+
deprecatedAPIsMessage)
162+
if isK8sVersionInformedEQ(semVerVersionProvided, k8sVersionToCheck, semverMinKube) {
163+
// We only raise an error when the version >= 1.26 was informed via
135164
// the k8s key/value option or is specifically defined in the CSV
136-
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)
137-
if isUnsupported {
138-
errs = append(errs, msg)
139-
} else {
140-
warns = append(warns, msg)
141-
}
165+
errs = append(errs, msg)
166+
} else {
167+
warns = append(warns, msg)
142168
}
143169
}
144-
145170
return errs, warns
146171
}
147172

173+
// isK8sVersionInformedEQ returns true only if the key/value OR minKubeVersion were informed and are >= semVerAPIUnsupported
174+
func isK8sVersionInformedEQ(semVerVersionProvided semver.Version, semVerAPIUnsupported semver.Version, semverMinKube semver.Version) bool {
175+
return semVerVersionProvided.GE(semVerAPIUnsupported) || semverMinKube.GE(semVerAPIUnsupported)
176+
}
177+
178+
func verifyK8sVersionInformed(versionProvided string) error {
179+
if _, err := semver.ParseTolerant(versionProvided); err != nil {
180+
return fmt.Errorf("invalid value informed via the k8s key option : %s", versionProvided)
181+
}
182+
return nil
183+
}
184+
148185
// generateMessageWithDeprecatedAPIs will return a list with the kind and the name
149186
// of the resource which were found and required to be upgraded
150187
func generateMessageWithDeprecatedAPIs(deprecatedAPIs map[string][]string) string {
151188
msg := ""
152189
count := 0
153-
for k, v := range deprecatedAPIs {
190+
191+
keys := make([]string, 0, len(deprecatedAPIs))
192+
for k := range deprecatedAPIs {
193+
keys = append(keys, k)
194+
}
195+
sort.Strings(keys)
196+
197+
deprecatedAPIsSorted := make(map[string][]string)
198+
for _, key := range keys {
199+
deprecatedAPIsSorted[key] = deprecatedAPIs[key]
200+
}
201+
202+
for k, v := range deprecatedAPIsSorted {
154203
if count == len(deprecatedAPIs)-1 {
155204
msg = msg + fmt.Sprintf("%s: (%+q)", k, v)
156205
} else {
@@ -160,9 +209,6 @@ func generateMessageWithDeprecatedAPIs(deprecatedAPIs map[string][]string) strin
160209
return msg
161210
}
162211

163-
// todo: we need to improve this code since we ought to map the kinds, apis and ocp/k8s versions
164-
// where them are no longer supported ( removed ) instead of have this fixed in this way.
165-
166212
// getRemovedAPIsOn1_22From return the list of resources which were deprecated
167213
// and are no longer be supported in 1.22. More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22
168214
func getRemovedAPIsOn1_22From(bundle *manifests.Bundle) map[string][]string {
@@ -228,3 +274,59 @@ func getRemovedAPIsOn1_22From(bundle *manifests.Bundle) map[string][]string {
228274
}
229275
return deprecatedAPIs
230276
}
277+
278+
// getRemovedAPIsOn1_25From return the list of resources which were deprecated
279+
// and are no longer be supported in 1.25. More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-25
280+
func getRemovedAPIsOn1_25From(bundle *manifests.Bundle) map[string][]string {
281+
deprecatedAPIs := make(map[string][]string)
282+
for _, obj := range bundle.Objects {
283+
switch u := obj.GetObjectKind().(type) {
284+
case *unstructured.Unstructured:
285+
switch u.GetAPIVersion() {
286+
case "batch/v1beta1":
287+
if u.GetKind() == "CronJob" {
288+
deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName())
289+
}
290+
case "discovery.k8s.io/v1beta1":
291+
if u.GetKind() == "EndpointSlice" {
292+
deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName())
293+
}
294+
case "events.k8s.io/v1beta1":
295+
if u.GetKind() == "Event" {
296+
deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName())
297+
}
298+
case "autoscaling/v2beta1":
299+
if u.GetKind() == "HorizontalPodAutoscaler" {
300+
deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName())
301+
}
302+
case "policy/v1beta1":
303+
if u.GetKind() == "PodDisruptionBudget" || u.GetKind() == "PodSecurityPolicy" {
304+
deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName())
305+
}
306+
case "node.k8s.io/v1beta1":
307+
if u.GetKind() == "RuntimeClass" {
308+
deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName())
309+
}
310+
}
311+
}
312+
}
313+
return deprecatedAPIs
314+
}
315+
316+
// getRemovedAPIsOn1_26From return the list of resources which were deprecated
317+
// and are no longer be supported in 1.26. More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-26
318+
func getRemovedAPIsOn1_26From(bundle *manifests.Bundle) map[string][]string {
319+
deprecatedAPIs := make(map[string][]string)
320+
for _, obj := range bundle.Objects {
321+
switch u := obj.GetObjectKind().(type) {
322+
case *unstructured.Unstructured:
323+
switch u.GetAPIVersion() {
324+
case "autoscaling/v2beta2":
325+
if u.GetKind() == "HorizontalPodAutoscaler" {
326+
deprecatedAPIs[u.GetKind()] = append(deprecatedAPIs[u.GetKind()], obj.GetName())
327+
}
328+
}
329+
}
330+
}
331+
return deprecatedAPIs
332+
}

0 commit comments

Comments
 (0)