Skip to content

Commit

Permalink
feat: support for YAML and JSON outputs of gwctl (#2940)
Browse files Browse the repository at this point in the history
Signed-off-by: Yashvardhan Kukreja <yash.kukreja.98@gmail.com>
  • Loading branch information
yashvardhan-kukreja authored May 1, 2024
1 parent c9b3d31 commit 8314ca1
Show file tree
Hide file tree
Showing 21 changed files with 1,317 additions and 184 deletions.
13 changes: 7 additions & 6 deletions gwctl/cmd/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
55 changes: 38 additions & 17 deletions gwctl/cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand All @@ -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
}
Expand All @@ -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 = ""
Expand All @@ -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":
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
}
10 changes: 10 additions & 0 deletions gwctl/pkg/common/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
20 changes: 20 additions & 0 deletions gwctl/pkg/common/testhelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package common

import (
"encoding/json"
"fmt"
"strings"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -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
}
6 changes: 6 additions & 0 deletions gwctl/pkg/policymanager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"`
Expand Down
12 changes: 6 additions & 6 deletions gwctl/pkg/printer/backends.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
}
}
4 changes: 2 additions & 2 deletions gwctl/pkg/printer/backends_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions gwctl/pkg/printer/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
Loading

0 comments on commit 8314ca1

Please sign in to comment.