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

Update command #673

Merged
merged 7 commits into from
Aug 1, 2019
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions pkg/kudoctl/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ and serves as an API aggregation layer.

cmd.AddCommand(newInstallCmd())
cmd.AddCommand(newUpgradeCmd())
cmd.AddCommand(newUpdateCmd())
cmd.AddCommand(newGetCmd())
cmd.AddCommand(newPlanCmd())
cmd.AddCommand(newTestCmd())
Expand Down
109 changes: 109 additions & 0 deletions pkg/kudoctl/cmd/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cmd

import (
"fmt"

"github.com/kudobuilder/kudo/pkg/kudoctl/cmd/install"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var (
updateExample = `
The update argument must be a name of the instance.

# Update dev-flink instance with setting parameter param with value value
kubectl kudo upgrade dev-flink -p param=value
Copy link
Contributor

Choose a reason for hiding this comment

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

Lol

Suggested change
kubectl kudo upgrade dev-flink -p param=value
kubectl kudo update dev-flink -p param=value


# Update dev-flink instance in namespace services with setting parameter param with value value
kubectl kudo upgrade dev-flink -n services -p param=value`
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
kubectl kudo upgrade dev-flink -n services -p param=value`
kubectl kudo update dev-flink -n services -p param=value`

😉

)

type updateOptions struct {
Namespace string
Parameters map[string]string
}

// defaultOptions initializes the install command options to its defaults
var defaultUpdateOptions = &updateOptions{
Namespace: "default",
}

// newUpgradeCmd creates the install command for the CLI
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// newUpgradeCmd creates the install command for the CLI
// newUpdateCmd creates the install command for the CLI

func newUpdateCmd() *cobra.Command {
options := defaultUpdateOptions
var parameters []string
upgradeCmd := &cobra.Command{
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
upgradeCmd := &cobra.Command{
updateCmd := &cobra.Command{

Use: "update <instance-name>",
Short: "Update installed KUDO operator.",
Long: `Update installed KUDO operator with new parameters.`,
Copy link
Contributor

@zen-dog zen-dog Aug 1, 2019

Choose a reason for hiding this comment

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

Should we also mention explicitly that one can not remove existing parameters this way?

Suggested change
Long: `Update installed KUDO operator with new parameters.`,
Long: `Update installed KUDO operator with new parameters. Existing parameters can not be removed for now.`,

Example: updateExample,
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 runUpdate(args, options)
},
SilenceUsage: true,

Choose a reason for hiding this comment

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

Why are we setting this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

lol good question :D I don't know. Copy-paste 😛

SilenceUsage is an option to silence usage when an error occurs. ... sounds reasonable to me though 🤷‍♀ lot of errors can come up from non-usage errors so maybe that's why we have decided to not print it there? No hard opinion though...

Copy link
Contributor

Choose a reason for hiding this comment

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

That seems like a debug remnant? Not sure why we would set it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay, should I remove it then from all comands?

}

upgradeCmd.Flags().StringArrayVarP(&parameters, "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.")

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 validateUpdateCmd(args []string, options *updateOptions) error {
if len(args) != 1 {
return errors.New("expecting exactly one argument - name of the instance installed in your cluster")
}
if len(options.Parameters) == 0 {
return errors.New("Need to specify at least one parameter to override via -p otherwise there is nothing to update")
}

return nil
}

func runUpdate(args []string, options *updateOptions) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

We have this patter (which I like) where we separate building of the command from the actual code that is executed in different files. It might seem like overkill here, but these things tend to grow 😉

Copy link
Contributor Author

@alenkacz alenkacz Aug 1, 2019

Choose a reason for hiding this comment

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

yeah my current personal opinion is to keep it in one file as long as the command is simple enough...

Copy link
Contributor

Choose a reason for hiding this comment

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

...for now 😉 But I'll leave this up to you

err := validateUpdateCmd(args, options)
if err != nil {
return err
}
instanceToUpdate := args[0]

kc, err := kudo.NewClient(options.Namespace, viper.GetString("kubeconfig"))
if err != nil {
return errors.Wrap(err, "creating kudo client")
}

return update(instanceToUpdate, kc, options)
}

func update(instanceToUpdate string, kc *kudo.Client, options *updateOptions) error {
// Make sure the instance you want to upgrade exists
instance, err := kc.GetInstance(instanceToUpdate, 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", instanceToUpdate, options.Namespace)
}

// Change instance to point to the new OV and optionally update parameters
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this comment makes sense here

err = kc.UpdateInstance(instanceToUpdate, options.Namespace, nil, options.Parameters)
Copy link
Contributor

Choose a reason for hiding this comment

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

also, why do we pass nil as operatorVersionName here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because we don't want to update the version in this call. I use nil here as I would use None in scala

if err != nil {
return errors.Wrapf(err, "updating instance %s", instanceToUpdate)
}
fmt.Printf("Instance %s was updated ヽ(•‿•)ノ")
return nil
}
99 changes: 99 additions & 0 deletions pkg/kudoctl/cmd/update_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package cmd

import (
"strings"
"testing"

"github.com/kudobuilder/kudo/pkg/apis/kudo/v1alpha1"
util "github.com/kudobuilder/kudo/pkg/util/kudo"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestUpdateCommand_Validation(t *testing.T) {
tests := []struct {
name string
args []string
parameters map[string]string
err string
}{
{"no argument", []string{}, map[string]string{"param": "value"}, "expecting exactly one argument - name of the instance installed in your cluster"},
{"too many arguments", []string{"aaa", "bbb"}, map[string]string{"param": "value"}, "expecting exactly one argument - name of the instance installed in your cluster"},
{"no instance name", []string{"arg"}, map[string]string{}, "Need to specify at least one parameter to override via -p otherwise there is nothing to update"},
}

for _, tt := range tests {
cmd := newUpdateCmd()
cmd.SetArgs(tt.args)
for _, v := range tt.parameters {
cmd.Flags().Set("p", v)
}
_, err := cmd.ExecuteC()
if err.Error() != tt.err {
t.Errorf("%s: expecting error %s got %v", tt.name, tt.err, err)
}
}
}

func TestUpdate(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",
util.OperatorLabel: "test",
},
Name: "test",
},
Spec: v1alpha1.InstanceSpec{
OperatorVersion: v1.ObjectReference{
Name: "test-1.0",
},
},
}

installNamespace := "default"
tests := []struct {
name string
instanceExists bool
parameters map[string]string
errMessageContains string
}{
{"instance does not exist", false, map[string]string{"param": "value"}, "instance test in namespace default does not exist in the cluster"},
{"update parameters", true, map[string]string{"param": "value"}, ""},
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice tests 👏 How about giving the testInstance an existing parameter param: oldValue?

Copy link
Contributor Author

@alenkacz alenkacz Aug 1, 2019

Choose a reason for hiding this comment

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

yeah I think the current patch implementation is incorrect, since this code for patch is already on master, I'll address that in another PR and add the test as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay I did it in here

}

for _, tt := range tests {
c := newTestClient()
if tt.instanceExists {
c.InstallInstanceObjToCluster(&testInstance, installNamespace)
}

err := update(testInstance.Name, c, &updateOptions{
Namespace: installNamespace,
Parameters: tt.parameters,
})
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)
}
for k, v := range tt.parameters {
value, ok := instance.Spec.Parameters[k]
if !ok || value != v {
t.Errorf("%s: expected parameter %s to be updated to %s but params are %v", tt.name, k, v, instance.Spec.Parameters)
}
}
}
}
}
3 changes: 2 additions & 1 deletion pkg/kudoctl/cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/kudobuilder/kudo/pkg/kudoctl/cmd/install"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/repo"
util "github.com/kudobuilder/kudo/pkg/util/kudo"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -157,7 +158,7 @@ func upgrade(newOv *v1alpha1.OperatorVersion, kc *kudo.Client, options *options)
}

