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
11 changes: 9 additions & 2 deletions pkg/kn/commands/service/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package service
import (
"errors"
"fmt"
"time"

"github.com/spf13/cobra"

Expand All @@ -25,6 +26,8 @@ import (

// 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 @@ -48,9 +51,12 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
if err != nil {
return err
}

for _, name := range args {
err = client.DeleteService(name)
timeout := time.Duration(0)
if !waitFlags.NoWait {
timeout = time.Duration(waitFlags.TimeoutInSeconds) * time.Second
}
err = client.DeleteService(name, timeout)
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "%s.\n", err)
} else {
Expand All @@ -61,5 +67,6 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
},
}
commands.AddNamespaceFlags(serviceDeleteCommand.Flags(), false)
waitFlags.AddConditionWaitFlags(serviceDeleteCommand, commands.WaitDefaultTimeout, "Delete", "service")
return serviceDeleteCommand
}
27 changes: 23 additions & 4 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 @@ -30,7 +31,7 @@ func TestServiceDeleteMock(t *testing.T) {
// Recording:
r := client.Recorder()

r.DeleteService("foo", nil)
r.DeleteService("foo", mock.Any(), nil)

output, err := executeServiceCommand(client, "delete", "foo")
assert.NilError(t, err)
Expand All @@ -40,16 +41,34 @@ func TestServiceDeleteMock(t *testing.T) {

}

func TestServiceDeleteMockNoWait(t *testing.T) {
// New mock client
client := clientservingv1.NewMockKnServiceClient(t)

// Recording:
r := client.Recorder()

r.DeleteService("foo", mock.Any(), nil)

output, err := executeServiceCommand(client, "delete", "foo", "--no-wait")
assert.NilError(t, err)
assert.Assert(t, util.ContainsAll(output, "deleted", "foo", "default"))

r.Validate()

}

func TestMultipleServiceDeleteMock(t *testing.T) {
// New mock client
client := clientservingv1.NewMockKnServiceClient(t)

// Recording:
r := client.Recorder()
// Wait for delete event

r.DeleteService("foo", nil)
r.DeleteService("bar", nil)
r.DeleteService("baz", nil)
r.DeleteService("foo", mock.Any(), nil)
r.DeleteService("bar", mock.Any(), nil)
r.DeleteService("baz", mock.Any(), nil)

output, err := executeServiceCommand(client, "delete", "foo", "bar", "baz")
assert.NilError(t, err)
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, "", "")},
}
}
23 changes: 21 additions & 2 deletions pkg/serving/v1/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@ type KnServingClient interface {
UpdateServiceWithRetry(name string, updateFunc serviceUpdateFunc, nrRetries int) error

// Delete a service by name
DeleteService(name string) error
DeleteService(name string, timeout time.Duration) error
dsimansk marked this conversation as resolved.
Show resolved Hide resolved

// Wait for a service to become ready, but not longer than provided timeout.
// 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 @@ -253,7 +255,24 @@ func updateServiceWithRetry(cl KnServingClient, name string, updateFunc serviceU
}

