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

feature(service): Wait on update for service to become ready. #271

Merged
merged 4 commits into from
Jul 22, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 18 additions & 1 deletion CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,29 @@
[cols="1,10,3", options="header", width="100%"]
|===
| | Description | PR

| 🎁🐛🧽🗑️
|
| https://github.com/knative/client/pull/[#]
|===
////

## v0.8.0 (unreleased)

[cols="1,10,3", options="header", width="100%"]
|===
| | Description | PR

| 🎁
| Wait for service to become ready with `kn service update` (same behaviour as for `kn service create`)
| https://github.com/knative/client/pull/271[#271]

| 🐛
| Better error handling when providing wrong kubeconfig option
| https://github.com/knative/client/pull/222[#222]

|===

## v0.2.0 (2019-07-10)

[cols="1,10,3", options="header", width="100%"]
Expand All @@ -35,7 +52,7 @@

| 🐛
| Retry update operation on an optimistic lock failure
| https://github.com/knative/client/pull/[#240]
| https://github.com/knative/client/pull/240[#240]

| 🎁
| Add `kn route list`
Expand Down
2 changes: 2 additions & 0 deletions docs/cmd/kn_service_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ kn service update NAME [flags]
### Options

```
--async Update service and don't wait for it to become ready.

Choose a reason for hiding this comment

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

we should rename this flag to --wait=bool. async is too confusing imho with serveless "async" handler configuration (no expected response).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Happy to rename when we decided to on the general direction. See #283 and #284 for voting how to continue with boolean flags (like --help=true)

--concurrency-limit int Hard Limit of concurrent requests to be processed by a single replica.
--concurrency-target int Recommendation for when to scale up based on the concurrent number of incoming request. Defaults to --concurrency-limit when given.
-e, --env stringArray Environment variable to set. NAME=value; you may provide this flag any number of times to set multiple environment variables.
Expand All @@ -40,6 +41,7 @@ kn service update NAME [flags]
-p, --port int32 The port where application listens on.
--requests-cpu string The requested CPU (e.g., 250m).
--requests-memory string The requested memory (e.g., 64Mi).
--wait-timeout int Seconds to wait before giving up on waiting for service to be ready. (default 60)

Choose a reason for hiding this comment

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

shouldnt we set wait-timeout to be time.Duration. otherwise what's the unit of this flag?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As the message says, its "Seconds to wait". I think (hope) that's good enough, but we can improve on adding a unit, too (like moving to a string and parsing "1min"). personally, I think that's overkill.

```

### Options inherited from parent commands
Expand Down
20 changes: 20 additions & 0 deletions pkg/kn/commands/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
package service

import (
"io"
"time"

"github.com/knative/client/pkg/kn/commands"
serving_kn_v1alpha1 "github.com/knative/client/pkg/serving/v1alpha1"

"fmt"

Choose a reason for hiding this comment

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

i thought we were grouping std imports together?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are running now "goimports" unconditionally on every build. So we take exactly what is spit out there, that there is no ambiguity anymore. Is there anything we could optimize ?


"github.com/spf13/cobra"
)

Expand All @@ -36,3 +43,16 @@ func NewServiceCommand(p *commands.KnParams) *cobra.Command {
serviceCmd.AddCommand(NewServiceUpdateCommand(p))
return serviceCmd
}

