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

Validate KUDO installation before running commands #1113

Merged
merged 13 commits into from
Dec 2, 2019
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) {
nfnt marked this conversation as resolved.
Show resolved Hide resolved
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
20 changes: 20 additions & 0 deletions pkg/kudoctl/cmd/init/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package init

import (
"fmt"
"os"

"github.com/kudobuilder/kudo/pkg/kudoctl/clog"
"github.com/kudobuilder/kudo/pkg/kudoctl/kube"
Expand Down Expand Up @@ -82,6 +83,25 @@ func Install(client *kube.Client, opts Options, crdOnly bool) error {
return nil
}

func ValidateManager(client *kube.Client, opts Options) error {
ANeumann82 marked this conversation as resolved.
Show resolved Hide resolved
s := generateDeployment(opts)
set, err := client.KubeClient.AppsV1().StatefulSets(opts.Namespace).Get(s.Name, metav1.GetOptions{})

if err != nil {
if os.IsTimeout(err) {
return err
}
return fmt.Errorf("failed to retrieve KUDO manager: %v", err)
}
expectedImage := s.Spec.Template.Spec.Containers[0].Image
actualImage := set.Spec.Template.Spec.Containers[0].Image
if actualImage != expectedImage {
return fmt.Errorf("deployed KUDO manager image %s differes from expected image %s", actualImage, expectedImage)
}

return nil
}

// Install uses Kubernetes client to install KUDO.
func installManager(client kubernetes.Interface, opts Options) error {
if err := installStatefulSet(client.AppsV1(), opts); err != nil {
Expand Down
12 changes: 8 additions & 4 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
ValidateInstallation bool
}

// DefaultSettings initializes the settings to its defaults
var DefaultSettings = &Settings{
Namespace: "default",
RequestTimeout: 0,
Namespace: "default",
RequestTimeout: 0,
ValidateInstallation: 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.ValidateInstallation, "validate-install", true, "ValidateInstallation KUDO installation before running.")
ANeumann82 marked this conversation as resolved.
Show resolved Hide resolved
}

// 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.ValidateInstallation)
}
45 changes: 21 additions & 24 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) {
ANeumann82 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't mind adding a test for this method actually verifying the behavior with validate=true 👼

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a unit test, but I have a hard time coming up with a test in the current structure. I'll start a refactoring PR next, and going to tackle the whole testing thing there.


// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
Expand All @@ -42,44 +43,40 @@ 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)
if err != nil {
return nil, err
}

// use the apiextensions clientset to check for the existence of KUDO CRDs in the cluster
extensionsClientset, err := extensionsclient.NewForConfig(config)
kubeClient, err := kube.GetKubeClient(kubeConfigPath)
if err != nil {
return nil, err
return nil, clog.Errorf("could not get Kubernetes client: %s", err)
}

_, err = extensionsClientset.CustomResourceDefinitions().Get("operators.kudo.dev", v1.GetOptions{})
err = kudoinit.ValidateManager(kubeClient, kudoinit.NewOptions("", ""))
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.
// timeout is not a wrappable error, timeout is an underlying issue that is NOT validation specific,
// there is no value in wrapping or converting as well. best to provide the actual error for proper reporting.
if os.IsTimeout(err) {
return nil, err
}
return nil, fmt.Errorf("operators crd: %w", err)
clog.V(0).Printf("KUDO manager not correctly installed. Do you need to run kudo init?")
if validateInstall {
nfnt marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("KUDO manager invalid: %v", err)
}
}

_, err = extensionsClientset.CustomResourceDefinitions().Get("operatorversions.kudo.dev", v1.GetOptions{})
err = kudoinit.CRDs().ValidateInstallation(kubeClient)
if err != nil {
// timeout details above for first CRD
// see above
if os.IsTimeout(err) {
return nil, err
}
return nil, fmt.Errorf("operatorversions crd: %w", err)
clog.V(0).Printf("Cluster CRDS are not set up correctly. Do you need to run kudo init?")
ANeumann82 marked this conversation as resolved.
Show resolved Hide resolved
if validateInstall {
ANeumann82 marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("CRDs invalid: %v", 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
ANeumann82 marked this conversation as resolved.
Show resolved Hide resolved
}

return &Client{
Expand Down