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

feat(wait): add wait for delete operation #682

Merged
merged 10 commits into from
Feb 25, 2020
3 changes: 3 additions & 0 deletions docs/cmd/kn_service_delete.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ kn service delete NAME [flags]
### Options

```
--async DEPRECATED: please use --no-wait instead. Delete service and don't wait for it to become ready.
Copy link
Contributor

Choose a reason for hiding this comment

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

don't add that, not deeded to add a deprecated option.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's still part of AddConditionWaitFlags() and I wish I could create overloaded method...:) But of course, I agress it shouldn't be created with deprecated flag.

Copy link
Contributor

Choose a reason for hiding this comment

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

yep, I've seen. its ok.

-h, --help help for delete
-n, --namespace string Specify the namespace to operate in.
--no-wait Delete service and don't wait for it to become ready.
--wait-timeout int Seconds to wait before giving up on waiting for service to be ready. (default 600)
```

### Options inherited from parent commands
Expand Down
17 changes: 17 additions & 0 deletions pkg/kn/commands/service/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ package service
import (
"errors"
"fmt"
"time"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/watch"

"knative.dev/client/pkg/kn/commands"
)

// NewServiceDeleteCommand represent 'service delete' command
func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
var waitFlags commands.WaitFlags

serviceDeleteCommand := &cobra.Command{
Use: "delete NAME",
Short: "Delete a service.",
Expand All @@ -50,7 +54,19 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
}

for _, name := range args {
waitC := make(chan error)
defer close(waitC)
if !waitFlags.NoWait {
go func(s string, c chan error) {
err := client.WaitForEvent("service", s, time.Duration(waitFlags.TimeoutInSeconds)*time.Second,
func(evt *watch.Event) bool { return evt.Type == watch.Deleted })
c <- err
}(name, waitC)
}
err = client.DeleteService(name)
if err == nil && !waitFlags.NoWait {
err = <-waitC
}
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "%s.\n", err)
} else {
Expand All @@ -61,5 +77,6 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
},
}
commands.AddNamespaceFlags(serviceDeleteCommand.Flags(), false)
waitFlags.AddConditionWaitFlags(serviceDeleteCommand, commands.WaitDefaultTimeout, "Delete", "service")
return serviceDeleteCommand
}
7 changes: 7 additions & 0 deletions pkg/kn/commands/service/delete_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

clientservingv1 "knative.dev/client/pkg/serving/v1"
"knative.dev/client/pkg/util"
"knative.dev/client/pkg/util/mock"
)