func waitForService(client serving_kn_v1alpha1.KnClient, serviceName string, out io.Writer, timeout int) error {
fmt.Fprintf(out, "Waiting for service '%s' to become ready ... ", serviceName)
Copy link
Contributor

@maximilien maximilien Jul 18, 2019

Choose a reason for hiding this comment

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

And what happens if user CTRL-C out. I assume the API request continues but kn fails with user cancel? Might want to be clear. Since alternatively you could trap CTL-C and try to cancel cleanly or even undo change... Not advocating for clean up at this point but users will want to cancel and let's anticipate that and aid them.

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 point. But I would interpret CTL-C as "cancelling the waiting" (that is where it currently states). We could try to catch that and print out something like "Cancelled waiting for service to become ready". It then just turns into an async behaviour where you have to check the server state.

But a complete rollback is imo out of scope (that would mean to implement a fully transactional behaviour, recording the previous state and as you are on the client you don't have full control what's happening on the server-side. At least some events have been produced, which might trigger some other, non-rollbackable actions etc. A client-side managed transaction with possible compensation actions is not something easy.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK on canceling the waiting. Best.

Choose a reason for hiding this comment

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

Since alternatively you could trap CTL-C and try to cancel cleanly or even undo change
this would be a very surprising behaviour to most users.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I still have to add that kind of trapping for CTRL-C and printing out something like

"Interrupt: Stop waiting on service completion. Please check the service's status yourself."

or so.

flush(out)

err := client.WaitForService(serviceName, time.Duration(timeout)*time.Second)
if err != nil {
fmt.Fprintln(out)
return err
}
fmt.Fprintln(out, "OK")
return nil
}
12 changes: 3 additions & 9 deletions pkg/kn/commands/service/service_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"errors"
"fmt"
"io"
"time"

"github.com/knative/client/pkg/kn/commands"
"github.com/knative/client/pkg/serving/v1alpha1"
Expand Down Expand Up @@ -105,24 +104,19 @@ func NewServiceCreateCommand(p *commands.KnParams) *cobra.Command {

if !waitFlags.Async {
out := cmd.OutOrStdout()
fmt.Fprintf(out, "Waiting for service '%s' to become ready ... ", name)
flush(out)

err := client.WaitForService(name, time.Duration(waitFlags.TimeoutInSeconds)*time.Second)
err := waitForService(client, name, out, waitFlags.TimeoutInSeconds)
if err != nil {
fmt.Fprintln(out)
return err
}
fmt.Fprintln(out, "OK")
return showUrl(client, name, namespace, cmd.OutOrStdout())
return showUrl(client, name, namespace, out)
}

return nil
},
}
commands.AddNamespaceFlags(serviceCreateCommand.Flags(), false)
editFlags.AddCreateFlags(serviceCreateCommand)
waitFlags.AddConditionWaitFlags(serviceCreateCommand, 60, "service")
waitFlags.AddConditionWaitFlags(serviceCreateCommand, 60, "Create", "service")
return serviceCreateCommand
}

