Skip to content

Commit

Permalink
Validate KUDO CRDs before running commands (#1113)
Browse files Browse the repository at this point in the history
* Verify that the correct and expected CRDs are installed before executing any commands
* Added parameter to ignore the validation
  • Loading branch information
ANeumann82 committed Dec 2, 2019
1 parent 06c92b7 commit 1e9e07d
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 36 deletions.
2 changes: 1 addition & 1 deletion pkg/kudoctl/clog/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func (v Verbose) Printf(format string, args ...interface{}) {
func InitWithFlags(f *pflag.FlagSet, out io.Writer) {
// allows for initialization of writer in testing without CLI flags
if f != nil {
f.VarP(&logging.verbosity, "v", "v", "log level for V logs")
f.VarP(&logging.verbosity, "v", "v", "Log level for V logs")
}
logging.out = out
}
Expand Down
36 changes: 33 additions & 3 deletions pkg/kudoctl/cmd/init/crds.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package init

import (
"fmt"
"os"
"strings"

"github.com/kudobuilder/kudo/pkg/kudoctl/clog"
"github.com/kudobuilder/kudo/pkg/kudoctl/kube"

apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
Expand Down Expand Up @@ -31,6 +34,20 @@ func installCrds(client apiextensionsclient.Interface) error {
return nil
}

func validateInstallation(client v1beta1.CustomResourceDefinitionsGetter, crd *apiextv1beta1.CustomResourceDefinition) error {
existingCrd, err := client.CustomResourceDefinitions().Get(crd.Name, v1.GetOptions{})
if err != nil {
if os.IsTimeout(err) {
return err
}
return fmt.Errorf("failed to retrieve CRD %s: %v", crd.Name, err)
}
if existingCrd.Spec.Version != crd.Spec.Version {
return fmt.Errorf("installed CRD %s has invalid version %s, expected %s", crd.Name, existingCrd.Spec.Version, crd.Spec.Version)
}
return nil
}

func install(client v1beta1.CustomResourceDefinitionsGetter, crd *apiextv1beta1.CustomResourceDefinition) error {
_, err := client.CustomResourceDefinitions().Create(crd)
if kerrors.IsAlreadyExists(err) {
Expand Down Expand Up @@ -211,9 +228,9 @@ func generateCrd(kind string, plural string) *apiextv1beta1.CustomResourceDefini

// KudoCrds represents custom resource definitions needed to run KUDO
type KudoCrds struct {
Operator runtime.Object
OperatorVersion runtime.Object
Instance runtime.Object
Operator *apiextv1beta1.CustomResourceDefinition
OperatorVersion *apiextv1beta1.CustomResourceDefinition
Instance *apiextv1beta1.CustomResourceDefinition
}

// AsArray returns all CRDs as array of runtime objects
Expand All @@ -236,6 +253,19 @@ func (c KudoCrds) AsYaml() ([]string, error) {
return manifests, nil
}

func (c KudoCrds) ValidateInstallation(client *kube.Client) error {
if err := validateInstallation(client.ExtClient.ApiextensionsV1beta1(), c.Operator); err != nil {
return err
}
if err := validateInstallation(client.ExtClient.ApiextensionsV1beta1(), c.OperatorVersion); err != nil {
return err
}
if err := validateInstallation(client.ExtClient.ApiextensionsV1beta1(), c.Instance); err != nil {
return err
}
return nil
}

// CRDs returns the runtime.Object representation of all the CRDs KUDO requires
func CRDs() KudoCrds {
return KudoCrds{
Expand Down
8 changes: 6 additions & 2 deletions pkg/kudoctl/env/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,24 @@ type Settings struct {
Namespace string
// RequestTimeout is the timeout value (in seconds) when making API calls via the KUDO client
RequestTimeout int64
// Validate KUDO installation before creating a KUDO client
Validate bool
}

// DefaultSettings initializes the settings to its defaults
var DefaultSettings = &Settings{
Namespace: "default",
RequestTimeout: 0,
Validate: true,
}

// AddFlags binds flags to the given flagset.
func (s *Settings) AddFlags(fs *pflag.FlagSet) {
fs.StringVar((*string)(&s.Home), "home", kudoHome(), "location of your KUDO config.")
fs.StringVar((*string)(&s.Home), "home", kudoHome(), "Location of your KUDO config.")
fs.StringVar(&s.KubeConfig, "kubeconfig", kubeConfigHome(), "Path to your Kubernetes configuration file.")
fs.StringVarP(&s.Namespace, "namespace", "n", "default", "Target namespace for the object.")
fs.Int64Var(&s.RequestTimeout, "request-timeout", 0, "Request timeout value, in seconds. Defaults to 0 (unlimited)")
fs.BoolVar(&s.Validate, "validate-install", true, "Validate KUDO installation before running.")
}

// OverrideDefault used for deviations from global defaults
Expand All @@ -68,5 +72,5 @@ func (s *Settings) OverrideDefault(fs *pflag.FlagSet, name, value string) string

// GetClient is a helper function that takes the Settings struct and returns a new KUDO Client
func GetClient(s *Settings) (*kudo.Client, error) {
return kudo.NewClient(s.KubeConfig, s.RequestTimeout)
return kudo.NewClient(s.KubeConfig, s.RequestTimeout, s.Validate)
}
42 changes: 13 additions & 29 deletions pkg/kudoctl/util/kudo/kudo.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import (
"github.com/kudobuilder/kudo/pkg/apis/kudo/v1beta1"
"github.com/kudobuilder/kudo/pkg/client/clientset/versioned"
"github.com/kudobuilder/kudo/pkg/kudoctl/clog"
kudoinit "github.com/kudobuilder/kudo/pkg/kudoctl/cmd/init"
"github.com/kudobuilder/kudo/pkg/kudoctl/kube"
"github.com/kudobuilder/kudo/pkg/util/kudo"
"github.com/kudobuilder/kudo/pkg/version"

v1core "k8s.io/api/core/v1"
extensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -31,7 +32,7 @@ type Client struct {
}

// NewClient creates new KUDO Client
func NewClient(kubeConfigPath string, requestTimeout int64) (*Client, error) {
func NewClient(kubeConfigPath string, requestTimeout int64, validateInstall bool) (*Client, error) {

// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
Expand All @@ -42,44 +43,27 @@ func NewClient(kubeConfigPath string, requestTimeout int64) (*Client, error) {
// set default configs
config.Timeout = time.Duration(requestTimeout) * time.Second

// create the clientset
kudoClientset, err := versioned.NewForConfig(config)
kubeClient, err := kube.GetKubeClient(kubeConfigPath)
if err != nil {
return nil, err
return nil, clog.Errorf("could not get Kubernetes client: %s", err)
}

// use the apiextensions clientset to check for the existence of KUDO CRDs in the cluster
extensionsClientset, err := extensionsclient.NewForConfig(config)
err = kudoinit.CRDs().ValidateInstallation(kubeClient)
if err != nil {
return nil, err
}

_, err = extensionsClientset.CustomResourceDefinitions().Get("operators.kudo.dev", v1.GetOptions{})
if err != nil {
// timeout is not a wrappable error, timeout is an underlying issue that is NOT CRD specific, there is no value in wrapping or converting as well.
// best to provide the actual error for proper reporting.
// see above
if os.IsTimeout(err) {
return nil, err
}
return nil, fmt.Errorf("operators crd: %w", err)
}

_, err = extensionsClientset.CustomResourceDefinitions().Get("operatorversions.kudo.dev", v1.GetOptions{})
if err != nil {
// timeout details above for first CRD
if os.IsTimeout(err) {
return nil, err
clog.V(0).Printf("KUDO CRDs are not set up correctly. Do you need to run kudo init?")
if validateInstall {
return nil, fmt.Errorf("CRDs invalid: %v", err)
}
return nil, fmt.Errorf("operatorversions crd: %w", err)
}

_, err = extensionsClientset.CustomResourceDefinitions().Get("instances.kudo.dev", v1.GetOptions{})
// create the clientset
kudoClientset, err := versioned.NewForConfig(config)
if err != nil {
// timeout details above for first CRD
if os.IsTimeout(err) {
return nil, err
}
return nil, fmt.Errorf("instances crd: %w", err)
return nil, err
}

return &Client{
Expand Down
15 changes: 14 additions & 1 deletion pkg/kudoctl/util/kudo/kudo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ func newTestSimpleK2o() *Client {
return NewClientFromK8s(fake.NewSimpleClientset())
}

func TestKudoClientValidate(t *testing.T) {
tests := []struct {
err string
}{
{"CRDs invalid: failed to retrieve CRD"}, // verify that NewClient tries to validate CRDs
}

for _, tt := range tests {
_, err := NewClient("testdata/test-config", 0, true)
assert.ErrorContains(t, err, tt.err)
}
}

func TestNewK2oClient(t *testing.T) {
tests := []struct {
err string
Expand All @@ -28,7 +41,7 @@ func TestNewK2oClient(t *testing.T) {

for _, tt := range tests {
// Just interested in errors
_, err := NewClient("", 0)
_, err := NewClient("", 0, false)
assert.ErrorContains(t, err, tt.err)
}
}
Expand Down
14 changes: 14 additions & 0 deletions pkg/kudoctl/util/kudo/testdata/test-config
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
clusters:
- cluster:
server: 127.0.0.1:1
name: cluster
contexts:
- context:
cluster: cluster
user: user
name: cluster
current-context: cluster
preferences: {}
users:
- name: user
user: {}

0 comments on commit 1e9e07d

Please sign in to comment.