From 3fc60e9c8f47922ac9f3aabc9428d403c97ebd05 Mon Sep 17 00:00:00 2001 From: Rashmi Gottipati Date: Sun, 14 Apr 2024 15:52:22 -0400 Subject: [PATCH] Add validation to CRDUpgradeSafety preflight check to prevent removal of existing fields (#922) Signed-off-by: Rashmi Gottipati --- go.mod | 1 + go.sum | 2 + pkg/kapp/crdupgradesafety/preflight.go | 1 + pkg/kapp/crdupgradesafety/validator.go | 28 ++ pkg/kapp/crdupgradesafety/validator_test.go | 234 +++++++++++--- ...gradesafety_noexistingfieldremoval_test.go | 118 +++++++ .../openshift/crd-schema-checker/LICENSE | 201 ++++++++++++ .../comp_lists_must_have_ssa_tags.go | 60 ++++ .../comp_must_have_status.go | 62 ++++ .../pkg/manifestcomparators/comp_no_bools.go | 58 ++++ .../comp_no_enum_removal.go | 85 +++++ .../comp_no_field_removal.go | 74 +++++ .../pkg/manifestcomparators/comp_no_floats.go | 56 ++++ .../pkg/manifestcomparators/comp_no_maps.go | 62 ++++ .../comp_no_new_required_fields.go | 168 ++++++++++ .../pkg/manifestcomparators/comp_no_uints.go | 55 ++++ .../pkg/manifestcomparators/helpers.go | 121 ++++++++ .../pkg/manifestcomparators/interfaces.go | 32 ++ .../racheting_validator.go | 53 ++++ .../pkg/manifestcomparators/registry.go | 80 +++++ .../pkg/manifestcomparators/simple_tester.go | 292 ++++++++++++++++++ .../pkg/resourceread/apiextensions.go | 37 +++ vendor/modules.txt | 4 + 23 files changed, 1848 insertions(+), 36 deletions(-) create mode 100644 test/e2e/preflight_crdupgradesafety_noexistingfieldremoval_test.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/LICENSE create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_lists_must_have_ssa_tags.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_must_have_status.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_bools.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_enum_removal.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_field_removal.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_floats.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_maps.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_new_required_fields.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_uints.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/helpers.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/interfaces.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/racheting_validator.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/registry.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/simple_tester.go create mode 100644 vendor/github.com/openshift/crd-schema-checker/pkg/resourceread/apiextensions.go diff --git a/go.mod b/go.mod index a0cdbea3a..9ee6e5dd1 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/k14s/difflib v0.0.0-20240118055029-596a7a5585c3 github.com/k14s/ytt v0.36.0 github.com/mitchellh/go-wordwrap v1.0.1 + github.com/openshift/crd-schema-checker v0.0.0-20240404194209-35a9033b1d11 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 0bdd8bd7b..985e05238 100644 --- a/go.sum +++ b/go.sum @@ -241,6 +241,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/openshift/crd-schema-checker v0.0.0-20240404194209-35a9033b1d11 h1:eTNDkNRNV5lZvUbVM9Nop0lBcljSnA8rZX6yQPZ0ZnU= +github.com/openshift/crd-schema-checker v0.0.0-20240404194209-35a9033b1d11/go.mod h1:EmVJt97N+pfWFsli/ipXTBZqSG5F5KGQhm3c3IsGq1o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/pkg/kapp/crdupgradesafety/preflight.go b/pkg/kapp/crdupgradesafety/preflight.go index 962d21cd8..8c35d00b6 100644 --- a/pkg/kapp/crdupgradesafety/preflight.go +++ b/pkg/kapp/crdupgradesafety/preflight.go @@ -36,6 +36,7 @@ func NewPreflight(df cmdcore.DepsFactory, enabled bool) *Preflight { Validations: []Validation{ NewValidationFunc("NoScopeChange", NoScopeChange), NewValidationFunc("NoStoredVersionRemoved", NoStoredVersionRemoved), + NewValidationFunc("NoExistingFieldRemoved", NoExistingFieldRemoved), }, }, } diff --git a/pkg/kapp/crdupgradesafety/validator.go b/pkg/kapp/crdupgradesafety/validator.go index fe028152d..9cb970b56 100644 --- a/pkg/kapp/crdupgradesafety/validator.go +++ b/pkg/kapp/crdupgradesafety/validator.go @@ -6,7 +6,9 @@ package crdupgradesafety import ( "errors" "fmt" + "strings" + "github.com/openshift/crd-schema-checker/pkg/manifestcomparators" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/util/sets" ) @@ -91,3 +93,29 @@ func NoStoredVersionRemoved(old, new v1.CustomResourceDefinition) error { return nil } + +func NoExistingFieldRemoved(old, new v1.CustomResourceDefinition) error { + reg := manifestcomparators.NewRegistry() + err := reg.AddComparator(manifestcomparators.NoFieldRemoval()) + if err != nil { + return err + } + + results, errs := reg.Compare(&old, &new) + if len(errs) > 0 { + return errors.Join(errs...) + } + + errSet := []error{} + + for _, result := range results { + if len(result.Errors) > 0 { + errSet = append(errSet, errors.New(strings.Join(result.Errors, "\n"))) + } + } + if len(errSet) > 0 { + return errors.Join(errSet...) + } + + return nil +} diff --git a/pkg/kapp/crdupgradesafety/validator_test.go b/pkg/kapp/crdupgradesafety/validator_test.go index bfed7cb9d..53da0c482 100644 --- a/pkg/kapp/crdupgradesafety/validator_test.go +++ b/pkg/kapp/crdupgradesafety/validator_test.go @@ -7,8 +7,9 @@ import ( "errors" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) func TestValidator(t *testing.T) { @@ -24,7 +25,7 @@ func TestValidator(t *testing.T) { { name: "passing validator, no error", validations: []Validation{ - NewValidationFunc("pass", func(_, _ v1.CustomResourceDefinition) error { + NewValidationFunc("pass", func(_, _ apiextensionsv1.CustomResourceDefinition) error { return nil }), }, @@ -32,7 +33,7 @@ func TestValidator(t *testing.T) { { name: "failing validator, error", validations: []Validation{ - NewValidationFunc("fail", func(_, _ v1.CustomResourceDefinition) error { + NewValidationFunc("fail", func(_, _ apiextensionsv1.CustomResourceDefinition) error { return errors.New("boom") }), }, @@ -41,10 +42,10 @@ func TestValidator(t *testing.T) { { name: "passing+failing validator, error", validations: []Validation{ - NewValidationFunc("pass", func(_, _ v1.CustomResourceDefinition) error { + NewValidationFunc("pass", func(_, _ apiextensionsv1.CustomResourceDefinition) error { return nil }), - NewValidationFunc("fail", func(_, _ v1.CustomResourceDefinition) error { + NewValidationFunc("fail", func(_, _ apiextensionsv1.CustomResourceDefinition) error { return errors.New("boom") }), }, @@ -55,7 +56,7 @@ func TestValidator(t *testing.T) { v := Validator{ Validations: tc.validations, } - var o, n v1.CustomResourceDefinition + var o, n apiextensionsv1.CustomResourceDefinition err := v.Validate(o, n) require.Equal(t, tc.shouldErr, err != nil) @@ -66,33 +67,33 @@ func TestValidator(t *testing.T) { func TestNoScopeChange(t *testing.T) { for _, tc := range []struct { name string - old v1.CustomResourceDefinition - new v1.CustomResourceDefinition + old apiextensionsv1.CustomResourceDefinition + new apiextensionsv1.CustomResourceDefinition shouldError bool }{ { name: "no scope change, no error", - old: v1.CustomResourceDefinition{ - Spec: v1.CustomResourceDefinitionSpec{ - Scope: v1.ClusterScoped, + old: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Scope: apiextensionsv1.ClusterScoped, }, }, - new: v1.CustomResourceDefinition{ - Spec: v1.CustomResourceDefinitionSpec{ - Scope: v1.ClusterScoped, + new: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Scope: apiextensionsv1.ClusterScoped, }, }, }, { name: "scope change, error", - old: v1.CustomResourceDefinition{ - Spec: v1.CustomResourceDefinitionSpec{ - Scope: v1.ClusterScoped, + old: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Scope: apiextensionsv1.ClusterScoped, }, }, - new: v1.CustomResourceDefinition{ - Spec: v1.CustomResourceDefinitionSpec{ - Scope: v1.NamespaceScoped, + new: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Scope: apiextensionsv1.NamespaceScoped, }, }, shouldError: true, @@ -108,28 +109,28 @@ func TestNoScopeChange(t *testing.T) { func TestNoStoredVersionRemoved(t *testing.T) { for _, tc := range []struct { name string - old v1.CustomResourceDefinition - new v1.CustomResourceDefinition + old apiextensionsv1.CustomResourceDefinition + new apiextensionsv1.CustomResourceDefinition shouldError bool }{ { name: "no stored versions, no error", - new: v1.CustomResourceDefinition{ - Spec: v1.CustomResourceDefinitionSpec{ - Versions: []v1.CustomResourceDefinitionVersion{ + new: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ { Name: "v1alpha1", }, }, }, }, - old: v1.CustomResourceDefinition{}, + old: apiextensionsv1.CustomResourceDefinition{}, }, { name: "stored versions, no stored version removed, no error", - new: v1.CustomResourceDefinition{ - Spec: v1.CustomResourceDefinitionSpec{ - Versions: []v1.CustomResourceDefinitionVersion{ + new: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ { Name: "v1alpha1", }, @@ -139,8 +140,8 @@ func TestNoStoredVersionRemoved(t *testing.T) { }, }, }, - old: v1.CustomResourceDefinition{ - Status: v1.CustomResourceDefinitionStatus{ + old: apiextensionsv1.CustomResourceDefinition{ + Status: apiextensionsv1.CustomResourceDefinitionStatus{ StoredVersions: []string{ "v1alpha1", }, @@ -149,17 +150,17 @@ func TestNoStoredVersionRemoved(t *testing.T) { }, { name: "stored versions, stored version removed, error", - new: v1.CustomResourceDefinition{ - Spec: v1.CustomResourceDefinitionSpec{ - Versions: []v1.CustomResourceDefinitionVersion{ + new: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ { Name: "v1alpha2", }, }, }, }, - old: v1.CustomResourceDefinition{ - Status: v1.CustomResourceDefinitionStatus{ + old: apiextensionsv1.CustomResourceDefinition{ + Status: apiextensionsv1.CustomResourceDefinitionStatus{ StoredVersions: []string{ "v1alpha1", }, @@ -174,3 +175,164 @@ func TestNoStoredVersionRemoved(t *testing.T) { }) } } + +func TestNoExistingFieldRemoved(t *testing.T) { + for _, tc := range []struct { + name string + new apiextensionsv1.CustomResourceDefinition + old apiextensionsv1.CustomResourceDefinition + shouldError bool + }{ + { + name: "no existing field removed, no error", + old: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "fieldOne": { + Type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + new: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "fieldOne": { + Type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "existing field removed, error", + old: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "fieldOne": { + Type: "string", + }, + "fieldTwo": { + Type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + new: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "fieldOne": { + Type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + shouldError: true, + }, + { + name: "new version is added with the field removed, no error", + old: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "fieldOne": { + Type: "string", + }, + "fieldTwo": { + Type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + new: apiextensionsv1.CustomResourceDefinition{ + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "fieldOne": { + Type: "string", + }, + "fieldTwo": { + Type: "string", + }, + }, + }, + }, + }, + { + Name: "v1alpha2", + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "fieldOne": { + Type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + err := NoExistingFieldRemoved(tc.old, tc.new) + assert.Equal(t, tc.shouldError, err != nil) + }) + } +} diff --git a/test/e2e/preflight_crdupgradesafety_noexistingfieldremoval_test.go b/test/e2e/preflight_crdupgradesafety_noexistingfieldremoval_test.go new file mode 100644 index 000000000..9d2dd9c87 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_noexistingfieldremoval_test.go @@ -0,0 +1,118 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyExistingFieldRemoval(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + kubectl := Kubectl{t, env.Namespace, logger} + + testName := "preflightcrdupgradesafetyexistingfieldremoval" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + status: + type: object + pollInterval: + type: string + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + RemoveClusterResource(t, "ns", testName, "", kubectl) + } + cleanUp() + defer cleanUp() + + kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD that removes an existing field, preflight check enabled, should error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update), AllowError: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "version/v1alpha1 field/^.pollInterval may not be removed") + }) +} diff --git a/vendor/github.com/openshift/crd-schema-checker/LICENSE b/vendor/github.com/openshift/crd-schema-checker/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_lists_must_have_ssa_tags.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_lists_must_have_ssa_tags.go new file mode 100644 index 000000000..96ce9d3ff --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_lists_must_have_ssa_tags.go @@ -0,0 +1,60 @@ +package manifestcomparators + +import ( + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type listsMustHaveSSATags struct{} + +func ListsMustHaveSSATags() CRDComparator { + return listsMustHaveSSATags{} +} + +func (listsMustHaveSSATags) Name() string { + return "ListsMustHaveSSATags" +} + +func (listsMustHaveSSATags) WhyItMatters() string { + return "Lists require x-kubernetes-list-type tags in order to properly merge different requests from different field managers. " + + "Valid value are 'atomic', 'set', and 'map' and are indicated in kubebuilder tags with '// +listType=' and " + + "'// +listMapKey='." +} + +func (b listsMustHaveSSATags) Validate(crd *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + errsToReport := []string{} + + for _, newVersion := range crd.Spec.Versions { + fieldsWithoutListType := []string{} + SchemaHas(newVersion.Schema.OpenAPIV3Schema, field.NewPath("^"), field.NewPath("^"), nil, + func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool { + if s.Type != "array" { + return false + } + if s.XListType == nil || len(*s.XListType) == 0 { + fieldsWithoutListType = append(fieldsWithoutListType, simpleLocation.String()) + } + return false + }) + + for _, newMapField := range fieldsWithoutListType { + errsToReport = append(errsToReport, fmt.Sprintf("crd/%v version/%v field/%v must set x-kubernetes-list-type", crd.Name, newVersion.Name, newMapField)) + } + + } + + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: errsToReport, + Warnings: nil, + Infos: nil, + }, nil +} + +func (b listsMustHaveSSATags) Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + return RatchetCompare(b, existingCRD, newCRD) +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_must_have_status.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_must_have_status.go new file mode 100644 index 000000000..d6eba6576 --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_must_have_status.go @@ -0,0 +1,62 @@ +package manifestcomparators + +import ( + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type mustHaveStatus struct{} + +func MustHaveStatus() CRDComparator { + return mustHaveStatus{} +} + +func (mustHaveStatus) Name() string { + return "MustHaveStatus" +} + +func (mustHaveStatus) WhyItMatters() string { + return "When the schema has a status field, it should be controlled via a status suberesource for different permissions " + + "to control those who can control desired state from those who can control the actual state." +} + +func (b mustHaveStatus) Validate(crd *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + const statusField = "^.status" + errsToReport := []string{} + + for _, newVersion := range crd.Spec.Versions { + if newVersion.Subresources != nil && newVersion.Subresources.Status != nil { + continue + } + + hasStatus := false + SchemaHas(newVersion.Schema.OpenAPIV3Schema, field.NewPath("^"), field.NewPath("^"), nil, + func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool { + if simpleLocation.String() == statusField { + hasStatus = true + return true + } + return false + }) + + if hasStatus { + errsToReport = append(errsToReport, fmt.Sprintf("crd/%v version/%v field/%v must have a status subresource in .spec.version[name=%v].subresources.status to match its schema.", crd.Name, newVersion.Name, statusField, newVersion.Name)) + } + + } + + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: errsToReport, + Warnings: nil, + Infos: nil, + }, nil +} + +func (b mustHaveStatus) Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + return RatchetCompare(b, existingCRD, newCRD) +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_bools.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_bools.go new file mode 100644 index 000000000..1be7ea9ff --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_bools.go @@ -0,0 +1,58 @@ +package manifestcomparators + +import ( + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type noBools struct{} + +func NoBools() CRDComparator { + return noBools{} +} + +func (noBools) Name() string { + return "NoBools" +} + +func (noBools) WhyItMatters() string { + return "Booleans rarely stay booleans and can never develop new options. This frequently leads to cases where there " + + "are multiple boolean fields, with some combinations of values not being allowed. Additionally, strings provide " + + "expressive names and values, describing degrees or conditions of a thing. Also, booleans cannot be defaulted, " + + "pointers to booleans can be, but at that point you've already got a tri-state, so it's not a boolean is it..." +} + +func (b noBools) Validate(crd *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + errsToReport := []string{} + + for _, newVersion := range crd.Spec.Versions { + newBoolFields := []string{} + SchemaHas(newVersion.Schema.OpenAPIV3Schema, field.NewPath("^"), field.NewPath("^"), nil, + func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool { + if s.Type == "boolean" { + newBoolFields = append(newBoolFields, simpleLocation.String()) + } + return false + }) + + for _, newBoolField := range newBoolFields { + errsToReport = append(errsToReport, fmt.Sprintf("crd/%v version/%v field/%v may not be a boolean", crd.Name, newVersion.Name, newBoolField)) + } + + } + + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: errsToReport, + Warnings: nil, + Infos: nil, + }, nil +} + +func (b noBools) Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + return RatchetCompare(b, existingCRD, newCRD) +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_enum_removal.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_enum_removal.go new file mode 100644 index 000000000..632f0147f --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_enum_removal.go @@ -0,0 +1,85 @@ +package manifestcomparators + +import ( + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type noEnumRemoval struct{} + +func NoEnumRemoval() CRDComparator { + return noEnumRemoval{} +} + +func (noEnumRemoval) Name() string { + return "NoEnumRemoval" +} + +func (noEnumRemoval) WhyItMatters() string { + return "If enums are removed, then clients that use those enum values will not be able to upgrade to the newest CRD." +} + +func getEnums(version *apiextensionsv1.CustomResourceDefinitionVersion) map[string]sets.String { + enumsMap := make(map[string]sets.String) + SchemaHas(version.Schema.OpenAPIV3Schema, field.NewPath("^"), field.NewPath("^"), nil, + func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool { + for _, enum := range s.Enum { + _, exists := enumsMap[simpleLocation.String()] + if !exists { + enumsMap[simpleLocation.String()] = sets.NewString() + } + enumsMap[simpleLocation.String()].Insert(string(enum.Raw)) + } + return false + }) + + return enumsMap +} + +func (b noEnumRemoval) Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + if existingCRD == nil { + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: nil, + Warnings: nil, + Infos: nil, + }, nil + } + errsToReport := []string{} + + for _, newVersion := range newCRD.Spec.Versions { + + existingVersion := GetVersionByName(existingCRD, newVersion.Name) + if existingVersion == nil { + continue + } + + existingEnumsMap := getEnums(existingVersion) + newEnumsMap := getEnums(&newVersion) + + for field, existingEnums := range existingEnumsMap { + newEnums, exists := newEnumsMap[field] + if exists { + removedEnums := existingEnums.Difference(newEnums) + for _, removedEnum := range removedEnums.List() { + errsToReport = append(errsToReport, fmt.Sprintf("crd/%v version/%v enum/%v may not be removed for field/%v", newCRD.Name, newVersion.Name, removedEnum, field)) + } + } + } + + } + + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: errsToReport, + Warnings: nil, + Infos: nil, + }, nil +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_field_removal.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_field_removal.go new file mode 100644 index 000000000..d345809bd --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_field_removal.go @@ -0,0 +1,74 @@ +package manifestcomparators + +import ( + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type noFieldRemoval struct{} + +func NoFieldRemoval() CRDComparator { + return noFieldRemoval{} +} + +func (noFieldRemoval) Name() string { + return "NoFieldRemoval" +} + +func (noFieldRemoval) WhyItMatters() string { + return "If fields are removed, then clients that rely on those fields will not be able to read them or write them." +} + +func getFields(version *apiextensionsv1.CustomResourceDefinitionVersion) sets.String { + fields := sets.NewString() + SchemaHas(version.Schema.OpenAPIV3Schema, field.NewPath("^"), field.NewPath("^"), nil, + func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool { + fields.Insert(simpleLocation.String()) + return false + }) + + return fields +} + +func (b noFieldRemoval) Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + if existingCRD == nil { + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: nil, + Warnings: nil, + Infos: nil, + }, nil + } + errsToReport := []string{} + + for _, newVersion := range newCRD.Spec.Versions { + + existingVersion := GetVersionByName(existingCRD, newVersion.Name) + if existingVersion == nil { + continue + } + + existingFields := getFields(existingVersion) + newFields := getFields(&newVersion) + + removedFields := existingFields.Difference(newFields) + for _, removedField := range removedFields.List() { + errsToReport = append(errsToReport, fmt.Sprintf("crd/%v version/%v field/%v may not be removed", newCRD.Name, newVersion.Name, removedField)) + } + + } + + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: errsToReport, + Warnings: nil, + Infos: nil, + }, nil +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_floats.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_floats.go new file mode 100644 index 000000000..fe2b8a6a4 --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_floats.go @@ -0,0 +1,56 @@ +package manifestcomparators + +import ( + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type noFloats struct{} + +func NoFloats() CRDComparator { + return noFloats{} +} + +func (noFloats) Name() string { + return "NoFloats" +} + +func (noFloats) WhyItMatters() string { + return "Floating-point values cannot be reliably round-tripped (encoded and re-decoded) without changing, " + + "and have varying precision and representations across languages and architectures." +} + +func (b noFloats) Validate(crd *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + errsToReport := []string{} + + for _, newVersion := range crd.Spec.Versions { + newFloatFields := []string{} + SchemaHas(newVersion.Schema.OpenAPIV3Schema, field.NewPath("^"), field.NewPath("^"), nil, + func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool { + if s.Type == "number" { + newFloatFields = append(newFloatFields, simpleLocation.String()) + } + return false + }) + + for _, newFloatField := range newFloatFields { + errsToReport = append(errsToReport, fmt.Sprintf("crd/%v version/%v field/%v may not be a float", crd.Name, newVersion.Name, newFloatField)) + } + + } + + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: errsToReport, + Warnings: nil, + Infos: nil, + }, nil +} + +func (b noFloats) Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + return RatchetCompare(b, existingCRD, newCRD) +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_maps.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_maps.go new file mode 100644 index 000000000..f87d4f34a --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_maps.go @@ -0,0 +1,62 @@ +package manifestcomparators + +import ( + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type noMaps struct{} + +func NoMaps() CRDComparator { + return noMaps{} +} + +func (noMaps) Name() string { + return "NoMaps" +} + +func (noMaps) WhyItMatters() string { + return "When serialized into yaml or json, maps don't have \"names\" associated with their key. This makes " + + "it less obvious what the key of map means or what is for. Additionally, maps are not guaranteed stable " + + "for serialization, but lists are always ordered. Instead of maps, use lists with a field that functions as " + + "a key and use a listMapKey marker for server-side-apply." +} + +func (b noMaps) Validate(crd *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + errsToReport := []string{} + + for _, newVersion := range crd.Spec.Versions { + newMapFields := []string{} + SchemaHas(newVersion.Schema.OpenAPIV3Schema, field.NewPath("^"), field.NewPath("^"), nil, + func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool { + if s.Type == "object" { + // I think this is how openapi v3 marks maps: https://swagger.io/docs/specification/data-models/dictionaries/ + // "normal" objects appear to use properties, not additionalProperties. + if s.AdditionalProperties != nil { + newMapFields = append(newMapFields, simpleLocation.String()) + } + } + return false + }) + + for _, newMapField := range newMapFields { + errsToReport = append(errsToReport, fmt.Sprintf("crd/%v version/%v field/%v may not be a map", crd.Name, newVersion.Name, newMapField)) + } + + } + + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: errsToReport, + Warnings: nil, + Infos: nil, + }, nil +} + +func (b noMaps) Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + return RatchetCompare(b, existingCRD, newCRD) +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_new_required_fields.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_new_required_fields.go new file mode 100644 index 000000000..9a21915fa --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_new_required_fields.go @@ -0,0 +1,168 @@ +package manifestcomparators + +import ( + "fmt" + "strings" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type noNewRequiredFields struct{} + +func NoNewRequiredFields() CRDComparator { + return noNewRequiredFields{} +} + +func (noNewRequiredFields) Name() string { + return "NoNewRequiredFields" +} + +func (noNewRequiredFields) WhyItMatters() string { + return "If new fields are required, then old clients will not function properly. Even if CRD defaulting is used, " + + "CRD defaulting requires allowing an object with an empty or missing value to then get defaulted." +} + +func (b noNewRequiredFields) Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + if existingCRD == nil { + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: nil, + Warnings: nil, + Infos: nil, + }, nil + } + errsToReport := []string{} + + for _, newVersion := range newCRD.Spec.Versions { + + existingVersion := GetVersionByName(existingCRD, newVersion.Name) + if existingVersion == nil { + continue + } + + existingRequiredFields := map[string]sets.String{} + existingSimpleLocationToJSONSchemaProps := map[string]*apiextensionsv1.JSONSchemaProps{} + SchemaHas(existingVersion.Schema.OpenAPIV3Schema, field.NewPath("^"), field.NewPath("^"), nil, + func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool { + existingRequiredFields[simpleLocation.String()] = sets.NewString(s.Required...) + existingSimpleLocationToJSONSchemaProps[simpleLocation.String()] = s + return false + }) + + // New fields can be required if they are wrapped inside new structs that are themselves optional. + // For instance, you cannot add .spec.thingy as required, but if you add .spec.top as optional and at the same + // time add .spec.top.thingy as required, this is allowed. + // Similar logic exists for adding an array with minlength > 0 + newRequiredFields := sets.NewString() + newSimpleLocationToRequiredFields := map[string]sets.String{} + newToSimpleLocation := map[*apiextensionsv1.JSONSchemaProps]*field.Path{} + SchemaHas(newVersion.Schema.OpenAPIV3Schema, field.NewPath("^"), field.NewPath("^"), nil, + func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, ancestors []*apiextensionsv1.JSONSchemaProps) bool { + newSimpleLocationToRequiredFields[simpleLocation.String()] = sets.NewString(s.Required...) + newToSimpleLocation[s] = simpleLocation + + if s.Type == "array" { + // if it's an array, we have a different property to check. A new array cannot be required unless it's ancestor is new. + if s.MinLength == nil || *s.MinLength == 0 { + // if there is no required length, this is fine + return false + } + // this means we're an array with a minLength, check to see if any parent wrapper is both new and optional. + if isAnyAncestorNewAndNullable(ancestors, existingSimpleLocationToJSONSchemaProps, newToSimpleLocation, newSimpleLocationToRequiredFields) { + return false + } + + // if we search all ancestors and couldn't find a new, optional element, then the current array cannot + // have a minLength greater than zero. + newRequiredFields.Insert(fmt.Sprintf("%s", simpleLocation.String())) + return false + } + + if len(s.Required) == 0 { + // if nothing is required, nothing to check. + return false + } + + existingRequired, existedBefore := existingRequiredFields[simpleLocation.String()] + if !existedBefore && s.Nullable { + // if the parent of the required field (current element) didn't exist in the schema before AND + // if the parent of the required field is nullable (client doesn't have to set it), + // then we can allow a child to be required. + return false + } + + if isAnyAncestorNewAndNullable(ancestors, existingSimpleLocationToJSONSchemaProps, newToSimpleLocation, newSimpleLocationToRequiredFields) { + // if any ancestor of the parent of the required field is new and nullable, then required is allowed. + return false + } + + // this covers newly required fields. + newRequired := sets.NewString(s.Required...) + if disallowedRequired := newRequired.Difference(existingRequired); len(disallowedRequired) > 0 { + for _, curr := range disallowedRequired.List() { + newRequiredFields.Insert(fmt.Sprintf("%s.%s", simpleLocation.String(), curr)) + } + return false + } + + return false + }) + + for _, newRequiredField := range newRequiredFields.List() { + errsToReport = append(errsToReport, fmt.Sprintf("crd/%v version/%v field/%v is new and may not be required", newCRD.Name, newVersion.Name, newRequiredField)) + } + + } + + return ComparisonResults{ + Name: b.Name(), + WhyItMatters: b.WhyItMatters(), + + Errors: errsToReport, + Warnings: nil, + Infos: nil, + }, nil +} + +func isAnyAncestorNewAndNullable( + ancestors []*apiextensionsv1.JSONSchemaProps, + existingSimpleLocationToJSONSchemaProps map[string]*apiextensionsv1.JSONSchemaProps, + newToSimpleLocation map[*apiextensionsv1.JSONSchemaProps]*field.Path, + newSimpleLocationToRequiredFields map[string]sets.String) bool { + + for i := len(ancestors) - 1; i >= 0; i-- { + ancestor := ancestors[i] + ancestorSimpleName := newToSimpleLocation[ancestor] + isOptionalArray := ancestor.Type == "array" && (ancestor.MinLength == nil || *ancestor.MinLength == 0) + isAncestoryOptional := ancestor.Nullable || isOptionalArray + if !isAncestoryOptional { + // if this ancestor isn't nullable, then it cannot allow the current element to be required + continue + } + + if _, existed := existingSimpleLocationToJSONSchemaProps[ancestorSimpleName.String()]; existed { + // if this ancestor previously existed, then it cannot allow the current element to be required + continue + } + if i == 0 { + // if the current accessor is the top level and Nullable, then it isn't required + return true + } + + // does the current ancestor require + parentOfAncestor := ancestors[i-1] + tokens := strings.Split(ancestorSimpleName.String(), ".") + lastStep := tokens[len(tokens)-1] + prevAncestorRequiredFields := newSimpleLocationToRequiredFields[newToSimpleLocation[parentOfAncestor].String()] + if !prevAncestorRequiredFields.Has(lastStep) { + // the current ancestor is not required, then we're ok and don't need to search further + return true + } + } + + return false +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_uints.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_uints.go new file mode 100644 index 000000000..d3b0e1482 --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/comp_no_uints.go @@ -0,0 +1,55 @@ +package manifestcomparators + +import ( + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type noUints struct{} + +func NoUints() CRDComparator { + return noUints{} +} + +func (noUints) Name() string { + return "NoUints" +} + +func (noUints) WhyItMatters() string { + return "Unsigned integers don't have consistent support across languages and libraries." +} + +func (n noUints) Validate(crd *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + errsToReport := []string{} + + for _, newVersion := range crd.Spec.Versions { + uintFields := []string{} + SchemaHas(newVersion.Schema.OpenAPIV3Schema, field.NewPath("^"), field.NewPath("^"), nil, + func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool { + if s.Format == "uint" { + uintFields = append(uintFields, simpleLocation.String()) + } + return false + }) + + for _, newUintField := range uintFields { + errsToReport = append(errsToReport, fmt.Sprintf("crd/%v version/%v field/%v may not be a uint", crd.Name, newVersion.Name, newUintField)) + } + + } + + return ComparisonResults{ + Name: n.Name(), + WhyItMatters: n.WhyItMatters(), + + Errors: errsToReport, + Warnings: nil, + Infos: nil, + }, nil +} + +func (n noUints) Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + return RatchetCompare(n, existingCRD, newCRD) +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/helpers.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/helpers.go new file mode 100644 index 000000000..3f34711c2 --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/helpers.go @@ -0,0 +1,121 @@ +package manifestcomparators + +import ( + "sync" + + "k8s.io/apimachinery/pkg/util/validation/field" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" +) + +// GetVersionByName can be nil if the version doesn't exist +func GetVersionByName(crd *apiextensionsv1.CustomResourceDefinition, versionName string) *apiextensionsv1.CustomResourceDefinitionVersion { + if crd == nil { + return nil + } + + for i := range crd.Spec.Versions { + if crd.Spec.Versions[i].Name == versionName { + return &crd.Spec.Versions[i] + } + } + + return nil +} + +// ancestry is an order list of ancestors of s, where index 0 is the root and index len-1 is the direct parent +type SchemaWalkerFunc func(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, ancestry []*apiextensionsv1.JSONSchemaProps) bool + +// SchemaHas recursively traverses the Schema and calls the `pred` +// predicate to see if the schema contains specific values. +// +// The predicate MUST NOT keep a copy of the json schema NOR modify the +// schema. +func SchemaHas(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, ancestry []*apiextensionsv1.JSONSchemaProps, pred SchemaWalkerFunc) bool { + if s == nil { + return false + } + + if pred(s, fldPath, simpleLocation, ancestry) { + return true + } + + nextAncestry := append(ancestry, s) + + if s.Items != nil { + if s.Items != nil && schemaHasRecurse(s.Items.Schema, fldPath.Child("items"), simpleLocation.Key("*"), nextAncestry, pred) { + return true + } + for i := range s.Items.JSONSchemas { + if schemaHasRecurse(&s.Items.JSONSchemas[i], fldPath.Child("items", "jsonSchemas").Index(i), simpleLocation.Index(i), nextAncestry, pred) { + return true + } + } + } + for i := range s.AllOf { + if schemaHasRecurse(&s.AllOf[i], fldPath.Child("allOf").Index(i), simpleLocation, nextAncestry, pred) { + return true + } + } + for i := range s.AnyOf { + if schemaHasRecurse(&s.AnyOf[i], fldPath.Child("anyOf").Index(i), simpleLocation, nextAncestry, pred) { + return true + } + } + for i := range s.OneOf { + if schemaHasRecurse(&s.OneOf[i], fldPath.Child("oneOf").Index(i), simpleLocation, nextAncestry, pred) { + return true + } + } + if schemaHasRecurse(s.Not, fldPath.Child("not"), simpleLocation, nextAncestry, pred) { + return true + } + for propertyName, s := range s.Properties { + if schemaHasRecurse(&s, fldPath.Child("properties").Key(propertyName), simpleLocation.Child(propertyName), nextAncestry, pred) { + return true + } + } + if s.AdditionalProperties != nil { + if schemaHasRecurse(s.AdditionalProperties.Schema, fldPath.Child("additionalProperties", "schema"), simpleLocation.Key("*"), nextAncestry, pred) { + return true + } + } + for patternName, s := range s.PatternProperties { + if schemaHasRecurse(&s, fldPath.Child("allOf").Key(patternName), simpleLocation, nextAncestry, pred) { + return true + } + } + if s.AdditionalItems != nil { + if schemaHasRecurse(s.AdditionalItems.Schema, fldPath.Child("additionalItems", "schema"), simpleLocation, nextAncestry, pred) { + return true + } + } + for _, s := range s.Definitions { + if schemaHasRecurse(&s, fldPath.Child("definitions"), simpleLocation, nextAncestry, pred) { + return true + } + } + for dependencyName, d := range s.Dependencies { + if schemaHasRecurse(d.Schema, fldPath.Child("dependencies").Key(dependencyName).Child("schema"), simpleLocation, nextAncestry, pred) { + return true + } + } + + return false +} + +var schemaPool = sync.Pool{ + New: func() any { + return new(apiextensionsv1.JSONSchemaProps) + }, +} + +func schemaHasRecurse(s *apiextensionsv1.JSONSchemaProps, fldPath, simpleLocation *field.Path, ancestry []*apiextensionsv1.JSONSchemaProps, pred SchemaWalkerFunc) bool { + if s == nil { + return false + } + schema := schemaPool.Get().(*apiextensionsv1.JSONSchemaProps) + defer schemaPool.Put(schema) + *schema = *s + return SchemaHas(schema, fldPath, simpleLocation, ancestry, pred) +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/interfaces.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/interfaces.go new file mode 100644 index 000000000..4cc10424d --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/interfaces.go @@ -0,0 +1,32 @@ +package manifestcomparators + +import apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + +type ComparisonResults struct { + Name string `yaml:"name"` + WhyItMatters string `yaml:"whyItMatters"` + + Errors []string `yaml:"errors"` + Warnings []string `yaml:"warnings"` + Infos []string `yaml:"infos"` +} + +type CRDComparator interface { + Name() string + WhyItMatters() string + Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) +} + +type SingleCRDValidator interface { + Validate(crd *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) +} + +type CRDComparatorRegistry interface { + AddComparator(comparator CRDComparator) error + GetComparator(name string) (CRDComparator, error) + + KnownComparators() []string + AllComparators() []CRDComparator + + Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition, names ...string) ([]ComparisonResults, []error) +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/racheting_validator.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/racheting_validator.go new file mode 100644 index 000000000..a79e15a40 --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/racheting_validator.go @@ -0,0 +1,53 @@ +package manifestcomparators + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" +) + +func RatchetCompare(validator SingleCRDValidator, existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition) (ComparisonResults, error) { + var oldResults ComparisonResults + if existingCRD != nil { + var err error + oldResults, err = validator.Validate(existingCRD) + if err != nil { + return ComparisonResults{}, err + } + } + + newResults, err := validator.Validate(newCRD) + if err != nil { + return ComparisonResults{}, err + } + + ret := ComparisonResults{ + Name: newResults.Name, + WhyItMatters: newResults.WhyItMatters, + Errors: stringDiff(newResults.Errors, oldResults.Errors), + Warnings: stringDiff(newResults.Warnings, oldResults.Warnings), + Infos: stringDiff(newResults.Infos, oldResults.Infos), + } + + return ret, nil +} + +func stringDiff(s1 []string, s2 []string) []string { + ret := []string{} + for _, curr := range s1 { + if stringListContains(s2, curr) { + continue + } + + ret = append(ret, curr) + } + + return ret +} + +func stringListContains(haystack []string, needle string) bool { + for _, straw := range haystack { + if straw == needle { + return true + } + } + return false +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/registry.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/registry.go new file mode 100644 index 000000000..e060bbabd --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/registry.go @@ -0,0 +1,80 @@ +package manifestcomparators + +import ( + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/sets" +) + +type crdComparatorRegistry struct { + comparators map[string]CRDComparator +} + +func NewRegistry() CRDComparatorRegistry { + return &crdComparatorRegistry{ + comparators: map[string]CRDComparator{}, + } +} + +func (r *crdComparatorRegistry) AddComparator(comparator CRDComparator) error { + if _, ok := r.comparators[comparator.Name()]; ok { + return fmt.Errorf("comparator/%v is already registered", comparator.Name()) + } + + r.comparators[comparator.Name()] = comparator + return nil +} + +func (r *crdComparatorRegistry) GetComparator(name string) (CRDComparator, error) { + ret, ok := r.comparators[name] + if !ok { + return nil, fmt.Errorf("comparator/%v is not registered", name) + + } + return ret, nil +} + +func (r *crdComparatorRegistry) KnownComparators() []string { + keys := sets.StringKeySet(r.comparators) + return keys.List() +} + +func (r *crdComparatorRegistry) AllComparators() []CRDComparator { + ret := []CRDComparator{} + + keys := sets.StringKeySet(r.comparators) + for _, name := range keys.List() { + ret = append(ret, r.comparators[name]) + } + + return ret +} + +func (r *crdComparatorRegistry) Compare(existingCRD, newCRD *apiextensionsv1.CustomResourceDefinition, names ...string) ([]ComparisonResults, []error) { + comparators := []CRDComparator{} + if len(names) == 0 { + comparators = r.AllComparators() + } else { + for _, name := range names { + comparator, err := r.GetComparator(name) + if err != nil { + return nil, []error{err} + } + comparators = append(comparators, comparator) + } + } + + ret := []ComparisonResults{} + errs := []error{} + for _, comparator := range comparators { + currResults, err := comparator.Compare(existingCRD, newCRD) + if err != nil { + errs = append(errs, err) + continue + } + ret = append(ret, currResults) + } + + return ret, errs +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/simple_tester.go b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/simple_tester.go new file mode 100644 index 000000000..24e3bafd8 --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/manifestcomparators/simple_tester.go @@ -0,0 +1,292 @@ +package manifestcomparators + +import ( + "bufio" + "bytes" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/openshift/crd-schema-checker/pkg/resourceread" + "gopkg.in/yaml.v2" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" +) + +func AllTestsInDir(directory string) ([]ComparatorTest, error) { + ret := []ComparatorTest{} + err := filepath.WalkDir(directory, func(path string, info os.DirEntry, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + return nil + } + + if containsDirectory, err := containsDir(path); err != nil { + return err + } else if containsDirectory { + return nil + } + + // so now we have only leave nodes + relativePath, err := filepath.Rel(directory, path) + if err != nil { + return err + } + + currTest, err := TestInDir(relativePath, path) + if err != nil { + return err + } + ret = append(ret, currTest) + + return nil + }) + if err != nil { + return nil, err + } + + return ret, nil +} + +func AllTestsInDirForComparator(comparator CRDComparator, directory string) ([]*simpleComparatorTest, error) { + registry := NewRegistry() + registry.AddComparator(comparator) + return AllTestsInDirForRegistry(registry, directory) +} + +func RunAllTestsInDirForComparator(t *testing.T, comparator CRDComparator, directory string) { + tests, err := AllTestsInDirForComparator(comparator, directory) + if err != nil { + t.Fatal(err) + } + + for _, test := range tests { + t.Run(test.ComparatorTest.Name, test.Test) + } +} + +func RunAllTestsInDirForRegistry(t *testing.T, registry CRDComparatorRegistry, directory string) { + tests, err := AllTestsInDirForRegistry(registry, directory) + if err != nil { + t.Fatal(err) + } + + for _, test := range tests { + t.Run(test.ComparatorTest.Name, test.Test) + } +} + +func AllTestsInDirForRegistry(registry CRDComparatorRegistry, directory string) ([]*simpleComparatorTest, error) { + tests, err := AllTestsInDir(directory) + if err != nil { + return nil, err + } + ret := []*simpleComparatorTest{} + + for i := range tests { + ret = append(ret, &simpleComparatorTest{ + ComparatorTest: tests[i], + registry: registry, + }) + } + + return ret, nil +} + +func TestInDir(testName, directory string) (ComparatorTest, error) { + ret := ComparatorTest{ + Name: testName, + } + + optionalExistingCRDFile := filepath.Join(directory, "existing.yaml") + existingBytes, err := os.ReadFile(optionalExistingCRDFile) + if err != nil && !os.IsNotExist(err) { + return ComparatorTest{}, err + } + if len(existingBytes) > 0 { + crd, err := resourceread.ReadCustomResourceDefinitionV1(existingBytes) + if err != nil { + return ComparatorTest{}, err + } + ret.ExistingCRD = crd + } + + requiredNewCRDFile := filepath.Join(directory, "new.yaml") + newBytes, err := os.ReadFile(requiredNewCRDFile) + if err != nil { + return ComparatorTest{}, err + } + newCRD, err := resourceread.ReadCustomResourceDefinitionV1(newBytes) + if err != nil { + return ComparatorTest{}, err + } + ret.NewCRD = newCRD + + optionalExpectedFile := filepath.Join(directory, "expected.yaml") + expectedBytes, err := os.ReadFile(optionalExpectedFile) + if err != nil && !os.IsNotExist(err) { + return ComparatorTest{}, err + } + if len(expectedBytes) > 0 { + expected := &ComparisonResultsList{} + if err := yaml.Unmarshal(expectedBytes, expected); err != nil { + return ComparatorTest{}, err + } + ret.ExpectedResults = expected.Items + } + + optionalExpectedErrorsFile := filepath.Join(directory, "errors.txt") + expectedErrorsBytes, err := os.ReadFile(optionalExpectedErrorsFile) + if err != nil && !os.IsNotExist(err) { + return ComparatorTest{}, err + } + if len(expectedErrorsBytes) > 0 { + expectedErrors := []string{} + scanner := bufio.NewScanner(bytes.NewBuffer(expectedErrorsBytes)) + for scanner.Scan() { + expectedErrors = append(expectedErrors, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return ComparatorTest{}, err + } + ret.ExpectedErrors = expectedErrors + } + + return ret, nil +} + +func containsDir(path string) (bool, error) { + entries, err := os.ReadDir(path) + if err != nil { + return false, err + } + for _, entry := range entries { + if entry.IsDir() { + return true, nil + } + } + return false, nil +} + +type ComparisonResultsList struct { + Items []ComparisonResults `yaml:"items"` +} + +// ComparatorTest represents the directory style test we have. +type ComparatorTest struct { + Name string + ExistingCRD *apiextensionsv1.CustomResourceDefinition + NewCRD *apiextensionsv1.CustomResourceDefinition + + ExpectedResults []ComparisonResults + ExpectedErrors []string +} + +type simpleComparatorTest struct { + ComparatorTest ComparatorTest + registry CRDComparatorRegistry +} + +func (tc *simpleComparatorTest) Test(t *testing.T) { + actualResults, actualErrors := tc.registry.Compare(tc.ComparatorTest.ExistingCRD, tc.ComparatorTest.NewCRD) + tc.ComparatorTest.Test(t, actualResults, actualErrors) +} + +func (tc *ComparatorTest) Test(t *testing.T, actualResults []ComparisonResults, actualErrors []error) { + switch { + case len(tc.ExpectedErrors) == 0 && len(actualErrors) == 0: + case len(tc.ExpectedErrors) == 0 && len(actualErrors) != 0: + t.Fatalf("0 errors expected, got %v", actualErrors) + case len(tc.ExpectedErrors) != 0 && len(actualErrors) == 0: + t.Fatalf("expected some errors: %v, got none", tc.ExpectedErrors) + case len(tc.ExpectedErrors) != 0 && len(actualErrors) != 0: + if !reflect.DeepEqual(tc.ExpectedErrors, actualErrors) { + t.Fatalf("expected some errors: %v, got different errors: %v", tc.ExpectedErrors, actualErrors) + } + } + + // check to be sure that every expected message appeared + for _, expected := range tc.ExpectedResults { + expectedBytes, err := yaml.Marshal(expected) + if err != nil { + t.Error(err) + } + + actualPtr := findResultsForComparator(expected.Name, actualResults) + if actualPtr == nil { + // this is only an error when we expect a message + if len(expected.Errors) == 0 && len(expected.Warnings) == 0 && len(expected.Infos) == 0 { + continue + } + t.Errorf("missing expectedResults[%v]: expected\n%v\n", expected.Name, string(expectedBytes)) + continue + } + + actual := *actualPtr + actualBytes, err := yaml.Marshal(actual) + if err != nil { + t.Error(err) + } + noErrorsAsExpected := len(expected.Errors) == 0 && len(actual.Errors) == 0 + if !noErrorsAsExpected && !reflect.DeepEqual(expected.Errors, actual.Errors) { + t.Errorf("mismatched errors for expectedResults[%v]: expected\n%v\n, got\n%v\n", expected.Name, string(expectedBytes), string(actualBytes)) + } + noWarningsAsExpected := len(expected.Warnings) == 0 && len(actual.Warnings) == 0 + if !noWarningsAsExpected && !reflect.DeepEqual(expected.Warnings, actual.Warnings) { + t.Errorf("mismatched warnings for expectedResults[%v]: expected\n%v\n, got\n%v\n", expected.Name, string(expectedBytes), string(actualBytes)) + } + noInfosAsExpected := len(expected.Infos) == 0 && len(actual.Infos) == 0 + if !noInfosAsExpected && !reflect.DeepEqual(expected.Infos, actual.Infos) { + t.Errorf("mismatched infos for expectedResults[%v]: expected\n%v\n, got\n%v\n", expected.Name, string(expectedBytes), string(actualBytes)) + } + } + + // check to be sure that we didn't get an extra message + for _, actual := range actualResults { + actualBytes, err := yaml.Marshal(actual) + if err != nil { + t.Error(err) + } + + expectedPtr := findResultsForComparator(actual.Name, tc.ExpectedResults) + if expectedPtr == nil { + // this is only an error when we expect a message + if len(actual.Errors) == 0 && len(actual.Warnings) == 0 && len(actual.Infos) == 0 { + continue + } + t.Errorf("missing expectedResults for actual[%v]: got\n%v\n", actual.Name, string(actualBytes)) + continue + } + + expected := *expectedPtr + expectedBytes, err := yaml.Marshal(expected) + if err != nil { + t.Error(err) + } + noErrorsAsExpected := len(expected.Errors) == 0 && len(actual.Errors) == 0 + if !noErrorsAsExpected && !reflect.DeepEqual(expected.Errors, actual.Errors) { + t.Errorf("mismatched errors for expectedResults[%v]: expected\n%v\n, got\n%v\n", expected.Name, string(expectedBytes), string(actualBytes)) + } + noWarningsAsExpected := len(expected.Warnings) == 0 && len(actual.Warnings) == 0 + if !noWarningsAsExpected && !reflect.DeepEqual(expected.Warnings, actual.Warnings) { + t.Errorf("mismatched warnings for expectedResults[%v]: expected\n%v\n, got\n%v\n", expected.Name, string(expectedBytes), string(actualBytes)) + } + noInfosAsExpected := len(expected.Infos) == 0 && len(actual.Infos) == 0 + if !noInfosAsExpected && !reflect.DeepEqual(expected.Infos, actual.Infos) { + t.Errorf("mismatched infos for expectedResults[%v]: expected\n%v\n, got\n%v\n", expected.Name, string(expectedBytes), string(actualBytes)) + } + } + +} + +func findResultsForComparator(name string, results []ComparisonResults) *ComparisonResults { + for i := range results { + if results[i].Name == name { + return &results[i] + } + } + + return nil +} diff --git a/vendor/github.com/openshift/crd-schema-checker/pkg/resourceread/apiextensions.go b/vendor/github.com/openshift/crd-schema-checker/pkg/resourceread/apiextensions.go new file mode 100644 index 000000000..824da55aa --- /dev/null +++ b/vendor/github.com/openshift/crd-schema-checker/pkg/resourceread/apiextensions.go @@ -0,0 +1,37 @@ +package resourceread + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var ( + apiExtensionsScheme = runtime.NewScheme() + apiExtensionsCodecs = serializer.NewCodecFactory(apiExtensionsScheme) +) + +func init() { + utilruntime.Must(apiextensionsv1.AddToScheme(apiExtensionsScheme)) +} + +func ReadCustomResourceDefinitionV1(objBytes []byte) (*apiextensionsv1.CustomResourceDefinition, error) { + requiredObj, err := runtime.Decode(apiExtensionsCodecs.UniversalDecoder(apiextensionsv1.SchemeGroupVersion), objBytes) + if err != nil { + return nil, err + } + return requiredObj.(*apiextensionsv1.CustomResourceDefinition), nil +} + +func ReadCustomResourceDefinitionV1OrDie(objBytes []byte) *apiextensionsv1.CustomResourceDefinition { + requiredObj, err := runtime.Decode(apiExtensionsCodecs.UniversalDecoder(apiextensionsv1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return requiredObj.(*apiextensionsv1.CustomResourceDefinition) +} + +func WriteCustomResourceDefinitionV1OrDie(obj *apiextensionsv1.CustomResourceDefinition) string { + return runtime.EncodeOrDie(apiExtensionsCodecs.LegacyCodec(apiextensionsv1.SchemeGroupVersion), obj) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 43993551e..62961e8e8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -136,6 +136,10 @@ github.com/modern-go/reflect2 github.com/munnerz/goautoneg # github.com/nxadm/tail v1.4.8 ## explicit; go 1.13 +# github.com/openshift/crd-schema-checker v0.0.0-20240404194209-35a9033b1d11 +## explicit; go 1.20 +github.com/openshift/crd-schema-checker/pkg/manifestcomparators +github.com/openshift/crd-schema-checker/pkg/resourceread # github.com/pmezard/go-difflib v1.0.0 ## explicit github.com/pmezard/go-difflib/difflib