func TestServiceDeleteMock(t *testing.T) {
Expand All @@ -31,6 +32,8 @@ func TestServiceDeleteMock(t *testing.T) {
r := client.Recorder()

r.DeleteService("foo", nil)
// Wait for delete event
r.WaitForEvent("service", "foo", mock.Any(), mock.Any(), nil)

output, err := executeServiceCommand(client, "delete", "foo")
assert.NilError(t, err)
Expand All @@ -46,6 +49,10 @@ func TestMultipleServiceDeleteMock(t *testing.T) {

// Recording:
r := client.Recorder()
// Wait for delete event
r.WaitForEvent("service", "foo", mock.Any(), mock.Any(), nil)
r.WaitForEvent("service", "bar", mock.Any(), mock.Any(), nil)
r.WaitForEvent("service", "baz", mock.Any(), mock.Any(), nil)

r.DeleteService("foo", nil)
r.DeleteService("bar", nil)
Expand Down
24 changes: 24 additions & 0 deletions pkg/kn/commands/service/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ package service
import (
"testing"

"github.com/pkg/errors"
"gotest.tools/assert"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
client_testing "k8s.io/client-go/testing"
clienttesting "k8s.io/client-go/testing"

"knative.dev/client/pkg/kn/commands"
"knative.dev/client/pkg/util"
"knative.dev/client/pkg/wait"
)

func fakeServiceDelete(args []string) (action client_testing.Action, name string, output string, err error) {
Expand All @@ -35,6 +40,17 @@ func fakeServiceDelete(args []string) (action client_testing.Action, name string
name = deleteAction.GetName()
return true, nil, nil
})
fakeServing.AddWatchReactor("services",
func(a client_testing.Action) (bool, watch.Interface, error) {
watchAction := a.(clienttesting.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(getServiceDeleteEvents("test-service"))
w.Start()
return true, w, nil
})
cmd.SetArgs(args)
err = cmd.Execute()
if err != nil {
Expand Down Expand Up @@ -79,3 +95,11 @@ func TestMultipleServiceDelete(t *testing.T) {
assert.Check(t, util.ContainsAll(output, "Service", sevName2, "deleted", "namespace", commands.FakeNamespace))
assert.Check(t, util.ContainsAll(output, "Service", sevName3, "deleted", "namespace", commands.FakeNamespace))
}

func getServiceDeleteEvents(name string) []watch.Event {
return []watch.Event{
{watch.Added, wait.CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionUnknown, "", "msg1")},
{watch.Modified, wait.CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionTrue, "", "msg2")},
{watch.Deleted, wait.CreateTestServiceWithConditions(name, corev1.ConditionTrue, corev1.ConditionTrue, "", "")},
}
}
8 changes: 8 additions & 0 deletions pkg/serving/v1/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ type KnServingClient interface {
// Return error and how long has been waited
WaitForService(name string, timeout time.Duration, msgCallback wait.MessageCallback) (error, time.Duration)

WaitForEvent(kind, name string, timeout time.Duration, done wait.EventDone) error

dsimansk marked this conversation as resolved.
Show resolved Hide resolved
// Get a configuration by name
GetConfiguration(name string) (*servingv1.Configuration, error)

Expand Down Expand Up @@ -271,6 +273,12 @@ func (cl *knServingClient) WaitForService(name string, timeout time.Duration, ms
return waitForReady.Wait(name, timeout, msgCallback)
}

func (cl *knServingClient) WaitForEvent(kind, name string, timeout time.Duration, done wait.EventDone) error {
waitForEvent := wait.NewWaitForEvent(kind, cl.WatchService, done)
err, _ := waitForEvent.Wait(name, timeout, wait.NoopMessageCallback())
return err
}

// Get the configuration for a service
func (cl *knServingClient) GetConfiguration(name string) (*servingv1.Configuration, error) {
configuration, err := cl.client.Configurations(cl.namespace).Get(name, v1.GetOptions{})
Expand Down
10 changes: 10 additions & 0 deletions pkg/serving/v1/client_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ func (c *MockKnServingClient) WaitForService(name string, timeout time.Duration,
return mock.ErrorOrNil(call.Result[0]), call.Result[1].(time.Duration)
}

// Wait for a service to become ready, but not longer than provided timeout
Copy link
Contributor

Choose a reason for hiding this comment

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

Golint comments: comment on exported method ServingRecorder.WaitForEvent should be of the form "WaitForEvent ...". More info.

func (sr *ServingRecorder) WaitForEvent(kind, name interface{}, timeout interface{}, done interface{}, err error) {
sr.r.Add("WaitForEvent", []interface{}{kind, name, timeout, done}, []interface{}{err})
}

func (c *MockKnServingClient) WaitForEvent(kind, name string, timeout time.Duration, done wait.EventDone) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Golint comments: exported method MockKnServingClient.WaitForEvent should have comment or be unexported. More info.

call := c.recorder.r.VerifyCall("WaitForEvent", kind, name, timeout, done)
return mock.ErrorOrNil(call.Result[0])
}

// Get a revision by name
func (sr *ServingRecorder) GetRevision(name interface{}, revision *servingv1.Revision, err error) {
sr.r.Add("GetRevision", []interface{}{name}, []interface{}{revision, err})
Expand Down
43 changes: 41 additions & 2 deletions pkg/wait/wait_for_ready.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,17 @@ type waitForReadyConfig struct {
kind string
}

type waitForEvent struct {
watchMaker WatchMaker
eventDone EventDone
kind string
}

type EventDone func(ev *watch.Event) bool
Copy link
Contributor

Choose a reason for hiding this comment

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

Golint comments: exported type EventDone should have comment or be unexported. More info.


// Interface used for waiting of a resource of a given name to reach a definitive
// state in its "Ready" condition.
type WaitForReady interface {
type Wait interface {

// Wait on resource the resource with this name until a given timeout
// and write event messages for unknown event to the status writer.
Expand All @@ -56,14 +64,22 @@ type ConditionsExtractor func(obj runtime.Object) (apis.Conditions, error)
type MessageCallback func(durationSinceState time.Duration, message string)

// Constructor with resource type specific configuration
func NewWaitForReady(kind string, watchMaker WatchMaker, extractor ConditionsExtractor) WaitForReady {
func NewWaitForReady(kind string, watchMaker WatchMaker, extractor ConditionsExtractor) Wait {
return &waitForReadyConfig{
kind: kind,
watchMaker: watchMaker,
conditionsExtractor: extractor,
}
}

func NewWaitForEvent(kind string, watchMaker WatchMaker, eventDone EventDone) Wait {
Copy link
Contributor

Choose a reason for hiding this comment

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

Golint comments: exported function NewWaitForEvent should have comment or be unexported. More info.

return &waitForEvent{
kind: kind,
watchMaker: watchMaker,
eventDone: eventDone,
}
}

// A simple message callback which prints out messages line by line
func SimpleMessageCallback(out io.Writer) MessageCallback {
oldMessage := ""
Expand Down Expand Up @@ -178,6 +194,29 @@ func (w *waitForReadyConfig) waitForReadyCondition(start time.Time, name string,
}
}

func (w *waitForEvent) Wait(name string, timeout time.Duration, msgCallback MessageCallback) (error, time.Duration) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Golint arg-order: error should be the last type when returning multiple items.

watcher, err := w.watchMaker(name, timeout)
if err != nil {
return err, 0
}
defer watcher.Stop()
start := time.Now()
// channel used to transport the error
errChan := make(chan error)
for {
select {
case <-time.After(timeout):
dsimansk marked this conversation as resolved.
Show resolved Hide resolved
return nil, time.Since(start)
case err = <-errChan:
return err, time.Since(start)
case event := <-watcher.ResultChan():
if w.eventDone(&event) {
return nil, time.Since(start)
}
}
}
}

// Going over Unstructured to keep that function generally applicable.
// Alternative implemenentation: Add a func-field to waitForReadyConfig which has to be
// provided for every resource (like the conditions extractor)
dsimansk marked this conversation as resolved.
Show resolved Hide resolved
Expand Down