Repository: knative/client (go)

Issue:
Consider adding support for `--all` on `kn service delete`
### Feature request

Kubectl has a `--all` option to mean "all instances of the specified resource in the namespace". So for example: `kubectl delete ksvc --all` will delete all KnServices in the default namespace. I find this very useful when I want to quickly clean things up.

It would be really nice if we could do: `kn service delete --all` to delete all KnServices.

It might be good to look into whether we should support `--all` on other commands, like: describe, export, list and update. I'm less sure about some of those, but I do think `delete` will be popular.

### Use case

<!-- Please add a concrete use case to demonstrate how such a feature would add value for the user. If you don't have a use case for your feature, please remove this section (however providing a good use case increases the likelihood to be picked up) -->

/kind proposal


Pull request description:
Add kn service delete --all
## Description

This pull request adds the ability to delete all Services in a namespace using `kn service delete --all`. Follows same behavior as `kubectl delete ksvc --all`.  

## Changes

* Add `--all` flag to `kn service delete`. Boolean value that will delete all Services when specified. False by default. 
* Add `getServiceNames` function to retrieve all the names of Services in a namespace when `--all` flag is specified.
* Unit tests
* Minor reordering of test data in `list_test_data`

## Reference

Fixes #792 

/lint


Patch:
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index af95da59..44f766e0 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -16,6 +16,10 @@
 |===
 | | Description | PR
 
+| 🎁
+| Add kn service delete --all
+| https://github.com/knative/client/pull/836[#836]
+
 | 🐛
 | Skip LatestReadyRevisionName if Revision is Pending or Unknown
 | https://github.com/knative/client/pull/825[#825]
diff --git a/docs/cmd/kn_service_delete.md b/docs/cmd/kn_service_delete.md
index 394463a5..deaf8377 100644
--- a/docs/cmd/kn_service_delete.md
+++ b/docs/cmd/kn_service_delete.md
@@ -19,11 +19,15 @@ kn service delete NAME [flags]
 
   # Delete a service 'svc2' in 'ns1' namespace
   kn service delete svc2 -n ns1
+
+  # Delete all services in 'ns1' namespace
+  kn service delete --all -n ns1
 ```
 
 ### Options
 
 ```
+      --all                Delete all services in a namespace.
       --async              DEPRECATED: please use --no-wait instead. Do not wait for 'service delete' operation to be completed. (default true)
   -h, --help               help for delete
   -n, --namespace string   Specify the namespace to operate in.
diff --git a/pkg/kn/commands/service/delete.go b/pkg/kn/commands/service/delete.go
index 73a6e17b..49c3504f 100644
--- a/pkg/kn/commands/service/delete.go
+++ b/pkg/kn/commands/service/delete.go
@@ -22,6 +22,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"knative.dev/client/pkg/kn/commands"
+	clientservingv1 "knative.dev/client/pkg/serving/v1"
 )
 
 // NewServiceDeleteCommand represent 'service delete' command
@@ -36,13 +37,26 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
   kn service delete svc1
 
   # Delete a service 'svc2' in 'ns1' namespace