// Delete a service by name
func (cl *knServingClient) DeleteService(serviceName string) error {
func (cl *knServingClient) DeleteService(serviceName string, timeout time.Duration) error {
if timeout == 0 {
return cl.deleteService(serviceName)
}
waitC := make(chan error)
go func(name string, c chan error) {
dsimansk marked this conversation as resolved.
Show resolved Hide resolved
waitForEvent := wait.NewWaitForEvent("service", cl.WatchService, func(evt *watch.Event) bool { return evt.Type == watch.Deleted })
err, _ := waitForEvent.Wait(name, timeout, wait.NoopMessageCallback())
c <- err
}(serviceName, waitC)
err := cl.deleteService(serviceName)
if err != nil {
return err
}
return <-waitC
}

func (cl *knServingClient) deleteService(serviceName string) error {
err := cl.client.Services(cl.namespace).Delete(
serviceName,
&v1.DeleteOptions{},
Expand Down
8 changes: 4 additions & 4 deletions pkg/serving/v1/client_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ func (c *MockKnServingClient) UpdateServiceWithRetry(name string, updateFunc ser
}

// Delete a service by name
func (sr *ServingRecorder) DeleteService(name interface{}, err error) {
sr.r.Add("DeleteService", []interface{}{name}, []interface{}{err})
func (sr *ServingRecorder) DeleteService(name, timeout interface{}, err error) {
sr.r.Add("DeleteService", []interface{}{name, timeout}, []interface{}{err})
}

func (c *MockKnServingClient) DeleteService(name string) error {
call := c.recorder.r.VerifyCall("DeleteService", name)
func (c *MockKnServingClient) DeleteService(name string, timeout time.Duration) 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.DeleteService should have comment or be unexported. More info.

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

Expand Down
4 changes: 2 additions & 2 deletions pkg/serving/v1/client_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestMockKnClient(t *testing.T) {
recorder.ListServices(mock.Any(), nil, nil)
recorder.CreateService(&servingv1.Service{}, nil)
recorder.UpdateService(&servingv1.Service{}, nil)
recorder.DeleteService("hello", nil)
recorder.DeleteService("hello", time.Duration(10)*time.Second, nil)
recorder.WaitForService("hello", time.Duration(10)*time.Second, wait.NoopMessageCallback(), nil, 10*time.Second)
recorder.GetRevision("hello", nil, nil)
recorder.ListRevisions(mock.Any(), nil, nil)
Expand All @@ -50,7 +50,7 @@ func TestMockKnClient(t *testing.T) {
client.ListServices(WithName("blub"))
client.CreateService(&servingv1.Service{})
client.UpdateService(&servingv1.Service{})
client.DeleteService("hello")
client.DeleteService("hello", time.Duration(10)*time.Second)
client.WaitForService("hello", time.Duration(10)*time.Second, wait.NoopMessageCallback())
client.GetRevision("hello")
client.ListRevisions(WithName("blub"))
Expand Down
23 changes: 21 additions & 2 deletions pkg/serving/v1/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,19 +179,38 @@ func TestDeleteService(t *testing.T) {
}
return true, nil, errors.NewNotFound(servingv1.Resource("service"), name)
})
serving.AddWatchReactor("services",
func(a clienttesting.Action) (bool, watch.Interface, error) {
watchAction := a.(clienttesting.WatchAction)
name, found := watchAction.GetWatchRestrictions().Fields.RequiresExactMatch("metadata.name")
if !found {
return true, nil, errors.NewNotFound(servingv1.Resource("service"), name)
}
w := wait.NewFakeWatch(getServiceDeleteEvents("test-service"))
w.Start()
return true, w, nil
})

t.Run("delete existing service returns no error", func(t *testing.T) {
err := client.DeleteService(serviceName)
err := client.DeleteService(serviceName, time.Duration(10)*time.Second)
assert.NilError(t, err)
})

t.Run("trying to delete non-existing service returns error", func(t *testing.T) {
err := client.DeleteService(nonExistingServiceName)
err := client.DeleteService(nonExistingServiceName, time.Duration(10)*time.Second)
assert.ErrorContains(t, err, "not found")
assert.ErrorContains(t, err, nonExistingServiceName)
})
}

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, "", "")},
}
}

func TestGetRevision(t *testing.T) {
serving, client := setup()

Expand Down
46 changes: 44 additions & 2 deletions pkg/wait/wait_for_ready.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,19 @@ type waitForReadyConfig struct {
kind string
}

// Callbacks and configuration used while waiting for event
type waitForEvent struct {
watchMaker WatchMaker
eventDone EventDone
kind string
}

// Done marker to stop actual waiting on given event state
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 type EventDone should be of the form "EventDone ..." (with optional leading article). More info.

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 +66,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 +196,30 @@ func (w *waitForReadyConfig) waitForReadyCondition(start time.Time, name string,
}
}

// Wait until the expected EventDone is satisfied
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 fmt.Errorf("timeout: %s '%s' not ready after %d seconds", w.kind, name, int(timeout/time.Second)), 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
Loading