diff --git a/config/samples/first-operator/templates/deployment.yaml b/config/samples/first-operator/templates/deployment.yaml index d615d13a8..73b0ad025 100644 --- a/config/samples/first-operator/templates/deployment.yaml +++ b/config/samples/first-operator/templates/deployment.yaml @@ -6,7 +6,7 @@ spec: selector: matchLabels: app: nginx - replicas: {{ .Params.Replicas }} # tells deployment to run 2 pods matching the template + replicas: {{ .Params.replicas }} # tells deployment to run 2 pods matching the template template: metadata: labels: diff --git a/go.mod b/go.mod index 62d549885..24daddc6f 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 // indirect github.com/pkg/errors v0.8.1 github.com/pmezard/go-difflib v1.0.0 - github.com/rogpeppe/go-internal v1.2.2 // indirect + github.com/rogpeppe/go-internal v1.2.2 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 github.com/spf13/viper v1.4.0 diff --git a/pkg/kudoctl/cmd/install.go b/pkg/kudoctl/cmd/install.go index a4746642a..234f783ef 100644 --- a/pkg/kudoctl/cmd/install.go +++ b/pkg/kudoctl/cmd/install.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "strings" "github.com/kudobuilder/kudo/pkg/kudoctl/cmd/install" "github.com/pkg/errors" @@ -32,47 +31,6 @@ var ( kubectl kudo install kafka --version=1.1.1` ) -// getParameterMap takes a slice of parameter strings, parses parameters into a map of keys and values -func getParameterMap(raw []string) (map[string]string, error) { - var errs []string - parameters := make(map[string]string) - - for _, a := range raw { - key, value, err := parseParameter(a) - if err != nil { - errs = append(errs, *err) - continue - } - parameters[key] = value - } - - if errs != nil { - return nil, errors.New(strings.Join(errs, ", ")) - } - - return parameters, nil -} - -// parseParameter does all the parsing logic for an instance of a parameter provided to the command line -// it expects `=` as a delimiter as in key=value. It separates keys from values as a return. Any unexpected param will result in a -// detailed error message. -func parseParameter(raw string) (key string, param string, err *string) { - - var errMsg string - s := strings.SplitN(raw, "=", 2) - if len(s) < 2 { - errMsg = fmt.Sprintf("parameter not set: %+v", raw) - } else if s[0] == "" { - errMsg = fmt.Sprintf("parameter name can not be empty: %+v", raw) - } else if s[1] == "" { - errMsg = fmt.Sprintf("parameter value can not be empty: %+v", raw) - } - if errMsg != "" { - return "", "", &errMsg - } - return s[0], s[1], nil -} - // newInstallCmd creates the install command for the CLI func newInstallCmd() *cobra.Command { options := install.DefaultOptions @@ -85,7 +43,7 @@ func newInstallCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { // Prior to command execution we parse and validate passed parameters var err error - options.Parameters, err = getParameterMap(parameters) + options.Parameters, err = install.GetParameterMap(parameters) if err != nil { return errors.WithMessage(err, "could not parse parameters") } diff --git a/pkg/kudoctl/cmd/install/install.go b/pkg/kudoctl/cmd/install/install.go index 508f6720b..507b73f0f 100644 --- a/pkg/kudoctl/cmd/install/install.go +++ b/pkg/kudoctl/cmd/install/install.go @@ -49,19 +49,19 @@ func validate(args []string, options *Options) error { return nil } -// getPackageCRDs tries to look for package files resolving the operator name to: +// GetPackageCRDs tries to look for package files resolving the operator name to: // - a local tar.gz file // - a local directory // - a url to a tar.gz // - a operator name in the remote repository // in that order. Should there exist a local folder e.g. `cassandra` it will take precedence // over the remote repository package with the same name. -func getPackageCRDs(name string, options *Options, repository repo.Repository) (*bundle.PackageCRDs, error) { +func GetPackageCRDs(name string, version string, repository repo.Repository) (*bundle.PackageCRDs, error) { // Local files/folder have priority if _, err := os.Stat(name); err == nil { f := finder.NewLocal() - b, err := f.GetBundle(name, options.PackageVersion) + b, err := f.GetBundle(name, version) if err != nil { return nil, err } @@ -70,14 +70,14 @@ func getPackageCRDs(name string, options *Options, repository repo.Repository) ( if http.IsValidURL(name) { f := finder.NewURL() - b, err := f.GetBundle(name, options.PackageVersion) + b, err := f.GetBundle(name, version) if err != nil { return nil, err } return b.GetCRDs() } - b, err := repository.GetBundle(name, options.PackageVersion) + b, err := repository.GetBundle(name, version) if err != nil { return nil, err } @@ -96,7 +96,7 @@ func installOperator(operatorArgument string, options *Options) error { return errors.Wrap(err, "creating kudo client") } - crds, err := getPackageCRDs(operatorArgument, options, repository) + crds, err := GetPackageCRDs(operatorArgument, options.PackageVersion, repository) if err != nil { return errors.Wrapf(err, "failed to resolve package CRDs for operator: %s", operatorArgument) } @@ -131,7 +131,7 @@ func installCrds(crds *bundle.PackageCRDs, kc *kudo.Client, options *Options) er if err != nil { return errors.Wrap(err, "retrieving existing operator versions") } - if !versionExists(versionsInstalled, operatorVersion) { + if !VersionExists(versionsInstalled, operatorVersion) { // this version does not exist in the cluster if err := installSingleOperatorVersionToCluster(operatorName, options.Namespace, kc, crds.OperatorVersion); err != nil { return errors.Wrapf(err, "installing OperatorVersion CRD for operator: %s", operatorName) @@ -190,7 +190,8 @@ func validateCrds(crds *bundle.PackageCRDs, skipInstance bool) error { return nil } -func versionExists(versions []string, currentVersion string) bool { +// VersionExists looks for string version inside collection of versions +func VersionExists(versions []string, currentVersion string) bool { for _, v := range versions { if v == currentVersion { return true diff --git a/pkg/kudoctl/cmd/install/params.go b/pkg/kudoctl/cmd/install/params.go new file mode 100644 index 000000000..286a0e1a8 --- /dev/null +++ b/pkg/kudoctl/cmd/install/params.go @@ -0,0 +1,48 @@ +package install + +import ( + "errors" + "fmt" + "strings" +) + +// GetParameterMap takes a slice of parameter strings, parses parameters into a map of keys and values +func GetParameterMap(raw []string) (map[string]string, error) { + var errs []string + parameters := make(map[string]string) + + for _, a := range raw { + key, value, err := parseParameter(a) + if err != nil { + errs = append(errs, *err) + continue + } + parameters[key] = value + } + + if errs != nil { + return nil, errors.New(strings.Join(errs, ", ")) + } + + return parameters, nil +} + +// parseParameter does all the parsing logic for an instance of a parameter provided to the command line +// it expects `=` as a delimiter as in key=value. It separates keys from values as a return. Any unexpected param will result in a +// detailed error message. +func parseParameter(raw string) (key string, param string, err *string) { + + var errMsg string + s := strings.SplitN(raw, "=", 2) + if len(s) < 2 { + errMsg = fmt.Sprintf("parameter not set: %+v", raw) + } else if s[0] == "" { + errMsg = fmt.Sprintf("parameter name can not be empty: %+v", raw) + } else if s[1] == "" { + errMsg = fmt.Sprintf("parameter value can not be empty: %+v", raw) + } + if errMsg != "" { + return "", "", &errMsg + } + return s[0], s[1], nil +} diff --git a/pkg/kudoctl/cmd/install/params_test.go b/pkg/kudoctl/cmd/install/params_test.go new file mode 100644 index 000000000..987e87e72 --- /dev/null +++ b/pkg/kudoctl/cmd/install/params_test.go @@ -0,0 +1,30 @@ +package install + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var parameterParsingTests = []struct { + paramStr string + key string + value string + err string +}{ + {"foo", "", "", "parameter not set: foo"}, + {"foo=", "", "", "parameter value can not be empty: foo="}, + {"=bar", "", "", "parameter name can not be empty: =bar"}, + {"foo=bar", "foo", "bar", ""}, +} + +func TestTableParameterParsing(t *testing.T) { + for _, test := range parameterParsingTests { + key, value, err := parseParameter(test.paramStr) + assert.Equal(t, key, test.key) + assert.Equal(t, value, test.value) + if err != nil { + assert.Equal(t, *err, test.err) + } + } +} diff --git a/pkg/kudoctl/cmd/install_test.go b/pkg/kudoctl/cmd/install_test.go index 0d9570c29..0b2fb7294 100644 --- a/pkg/kudoctl/cmd/install_test.go +++ b/pkg/kudoctl/cmd/install_test.go @@ -49,26 +49,3 @@ func TestTableNewInstallCmd_WithParameters(t *testing.T) { assert.NotNil(t, err, test.errorMessage) } } - -var parameterParsingTests = []struct { - paramStr string - key string - value string - err string -}{ - {"foo", "", "", "parameter not set: foo"}, - {"foo=", "", "", "parameter value can not be empty: foo="}, - {"=bar", "", "", "parameter name can not be empty: =bar"}, - {"foo=bar", "foo", "bar", ""}, -} - -func TestTableParameterParsing(t *testing.T) { - for _, test := range parameterParsingTests { - key, value, err := parseParameter(test.paramStr) - assert.Equal(t, key, test.key) - assert.Equal(t, value, test.value) - if err != nil { - assert.Equal(t, *err, test.err) - } - } -} diff --git a/pkg/kudoctl/cmd/root.go b/pkg/kudoctl/cmd/root.go index 327a46992..7aa7617ba 100644 --- a/pkg/kudoctl/cmd/root.go +++ b/pkg/kudoctl/cmd/root.go @@ -45,6 +45,7 @@ and serves as an API aggregation layer. } cmd.AddCommand(newInstallCmd()) + cmd.AddCommand(newUpgradeCmd()) cmd.AddCommand(newGetCmd()) cmd.AddCommand(newPlanCmd()) cmd.AddCommand(newTestCmd()) diff --git a/pkg/kudoctl/cmd/upgrade.go b/pkg/kudoctl/cmd/upgrade.go new file mode 100644 index 000000000..f2a6c8f68 --- /dev/null +++ b/pkg/kudoctl/cmd/upgrade.go @@ -0,0 +1,165 @@ +package cmd + +import ( + "fmt" + + "github.com/Masterminds/semver" + "github.com/kudobuilder/kudo/pkg/apis/kudo/v1alpha1" + "github.com/kudobuilder/kudo/pkg/kudoctl/cmd/install" + "github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo" + "github.com/kudobuilder/kudo/pkg/kudoctl/util/repo" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + upgradeExample = ` + The upgrade argument must be a name of the package in the repository, a path to package in *.tar.gz format, + or a path to an unpacked package directory. + + # Upgrade flink instance dev-flink to the latest version + kubectl kudo upgrade flink --instance dev-flink + + *Note*: should you have a local "flink" folder in the current directory it will take precedence over the remote repository. + + # Upgrade flink to the version 1.1.1 + kubectl kudo upgrade flink --instance dev-flink --version 1.1.1 + + # By default parameters are all reused from the previous installation, if you need to modify, use -p + kubectl kudo upgrade flink --instance dev-flink -p param=xxx` +) + +type options struct { + InstanceName string + Namespace string + PackageVersion string + Parameters map[string]string +} + +// defaultOptions initializes the install command options to its defaults +var defaultOptions = &options{ + Namespace: "default", +} + +// newUpgradeCmd creates the install command for the CLI +func newUpgradeCmd() *cobra.Command { + options := defaultOptions + var parameters []string + upgradeCmd := &cobra.Command{ + Use: "upgrade ", + Short: "Upgrade KUDO package.", + Long: `Upgrade KUDO package from current version to new version.`, + Example: upgradeExample, + RunE: func(cmd *cobra.Command, args []string) error { + // Prior to command execution we parse and validate passed parameters + var err error + options.Parameters, err = install.GetParameterMap(parameters) + if err != nil { + return errors.WithMessage(err, "could not parse parameters") + } + return runUpgrade(args, options) + }, + SilenceUsage: true, + } + + upgradeCmd.Flags().StringVar(&options.InstanceName, "instance", "", "The instance name.") + upgradeCmd.Flags().StringArrayVarP(¶meters, "parameter", "p", nil, "The parameter name and value separated by '='") + upgradeCmd.Flags().StringVar(&options.Namespace, "namespace", defaultOptions.Namespace, "The namespace where the instance you want to upgrade is installed in.") + upgradeCmd.Flags().StringVarP(&options.PackageVersion, "version", "v", "", "A specific package version on the official repository. When installing from other sources than official repository, version from inside operator.yaml will be used. (default to the most recent)") + + const usageFmt = "Usage:\n %s\n\nFlags:\n%s" + upgradeCmd.SetUsageFunc(func(cmd *cobra.Command) error { + fmt.Fprintf(upgradeCmd.OutOrStderr(), usageFmt, upgradeCmd.UseLine(), upgradeCmd.Flags().FlagUsages()) + return nil + }) + return upgradeCmd +} + +func validateCmd(args []string, options *options) error { + if len(args) != 1 { + return fmt.Errorf("expecting exactly one argument - name of the package or path to upgrade") + } + if options.InstanceName == "" { + return fmt.Errorf("please use --instance and specify instance name. It cannot be empty") + } + + return nil +} + +func runUpgrade(args []string, options *options) error { + err := validateCmd(args, options) + if err != nil { + return err + } + packageToUpgrade := args[0] + + kc, err := kudo.NewClient(options.Namespace, viper.GetString("kubeconfig")) + if err != nil { + return errors.Wrap(err, "creating kudo client") + } + + // Resolve the package to upgrade to + repository, err := repo.NewOperatorRepository(repo.Default) + if err != nil { + return errors.WithMessage(err, "could not build operator repository") + } + crds, err := install.GetPackageCRDs(packageToUpgrade, options.PackageVersion, repository) + if err != nil { + return errors.Wrapf(err, "failed to resolve package CRDs for operator: %s", packageToUpgrade) + } + + return upgrade(crds.OperatorVersion, kc, options) +} + +func upgrade(newOv *v1alpha1.OperatorVersion, kc *kudo.Client, options *options) error { + operatorName := newOv.Spec.Operator.Name + nextOperatorVersion := newOv.Spec.Version + + // Make sure the instance you want to upgrade exists + instance, err := kc.GetInstance(options.InstanceName, options.Namespace) + if err != nil { + return errors.Wrapf(err, "verifying the instance does not already exist") + } + if instance == nil { + return fmt.Errorf("instance %s in namespace %s does not exist in the cluster", options.InstanceName, options.Namespace) + } + + // Check OperatorVersion and if upgraded version is higher than current version + ov, err := kc.GetOperatorVersion(instance.Spec.OperatorVersion.Name, options.Namespace) + if err != nil { + return errors.Wrap(err, "retrieving existing operator version") + } + if ov == nil { + return fmt.Errorf("no operator version for this operator installed yet for %s in namespace %s. Please use install command if you want to install new operator into cluster", operatorName, options.Namespace) + } + oldVersion, err := semver.NewVersion(ov.Spec.Version) + if err != nil { + return errors.Wrapf(err, "when parsing %s as semver", ov.Spec.Version) + } + newVersion, err := semver.NewVersion(nextOperatorVersion) + if err != nil { + return errors.Wrapf(err, "when parsing %s as semver", nextOperatorVersion) + } + if !oldVersion.LessThan(newVersion) { + return fmt.Errorf("upgraded version %s is the same or smaller as current version %s -> not upgrading", nextOperatorVersion, ov.Spec.Version) + } + + // install OV + versionsInstalled, err := kc.OperatorVersionsInstalled(operatorName, options.Namespace) + if err != nil { + return errors.Wrap(err, "retrieving existing operator versions") + } + if !install.VersionExists(versionsInstalled, nextOperatorVersion) { + if _, err := kc.InstallOperatorVersionObjToCluster(newOv, options.Namespace); err != nil { + return errors.Wrapf(err, "failed installing OperatorVersion %s for operator: %s", nextOperatorVersion, operatorName) + } + } + + // Change instance to point to the new OV and optionally update parameters + err = kc.UpdateInstance(options.InstanceName, options.Namespace, newOv.Name, options.Parameters) + if err != nil { + return errors.Wrapf(err, "updating instance to point to new operatorversion %s", newOv.Name) + } + return nil +} diff --git a/pkg/kudoctl/cmd/upgrade_test.go b/pkg/kudoctl/cmd/upgrade_test.go new file mode 100644 index 000000000..d5c38eac8 --- /dev/null +++ b/pkg/kudoctl/cmd/upgrade_test.go @@ -0,0 +1,133 @@ +package cmd + +import ( + "fmt" + "strings" + "testing" + + "github.com/kudobuilder/kudo/pkg/apis/kudo/v1alpha1" + "github.com/kudobuilder/kudo/pkg/client/clientset/versioned/fake" + "github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + util "github.com/kudobuilder/kudo/pkg/util/kudo" +) + +func TestUpgradeCommand_Validation(t *testing.T) { + tests := []struct { + name string + args []string + instanceName string + err string + }{ + {"no argument", []string{}, "instance", "expecting exactly one argument - name of the package or path to upgrade"}, + {"too many arguments", []string{"aaa", "bbb"}, "instance", "expecting exactly one argument - name of the package or path to upgrade"}, + {"no instance name", []string{"arg"}, "", "please use --instance and specify instance name. It cannot be empty"}, + } + + for _, tt := range tests { + cmd := newUpgradeCmd() + cmd.SetArgs(tt.args) + if tt.instanceName != "" { + cmd.Flags().Set("instance", tt.instanceName) + } + _, err := cmd.ExecuteC() + if err.Error() != tt.err { + t.Errorf("%s: expecting error %s got %v", tt.name, tt.err, err) + } + } +} + +func newTestClient() *kudo.Client { + return kudo.NewClientFromK8s(fake.NewSimpleClientset()) +} + +func TestUpgrade(t *testing.T) { + testOv := v1alpha1.OperatorVersion{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kudo.dev/v1alpha1", + Kind: "OperatorVersion", + }, + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "controller-tools.k8s.io": "1.0", + }, + Name: "test-1.0", + }, + Spec: v1alpha1.OperatorVersionSpec{ + Version: "1.0", + Operator: v1.ObjectReference{ + Name: "test", + }, + }, + } + + testInstance := v1alpha1.Instance{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kudo.dev/v1alpha1", + Kind: "Instance", + }, + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "controller-tools.k8s.io": "1.0", + util.OperatorLabel: "test", + }, + Name: "test", + }, + Spec: v1alpha1.InstanceSpec{ + OperatorVersion: v1.ObjectReference{ + Name: "test-1.0", + }, + }, + } + + installNamespace := "default" + tests := []struct { + name string + newVersion string + instanceExists bool + ovExists bool + errMessageContains string + }{ + {"instance does not exist", "1.1.1", false, true, "instance test in namespace default does not exist in the cluster"}, + {"operatorversion does not exist", "1.1.1", true, false, "no operator version for this operator installed yet"}, + {"upgrade to same version", "1.0", true, true, "upgraded version 1.0 is the same or smaller"}, + {"upgrade to smaller version", "0.1", true, true, "upgraded version 0.1 is the same or smaller"}, + {"upgrade to smaller version", "1.1.1", true, true, ""}, + } + + for _, tt := range tests { + c := newTestClient() + if tt.instanceExists { + c.InstallInstanceObjToCluster(&testInstance, installNamespace) + } + if tt.ovExists { + c.InstallOperatorVersionObjToCluster(&testOv, installNamespace) + } + newOv := testOv + newOv.Spec.Version = tt.newVersion + + err := upgrade(&newOv, c, &options{ + InstanceName: "test", + Namespace: installNamespace, + }) + if err != nil { + if !strings.Contains(err.Error(), tt.errMessageContains) { + t.Errorf("%s: expected error '%s' but got '%v'", tt.name, tt.errMessageContains, err) + } + } else if tt.errMessageContains != "" { + t.Errorf("%s: expected no error but got %v", tt.name, err) + } else { + // the upgrade should have passed without error + instance, err := c.GetInstance(testInstance.Name, installNamespace) + if err != nil { + t.Errorf("%s: error when getting instance to verify the test: %v", tt.name, err) + } + expectedVersion := fmt.Sprintf("test-%s", tt.newVersion) + if instance.Spec.OperatorVersion.Name != expectedVersion { + t.Errorf("%s: instance has wrong version '%s', expected '%s'", tt.name, instance.Spec.OperatorVersion.Name, expectedVersion) + } + } + } +} diff --git a/pkg/kudoctl/util/kudo/kudo.go b/pkg/kudoctl/util/kudo/kudo.go index 2042697d4..cd4aa63c8 100644 --- a/pkg/kudoctl/util/kudo/kudo.go +++ b/pkg/kudoctl/util/kudo/kudo.go @@ -1,15 +1,18 @@ package kudo import ( + "encoding/json" "fmt" "strings" "time" "github.com/kudobuilder/kudo/pkg/util/kudo" + "k8s.io/apimachinery/pkg/types" "github.com/kudobuilder/kudo/pkg/apis/kudo/v1alpha1" "github.com/kudobuilder/kudo/pkg/client/clientset/versioned" "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/clientcmd" ) @@ -115,6 +118,56 @@ func (c *Client) InstanceExistsInCluster(operatorName, namespace, version, insta return true, nil } +// GetInstance queries kubernetes api for instance of given name in given namespace +// returns error for error conditions. Instance not found is not considered an error and will result in 'nil, nil' +func (c *Client) GetInstance(name, namespace string) (*v1alpha1.Instance, error) { + instance, err := c.clientset.KudoV1alpha1().Instances(namespace).Get(name, v1.GetOptions{}) + if apierrors.IsNotFound(err) { + return nil, nil + } + return instance, err +} + +// GetOperatorVersion queries kubernetes api for operatorversion of given name in given namespace +// returns error for all other errors that not found, not found is treated as result being 'nil, nil' +func (c *Client) GetOperatorVersion(name, namespace string) (*v1alpha1.OperatorVersion, error) { + ov, err := c.clientset.KudoV1alpha1().OperatorVersions(namespace).Get(name, v1.GetOptions{}) + if apierrors.IsNotFound(err) { + return nil, nil + } + return ov, err +} + +type patchValue struct { + Op string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value"` +} + +// UpdateInstance updates operatorversion on instance +func (c *Client) UpdateInstance(instanceName, namespace, operatorVersionName string, parameters map[string]string) error { + instancePatch := []patchValue{ + patchValue{ + Op: "replace", + Path: "/spec/operatorVersion/name", + Value: operatorVersionName, + }, + } + if parameters != nil { + instancePatch = append(instancePatch, patchValue{ + Op: "add", + Path: "/spec/parameters", + Value: parameters, + }) + } + serializedPatch, err := json.Marshal(instancePatch) + if err != nil { + return err + } + _, err = c.clientset.KudoV1alpha1().Instances(namespace).Patch(instanceName, types.JSONPatchType, serializedPatch) + return err +} + // ListInstances lists all instances of given operator installed in the cluster in a given ns func (c *Client) ListInstances(namespace string) ([]string, error) { instances, err := c.clientset.KudoV1alpha1().Instances(namespace).List(v1.ListOptions{}) diff --git a/pkg/kudoctl/util/kudo/kudo_test.go b/pkg/kudoctl/util/kudo/kudo_test.go index 38acda1af..c00ebe732 100644 --- a/pkg/kudoctl/util/kudo/kudo_test.go +++ b/pkg/kudoctl/util/kudo/kudo_test.go @@ -33,7 +33,7 @@ func TestNewK2oClient(t *testing.T) { } } -func TestK2oClient_OperatorExistsInCluster(t *testing.T) { +func TestKudoClient_OperatorExistsInCluster(t *testing.T) { obj := v1alpha1.Operator{ TypeMeta: metav1.TypeMeta{ @@ -83,7 +83,7 @@ func TestK2oClient_OperatorExistsInCluster(t *testing.T) { } } -func TestK2oClient_InstanceExistsInCluster(t *testing.T) { +func TestKudoClient_InstanceExistsInCluster(t *testing.T) { obj := v1alpha1.Instance{ TypeMeta: metav1.TypeMeta{ APIVersion: "kudo.dev/v1alpha1", @@ -157,7 +157,7 @@ func TestK2oClient_InstanceExistsInCluster(t *testing.T) { } } -func TestK2oClient_ListInstances(t *testing.T) { +func TestKudoClient_ListInstances(t *testing.T) { obj := v1alpha1.Instance{ TypeMeta: metav1.TypeMeta{ APIVersion: "kudo.dev/v1alpha1", @@ -207,7 +207,7 @@ func TestK2oClient_ListInstances(t *testing.T) { } } -func TestK2oClient_OperatorVersionsInstalled(t *testing.T) { +func TestKudoClient_OperatorVersionsInstalled(t *testing.T) { operatorName := "test" obj := v1alpha1.OperatorVersion{ TypeMeta: metav1.TypeMeta{ @@ -256,7 +256,7 @@ func TestK2oClient_OperatorVersionsInstalled(t *testing.T) { } } -func TestK2oClient_InstallOperatorObjToCluster(t *testing.T) { +func TestKudoClient_InstallOperatorObjToCluster(t *testing.T) { obj := v1alpha1.Operator{ TypeMeta: metav1.TypeMeta{ APIVersion: "kudo.dev/v1alpha1", @@ -302,7 +302,7 @@ func TestK2oClient_InstallOperatorObjToCluster(t *testing.T) { } } -func TestK2oClient_InstallOperatorVersionObjToCluster(t *testing.T) { +func TestKudoClient_InstallOperatorVersionObjToCluster(t *testing.T) { obj := v1alpha1.OperatorVersion{ TypeMeta: metav1.TypeMeta{ APIVersion: "kudo.dev/v1alpha1", @@ -348,7 +348,7 @@ func TestK2oClient_InstallOperatorVersionObjToCluster(t *testing.T) { } } -func TestK2oClient_InstallInstanceObjToCluster(t *testing.T) { +func TestKudoClient_InstallInstanceObjToCluster(t *testing.T) { obj := v1alpha1.Instance{ TypeMeta: metav1.TypeMeta{ APIVersion: "kudo.dev/v1alpha1", @@ -393,3 +393,163 @@ func TestK2oClient_InstallInstanceObjToCluster(t *testing.T) { } } } + +func TestKudoClient_GetInstance(t *testing.T) { + testInstance := v1alpha1.Instance{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kudo.dev/v1alpha1", + Kind: "Instance", + }, + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "controller-tools.k8s.io": "1.0", + kudo.OperatorLabel: "test", + }, + Name: "test", + }, + Spec: v1alpha1.InstanceSpec{ + OperatorVersion: v1.ObjectReference{ + Name: "test-1.0", + }, + }, + } + + installNamespace := "default" + tests := []struct { + name string + found bool + namespaceToQuery string + storedInstance *v1alpha1.Instance + }{ + {"no instance exists", false, installNamespace, nil}, // 1 + {"instance exists", true, installNamespace, &testInstance}, // 2 + {"instance exists in different namespace", false, "otherns", &testInstance}, // 3 + } + + for i, tt := range tests { + k2o := newTestSimpleK2o() + + // create Instance + if tt.storedInstance != nil { + _, err := k2o.clientset.KudoV1alpha1().Instances(installNamespace).Create(tt.storedInstance) + if err != nil { + t.Errorf("%d: Error creating instance in tests setup", i+1) + } + } + + // test if Instance exists in namespace + actual, _ := k2o.GetInstance(testInstance.Name, tt.namespaceToQuery) + if (actual != nil) != tt.found { + t.Errorf("%s:\nexpected to be found: %v\n got: %v", tt.name, tt.found, actual) + } + } +} + +func TestKudoClient_GetOperatorVersion(t *testing.T) { + operatorName := "test" + testOv := v1alpha1.OperatorVersion{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kudo.dev/v1alpha1", + Kind: "OperatorVersion", + }, + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "controller-tools.k8s.io": "1.0", + }, + Name: fmt.Sprintf("%s-1.0", operatorName), + }, + Spec: v1alpha1.OperatorVersionSpec{ + Version: "1.0", + }, + } + + installNamespace := "default" + tests := []struct { + name string + found bool + namespace string + storedOv *v1alpha1.OperatorVersion + }{ + {"no operator version defined", false, installNamespace, nil}, + {"operator version exists in the same namespace", true, installNamespace, &testOv}, + {"operator version exists in different namespace", false, "otherns", &testOv}, + } + + for _, tt := range tests { + k2o := newTestSimpleK2o() + + // create Instance + if tt.storedOv != nil { + _, err := k2o.clientset.KudoV1alpha1().OperatorVersions(installNamespace).Create(tt.storedOv) + if err != nil { + t.Errorf("Error creating operator version in tests setup for %s", tt.name) + } + } + + // get OV by name and namespace + actual, _ := k2o.GetOperatorVersion(testOv.Name, tt.namespace) + if actual != nil != tt.found { + t.Errorf("%s:\nexpected to be found: %v\n got: %v", tt.name, tt.found, actual) + } + } +} + +func TestKudoClient_UpdateOperatorVersion(t *testing.T) { + testInstance := v1alpha1.Instance{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kudo.dev/v1alpha1", + Kind: "Instance", + }, + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "controller-tools.k8s.io": "1.0", + kudo.OperatorLabel: "test", + }, + Name: "test", + }, + Spec: v1alpha1.InstanceSpec{ + OperatorVersion: v1.ObjectReference{ + Name: "test-1.0", + }, + }, + } + + installNamespace := "default" + tests := []struct { + name string + patchToVersion string + existingParameters map[string]string + parametersToPatch map[string]string + namespace string + }{ + {"patch to version", "1.1.1", nil, nil, installNamespace}, + {"patch adding new parameter", "1.1.1", nil, map[string]string{"param": "value"}, installNamespace}, + {"patch updating parameter", "1.1.1", map[string]string{"param": "value"}, map[string]string{"param": "value2"}, installNamespace}, + } + + for _, tt := range tests { + k2o := newTestSimpleK2o() + + // create Instance + instanceToCreate := testInstance + instanceToCreate.Spec.Parameters = tt.existingParameters + _, err := k2o.clientset.KudoV1alpha1().Instances(installNamespace).Create(&instanceToCreate) + if err != nil { + t.Errorf("Error creating operator version in tests setup for %s", tt.name) + } + + err = k2o.UpdateInstance(testInstance.Name, installNamespace, "test-1.1.1", tt.parametersToPatch) + instance, _ := k2o.GetInstance(testInstance.Name, installNamespace) + expectedVersion := fmt.Sprintf("test-%s", tt.patchToVersion) + if err != nil || instance.Spec.OperatorVersion.Name != expectedVersion { + t.Errorf("%s:\nexpected version: %v\n got: %v, err: %v", tt.name, expectedVersion, instance.Spec.OperatorVersion.Name, err) + } + + for n, v := range tt.parametersToPatch { + found, ok := instance.Spec.Parameters[n] + if !ok || found != v { + t.Errorf("%s: Value of parameter %s should have been updated to %s but is %s", tt.name, n, v, found) + } + } + } +}