-  kn service delete svc2 -n ns1`,
+  kn service delete svc2 -n ns1
+
+  # Delete all services in 'ns1' namespace
+  kn service delete --all -n ns1`,
 
 		RunE: func(cmd *cobra.Command, args []string) error {
-			if len(args) < 1 {
+			all, err := cmd.Flags().GetBool("all")
+			if err != nil {
+				return err
+			}
+			argsLen := len(args)
+
+			if argsLen < 1 && !all {
 				return errors.New("'service delete' requires the service name(s)")
 			}
 
+			if argsLen > 0 && all {
+				return errors.New("'service delete' with --all flag requires no arguments")
+			}
+
 			namespace, err := p.GetNamespace(cmd)
 			if err != nil {
 				return err
@@ -51,6 +65,18 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
 			if err != nil {
 				return err
 			}
+
+			if all {
+				args, err = getServiceNames(client)
+				if err != nil {
+					return err
+				}
+				if len(args) == 0 {
+					fmt.Fprintf(cmd.OutOrStdout(), "No services found.\n")
+					return nil
+				}
+			}
+
 			for _, name := range args {
 				timeout := time.Duration(0)
 				if waitFlags.Wait {
@@ -66,7 +92,21 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
 			return nil
 		},
 	}
+	flags := serviceDeleteCommand.Flags()
+	flags.Bool("all", false, "Delete all services in a namespace.")
 	commands.AddNamespaceFlags(serviceDeleteCommand.Flags(), false)
 	waitFlags.AddConditionWaitFlags(serviceDeleteCommand, commands.WaitDefaultTimeout, "delete", "service", "deleted")
 	return serviceDeleteCommand
 }
+
+func getServiceNames(client clientservingv1.KnServingClient) ([]string, error) {
+	serviceList, err := client.ListServices()
+	if err != nil {
+		return []string{}, err
+	}
+	serviceNames := []string{}
+	for _, service := range serviceList.Items {
+		serviceNames = append(serviceNames, service.Name)
+	}
+	return serviceNames, nil
+}


Test patch:
diff --git a/pkg/kn/commands/service/delete_mock_test.go b/pkg/kn/commands/service/delete_mock_test.go
index f901b9dd..8b5d918f 100644
--- a/pkg/kn/commands/service/delete_mock_test.go
+++ b/pkg/kn/commands/service/delete_mock_test.go
@@ -22,6 +22,7 @@ import (
 	clientservingv1 "knative.dev/client/pkg/serving/v1"
 	"knative.dev/client/pkg/util"
 	"knative.dev/client/pkg/util/mock"
+	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
 )
 
 func TestServiceDeleteMock(t *testing.T) {
@@ -77,6 +78,56 @@ func TestMultipleServiceDeleteMock(t *testing.T) {
 	r.Validate()
 }
 
+func TestServiceDeleteAllMock(t *testing.T) {
+	// New mock client
+	client := clientservingv1.NewMockKnServiceClient(t)
+
+	// Recording:
+	r := client.Recorder()
+
+	// Wait for delete event
+	r.DeleteService("foo", mock.Any(), nil)
+	r.DeleteService("bar", mock.Any(), nil)
+	r.DeleteService("baz", mock.Any(), nil)
+
+	service1 := createMockServiceWithParams("foo", "default", "http://foo.default.example.com", "foo-xyz")
+	service2 := createMockServiceWithParams("bar", "default", "http://bar.default.example.com", "bar-xyz")
+	service3 := createMockServiceWithParams("baz", "default", "http://baz.default.example.com", "baz-xyz")
+	serviceList := &servingv1.ServiceList{Items: []servingv1.Service{*service1, *service2, *service3}}
+	r.ListServices(mock.Any(), serviceList, nil)
+
+	output, err := executeServiceCommand(client, "delete", "--all")
+	assert.NilError(t, err)
+	assert.Assert(t, util.ContainsAll(output, "deleted", "foo", "bar", "baz", "default"))
+
+	r.Validate()
+}
+
+func TestServiceDeleteAllErrorFromArgMock(t *testing.T) {
+	// New mock client
+	client := clientservingv1.NewMockKnServiceClient(t)
+
+	_, err := executeServiceCommand(client, "delete", "foo", "--all")
+	assert.Error(t, err, "'service delete' with --all flag requires no arguments")
+}
+
+func TestServiceDeleteAllNoServicesMock(t *testing.T) {
+	// New mock client
+	client := clientservingv1.NewMockKnServiceClient(t)
+
+	// Recording:
+	r := client.Recorder()
+
+	serviceList := &servingv1.ServiceList{Items: []servingv1.Service{}}
+	r.ListServices(mock.Any(), serviceList, nil)
+
+	output, err := executeServiceCommand(client, "delete", "--all")
+	assert.NilError(t, err)
+	assert.Assert(t, util.ContainsAll(output, "No", "services", "found"))
+
+	r.Validate()
+}
+
 func TestServiceDeleteNoSvcNameMock(t *testing.T) {
 	// New mock client
 	client := clientservingv1.NewMockKnServiceClient(t)
diff --git a/pkg/kn/commands/service/list_mock_test.go b/pkg/kn/commands/service/list_mock_test.go
index 0f42f2b3..c3ec42b9 100644
--- a/pkg/kn/commands/service/list_mock_test.go
+++ b/pkg/kn/commands/service/list_mock_test.go
@@ -94,8 +94,8 @@ func TestServiceListDefaultOutputMock(t *testing.T) {
 	r := client.Recorder()
 
 	service1 := createMockServiceWithParams("foo", "default", "http://foo.default.example.com", "foo-xyz")
-	service3 := createMockServiceWithParams("sss", "default", "http://sss.default.example.com", "sss-xyz")
 	service2 := createMockServiceWithParams("bar", "default", "http://bar.default.example.com", "bar-xyz")
+	service3 := createMockServiceWithParams("sss", "default", "http://sss.default.example.com", "sss-xyz")
 	serviceList := &servingv1.ServiceList{Items: []servingv1.Service{*service1, *service2, *service3}}
 	r.ListServices(mock.Any(), serviceList, nil)
 
diff --git a/test/e2e/service_test.go b/test/e2e/service_test.go
index 7efa05bf..ead29c71 100644
--- a/test/e2e/service_test.go
+++ b/test/e2e/service_test.go
@@ -58,6 +58,12 @@ func TestService(t *testing.T) {
 
 	t.Log("create service private and make public")
 	serviceCreatePrivateUpdatePublic(r, "hello-private-public")
+
+	t.Log("delete all services in a namespace")
+	test.ServiceCreate(r, "svc1")
+	test.ServiceCreate(r, "service2")
+	test.ServiceCreate(r, "ksvc3")
+	serviceDeleteAll(r, "svc1", "service2", "ksvc3")
 }
 
 func serviceCreatePrivate(r *test.KnRunResultCollector, serviceName string) {
@@ -133,3 +139,19 @@ func serviceMultipleDelete(r *test.KnRunResultCollector, existService, nonexistS
 	assert.Check(r.T(), strings.Contains(out.Stdout, expectedSuccess), "Failed to get 'successfully deleted' message")
 	assert.Check(r.T(), strings.Contains(out.Stdout, expectedErr), "Failed to get 'not found' error")
 }
+
+func serviceDeleteAll(r *test.KnRunResultCollector, service1 string, service2 string, service3 string) {
+	out := r.KnTest().Kn().Run("service", "list")
+	r.AssertNoError(out)
+	assert.Check(r.T(), strings.Contains(out.Stdout, service1), "The service ", service1, " does not exist (but is expected to exist)")
+	assert.Check(r.T(), strings.Contains(out.Stdout, service2), "The service ", service2, " does not exist (but is expected to exist)")
+	assert.Check(r.T(), strings.Contains(out.Stdout, service3), "The service ", service3, " does not exist (but is expected to exist)")
+
+	out = r.KnTest().Kn().Run("service", "delete", "--all")
+	r.AssertNoError(out)
+
+	namespace := r.KnTest().Kn().Namespace()
+	expectedSuccess := fmt.Sprintf("Service '%s' successfully deleted in namespace '%s'.\nService '%s' successfully deleted in namespace '%s'.\nService '%s' successfully deleted in namespace '%s'.\n",
+		service3, namespace, service2, namespace, service1, namespace)
+	assert.Check(r.T(), strings.Contains(out.Stdout, expectedSuccess), "Failed to get 'successfully deleted' message")
+}