// Change instance to point to the new OV and optionally update parameters
err = kc.UpdateInstance(options.InstanceName, options.Namespace, newOv.Name, options.Parameters)
err = kc.UpdateInstance(options.InstanceName, options.Namespace, util.String(ov.Name), options.Parameters)
Copy link
Contributor

Choose a reason for hiding this comment

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

Hu? Why did this change from newOv to ov?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch :)

if err != nil {
return errors.Wrapf(err, "updating instance to point to new operatorversion %s", newOv.Name)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/kudoctl/util/kudo/kudo.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ type patchValue struct {
}

// UpdateInstance updates operatorversion on instance
func (c *Client) UpdateInstance(instanceName, namespace, operatorVersionName string, parameters map[string]string) error {
func (c *Client) UpdateInstance(instanceName, namespace string, operatorVersionName *string, parameters map[string]string) error {
instancePatch := []patchValue{
patchValue{
Op: "replace",
Expand Down
23 changes: 15 additions & 8 deletions pkg/kudoctl/util/kudo/kudo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/kudobuilder/kudo/pkg/apis/kudo/v1alpha1"
"github.com/kudobuilder/kudo/pkg/client/clientset/versioned/fake"
util "github.com/kudobuilder/kudo/pkg/util/kudo"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
Expand Down Expand Up @@ -517,14 +518,15 @@ func TestKudoClient_UpdateOperatorVersion(t *testing.T) {
installNamespace := "default"
tests := []struct {
name string
patchToVersion 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},
{"patch to version", util.String("test-1.1.1"), nil, nil, installNamespace},
{"patch adding new parameter", util.String("test-1.1.1"), nil, map[string]string{"param": "value"}, installNamespace},
{"patch updating parameter", util.String("test-1.1.1"), map[string]string{"param": "value"}, map[string]string{"param": "value2"}, installNamespace},
{"do not patch the version", nil, map[string]string{"param": "value"}, map[string]string{"param": "value2"}, installNamespace},
}

for _, tt := range tests {
Expand All @@ -538,11 +540,16 @@ func TestKudoClient_UpdateOperatorVersion(t *testing.T) {
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)
err = k2o.UpdateInstance(testInstance.Name, installNamespace, tt.patchToVersion, 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)
if tt.patchToVersion != nil {
if err != nil || instance.Spec.OperatorVersion.Name != util.StringValue(tt.patchToVersion) {
t.Errorf("%s:\nexpected version: %v\n got: %v, err: %v", tt.name, util.StringValue(tt.patchToVersion), instance.Spec.OperatorVersion.Name, err)
}
} else {
if instance.Spec.OperatorVersion.Name != testInstance.Spec.OperatorVersion.Name {
t.Errorf("%s:\nexpected version to not change from: %v\n err: %v", tt.name, instance.Spec.OperatorVersion.Name, err)
}
}

for n, v := range tt.parametersToPatch {
Expand Down