Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

wip: no new required field with crd-schema-checker #919

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions pkg/kapp/crdupgradesafety/preflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func NewPreflight(df cmdcore.DepsFactory, enabled bool) *Preflight {
Validations: []Validation{
NewValidationFunc("NoScopeChange", NoScopeChange),
NewValidationFunc("NoStoredVersionRemoved", NoStoredVersionRemoved),
NewValidationFunc("NoNewRequiredField", NoNewRequiredField),
},
},
}
Expand Down
28 changes: 28 additions & 0 deletions pkg/kapp/crdupgradesafety/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -91,3 +93,29 @@ func NoStoredVersionRemoved(old, new v1.CustomResourceDefinition) error {

return nil
}

func NoNewRequiredField(old, new v1.CustomResourceDefinition) error {
reg := manifestcomparators.NewRegistry()
err := reg.AddComparator(manifestcomparators.NoNewRequiredFields())
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
}
216 changes: 180 additions & 36 deletions pkg/kapp/crdupgradesafety/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ 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"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestValidator(t *testing.T) {
Expand All @@ -24,15 +26,15 @@ 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
}),
},
},
{
name: "failing validator, error",
validations: []Validation{
NewValidationFunc("fail", func(_, _ v1.CustomResourceDefinition) error {
NewValidationFunc("fail", func(_, _ apiextensionsv1.CustomResourceDefinition) error {
return errors.New("boom")
}),
},
Expand All @@ -41,10 +43,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")
}),
},
Expand All @@ -55,7 +57,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)
Expand All @@ -66,33 +68,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,
Expand All @@ -108,28 +110,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",
},
Expand All @@ -139,8 +141,8 @@ func TestNoStoredVersionRemoved(t *testing.T) {
},
},
},
old: v1.CustomResourceDefinition{
Status: v1.CustomResourceDefinitionStatus{
old: apiextensionsv1.CustomResourceDefinition{
Status: apiextensionsv1.CustomResourceDefinitionStatus{
StoredVersions: []string{
"v1alpha1",
},
Expand All @@ -149,17 +151,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",
},
Expand All @@ -174,3 +176,145 @@ func TestNoStoredVersionRemoved(t *testing.T) {
})
}
}

func TestNoNewRequiredField(t *testing.T) {
for _, tc := range []struct {
name string
new apiextensionsv1.CustomResourceDefinition
old apiextensionsv1.CustomResourceDefinition
shouldError bool
}{
{
name: "no field added, no error",
old: apiextensionsv1.CustomResourceDefinition{
ObjectMeta: v1.ObjectMeta{
Name: "tests.nofieldremove.com",
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "nofieldremove.com",
Names: apiextensionsv1.CustomResourceDefinitionNames{
Kind: "Test",
ListKind: "TestList",
Plural: "tests",
Singular: "test",
},
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: "v1alpha1",
Schema: &apiextensionsv1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1.JSONSchemaProps{
"fieldOne": {
Type: "string",
},
},
},
},
},
},
},
},
new: apiextensionsv1.CustomResourceDefinition{
ObjectMeta: v1.ObjectMeta{
Name: "tests.nofieldremove.com",
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "nofieldremove.com",
Names: apiextensionsv1.CustomResourceDefinitionNames{
Kind: "Test",
ListKind: "TestList",
Plural: "tests",
Singular: "test",
},
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: "v1alpha1",
Schema: &apiextensionsv1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1.JSONSchemaProps{
"fieldOne": {
Type: "string",
},
},
},
},
},
},
},
},
},
{
name: "required field added, error",
old: apiextensionsv1.CustomResourceDefinition{
ObjectMeta: v1.ObjectMeta{
Name: "tests.nofieldremove.com",
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "nofieldremove.com",
Names: apiextensionsv1.CustomResourceDefinitionNames{
Kind: "Test",
ListKind: "TestList",
Plural: "tests",
Singular: "test",
},
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: "v1alpha1",
Schema: &apiextensionsv1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1.JSONSchemaProps{
"fieldOne": {
Type: "string",
},
},
},
},
},
},
},
},
new: apiextensionsv1.CustomResourceDefinition{
ObjectMeta: v1.ObjectMeta{
Name: "tests.nofieldremove.com",
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "nofieldremove.com",
Names: apiextensionsv1.CustomResourceDefinitionNames{
Kind: "Test",
ListKind: "TestList",
Plural: "tests",
Singular: "test",
},
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: "v1alpha1",
Schema: &apiextensionsv1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1.JSONSchemaProps{
"fieldOne": {
Type: "string",
},
"fieldTwo": {
Type: "string",
},
},
Required: []string{"fieldTwo"},
},
},
},
},
},
},
shouldError: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
err := NoNewRequiredField(tc.old, tc.new)
assert.Equal(t, tc.shouldError, err != nil)
})
}
}
Loading
Loading