diff --git a/docs/cmd/kn.md b/docs/cmd/kn.md index 0beffe979c..f172ab627d 100644 --- a/docs/cmd/kn.md +++ b/docs/cmd/kn.md @@ -22,6 +22,7 @@ Eventing: Manage event subscriptions and channels. Connect up event sources. * [kn completion](kn_completion.md) - Output shell completion code (default Bash) * [kn revision](kn_revision.md) - Revision command group +* [kn route](kn_route.md) - Route command group * [kn service](kn_service.md) - Service command group * [kn version](kn_version.md) - Prints the client version diff --git a/docs/cmd/kn_route.md b/docs/cmd/kn_route.md new file mode 100644 index 0000000000..9b32fc0289 --- /dev/null +++ b/docs/cmd/kn_route.md @@ -0,0 +1,26 @@ +## kn route + +Route command group + +### Synopsis + +Route command group + +### Options + +``` + -h, --help help for route +``` + +### Options inherited from parent commands + +``` + --config string config file (default is $HOME/.kn/config.yaml) + --kubeconfig string kubectl config file (default is $HOME/.kube/config) +``` + +### SEE ALSO + +* [kn](kn.md) - Knative client +* [kn route list](kn_route_list.md) - List available routes. + diff --git a/docs/cmd/kn_route_list.md b/docs/cmd/kn_route_list.md new file mode 100644 index 0000000000..07babbbfb1 --- /dev/null +++ b/docs/cmd/kn_route_list.md @@ -0,0 +1,48 @@ +## kn route list + +List available routes. + +### Synopsis + +List available routes. + +``` +kn route list NAME [flags] +``` + +### Examples + +``` + + # List all routes + kn route list + + # List route 'web' in namespace 'dev' + kn route list web -n dev + + # List all routes in yaml format + kn route list -o yaml +``` + +### Options + +``` + --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + --allow-missing-template-keys If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats. (default true) + -h, --help help for list + -n, --namespace string List the requested object(s) in given namespace. + -o, --output string Output format. One of: json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file. + --template string Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]. +``` + +### Options inherited from parent commands + +``` + --config string config file (default is $HOME/.kn/config.yaml) + --kubeconfig string kubectl config file (default is $HOME/.kube/config) +``` + +### SEE ALSO + +* [kn route](kn_route.md) - Route command group + diff --git a/pkg/kn/commands/route/human_readable_flags.go b/pkg/kn/commands/route/human_readable_flags.go new file mode 100644 index 0000000000..5ed75c40af --- /dev/null +++ b/pkg/kn/commands/route/human_readable_flags.go @@ -0,0 +1,82 @@ +// Copyright © 2019 The Knative Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "fmt" + + "github.com/knative/client/pkg/kn/commands" + hprinters "github.com/knative/client/pkg/printers" + servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +// RouteListHandlers adds print handlers for route list command +func RouteListHandlers(h hprinters.PrintHandler) { + kRouteColumnDefinitions := []metav1beta1.TableColumnDefinition{ + {Name: "Name", Type: "string", Description: "Name of the Knative route."}, + {Name: "URL", Type: "string", Description: "URL of the Knative route."}, + {Name: "Age", Type: "string", Description: "Age of the Knative route."}, + {Name: "Conditions", Type: "string", Description: "Conditions describing statuses of route components."}, + {Name: "Traffic", Type: "integer", Description: "Traffic configured for route."}, + } + h.TableHandler(kRouteColumnDefinitions, printRoute) + h.TableHandler(kRouteColumnDefinitions, printKRouteList) +} + +// printKRouteList populates the Knative route list table rows +func printKRouteList(kRouteList *servingv1alpha1.RouteList, options hprinters.PrintOptions) ([]metav1beta1.TableRow, error) { + rows := make([]metav1beta1.TableRow, 0, len(kRouteList.Items)) + for _, ksvc := range kRouteList.Items { + r, err := printRoute(&ksvc, options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + +// printRoute populates the Knative route table rows +func printRoute(route *servingv1alpha1.Route, options hprinters.PrintOptions) ([]metav1beta1.TableRow, error) { + name := route.Name + url := route.Status.URL + age := commands.TranslateTimestampSince(route.CreationTimestamp) + conditions := commands.ConditionsValue(route.Status.Conditions) + traffic := calculateTraffic(route.Status.Traffic) + row := metav1beta1.TableRow{ + Object: runtime.RawExtension{Object: route}, + } + row.Cells = append(row.Cells, + name, + url, + age, + conditions, + traffic) + return []metav1beta1.TableRow{row}, nil +} + +func calculateTraffic(targets []servingv1alpha1.TrafficTarget) string { + var traffic string + for _, target := range targets { + if len(traffic) > 0 { + traffic = fmt.Sprintf("%s, %d%% -> %s", traffic, target.Percent, target.RevisionName) + } else { + traffic = fmt.Sprintf("%d%% -> %s", target.Percent, target.RevisionName) + } + } + return traffic +} diff --git a/pkg/kn/commands/route/list.go b/pkg/kn/commands/route/list.go new file mode 100644 index 0000000000..ee2e5e6b55 --- /dev/null +++ b/pkg/kn/commands/route/list.go @@ -0,0 +1,87 @@ +// Copyright © 2019 The Knative Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "fmt" + + "github.com/knative/client/pkg/kn/commands" + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// NewrouteListCommand represents 'kn route list' command +func NewRouteListCommand(p *commands.KnParams) *cobra.Command { + routeListFlags := NewRouteListFlags() + routeListCommand := &cobra.Command{ + Use: "list NAME", + Short: "List available routes.", + Example: ` + # List all routes + kn route list + + # List route 'web' in namespace 'dev' + kn route list web -n dev + + # List all routes in yaml format + kn route list -o yaml`, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := p.ServingFactory() + if err != nil { + return err + } + namespace, err := p.GetNamespace(cmd) + if err != nil { + return err + } + var listOptions v1.ListOptions + switch len(args) { + case 0: + listOptions = v1.ListOptions{} + case 1: + listOptions.FieldSelector = fields.Set(map[string]string{"metadata.name": args[0]}).String() + default: + return fmt.Errorf("'kn route list' accepts maximum 1 argument.") + } + route, err := client.Routes(namespace).List(listOptions) + if err != nil { + return err + } + if len(route.Items) == 0 { + fmt.Fprintf(cmd.OutOrStdout(), "No resources found.\n") + return nil + } + route.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{ + Group: "knative.dev", + Version: "v1alpha1", + Kind: "route", + }) + printer, err := routeListFlags.ToPrinter() + if err != nil { + return err + } + err = printer.PrintObj(route, cmd.OutOrStdout()) + if err != nil { + return err + } + return nil + }, + } + commands.AddNamespaceFlags(routeListCommand.Flags(), true) + routeListFlags.AddFlags(routeListCommand) + return routeListCommand +} diff --git a/pkg/kn/commands/route/list_flags.go b/pkg/kn/commands/route/list_flags.go new file mode 100644 index 0000000000..7a0c613887 --- /dev/null +++ b/pkg/kn/commands/route/list_flags.go @@ -0,0 +1,71 @@ +// Copyright © 2019 The Knative Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or im +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "github.com/knative/client/pkg/kn/commands" + hprinters "github.com/knative/client/pkg/printers" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +// RouteListFlags composes common printer flag structs +// used in the 'kn route list' command. +type RouteListFlags struct { + GenericPrintFlags *genericclioptions.PrintFlags + HumanReadableFlags *commands.HumanPrintFlags +} + +// AllowedFormats is the list of formats in which data can be displayed +func (f *RouteListFlags) AllowedFormats() []string { + formats := f.GenericPrintFlags.AllowedFormats() + formats = append(formats, f.HumanReadableFlags.AllowedFormats()...) + return formats +} + +// ToPrinter attempts to find a composed set of RouteListFlags suitable for +// returning a printer based on current flag values. +func (f *RouteListFlags) ToPrinter() (hprinters.ResourcePrinter, error) { + // if there are flags specified for generic printing + if f.GenericPrintFlags.OutputFlagSpecified() { + p, err := f.GenericPrintFlags.ToPrinter() + if err != nil { + return nil, err + } + return p, nil + } + // if no flags specified, use the table printing + p, err := f.HumanReadableFlags.ToPrinter(RouteListHandlers) + if err != nil { + return nil, err + } + return p, nil +} + +// AddFlags receives a *cobra.Command reference and binds +// flags related to humanreadable and template printing. +func (f *RouteListFlags) AddFlags(cmd *cobra.Command) { + f.GenericPrintFlags.AddFlags(cmd) + f.HumanReadableFlags.AddFlags(cmd) +} + +// NewRouteListFlags returns flags associated with humanreadable, +// template, and "name" printing, with default values set. +func NewRouteListFlags() *RouteListFlags { + return &RouteListFlags{ + GenericPrintFlags: genericclioptions.NewPrintFlags(""), + HumanReadableFlags: commands.NewHumanPrintFlags(), + } +} diff --git a/pkg/kn/commands/route/list_flags_test.go b/pkg/kn/commands/route/list_flags_test.go new file mode 100644 index 0000000000..80cc32fa85 --- /dev/null +++ b/pkg/kn/commands/route/list_flags_test.go @@ -0,0 +1,47 @@ +// Copyright © 2019 The Knative Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or im +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "reflect" + "testing" + + "github.com/knative/client/pkg/kn/commands" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +func TestRoutListFlags(t *testing.T) { + testObject := createMockRouteMeta("foo") + knParams := &commands.KnParams{} + cmd, _, buf := commands.CreateTestKnCommand(NewRouteCommand(knParams), knParams) + routeListFlags := NewRouteListFlags() + routeListFlags.AddFlags(cmd) + printer, err := routeListFlags.ToPrinter() + if genericclioptions.IsNoCompatiblePrinterError(err) { + t.Fatalf("Expected to match human readable printer.") + } + if err != nil { + t.Fatalf("Failed to find a proper printer.") + } + err = printer.PrintObj(testObject, buf) + if err != nil { + t.Fatalf("Failed to print the object.") + } + actualFormats := routeListFlags.AllowedFormats() + expectedFormats := []string{"json", "yaml", "name", "go-template", "go-template-file", "template", "templatefile", "jsonpath", "jsonpath-file"} + if reflect.DeepEqual(actualFormats, expectedFormats) { + t.Fatalf("Expecting allowed formats:\n%s\nFound:\n%s\n", expectedFormats, actualFormats) + } +} diff --git a/pkg/kn/commands/route/list_test.go b/pkg/kn/commands/route/list_test.go new file mode 100644 index 0000000000..af5a00e70f --- /dev/null +++ b/pkg/kn/commands/route/list_test.go @@ -0,0 +1,125 @@ +// Copyright © 2018 The Knative Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "strings" + "testing" + + "github.com/knative/client/pkg/kn/commands" + "github.com/knative/serving/pkg/apis/serving/v1alpha1" + "github.com/knative/serving/pkg/apis/serving/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + client_testing "k8s.io/client-go/testing" +) + +func fakeRouteList(args []string, response *v1alpha1.RouteList) (action client_testing.Action, output []string, err error) { + knParams := &commands.KnParams{} + cmd, fakeServing, buf := commands.CreateTestKnCommand(NewRouteCommand(knParams), knParams) + fakeServing.AddReactor("*", "*", + func(a client_testing.Action) (bool, runtime.Object, error) { + action = a + return true, response, nil + }) + cmd.SetArgs(args) + err = cmd.Execute() + if err != nil { + return + } + output = strings.Split(buf.String(), "\n") + return +} + +func TestListEmpty(t *testing.T) { + action, output, err := fakeRouteList([]string{"route", "list"}, &v1alpha1.RouteList{}) + if err != nil { + t.Error(err) + return + } + if action == nil { + t.Errorf("No action") + } else if !action.Matches("list", "routes") { + t.Errorf("Bad action %v", action) + } else if output[0] != "No resources found." { + t.Errorf("Bad output %s", output[0]) + } +} + +func TestRouteListDefaultOutput(t *testing.T) { + route1 := createMockRouteSingleTarget("foo", "foo-01234", 100) + route2 := createMockRouteSingleTarget("bar", "bar-98765", 100) + routeList := &v1alpha1.RouteList{Items: []v1alpha1.Route{*route1, *route2}} + action, output, err := fakeRouteList([]string{"route", "list"}, routeList) + if err != nil { + t.Fatal(err) + } + if action == nil { + t.Errorf("No action") + } else if !action.Matches("list", "routes") { + t.Errorf("Bad action %v", action) + } + commands.TestContains(t, output[0], []string{"NAME", "URL", "AGE", "CONDITIONS", "TRAFFIC"}, "column header") + commands.TestContains(t, output[1], []string{"foo", "100% -> foo-01234"}, "value") + commands.TestContains(t, output[2], []string{"bar", "100% -> bar-98765"}, "value") +} + +func TestRouteListWithTwoTargetsOutput(t *testing.T) { + route := createMockRouteTwoTarget("foo", "foo-01234", "foo-98765", 20, 80) + routeList := &v1alpha1.RouteList{Items: []v1alpha1.Route{*route}} + action, output, err := fakeRouteList([]string{"route", "list"}, routeList) + if err != nil { + t.Fatal(err) + } + if action == nil { + t.Errorf("No action") + } else if !action.Matches("list", "routes") { + t.Errorf("Bad action %v", action) + } + commands.TestContains(t, output[0], []string{"NAME", "URL", "AGE", "CONDITIONS", "TRAFFIC"}, "column header") + commands.TestContains(t, output[1], []string{"foo", "20% -> foo-01234, 80% -> foo-98765"}, "value") +} + +func createMockRouteMeta(name string) *v1alpha1.Route { + route := &v1alpha1.Route{} + route.Kind = "Route" + route.APIVersion = "knative.dev/v1alpha1" + route.Name = name + route.Namespace = commands.FakeNamespace + return route +} + +func createMockTrafficTarget(revision string, percent int) *v1alpha1.TrafficTarget { + return &v1alpha1.TrafficTarget{ + TrafficTarget: v1beta1.TrafficTarget{ + RevisionName: revision, + Percent: percent, + }, + } +} + +func createMockRouteSingleTarget(name, revision string, percent int) *v1alpha1.Route { + route := createMockRouteMeta(name) + target := createMockTrafficTarget(revision, percent) + route.Status.Traffic = []v1alpha1.TrafficTarget{*target} + return route +} + +func createMockRouteTwoTarget(name string, rev1, rev2 string, percent1, percent2 int) *v1alpha1.Route { + route := createMockRouteMeta(name) + target1 := createMockTrafficTarget(rev1, percent1) + target2 := createMockTrafficTarget(rev2, percent2) + route.Status.Traffic = []v1alpha1.TrafficTarget{*target1, *target2} + return route +} diff --git a/pkg/kn/commands/route/route.go b/pkg/kn/commands/route/route.go new file mode 100644 index 0000000000..707d6dcff1 --- /dev/null +++ b/pkg/kn/commands/route/route.go @@ -0,0 +1,29 @@ +// Copyright © 2019 The Knative Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package route + +import ( + "github.com/knative/client/pkg/kn/commands" + "github.com/spf13/cobra" +) + +func NewRouteCommand(p *commands.KnParams) *cobra.Command { + routeCmd := &cobra.Command{ + Use: "route", + Short: "Route command group", + } + routeCmd.AddCommand(NewRouteListCommand(p)) + return routeCmd +} diff --git a/pkg/kn/core/root.go b/pkg/kn/core/root.go index 7522af0255..c9e88145e2 100644 --- a/pkg/kn/core/root.go +++ b/pkg/kn/core/root.go @@ -22,6 +22,7 @@ import ( "github.com/knative/client/pkg/kn/commands" "github.com/knative/client/pkg/kn/commands/revision" + "github.com/knative/client/pkg/kn/commands/route" "github.com/knative/client/pkg/kn/commands/service" homedir "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" @@ -68,6 +69,7 @@ Eventing: Manage event subscriptions and channels. Connect up event sources.`, rootCmd.AddCommand(service.NewServiceCommand(p)) rootCmd.AddCommand(revision.NewRevisionCommand(p)) + rootCmd.AddCommand(route.NewRouteCommand(p)) rootCmd.AddCommand(commands.NewCompletionCommand(p)) rootCmd.AddCommand(commands.NewVersionCommand(p)) diff --git a/test/e2e-smoke-tests.sh b/test/e2e-smoke-tests.sh index d1903f09e1..315766b8b6 100755 --- a/test/e2e-smoke-tests.sh +++ b/test/e2e-smoke-tests.sh @@ -58,6 +58,7 @@ kubectl create ns $KN_E2E_SMOKE_TESTS_NAMESPACE || fail_test ./kn service list -n $KN_E2E_SMOKE_TESTS_NAMESPACE || fail_test ./kn service describe hello -n $KN_E2E_SMOKE_TESTS_NAMESPACE || fail_test ./kn service describe svc1 -n $KN_E2E_SMOKE_TESTS_NAMESPACE || fail_test +./kn route list -n $KN_E2E_SMOKE_TESTS_NAMESPACE || fail_test ./kn service delete hello -n $KN_E2E_SMOKE_TESTS_NAMESPACE || fail_test ./kn service delete foo -n $KN_E2E_SMOKE_TESTS_NAMESPACE || fail_test ./kn service list -n $KN_E2E_SMOKE_TESTS_NAMESPACE | grep -q svc1 || fail_test diff --git a/test/e2e/basic_workflow_test.go b/test/e2e/basic_workflow_test.go index 0f874b7152..6fa92818ba 100644 --- a/test/e2e/basic_workflow_test.go +++ b/test/e2e/basic_workflow_test.go @@ -54,6 +54,8 @@ func TestBasicWorkflow(t *testing.T) { testServiceCreate(t, k, "svc2") testRevisionListForService(t, k, "hello") testRevisionListForService(t, k, "svc2") + testRouteList(t, k) + testRouteListWithArgument(t, k, "hello") testServiceDelete(t, k, "hello") testServiceDelete(t, k, "svc2") testServiceListEmpty(t, k) @@ -143,6 +145,34 @@ func testServiceUpdate(t *testing.T, k kn, serviceName string, args []string) { } } +func testRouteList(t *testing.T, k kn) { + out, err := k.RunWithOpts([]string{"route", "list"}, runOpts{}) + if err != nil { + t.Errorf(fmt.Sprintf("Error executing 'kn route list' command. Error: %s", err.Error())) + } + expectedHeaders := []string{"NAME", "URL", "AGE", "CONDITIONS", "TRAFFIC"} + for _, header := range expectedHeaders { + if !strings.Contains(out, header) { + t.Errorf("Expected to include header %s in 'kn route list' output. Actual output:\n%s\n", header, out) + } + } +} + +func testRouteListWithArgument(t *testing.T, k kn, routeName string) { + out, err := k.RunWithOpts([]string{"route", "list", routeName}, runOpts{}) + if err != nil { + t.Errorf("Error executing 'kn route list %s' command. Error: %s", routeName, err.Error()) + } + expectedOutput := routeName + if !strings.Contains(out, expectedOutput) { + t.Errorf("Expected output incorrect, expecting to include:\n%s\n Instead found:\n%s\n", expectedOutput, out) + } + expectedOutput = fmt.Sprintf("100%% -> %s", routeName) + if !strings.Contains(out, expectedOutput) { + t.Errorf("Expected output incorrect, expecting to include:\n%s\n Instead found:\n%s\n", expectedOutput, out) + } +} + func testServiceDelete(t *testing.T, k kn, serviceName string) { out, err := k.RunWithOpts([]string{"service", "delete", serviceName}, runOpts{NoNamespace: false}) if err != nil { diff --git a/vendor/modules.txt b/vendor/modules.txt index c1806fae02..5274c1b6b9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -203,11 +203,11 @@ k8s.io/apimachinery/pkg/apis/meta/v1beta1 k8s.io/apimachinery/pkg/labels k8s.io/apimachinery/pkg/runtime k8s.io/apimachinery/pkg/runtime/schema +k8s.io/apimachinery/pkg/fields k8s.io/apimachinery/pkg/api/errors k8s.io/apimachinery/pkg/api/resource k8s.io/apimachinery/pkg/api/meta k8s.io/apimachinery/pkg/util/runtime -k8s.io/apimachinery/pkg/fields k8s.io/apimachinery/pkg/watch k8s.io/apimachinery/pkg/api/equality k8s.io/apimachinery/pkg/api/validation