diff --git a/go.mod b/go.mod index 484b1d988..6a0172c9e 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/json-iterator/go v1.1.6 // indirect github.com/knative/build v0.5.0 // indirect - github.com/knative/pkg v0.0.0-20190329155329-916205998db9 // indirect + github.com/knative/pkg v0.0.0-20190329155329-916205998db9 github.com/knative/serving v0.5.2 github.com/knative/test-infra v0.0.0-20190509163238-a721698dbe49 github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a // indirect diff --git a/pkg/kn/commands/human_readable_flags.go b/pkg/kn/commands/human_readable_flags.go new file mode 100644 index 000000000..f7e16d897 --- /dev/null +++ b/pkg/kn/commands/human_readable_flags.go @@ -0,0 +1,54 @@ +// 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 commands + +import ( + hprinters "github.com/knative/client/pkg/printers" + "github.com/spf13/cobra" +) + +// HumanPrintFlags provides default flags necessary for printing. +// Given the following flag values, a printer can be requested that knows +// how to handle printing based on these values. +type HumanPrintFlags struct { + //TODO: Add more flags as required +} + +// AllowedFormats returns more customized formating options +func (f *HumanPrintFlags) AllowedFormats() []string { + // TODO: Add more formats eg: wide + return []string{""} +} + +// ToPrinter receives returns a printer capable of +// handling human-readable output. +func (f *HumanPrintFlags) ToPrinter() (hprinters.ResourcePrinter, error) { + p := hprinters.NewTablePrinter(hprinters.PrintOptions{}) + // Add the column definitions and respective functions + ServiceGetHandlers(p) + return p, nil +} + +// AddFlags receives a *cobra.Command reference and binds +// flags related to human-readable printing to it +func (f *HumanPrintFlags) AddFlags(c *cobra.Command) { + //TODO: Add more flags as required +} + +// NewHumanPrintFlags returns flags associated with +// human-readable printing, with default values set. +func NewHumanPrintFlags() *HumanPrintFlags { + return &HumanPrintFlags{} +} diff --git a/pkg/kn/commands/service.go b/pkg/kn/commands/service.go index 77441e92a..392c25067 100644 --- a/pkg/kn/commands/service.go +++ b/pkg/kn/commands/service.go @@ -23,7 +23,7 @@ func NewServiceCommand(p *KnParams) *cobra.Command { Use: "service", Short: "Service command group", } - serviceCmd.AddCommand(NewServiceListCommand(p)) + serviceCmd.AddCommand(NewServiceGetCommand(p)) serviceCmd.AddCommand(NewServiceDescribeCommand(p)) serviceCmd.AddCommand(NewServiceCreateCommand(p)) serviceCmd.AddCommand(NewServiceDeleteCommand(p)) diff --git a/pkg/kn/commands/service_list.go b/pkg/kn/commands/service_get.go similarity index 66% rename from pkg/kn/commands/service_list.go rename to pkg/kn/commands/service_get.go index a8e0696ae..1f2212964 100644 --- a/pkg/kn/commands/service_list.go +++ b/pkg/kn/commands/service_get.go @@ -1,4 +1,4 @@ -// Copyright © 2018 The Knative Authors +// 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. @@ -15,21 +15,20 @@ package commands import ( + "fmt" + "github.com/spf13/cobra" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/genericclioptions" ) -var serviceListPrintFlags *genericclioptions.PrintFlags +// NewServiceGetCommand represents 'kn service get' command +func NewServiceGetCommand(p *KnParams) *cobra.Command { + serviceGetFlags := NewServiceGetFlags() -// listCmd represents the list command -func NewServiceListCommand(p *KnParams) *cobra.Command { - serviceListPrintFlags := genericclioptions.NewPrintFlags("").WithDefaultOutput( - "jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}") - serviceListCommand := &cobra.Command{ - Use: "list", - Short: "List available services.", + serviceGetCommand := &cobra.Command{ + Use: "get", + Short: "Get available services.", RunE: func(cmd *cobra.Command, args []string) error { client, err := p.ServingFactory() if err != nil { @@ -43,15 +42,20 @@ func NewServiceListCommand(p *KnParams) *cobra.Command { if err != nil { return err } - - printer, err := serviceListPrintFlags.ToPrinter() - if err != nil { - return err + if len(service.Items) == 0 { + fmt.Fprintf(cmd.OutOrStdout(), "No resources found.\n") + return nil } service.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{ Group: "knative.dev", Version: "v1alpha1", Kind: "Service"}) + + printer, err := serviceGetFlags.ToPrinter() + if err != nil { + return err + } + err = printer.PrintObj(service, cmd.OutOrStdout()) if err != nil { return err @@ -59,7 +63,7 @@ func NewServiceListCommand(p *KnParams) *cobra.Command { return nil }, } - AddNamespaceFlags(serviceListCommand.Flags(), true) - serviceListPrintFlags.AddFlags(serviceListCommand) - return serviceListCommand + AddNamespaceFlags(serviceGetCommand.Flags(), true) + serviceGetFlags.AddFlags(serviceGetCommand) + return serviceGetCommand } diff --git a/pkg/kn/commands/service_get_flags.go b/pkg/kn/commands/service_get_flags.go new file mode 100644 index 000000000..2bfe67aed --- /dev/null +++ b/pkg/kn/commands/service_get_flags.go @@ -0,0 +1,181 @@ +// 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 commands + +import ( + "fmt" + hprinters "github.com/knative/client/pkg/printers" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/cli-runtime/pkg/genericclioptions" + "time" +) + +// ServiceGetFlags composes common printer flag structs +// used in the Get command. +type ServiceGetFlags struct { + GenericPrintFlags *genericclioptions.PrintFlags + HumanReadableFlags *HumanPrintFlags +} + +// AllowedFormats is the list of formats in which data can be displayed +func (f *ServiceGetFlags) AllowedFormats() []string { + formats := f.GenericPrintFlags.AllowedFormats() + formats = append(formats, f.HumanReadableFlags.AllowedFormats()...) + return formats +} + +// ToPrinter attempts to find a composed set of ServiceGetFlags suitable for +// returning a printer based on current flag values. +func (f *ServiceGetFlags) 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() + 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 *ServiceGetFlags) AddFlags(cmd *cobra.Command) { + f.GenericPrintFlags.AddFlags(cmd) + f.HumanReadableFlags.AddFlags(cmd) +} + +// NewGetPrintFlags returns flags associated with humanreadable, +// template, and "name" printing, with default values set. +func NewServiceGetFlags() *ServiceGetFlags { + return &ServiceGetFlags{ + GenericPrintFlags: genericclioptions.NewPrintFlags(""), + HumanReadableFlags: NewHumanPrintFlags(), + } +} + +// ServiceGetHandlers adds print handlers for service get command +func ServiceGetHandlers(h hprinters.PrintHandler) { + kServiceColumnDefinitions := []metav1beta1.TableColumnDefinition{ + {Name: "Name", Type: "string", Description: "Name of the knative service."}, + {Name: "Domain", Type: "string", Description: "Domain name of the knative service."}, + //{Name: "LastCreatedRevision", Type: "string", Description: "Name of last revision created."}, + //{Name: "LastReadyRevision", Type: "string", Description: "Name of last ready revision."}, + {Name: "Generation", Type: "integer", Description: "Sequence number of 'Generation' of the service that was last processed by the controller."}, + {Name: "Age", Type: "string", Description: "Age of the service."}, + {Name: "Conditions", Type: "string", Description: "Conditions describing statuses of service components."}, + {Name: "Ready", Type: "string", Description: "Ready condition status of the service."}, + {Name: "Reason", Type: "string", Description: "Reason for non-ready condition of the service."}, + } + h.TableHandler(kServiceColumnDefinitions, printKService) + h.TableHandler(kServiceColumnDefinitions, printKServiceList) +} + +// conditionsValue returns the True conditions count among total conditions +func conditionsValue(conditions duckv1alpha1.Conditions) string { + var ok int + for _, condition := range conditions { + if condition.Status == "True" { + ok++ + } + } + return fmt.Sprintf("%d OK / %d", ok, len(conditions)) +} + +// readyCondition returns status of resource's Ready type condition +func readyCondition(conditions duckv1alpha1.Conditions) string { + for _, condition := range conditions { + if condition.Type == duckv1alpha1.ConditionReady { + return string(condition.Status) + } + } + return "" +} + +func nonReadyConditionReason(conditions duckv1alpha1.Conditions) string { + for _, condition := range conditions { + if condition.Type == duckv1alpha1.ConditionReady { + if string(condition.Status) == "True" { + return "" + } + if condition.Message != "" { + return fmt.Sprintf("%s : %s", condition.Reason, condition.Message) + } + return string(condition.Reason) + } + } + return "" +} + +// translateTimestampSince returns the elapsed time since timestamp in +// human-readable approximation. +func translateTimestampSince(timestamp metav1.Time) string { + if timestamp.IsZero() { + return "" + } + return duration.HumanDuration(time.Since(timestamp.Time)) +} + +// printKServiceList populates the knative service list table rows +func printKServiceList(kServiceList *servingv1alpha1.ServiceList, options hprinters.PrintOptions) ([]metav1beta1.TableRow, error) { + rows := make([]metav1beta1.TableRow, 0, len(kServiceList.Items)) + for _, ksvc := range kServiceList.Items { + r, err := printKService(&ksvc, options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + +// printKService populates the knative service table rows +func printKService(kService *servingv1alpha1.Service, options hprinters.PrintOptions) ([]metav1beta1.TableRow, error) { + name := kService.Name + domain := kService.Status.Domain + //lastCreatedRevision := kService.Status.LatestCreatedRevisionName + //lastReadyRevision := kService.Status.LatestReadyRevisionName + generation := kService.Status.ObservedGeneration + age := translateTimestampSince(kService.CreationTimestamp) + conditions := conditionsValue(kService.Status.Conditions) + ready := readyCondition(kService.Status.Conditions) + reason := nonReadyConditionReason(kService.Status.Conditions) + + row := metav1beta1.TableRow{ + Object: runtime.RawExtension{Object: kService}, + } + row.Cells = append(row.Cells, + name, + domain, + //lastCreatedRevision, + //lastReadyRevision, + generation, + age, + conditions, + ready, + reason) + return []metav1beta1.TableRow{row}, nil +} diff --git a/pkg/kn/commands/service_list_test.go b/pkg/kn/commands/service_get_test.go similarity index 51% rename from pkg/kn/commands/service_list_test.go rename to pkg/kn/commands/service_get_test.go index c34acc9ce..2dd559247 100644 --- a/pkg/kn/commands/service_list_test.go +++ b/pkg/kn/commands/service_get_test.go @@ -19,6 +19,8 @@ import ( "strings" "testing" + //servinglib "github.com/knative/client/pkg/serving" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" "github.com/knative/serving/pkg/apis/serving/v1alpha1" serving "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1" "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1/fake" @@ -27,7 +29,7 @@ import ( client_testing "k8s.io/client-go/testing" ) -func fakeList(args []string, response *v1alpha1.ServiceList) (action client_testing.Action, output []string, err error) { +func fakeGet(args []string, response *v1alpha1.ServiceList) (action client_testing.Action, output []string, err error) { buf := new(bytes.Buffer) fakeServing := &fake.FakeServingV1alpha1{&client_testing.Fake{}} cmd := NewKnCommand(KnParams{ @@ -48,58 +50,67 @@ func fakeList(args []string, response *v1alpha1.ServiceList) (action client_test return } -func TestListEmpty(t *testing.T) { - action, output, err := fakeList([]string{"service", "list"}, &v1alpha1.ServiceList{}) +func TestGetEmpty(t *testing.T) { + action, output, err := fakeGet([]string{"service", "get"}, &v1alpha1.ServiceList{}) if err != nil { t.Error(err) return } - for _, s := range output { - if s != "" { - t.Errorf("Bad output line %v", s) - } - } if action == nil { t.Errorf("No action") } else if !action.Matches("list", "services") { t.Errorf("Bad action %v", action) + } else if output[0] != "No resources found." { + t.Errorf("Bad output %s", output[0]) } } -var serviceType = metav1.TypeMeta{ - Kind: "service", - APIVersion: "serving.knative.dev/v1alpha1", -} - func TestListDefaultOutput(t *testing.T) { - action, output, err := fakeList([]string{"service", "list"}, &v1alpha1.ServiceList{ - Items: []v1alpha1.Service{ - v1alpha1.Service{ - TypeMeta: serviceType, - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - }, - v1alpha1.Service{ - TypeMeta: serviceType, - ObjectMeta: metav1.ObjectMeta{ - Name: "bar", - }, - }, - }, - }) + service1 := createMockServiceWithParams(t, "foo", "foo.default.example.com", 1) + service2 := createMockServiceWithParams(t, "bar", "bar.default.example.com", 2) + serviceList := &v1alpha1.ServiceList{Items: []v1alpha1.Service{*service1, *service2}} + action, output, err := fakeGet([]string{"service", "get"}, serviceList) if err != nil { t.Fatal(err) } - expected := []string{"foo", "bar", ""} - for i, s := range output { - if s != expected[i] { - t.Errorf("Bad output line %v expected %v", s, expected[i]) - } - } if action == nil { t.Errorf("No action") } else if !action.Matches("list", "services") { t.Errorf("Bad action %v", action) } + testContains(t, output[0], []string{"NAME", "DOMAIN", "GENERATION", "AGE", "CONDITIONS", "READY", "REASON"}, "column header") + testContains(t, output[1], []string{"foo", "foo.default.example.com", "1"}, "value") + testContains(t, output[2], []string{"bar", "bar.default.example.com", "2"}, "value") +} + +func testContains(t *testing.T, output string, sub []string, element string) { + for _, each := range sub { + if !strings.Contains(output, each) { + t.Errorf("Missing %s: %s", element, each) + } + } +} + +func createMockServiceWithParams(t *testing.T, name, domain string, generation int64) *v1alpha1.Service { + service := &v1alpha1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "knative.dev/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: v1alpha1.ServiceSpec{ + RunLatest: &v1alpha1.RunLatestType{}, + }, + Status: v1alpha1.ServiceStatus{ + Status: duckv1alpha1.Status{ + ObservedGeneration: generation}, + RouteStatusFields: v1alpha1.RouteStatusFields{ + Domain: domain, + }, + }, + } + return service } diff --git a/vendor/k8s.io/apimachinery/pkg/util/duration/duration.go b/vendor/k8s.io/apimachinery/pkg/util/duration/duration.go new file mode 100644 index 000000000..961ec5ed8 --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/util/duration/duration.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 The Kubernetes 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 duration + +import ( + "fmt" + "time" +) + +// ShortHumanDuration returns a succint representation of the provided duration +// with limited precision for consumption by humans. +func ShortHumanDuration(d time.Duration) string { + // Allow deviation no more than 2 seconds(excluded) to tolerate machine time + // inconsistence, it can be considered as almost now. + if seconds := int(d.Seconds()); seconds < -1 { + return fmt.Sprintf("") + } else if seconds < 0 { + return fmt.Sprintf("0s") + } else if seconds < 60 { + return fmt.Sprintf("%ds", seconds) + } else if minutes := int(d.Minutes()); minutes < 60 { + return fmt.Sprintf("%dm", minutes) + } else if hours := int(d.Hours()); hours < 24 { + return fmt.Sprintf("%dh", hours) + } else if hours < 24*365 { + return fmt.Sprintf("%dd", hours/24) + } + return fmt.Sprintf("%dy", int(d.Hours()/24/365)) +} + +// HumanDuration returns a succint representation of the provided duration +// with limited precision for consumption by humans. It provides ~2-3 significant +// figures of duration. +func HumanDuration(d time.Duration) string { + // Allow deviation no more than 2 seconds(excluded) to tolerate machine time + // inconsistence, it can be considered as almost now. + if seconds := int(d.Seconds()); seconds < -1 { + return fmt.Sprintf("") + } else if seconds < 0 { + return fmt.Sprintf("0s") + } else if seconds < 60*2 { + return fmt.Sprintf("%ds", seconds) + } + minutes := int(d / time.Minute) + if minutes < 10 { + s := int(d/time.Second) % 60 + if s == 0 { + return fmt.Sprintf("%dm", minutes) + } + return fmt.Sprintf("%dm%ds", minutes, s) + } else if minutes < 60*3 { + return fmt.Sprintf("%dm", minutes) + } + hours := int(d / time.Hour) + if hours < 8 { + m := int(d/time.Minute) % 60 + if m == 0 { + return fmt.Sprintf("%dh", hours) + } + return fmt.Sprintf("%dh%dm", hours, m) + } else if hours < 48 { + return fmt.Sprintf("%dh", hours) + } else if hours < 24*8 { + h := hours % 24 + if h == 0 { + return fmt.Sprintf("%dd", hours/24) + } + return fmt.Sprintf("%dd%dh", hours/24, h) + } else if hours < 24*365*2 { + return fmt.Sprintf("%dd", hours/24) + } else if hours < 24*365*8 { + return fmt.Sprintf("%dy%dd", hours/24/365, (hours/24)%365) + } + return fmt.Sprintf("%dy", int(hours/24/365)) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e67f99a12..07b2cdaf1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -63,11 +63,11 @@ github.com/json-iterator/go github.com/knative/build/pkg/apis/build/v1alpha1 github.com/knative/build/pkg/apis/build # github.com/knative/pkg v0.0.0-20190329155329-916205998db9 -github.com/knative/pkg/apis github.com/knative/pkg/apis/duck/v1alpha1 +github.com/knative/pkg/apis +github.com/knative/pkg/apis/duck github.com/knative/pkg/kmeta github.com/knative/pkg/kmp -github.com/knative/pkg/apis/duck github.com/knative/pkg/configmap # github.com/knative/serving v0.5.2 github.com/knative/serving/pkg/apis/serving/v1alpha1 @@ -195,10 +195,11 @@ k8s.io/api/storage/v1beta1 # k8s.io/apimachinery v0.0.0-20190221084156-01f179d85dbc k8s.io/apimachinery/pkg/api/resource k8s.io/apimachinery/pkg/apis/meta/v1 -k8s.io/apimachinery/pkg/runtime/schema -k8s.io/apimachinery/pkg/api/meta k8s.io/apimachinery/pkg/apis/meta/v1beta1 k8s.io/apimachinery/pkg/runtime +k8s.io/apimachinery/pkg/runtime/schema +k8s.io/apimachinery/pkg/util/duration +k8s.io/apimachinery/pkg/api/meta k8s.io/apimachinery/pkg/util/runtime k8s.io/apimachinery/pkg/api/equality k8s.io/apimachinery/pkg/api/validation @@ -213,12 +214,12 @@ k8s.io/apimachinery/pkg/conversion k8s.io/apimachinery/pkg/fields k8s.io/apimachinery/pkg/labels k8s.io/apimachinery/pkg/selection +k8s.io/apimachinery/pkg/conversion/queryparams +k8s.io/apimachinery/pkg/util/errors k8s.io/apimachinery/pkg/util/json +k8s.io/apimachinery/pkg/util/naming k8s.io/apimachinery/pkg/util/net k8s.io/apimachinery/pkg/util/yaml -k8s.io/apimachinery/pkg/util/errors -k8s.io/apimachinery/pkg/conversion/queryparams -k8s.io/apimachinery/pkg/util/naming k8s.io/apimachinery/pkg/apis/meta/v1/validation k8s.io/apimachinery/pkg/util/validation/field k8s.io/apimachinery/pkg/runtime/serializer/json @@ -256,6 +257,7 @@ k8s.io/client-go/util/jsonpath k8s.io/client-go/tools/auth k8s.io/client-go/tools/clientcmd/api/latest k8s.io/client-go/testing +k8s.io/client-go/dynamic k8s.io/client-go/tools/cache k8s.io/client-go/pkg/version k8s.io/client-go/plugin/pkg/client/auth/exec @@ -267,7 +269,6 @@ k8s.io/client-go/util/flowcontrol k8s.io/client-go/kubernetes/scheme k8s.io/client-go/third_party/forked/golang/template k8s.io/client-go/tools/clientcmd/api/v1 -k8s.io/client-go/dynamic k8s.io/client-go/tools/pager k8s.io/client-go/util/buffer k8s.io/client-go/util/retry