Expand Down
14 changes: 13 additions & 1 deletion pkg/kn/commands/service/service_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (

func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
var editFlags ConfigurationEditFlags
var waitFlags commands.WaitFlags

serviceUpdateCommand := &cobra.Command{
Use: "update NAME",
Expand Down Expand Up @@ -56,7 +57,8 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {

var retries = 0
for {
service, err := client.GetService(args[0])
name := args[0]
service, err := client.GetService(name)
if err != nil {
return err
}
Expand All @@ -76,6 +78,15 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
}
return err
}

if !waitFlags.Async {
out := cmd.OutOrStdout()
err := waitForService(client, name, out, waitFlags.TimeoutInSeconds)
if err != nil {
return err
}
}

fmt.Fprintf(cmd.OutOrStdout(), "Service '%s' updated in namespace '%s'.\n", args[0], namespace)
return nil
}
Expand All @@ -84,5 +95,6 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {

commands.AddNamespaceFlags(serviceUpdateCommand.Flags(), false)
editFlags.AddUpdateFlags(serviceUpdateCommand)
waitFlags.AddConditionWaitFlags(serviceUpdateCommand, 60, "Update", "service")
return serviceUpdateCommand
}
61 changes: 54 additions & 7 deletions pkg/kn/commands/service/service_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,23 @@ import (
"strings"
"testing"

"gotest.tools/assert"

"github.com/knative/client/pkg/kn/commands"
servinglib "github.com/knative/client/pkg/serving"
"github.com/knative/client/pkg/util"
"github.com/knative/client/pkg/wait"

"github.com/knative/serving/pkg/apis/serving/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
client_testing "k8s.io/client-go/testing"
)

func fakeServiceUpdate(original *v1alpha1.Service, args []string) (
func fakeServiceUpdate(original *v1alpha1.Service, args []string, sync bool) (
action client_testing.Action,
updated *v1alpha1.Service,
output string,
Expand All @@ -55,6 +61,24 @@ func fakeServiceUpdate(original *v1alpha1.Service, args []string) (
func(a client_testing.Action) (bool, runtime.Object, error) {
return true, original, nil
})
if sync {
fakeServing.AddWatchReactor("services",
func(a client_testing.Action) (bool, watch.Interface, error) {
watchAction := a.(client_testing.WatchAction)
_, found := watchAction.GetWatchRestrictions().Fields.RequiresExactMatch("metadata.name")
if !found {
return true, nil, errors.New("no field selector on metadata.name found")
}
w := wait.NewFakeWatch(getServiceEvents("test-service"))
w.Start()
return true, w, nil
})
fakeServing.AddReactor("get", "services",
func(a client_testing.Action) (bool, runtime.Object, error) {
return true, &v1alpha1.Service{}, nil
})
}

cmd.SetArgs(args)
err = cmd.Execute()
if err != nil {
Expand All @@ -64,6 +88,29 @@ func fakeServiceUpdate(original *v1alpha1.Service, args []string) (
return
}

func TestServiceUpdateImageSync(t *testing.T) {
orig := newEmptyService()

template, err := servinglib.RevisionTemplateOfService(orig)
if err != nil {
t.Fatal(err)
}

servinglib.UpdateImage(template, "gcr.io/foo/bar:baz")

action, updated, output, err := fakeServiceUpdate(orig, []string{
"service", "update", "foo", "--image", "gcr.io/foo/quux:xyzzy", "--namespace", "bar"}, true)

assert.NilError(t, err)
assert.Assert(t, action.Matches("update", "services"))

template, err = servinglib.RevisionTemplateOfService(updated)
assert.NilError(t, err)

assert.Equal(t, template.Spec.DeprecatedContainer.Image, "gcr.io/foo/quux:xyzzy")
assert.Assert(t, util.ContainsAll(strings.ToLower(output), "update", "foo", "service", "namespace", "bar", "ok", "waiting"))
}

func TestServiceUpdateImage(t *testing.T) {
orig := newEmptyService()

Expand All @@ -75,7 +122,7 @@ func TestServiceUpdateImage(t *testing.T) {
servinglib.UpdateImage(template, "gcr.io/foo/bar:baz")

action, updated, output, err := fakeServiceUpdate(orig, []string{
"service", "update", "foo", "--image", "gcr.io/foo/quux:xyzzy", "--namespace", "bar"})
"service", "update", "foo", "--image", "gcr.io/foo/quux:xyzzy", "--namespace", "bar", "--async"}, false)

Choose a reason for hiding this comment

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

it seems strange that we placed non-default behaviour in bunch of tests. given that these are unit tests i dont see a good reason why we would add --async to bunch of them instead of just having a dedicated test that tests --async.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, that we should update most of them. The more involved part is to get the reactor right for simulating the sync behaviour, but we can change that.

There is at least one test for the sync behaviour.

That not all tests have been changed is mostly because of lack of time. As we should refactor the create/update tests anyway (think there is even an issue open for that), I thought its ok to postpone that, too.


if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -104,7 +151,7 @@ func TestServiceUpdateMaxMinScale(t *testing.T) {

action, updated, _, err := fakeServiceUpdate(original, []string{
"service", "update", "foo",
"--min-scale", "1", "--max-scale", "5", "--concurrency-target", "10", "--concurrency-limit", "100"})
"--min-scale", "1", "--max-scale", "5", "--concurrency-target", "10", "--concurrency-limit", "100", "--async"}, false)

if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -169,7 +216,7 @@ func TestServiceUpdateEnv(t *testing.T) {
servinglib.UpdateImage(template, "gcr.io/foo/bar:baz")

action, updated, _, err := fakeServiceUpdate(orig, []string{
"service", "update", "foo", "-e", "TARGET=Awesome"})
"service", "update", "foo", "-e", "TARGET=Awesome", "--async"}, false)

if err != nil {
t.Fatal(err)
Expand All @@ -195,7 +242,7 @@ func TestServiceUpdateRequestsLimitsCPU(t *testing.T) {
service := createMockServiceWithResources(t, "250", "64Mi", "1000m", "1024Mi")

action, updated, _, err := fakeServiceUpdate(service, []string{
"service", "update", "foo", "--requests-cpu", "500m", "--limits-cpu", "1000m"})
"service", "update", "foo", "--requests-cpu", "500m", "--limits-cpu", "1000m", "--async"}, false)
if err != nil {
t.Fatal(err)
} else if !action.Matches("update", "services") {
Expand Down Expand Up @@ -233,7 +280,7 @@ func TestServiceUpdateRequestsLimitsMemory(t *testing.T) {
service := createMockServiceWithResources(t, "100m", "64Mi", "1000m", "1024Mi")

action, updated, _, err := fakeServiceUpdate(service, []string{
"service", "update", "foo", "--requests-memory", "128Mi", "--limits-memory", "2048Mi"})
"service", "update", "foo", "--requests-memory", "128Mi", "--limits-memory", "2048Mi", "--async"}, false)
if err != nil {
t.Fatal(err)
} else if !action.Matches("update", "services") {
Expand Down Expand Up @@ -273,7 +320,7 @@ func TestServiceUpdateRequestsLimitsCPU_and_Memory(t *testing.T) {
action, updated, _, err := fakeServiceUpdate(service, []string{
"service", "update", "foo",
"--requests-cpu", "500m", "--limits-cpu", "2000m",
"--requests-memory", "128Mi", "--limits-memory", "2048Mi"})
"--requests-memory", "128Mi", "--limits-memory", "2048Mi", "--async"}, false)
if err != nil {
t.Fatal(err)
} else if !action.Matches("update", "services") {
Expand Down
43 changes: 0 additions & 43 deletions pkg/kn/commands/service/wait_args.go

This file was deleted.

4 changes: 2 additions & 2 deletions pkg/kn/commands/wait_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ type WaitFlags struct {
// Add flags which influence the sync/async behaviour when creating or updating
// resources. Set `waitDefault` argument if the default behaviour is synchronous.
// Use `what` for describing what is waited for.
func (p *WaitFlags) AddConditionWaitFlags(command *cobra.Command, waitTimeoutDefault int, what string) {
waitUsage := fmt.Sprintf("Create %s and don't wait for it to become ready.", what)
func (p *WaitFlags) AddConditionWaitFlags(command *cobra.Command, waitTimeoutDefault int, action string, what string) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice 👍

waitUsage := fmt.Sprintf("%s %s and don't wait for it to become ready.", action, what)
command.Flags().BoolVar(&p.Async, "async", false, waitUsage)

timeoutUsage := fmt.Sprintf("Seconds to wait before giving up on waiting for %s to be ready.", what)
Expand Down
4 changes: 2 additions & 2 deletions pkg/kn/commands/wait_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestAddWaitForReadyFlags(t *testing.T) {

flags := &WaitFlags{}
cmd := cobra.Command{}
flags.AddConditionWaitFlags(&cmd, 60, "service")
flags.AddConditionWaitFlags(&cmd, 60, "Create", "service")

err := cmd.ParseFlags(tc.args)
if err != nil && !tc.isParseErrorExpected {
Expand All @@ -66,7 +66,7 @@ func TestAddWaitUsageMessage(t *testing.T) {

flags := &WaitFlags{}
cmd := cobra.Command{}
flags.AddConditionWaitFlags(&cmd, 60, "blub")
flags.AddConditionWaitFlags(&cmd, 60, "bla", "blub")
if !strings.Contains(cmd.UsageString(), "blub") {
t.Error("no type returned in usage")
}
Expand Down