Skip to content

🐛 fix crdupgradesafety diff when items schema differ #1863

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

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module github.com/operator-framework/operator-controller
go 1.23.4

require (
carvel.dev/kapp v0.64.0
github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/semver/v3 v3.3.1
github.com/blang/semver/v4 v4.0.0
Expand All @@ -17,6 +16,7 @@ require (
github.com/klauspost/compress v1.18.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/openshift/crd-schema-checker v0.0.0-20240404194209-35a9033b1d11
github.com/operator-framework/api v0.30.0
github.com/operator-framework/helm-operator-plugins v0.8.0
github.com/operator-framework/operator-registry v1.50.0
Expand All @@ -43,7 +43,6 @@ require (
)

require (
carvel.dev/vendir v0.40.0 // indirect
cel.dev/expr v0.18.0 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
Expand Down Expand Up @@ -76,9 +75,6 @@ require (
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/containers/ocicrypt v1.2.0 // indirect
github.com/containers/storage v1.56.1 // indirect
github.com/cppforlife/cobrautil v0.0.0-20221130162803-acdfead391ef // indirect
github.com/cppforlife/color v1.9.1-0.20200716202919-6706ac40b835 // indirect
github.com/cppforlife/go-cli-ui v0.0.0-20220425131040-94f26b16bc14 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
Expand Down Expand Up @@ -133,16 +129,13 @@ require (
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/joelanford/ignore v0.1.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 // indirect
github.com/k14s/ytt v0.36.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
Expand Down Expand Up @@ -176,7 +169,6 @@ require (
github.com/oklog/ulid v1.3.1 // indirect
github.com/onsi/gomega v1.36.2 // indirect
github.com/opencontainers/runtime-spec v1.2.0 // indirect
github.com/openshift/crd-schema-checker v0.0.0-20240404194209-35a9033b1d11 // indirect
github.com/operator-framework/operator-lib v0.17.0 // indirect
github.com/otiai10/copy v1.14.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
Expand All @@ -203,8 +195,6 @@ require (
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
github.com/vbauerster/mpb/v8 v8.8.3 // indirect
github.com/vito/go-interact v1.0.1 // indirect
github.com/vmware-tanzu/carvel-kapp-controller v0.51.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
Expand Down
282 changes: 0 additions & 282 deletions go.sum

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Originally copied from https://github.com/carvel-dev/kapp/tree/d7fc2e15439331aa3a379485bb124e91a0829d2e
// Attribution:
// Copyright 2024 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package crdupgradesafety

import (
"errors"
"fmt"
"reflect"

"github.com/openshift/crd-schema-checker/pkg/manifestcomparators"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
)

// ChangeValidation is a function that accepts a FieldDiff
// as a parameter and should return:
// - a boolean representation of whether or not the change
// - an error if the change would be unsafe
// has been fully handled (i.e no additional changes exist)
type ChangeValidation func(diff FieldDiff) (bool, error)

// ChangeValidator is a Validation implementation focused on
// handling updates to existing fields in a CRD
type ChangeValidator struct {
// Validations is a slice of ChangeValidations
// to run against each changed field
Validations []ChangeValidation
}

func (cv *ChangeValidator) Name() string {
return "ChangeValidator"
}

// Validate will compare each version in the provided existing and new CRDs.
// Since the ChangeValidator is tailored to handling updates to existing fields in
// each version of a CRD. As such the following is assumed:
// - Validating the removal of versions during an update is handled outside of this
// validator. If a version in the existing version of the CRD does not exist in the new
// version that version of the CRD is skipped in this validator.
// - Removal of existing fields is unsafe. Regardless of whether or not this is handled
// by a validator outside this one, if a field is present in a version provided by the existing CRD
// but not present in the same version provided by the new CRD this validation will fail.
//
// Additionally, any changes that are not validated and handled by the known ChangeValidations
// are deemed as unsafe and returns an error.
func (cv *ChangeValidator) Validate(old, new apiextensionsv1.CustomResourceDefinition) error {
errs := []error{}
for _, version := range old.Spec.Versions {
newVersion := manifestcomparators.GetVersionByName(&new, version.Name)
if newVersion == nil {
// if the new version doesn't exist skip this version
continue
}
flatOld := FlattenSchema(version.Schema.OpenAPIV3Schema)
flatNew := FlattenSchema(newVersion.Schema.OpenAPIV3Schema)

diffs, err := CalculateFlatSchemaDiff(flatOld, flatNew)
if err != nil {
errs = append(errs, fmt.Errorf("calculating schema diff for CRD version %q", version.Name))
continue
}

for field, diff := range diffs {
handled := false
for _, validation := range cv.Validations {
ok, err := validation(diff)
if err != nil {
errs = append(errs, fmt.Errorf("version %q, field %q: %w", version.Name, field, err))
}
if ok {
handled = true
break
}
}

if !handled {
errs = append(errs, fmt.Errorf("version %q, field %q has unknown change, refusing to determine that change is safe", version.Name, field))
}
}
}

if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}

type FieldDiff struct {
Old *apiextensionsv1.JSONSchemaProps
New *apiextensionsv1.JSONSchemaProps
}

// FlatSchema is a flat representation of a CRD schema.
type FlatSchema map[string]*apiextensionsv1.JSONSchemaProps

// FlattenSchema takes in a CRD version OpenAPIV3Schema and returns
// a flattened representation of it. For example, a CRD with a schema of:
// ```yaml
//
// ...
// spec:
// type: object
// properties:
// foo:
// type: string
// bar:
// type: string
// ...
//
// ```
// would be represented as:
//
// map[string]*apiextensionsv1.JSONSchemaProps{
// "^": {},
// "^.spec": {},
// "^.spec.foo": {},
// "^.spec.bar": {},
// }
//
// where "^" represents the "root" schema
func FlattenSchema(schema *apiextensionsv1.JSONSchemaProps) FlatSchema {
fieldMap := map[string]*apiextensionsv1.JSONSchemaProps{}

manifestcomparators.SchemaHas(schema,
field.NewPath("^"),
field.NewPath("^"),
nil,
func(s *apiextensionsv1.JSONSchemaProps, _, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool {
fieldMap[simpleLocation.String()] = s.DeepCopy()
return false
})

return fieldMap
}

// CalculateFlatSchemaDiff finds fields in a FlatSchema that are different
// and returns a mapping of field --> old and new field schemas. If a field
// exists in the old FlatSchema but not the new an empty diff mapping and an error is returned.
func CalculateFlatSchemaDiff(o, n FlatSchema) (map[string]FieldDiff, error) {
diffMap := map[string]FieldDiff{}
for field, schema := range o {
if _, ok := n[field]; !ok {
return diffMap, fmt.Errorf("field %q in existing not found in new", field)
}
newSchema := n[field]

// Copy the schemas and remove any child properties for comparison.
// In theory this will focus in on detecting changes for only the
// field we are looking at and ignore changes in the children fields.
// Since we are iterating through the map that should have all fields
// we should still detect changes in the children fields.
oldCopy := schema.DeepCopy()
newCopy := newSchema.DeepCopy()
oldCopy.Properties, oldCopy.Items = nil, nil
newCopy.Properties, newCopy.Items = nil, nil
if !reflect.DeepEqual(oldCopy, newCopy) {
diffMap[field] = FieldDiff{
Old: oldCopy,
New: newCopy,
}
}
}
return diffMap, nil
}
Loading
Loading