Skip to content

Commit 8e7ffc3

Browse files
Implement basic functionality of gwctl describe namespace (#2836)
* Implement basic functionality of gwctl describe namespace * Finalized printing of gwctl describe namespace command * Added test for gwctl describe namespace and modified ResourceModel.addNamespace() function to accept corev1.Namespace * Fixed failing httproutes test and Makefile
1 parent 1ba825b commit 8e7ffc3

File tree

10 files changed

+376
-21
lines changed

10 files changed

+376
-21
lines changed

gwctl/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ deps:
1919

2020
build: deps
2121
@echo "Building gwctl..."
22-
@go build -o bin/gwctl cmd/main.go
22+
@go build -o bin/gwctl main.go

gwctl/cmd/describe.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func NewDescribeCommand() *cobra.Command {
3636
var allNamespacesFlag bool
3737

3838
cmd := &cobra.Command{
39-
Use: "describe {policies|httproutes|gateways|gatewayclasses|backends} RESOURCE_NAME",
39+
Use: "describe {policies|httproutes|gateways|gatewayclasses|backends|namespace} RESOURCE_NAME",
4040
Short: "Show details of a specific resource or group of resources",
4141
Args: cobra.RangeArgs(1, 2),
4242
Run: func(cmd *cobra.Command, args []string) {
@@ -78,6 +78,7 @@ func runDescribe(cmd *cobra.Command, args []string, params *utils.CmdParams) {
7878
gwPrinter := &printer.GatewaysPrinter{Out: params.Out}
7979
gwcPrinter := &printer.GatewayClassesPrinter{Out: params.Out}
8080
backendsPrinter := &printer.BackendsPrinter{Out: params.Out}
81+
namespacesPrinter := &printer.NamespacesPrinter{Out: params.Out}
8182

8283
switch kind {
8384
case "policy", "policies":
@@ -146,6 +147,19 @@ func runDescribe(cmd *cobra.Command, args []string, params *utils.CmdParams) {
146147
}
147148
backendsPrinter.PrintDescribeView(resourceModel)
148149

150+
case "namespace", "namespaces":
151+
filter := resourcediscovery.Filter{}
152+
if len(args) > 1 {
153+
filter.Name = args[1]
154+
}
155+
156+
resourceModel, err := discoverer.DiscoverResourcesForNamespace(filter)
157+
if err != nil {
158+
fmt.Fprintf(os.Stderr, "failed to discover Namespace resources: %v\n", err)
159+
os.Exit(1)
160+
}
161+
namespacesPrinter.PrintDescribeView(resourceModel)
162+
149163
default:
150164
fmt.Fprintf(os.Stderr, "Unrecognized RESOURCE_TYPE\n")
151165
}

gwctl/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
k8s.io/apimachinery v0.29.2
1212
k8s.io/client-go v0.29.2
1313
k8s.io/klog/v2 v2.120.1
14+
k8s.io/utils v0.0.0-20240102154912-e7106e64919e
1415
sigs.k8s.io/controller-runtime v0.17.2
1516
sigs.k8s.io/gateway-api v1.0.0
1617
sigs.k8s.io/yaml v1.4.0
@@ -53,7 +54,6 @@ require (
5354
gopkg.in/yaml.v2 v2.4.0 // indirect
5455
gopkg.in/yaml.v3 v3.0.1 // indirect
5556
k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910 // indirect
56-
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
5757
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
5858
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
5959
)

gwctl/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
7979
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
8080
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
8181
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
82-
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
83-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
82+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
83+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
8484
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
8585
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
8686
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

gwctl/pkg/printer/httproutes_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"github.com/google/go-cmp/cmp"
2828
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
29+
corev1 "k8s.io/api/core/v1"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3132
"k8s.io/apimachinery/pkg/runtime"
@@ -57,6 +58,22 @@ func TestHTTPRoutesPrinter_Print(t *testing.T) {
5758
Description: common.PtrTo("random"),
5859
},
5960
},
61+
&corev1.Namespace{
62+
ObjectMeta: metav1.ObjectMeta{
63+
Name: "ns1",
64+
},
65+
Status: corev1.NamespaceStatus{
66+
Phase: corev1.NamespaceActive,
67+
},
68+
},
69+
&corev1.Namespace{
70+
ObjectMeta: metav1.ObjectMeta{
71+
Name: "ns2",
72+
},
73+
Status: corev1.NamespaceStatus{
74+
Phase: corev1.NamespaceActive,
75+
},
76+
},
6077
&gatewayv1.Gateway{
6178
ObjectMeta: metav1.ObjectMeta{
6279
Name: "demo-gateway-1",

gwctl/pkg/printer/namespace.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package printer
18+
19+
import (
20+
"fmt"
21+
"io"
22+
"os"
23+
24+
"sigs.k8s.io/gateway-api/gwctl/pkg/policymanager"
25+
"sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery"
26+
"sigs.k8s.io/yaml"
27+
)
28+
29+
type NamespacesPrinter struct {
30+
Out io.Writer
31+
}
32+
33+
type namespaceDescribeView struct {
34+
Name string `json:",omitempty"`
35+
Labels map[string]string `json:",omitempty"`
36+
Annotations map[string]string `json:",omitempty"`
37+
Status string `json:",omitempty"`
38+
DirectlyAttachedPolicies []policymanager.ObjRef `json:",omitempty"`
39+
}
40+
41+
func (nsp *NamespacesPrinter) PrintDescribeView(resourceModel *resourcediscovery.ResourceModel) {
42+
index := 0
43+
for _, namespaceNode := range resourceModel.Namespaces {
44+
index++
45+
46+
views := []namespaceDescribeView{
47+
{
48+
Name: namespaceNode.Namespace.Name,
49+
},
50+
{
51+
Annotations: namespaceNode.Namespace.Annotations,
52+
Labels: namespaceNode.Namespace.Labels,
53+
},
54+
{
55+
Status: string(namespaceNode.Namespace.Status.Phase),
56+
},
57+
}
58+
59+
if policyRefs := resourcediscovery.ConvertPoliciesMapToPolicyRefs(namespaceNode.Policies); len(policyRefs) != 0 {
60+
views = append(views, namespaceDescribeView{
61+
DirectlyAttachedPolicies: policyRefs,
62+
})
63+
}
64+
65+
for _, view := range views {
66+
b, err := yaml.Marshal(view)
67+
if err != nil {
68+
fmt.Fprintf(os.Stderr, "failed to marshal to yaml: %v\n", err)
69+
os.Exit(1)
70+
}
71+
fmt.Fprint(nsp.Out, string(b))
72+
}
73+
74+
if index+1 <= len(resourceModel.Namespaces) {
75+
fmt.Fprintf(nsp.Out, "\n\n")
76+
}
77+
}
78+
}

gwctl/pkg/printer/namespace_test.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package printer
18+
19+
import (
20+
"bytes"
21+
"testing"
22+
23+
"github.com/google/go-cmp/cmp"
24+
corev1 "k8s.io/api/core/v1"
25+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28+
"k8s.io/apimachinery/pkg/runtime"
29+
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
30+
"sigs.k8s.io/gateway-api/gwctl/pkg/utils"
31+
"sigs.k8s.io/gateway-api/gwctl/pkg/common"
32+
"sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery"
33+
)
34+
35+
func TestNamespacePrinter_PrintDescribeView(t *testing.T) {
36+
objects := []runtime.Object{
37+
// Defining a Namespace called development
38+
&corev1.Namespace{
39+
ObjectMeta: metav1.ObjectMeta{
40+
Name: "development",
41+
Labels: map[string]string{
42+
"type": "test-namespace",
43+
},
44+
Annotations: map[string]string{
45+
"test-annotation": "development-annotation",
46+
},
47+
},
48+
Status: corev1.NamespaceStatus{
49+
Phase: corev1.NamespaceActive,
50+
},
51+
},
52+
53+
// CRD and definition for HealthCheckPolicy attached to development namespace
54+
&apiextensionsv1.CustomResourceDefinition{
55+
ObjectMeta: metav1.ObjectMeta{
56+
Name: "healthcheckpolicies.foo.com",
57+
Labels: map[string]string{
58+
gatewayv1alpha2.PolicyLabelKey: "inherited",
59+
},
60+
},
61+
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
62+
Scope: apiextensionsv1.ClusterScoped,
63+
Group: "foo.com",
64+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{Name: "v1"}},
65+
Names: apiextensionsv1.CustomResourceDefinitionNames{
66+
Plural: "healthcheckpolicies",
67+
Kind: "HealthCheckPolicy",
68+
},
69+
},
70+
},
71+
&unstructured.Unstructured{
72+
Object: map[string]interface{}{
73+
"apiVersion": "foo.com/v1",
74+
"kind": "HealthCheckPolicy",
75+
"metadata": map[string]interface{}{
76+
"name": "health-check-gatewayclass",
77+
},
78+
"spec": map[string]interface{}{
79+
"override": map[string]interface{}{
80+
"key1": "value-parent-1",
81+
"key3": "value-parent-3",
82+
"key5": "value-parent-5",
83+
},
84+
"default": map[string]interface{}{
85+
"key2": "value-parent-2",
86+
"key4": "value-parent-4",
87+
},
88+
"targetRef": map[string]interface{}{
89+
"kind": "Namespace",
90+
"name": "development",
91+
},
92+
},
93+
},
94+
},
95+
96+
// Defining a Namespace called production
97+
&corev1.Namespace{
98+
ObjectMeta: metav1.ObjectMeta{
99+
Name: "production",
100+
Labels: map[string]string{
101+
"type": "production-namespace",
102+
},
103+
},
104+
Status: corev1.NamespaceStatus{
105+
Phase: corev1.NamespaceActive,
106+
},
107+
},
108+
// CRD and definition for TimeoutPolicy attached to default namespace
109+
&apiextensionsv1.CustomResourceDefinition{
110+
ObjectMeta: metav1.ObjectMeta{
111+
Name: "timeoutpolicies.bar.com",
112+
Labels: map[string]string{
113+
gatewayv1alpha2.PolicyLabelKey: "direct",
114+
},
115+
},
116+
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
117+
Scope: apiextensionsv1.ClusterScoped,
118+
Group: "bar.com",
119+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{Name: "v1"}},
120+
Names: apiextensionsv1.CustomResourceDefinitionNames{
121+
Plural: "timeoutpolicies",
122+
Kind: "TimeoutPolicy",
123+
},
124+
},
125+
},
126+
&unstructured.Unstructured{
127+
Object: map[string]interface{}{
128+
"apiVersion": "bar.com/v1",
129+
"kind": "TimeoutPolicy",
130+
"metadata": map[string]interface{}{
131+
"name": "timeout-policy-namespace",
132+
},
133+
"spec": map[string]interface{}{
134+
"condition": "path=/abc",
135+
"seconds": int64(30),
136+
"targetRef": map[string]interface{}{
137+
"kind": "Namespace",
138+
"name": "production",
139+
},
140+
},
141+
},
142+
},
143+
}
144+
145+
params := utils.MustParamsForTest(t, common.MustClientsForTest(t, objects...))
146+
discoverer := resourcediscovery.Discoverer{
147+
K8sClients: params.K8sClients,
148+
PolicyManager: params.PolicyManager,
149+
}
150+
resourceModel, err := discoverer.DiscoverResourcesForNamespace(resourcediscovery.Filter{})
151+
if err != nil {
152+
t.Fatalf("Failed to construct resourceModel: %v", resourceModel)
153+
}
154+
155+
nsp := &NamespacesPrinter{
156+
Out: params.Out,
157+
}
158+
nsp.PrintDescribeView(resourceModel)
159+
160+
got := params.Out.(*bytes.Buffer).String()
161+
want := `
162+
Name: development
163+
Annotations:
164+
test-annotation: development-annotation
165+
Labels:
166+
type: test-namespace
167+
Status: Active
168+
DirectlyAttachedPolicies:
169+
- Group: foo.com
170+
Kind: HealthCheckPolicy
171+
Name: health-check-gatewayclass
172+
173+
174+
Name: production
175+
Labels:
176+
type: production-namespace
177+
Status: Active
178+
DirectlyAttachedPolicies:
179+
- Group: bar.com
180+
Kind: TimeoutPolicy
181+
Name: timeout-policy-namespace
182+
`
183+
if diff := cmp.Diff(common.YamlString(want), common.YamlString(got), common.YamlStringTransformer); diff != "" {
184+
t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", got, want, diff)
185+
}
186+
187+
}

0 commit comments

Comments
 (0)