From d0cb3fc60465445aa7176f1eb7cc69496c5199f1 Mon Sep 17 00:00:00 2001 From: Jongwoo Han Date: Fri, 8 Mar 2024 16:34:33 +0900 Subject: [PATCH] Implement `gwctl get gatewayclass` (#2847) --- gwctl/pkg/cmd/get/get.go | 17 +++- gwctl/pkg/printer/gatewayclasses.go | 50 +++++++++++- gwctl/pkg/printer/gatewayclasses_test.go | 99 +++++++++++++++++++++++- 3 files changed, 159 insertions(+), 7 deletions(-) diff --git a/gwctl/pkg/cmd/get/get.go b/gwctl/pkg/cmd/get/get.go index aea521619f..d6e6579453 100644 --- a/gwctl/pkg/cmd/get/get.go +++ b/gwctl/pkg/cmd/get/get.go @@ -38,7 +38,7 @@ func NewGetCommand(params *utils.CmdParams) *cobra.Command { flags := &getFlags{} cmd := &cobra.Command{ - Use: "get {gateways|policies|policycrds|httproutes}", + Use: "get {gateways|gatewayclasses|policies|policycrds|httproutes}", Short: "Display one or many resources", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { @@ -64,7 +64,8 @@ func runGet(args []string, params *utils.CmdParams, flags *getFlags) { } realClock := clock.RealClock{} gwPrinter := &printer.GatewaysPrinter{Out: params.Out, Clock: realClock} - policiesPrinter := &printer.PoliciesPrinter{Out: params.Out, Clock: realClock} + gwcPrinter := &printer.GatewayClassesPrinter{Out: params.Out, Clock: realClock} + policiesPrinter := &printer.PoliciesPrinter{Out: params.Out} httpRoutesPrinter := &printer.HTTPRoutesPrinter{Out: params.Out, Clock: realClock} switch kind { @@ -79,6 +80,18 @@ func runGet(args []string, params *utils.CmdParams, flags *getFlags) { } gwPrinter.Print(resourceModel) + case "gatewayclass", "gatewayclasses": + filter := resourcediscovery.Filter{Namespace: ns} + if len(args) > 1 { + filter.Name = args[1] + } + 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) + case "policy", "policies": list := params.PolicyManager.GetPolicies() policiesPrinter.Print(list) diff --git a/gwctl/pkg/printer/gatewayclasses.go b/gwctl/pkg/printer/gatewayclasses.go index e58c883248..64ae10d6c8 100644 --- a/gwctl/pkg/printer/gatewayclasses.go +++ b/gwctl/pkg/printer/gatewayclasses.go @@ -19,15 +19,21 @@ package printer import ( "fmt" "io" - - "sigs.k8s.io/yaml" + "sort" + "strings" + "text/tabwriter" "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" "sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery" + "sigs.k8s.io/yaml" + + "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/utils/clock" ) type GatewayClassesPrinter struct { - Out io.Writer + Out io.Writer + Clock clock.Clock } type gatewayClassDescribeView struct { @@ -39,6 +45,44 @@ type gatewayClassDescribeView struct { DirectlyAttachedPolicies []policymanager.ObjRef `json:",omitempty"` } +func (gcp *GatewayClassesPrinter) Print(model *resourcediscovery.ResourceModel) { + tw := tabwriter.NewWriter(gcp.Out, 0, 0, 2, ' ', 0) + row := []string{"NAME", "CONTROLLER", "ACCEPTED", "AGE"} + tw.Write([]byte(strings.Join(row, "\t") + "\n")) + + 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) + }) + + for _, gatewayClassNode := range gatewayClassNodes { + accepted := "Unknown" + for _, condition := range gatewayClassNode.GatewayClass.Status.Conditions { + if condition.Type == "Accepted" { + accepted = string(condition.Status) + } + } + + age := duration.HumanDuration(gcp.Clock.Since(gatewayClassNode.GatewayClass.GetCreationTimestamp().Time)) + + row := []string{ + gatewayClassNode.GatewayClass.GetName(), + string(gatewayClassNode.GatewayClass.Spec.ControllerName), + accepted, + age, + } + tw.Write([]byte(strings.Join(row, "\t") + "\n")) + } + tw.Flush() +} + func (gcp *GatewayClassesPrinter) PrintDescribeView(resourceModel *resourcediscovery.ResourceModel) { index := 0 for _, gatewayClassNode := range resourceModel.GatewayClasses { diff --git a/gwctl/pkg/printer/gatewayclasses_test.go b/gwctl/pkg/printer/gatewayclasses_test.go index 8a1ad42e72..f4730f3993 100644 --- a/gwctl/pkg/printer/gatewayclasses_test.go +++ b/gwctl/pkg/printer/gatewayclasses_test.go @@ -19,21 +19,115 @@ package printer import ( "bytes" "testing" + "time" "github.com/google/go-cmp/cmp" + 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" "k8s.io/apimachinery/pkg/runtime" + testingclock "k8s.io/utils/clock/testing" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - "sigs.k8s.io/gateway-api/gwctl/pkg/cmd/utils" "sigs.k8s.io/gateway-api/gwctl/pkg/common" "sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery" ) +func TestGatewayClassesPrinter_Print(t *testing.T) { + fakeClock := testingclock.NewFakeClock(time.Now()) + objects := []runtime.Object{ + &gatewayv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar-com-internal-gateway-class", + CreationTimestamp: metav1.Time{ + Time: fakeClock.Now().Add(-365 * 24 * time.Hour), + }, + }, + Spec: gatewayv1.GatewayClassSpec{ + ControllerName: "bar.baz/internal-gateway-class", + }, + Status: gatewayv1.GatewayClassStatus{ + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: "True", + }, + }, + }, + }, + &gatewayv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-com-external-gateway-class", + CreationTimestamp: metav1.Time{ + Time: fakeClock.Now().Add(-100 * 24 * time.Hour), + }, + }, + Spec: gatewayv1.GatewayClassSpec{ + ControllerName: "foo.com/external-gateway-class", + }, + Status: gatewayv1.GatewayClassStatus{ + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: "False", + }, + }, + }, + }, + &gatewayv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-com-internal-gateway-class", + CreationTimestamp: metav1.Time{ + Time: fakeClock.Now().Add(-24 * time.Minute), + }, + }, + Spec: gatewayv1.GatewayClassSpec{ + ControllerName: "foo.com/internal-gateway-class", + }, + Status: gatewayv1.GatewayClassStatus{ + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: "Unknown", + }, + }, + }, + }, + } + + params := utils.MustParamsForTest(t, common.MustClientsForTest(t, objects...)) + 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{ + Out: params.Out, + Clock: fakeClock, + } + gcp.Print(resourceModel) + + got := params.Out.(*bytes.Buffer).String() + want := ` +NAME CONTROLLER ACCEPTED AGE +bar-com-internal-gateway-class bar.baz/internal-gateway-class True 365d +foo-com-external-gateway-class foo.com/external-gateway-class False 100d +foo-com-internal-gateway-class foo.com/internal-gateway-class Unknown 24m +` + if diff := cmp.Diff(common.YamlString(want), common.YamlString(got), common.YamlStringTransformer); diff != "" { + t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", got, want, diff) + } +} + func TestGatewayClassesPrinter_PrintDescribeView(t *testing.T) { + fakeClock := testingclock.NewFakeClock(time.Now()) objects := []runtime.Object{ &gatewayv1.GatewayClass{ ObjectMeta: metav1.ObjectMeta{ @@ -89,7 +183,8 @@ func TestGatewayClassesPrinter_PrintDescribeView(t *testing.T) { } gcp := &GatewayClassesPrinter{ - Out: params.Out, + Out: params.Out, + Clock: fakeClock, } gcp.PrintDescribeView(resourceModel)