From 8314ca1b405a635b84f1cc96295ccd363c9cbfd6 Mon Sep 17 00:00:00 2001 From: Yashvardhan Kukreja Date: Thu, 2 May 2024 00:10:51 +0530 Subject: [PATCH] feat: support for YAML and JSON outputs of gwctl (#2940) Signed-off-by: Yashvardhan Kukreja --- gwctl/cmd/describe.go | 13 +- gwctl/cmd/get.go | 55 +++-- gwctl/pkg/common/clients.go | 10 + gwctl/pkg/common/testhelpers.go | 20 ++ gwctl/pkg/policymanager/manager.go | 6 + gwctl/pkg/printer/backends.go | 12 +- gwctl/pkg/printer/backends_test.go | 4 +- gwctl/pkg/printer/common.go | 31 +++ gwctl/pkg/printer/gatewayclasses.go | 41 ++-- gwctl/pkg/printer/gatewayclasses_test.go | 140 ++++++++++- gwctl/pkg/printer/gateways.go | 33 ++- gwctl/pkg/printer/gateways_test.go | 287 ++++++++++++++++++++++- gwctl/pkg/printer/httproutes.go | 37 ++- gwctl/pkg/printer/httproutes_test.go | 159 ++++++++++++- gwctl/pkg/printer/namespace.go | 46 ++-- gwctl/pkg/printer/namespace_test.go | 119 +++++++++- gwctl/pkg/printer/policies.go | 86 ++++--- gwctl/pkg/printer/policies_test.go | 269 ++++++++++++++++++++- gwctl/pkg/printer/printer.go | 79 +++++++ gwctl/pkg/resourcediscovery/nodes.go | 18 ++ gwctl/pkg/utils/types.go | 36 +++ 21 files changed, 1317 insertions(+), 184 deletions(-) create mode 100644 gwctl/pkg/printer/printer.go diff --git a/gwctl/cmd/describe.go b/gwctl/cmd/describe.go index 82024d79bc..64be87d6a0 100644 --- a/gwctl/cmd/describe.go +++ b/gwctl/cmd/describe.go @@ -82,12 +82,13 @@ func runDescribe(cmd *cobra.Command, args []string, params *utils.CmdParams) { K8sClients: params.K8sClients, PolicyManager: params.PolicyManager, } - policiesPrinter := &printer.PoliciesPrinter{Out: params.Out, Clock: clock.RealClock{}} - httpRoutesPrinter := &printer.HTTPRoutesPrinter{Out: params.Out, Clock: clock.RealClock{}} - gwPrinter := &printer.GatewaysPrinter{Out: params.Out, Clock: clock.RealClock{}} - gwcPrinter := &printer.GatewayClassesPrinter{Out: params.Out, Clock: clock.RealClock{}} - backendsPrinter := &printer.BackendsPrinter{Out: params.Out} - namespacesPrinter := &printer.NamespacesPrinter{Out: params.Out, Clock: clock.RealClock{}} + + policiesPrinter := &printer.PoliciesPrinter{Writer: params.Out, Clock: clock.RealClock{}} + httpRoutesPrinter := &printer.HTTPRoutesPrinter{Writer: params.Out, Clock: clock.RealClock{}} + gwPrinter := &printer.GatewaysPrinter{Writer: params.Out, Clock: clock.RealClock{}} + gwcPrinter := &printer.GatewayClassesPrinter{Writer: params.Out, Clock: clock.RealClock{}} + backendsPrinter := &printer.BackendsPrinter{Writer: params.Out} + namespacesPrinter := &printer.NamespacesPrinter{Writer: params.Out, Clock: clock.RealClock{}} switch kind { case "policy", "policies": diff --git a/gwctl/cmd/get.go b/gwctl/cmd/get.go index eb3e1cb504..01a1f5cba0 100644 --- a/gwctl/cmd/get.go +++ b/gwctl/cmd/get.go @@ -34,6 +34,7 @@ func NewGetCommand() *cobra.Command { var namespaceFlag string var allNamespacesFlag bool var labelSelector string + var outputFormat string cmd := &cobra.Command{ Use: "get {namespaces|gateways|gatewayclasses|policies|policycrds|httproutes}", @@ -47,6 +48,7 @@ func NewGetCommand() *cobra.Command { cmd.Flags().StringVarP(&namespaceFlag, "namespace", "n", "default", "") cmd.Flags().BoolVarP(&allNamespacesFlag, "all-namespaces", "A", false, "If present, list requested resources from all namespaces.") cmd.Flags().StringVarP(&labelSelector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") + cmd.Flags().StringVarP(&outputFormat, "output", "o", "", `Output format. Must be one of (yaml, json)`) return cmd } @@ -70,6 +72,16 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { fmt.Fprintf(os.Stderr, "failed to read flag \"selector\": %v\n", err) os.Exit(1) } + output, err := cmd.Flags().GetString("output") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read flag \"output\": %v\n", err) + os.Exit(1) + } + outputFormat, err := utils.ValidateAndReturnOutputFormat(output) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } if allNs { ns = "" @@ -80,12 +92,16 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { PolicyManager: params.PolicyManager, } realClock := clock.RealClock{} - nsPrinter := &printer.NamespacesPrinter{Out: params.Out, Clock: realClock} - gwPrinter := &printer.GatewaysPrinter{Out: params.Out, Clock: realClock} - gwcPrinter := &printer.GatewayClassesPrinter{Out: params.Out, Clock: realClock} - policiesPrinter := &printer.PoliciesPrinter{Out: params.Out, Clock: realClock} - httpRoutesPrinter := &printer.HTTPRoutesPrinter{Out: params.Out, Clock: realClock} - backendsPrinter := &printer.BackendsPrinter{Out: params.Out, Clock: realClock} + + nsPrinter := &printer.NamespacesPrinter{Writer: params.Out, Clock: realClock} + gwPrinter := &printer.GatewaysPrinter{Writer: params.Out, Clock: realClock} + gwcPrinter := &printer.GatewayClassesPrinter{Writer: params.Out, Clock: realClock} + policiesPrinter := &printer.PoliciesPrinter{Writer: params.Out, Clock: realClock} + httpRoutesPrinter := &printer.HTTPRoutesPrinter{Writer: params.Out, Clock: realClock} + backendsPrinter := &printer.BackendsPrinter{Writer: params.Out, Clock: realClock} + + var resourceModel *resourcediscovery.ResourceModel + var printerImpl printer.Printer switch kind { case "namespace", "namespaces", "ns": @@ -94,12 +110,12 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) os.Exit(1) } - resourceModel, err := discoverer.DiscoverResourcesForNamespace(resourcediscovery.Filter{Labels: selector}) + resourceModel, err = discoverer.DiscoverResourcesForNamespace(resourcediscovery.Filter{Labels: selector}) if err != nil { fmt.Fprintf(os.Stderr, "failed to discover Namespace resources: %v\n", err) os.Exit(1) } - nsPrinter.Print(resourceModel) + printerImpl = nsPrinter case "gateway", "gateways": selector, err := labels.Parse(labelSelector) @@ -111,12 +127,12 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { if len(args) > 1 { filter.Name = args[1] } - resourceModel, err := discoverer.DiscoverResourcesForGateway(filter) + resourceModel, err = discoverer.DiscoverResourcesForGateway(filter) if err != nil { fmt.Fprintf(os.Stderr, "failed to discover Gateway resources: %v\n", err) os.Exit(1) } - gwPrinter.Print(resourceModel) + printerImpl = gwPrinter case "gatewayclass", "gatewayclasses": selector, err := labels.Parse(labelSelector) @@ -128,20 +144,22 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { if len(args) > 1 { filter.Name = args[1] } - resourceModel, err := discoverer.DiscoverResourcesForGatewayClass(filter) + resourceModel, err = discoverer.DiscoverResourcesForGatewayClass(filter) if err != nil { fmt.Fprintf(os.Stderr, "failed to discover GatewayClass resources: %v\n", err) os.Exit(1) } - gwcPrinter.Print(resourceModel) + printerImpl = gwcPrinter case "policy", "policies": list := params.PolicyManager.GetPolicies() - policiesPrinter.PrintPoliciesGetView(list) + policiesPrinter.PrintPolicies(list, outputFormat) + return case "policycrd", "policycrds": list := params.PolicyManager.GetCRDs() - policiesPrinter.PrintPolicyCRDsGetView(list) + policiesPrinter.PrintCRDs(list, outputFormat) + return case "httproute", "httproutes": selector, err := labels.Parse(labelSelector) @@ -153,12 +171,12 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { if len(args) > 1 { filter.Name = args[1] } - resourceModel, err := discoverer.DiscoverResourcesForHTTPRoute(filter) + resourceModel, err = discoverer.DiscoverResourcesForHTTPRoute(filter) if err != nil { fmt.Fprintf(os.Stderr, "failed to discover HTTPRoute resources: %v\n", err) os.Exit(1) } - httpRoutesPrinter.Print(resourceModel) + printerImpl = httpRoutesPrinter case "backend", "backends": selector, err := labels.Parse(labelSelector) @@ -170,14 +188,17 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { if len(args) > 1 { filter.Name = args[1] } - resourceModel, err := discoverer.DiscoverResourcesForBackend(filter) + resourceModel, err = discoverer.DiscoverResourcesForBackend(filter) if err != nil { fmt.Fprintf(os.Stderr, "failed to discover backend resources: %v\n", err) os.Exit(1) } backendsPrinter.Print(resourceModel) + return default: fmt.Fprintf(os.Stderr, "Unrecognized RESOURCE_TYPE\n") + os.Exit(1) } + printer.Print(printerImpl, resourceModel, outputFormat) } diff --git a/gwctl/pkg/common/clients.go b/gwctl/pkg/common/clients.go index 943ec617d6..f4451b4d46 100644 --- a/gwctl/pkg/common/clients.go +++ b/gwctl/pkg/common/clients.go @@ -131,3 +131,13 @@ func MustClientsForTest(t *testing.T, initRuntimeObjects ...runtime.Object) *K8s func PtrTo[T any](a T) *T { return &a } + +func MapToValues[K comparable, V any](obj map[K]V) []V { + values := make([]V, len(obj)) + i := 0 + for _, v := range obj { + values[i] = v + i++ + } + return values +} diff --git a/gwctl/pkg/common/testhelpers.go b/gwctl/pkg/common/testhelpers.go index b971d7928e..46bf354bff 100644 --- a/gwctl/pkg/common/testhelpers.go +++ b/gwctl/pkg/common/testhelpers.go @@ -17,6 +17,8 @@ limitations under the License. package common import ( + "encoding/json" + "fmt" "strings" "github.com/google/go-cmp/cmp" @@ -51,3 +53,21 @@ var YamlStringTransformer = cmp.Transformer("YamlLines", func(s YamlString) []st } return lines[start : end+1] }) + +type JSONString string + +func (src JSONString) CmpDiff(tgt JSONString) (diff string, err error) { + var srcMap, targetMap map[string]interface{} + err = json.Unmarshal([]byte(src), &srcMap) + if err != nil { + err = fmt.Errorf("failed to unmarshal the source json: %w", err) + return + } + err = json.Unmarshal([]byte(tgt), &targetMap) + if err != nil { + err = fmt.Errorf("failed to unmarshal the target json: %w", err) + return + } + + return cmp.Diff(srcMap, targetMap), nil +} diff --git a/gwctl/pkg/policymanager/manager.go b/gwctl/pkg/policymanager/manager.go index ee0602e0c5..62205fb6de 100644 --- a/gwctl/pkg/policymanager/manager.go +++ b/gwctl/pkg/policymanager/manager.go @@ -22,6 +22,8 @@ import ( "fmt" "strings" + "sigs.k8s.io/controller-runtime/pkg/client" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -180,6 +182,8 @@ type PolicyCRD struct { crd apiextensionsv1.CustomResourceDefinition } +func (p PolicyCRD) ClientObject() client.Object { return p.CRD() } + // ID returns a unique identifier for this PolicyCRD. func (p PolicyCRD) ID() PolicyCrdID { return PolicyCrdID(p.crd.Spec.Names.Kind + "." + p.crd.Spec.Group) @@ -220,6 +224,8 @@ type Policy struct { inherited bool } +func (p Policy) ClientObject() client.Object { return p.Unstructured() } + type ObjRef struct { Group string `json:",omitempty"` Kind string `json:",omitempty"` diff --git a/gwctl/pkg/printer/backends.go b/gwctl/pkg/printer/backends.go index ed1912287c..0f4f6d6ab4 100644 --- a/gwctl/pkg/printer/backends.go +++ b/gwctl/pkg/printer/backends.go @@ -29,19 +29,19 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" - "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" "sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery" + + "sigs.k8s.io/yaml" ) type BackendsPrinter struct { - Out io.Writer + io.Writer Clock clock.Clock } func (bp *BackendsPrinter) Print(resourceModel *resourcediscovery.ResourceModel) { - tw := tabwriter.NewWriter(bp.Out, 0, 0, 2, ' ', 0) + tw := tabwriter.NewWriter(bp, 0, 0, 2, ' ', 0) row := []string{"NAMESPACE", "NAME", "TYPE", "REFERRED BY ROUTES", "AGE", "POLICIES"} _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")) if err != nil { @@ -163,11 +163,11 @@ func (bp *BackendsPrinter) PrintDescribeView(resourceModel *resourcediscovery.Re fmt.Fprintf(os.Stderr, "failed to marshal to yaml: %v\n", err) os.Exit(1) } - fmt.Fprint(bp.Out, string(b)) + fmt.Fprint(bp, string(b)) } if index+1 <= len(resourceModel.Backends) { - fmt.Fprintf(bp.Out, "\n\n") + fmt.Fprintf(bp, "\n\n") } } } diff --git a/gwctl/pkg/printer/backends_test.go b/gwctl/pkg/printer/backends_test.go index 4fea9ff879..b0db3f2ec6 100644 --- a/gwctl/pkg/printer/backends_test.go +++ b/gwctl/pkg/printer/backends_test.go @@ -465,8 +465,8 @@ func TestBackendsPrinter_Print(t *testing.T) { } bp := &BackendsPrinter{ - Out: params.Out, - Clock: fakeClock, + Writer: params.Out, + Clock: fakeClock, } bp.Print(resourceModel) diff --git a/gwctl/pkg/printer/common.go b/gwctl/pkg/printer/common.go index 66f29e6e35..6fec2f199e 100644 --- a/gwctl/pkg/printer/common.go +++ b/gwctl/pkg/printer/common.go @@ -20,12 +20,14 @@ import ( "fmt" "io" "os" + "sort" "strings" "text/tabwriter" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/utils/clock" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" ) @@ -148,3 +150,32 @@ func convertEventsSliceToTable(events []corev1.Event, clock clock.Clock) *Table } return table } + +type NodeResource interface { + ClientObject() client.Object +} + +func ClientObjects[K NodeResource](nodes []K) []client.Object { + clientObjects := make([]client.Object, len(nodes)) + for i, node := range nodes { + clientObjects[i] = node.ClientObject() + } + return clientObjects +} + +func SortByString[K NodeResource](items []K) []K { + sort.Slice(items, func(i, j int) bool { + a := client.ObjectKeyFromObject(items[i].ClientObject()).String() + b := client.ObjectKeyFromObject(items[j].ClientObject()).String() + return a < b + }) + return items +} + +func NodeResources[K NodeResource](items []K) []NodeResource { + output := make([]NodeResource, len(items)) + for i, item := range items { + output[i] = item + } + return output +} diff --git a/gwctl/pkg/printer/gatewayclasses.go b/gwctl/pkg/printer/gatewayclasses.go index 8186ea283e..3345be6525 100644 --- a/gwctl/pkg/printer/gatewayclasses.go +++ b/gwctl/pkg/printer/gatewayclasses.go @@ -20,23 +20,26 @@ import ( "fmt" "io" "os" - "sort" "strings" "text/tabwriter" - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" - "sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/utils/clock" "k8s.io/utils/ptr" + + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/gwctl/pkg/common" + "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" + "sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery" + "sigs.k8s.io/yaml" ) +var _ Printer = (*GatewayClassesPrinter)(nil) + type GatewayClassesPrinter struct { - Out io.Writer + io.Writer Clock clock.Clock } @@ -57,8 +60,12 @@ type gatewayClassDescribeView struct { DirectlyAttachedPolicies []policymanager.ObjRef `json:",omitempty"` } -func (gcp *GatewayClassesPrinter) Print(model *resourcediscovery.ResourceModel) { - tw := tabwriter.NewWriter(gcp.Out, 0, 0, 2, ' ', 0) +func (gcp *GatewayClassesPrinter) GetPrintableNodes(resourceModel *resourcediscovery.ResourceModel) []NodeResource { + return NodeResources(common.MapToValues(resourceModel.GatewayClasses)) +} + +func (gcp *GatewayClassesPrinter) PrintTable(resourceModel *resourcediscovery.ResourceModel) { + tw := tabwriter.NewWriter(gcp, 0, 0, 2, ' ', 0) row := []string{"NAME", "CONTROLLER", "ACCEPTED", "AGE"} _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")) if err != nil { @@ -66,19 +73,9 @@ func (gcp *GatewayClassesPrinter) Print(model *resourcediscovery.ResourceModel) os.Exit(1) } - gatewayClassNodes := make([]*resourcediscovery.GatewayClassNode, 0, len(model.GatewayClasses)) - for _, gatewayClassNode := range model.GatewayClasses { - gatewayClassNodes = append(gatewayClassNodes, gatewayClassNode) - } - - sort.Slice(gatewayClassNodes, func(i, j int) bool { - if gatewayClassNodes[i].GatewayClass.GetName() != gatewayClassNodes[j].GatewayClass.GetName() { - return gatewayClassNodes[i].GatewayClass.GetName() < gatewayClassNodes[j].GatewayClass.GetName() - } - return string(gatewayClassNodes[i].GatewayClass.Spec.ControllerName) < string(gatewayClassNodes[j].GatewayClass.Spec.ControllerName) - }) + gatewayClassNodes := common.MapToValues(resourceModel.GatewayClasses) - for _, gatewayClassNode := range gatewayClassNodes { + for _, gatewayClassNode := range SortByString(gatewayClassNodes) { accepted := "Unknown" for _, condition := range gatewayClassNode.GatewayClass.Status.Conditions { if condition.Type == "Accepted" { @@ -163,12 +160,12 @@ func (gcp *GatewayClassesPrinter) PrintDescribeView(resourceModel *resourcedisco emptyOutput := strings.TrimSpace(output) == "{}" if !emptyOutput { - fmt.Fprint(gcp.Out, output) + fmt.Fprint(gcp, output) } } if index+1 <= len(resourceModel.GatewayClasses) { - fmt.Fprintf(gcp.Out, "\n\n") + fmt.Fprintf(gcp, "\n\n") } } } diff --git a/gwctl/pkg/printer/gatewayclasses_test.go b/gwctl/pkg/printer/gatewayclasses_test.go index aeb5853f8e..46defa351f 100644 --- a/gwctl/pkg/printer/gatewayclasses_test.go +++ b/gwctl/pkg/printer/gatewayclasses_test.go @@ -18,6 +18,7 @@ package printer import ( "bytes" + "fmt" "testing" "time" @@ -29,6 +30,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" testingclock "k8s.io/utils/clock/testing" + apisv1beta1 "sigs.k8s.io/gateway-api/apis/applyconfiguration/apis/v1beta1" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/gwctl/pkg/common" @@ -36,7 +38,7 @@ import ( "sigs.k8s.io/gateway-api/gwctl/pkg/utils" ) -func TestGatewayClassesPrinter_Print(t *testing.T) { +func TestGatewayClassesPrinter_PrintTable(t *testing.T) { fakeClock := testingclock.NewFakeClock(time.Now()) objects := []runtime.Object{ &gatewayv1.GatewayClass{ @@ -109,10 +111,10 @@ func TestGatewayClassesPrinter_Print(t *testing.T) { } gcp := &GatewayClassesPrinter{ - Out: params.Out, - Clock: fakeClock, + Writer: params.Out, + Clock: fakeClock, } - gcp.Print(resourceModel) + Print(gcp, resourceModel, utils.OutputFormatTable) got := params.Out.(*bytes.Buffer).String() want := ` @@ -238,8 +240,8 @@ Status: {} } gcp := &GatewayClassesPrinter{ - Out: params.Out, - Clock: fakeClock, + Writer: params.Out, + Clock: fakeClock, } gcp.PrintDescribeView(resourceModel) @@ -250,3 +252,129 @@ Status: {} }) } } + +// TestGatewayClassesPrinter_PrintJsonYaml tests the -o json/yaml output of the `get` subcommand +func TestGatewayClassesPrinter_PrintJsonYaml(t *testing.T) { + fakeClock := testingclock.NewFakeClock(time.Now()) + creationTime := fakeClock.Now().Add(-365 * 24 * time.Hour).UTC() // UTC being necessary for consistently handling the time while marshaling/unmarshaling its JSON + + gtwName := "foo-com-internal-gateway-class" + gtwApplyConfig := apisv1beta1.GatewayClass(gtwName) + + gtwObject := &gatewayv1.GatewayClass{ + TypeMeta: metav1.TypeMeta{ + APIVersion: *gtwApplyConfig.APIVersion, + Kind: *gtwApplyConfig.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-com-internal-gateway-class", + Labels: map[string]string{"app": "foo", "env": "internal"}, + CreationTimestamp: metav1.Time{ + Time: creationTime, + }, + }, + Spec: gatewayv1.GatewayClassSpec{ + ControllerName: gatewayv1.GatewayController(gtwName + "/controller"), + }, + Status: gatewayv1.GatewayClassStatus{ + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: metav1.ConditionTrue, + }, + }, + }, + } + gtwObject.APIVersion = *gtwApplyConfig.APIVersion + gtwObject.Kind = *gtwApplyConfig.Kind + + params := utils.MustParamsForTest(t, common.MustClientsForTest(t, gtwObject)) + discoverer := resourcediscovery.Discoverer{ + K8sClients: params.K8sClients, + PolicyManager: params.PolicyManager, + } + resourceModel, err := discoverer.DiscoverResourcesForGatewayClass(resourcediscovery.Filter{}) + if err != nil { + t.Fatalf("Failed to construct resourceModel: %v", resourceModel) + } + + gcp := &GatewayClassesPrinter{ + Writer: params.Out, + Clock: fakeClock, + } + Print(gcp, resourceModel, utils.OutputFormatJSON) + + gotJSON := common.JSONString(params.Out.(*bytes.Buffer).String()) + wantJSON := common.JSONString(fmt.Sprintf(` + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "gateway.networking.k8s.io/v1beta1", + "kind": "GatewayClass", + "metadata": { + "creationTimestamp": "%s", + "labels": { + "app": "foo", + "env": "internal" + }, + "name": "foo-com-internal-gateway-class", + "resourceVersion": "999" + }, + "spec": { + "controllerName": "foo-com-internal-gateway-class/controller" + }, + "status": { + "conditions": [ + { + "lastTransitionTime": null, + "message": "", + "reason": "", + "status": "True", + "type": "Accepted" + } + ] + } + } + ], + "kind": "List" + }`, creationTime.Format(time.RFC3339))) + diff, err := wantJSON.CmpDiff(gotJSON) + if err != nil { + t.Fatalf("Failed to compare the json diffs: %v", diff) + } + if diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", gotJSON, wantJSON, diff) + } + + gcp.Writer = &bytes.Buffer{} + + Print(gcp, resourceModel, utils.OutputFormatYAML) + + gotYaml := common.YamlString(gcp.Writer.(*bytes.Buffer).String()) + wantYaml := common.YamlString(fmt.Sprintf(` +apiVersion: v1 +items: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: GatewayClass + metadata: + creationTimestamp: "%s" + labels: + app: foo + env: internal + name: foo-com-internal-gateway-class + resourceVersion: "999" + spec: + controllerName: foo-com-internal-gateway-class/controller + status: + conditions: + - lastTransitionTime: null + message: "" + reason: "" + status: "True" + type: Accepted +kind: List`, creationTime.Format(time.RFC3339))) + if diff := cmp.Diff(wantYaml, gotYaml, common.YamlStringTransformer); diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", gotYaml, wantYaml, diff) + } +} diff --git a/gwctl/pkg/printer/gateways.go b/gwctl/pkg/printer/gateways.go index 24db380351..7c28c6026c 100644 --- a/gwctl/pkg/printer/gateways.go +++ b/gwctl/pkg/printer/gateways.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "os" - "sort" "strings" "text/tabwriter" @@ -28,15 +27,23 @@ import ( "k8s.io/apimachinery/pkg/util/duration" "k8s.io/utils/clock" + + "sigs.k8s.io/gateway-api/gwctl/pkg/common" ) +var _ Printer = (*GatewaysPrinter)(nil) + type GatewaysPrinter struct { - Out io.Writer + io.Writer Clock clock.Clock } -func (gp *GatewaysPrinter) Print(resourceModel *resourcediscovery.ResourceModel) { - tw := tabwriter.NewWriter(gp.Out, 0, 0, 2, ' ', 0) +func (gp *GatewaysPrinter) GetPrintableNodes(resourceModel *resourcediscovery.ResourceModel) []NodeResource { + return NodeResources(common.MapToValues(resourceModel.Gateways)) +} + +func (gp *GatewaysPrinter) PrintTable(resourceModel *resourcediscovery.ResourceModel) { + tw := tabwriter.NewWriter(gp, 0, 0, 2, ' ', 0) row := []string{"NAME", "CLASS", "ADDRESSES", "PORTS", "PROGRAMMED", "AGE"} _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")) if err != nil { @@ -44,19 +51,9 @@ func (gp *GatewaysPrinter) Print(resourceModel *resourcediscovery.ResourceModel) os.Exit(1) } - gatewayNodes := make([]*resourcediscovery.GatewayNode, 0, len(resourceModel.Gateways)) - for _, gatewayNode := range resourceModel.Gateways { - gatewayNodes = append(gatewayNodes, gatewayNode) - } - - sort.Slice(gatewayNodes, func(i, j int) bool { - if gatewayNodes[i].Gateway.GetName() != gatewayNodes[j].Gateway.GetName() { - return gatewayNodes[i].Gateway.GetName() < gatewayNodes[j].Gateway.GetName() - } - return gatewayNodes[i].Gateway.Spec.GatewayClassName < gatewayNodes[j].Gateway.Spec.GatewayClassName - }) + gatewayNodes := common.MapToValues(resourceModel.Gateways) - for _, gatewayNode := range gatewayNodes { + for _, gatewayNode := range SortByString(gatewayNodes) { var addresses []string for _, address := range gatewayNode.Gateway.Status.Addresses { addresses = append(addresses, address.Value) @@ -160,10 +157,10 @@ func (gp *GatewaysPrinter) PrintDescribeView(resourceModel *resourcediscovery.Re // Events pairs = append(pairs, &DescriberKV{Key: "Events", Value: convertEventsSliceToTable(gatewayNode.Events, gp.Clock)}) - Describe(gp.Out, pairs) + Describe(gp, pairs) if index+1 <= len(resourceModel.Gateways) { - fmt.Fprintf(gp.Out, "\n\n") + fmt.Fprintf(gp, "\n\n") } } } diff --git a/gwctl/pkg/printer/gateways_test.go b/gwctl/pkg/printer/gateways_test.go index 975434629b..05d36f7180 100644 --- a/gwctl/pkg/printer/gateways_test.go +++ b/gwctl/pkg/printer/gateways_test.go @@ -18,6 +18,7 @@ package printer import ( "bytes" + "fmt" "testing" "time" @@ -30,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" testingclock "k8s.io/utils/clock/testing" + apisv1beta1 "sigs.k8s.io/gateway-api/apis/applyconfiguration/apis/v1beta1" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/gwctl/pkg/common" @@ -37,7 +39,7 @@ import ( "sigs.k8s.io/gateway-api/gwctl/pkg/utils" ) -func TestGatewaysPrinter_Print(t *testing.T) { +func TestGatewaysPrinter_PrintTable(t *testing.T) { fakeClock := testingclock.NewFakeClock(time.Now()) objects := []runtime.Object{ &gatewayv1.GatewayClass{ @@ -184,10 +186,10 @@ func TestGatewaysPrinter_Print(t *testing.T) { } gp := &GatewaysPrinter{ - Out: params.Out, - Clock: fakeClock, + Writer: params.Out, + Clock: fakeClock, } - gp.Print(resourceModel) + gp.PrintTable(resourceModel) got := params.Out.(*bytes.Buffer).String() want := ` @@ -374,8 +376,8 @@ func TestGatewaysPrinter_PrintDescribeView(t *testing.T) { } gp := &GatewaysPrinter{ - Out: params.Out, - Clock: fakeClock, + Writer: params.Out, + Clock: fakeClock, } gp.PrintDescribeView(resourceModel) @@ -422,3 +424,276 @@ Events: t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", got, want, diff) } } + +// TestGatewaysPrinter_PrintJsonYaml tests the -o json/yaml output of the `get` subcommand +func TestGatewaysPrinter_PrintJsonYaml(t *testing.T) { + fakeClock := testingclock.NewFakeClock(time.Now()) + creationTime := fakeClock.Now().Add(-5 * 24 * time.Hour).UTC() // UTC being necessary for consistently handling the time while marshaling/unmarshaling its JSON + gcName := "gateway-1" + gcApplyConfig := apisv1beta1.Gateway(gcName, "") + + gcObject := &gatewayv1.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: *gcApplyConfig.APIVersion, + Kind: *gcApplyConfig.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: gcName, + Labels: map[string]string{"app": "foo", "env": "internal"}, + CreationTimestamp: metav1.Time{ + Time: creationTime, + }, + }, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: "gatewayclass-1", + Listeners: []gatewayv1.Listener{ + { + Name: "http-8080", + Protocol: gatewayv1.HTTPProtocolType, + Port: gatewayv1.PortNumber(8080), + }, + }, + }, + Status: gatewayv1.GatewayStatus{ + Addresses: []gatewayv1.GatewayStatusAddress{ + { + Value: "192.168.100.5", + }, + }, + Conditions: []metav1.Condition{ + { + Type: "Programmed", + Status: "False", + }, + }, + }, + } + + objects := []runtime.Object{ + &gatewayv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gatewayclass-1", + }, + Spec: gatewayv1.GatewayClassSpec{ + ControllerName: "example.net/gateway-controller", + Description: common.PtrTo("random"), + }, + }, + gcObject, + } + + params := utils.MustParamsForTest(t, common.MustClientsForTest(t, objects...)) + discoverer := resourcediscovery.Discoverer{ + K8sClients: params.K8sClients, + PolicyManager: params.PolicyManager, + } + resourceModel, err := discoverer.DiscoverResourcesForGateway(resourcediscovery.Filter{}) + if err != nil { + t.Fatalf("Failed to construct resourceModel: %v", resourceModel) + } + + gp := &GatewaysPrinter{ + Writer: params.Out, + Clock: fakeClock, + } + Print(gp, resourceModel, utils.OutputFormatJSON) + + gotJSON := common.JSONString(params.Out.(*bytes.Buffer).String()) + wantJSON := common.JSONString(fmt.Sprintf(` + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "gateway.networking.k8s.io/v1beta1", + "kind": "Gateway", + "metadata": { + "creationTimestamp": "%s", + "labels": { + "app": "foo", + "env": "internal" + }, + "name": "gateway-1", + "resourceVersion": "999" + }, + "spec": { + "gatewayClassName": "gatewayclass-1", + "listeners": [ + { + "name": "http-8080", + "port": 8080, + "protocol": "HTTP" + } + ] + }, + "status": { + "addresses": [ + { + "value": "192.168.100.5" + } + ], + "conditions": [ + { + "lastTransitionTime": null, + "message": "", + "reason": "", + "status": "False", + "type": "Programmed" + } + ] + } + } + ], + "kind": "List" + }`, creationTime.Format(time.RFC3339))) + diff, err := wantJSON.CmpDiff(gotJSON) + if err != nil { + t.Fatalf("Failed to compare the json diffs: %v", diff) + } + if diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", gotJSON, wantJSON, diff) + } + + gp.Writer = &bytes.Buffer{} + + Print(gp, resourceModel, utils.OutputFormatYAML) + + gotYaml := common.YamlString(gp.Writer.(*bytes.Buffer).String()) + wantYaml := common.YamlString(fmt.Sprintf(` +apiVersion: v1 +items: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: "%s" + labels: + app: foo + env: internal + name: gateway-1 + resourceVersion: "999" + spec: + gatewayClassName: gatewayclass-1 + listeners: + - name: http-8080 + port: 8080 + protocol: HTTP + status: + addresses: + - value: 192.168.100.5 + conditions: + - lastTransitionTime: null + message: "" + reason: "" + status: "False" + type: Programmed +kind: List`, creationTime.Format(time.RFC3339))) + if diff := cmp.Diff(wantYaml, gotYaml, common.YamlStringTransformer); diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", gotYaml, wantYaml, diff) + } +} + +// TestGatewaysPrinter_PrintYaml tests the -o yaml output of the `get` subcommand +func TestGatewaysPrinter_PrintYaml(t *testing.T) { + fakeClock := testingclock.NewFakeClock(time.Now()) + creationTime := fakeClock.Now().Add(-5 * 24 * time.Hour).UTC() // UTC being necessary for consistently handling the time while marshaling/unmarshaling its JSON + gcName := "gateway-1" + gcApplyConfig := apisv1beta1.Gateway(gcName, "") + + gcObject := &gatewayv1.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: *gcApplyConfig.APIVersion, + Kind: *gcApplyConfig.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: gcName, + Labels: map[string]string{"app": "foo", "env": "internal"}, + CreationTimestamp: metav1.Time{ + Time: creationTime, + }, + }, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: "gatewayclass-1", + Listeners: []gatewayv1.Listener{ + { + Name: "http-8080", + Protocol: gatewayv1.HTTPProtocolType, + Port: gatewayv1.PortNumber(8080), + }, + }, + }, + Status: gatewayv1.GatewayStatus{ + Addresses: []gatewayv1.GatewayStatusAddress{ + { + Value: "192.168.100.5", + }, + }, + Conditions: []metav1.Condition{ + { + Type: "Programmed", + Status: "False", + }, + }, + }, + } + + objects := []runtime.Object{ + &gatewayv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gatewayclass-1", + }, + Spec: gatewayv1.GatewayClassSpec{ + ControllerName: "example.net/gateway-controller", + Description: common.PtrTo("random"), + }, + }, + gcObject, + } + + params := utils.MustParamsForTest(t, common.MustClientsForTest(t, objects...)) + discoverer := resourcediscovery.Discoverer{ + K8sClients: params.K8sClients, + PolicyManager: params.PolicyManager, + } + resourceModel, err := discoverer.DiscoverResourcesForGateway(resourcediscovery.Filter{}) + if err != nil { + t.Fatalf("Failed to construct resourceModel: %v", resourceModel) + } + + gp := &GatewaysPrinter{ + Writer: params.Out, + Clock: fakeClock, + } + Print(gp, resourceModel, utils.OutputFormatYAML) + + got := common.YamlString(params.Out.(*bytes.Buffer).String()) + want := common.YamlString(fmt.Sprintf(` +apiVersion: v1 +items: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: "%s" + labels: + app: foo + env: internal + name: gateway-1 + resourceVersion: "999" + spec: + gatewayClassName: gatewayclass-1 + listeners: + - name: http-8080 + port: 8080 + protocol: HTTP + status: + addresses: + - value: 192.168.100.5 + conditions: + - lastTransitionTime: null + message: "" + reason: "" + status: "False" + type: Programmed +kind: List`, creationTime.Format(time.RFC3339))) + if diff := cmp.Diff(want, got, common.YamlStringTransformer); diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", got, want, diff) + } +} diff --git a/gwctl/pkg/printer/httproutes.go b/gwctl/pkg/printer/httproutes.go index 92f9668c57..f108151e14 100644 --- a/gwctl/pkg/printer/httproutes.go +++ b/gwctl/pkg/printer/httproutes.go @@ -20,27 +20,32 @@ import ( "fmt" "io" "os" - "sort" "strings" "text/tabwriter" + "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/utils/clock" "sigs.k8s.io/yaml" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/gwctl/pkg/common" "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" "sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery" - - "k8s.io/apimachinery/pkg/util/duration" - "k8s.io/utils/clock" ) +var _ Printer = (*HTTPRoutesPrinter)(nil) + type HTTPRoutesPrinter struct { - Out io.Writer + io.Writer Clock clock.Clock } -func (hp *HTTPRoutesPrinter) Print(resourceModel *resourcediscovery.ResourceModel) { - tw := tabwriter.NewWriter(hp.Out, 0, 0, 2, ' ', 0) +func (hp *HTTPRoutesPrinter) GetPrintableNodes(resourceModel *resourcediscovery.ResourceModel) []NodeResource { + return NodeResources(common.MapToValues(resourceModel.HTTPRoutes)) +} + +func (hp *HTTPRoutesPrinter) PrintTable(resourceModel *resourcediscovery.ResourceModel) { + tw := tabwriter.NewWriter(hp, 0, 0, 2, ' ', 0) row := []string{"NAMESPACE", "NAME", "HOSTNAMES", "PARENT REFS", "AGE"} _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")) if err != nil { @@ -48,19 +53,9 @@ func (hp *HTTPRoutesPrinter) Print(resourceModel *resourcediscovery.ResourceMode os.Exit(1) } - httpRouteNodes := make([]*resourcediscovery.HTTPRouteNode, 0, len(resourceModel.HTTPRoutes)) - for _, httpRouteNode := range resourceModel.HTTPRoutes { - httpRouteNodes = append(httpRouteNodes, httpRouteNode) - } - - sort.Slice(httpRouteNodes, func(i, j int) bool { - if httpRouteNodes[i].HTTPRoute.GetNamespace() != httpRouteNodes[j].HTTPRoute.GetNamespace() { - return httpRouteNodes[i].HTTPRoute.GetNamespace() < httpRouteNodes[j].HTTPRoute.GetNamespace() - } - return httpRouteNodes[i].HTTPRoute.GetName() < httpRouteNodes[j].HTTPRoute.GetName() - }) + httpRouteNodes := common.MapToValues(resourceModel.HTTPRoutes) - for _, httpRouteNode := range httpRouteNodes { + for _, httpRouteNode := range SortByString(httpRouteNodes) { var hostNames []string for _, hostName := range httpRouteNode.HTTPRoute.Spec.Hostnames { hostNames = append(hostNames, string(hostName)) @@ -135,11 +130,11 @@ func (hp *HTTPRoutesPrinter) PrintDescribeView(resourceModel *resourcediscovery. fmt.Fprintf(os.Stderr, "failed to marshal to yaml: %v\n", err) os.Exit(1) } - fmt.Fprint(hp.Out, string(b)) + fmt.Fprint(hp, string(b)) } if index+1 <= len(resourceModel.HTTPRoutes) { - fmt.Fprintf(hp.Out, "\n\n") + fmt.Fprintf(hp, "\n\n") } } } diff --git a/gwctl/pkg/printer/httproutes_test.go b/gwctl/pkg/printer/httproutes_test.go index e35ced4966..694123d41d 100644 --- a/gwctl/pkg/printer/httproutes_test.go +++ b/gwctl/pkg/printer/httproutes_test.go @@ -18,6 +18,7 @@ package printer import ( "bytes" + "fmt" "testing" "time" @@ -30,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" testingclock "k8s.io/utils/clock/testing" + apisv1beta1 "sigs.k8s.io/gateway-api/apis/applyconfiguration/apis/v1beta1" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/gwctl/pkg/common" @@ -37,7 +39,7 @@ import ( "sigs.k8s.io/gateway-api/gwctl/pkg/utils" ) -func TestHTTPRoutesPrinter_Print(t *testing.T) { +func TestHTTPRoutesPrinter_PrintTable(t *testing.T) { fakeClock := testingclock.NewFakeClock(time.Now()) objects := []runtime.Object{ &gatewayv1.GatewayClass{ @@ -214,11 +216,11 @@ func TestHTTPRoutesPrinter_Print(t *testing.T) { } hp := &HTTPRoutesPrinter{ - Out: params.Out, - Clock: fakeClock, + Writer: params.Out, + Clock: fakeClock, } - hp.Print(resourceModel) + hp.PrintTable(resourceModel) got := params.Out.(*bytes.Buffer).String() want := ` @@ -403,8 +405,8 @@ func TestHTTPRoutesPrinter_PrintDescribeView(t *testing.T) { } hp := &HTTPRoutesPrinter{ - Out: params.Out, - Clock: fakeClock, + Writer: params.Out, + Clock: fakeClock, } hp.PrintDescribeView(resourceModel) @@ -435,3 +437,148 @@ EffectivePolicies: t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", got, want, diff) } } + +// TestHTTPRoutesPrinter_PrintJsonYaml tests the correctness of JSON/YAML output associated with -o json/yaml of `get` subcommand +func TestHTTPRoutesPrinter_PrintJsonYaml(t *testing.T) { + fakeClock := testingclock.NewFakeClock(time.Now()) + creationTime := fakeClock.Now().Add(-24 * time.Hour).UTC() // UTC being necessary for consistently handling the time while marshaling/unmarshaling its JSON + + hrName, hrNamespace := "httproute-1", "default" + hrConfig := apisv1beta1.HTTPRoute(hrName, hrNamespace) + + objects := []runtime.Object{ + &gatewayv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gatewayclass-1", + }, + Spec: gatewayv1.GatewayClassSpec{ + ControllerName: "example.net/gateway-controller", + Description: common.PtrTo("random"), + }, + }, + + &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-1", + Namespace: "default", + }, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: "gatewayclass-1", + }, + }, + &gatewayv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: *hrConfig.APIVersion, + Kind: *hrConfig.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: hrName, + Namespace: hrNamespace, + CreationTimestamp: metav1.Time{ + Time: creationTime, + }, + Labels: map[string]string{"app": "foo", "env": "internal"}, + }, + Spec: gatewayv1.HTTPRouteSpec{ + Hostnames: []gatewayv1.Hostname{"example.com"}, + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{ + { + Name: "gateway-1", + }, + }, + }, + }, + }, + } + + params := utils.MustParamsForTest(t, common.MustClientsForTest(t, objects...)) + discoverer := resourcediscovery.Discoverer{ + K8sClients: params.K8sClients, + PolicyManager: params.PolicyManager, + } + + resourceModel, err := discoverer.DiscoverResourcesForHTTPRoute(resourcediscovery.Filter{}) + if err != nil { + t.Fatalf("Failed to discover resources: %v", err) + } + + hp := &HTTPRoutesPrinter{ + Writer: params.Out, + Clock: fakeClock, + } + Print(hp, resourceModel, utils.OutputFormatJSON) + + gotJSON := common.JSONString(params.Out.(*bytes.Buffer).String()) + wantJSON := common.JSONString(fmt.Sprintf(` + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "gateway.networking.k8s.io/v1beta1", + "kind": "HTTPRoute", + "metadata": { + "creationTimestamp": "%s", + "labels": { + "app": "foo", + "env": "internal" + }, + "name": "httproute-1", + "namespace": "default", + "resourceVersion": "999" + }, + "spec": { + "hostnames": [ + "example.com" + ], + "parentRefs": [ + { + "name": "gateway-1" + } + ] + }, + "status": { + "parents": null + } + } + ], + "kind": "List" + }`, creationTime.Format(time.RFC3339))) + diff, err := wantJSON.CmpDiff(gotJSON) + if err != nil { + t.Fatalf("Failed to compare the json diffs: %v", diff) + } + if diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", gotJSON, wantJSON, diff) + } + + hp.Writer = &bytes.Buffer{} + + Print(hp, resourceModel, utils.OutputFormatYAML) + + gotYaml := common.YamlString(hp.Writer.(*bytes.Buffer).String()) + wantYaml := common.YamlString(fmt.Sprintf(` +apiVersion: v1 +items: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + creationTimestamp: "%s" + labels: + app: foo + env: internal + name: httproute-1 + namespace: default + resourceVersion: "999" + spec: + hostnames: + - example.com + parentRefs: + - name: gateway-1 + status: + parents: null +kind: List`, creationTime.Format(time.RFC3339))) + if diff := cmp.Diff(wantYaml, gotYaml, common.YamlStringTransformer); diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", gotYaml, wantYaml, diff) + } +} diff --git a/gwctl/pkg/printer/namespace.go b/gwctl/pkg/printer/namespace.go index 7e9729e458..cf4e90a6ce 100644 --- a/gwctl/pkg/printer/namespace.go +++ b/gwctl/pkg/printer/namespace.go @@ -20,20 +20,23 @@ import ( "fmt" "io" "os" - "sort" "strings" "text/tabwriter" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/utils/clock" - "sigs.k8s.io/yaml" + "sigs.k8s.io/gateway-api/gwctl/pkg/common" "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" "sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery" + + "sigs.k8s.io/yaml" ) +var _ Printer = (*NamespacesPrinter)(nil) + type NamespacesPrinter struct { - Out io.Writer + io.Writer Clock clock.Clock } @@ -45,8 +48,12 @@ type namespaceDescribeView struct { DirectlyAttachedPolicies []policymanager.ObjRef `json:",omitempty"` } -func (nsp *NamespacesPrinter) Print(resourceModel *resourcediscovery.ResourceModel) { - tw := tabwriter.NewWriter(nsp.Out, 0, 0, 2, ' ', 0) +func (nsp *NamespacesPrinter) GetPrintableNodes(resourceModel *resourcediscovery.ResourceModel) []NodeResource { + return NodeResources(common.MapToValues(resourceModel.Namespaces)) +} + +func (nsp *NamespacesPrinter) PrintTable(resourceModel *resourcediscovery.ResourceModel) { + tw := tabwriter.NewWriter(nsp, 0, 0, 2, ' ', 0) row := []string{"NAME", "STATUS", "AGE"} _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")) if err != nil { @@ -54,16 +61,8 @@ func (nsp *NamespacesPrinter) Print(resourceModel *resourcediscovery.ResourceMod os.Exit(1) } - namespaceNodes := make([]*resourcediscovery.NamespaceNode, 0, len(resourceModel.Namespaces)) - for _, namespaceNode := range resourceModel.Namespaces { - namespaceNodes = append(namespaceNodes, namespaceNode) - } - - sort.Slice(namespaceNodes, func(i, j int) bool { - return namespaceNodes[i].Namespace.Name < namespaceNodes[j].Namespace.Name - }) - - for _, namespaceNode := range namespaceNodes { + namespaceNodes := common.MapToValues(resourceModel.Namespaces) + for _, namespaceNode := range SortByString(namespaceNodes) { age := duration.HumanDuration(nsp.Clock.Since(namespaceNode.Namespace.CreationTimestamp.Time)) row := []string{ namespaceNode.Namespace.Name, @@ -80,18 +79,9 @@ func (nsp *NamespacesPrinter) Print(resourceModel *resourcediscovery.ResourceMod } func (nsp *NamespacesPrinter) PrintDescribeView(resourceModel *resourcediscovery.ResourceModel) { - namespaceNodes := make([]*resourcediscovery.NamespaceNode, 0, len(resourceModel.Namespaces)) - - for _, namespaceNode := range resourceModel.Namespaces { - namespaceNodes = append(namespaceNodes, namespaceNode) - } - - sort.Slice(namespaceNodes, func(i, j int) bool { - return namespaceNodes[i].Namespace.Name < namespaceNodes[j].Namespace.Name - }) - + namespaceNodes := common.MapToValues(resourceModel.Namespaces) index := 0 - for _, namespaceNode := range namespaceNodes { + for _, namespaceNode := range SortByString(namespaceNodes) { index++ views := []namespaceDescribeView{ @@ -119,11 +109,11 @@ func (nsp *NamespacesPrinter) PrintDescribeView(resourceModel *resourcediscovery fmt.Fprintf(os.Stderr, "failed to marshal to yaml: %v\n", err) os.Exit(1) } - fmt.Fprint(nsp.Out, string(b)) + fmt.Fprint(nsp, string(b)) } if index+1 <= len(resourceModel.Namespaces) { - fmt.Fprintf(nsp.Out, "\n\n") + fmt.Fprintf(nsp, "\n\n") } } } diff --git a/gwctl/pkg/printer/namespace_test.go b/gwctl/pkg/printer/namespace_test.go index 6e8d7e56ee..fc33b0883c 100644 --- a/gwctl/pkg/printer/namespace_test.go +++ b/gwctl/pkg/printer/namespace_test.go @@ -18,6 +18,7 @@ package printer import ( "bytes" + "fmt" "testing" "time" @@ -36,7 +37,7 @@ import ( "sigs.k8s.io/gateway-api/gwctl/pkg/utils" ) -func TestNamespacePrinter_Print(t *testing.T) { +func TestNamespacePrinter_PrintTable(t *testing.T) { fakeClock := testingclock.NewFakeClock(time.Now()) objects := []runtime.Object{ &corev1.Namespace{ @@ -85,10 +86,10 @@ func TestNamespacePrinter_Print(t *testing.T) { } nsp := &NamespacesPrinter{ - Out: params.Out, - Clock: fakeClock, + Writer: params.Out, + Clock: fakeClock, } - nsp.Print(resourceModel) + nsp.PrintTable(resourceModel) got := params.Out.(*bytes.Buffer).String() want := ` @@ -225,8 +226,8 @@ func TestNamespacePrinter_PrintDescribeView(t *testing.T) { } nsp := &NamespacesPrinter{ - Out: params.Out, - Clock: fakeClock, + Writer: params.Out, + Clock: fakeClock, } nsp.PrintDescribeView(resourceModel) @@ -257,3 +258,109 @@ DirectlyAttachedPolicies: t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", got, want, diff) } } + +// TestNamespacesPrinter_PrintJsonYaml tests the correctness of JSON/YAML output associated with -o json/yaml of `get` subcommand +func TestNamespacesPrinter_PrintJsonYaml(t *testing.T) { + fakeClock := testingclock.NewFakeClock(time.Now()) + creationTime := fakeClock.Now().Add(-46 * 24 * time.Hour).UTC() // UTC being necessary for consistently handling the time while marshaling/unmarshaling its JSON + + nsObject := &corev1.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "v1", + APIVersion: "Namespace", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "namespace-1", + CreationTimestamp: metav1.Time{ + Time: creationTime, + }, + Labels: map[string]string{"app": "foo", "env": "internal"}, + }, + Spec: corev1.NamespaceSpec{ + Finalizers: []corev1.FinalizerName{"kubernetes"}, + }, + Status: corev1.NamespaceStatus{ + Phase: corev1.NamespaceActive, + }, + } + + params := utils.MustParamsForTest(t, common.MustClientsForTest(t, nsObject)) + discoverer := resourcediscovery.Discoverer{ + K8sClients: params.K8sClients, + PolicyManager: params.PolicyManager, + } + resourceModel, err := discoverer.DiscoverResourcesForNamespace(resourcediscovery.Filter{}) + if err != nil { + t.Fatalf("Failed to construct resourceModel: %v", resourceModel) + } + + nsp := &NamespacesPrinter{ + Writer: params.Out, + Clock: fakeClock, + } + Print(nsp, resourceModel, utils.OutputFormatJSON) + + gotJSON := common.JSONString(params.Out.(*bytes.Buffer).String()) + wantJSON := common.JSONString(fmt.Sprintf(` + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "Namespace", + "kind": "v1", + "metadata": { + "creationTimestamp": "%s", + "labels": { + "app": "foo", + "env": "internal" + }, + "name": "namespace-1", + "resourceVersion": "999" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } + } + ], + "kind": "List" + }`, creationTime.Format(time.RFC3339))) + diff, err := wantJSON.CmpDiff(gotJSON) + if err != nil { + t.Fatalf("Failed to compare the json diffs: %v", diff) + } + if diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", gotJSON, wantJSON, diff) + } + + nsp.Writer = &bytes.Buffer{} + Print(nsp, resourceModel, utils.OutputFormatYAML) + + gotYaml := common.YamlString(nsp.Writer.(*bytes.Buffer).String()) + wantYaml := common.YamlString(fmt.Sprintf(` +apiVersion: v1 +items: +- apiVersion: Namespace + kind: v1 + metadata: + creationTimestamp: "%s" + labels: + app: foo + env: internal + name: namespace-1 + resourceVersion: "999" + spec: + finalizers: + - kubernetes + status: + phase: Active +kind: List +`, creationTime.Format(time.RFC3339))) + if diff := cmp.Diff(wantYaml, gotYaml, common.YamlStringTransformer); diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", gotYaml, wantYaml, diff) + } +} diff --git a/gwctl/pkg/printer/policies.go b/gwctl/pkg/printer/policies.go index 9bdca9ee93..87b618ee24 100644 --- a/gwctl/pkg/printer/policies.go +++ b/gwctl/pkg/printer/policies.go @@ -32,21 +32,33 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/utils/clock" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "sigs.k8s.io/gateway-api/gwctl/pkg/utils" ) type PoliciesPrinter struct { - Out io.Writer + io.Writer Clock clock.Clock } -func (pp *PoliciesPrinter) PrintPoliciesGetView(policies []policymanager.Policy) { - sort.Slice(policies, func(i, j int) bool { - a := fmt.Sprintf("%v/%v", policies[i].Unstructured().GetNamespace(), policies[i].Unstructured().GetName()) - b := fmt.Sprintf("%v/%v", policies[j].Unstructured().GetNamespace(), policies[j].Unstructured().GetName()) - return a < b - }) +func (pp *PoliciesPrinter) printClientObjects(objects []client.Object, format utils.OutputFormat) { + printablePayload, err := renderPrintableObject(objects) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to render the printable objects %v\n", err) + os.Exit(1) + } + output, err := utils.MarshalWithFormat(printablePayload, format) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to marshal the object %v\n", err) + os.Exit(1) + } + fmt.Fprint(pp, string(output)) +} - tw := tabwriter.NewWriter(pp.Out, 0, 0, 2, ' ', 0) +func (pp *PoliciesPrinter) printPoliciesTable(sortedPoliciesList []policymanager.Policy) { + tw := tabwriter.NewWriter(pp, 0, 0, 2, ' ', 0) row := []string{"NAME", "KIND", "TARGET NAME", "TARGET KIND", "POLICY TYPE", "AGE"} _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")) if err != nil { @@ -54,7 +66,7 @@ func (pp *PoliciesPrinter) PrintPoliciesGetView(policies []policymanager.Policy) os.Exit(1) } - for _, policy := range policies { + for _, policy := range sortedPoliciesList { policyType := "Direct" if policy.IsInherited() { policyType = "Inherited" @@ -81,14 +93,23 @@ func (pp *PoliciesPrinter) PrintPoliciesGetView(policies []policymanager.Policy) tw.Flush() } -func (pp *PoliciesPrinter) PrintPolicyCRDsGetView(policyCRDs []policymanager.PolicyCRD) { - sort.Slice(policyCRDs, func(i, j int) bool { - a := fmt.Sprintf("%v/%v", policyCRDs[i].CRD().GetNamespace(), policyCRDs[i].CRD().GetName()) - b := fmt.Sprintf("%v/%v", policyCRDs[j].CRD().GetNamespace(), policyCRDs[j].CRD().GetName()) - return a < b - }) +func (pp *PoliciesPrinter) PrintPolicies(policies []policymanager.Policy, format utils.OutputFormat) { + sortedPolicies := SortByString(policies) + clientObjects := ClientObjects(sortedPolicies) + + switch format { + case utils.OutputFormatJSON, utils.OutputFormatYAML: + pp.printClientObjects(clientObjects, format) + case utils.OutputFormatTable: + pp.printPoliciesTable(sortedPolicies) + default: + fmt.Fprintf(os.Stderr, "unknown output format '%s' found\n", format) + os.Exit(1) + } +} - tw := tabwriter.NewWriter(pp.Out, 0, 0, 2, ' ', 0) +func (pp *PoliciesPrinter) printCRDsTable(sortedPolicyCRDsList []policymanager.PolicyCRD) { + tw := tabwriter.NewWriter(pp, 0, 0, 2, ' ', 0) row := []string{"NAME", "POLICY TYPE", "SCOPE", "AGE"} _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")) if err != nil { @@ -96,7 +117,7 @@ func (pp *PoliciesPrinter) PrintPolicyCRDsGetView(policyCRDs []policymanager.Pol os.Exit(1) } - for _, policyCRD := range policyCRDs { + for _, policyCRD := range sortedPolicyCRDsList { policyType := "Direct" if policyCRD.IsInherited() { policyType = "Inherited" @@ -119,6 +140,21 @@ func (pp *PoliciesPrinter) PrintPolicyCRDsGetView(policyCRDs []policymanager.Pol tw.Flush() } +func (pp *PoliciesPrinter) PrintCRDs(policyCRDs []policymanager.PolicyCRD, format utils.OutputFormat) { + sortedPolicyCRDs := SortByString(policyCRDs) + clientObjects := ClientObjects(sortedPolicyCRDs) + + switch format { + case utils.OutputFormatJSON, utils.OutputFormatYAML: + pp.printClientObjects(clientObjects, format) + case utils.OutputFormatTable: + pp.printCRDsTable(sortedPolicyCRDs) + default: + fmt.Fprintf(os.Stderr, "unknown output format '%s' found\n", format) + os.Exit(1) + } +} + type policyDescribeView struct { Name string `json:",omitempty"` Namespace string `json:",omitempty"` @@ -129,13 +165,7 @@ type policyDescribeView struct { } func (pp *PoliciesPrinter) PrintPoliciesDescribeView(policies []policymanager.Policy) { - sort.Slice(policies, func(i, j int) bool { - a := fmt.Sprintf("%v/%v", policies[i].Unstructured().GetNamespace(), policies[i].Unstructured().GetName()) - b := fmt.Sprintf("%v/%v", policies[j].Unstructured().GetNamespace(), policies[j].Unstructured().GetName()) - return a < b - }) - - for i, policy := range policies { + for i, policy := range SortByString(policies) { views := []policyDescribeView{ { Name: policy.Unstructured().GetName(), @@ -159,11 +189,11 @@ func (pp *PoliciesPrinter) PrintPoliciesDescribeView(policies []policymanager.Po fmt.Fprintf(os.Stderr, "failed to marshal to yaml: %v\n", err) os.Exit(1) } - fmt.Fprint(pp.Out, string(b)) + fmt.Fprint(pp, string(b)) } if i+1 != len(policies) { - fmt.Fprintf(pp.Out, "\n\n") + fmt.Fprintf(pp, "\n\n") } } } @@ -226,11 +256,11 @@ func (pp *PoliciesPrinter) PrintPolicyCRDsDescribeView(policyCrds []policymanage fmt.Fprintf(os.Stderr, "failed to marshal to yaml: %v\n", err) os.Exit(1) } - fmt.Fprint(pp.Out, string(b)) + fmt.Fprint(pp, string(b)) } if i+1 != len(policyCrds) { - fmt.Fprintf(pp.Out, "\n\n") + fmt.Fprintf(pp, "\n\n") } } } diff --git a/gwctl/pkg/printer/policies_test.go b/gwctl/pkg/printer/policies_test.go index 9a6ff127da..7433094ff3 100644 --- a/gwctl/pkg/printer/policies_test.go +++ b/gwctl/pkg/printer/policies_test.go @@ -18,6 +18,7 @@ package printer import ( "bytes" + "fmt" "testing" "time" @@ -165,12 +166,12 @@ func TestPoliciesPrinter_Print_And_PrintDescribeView(t *testing.T) { params := utils.MustParamsForTest(t, common.MustClientsForTest(t, objects...)) pp := &PoliciesPrinter{ - Out: &bytes.Buffer{}, - Clock: fakeClock, + Writer: &bytes.Buffer{}, + Clock: fakeClock, } - pp.PrintPoliciesGetView(params.PolicyManager.GetPolicies()) - got := pp.Out.(*bytes.Buffer).String() + pp.PrintPolicies(params.PolicyManager.GetPolicies(), utils.OutputFormatTable) + got := pp.Writer.(*bytes.Buffer).String() want := ` NAME KIND TARGET NAME TARGET KIND POLICY TYPE AGE health-check-gateway HealthCheckPolicy.foo.com foo-gateway Gateway Inherited 20d @@ -182,9 +183,9 @@ timeout-policy-namespace TimeoutPolicy.bar.com default Namespac t.Errorf("Print: Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", got, want, diff) } - pp.Out = &bytes.Buffer{} + pp.Writer = &bytes.Buffer{} pp.PrintPoliciesDescribeView(params.PolicyManager.GetPolicies()) - got = pp.Out.(*bytes.Buffer).String() + got = pp.Writer.(*bytes.Buffer).String() want = ` Name: health-check-gateway Group: foo.com @@ -339,12 +340,12 @@ func TestPoliciesPrinter_PrintCRDs(t *testing.T) { params := utils.MustParamsForTest(t, common.MustClientsForTest(t, objects...)) pp := &PoliciesPrinter{ - Out: &bytes.Buffer{}, - Clock: fakeClock, + Writer: &bytes.Buffer{}, + Clock: fakeClock, } - pp.PrintPolicyCRDsGetView(params.PolicyManager.GetCRDs()) + pp.printCRDsTable(params.PolicyManager.GetCRDs()) - got := pp.Out.(*bytes.Buffer).String() + got := pp.Writer.(*bytes.Buffer).String() want := ` NAME POLICY TYPE SCOPE AGE healthcheckpolicies.foo.com Inherited Cluster 24d @@ -355,6 +356,250 @@ timeoutpolicies.bar.com Direct Namespaced 5m } } +// TestPoliciesPrinter_PrintCRDs_JsonYaml tests the correctness of JSON/YAML output associated with -o json/yaml of `get` subcommand +func TestPoliciesPrinter_PrintCRDs_JsonYaml(t *testing.T) { + fakeClock := testingclock.NewFakeClock(time.Now()) + creationTime1 := fakeClock.Now().Add(-24 * 24 * time.Hour).UTC() // UTC being necessary for consistently handling the time while marshaling/unmarshaling its JSON + creationTime2 := fakeClock.Now().Add(-5 * time.Minute).UTC() // UTC being necessary for consistently handling the time while marshaling/unmarshaling its JSON + + objects := []runtime.Object{ + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "healthcheckpolicies.foo.com", + Labels: map[string]string{ + gatewayv1alpha2.PolicyLabelKey: "inherited", + }, + CreationTimestamp: metav1.Time{ + Time: creationTime1, + }, + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Scope: apiextensionsv1.ClusterScoped, + Group: "foo.com", + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{Name: "v1"}}, + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "healthcheckpolicies", + Kind: "HealthCheckPolicy", + }, + }, + }, + &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "foo.com/v1", + "kind": "HealthCheckPolicy", + "metadata": map[string]interface{}{ + "name": "health-check-gateway", + }, + "spec": map[string]interface{}{ + "override": map[string]interface{}{ + "key1": "value-child-1", + }, + "default": map[string]interface{}{ + "key2": "value-child-2", + "key5": "value-child-5", + }, + "targetRef": map[string]interface{}{ + "group": "gateway.networking.k8s.io", + "kind": "Gateway", + "name": "foo-gateway", + "namespace": "default", + }, + }, + }, + }, + + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "timeoutpolicies.bar.com", + Labels: map[string]string{ + gatewayv1alpha2.PolicyLabelKey: "direct", + }, + CreationTimestamp: metav1.Time{ + Time: creationTime2, + }, + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Scope: apiextensionsv1.NamespaceScoped, + Group: "bar.com", + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{Name: "v1"}}, + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "timeoutpolicies", + Kind: "TimeoutPolicy", + }, + }, + }, + &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "bar.com/v1", + "kind": "TimeoutPolicy", + "metadata": map[string]interface{}{ + "name": "timeout-policy-namespace", + }, + "spec": map[string]interface{}{ + "condition": "path=/abc", + "seconds": int64(30), + "targetRef": map[string]interface{}{ + "kind": "Namespace", + "name": "default", + }, + }, + }, + }, + } + + params := utils.MustParamsForTest(t, common.MustClientsForTest(t, objects...)) + pp := &PoliciesPrinter{ + Writer: &bytes.Buffer{}, + Clock: fakeClock, + } + pp.PrintCRDs(params.PolicyManager.GetCRDs(), utils.OutputFormatJSON) + + gotJSON := common.JSONString(pp.Writer.(*bytes.Buffer).String()) + wantJSON := common.JSONString(fmt.Sprintf(`{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "creationTimestamp": "%s", + "labels": { + "gateway.networking.k8s.io/policy": "inherited" + }, + "name": "healthcheckpolicies.foo.com", + "resourceVersion": "999" + }, + "spec": { + "group": "foo.com", + "names": { + "kind": "HealthCheckPolicy", + "plural": "healthcheckpolicies" + }, + "scope": "Cluster", + "versions": [ + { + "name": "v1", + "served": false, + "storage": false + } + ] + }, + "status": { + "acceptedNames": { + "kind": "", + "plural": "" + }, + "conditions": null, + "storedVersions": null + } + }, + { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "creationTimestamp": "%s", + "labels": { + "gateway.networking.k8s.io/policy": "direct" + }, + "name": "timeoutpolicies.bar.com", + "resourceVersion": "999" + }, + "spec": { + "group": "bar.com", + "names": { + "kind": "TimeoutPolicy", + "plural": "timeoutpolicies" + }, + "scope": "Namespaced", + "versions": [ + { + "name": "v1", + "served": false, + "storage": false + } + ] + }, + "status": { + "acceptedNames": { + "kind": "", + "plural": "" + }, + "conditions": null, + "storedVersions": null + } + } + ], + "kind": "List" + }`, creationTime1.Format(time.RFC3339), creationTime2.Format(time.RFC3339))) + diff, err := wantJSON.CmpDiff(gotJSON) + if err != nil { + t.Fatalf("Failed to compare the json diffs: %v", diff) + } + if diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", gotJSON, wantJSON, diff) + } + + pp.Writer = &bytes.Buffer{} + pp.PrintCRDs(params.PolicyManager.GetCRDs(), utils.OutputFormatYAML) + + gotYaml := common.YamlString(pp.Writer.(*bytes.Buffer).String()) + wantYaml := common.YamlString(fmt.Sprintf(` +apiVersion: v1 +items: +- apiVersion: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + metadata: + creationTimestamp: "%s" + labels: + gateway.networking.k8s.io/policy: inherited + name: healthcheckpolicies.foo.com + resourceVersion: "999" + spec: + group: foo.com + names: + kind: HealthCheckPolicy + plural: healthcheckpolicies + scope: Cluster + versions: + - name: v1 + served: false + storage: false + status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null +- apiVersion: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + metadata: + creationTimestamp: "%s" + labels: + gateway.networking.k8s.io/policy: direct + name: timeoutpolicies.bar.com + resourceVersion: "999" + spec: + group: bar.com + names: + kind: TimeoutPolicy + plural: timeoutpolicies + scope: Namespaced + versions: + - name: v1 + served: false + storage: false + status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null +kind: List`, creationTime1.Format(time.RFC3339), creationTime2.Format(time.RFC3339))) + if diff := cmp.Diff(wantYaml, gotYaml, common.YamlStringTransformer); diff != "" { + t.Errorf("PrintDescribeView: Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", gotYaml, wantYaml, diff) + } +} + func TestPolicyCrd_PrintDescribeView(t *testing.T) { objects := []runtime.Object{ &apiextensionsv1.CustomResourceDefinition{ @@ -439,11 +684,11 @@ func TestPolicyCrd_PrintDescribeView(t *testing.T) { params := utils.MustParamsForTest(t, common.MustClientsForTest(t, objects...)) pp := &PoliciesPrinter{ - Out: &bytes.Buffer{}, + Writer: &bytes.Buffer{}, } pp.PrintPolicyCRDsDescribeView(params.PolicyManager.GetCRDs()) - got := pp.Out.(*bytes.Buffer).String() + got := pp.Writer.(*bytes.Buffer).String() want := ` Name: healthcheckpolicies.foo.com APIVersion: apiextensions.k8s.io/v1 diff --git a/gwctl/pkg/printer/printer.go b/gwctl/pkg/printer/printer.go new file mode 100644 index 0000000000..f327ff7121 --- /dev/null +++ b/gwctl/pkg/printer/printer.go @@ -0,0 +1,79 @@ +/* +Copyright 2024 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 printer + +import ( + "fmt" + "io" + "os" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery" + "sigs.k8s.io/gateway-api/gwctl/pkg/utils" +) + +type Printer interface { + io.Writer + GetPrintableNodes(resourceModel *resourcediscovery.ResourceModel) []NodeResource + PrintTable(resourceModel *resourcediscovery.ResourceModel) +} + +func Print(p Printer, resourceModel *resourcediscovery.ResourceModel, format utils.OutputFormat) { + switch format { + case utils.OutputFormatTable: + p.PrintTable(resourceModel) + case utils.OutputFormatJSON, utils.OutputFormatYAML: + nodes := SortByString(p.GetPrintableNodes(resourceModel)) + clientObjects := ClientObjects(nodes) + + printablePayload, err := renderPrintableObject(clientObjects) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to form the printable payload %v\n", err) + os.Exit(1) + } + output, err := utils.MarshalWithFormat(printablePayload, format) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to marshal the object %v\n", err) + os.Exit(1) + } + fmt.Fprint(p, string(output)) + default: + fmt.Fprintf(os.Stderr, "Unrecognized output format: %s\n", format) + os.Exit(1) + } +} + +func renderPrintableObject(objs []client.Object) (runtime.Object, error) { + list := &unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "kind": "List", + "apiVersion": "v1", + }, + } + for _, obj := range objs { + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return list, err + } + list.Items = append(list.Items, unstructured.Unstructured{Object: unstructuredObj}) + } + return list, nil +} diff --git a/gwctl/pkg/resourcediscovery/nodes.go b/gwctl/pkg/resourcediscovery/nodes.go index bf1e7f4266..e496e945f9 100644 --- a/gwctl/pkg/resourcediscovery/nodes.go +++ b/gwctl/pkg/resourcediscovery/nodes.go @@ -20,6 +20,8 @@ import ( "fmt" "strings" + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" @@ -37,6 +39,10 @@ type resourceID struct { Name string } +func (r resourceID) String() string { + return fmt.Sprintf("%s|%s|%s|%s", r.Group, r.Kind, r.Namespace, r.Name) +} + type ( gatewayClassID resourceID namespaceID resourceID @@ -127,6 +133,8 @@ func NewGatewayClassNode(gatewayClass *gatewayv1.GatewayClass) *GatewayClassNode } } +func (g GatewayClassNode) ClientObject() client.Object { return g.GatewayClass } + func (g *GatewayClassNode) ID() gatewayClassID { //nolint:revive if g.GatewayClass == nil { klog.V(0).ErrorS(nil, "returning empty ID since GatewayClass is nil") @@ -165,6 +173,8 @@ func NewGatewayNode(gateway *gatewayv1.Gateway) *GatewayNode { } } +func (g GatewayNode) ClientObject() client.Object { return g.Gateway } + func (g *GatewayNode) ID() gatewayID { //nolint:revive if g.Gateway == nil { klog.V(0).ErrorS(nil, "returning empty ID since Gateway is nil") @@ -203,6 +213,8 @@ func NewHTTPRouteNode(httpRoute *gatewayv1.HTTPRoute) *HTTPRouteNode { } } +func (h HTTPRouteNode) ClientObject() client.Object { return h.HTTPRoute } + func (h *HTTPRouteNode) ID() httpRouteID { //nolint:revive if h.HTTPRoute == nil { klog.V(0).ErrorS(nil, "returning empty ID since HTTPRoute is nil") @@ -239,6 +251,8 @@ func NewBackendNode(backend *unstructured.Unstructured) *BackendNode { } } +func (b BackendNode) ClientObject() client.Object { return b.Backend } + func (b *BackendNode) ID() backendID { //nolint:revive if b.Backend == nil { klog.V(0).ErrorS(nil, "returning empty ID since Backend is empty") @@ -280,6 +294,8 @@ func NewNamespaceNode(namespace corev1.Namespace) *NamespaceNode { } } +func (n *NamespaceNode) ClientObject() client.Object { return n.Namespace } + func (n *NamespaceNode) ID() namespaceID { //nolint:revive if n.Namespace.Name == "" { klog.V(0).ErrorS(nil, "returning empty ID since Namespace is empty") @@ -319,6 +335,8 @@ func NewPolicyNode(policy *policymanager.Policy) *PolicyNode { } } +func (p PolicyNode) ClientObject() client.Object { return p.Policy.Unstructured() } + func (p *PolicyNode) ID() policyID { //nolint:revive if p.Policy == nil { klog.V(0).ErrorS(nil, "returning empty ID since Policy is empty") diff --git a/gwctl/pkg/utils/types.go b/gwctl/pkg/utils/types.go index 8952a7bc62..481b872a09 100644 --- a/gwctl/pkg/utils/types.go +++ b/gwctl/pkg/utils/types.go @@ -19,9 +19,13 @@ package utils import ( "bytes" "context" + "encoding/json" + "fmt" "io" "testing" + "sigs.k8s.io/yaml" + "sigs.k8s.io/gateway-api/gwctl/pkg/common" "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" ) @@ -43,3 +47,35 @@ func MustParamsForTest(t *testing.T, fakeClients *common.K8sClients) *CmdParams Out: &bytes.Buffer{}, } } + +type OutputFormat string + +const ( + OutputFormatJSON OutputFormat = "json" + OutputFormatYAML OutputFormat = "yaml" + OutputFormatTable OutputFormat = "" +) + +func ValidateAndReturnOutputFormat(format string) (OutputFormat, error) { + switch format { + case "json": + return OutputFormatJSON, nil + case "yaml": + return OutputFormatYAML, nil + case "": + return OutputFormatTable, nil + default: + var zero OutputFormat + return zero, fmt.Errorf("unknown format %s provided", format) + } +} + +func MarshalWithFormat(content any, format OutputFormat) ([]byte, error) { + if format == OutputFormatJSON { + return json.MarshalIndent(content, "", " ") + } + if format == OutputFormatYAML { + return yaml.Marshal(content) + } + return []byte{}, fmt.Errorf("format %s not found to support marshaling", format) +}