Skip to content

Commit ef791dc

Browse files
[NET-11256] Add gateway read command to consul-k8s CLI (#4432)
* Quickly prototype new `gateway proxy read` command * Consolidate routes into single file alongside gateway w/ separate file(s) for orphaned routes * Omit orphaned routes for single gateway read use case * Support JSON output flag for `gateway read` command * Wire up --help for `gateway read` command * Only create zip while when output is archive * Add a synopsis for the `gateway read` command * Add changelog entry * Add test coverage for new `gateway read` command * Minimize version jump between k8s.io dependencies to get working tests * Specify that the namespace is a Kubernetes one
1 parent 3a0b862 commit ef791dc

File tree

7 files changed

+628
-158
lines changed

7 files changed

+628
-158
lines changed

.changelog/4432.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
cli: Introduce `gateway read` for collecting multiple components of a gateway's configuration by running a single command.
3+
```

cli/cmd/gateway/read/command.go

+299
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
package read
2+
3+
import (
4+
"archive/zip"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"os"
9+
"strings"
10+
"sync"
11+
12+
helmcli "helm.sh/helm/v3/pkg/cli"
13+
"k8s.io/client-go/rest"
14+
"sigs.k8s.io/controller-runtime/pkg/client"
15+
gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
16+
gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
17+
"sigs.k8s.io/yaml"
18+
19+
"github.com/hashicorp/consul-k8s/cli/common"
20+
"github.com/hashicorp/consul-k8s/cli/common/flag"
21+
"github.com/hashicorp/consul-k8s/cli/common/terminal"
22+
)
23+
24+
type Command struct {
25+
*common.BaseCommand
26+
27+
kubernetes client.Client
28+
restConfig *rest.Config
29+
30+
set *flag.Sets
31+
32+
flagGatewayKind string
33+
flagGatewayNamespace string
34+
flagKubeConfig string
35+
flagKubeContext string
36+
flagOutput string
37+
38+
gatewayName string
39+
40+
initOnce sync.Once
41+
help string
42+
}
43+
44+
func (c *Command) Help() string {
45+
c.initOnce.Do(c.init)
46+
return fmt.Sprintf("%s\n\nUsage: consul-k8s gateway read <gateway-name> [flags]\n\n%s", c.Synopsis(), c.help)
47+
}
48+
49+
func (c *Command) Synopsis() string {
50+
return "Inspect the configuration for a given Gateway."
51+
}
52+
53+
// init establishes the flags for Command
54+
func (c *Command) init() {
55+
c.set = flag.NewSets()
56+
57+
f := c.set.NewSet("Command Options")
58+
f.StringVar(&flag.StringVar{
59+
Name: "namespace",
60+
Target: &c.flagGatewayNamespace,
61+
Usage: "The Kubernetes namespace of the Gateway to read",
62+
Aliases: []string{"n"},
63+
})
64+
f.StringVar(&flag.StringVar{
65+
Name: "output",
66+
Target: &c.flagOutput,
67+
Usage: "Output the Gateway configuration as 'json' in the terminal or 'archive' as a zip archive in the current directory named after the Gateway.",
68+
Default: "archive",
69+
Aliases: []string{"o"},
70+
})
71+
72+
f = c.set.NewSet("Global Options")
73+
f.StringVar(&flag.StringVar{
74+
Name: "kubeconfig",
75+
Aliases: []string{"c"},
76+
Target: &c.flagKubeConfig,
77+
Usage: "Set the path to kubeconfig file.",
78+
})
79+
f.StringVar(&flag.StringVar{
80+
Name: "context",
81+
Target: &c.flagKubeContext,
82+
Usage: "Set the Kubernetes context to use.",
83+
})
84+
85+
c.help = c.set.Help()
86+
}
87+
88+
// Run runs the command
89+
func (c *Command) Run(args []string) int {
90+
c.initOnce.Do(c.init)
91+
c.Log.ResetNamed("read")
92+
defer common.CloseWithError(c.BaseCommand)
93+
94+
if len(args) < 1 || strings.HasPrefix(args[0], "-") {
95+
c.UI.Output("Usage: gateway read <gateway-name>")
96+
return 1
97+
}
98+
99+
if len(args) > 1 {
100+
if err := c.set.Parse(args[1:]); err != nil {
101+
c.UI.Output(err.Error(), terminal.WithErrorStyle())
102+
return 1
103+
}
104+
}
105+
106+
c.gatewayName = args[0]
107+
108+
if err := c.initKubernetes(); err != nil {
109+
c.UI.Output(err.Error(), terminal.WithErrorStyle())
110+
return 1
111+
}
112+
113+
if err := c.fetchCRDs(); err != nil {
114+
c.UI.Output(err.Error(), terminal.WithErrorStyle())
115+
return 1
116+
}
117+
118+
return 0
119+
}
120+
121+
func (c *Command) fetchCRDs() error {
122+
// Fetch Gateway
123+
var gateway gwv1beta1.Gateway
124+
if err := c.kubernetes.Get(context.Background(), client.ObjectKey{Namespace: c.flagGatewayNamespace, Name: c.gatewayName}, &gateway); err != nil {
125+
return fmt.Errorf("error fetching Gateway CRD: %w", err)
126+
}
127+
128+
// Fetch GatewayClass referenced by Gateway
129+
var gatewayClass gwv1beta1.GatewayClass
130+
if err := c.kubernetes.Get(context.Background(), client.ObjectKey{Namespace: "", Name: string(gateway.Spec.GatewayClassName)}, &gatewayClass); err != nil {
131+
return fmt.Errorf("error fetching GatewayClass CRD: %w", err)
132+
}
133+
134+
// FUTURE Fetch GatewayClassConfig referenced by GatewayClass
135+
// This import requires resolving hairy dependency discrepancies between modules in this repo
136+
//var gatewayClassConfig v1alpha1.GatewayClassConfig
137+
//if err := c.kubernetes.Get(context.Background(), client.ObjectKey{Namespace: "", Name: gatewayClass.Spec.ParametersRef.Name}, &gatewayClassConfig); err != nil {
138+
// return fmt.Errorf("error fetching GatewayClassConfig CRD: %w", err)
139+
//}
140+
141+
// Fetch HTTPRoutes that reference the Gateway
142+
var httpRoutes gwv1beta1.HTTPRouteList
143+
if err := c.kubernetes.List(context.Background(), &httpRoutes); err != nil {
144+
return fmt.Errorf("error fetching HTTPRoute CRDs: %w", err)
145+
}
146+
147+
// Fetch TCPRoutes that reference the Gateway
148+
var tcpRoutes gwv1alpha2.TCPRouteList
149+
if err := c.kubernetes.List(context.Background(), &tcpRoutes); err != nil {
150+
return fmt.Errorf("error fetching TCPRoute CRDs: %w", err)
151+
}
152+
153+
// FUTURE Fetch MeshServices referenced by HTTPRoutes or TCPRoutes
154+
// This import requires resolving hairy dependency discrepancies between modules in this repo
155+
// var meshServices v1alpha1.MeshServiceList
156+
// if err := c.kubernetes.List(context.Background(), &meshServices); err != nil {
157+
// return fmt.Errorf("error fetching MeshService CRDs: %w", err)
158+
// }
159+
160+
gatewayWithRoutes := struct {
161+
Gateway gwv1beta1.Gateway `json:"gateway"`
162+
GatewayClass gwv1beta1.GatewayClass `json:"gatewayClass"`
163+
HTTPRoutes []gwv1beta1.HTTPRoute `json:"httpRoutes"`
164+
TCPRoutes []gwv1alpha2.TCPRoute `json:"tcpRoutes"`
165+
}{
166+
Gateway: gateway,
167+
GatewayClass: gatewayClass,
168+
HTTPRoutes: make([]gwv1beta1.HTTPRoute, 0, len(httpRoutes.Items)),
169+
TCPRoutes: make([]gwv1alpha2.TCPRoute, 0, len(tcpRoutes.Items)),
170+
}
171+
172+
for _, route := range httpRoutes.Items {
173+
for _, ref := range route.Spec.ParentRefs {
174+
switch {
175+
case string(ref.Name) != gateway.Name:
176+
// Route parent references gateway with different name
177+
continue
178+
case ref.Namespace != nil && string(*ref.Namespace) == gateway.Namespace:
179+
// Route parent explicitly references gateway with same name and namespace
180+
gatewayWithRoutes.HTTPRoutes = append(gatewayWithRoutes.HTTPRoutes, route)
181+
case ref.Namespace == nil && route.Namespace == gateway.Namespace:
182+
// Route parent implicitly references gateway with same name in local namespace
183+
gatewayWithRoutes.HTTPRoutes = append(gatewayWithRoutes.HTTPRoutes, route)
184+
}
185+
}
186+
}
187+
188+
for _, route := range tcpRoutes.Items {
189+
for _, ref := range route.Spec.ParentRefs {
190+
switch {
191+
case string(ref.Name) != gateway.Name:
192+
// Route parent references gateway with different name
193+
continue
194+
case ref.Namespace != nil && string(*ref.Namespace) == gateway.Namespace:
195+
// Route parent explicitly references gateway with same name and namespace
196+
gatewayWithRoutes.TCPRoutes = append(gatewayWithRoutes.TCPRoutes, route)
197+
case ref.Namespace == nil && route.Namespace == gateway.Namespace:
198+
// Route parent implicitly references gateway with same name in local namespace
199+
gatewayWithRoutes.TCPRoutes = append(gatewayWithRoutes.TCPRoutes, route)
200+
}
201+
}
202+
}
203+
204+
switch strings.ToLower(c.flagOutput) {
205+
case "json":
206+
if err := c.writeJSONOutput(gatewayWithRoutes); err != nil {
207+
return fmt.Errorf("error writing CRDs as JSON: %w", err)
208+
}
209+
default:
210+
file, err := os.Create(fmt.Sprintf("./%s.zip", c.gatewayName))
211+
if err != nil {
212+
return fmt.Errorf("error creating output file: %w", err)
213+
}
214+
215+
zipw := zip.NewWriter(file)
216+
defer zipw.Close()
217+
218+
if err := c.writeArchive(zipw, c.gatewayName+".yaml", gatewayWithRoutes); err != nil {
219+
return fmt.Errorf("error writing CRDs to zip archive: %w", err)
220+
}
221+
return zipw.Close()
222+
}
223+
224+
return nil
225+
}
226+
227+
func (c *Command) writeJSONOutput(obj interface{}) error {
228+
output, err := json.MarshalIndent(obj, "", "\t")
229+
if err != nil {
230+
return err
231+
}
232+
233+
c.UI.Output(string(output))
234+
return nil
235+
}
236+
237+
func (c *Command) writeArchive(zipw *zip.Writer, name string, obj interface{}) error {
238+
w, err := zipw.Create(name)
239+
if err != nil {
240+
return fmt.Errorf("error creating zip entry for %s: %w", name, err)
241+
}
242+
243+
objYaml, err := yaml.Marshal(obj)
244+
if err != nil {
245+
return fmt.Errorf("error marshalling %s: %w", name, err)
246+
}
247+
248+
_, err = w.Write(objYaml)
249+
if err != nil {
250+
return fmt.Errorf("error writing %s to zip archive: %w", name, err)
251+
}
252+
253+
c.UI.Output("Wrote to zip archive " + name)
254+
255+
return nil
256+
}
257+
258+
// initKubernetes initializes the REST config and uses it to initialize the k8s client.
259+
func (c *Command) initKubernetes() (err error) {
260+
settings := helmcli.New()
261+
262+
// If a kubeconfig was specified, use it
263+
if c.flagKubeConfig != "" {
264+
settings.KubeConfig = c.flagKubeConfig
265+
}
266+
267+
// If a kube context was specified, use it
268+
if c.flagKubeContext != "" {
269+
settings.KubeContext = c.flagKubeContext
270+
}
271+
272+
// Create a REST config from the settings for our Kubernetes client
273+
if c.restConfig == nil {
274+
if c.restConfig, err = settings.RESTClientGetter().ToRESTConfig(); err != nil {
275+
return fmt.Errorf("error creating Kubernetes REST config: %w", err)
276+
}
277+
}
278+
279+
// Create a controller-runtime client from c.restConfig
280+
if c.kubernetes == nil {
281+
if c.kubernetes, err = client.New(c.restConfig, client.Options{}); err != nil {
282+
return fmt.Errorf("error creating controller-runtime client: %w", err)
283+
}
284+
// FUTURE Fix dependency discrepancies between modules in this repo so that this scheme can be added (see above)
285+
//_ = v1alpha1.AddToScheme(c.kubernetes.Scheme())
286+
_ = gwv1alpha2.AddToScheme(c.kubernetes.Scheme())
287+
_ = gwv1beta1.AddToScheme(c.kubernetes.Scheme())
288+
}
289+
290+
// If no namespace was specified, use the one from the kube context
291+
if c.flagGatewayNamespace == "" {
292+
if c.flagOutput != "json" {
293+
c.UI.Output("No namespace specified, using current kube context namespace: %s", settings.Namespace())
294+
}
295+
c.flagGatewayNamespace = settings.Namespace()
296+
}
297+
298+
return nil
299+
}

0 commit comments

Comments
 (0)