Skip to content

Commit

Permalink
Add generic crd upgrade safety preflight check
Browse files Browse the repository at this point in the history
and some initial validations for handling scope changes
and removal of existing stored versions

Signed-off-by: everettraven <everettraven@gmail.com>
  • Loading branch information
everettraven committed Mar 28, 2024
1 parent f249ba3 commit e12d91d
Show file tree
Hide file tree
Showing 37 changed files with 17,140 additions and 6 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ require (
golang.org/x/net v0.22.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.29.3
k8s.io/apiextensions-apiserver v0.29.3
k8s.io/apimachinery v0.29.3
k8s.io/client-go v0.29.3
k8s.io/component-helpers v0.29.1
k8s.io/component-helpers v0.29.3
sigs.k8s.io/yaml v1.4.0
)

Expand Down
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
Expand Down Expand Up @@ -510,12 +510,14 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
k8s.io/apiextensions-apiserver v0.29.3 h1:9HF+EtZaVpFjStakF4yVufnXGPRppWFEQ87qnO91YeI=
k8s.io/apiextensions-apiserver v0.29.3/go.mod h1:po0XiY5scnpJfFizNGo6puNU6Fq6D70UJY2Cb2KwAVc=
k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU=
k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU=
k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg=
k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
k8s.io/component-helpers v0.29.1 h1:54MMEDu6xeJmMtAKztsPwu0kJKr4+jCUzaEIn2UXRoc=
k8s.io/component-helpers v0.29.1/go.mod h1:+I7xz4kfUgxWAPJIVKrqe4ml4rb9UGpazlOmhXYo+cY=
k8s.io/component-helpers v0.29.3 h1:1dqZswuZgT2ZMixYeORyCUOAApXxgsvjVSgfoUT+P4o=
k8s.io/component-helpers v0.29.3/go.mod h1:yiDqbRQrnQY+sPju/bL7EkwDJb6LVOots53uZNMZBos=
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
Expand Down
2 changes: 2 additions & 0 deletions pkg/kapp/cmd/kapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
cmdcore "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/cmd/core"
cmdsa "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/cmd/serviceaccount"
cmdtools "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/cmd/tools"
"github.com/vmware-tanzu/carvel-kapp/pkg/kapp/crdupgradesafety"
"github.com/vmware-tanzu/carvel-kapp/pkg/kapp/logger"
"github.com/vmware-tanzu/carvel-kapp/pkg/kapp/permissions"
"github.com/vmware-tanzu/carvel-kapp/pkg/kapp/preflight"
Expand Down Expand Up @@ -57,6 +58,7 @@ func NewDefaultKappCmd(ui *ui.ConfUI) *cobra.Command {
func defaultKappPreflightRegistry(depsFactory cmdcore.DepsFactory) *preflight.Registry {
registry := preflight.NewRegistry(map[string]preflight.Check{
"PermissionValidation": permissions.NewPreflight(depsFactory, false),
"CRDUpgradeSafety": crdupgradesafety.NewPreflight(depsFactory, false),
})

return registry
Expand Down
113 changes: 113 additions & 0 deletions pkg/kapp/crdupgradesafety/preflight.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2024 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package crdupgradesafety

import (
"context"
"errors"
"fmt"

cmdcore "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/cmd/core"
ctldgraph "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/diffgraph"
"github.com/vmware-tanzu/carvel-kapp/pkg/kapp/preflight"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)

var _ preflight.Check = (*Preflight)(nil)

// Preflight is an implementation of preflight.Check
// to make it easier to add crd upgrade validation
// as a preflight check
type Preflight struct {
depsFactory cmdcore.DepsFactory
enabled bool
validator *Validator
}

func NewPreflight(df cmdcore.DepsFactory, enabled bool) *Preflight {
return &Preflight{
depsFactory: df,
enabled: enabled,
validator: &Validator{
Validations: []Validation{
NewValidationFunc("NoScopeChange", NoScopeChange),
NewValidationFunc("NoStoredVersionRemoved", NoStoredVersionRemoved),
},
},
}
}

func (p *Preflight) Enabled() bool {
return p.enabled
}

func (p *Preflight) SetEnabled(enabled bool) {
p.enabled = enabled
}

func (p *Preflight) SetConfig(_ preflight.CheckConfig) error {
return nil
}

func (p *Preflight) Run(ctx context.Context, changeGraph *ctldgraph.ChangeGraph) error {
dCli, err := p.depsFactory.DynamicClient(cmdcore.DynamicClientOpts{})
if err != nil {
return fmt.Errorf("getting dynamic client: %w", err)
}
crdCli := dCli.Resource(v1.SchemeGroupVersion.WithResource("customresourcedefinitions"))

validateErrs := []error{}
for _, change := range changeGraph.All() {
// Loop through all the changes looking for "upsert" operations on
// a CRD. "upsert" is used for create + update operations
if change.Change.Op() != ctldgraph.ActualChangeOpUpsert {
continue
}
res := change.Change.Resource()
if res.GroupVersion().WithKind(res.Kind()) != v1.SchemeGroupVersion.WithKind("CustomResourceDefinition") {
continue
}

// to properly determine if this is an update operation, attempt to fetch
// the "old" CRD from the cluster
uOldCRD, err := crdCli.Get(ctx, res.Name(), metav1.GetOptions{})
if err != nil {
// if the resource is not found, this "upsert" operation
// translates to a "create" request being made. Skip this change
if apierrors.IsNotFound(err) {
continue
}

return fmt.Errorf("checking for existing CRD resource: %w", err)
}

oldCRD := &v1.CustomResourceDefinition{}
s := runtime.NewScheme()
if err := v1.AddToScheme(s); err != nil {
return fmt.Errorf("adding apiextension apis to scheme: %w", err)
}
if err := s.Convert(uOldCRD, oldCRD, nil); err != nil {
return fmt.Errorf("couldn't convert old CRD resource to a CRD object: %w", err)
}

newCRD := &v1.CustomResourceDefinition{}
if err := res.AsUncheckedTypedObj(newCRD); err != nil {
return fmt.Errorf("couldn't convert new CRD resource to a CRD object: %w", err)
}

if err = p.validator.Validate(*oldCRD, *newCRD); err != nil {
validateErrs = append(validateErrs, err)
}
}

if len(validateErrs) > 0 {
baseErr := errors.New("validation for safe CRD upgrades failed")
return errors.Join(append([]error{baseErr}, validateErrs...)...)
}

return nil
}
93 changes: 93 additions & 0 deletions pkg/kapp/crdupgradesafety/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2024 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package crdupgradesafety

import (
"errors"
"fmt"

v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/util/sets"
)

// Validation is a representation of a validation to run
// against a CRD being upgraded
type Validation interface {
// Validate contains the actual validation logic. An error being
// returned means validation has failed
Validate(old, new v1.CustomResourceDefinition) error
// Name returns a human-readable name for the validation
Name() string
}

// ValidateFunc is a function to validate a CustomResourceDefinition
// for safe upgrades. It accepts the old and new CRDs and returns an
// error if performing an upgrade from old -> new is unsafe.
type ValidateFunc func(old, new v1.CustomResourceDefinition) error

// ValidationFunc is a helper to wrap a ValidateFunc
// as an implementation of the Validation interface
type ValidationFunc struct {
name string
validateFunc ValidateFunc
}

func NewValidationFunc(name string, vfunc ValidateFunc) Validation {
return &ValidationFunc{
name: name,
validateFunc: vfunc,
}
}

func (vf *ValidationFunc) Name() string {
return vf.name
}

func (vf *ValidationFunc) Validate(old, new v1.CustomResourceDefinition) error {
return vf.validateFunc(old, new)
}

type Validator struct {
Validations []Validation
}

func (v *Validator) Validate(old, new v1.CustomResourceDefinition) error {
validateErrs := []error{}
for _, validation := range v.Validations {
if err := validation.Validate(old, new); err != nil {
formattedErr := fmt.Errorf("CustomResourceDefinition %s failed upgrade safety validation. %q validation failed: %w",
new.Name, validation.Name(), err)

validateErrs = append(validateErrs, formattedErr)
}
}
if len(validateErrs) > 0 {
return errors.Join(validateErrs...)
}
return nil
}

func NoScopeChange(old, new v1.CustomResourceDefinition) error {
if old.Spec.Scope != new.Spec.Scope {
return fmt.Errorf("scope changed from %q to %q", old.Spec.Scope, new.Spec.Scope)
}
return nil
}

func NoStoredVersionRemoved(old, new v1.CustomResourceDefinition) error {
newVersions := sets.New[string]()
for _, version := range new.Spec.Versions {
if !newVersions.Has(version.Name) {
newVersions.Insert(version.Name)
}
}

for _, storedVersion := range old.Status.StoredVersions {
if !newVersions.Has(storedVersion) {
return fmt.Errorf("stored version %q removed", storedVersion)
}
}

return nil
}

0 comments on commit e12d91d

Please sign in to comment.