-
Notifications
You must be signed in to change notification settings - Fork 263
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ kn service update NAME [flags] | |
### Options | ||
|
||
``` | ||
--async Update service and don't wait for it to become ready. | ||
--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. | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i thought we were grouping std imports together? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
) | ||
|
||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And what happens if user There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK on canceling the waiting. Best. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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 { | ||
|
@@ -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() | ||
|
||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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) | ||
|
@@ -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) | ||
|
@@ -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") { | ||
|
@@ -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") { | ||
|
@@ -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") { | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
)