diff --git a/pkg/kn/commands/channel/delete.go b/pkg/kn/commands/channel/delete.go index 3c22b5db2..ebb3eb5d2 100644 --- a/pkg/kn/commands/channel/delete.go +++ b/pkg/kn/commands/channel/delete.go @@ -30,6 +30,7 @@ func NewChannelDeleteCommand(p *commands.KnParams) *cobra.Command { Example: ` # Delete a channel 'pipe' kn channel delete pipe`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'kn channel delete' requires the channel name as single argument") diff --git a/pkg/kn/commands/channel/describe.go b/pkg/kn/commands/channel/describe.go index 216613402..38e03cdab 100644 --- a/pkg/kn/commands/channel/describe.go +++ b/pkg/kn/commands/channel/describe.go @@ -43,9 +43,10 @@ func NewChannelDescribeCommand(p *commands.KnParams) *cobra.Command { machineReadablePrintFlags := genericclioptions.NewPrintFlags("") cmd := &cobra.Command{ - Use: "describe NAME", - Short: "Show details of a channel", - Example: describeExample, + Use: "describe NAME", + Short: "Show details of a channel", + Example: describeExample, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'kn channel describe' requires the channel name given as single argument") diff --git a/pkg/kn/commands/completion_helper.go b/pkg/kn/commands/completion_helper.go index 90a3c62a9..d1816a5d7 100644 --- a/pkg/kn/commands/completion_helper.go +++ b/pkg/kn/commands/completion_helper.go @@ -29,11 +29,18 @@ type completionConfig struct { var ( resourceToFuncMap = map[string]func(config *completionConfig) []string{ - "broker": completeBroker, - "domain": completeDomain, - "revision": completeRevision, - "route": completeRoute, - "service": completeService, + "apiserver": completeApiserverSource, + "binding": completeBindingSource, + "broker": completeBroker, + "channel": completeChannel, + "container": completeContainerSource, + "domain": completeDomain, + "ping": completePingSource, + "revision": completeRevision, + "route": completeRoute, + "service": completeService, + "subscription": completeSubscription, + "trigger": completeTrigger, } ) @@ -231,3 +238,192 @@ func completeDomain(config *completionConfig) (suggestions []string) { } return } + +func completeTrigger(config *completionConfig) (suggestions []string) { + suggestions = make([]string, 0) + if len(config.args) != 0 { + return + } + namespace, err := config.params.GetNamespace(config.command) + if err != nil { + return + } + client, err := config.params.NewEventingClient(namespace) + if err != nil { + return + } + triggerList, err := client.ListTriggers(config.command.Context()) + if err != nil { + return + } + for _, sug := range triggerList.Items { + if !strings.HasPrefix(sug.Name, config.toComplete) { + continue + } + suggestions = append(suggestions, sug.Name) + } + return +} + +func completeContainerSource(config *completionConfig) (suggestions []string) { + suggestions = make([]string, 0) + if len(config.args) != 0 { + return + } + namespace, err := config.params.GetNamespace(config.command) + if err != nil { + return + } + client, err := config.params.NewSourcesClient(namespace) + if err != nil { + return + } + containerSourceList, err := client.ContainerSourcesClient().ListContainerSources(config.command.Context()) + if err != nil { + return + } + for _, sug := range containerSourceList.Items { + if !strings.HasPrefix(sug.Name, config.toComplete) { + continue + } + suggestions = append(suggestions, sug.Name) + } + return +} + +func completeApiserverSource(config *completionConfig) (suggestions []string) { + suggestions = make([]string, 0) + if len(config.args) != 0 { + return + } + namespace, err := config.params.GetNamespace(config.command) + if err != nil { + return + } + client, err := config.params.NewSourcesClient(namespace) + if err != nil { + return + } + apiServerSourceList, err := client.APIServerSourcesClient().ListAPIServerSource(config.command.Context()) + if err != nil { + return + } + for _, sug := range apiServerSourceList.Items { + if !strings.HasPrefix(sug.Name, config.toComplete) { + continue + } + suggestions = append(suggestions, sug.Name) + } + return +} + +func completeBindingSource(config *completionConfig) (suggestions []string) { + suggestions = make([]string, 0) + if len(config.args) != 0 { + return + } + namespace, err := config.params.GetNamespace(config.command) + if err != nil { + return + } + client, err := config.params.NewSourcesClient(namespace) + if err != nil { + return + } + bindingList, err := client.SinkBindingClient().ListSinkBindings(config.command.Context()) + if err != nil { + return + } + for _, sug := range bindingList.Items { + if !strings.HasPrefix(sug.Name, config.toComplete) { + continue + } + suggestions = append(suggestions, sug.Name) + } + return +} + +func completePingSource(config *completionConfig) (suggestions []string) { + suggestions = make([]string, 0) + if len(config.args) != 0 { + return + } + namespace, err := config.params.GetNamespace(config.command) + if err != nil { + return + } + + client, err := config.params.NewSourcesV1beta2Client(namespace) + if err != nil { + return + } + pingSourcesClient := client.PingSourcesClient() + + pingSourceList, err := pingSourcesClient.ListPingSource(config.command.Context()) + if err != nil { + return + } + for _, sug := range pingSourceList.Items { + if !strings.HasPrefix(sug.Name, config.toComplete) { + continue + } + suggestions = append(suggestions, sug.Name) + } + return +} + +func completeChannel(config *completionConfig) (suggestions []string) { + suggestions = make([]string, 0) + if len(config.args) != 0 { + return + } + namespace, err := config.params.GetNamespace(config.command) + if err != nil { + return + } + + client, err := config.params.NewMessagingClient(namespace) + if err != nil { + return + } + + channelList, err := client.ChannelsClient().ListChannel(config.command.Context()) + if err != nil { + return + } + for _, sug := range channelList.Items { + if !strings.HasPrefix(sug.Name, config.toComplete) { + continue + } + suggestions = append(suggestions, sug.Name) + } + return +} + +func completeSubscription(config *completionConfig) (suggestions []string) { + suggestions = make([]string, 0) + if len(config.args) != 0 { + return + } + namespace, err := config.params.GetNamespace(config.command) + if err != nil { + return + } + + client, err := config.params.NewMessagingClient(namespace) + if err != nil { + return + } + + subscriptionList, err := client.SubscriptionsClient().ListSubscription(config.command.Context()) + if err != nil { + return + } + for _, sug := range subscriptionList.Items { + if !strings.HasPrefix(sug.Name, config.toComplete) { + continue + } + suggestions = append(suggestions, sug.Name) + } + return +} diff --git a/pkg/kn/commands/completion_helper_test.go b/pkg/kn/commands/completion_helper_test.go index 496c3a06c..0422bd26b 100644 --- a/pkg/kn/commands/completion_helper_test.go +++ b/pkg/kn/commands/completion_helper_test.go @@ -25,7 +25,16 @@ import ( "gotest.tools/v3/assert" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" + v1beta1 "knative.dev/client/pkg/messaging/v1" clientv1alpha1 "knative.dev/client/pkg/serving/v1alpha1" + clientsourcesv1 "knative.dev/client/pkg/sources/v1" + "knative.dev/client/pkg/sources/v1beta2" + v12 "knative.dev/eventing/pkg/apis/messaging/v1" + sourcesv1 "knative.dev/eventing/pkg/apis/sources/v1" + sourcesv1beta2 "knative.dev/eventing/pkg/apis/sources/v1beta2" + sourcesv1fake "knative.dev/eventing/pkg/client/clientset/versioned/typed/sources/v1/fake" + sourcesv1beta2fake "knative.dev/eventing/pkg/client/clientset/versioned/typed/sources/v1beta2/fake" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -50,6 +59,19 @@ type testType struct { resource string } +type mockMessagingClient struct { + channelsClient v1beta1.KnChannelsClient + subscriptionsClient v1beta1.KnSubscriptionsClient +} + +func (m *mockMessagingClient) ChannelsClient() v1beta1.KnChannelsClient { + return m.channelsClient +} + +func (m *mockMessagingClient) SubscriptionsClient() v1beta1.KnSubscriptionsClient { + return m.subscriptionsClient +} + const ( testNs = "test-ns" errorNs = "error-ns" @@ -185,9 +207,204 @@ var ( testNsDomains = []v1alpha1.DomainMapping{testDomain1, testDomain2, testDomain3} ) +var ( + testTrigger1 = eventingv1.Trigger{ + TypeMeta: metav1.TypeMeta{ + Kind: "Trigger", + APIVersion: "eventing.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-trigger-1", Namespace: testNs}, + } + testTrigger2 = eventingv1.Trigger{ + TypeMeta: metav1.TypeMeta{ + Kind: "Trigger", + APIVersion: "eventing.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-trigger-2", Namespace: testNs}, + } + testTrigger3 = eventingv1.Trigger{ + TypeMeta: metav1.TypeMeta{ + Kind: "Trigger", + APIVersion: "eventing.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-trigger-3", Namespace: testNs}, + } + testNsTriggers = []eventingv1.Trigger{testTrigger1, testTrigger2, testTrigger3} +) + +var ( + testContainerSource1 = sourcesv1.ContainerSource{ + TypeMeta: metav1.TypeMeta{ + Kind: "ContainerSource", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-container-source-1", Namespace: testNs}, + } + testContainerSource2 = sourcesv1.ContainerSource{ + TypeMeta: metav1.TypeMeta{ + Kind: "ContainerSource", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-container-source-2", Namespace: testNs}, + } + testContainerSource3 = sourcesv1.ContainerSource{ + TypeMeta: metav1.TypeMeta{ + Kind: "ContainerSource", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-container-source-3", Namespace: testNs}, + } + testNsContainerSources = []sourcesv1.ContainerSource{testContainerSource1, testContainerSource2, testContainerSource3} + fakeSources = &sourcesv1fake.FakeSourcesV1{Fake: &clienttesting.Fake{}} +) + +var ( + testApiServerSource1 = sourcesv1.ApiServerSource{ + TypeMeta: metav1.TypeMeta{ + Kind: "ApiServerSource", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-ApiServer-source-1", Namespace: testNs}, + } + testApiServerSource2 = sourcesv1.ApiServerSource{ + TypeMeta: metav1.TypeMeta{ + Kind: "ApiServerSource", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-ApiServer-source-2", Namespace: testNs}, + } + testApiServerSource3 = sourcesv1.ApiServerSource{ + TypeMeta: metav1.TypeMeta{ + Kind: "ApiServerSource", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-ApiServer-source-3", Namespace: testNs}, + } + testNsApiServerSources = []sourcesv1.ApiServerSource{testApiServerSource1, testApiServerSource2, testApiServerSource3} +) + +var ( + testSinkBinding1 = sourcesv1.SinkBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "SinkBinding", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-sink-binding-1", Namespace: testNs}, + } + testSinkBinding2 = sourcesv1.SinkBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "SinkBinding", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-sink-binding-2", Namespace: testNs}, + } + testSinkBinding3 = sourcesv1.SinkBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "SinkBinding", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-sink-binding-3", Namespace: testNs}, + } + testNsSinkBindings = []sourcesv1.SinkBinding{testSinkBinding1, testSinkBinding2, testSinkBinding3} +) + +var ( + testPingSource1 = sourcesv1beta2.PingSource{ + TypeMeta: metav1.TypeMeta{ + Kind: "PingSource", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-ping-source-1", Namespace: testNs}, + } + testPingSource2 = sourcesv1beta2.PingSource{ + TypeMeta: metav1.TypeMeta{ + Kind: "PingSource", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-ping-source-2", Namespace: testNs}, + } + testPingSource3 = sourcesv1beta2.PingSource{ + TypeMeta: metav1.TypeMeta{ + Kind: "PingSource", + APIVersion: "sources.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-ping-source-3", Namespace: testNs}, + } + testNsPingSources = []sourcesv1beta2.PingSource{testPingSource1, testPingSource2, testPingSource3} + fakeSourcesV1Beta2 = &sourcesv1beta2fake.FakeSourcesV1beta2{Fake: &clienttesting.Fake{}} +) + +var ( + testChannel1 = v12.Channel{ + TypeMeta: metav1.TypeMeta{ + Kind: "Channel", + APIVersion: "messaging.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-channel-1", Namespace: testNs}, + } + testChannel2 = v12.Channel{ + TypeMeta: metav1.TypeMeta{ + Kind: "Channel", + APIVersion: "messaging.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-channel-2", Namespace: testNs}, + } + testChannel3 = v12.Channel{ + TypeMeta: metav1.TypeMeta{ + Kind: "Channel", + APIVersion: "messaging.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-channel-3", Namespace: testNs}, + } + testNsChannels = []v12.Channel{testChannel1, testChannel2, testChannel3} +) + +var ( + testSubscription1 = v12.Subscription{ + TypeMeta: metav1.TypeMeta{ + Kind: "Subscription", + APIVersion: "messaging.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-subscription-1", Namespace: testNs}, + } + testSubscription2 = v12.Subscription{ + TypeMeta: metav1.TypeMeta{ + Kind: "Subscription", + APIVersion: "messaging.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-subscription-2", Namespace: testNs}, + } + testSubscription3 = v12.Subscription{ + TypeMeta: metav1.TypeMeta{ + Kind: "Subscription", + APIVersion: "messaging.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test-subscription-3", Namespace: testNs}, + } + testNsSubscriptions = []v12.Subscription{testSubscription1, testSubscription2, testSubscription3} +) + var knParams = initialiseKnParams() func initialiseKnParams() *KnParams { + blankConfig, err := clientcmd.NewClientConfigFromBytes([]byte(`kind: Config +version: v1beta2 +users: +- name: u +clusters: +- name: c + cluster: + server: example.com +contexts: +- name: x + context: + user: u + cluster: c +current-context: x +`)) + if err != nil { + panic(err) + } return &KnParams{ NewServingClient: func(namespace string) (v1.KnServingClient, error) { return v1.NewKnServingClient(fakeServing, namespace), nil @@ -201,6 +418,13 @@ func initialiseKnParams() *KnParams { NewServingV1alpha1Client: func(namespace string) (clientv1alpha1.KnServingClient, error) { return clientv1alpha1.NewKnServingClient(fakeServingAlpha, namespace), nil }, + NewSourcesClient: func(namespace string) (clientsourcesv1.KnSourcesClient, error) { + return clientsourcesv1.NewKnSourcesClient(fakeSources, namespace), nil + }, + NewSourcesV1beta2Client: func(namespace string) (v1beta2.KnSourcesClient, error) { + return v1beta2.NewKnSourcesClient(fakeSourcesV1Beta2, namespace), nil + }, + ClientConfig: blankConfig, } } @@ -296,7 +520,7 @@ func TestResourceNameCompletionFuncBroker(t *testing.T) { fakeEventing.AddReactor("list", "brokers", func(action clienttesting.Action) (bool, runtime.Object, error) { if action.GetNamespace() == errorNs { - return true, nil, errors.NewInternalError(fmt.Errorf("unable to list services")) + return true, nil, errors.NewInternalError(fmt.Errorf("unable to list brokers")) } return true, &eventingv1.BrokerList{Items: testNsBrokers}, nil }) @@ -531,7 +755,7 @@ func TestResourceNameCompletionFuncRoute(t *testing.T) { fakeServing.AddReactor("list", "routes", func(a clienttesting.Action) (bool, runtime.Object, error) { if a.GetNamespace() == errorNs { - return true, nil, errors.NewInternalError(fmt.Errorf("unable to list services")) + return true, nil, errors.NewInternalError(fmt.Errorf("unable to list routes")) } return true, &servingv1.RouteList{Items: testNsRoutes}, nil }) @@ -609,7 +833,7 @@ func TestResourceNameCompletionFuncDomain(t *testing.T) { fakeServingAlpha.AddReactor("list", "domainmappings", func(a clienttesting.Action) (bool, runtime.Object, error) { if a.GetNamespace() == errorNs { - return true, nil, errors.NewInternalError(fmt.Errorf("unable to list services")) + return true, nil, errors.NewInternalError(fmt.Errorf("unable to list domains")) } return true, &v1alpha1.DomainMappingList{Items: testNsDomains}, nil }) @@ -681,6 +905,568 @@ func TestResourceNameCompletionFuncDomain(t *testing.T) { } } +func TestResourceNameCompletionFuncTrigger(t *testing.T) { + completionFunc := ResourceNameCompletionFunc(knParams) + + fakeServing.AddReactor("list", "triggers", + func(a clienttesting.Action) (bool, runtime.Object, error) { + if a.GetNamespace() == errorNs { + return true, nil, errors.NewInternalError(fmt.Errorf("unable to list triggers")) + } + return true, &eventingv1.TriggerList{Items: testNsTriggers}, nil + }) + + tests := []testType{ + { + "Empty suggestions when non-zero args", + testNs, + knParams, + []string{"xyz"}, + "", + "trigger", + }, + { + "Empty suggestions when no namespace flag", + "", + knParams, + nil, + "", + "trigger", + }, + { + "Suggestions when test-ns namespace set", + testNs, + knParams, + nil, + "", + "trigger", + }, + { + "Empty suggestions when toComplete is not a prefix", + testNs, + knParams, + nil, + "xyz", + "trigger", + }, + { + "Empty suggestions when error during list operation", + errorNs, + knParams, + nil, + "", + "trigger", + }, + } + for _, tt := range tests { + cmd := getResourceCommandWithTestSubcommand(tt.resource, tt.namespace != "", tt.resource != "no-parent") + t.Run(tt.name, func(t *testing.T) { + config := &completionConfig{ + params: tt.p, + command: cmd, + args: tt.args, + toComplete: tt.toComplete, + } + expectedFunc := resourceToFuncMap[tt.resource] + if expectedFunc == nil { + expectedFunc = func(config *completionConfig) []string { + return []string{} + } + } + cmd.Flags().Set("namespace", tt.namespace) + actualSuggestions, actualDirective := completionFunc(cmd, tt.args, tt.toComplete) + expectedSuggestions := expectedFunc(config) + expectedDirective := cobra.ShellCompDirectiveNoFileComp + assert.DeepEqual(t, actualSuggestions, expectedSuggestions) + assert.Equal(t, actualDirective, expectedDirective) + }) + } +} + +func TestResourceNameCompletionFuncContainerSource(t *testing.T) { + completionFunc := ResourceNameCompletionFunc(knParams) + + fakeSources.AddReactor("list", "containersources", + func(a clienttesting.Action) (bool, runtime.Object, error) { + if a.GetNamespace() == errorNs { + return true, nil, errors.NewInternalError(fmt.Errorf("unable to list container sources")) + } + return true, &sourcesv1.ContainerSourceList{Items: testNsContainerSources}, nil + }) + + tests := []testType{ + { + "Empty suggestions when non-zero args", + testNs, + knParams, + []string{"xyz"}, + "", + "container", + }, + { + "Empty suggestions when no namespace flag", + "", + knParams, + nil, + "", + "container", + }, + { + "Suggestions when test-ns namespace set", + testNs, + knParams, + nil, + "", + "container", + }, + { + "Empty suggestions when toComplete is not a prefix", + testNs, + knParams, + nil, + "xyz", + "container", + }, + { + "Empty suggestions when error during list operation", + errorNs, + knParams, + nil, + "", + "container", + }, + } + for _, tt := range tests { + cmd := getResourceCommandWithTestSubcommand(tt.resource, tt.namespace != "", tt.resource != "no-parent") + t.Run(tt.name, func(t *testing.T) { + config := &completionConfig{ + params: tt.p, + command: cmd, + args: tt.args, + toComplete: tt.toComplete, + } + expectedFunc := resourceToFuncMap[tt.resource] + if expectedFunc == nil { + expectedFunc = func(config *completionConfig) []string { + return []string{} + } + } + cmd.Flags().Set("namespace", tt.namespace) + actualSuggestions, actualDirective := completionFunc(cmd, tt.args, tt.toComplete) + expectedSuggestions := expectedFunc(config) + expectedDirective := cobra.ShellCompDirectiveNoFileComp + assert.DeepEqual(t, actualSuggestions, expectedSuggestions) + assert.Equal(t, actualDirective, expectedDirective) + }) + } +} + +func TestResourceNameCompletionFuncApiserverSource(t *testing.T) { + completionFunc := ResourceNameCompletionFunc(knParams) + + fakeSources.AddReactor("list", "apiserversources", + func(a clienttesting.Action) (bool, runtime.Object, error) { + if a.GetNamespace() == errorNs { + return true, nil, errors.NewInternalError(fmt.Errorf("unable to list apiserver sources")) + } + return true, &sourcesv1.ApiServerSourceList{Items: testNsApiServerSources}, nil + }) + + tests := []testType{ + { + "Empty suggestions when non-zero args", + testNs, + knParams, + []string{"xyz"}, + "", + "apiserver", + }, + { + "Empty suggestions when no namespace flag", + "", + knParams, + nil, + "", + "apiserver", + }, + { + "Suggestions when test-ns namespace set", + testNs, + knParams, + nil, + "", + "apiserver", + }, + { + "Empty suggestions when toComplete is not a prefix", + testNs, + knParams, + nil, + "xyz", + "apiserver", + }, + { + "Empty suggestions when error during list operation", + errorNs, + knParams, + nil, + "", + "apiserver", + }, + } + for _, tt := range tests { + cmd := getResourceCommandWithTestSubcommand(tt.resource, tt.namespace != "", tt.resource != "no-parent") + t.Run(tt.name, func(t *testing.T) { + config := &completionConfig{ + params: tt.p, + command: cmd, + args: tt.args, + toComplete: tt.toComplete, + } + expectedFunc := resourceToFuncMap[tt.resource] + if expectedFunc == nil { + expectedFunc = func(config *completionConfig) []string { + return []string{} + } + } + cmd.Flags().Set("namespace", tt.namespace) + actualSuggestions, actualDirective := completionFunc(cmd, tt.args, tt.toComplete) + expectedSuggestions := expectedFunc(config) + expectedDirective := cobra.ShellCompDirectiveNoFileComp + assert.DeepEqual(t, actualSuggestions, expectedSuggestions) + assert.Equal(t, actualDirective, expectedDirective) + }) + } +} + +func TestResourceNameCompletionFuncBindingSource(t *testing.T) { + completionFunc := ResourceNameCompletionFunc(knParams) + + fakeSources.AddReactor("list", "sinkbindings", + func(a clienttesting.Action) (bool, runtime.Object, error) { + if a.GetNamespace() == errorNs { + return true, nil, errors.NewInternalError(fmt.Errorf("unable to list binding sources")) + } + return true, &sourcesv1.SinkBindingList{Items: testNsSinkBindings}, nil + }) + + tests := []testType{ + { + "Empty suggestions when non-zero args", + testNs, + knParams, + []string{"xyz"}, + "", + "binding", + }, + { + "Empty suggestions when no namespace flag", + "", + knParams, + nil, + "", + "binding", + }, + { + "Suggestions when test-ns namespace set", + testNs, + knParams, + nil, + "", + "binding", + }, + { + "Empty suggestions when toComplete is not a prefix", + testNs, + knParams, + nil, + "xyz", + "binding", + }, + { + "Empty suggestions when error during list operation", + errorNs, + knParams, + nil, + "", + "binding", + }, + } + for _, tt := range tests { + cmd := getResourceCommandWithTestSubcommand(tt.resource, tt.namespace != "", tt.resource != "no-parent") + t.Run(tt.name, func(t *testing.T) { + config := &completionConfig{ + params: tt.p, + command: cmd, + args: tt.args, + toComplete: tt.toComplete, + } + expectedFunc := resourceToFuncMap[tt.resource] + if expectedFunc == nil { + expectedFunc = func(config *completionConfig) []string { + return []string{} + } + } + cmd.Flags().Set("namespace", tt.namespace) + actualSuggestions, actualDirective := completionFunc(cmd, tt.args, tt.toComplete) + expectedSuggestions := expectedFunc(config) + expectedDirective := cobra.ShellCompDirectiveNoFileComp + assert.DeepEqual(t, actualSuggestions, expectedSuggestions) + assert.Equal(t, actualDirective, expectedDirective) + }) + } +} + +func TestResourceNameCompletionFuncPingSource(t *testing.T) { + completionFunc := ResourceNameCompletionFunc(knParams) + + fakeSourcesV1Beta2.AddReactor("list", "pingsources", + func(a clienttesting.Action) (bool, runtime.Object, error) { + if a.GetNamespace() == errorNs { + return true, nil, errors.NewInternalError(fmt.Errorf("unable to list ping sources")) + } + return true, &sourcesv1beta2.PingSourceList{Items: testNsPingSources}, nil + }) + + tests := []testType{ + { + "Empty suggestions when non-zero args", + testNs, + knParams, + []string{"xyz"}, + "", + "ping", + }, + { + "Empty suggestions when no namespace flag", + "", + knParams, + nil, + "", + "ping", + }, + { + "Suggestions when test-ns namespace set", + testNs, + knParams, + nil, + "", + "ping", + }, + { + "Empty suggestions when toComplete is not a prefix", + testNs, + knParams, + nil, + "xyz", + "ping", + }, + { + "Empty suggestions when error during list operation", + errorNs, + knParams, + nil, + "", + "ping", + }, + } + for _, tt := range tests { + cmd := getResourceCommandWithTestSubcommand(tt.resource, tt.namespace != "", tt.resource != "no-parent") + t.Run(tt.name, func(t *testing.T) { + config := &completionConfig{ + params: tt.p, + command: cmd, + args: tt.args, + toComplete: tt.toComplete, + } + expectedFunc := resourceToFuncMap[tt.resource] + if expectedFunc == nil { + expectedFunc = func(config *completionConfig) []string { + return []string{} + } + } + cmd.Flags().Set("namespace", tt.namespace) + actualSuggestions, actualDirective := completionFunc(cmd, tt.args, tt.toComplete) + expectedSuggestions := expectedFunc(config) + expectedDirective := cobra.ShellCompDirectiveNoFileComp + assert.DeepEqual(t, actualSuggestions, expectedSuggestions) + assert.Equal(t, actualDirective, expectedDirective) + }) + } +} + +func TestResourceNameCompletionFuncChannel(t *testing.T) { + completionFunc := ResourceNameCompletionFunc(knParams) + + channelClient := v1beta1.NewMockKnChannelsClient(t) + channelClient.Recorder().ListChannel(&v12.ChannelList{Items: testNsChannels}, nil) + channelClient.Recorder().ListChannel(&v12.ChannelList{Items: testNsChannels}, nil) + + channelClient.Recorder().ListChannel(&v12.ChannelList{Items: testNsChannels}, nil) + channelClient.Recorder().ListChannel(&v12.ChannelList{Items: testNsChannels}, nil) + + channelClient.Recorder().ListChannel(&v12.ChannelList{}, fmt.Errorf("error listing channels")) + channelClient.Recorder().ListChannel(&v12.ChannelList{}, fmt.Errorf("error listing channels")) + + messagingClient := &mockMessagingClient{channelClient, nil} + + knParams.NewMessagingClient = func(namespace string) (v1beta1.KnMessagingClient, error) { + return messagingClient, nil + } + tests := []testType{ + { + "Empty suggestions when non-zero args", + testNs, + knParams, + []string{"xyz"}, + "", + "channel", + }, + { + "Empty suggestions when no namespace flag", + "", + knParams, + nil, + "", + "channel", + }, + { + "Suggestions when test-ns namespace set", + testNs, + knParams, + nil, + "", + "channel", + }, + { + "Empty suggestions when toComplete is not a prefix", + testNs, + knParams, + nil, + "xyz", + "channel", + }, + { + "Empty suggestions when error during list operation", + errorNs, + knParams, + nil, + "", + "channel", + }, + } + for _, tt := range tests { + cmd := getResourceCommandWithTestSubcommand(tt.resource, tt.namespace != "", tt.resource != "no-parent") + t.Run(tt.name, func(t *testing.T) { + config := &completionConfig{ + params: tt.p, + command: cmd, + args: tt.args, + toComplete: tt.toComplete, + } + expectedFunc := resourceToFuncMap[tt.resource] + if expectedFunc == nil { + expectedFunc = func(config *completionConfig) []string { + return []string{} + } + } + cmd.Flags().Set("namespace", tt.namespace) + actualSuggestions, actualDirective := completionFunc(cmd, tt.args, tt.toComplete) + expectedSuggestions := expectedFunc(config) + expectedDirective := cobra.ShellCompDirectiveNoFileComp + assert.DeepEqual(t, actualSuggestions, expectedSuggestions) + assert.Equal(t, actualDirective, expectedDirective) + }) + } + channelClient.Recorder().Validate() +} + +func TestResourceNameCompletionFuncSubscription(t *testing.T) { + completionFunc := ResourceNameCompletionFunc(knParams) + + subscriptionsClient := v1beta1.NewMockKnSubscriptionsClient(t) + subscriptionsClient.Recorder().ListSubscription(&v12.SubscriptionList{Items: testNsSubscriptions}, nil) + subscriptionsClient.Recorder().ListSubscription(&v12.SubscriptionList{Items: testNsSubscriptions}, nil) + + subscriptionsClient.Recorder().ListSubscription(&v12.SubscriptionList{Items: testNsSubscriptions}, nil) + subscriptionsClient.Recorder().ListSubscription(&v12.SubscriptionList{Items: testNsSubscriptions}, nil) + + subscriptionsClient.Recorder().ListSubscription(&v12.SubscriptionList{}, fmt.Errorf("error listing channels")) + subscriptionsClient.Recorder().ListSubscription(&v12.SubscriptionList{}, fmt.Errorf("error listing channels")) + + messagingClient := &mockMessagingClient{nil, subscriptionsClient} + + knParams.NewMessagingClient = func(namespace string) (v1beta1.KnMessagingClient, error) { + return messagingClient, nil + } + tests := []testType{ + { + "Empty suggestions when non-zero args", + testNs, + knParams, + []string{"xyz"}, + "", + "subscription", + }, + { + "Empty suggestions when no namespace flag", + "", + knParams, + nil, + "", + "subscription", + }, + { + "Suggestions when test-ns namespace set", + testNs, + knParams, + nil, + "", + "subscription", + }, + { + "Empty suggestions when toComplete is not a prefix", + testNs, + knParams, + nil, + "xyz", + "subscription", + }, + { + "Empty suggestions when error during list operation", + errorNs, + knParams, + nil, + "", + "subscription", + }, + } + for _, tt := range tests { + cmd := getResourceCommandWithTestSubcommand(tt.resource, tt.namespace != "", tt.resource != "no-parent") + t.Run(tt.name, func(t *testing.T) { + config := &completionConfig{ + params: tt.p, + command: cmd, + args: tt.args, + toComplete: tt.toComplete, + } + expectedFunc := resourceToFuncMap[tt.resource] + if expectedFunc == nil { + expectedFunc = func(config *completionConfig) []string { + return []string{} + } + } + cmd.Flags().Set("namespace", tt.namespace) + actualSuggestions, actualDirective := completionFunc(cmd, tt.args, tt.toComplete) + expectedSuggestions := expectedFunc(config) + expectedDirective := cobra.ShellCompDirectiveNoFileComp + assert.DeepEqual(t, actualSuggestions, expectedSuggestions) + assert.Equal(t, actualDirective, expectedDirective) + }) + } + subscriptionsClient.Recorder().Validate() +} + func getResourceCommandWithTestSubcommand(resource string, addNamespace, addSubcommand bool) *cobra.Command { testCommand := &cobra.Command{ Use: resource, diff --git a/pkg/kn/commands/source/apiserver/delete.go b/pkg/kn/commands/source/apiserver/delete.go index 4490ea30f..be7f2fb7f 100644 --- a/pkg/kn/commands/source/apiserver/delete.go +++ b/pkg/kn/commands/source/apiserver/delete.go @@ -31,6 +31,7 @@ func NewAPIServerDeleteCommand(p *commands.KnParams) *cobra.Command { Example: ` # Delete an ApiServerSource 'k8sevents' in default namespace kn source apiserver delete k8sevents`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("requires the name of the source as single argument") diff --git a/pkg/kn/commands/source/apiserver/describe.go b/pkg/kn/commands/source/apiserver/describe.go index ce502cd17..ad692bc14 100644 --- a/pkg/kn/commands/source/apiserver/describe.go +++ b/pkg/kn/commands/source/apiserver/describe.go @@ -42,9 +42,10 @@ func NewAPIServerDescribeCommand(p *commands.KnParams) *cobra.Command { machineReadablePrintFlags := genericclioptions.NewPrintFlags("") command := &cobra.Command{ - Use: "describe NAME", - Short: "Show details of an api-server source", - Example: describeExample, + Use: "describe NAME", + Short: "Show details of an api-server source", + Example: describeExample, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'kn source apiserver describe' requires name of the source as single argument") diff --git a/pkg/kn/commands/source/apiserver/update.go b/pkg/kn/commands/source/apiserver/update.go index a10655b44..f369a8aaf 100644 --- a/pkg/kn/commands/source/apiserver/update.go +++ b/pkg/kn/commands/source/apiserver/update.go @@ -38,6 +38,7 @@ func NewAPIServerUpdateCommand(p *commands.KnParams) *cobra.Command { # Update an ApiServerSource 'k8sevents' with different service account and sink service kn source apiserver update k8sevents --service-account newsa --sink ksvc:newsvc`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) (err error) { if len(args) != 1 { return errors.New("requires the name of the source as single argument") diff --git a/pkg/kn/commands/source/binding/delete.go b/pkg/kn/commands/source/binding/delete.go index 608d3eac1..ff1cf0dc2 100644 --- a/pkg/kn/commands/source/binding/delete.go +++ b/pkg/kn/commands/source/binding/delete.go @@ -30,6 +30,7 @@ func NewBindingDeleteCommand(p *commands.KnParams) *cobra.Command { Example: ` # Delete a sink binding with name 'my-binding' kn source binding delete my-binding`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("requires the name of the sink binding to delete as single argument") diff --git a/pkg/kn/commands/source/binding/describe.go b/pkg/kn/commands/source/binding/describe.go index ac9b98dad..152e2c568 100644 --- a/pkg/kn/commands/source/binding/describe.go +++ b/pkg/kn/commands/source/binding/describe.go @@ -43,9 +43,10 @@ func NewBindingDescribeCommand(p *commands.KnParams) *cobra.Command { machineReadablePrintFlags := genericclioptions.NewPrintFlags("") command := &cobra.Command{ - Use: "describe NAME", - Short: "Show details of a sink binding", - Example: describeExample, + Use: "describe NAME", + Short: "Show details of a sink binding", + Example: describeExample, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'kn source binding describe' requires name of the sink binding as single argument") diff --git a/pkg/kn/commands/source/binding/update.go b/pkg/kn/commands/source/binding/update.go index eb62c01e7..3886e3f3d 100644 --- a/pkg/kn/commands/source/binding/update.go +++ b/pkg/kn/commands/source/binding/update.go @@ -38,6 +38,7 @@ func NewBindingUpdateCommand(p *commands.KnParams) *cobra.Command { # Update the subject of a sink binding 'my-binding' to a new cronjob with label selector 'app=ping' kn source binding update my-binding --subject cronjob:batch/v1beta1:app=ping"`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) (err error) { if len(args) != 1 { return errors.New("requires the name of the sink binding to update as single argument") diff --git a/pkg/kn/commands/source/container/delete.go b/pkg/kn/commands/source/container/delete.go index 5374b4525..035423392 100644 --- a/pkg/kn/commands/source/container/delete.go +++ b/pkg/kn/commands/source/container/delete.go @@ -33,6 +33,7 @@ func NewContainerDeleteCommand(p *commands.KnParams) *cobra.Command { Example: ` # Delete a ContainerSource 'containersrc' in default namespace kn source container delete containersrc`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("requires the name of the source as single argument") diff --git a/pkg/kn/commands/source/container/describe.go b/pkg/kn/commands/source/container/describe.go index a4859c00f..7e5b1efe8 100644 --- a/pkg/kn/commands/source/container/describe.go +++ b/pkg/kn/commands/source/container/describe.go @@ -36,6 +36,7 @@ func NewContainerDescribeCommand(p *commands.KnParams) *cobra.Command { Example: ` # Describe a container source with name 'k8sevents' kn source container describe k8sevents`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'kn source container describe' requires name of the source as single argument") diff --git a/pkg/kn/commands/source/ping/delete.go b/pkg/kn/commands/source/ping/delete.go index a25576874..b7e8699b0 100644 --- a/pkg/kn/commands/source/ping/delete.go +++ b/pkg/kn/commands/source/ping/delete.go @@ -30,6 +30,7 @@ func NewPingDeleteCommand(p *commands.KnParams) *cobra.Command { Example: ` # Delete a Ping source 'my-ping' kn source ping delete my-ping`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'requires the name of the Ping source to delete as single argument") diff --git a/pkg/kn/commands/source/ping/describe.go b/pkg/kn/commands/source/ping/describe.go index 38bed794a..b6fc2c119 100644 --- a/pkg/kn/commands/source/ping/describe.go +++ b/pkg/kn/commands/source/ping/describe.go @@ -41,9 +41,10 @@ func NewPingDescribeCommand(p *commands.KnParams) *cobra.Command { machineReadablePrintFlags := genericclioptions.NewPrintFlags("") command := &cobra.Command{ - Use: "describe NAME", - Short: "Show details of a ping source", - Example: describeExample, + Use: "describe NAME", + Short: "Show details of a ping source", + Example: describeExample, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'kn source ping describe' requires name of the source as single argument") diff --git a/pkg/kn/commands/source/ping/update.go b/pkg/kn/commands/source/ping/update.go index 69344662c..1eda2f25c 100644 --- a/pkg/kn/commands/source/ping/update.go +++ b/pkg/kn/commands/source/ping/update.go @@ -42,6 +42,7 @@ func NewPingUpdateCommand(p *commands.KnParams) *cobra.Command { # Update the schedule of a Ping source 'my-ping' to fire every minute kn source ping update my-ping --schedule "* * * * *"`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) (err error) { if len(args) != 1 { return errors.New("name of Ping source required") diff --git a/pkg/kn/commands/subscription/delete.go b/pkg/kn/commands/subscription/delete.go index 8d54dcec6..26a7e2350 100644 --- a/pkg/kn/commands/subscription/delete.go +++ b/pkg/kn/commands/subscription/delete.go @@ -32,6 +32,7 @@ func NewSubscriptionDeleteCommand(p *commands.KnParams) *cobra.Command { Example: ` # Delete a subscription 'sub0' kn subscription delete sub0`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'kn subscription delete' requires the subscription name as single argument") diff --git a/pkg/kn/commands/subscription/describe.go b/pkg/kn/commands/subscription/describe.go index db70f733f..aced44a85 100644 --- a/pkg/kn/commands/subscription/describe.go +++ b/pkg/kn/commands/subscription/describe.go @@ -43,6 +43,7 @@ func NewSubscriptionDescribeCommand(p *commands.KnParams) *cobra.Command { Example: ` # Describe a subscription 'pipe' kn subscription describe pipe`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'kn subscription describe' requires the subscription name given as single argument") diff --git a/pkg/kn/commands/subscription/update.go b/pkg/kn/commands/subscription/update.go index e8c636182..851755cc0 100644 --- a/pkg/kn/commands/subscription/update.go +++ b/pkg/kn/commands/subscription/update.go @@ -43,7 +43,7 @@ func NewSubscriptionUpdateCommand(p *commands.KnParams) *cobra.Command { # Update a subscription 'sub1' with subscriber ksvc 'mirror', reply to a broker 'nest' and DeadLetterSink to a ksvc 'bucket' kn subscription update sub1 --sink mirror --sink-reply broker:nest --sink-dead-letter bucket`, - + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) (err error) { if len(args) != 1 { return errors.New("'kn subscription update' requires the subscription name given as single argument") diff --git a/pkg/kn/commands/trigger/delete.go b/pkg/kn/commands/trigger/delete.go index 24d70ad60..0335d2d24 100644 --- a/pkg/kn/commands/trigger/delete.go +++ b/pkg/kn/commands/trigger/delete.go @@ -30,6 +30,7 @@ func NewTriggerDeleteCommand(p *commands.KnParams) *cobra.Command { Example: ` # Delete a trigger 'mytrigger' in default namespace kn trigger delete mytrigger`, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'trigger delete' requires the name of the trigger as single argument") diff --git a/pkg/kn/commands/trigger/describe.go b/pkg/kn/commands/trigger/describe.go index 442143bb0..5ca33a7ab 100644 --- a/pkg/kn/commands/trigger/describe.go +++ b/pkg/kn/commands/trigger/describe.go @@ -40,9 +40,10 @@ func NewTriggerDescribeCommand(p *commands.KnParams) *cobra.Command { machineReadablePrintFlags := genericclioptions.NewPrintFlags("") command := &cobra.Command{ - Use: "describe NAME", - Short: "Show details of a trigger", - Example: describeExample, + Use: "describe NAME", + Short: "Show details of a trigger", + Example: describeExample, + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'kn trigger describe' requires name of the trigger as single argument") diff --git a/pkg/kn/commands/trigger/update.go b/pkg/kn/commands/trigger/update.go index 1d686c79c..d0ab10610 100644 --- a/pkg/kn/commands/trigger/update.go +++ b/pkg/kn/commands/trigger/update.go @@ -48,7 +48,7 @@ func NewTriggerUpdateCommand(p *commands.KnParams) *cobra.Command { # Update the sink of a trigger 'mytrigger' to 'ksvc:new-service' kn trigger update mytrigger --sink ksvc:new-service `, - + ValidArgsFunction: commands.ResourceNameCompletionFunc(p), RunE: func(cmd *cobra.Command, args []string) (err error) { if len(args) != 1 { return errors.New("name of trigger required") diff --git a/pkg/kn/commands/types.go b/pkg/kn/commands/types.go index 733371078..f999cf114 100644 --- a/pkg/kn/commands/types.go +++ b/pkg/kn/commands/types.go @@ -94,6 +94,10 @@ func (params *KnParams) Initialize() { if params.NewDynamicClient == nil { params.NewDynamicClient = params.newDynamicClient } + + if params.NewSourcesV1beta2Client == nil { + params.NewSourcesV1beta2Client = params.newSourcesClientV1beta2 + } } func (params *KnParams) newServingClient(namespace string) (clientservingv1.KnServingClient, error) {