From 9d62ec97de9f48f7285c99204c75a9fc22c6c1f6 Mon Sep 17 00:00:00 2001 From: Alex Kursell Date: Tue, 12 Mar 2019 12:52:23 -0400 Subject: [PATCH] Improve kubectl plugin --- cmd/plugin/commands/backends/backends.go | 84 +++++ cmd/plugin/commands/certs/certs.go | 70 ++++ cmd/plugin/commands/conf/conf.go | 78 +++++ cmd/plugin/commands/exec/exec.go | 72 ++++ cmd/plugin/commands/general/general.go | 60 ++++ cmd/plugin/commands/info/info.go | 58 +++ cmd/plugin/commands/ingresses/ingresses.go | 217 ++++++++++++ cmd/plugin/commands/logs/logs.go | 108 ++++++ cmd/plugin/commands/ssh/ssh.go | 53 +++ cmd/plugin/kubectl/kubectl.go | 132 +++++++ cmd/plugin/main.go | 389 +-------------------- cmd/plugin/request/request.go | 214 +++++++----- cmd/plugin/util/util.go | 22 +- internal/nginx/main.go | 2 +- 14 files changed, 1106 insertions(+), 453 deletions(-) create mode 100644 cmd/plugin/commands/backends/backends.go create mode 100644 cmd/plugin/commands/certs/certs.go create mode 100644 cmd/plugin/commands/conf/conf.go create mode 100644 cmd/plugin/commands/exec/exec.go create mode 100644 cmd/plugin/commands/general/general.go create mode 100644 cmd/plugin/commands/info/info.go create mode 100644 cmd/plugin/commands/ingresses/ingresses.go create mode 100644 cmd/plugin/commands/logs/logs.go create mode 100644 cmd/plugin/commands/ssh/ssh.go create mode 100644 cmd/plugin/kubectl/kubectl.go diff --git a/cmd/plugin/commands/backends/backends.go b/cmd/plugin/commands/backends/backends.go new file mode 100644 index 0000000000..b5571585f8 --- /dev/null +++ b/cmd/plugin/commands/backends/backends.go @@ -0,0 +1,84 @@ +/* +Copyright 2019 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 backends + +import ( + "fmt" + "github.com/spf13/cobra" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "k8s.io/ingress-nginx/cmd/plugin/kubectl" + "k8s.io/ingress-nginx/cmd/plugin/request" + "k8s.io/ingress-nginx/cmd/plugin/util" +) + +// CreateCommand creates and returns this cobra subcommand +func CreateCommand(flags *genericclioptions.ConfigFlags) *cobra.Command { + var pod, deployment *string + cmd := &cobra.Command{ + Use: "backends", + Short: "Inspect the dynamic backend information of an ingress-nginx instance", + RunE: func(cmd *cobra.Command, args []string) error { + backend, err := cmd.Flags().GetString("backend") + if err != nil { + return err + } + onlyList, err := cmd.Flags().GetBool("list") + if err != nil { + return err + } + if onlyList && backend != "" { + return fmt.Errorf("--list and --backend cannot both be specified") + } + + util.PrintError(backends(flags, *pod, *deployment, backend, onlyList)) + return nil + }, + } + + pod = util.AddPodFlag(cmd) + deployment = util.AddDeploymentFlag(cmd) + cmd.Flags().String("backend", "", "Output only the information for the given backend") + cmd.Flags().Bool("list", false, "Output a newline-separated list of backend names") + + return cmd +} + +func backends(flags *genericclioptions.ConfigFlags, podName string, deployment string, backend string, onlyList bool) error { + var command []string + if onlyList { + command = []string{"/dbg", "backends", "list"} + } else if backend != "" { + command = []string{"/dbg", "backends", "get", backend} + } else { + command = []string{"/dbg", "backends", "all"} + } + + pod, err := request.ChoosePod(flags, podName, deployment) + if err != nil { + return err + } + + out, err := kubectl.PodExecString(flags, &pod, command) + if err != nil { + return err + } + + fmt.Printf(out) + return nil +} diff --git a/cmd/plugin/commands/certs/certs.go b/cmd/plugin/commands/certs/certs.go new file mode 100644 index 0000000000..0588b64187 --- /dev/null +++ b/cmd/plugin/commands/certs/certs.go @@ -0,0 +1,70 @@ +/* +Copyright 2019 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 certs + +import ( + "fmt" + "github.com/spf13/cobra" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "k8s.io/ingress-nginx/cmd/plugin/kubectl" + "k8s.io/ingress-nginx/cmd/plugin/request" + "k8s.io/ingress-nginx/cmd/plugin/util" +) + +// CreateCommand creates and returns this cobra subcommand +func CreateCommand(flags *genericclioptions.ConfigFlags) *cobra.Command { + var pod, deployment *string + cmd := &cobra.Command{ + Use: "certs", + Short: "Output the certificate data stored in an ingress-nginx pod", + RunE: func(cmd *cobra.Command, args []string) error { + host, err := cmd.Flags().GetString("host") + if err != nil { + return err + } + + util.PrintError(certs(flags, *pod, *deployment, host)) + return nil + }, + } + + cmd.Flags().String("host", "", "Get the cert for this hostname") + cobra.MarkFlagRequired(cmd.Flags(), "host") + pod = util.AddPodFlag(cmd) + deployment = util.AddDeploymentFlag(cmd) + + return cmd +} + +func certs(flags *genericclioptions.ConfigFlags, podName string, deployment string, host string) error { + command := []string{"/dbg", "certs", "get", host} + + pod, err := request.ChoosePod(flags, podName, deployment) + if err != nil { + return err + } + + out, err := kubectl.PodExecString(flags, &pod, command) + if err != nil { + return err + } + + fmt.Print(out) + return nil +} diff --git a/cmd/plugin/commands/conf/conf.go b/cmd/plugin/commands/conf/conf.go new file mode 100644 index 0000000000..18d6a7e203 --- /dev/null +++ b/cmd/plugin/commands/conf/conf.go @@ -0,0 +1,78 @@ +/* +Copyright 2019 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 conf + +import ( + "fmt" + "github.com/spf13/cobra" + "strings" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "k8s.io/ingress-nginx/cmd/plugin/kubectl" + "k8s.io/ingress-nginx/cmd/plugin/request" + "k8s.io/ingress-nginx/cmd/plugin/util" + "k8s.io/ingress-nginx/internal/nginx" +) + +// CreateCommand creates and returns this cobra subcommand +func CreateCommand(flags *genericclioptions.ConfigFlags) *cobra.Command { + var pod, deployment *string + cmd := &cobra.Command{ + Use: "conf", + Short: "Inspect the generated nginx.conf", + RunE: func(cmd *cobra.Command, args []string) error { + host, err := cmd.Flags().GetString("host") + if err != nil { + return err + } + + util.PrintError(conf(flags, host, *pod, *deployment)) + return nil + }, + } + cmd.Flags().String("host", "", "Print just the server block with this hostname") + pod = util.AddPodFlag(cmd) + deployment = util.AddDeploymentFlag(cmd) + + return cmd +} + +func conf(flags *genericclioptions.ConfigFlags, host string, podName string, deployment string) error { + pod, err := request.ChoosePod(flags, podName, deployment) + if err != nil { + return err + } + + nginxConf, err := kubectl.PodExecString(flags, &pod, []string{"/dbg", "conf"}) + if err != nil { + return err + } + + if host != "" { + block, err := nginx.GetServerBlock(nginxConf, host) + if err != nil { + return err + } + + fmt.Println(strings.TrimRight(strings.Trim(block, " \n"), " \n\t")) + } else { + fmt.Print(nginxConf) + } + + return nil +} diff --git a/cmd/plugin/commands/exec/exec.go b/cmd/plugin/commands/exec/exec.go new file mode 100644 index 0000000000..05f056d2e1 --- /dev/null +++ b/cmd/plugin/commands/exec/exec.go @@ -0,0 +1,72 @@ +/* +Copyright 2019 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 exec + +import ( + "github.com/spf13/cobra" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "k8s.io/ingress-nginx/cmd/plugin/kubectl" + "k8s.io/ingress-nginx/cmd/plugin/request" + "k8s.io/ingress-nginx/cmd/plugin/util" +) + +// CreateCommand creates and returns this cobra subcommand +func CreateCommand(flags *genericclioptions.ConfigFlags) *cobra.Command { + opts := execFlags{} + var pod, deployment *string + + cmd := &cobra.Command{ + Use: "exec", + Short: "Execute a command inside an ingress-nginx pod", + RunE: func(cmd *cobra.Command, args []string) error { + util.PrintError(exec(flags, *pod, *deployment, args, opts)) + return nil + }, + } + pod = util.AddPodFlag(cmd) + deployment = util.AddDeploymentFlag(cmd) + cmd.Flags().BoolVarP(&opts.TTY, "tty", "t", false, "Stdin is a TTY") + cmd.Flags().BoolVarP(&opts.Stdin, "stdin", "i", false, "Pass stdin to the container") + + return cmd +} + +type execFlags struct { + TTY bool + Stdin bool +} + +func exec(flags *genericclioptions.ConfigFlags, podName string, deployment string, cmd []string, opts execFlags) error { + pod, err := request.ChoosePod(flags, podName, deployment) + if err != nil { + return err + } + + args := []string{"exec"} + if opts.TTY { + args = append(args, "-t") + } + if opts.Stdin { + args = append(args, "-i") + } + + args = append(args, []string{"-n", pod.Namespace, pod.Name, "--"}...) + args = append(args, cmd...) + return kubectl.Exec(flags, args) +} diff --git a/cmd/plugin/commands/general/general.go b/cmd/plugin/commands/general/general.go new file mode 100644 index 0000000000..4c6698e452 --- /dev/null +++ b/cmd/plugin/commands/general/general.go @@ -0,0 +1,60 @@ +/* +Copyright 2019 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 general + +import ( + "fmt" + "github.com/spf13/cobra" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "k8s.io/ingress-nginx/cmd/plugin/kubectl" + "k8s.io/ingress-nginx/cmd/plugin/request" + "k8s.io/ingress-nginx/cmd/plugin/util" +) + +// CreateCommand creates and returns this cobra subcommand +func CreateCommand(flags *genericclioptions.ConfigFlags) *cobra.Command { + var pod, deployment *string + cmd := &cobra.Command{ + Use: "general", + Short: "Inspect the other dynamic ingress-nginx information", + RunE: func(cmd *cobra.Command, args []string) error { + util.PrintError(general(flags, *pod, *deployment)) + return nil + }, + } + pod = util.AddPodFlag(cmd) + deployment = util.AddDeploymentFlag(cmd) + + return cmd +} + +func general(flags *genericclioptions.ConfigFlags, podName string, deployment string) error { + pod, err := request.ChoosePod(flags, podName, deployment) + if err != nil { + return err + } + + out, err := kubectl.PodExecString(flags, &pod, []string{"/dbg", "general"}) + if err != nil { + return err + } + + fmt.Print(out) + return nil +} diff --git a/cmd/plugin/commands/info/info.go b/cmd/plugin/commands/info/info.go new file mode 100644 index 0000000000..49005414f6 --- /dev/null +++ b/cmd/plugin/commands/info/info.go @@ -0,0 +1,58 @@ +/* +Copyright 2019 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 info + +import ( + "fmt" + "github.com/spf13/cobra" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "k8s.io/ingress-nginx/cmd/plugin/request" + "k8s.io/ingress-nginx/cmd/plugin/util" +) + +// CreateCommand creates and returns this cobra subcommand +func CreateCommand(flags *genericclioptions.ConfigFlags) *cobra.Command { + cmd := &cobra.Command{ + Use: "info", + Short: "Show information about the ingress-nginx service", + RunE: func(cmd *cobra.Command, args []string) error { + service, err := cmd.Flags().GetString("service") + if err != nil { + return err + } + + util.PrintError(info(flags, service)) + return nil + }, + } + + cmd.Flags().String("service", util.DefaultIngressServiceName, "The name of the ingress-nginx service") + return cmd +} + +func info(flags *genericclioptions.ConfigFlags, serviceName string) error { + service, err := request.GetServiceByName(flags, serviceName, nil) + if err != nil { + return err + } + + fmt.Printf("Service cluster IP address: %v\n", service.Spec.ClusterIP) + fmt.Printf("LoadBalancer IP|CNAME: %v\n", service.Spec.LoadBalancerIP) + return nil +} diff --git a/cmd/plugin/commands/ingresses/ingresses.go b/cmd/plugin/commands/ingresses/ingresses.go new file mode 100644 index 0000000000..da110b28c2 --- /dev/null +++ b/cmd/plugin/commands/ingresses/ingresses.go @@ -0,0 +1,217 @@ +/* +Copyright 2019 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 ingresses + +import ( + "fmt" + "github.com/spf13/cobra" + "os" + "text/tabwriter" + + "k8s.io/api/extensions/v1beta1" + "k8s.io/cli-runtime/pkg/genericclioptions" + + "k8s.io/ingress-nginx/cmd/plugin/request" + "k8s.io/ingress-nginx/cmd/plugin/util" +) + +// CreateCommand creates and returns this cobra subcommand +func CreateCommand(flags *genericclioptions.ConfigFlags) *cobra.Command { + cmd := &cobra.Command{ + Use: "ingresses", + Aliases: []string{"ingress", "ing"}, + Short: "Provide a short summary of all of the ingress definitions", + RunE: func(cmd *cobra.Command, args []string) error { + host, err := cmd.Flags().GetString("host") + if err != nil { + return err + } + + allNamespaces, err := cmd.Flags().GetBool("all-namespaces") + if err != nil { + return err + } + + util.PrintError(ingresses(flags, host, allNamespaces)) + return nil + }, + } + cmd.Flags().String("host", "", "Show just the ingress definitions for this hostname") + cmd.Flags().Bool("all-namespaces", false, "Find ingress definitions from all namespaces") + + return cmd +} + +func ingresses(flags *genericclioptions.ConfigFlags, host string, allNamespaces bool) error { + var namespace string + if allNamespaces { + namespace = "" + } else { + namespace = util.GetNamespace(flags) + } + + ingresses, err := request.GetIngressDefinitions(flags, namespace) + if err != nil { + return err + } + + rows := getIngressRows(&ingresses) + + if host != "" { + rowsWithHost := make([]ingressRow, 0) + for _, row := range rows { + if row.Host == host { + rowsWithHost = append(rowsWithHost, row) + } + } + rows = rowsWithHost + } + + printer := tabwriter.NewWriter(os.Stdout, 6, 4, 3, ' ', 0) + defer printer.Flush() + + if allNamespaces { + fmt.Fprintln(printer, "NAMESPACE\tINGRESS NAME\tHOST+PATH\tADDRESSES\tTLS\tSERVICE\tSERVICE PORT\tENDPOINTS") + } else { + fmt.Fprintln(printer, "INGRESS NAME\tHOST+PATH\tADDRESSES\tTLS\tSERVICE\tSERVICE PORT\tENDPOINTS") + } + + for _, row := range rows { + var tlsMsg string + if row.TLS { + tlsMsg = "YES" + } else { + tlsMsg = "NO" + } + + numEndpoints, err := request.GetNumEndpoints(flags, row.Namespace, row.ServiceName) + if err != nil { + return err + } + if numEndpoints == nil { + row.NumEndpoints = "N/A" + } else { + row.NumEndpoints = fmt.Sprint(*numEndpoints) + } + + if allNamespaces { + fmt.Fprintf(printer, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", row.Namespace, row.IngressName, row.Host+row.Path, row.Address, tlsMsg, row.ServiceName, row.ServicePort, row.NumEndpoints) + } else { + fmt.Fprintf(printer, "%v\t%v\t%v\t%v\t%v\t%v\t%v\n", row.IngressName, row.Host+row.Path, row.Address, tlsMsg, row.ServiceName, row.ServicePort, row.NumEndpoints) + } + } + + return nil +} + +type ingressRow struct { + Namespace string + IngressName string + Host string + Path string + TLS bool + ServiceName string + ServicePort string + Address string + NumEndpoints string +} + +func getIngressRows(ingresses *[]v1beta1.Ingress) []ingressRow { + rows := make([]ingressRow, 0) + + for _, ing := range *ingresses { + + address := "" + for _, lbIng := range ing.Status.LoadBalancer.Ingress { + if len(lbIng.IP) > 0 { + address = address + lbIng.IP + "," + } + if len(lbIng.Hostname) > 0 { + address = address + lbIng.Hostname + "," + } + } + if len(address) > 0 { + address = address[:len(address)-1] + } + + tlsHosts := make(map[string]struct{}) + for _, tls := range ing.Spec.TLS { + for _, host := range tls.Hosts { + tlsHosts[host] = struct{}{} + } + } + + defaultBackendService := "" + defaultBackendPort := "" + if ing.Spec.Backend != nil { + defaultBackendService = ing.Spec.Backend.ServiceName + defaultBackendPort = ing.Spec.Backend.ServicePort.String() + } + + // Handle catch-all ingress + if len(ing.Spec.Rules) == 0 && len(defaultBackendService) > 0 { + row := ingressRow{ + Namespace: ing.Namespace, + IngressName: ing.Name, + Host: "*", + ServiceName: defaultBackendService, + ServicePort: defaultBackendPort, + Address: address, + } + + rows = append(rows, row) + continue + } + + for _, rule := range ing.Spec.Rules { + _, hasTLS := tlsHosts[rule.Host] + + //Handle ingress with no paths + if rule.HTTP == nil { + row := ingressRow{ + Namespace: ing.Namespace, + IngressName: ing.Name, + Host: rule.Host, + Path: "", + TLS: hasTLS, + ServiceName: defaultBackendService, + ServicePort: defaultBackendPort, + Address: address, + } + rows = append(rows, row) + continue + } + + for _, path := range rule.HTTP.Paths { + row := ingressRow{ + Namespace: ing.Namespace, + IngressName: ing.Name, + Host: rule.Host, + Path: path.Path, + TLS: hasTLS, + ServiceName: path.Backend.ServiceName, + ServicePort: path.Backend.ServicePort.String(), + Address: address, + } + + rows = append(rows, row) + } + } + } + + return rows +} diff --git a/cmd/plugin/commands/logs/logs.go b/cmd/plugin/commands/logs/logs.go new file mode 100644 index 0000000000..86160a142d --- /dev/null +++ b/cmd/plugin/commands/logs/logs.go @@ -0,0 +1,108 @@ +/* +Copyright 2019 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 logs + +import ( + "fmt" + "github.com/spf13/cobra" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "k8s.io/ingress-nginx/cmd/plugin/kubectl" + "k8s.io/ingress-nginx/cmd/plugin/request" + "k8s.io/ingress-nginx/cmd/plugin/util" +) + +// CreateCommand creates and returns this cobra subcommand +func CreateCommand(flags *genericclioptions.ConfigFlags) *cobra.Command { + o := logsFlags{} + var pod, deployment *string + + cmd := &cobra.Command{ + Use: "logs", + Short: "Get the kubernetes logs for an ingress-nginx pod", + RunE: func(cmd *cobra.Command, args []string) error { + util.PrintError(logs(flags, *pod, *deployment, o)) + return nil + }, + } + pod = util.AddPodFlag(cmd) + deployment = util.AddDeploymentFlag(cmd) + + cmd.Flags().BoolVarP(&o.Follow, "follow", "f", o.Follow, "Specify if the logs should be streamed.") + cmd.Flags().BoolVar(&o.Timestamps, "timestamps", o.Timestamps, "Include timestamps on each line in the log output") + cmd.Flags().Int64Var(&o.LimitBytes, "limit-bytes", o.LimitBytes, "Maximum bytes of logs to return. Defaults to no limit.") + cmd.Flags().BoolVarP(&o.Previous, "previous", "p", o.Previous, "If true, print the logs for the previous instance of the container in a pod if it exists.") + cmd.Flags().Int64Var(&o.Tail, "tail", o.Tail, "Lines of recent log file to display. Defaults to -1 with no selector, showing all log lines otherwise 10, if a selector is provided.") + cmd.Flags().StringVar(&o.SinceTime, "since-time", o.SinceTime, "Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.") + cmd.Flags().StringVar(&o.SinceSeconds, "since", o.SinceSeconds, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.") + cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on.") + + return cmd +} + +type logsFlags struct { + SinceTime string + SinceSeconds string + Follow bool + Previous bool + Timestamps bool + LimitBytes int64 + Tail int64 + Selector string +} + +func (o *logsFlags) toStrings() []string { + r := []string{} + if o.SinceTime != "" { + r = append(r, "--since-time", o.SinceTime) + } + if o.SinceSeconds != "" { + r = append(r, "--since", o.SinceSeconds) + } + if o.Follow { + r = append(r, "--follow") + } + if o.Previous { + r = append(r, "--previous") + } + if o.Timestamps { + r = append(r, "--timestamps") + } + if o.LimitBytes != 0 { + r = append(r, "--limit-bytes", fmt.Sprintf("%v", o.LimitBytes)) + } + if o.Tail != 0 { + r = append(r, "--tail", fmt.Sprintf("%v", o.Tail)) + } + if o.Selector != "" { + r = append(r, "--selector", o.Selector) + } + + return r +} + +func logs(flags *genericclioptions.ConfigFlags, podName string, deployment string, opts logsFlags) error { + pod, err := request.ChoosePod(flags, podName, deployment) + if err != nil { + return err + } + + cmd := []string{"logs", "-n", pod.Namespace, pod.Name} + cmd = append(cmd, opts.toStrings()...) + return kubectl.Exec(flags, cmd) +} diff --git a/cmd/plugin/commands/ssh/ssh.go b/cmd/plugin/commands/ssh/ssh.go new file mode 100644 index 0000000000..77fcd9940c --- /dev/null +++ b/cmd/plugin/commands/ssh/ssh.go @@ -0,0 +1,53 @@ +/* +Copyright 2019 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 ssh + +import ( + "github.com/spf13/cobra" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "k8s.io/ingress-nginx/cmd/plugin/kubectl" + "k8s.io/ingress-nginx/cmd/plugin/request" + "k8s.io/ingress-nginx/cmd/plugin/util" +) + +// CreateCommand creates and returns this cobra subcommand +func CreateCommand(flags *genericclioptions.ConfigFlags) *cobra.Command { + var pod, deployment *string + cmd := &cobra.Command{ + Use: "ssh", + Short: "ssh into a running ingress-nginx pod", + RunE: func(cmd *cobra.Command, args []string) error { + util.PrintError(ssh(flags, *pod, *deployment)) + return nil + }, + } + pod = util.AddPodFlag(cmd) + deployment = util.AddDeploymentFlag(cmd) + + return cmd +} + +func ssh(flags *genericclioptions.ConfigFlags, podName string, deployment string) error { + pod, err := request.ChoosePod(flags, podName, deployment) + if err != nil { + return err + } + + return kubectl.Exec(flags, []string{"exec", "-it", "-n", pod.Namespace, pod.Name, "--", "/bin/bash"}) +} diff --git a/cmd/plugin/kubectl/kubectl.go b/cmd/plugin/kubectl/kubectl.go new file mode 100644 index 0000000000..0fdeeef8f2 --- /dev/null +++ b/cmd/plugin/kubectl/kubectl.go @@ -0,0 +1,132 @@ +/* +Copyright 2019 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 kubectl + +import ( + "bytes" + "fmt" + "io" + apiv1 "k8s.io/api/core/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "os" + "os/exec" + "strings" + "syscall" +) + +// PodExecString takes a pod and a command, uses kubectl exec to run the command in the pod +// and returns stdout as a string +func PodExecString(flags *genericclioptions.ConfigFlags, pod *apiv1.Pod, args []string) (string, error) { + args = append([]string{"exec", "-n", pod.Namespace, pod.Name}, args...) + return ExecToString(flags, args) +} + +// ExecToString runs a kubectl subcommand and returns stdout as a string +func ExecToString(flags *genericclioptions.ConfigFlags, args []string) (string, error) { + kArgs := getKubectlConfigFlags(flags) + kArgs = append(kArgs, args...) + + buf := bytes.NewBuffer(make([]byte, 0)) + err := execToWriter(append([]string{"kubectl"}, kArgs...), buf) + if err != nil { + return "", err + } + return buf.String(), nil +} + +// Exec replaces the current process with a kubectl invocation +func Exec(flags *genericclioptions.ConfigFlags, args []string) error { + kArgs := getKubectlConfigFlags(flags) + kArgs = append(kArgs, args...) + return execCommand(append([]string{"kubectl"}, kArgs...)) +} + +// Replaces the currently running process with the given command +func execCommand(args []string) error { + path, err := exec.LookPath(args[0]) + if err != nil { + return err + } + args[0] = path + + env := os.Environ() + return syscall.Exec(path, args, env) +} + +// Runs a command and returns stdout +func execToWriter(args []string, writer io.Writer) error { + cmd := exec.Command(args[0], args[1:]...) + + op, err := cmd.StdoutPipe() + if err != nil { + return err + } + + go io.Copy(writer, op) + err = cmd.Run() + if err != nil { + return err + } + + return nil +} + +// getKubectlConfigFlags serializes the parsed flag struct back into a series of command line args +// that can then be passed to kubectl. The mirror image of +// https://github.com/kubernetes/cli-runtime/blob/master/pkg/genericclioptions/config_flags.go#L251 +func getKubectlConfigFlags(flags *genericclioptions.ConfigFlags) []string { + out := []string{} + o := &out + + appendStringFlag(o, flags.KubeConfig, "kubeconfig") + appendStringFlag(o, flags.CacheDir, "cache-dir") + appendStringFlag(o, flags.CertFile, "client-certificate") + appendStringFlag(o, flags.KeyFile, "client-key") + appendStringFlag(o, flags.BearerToken, "token") + appendStringFlag(o, flags.Impersonate, "as") + appendStringArrayFlag(o, flags.ImpersonateGroup, "as-group") + appendStringFlag(o, flags.Username, "username") + appendStringFlag(o, flags.Password, "password") + appendStringFlag(o, flags.ClusterName, "cluster") + appendStringFlag(o, flags.AuthInfoName, "user") + //appendStringFlag(o, flags.Namespace, "namespace") + appendStringFlag(o, flags.Context, "context") + appendStringFlag(o, flags.APIServer, "server") + appendBoolFlag(o, flags.Insecure, "insecure-skip-tls-verify") + appendStringFlag(o, flags.CAFile, "certificate-authority") + appendStringFlag(o, flags.Timeout, "request-timeout") + + return out +} + +func appendStringFlag(out *[]string, in *string, flag string) { + if in != nil && *in != "" { + *out = append(*out, fmt.Sprintf("--%v=%v", flag, *in)) + } +} + +func appendBoolFlag(out *[]string, in *bool, flag string) { + if in != nil { + *out = append(*out, fmt.Sprintf("--%v=%v", flag, *in)) + } +} + +func appendStringArrayFlag(out *[]string, in *[]string, flag string) { + if in != nil && len(*in) > 0 { + *out = append(*out, fmt.Sprintf("--%v=%v'", flag, strings.Join(*in, ","))) + } +} diff --git a/cmd/plugin/main.go b/cmd/plugin/main.go index b5e851cb35..49a339fcce 100644 --- a/cmd/plugin/main.go +++ b/cmd/plugin/main.go @@ -20,18 +20,22 @@ import ( "fmt" "github.com/spf13/cobra" "os" - "strings" - "text/tabwriter" - "k8s.io/api/extensions/v1beta1" "k8s.io/cli-runtime/pkg/genericclioptions" //Just importing this is supposed to allow cloud authentication // eg GCP, AWS, Azure ... _ "k8s.io/client-go/plugin/pkg/client/auth" - "k8s.io/ingress-nginx/cmd/plugin/request" - "k8s.io/ingress-nginx/cmd/plugin/util" - "k8s.io/ingress-nginx/internal/nginx" + + "k8s.io/ingress-nginx/cmd/plugin/commands/backends" + "k8s.io/ingress-nginx/cmd/plugin/commands/certs" + "k8s.io/ingress-nginx/cmd/plugin/commands/conf" + "k8s.io/ingress-nginx/cmd/plugin/commands/exec" + "k8s.io/ingress-nginx/cmd/plugin/commands/general" + "k8s.io/ingress-nginx/cmd/plugin/commands/info" + "k8s.io/ingress-nginx/cmd/plugin/commands/ingresses" + "k8s.io/ingress-nginx/cmd/plugin/commands/logs" + "k8s.io/ingress-nginx/cmd/plugin/commands/ssh" ) func main() { @@ -44,373 +48,18 @@ func main() { flags := genericclioptions.NewConfigFlags() flags.AddFlags(rootCmd.PersistentFlags()) - ingCmd := &cobra.Command{ - Use: "ingresses", - Aliases: []string{"ingress", "ing"}, - Short: "Provide a short summary of all of the ingress definitions", - RunE: func(cmd *cobra.Command, args []string) error { - host, err := cmd.Flags().GetString("host") - if err != nil { - return err - } - - allNamespaces, err := cmd.Flags().GetBool("all-namespaces") - if err != nil { - return err - } - - util.PrintError(ingresses(flags, host, allNamespaces)) - return nil - }, - } - ingCmd.Flags().String("host", "", "Show just the ingress definitions for this hostname") - ingCmd.Flags().Bool("all-namespaces", false, "Find ingress definitions from all namespaces") - rootCmd.AddCommand(ingCmd) - - confCmd := &cobra.Command{ - Use: "conf", - Short: "Inspect the generated nginx.conf", - RunE: func(cmd *cobra.Command, args []string) error { - host, err := cmd.Flags().GetString("host") - if err != nil { - return err - } - - pod, err := cmd.Flags().GetString("pod") - if err != nil { - return err - } - - util.PrintError(conf(flags, host, pod)) - return nil - }, - } - confCmd.Flags().String("host", "", "Print just the server block with this hostname") - confCmd.Flags().String("pod", "", "Query a particular ingress-nginx pod") - rootCmd.AddCommand(confCmd) - - generalCmd := &cobra.Command{ - Use: "general", - Short: "Inspect the other dynamic ingress-nginx information", - RunE: func(cmd *cobra.Command, args []string) error { - pod, err := cmd.Flags().GetString("pod") - if err != nil { - return err - } - - util.PrintError(general(flags, pod)) - return nil - }, - } - generalCmd.Flags().String("pod", "", "Query a particular ingress-nginx pod") - rootCmd.AddCommand(generalCmd) - - infoCmd := &cobra.Command{ - Use: "info", - Short: "Show information about the ingress-nginx service", - RunE: func(cmd *cobra.Command, args []string) error { - util.PrintError(info(flags)) - return nil - }, - } - rootCmd.AddCommand(infoCmd) - - backendsCmd := &cobra.Command{ - Use: "backends", - Short: "Inspect the dynamic backend information of an ingress-nginx instance", - RunE: func(cmd *cobra.Command, args []string) error { - pod, err := cmd.Flags().GetString("pod") - if err != nil { - return err - } - backend, err := cmd.Flags().GetString("backend") - if err != nil { - return err - } - onlyList, err := cmd.Flags().GetBool("list") - if err != nil { - return err - } - if onlyList && backend != "" { - return fmt.Errorf("--list and --backend cannot both be specified") - } - - util.PrintError(backends(flags, pod, backend, onlyList)) - return nil - }, - } - backendsCmd.Flags().String("pod", "", "Query a particular ingress-nginx pod") - backendsCmd.Flags().String("backend", "", "Output only the information for the given backend") - backendsCmd.Flags().Bool("list", false, "Output a newline-separated list of backend names") - rootCmd.AddCommand(backendsCmd) - - certsCmd := &cobra.Command{ - Use: "certs", - Short: "Output the certificate data stored in an ingress-nginx pod", - RunE: func(cmd *cobra.Command, args []string) error { - pod, err := cmd.Flags().GetString("pod") - if err != nil { - return err - } - host, err := cmd.Flags().GetString("host") - if err != nil { - return err - } - - util.PrintError(certs(flags, pod, host)) - return nil - }, - } - certsCmd.Flags().String("host", "", "Get the cert for this hostname") - certsCmd.Flags().String("pod", "", "Query a particular ingress-nginx pod") - cobra.MarkFlagRequired(certsCmd.Flags(), "host") - rootCmd.AddCommand(certsCmd) + rootCmd.AddCommand(ingresses.CreateCommand(flags)) + rootCmd.AddCommand(conf.CreateCommand(flags)) + rootCmd.AddCommand(general.CreateCommand(flags)) + rootCmd.AddCommand(backends.CreateCommand(flags)) + rootCmd.AddCommand(info.CreateCommand(flags)) + rootCmd.AddCommand(certs.CreateCommand(flags)) + rootCmd.AddCommand(logs.CreateCommand(flags)) + rootCmd.AddCommand(exec.CreateCommand(flags)) + rootCmd.AddCommand(ssh.CreateCommand(flags)) if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } - -func certs(flags *genericclioptions.ConfigFlags, pod string, host string) error { - command := []string{"/dbg", "certs", "get", host} - var out string - var err error - if pod != "" { - out, err = request.NamedPodExec(flags, pod, command) - } else { - out, err = request.IngressPodExec(flags, command) - } - if err != nil { - return err - } - - fmt.Print(out) - return nil -} - -func info(flags *genericclioptions.ConfigFlags) error { - service, err := request.GetIngressService(flags) - if err != nil { - return err - } - - fmt.Printf("Service cluster IP address: %v\n", service.Spec.ClusterIP) - fmt.Printf("LoadBalancer IP|CNAME: %v\n", service.Spec.LoadBalancerIP) - return nil -} - -func backends(flags *genericclioptions.ConfigFlags, pod string, backend string, onlyList bool) error { - var command []string - if onlyList { - command = []string{"/dbg", "backends", "list"} - } else if backend != "" { - command = []string{"/dbg", "backends", "get", backend} - } else { - command = []string{"/dbg", "backends", "all"} - } - - var out string - var err error - if pod != "" { - out, err = request.NamedPodExec(flags, pod, command) - } else { - out, err = request.IngressPodExec(flags, command) - } - if err != nil { - return err - } - - fmt.Print(out) - return nil -} - -func general(flags *genericclioptions.ConfigFlags, pod string) error { - var general string - var err error - if pod != "" { - general, err = request.NamedPodExec(flags, pod, []string{"/dbg", "general"}) - } else { - general, err = request.IngressPodExec(flags, []string{"/dbg", "general"}) - } - if err != nil { - return err - } - - fmt.Print(general) - return nil -} - -func ingresses(flags *genericclioptions.ConfigFlags, host string, allNamespaces bool) error { - var namespace string - if allNamespaces { - namespace = "" - } else { - namespace = util.GetNamespace(flags) - } - - ingresses, err := request.GetIngressDefinitions(flags, namespace) - if err != nil { - return err - } - - rows := getIngressRows(&ingresses) - - if host != "" { - rowsWithHost := make([]ingressRow, 0) - for _, row := range rows { - if row.Host == host { - rowsWithHost = append(rowsWithHost, row) - } - } - rows = rowsWithHost - } - - printer := tabwriter.NewWriter(os.Stdout, 6, 4, 3, ' ', 0) - defer printer.Flush() - - if allNamespaces { - fmt.Fprintln(printer, "NAMESPACE\tINGRESS NAME\tHOST+PATH\tADDRESSES\tTLS\tSERVICE\tSERVICE PORT") - } else { - fmt.Fprintln(printer, "INGRESS NAME\tHOST+PATH\tADDRESSES\tTLS\tSERVICE\tSERVICE PORT") - } - - for _, row := range rows { - var tlsMsg string - if row.TLS { - tlsMsg = "YES" - } else { - tlsMsg = "NO" - } - if allNamespaces { - fmt.Fprintf(printer, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t\n", row.Namespace, row.IngressName, row.Host+row.Path, row.Address, tlsMsg, row.ServiceName, row.ServicePort) - } else { - fmt.Fprintf(printer, "%v\t%v\t%v\t%v\t%v\t%v\t\n", row.IngressName, row.Host+row.Path, row.Address, tlsMsg, row.ServiceName, row.ServicePort) - } - } - - return nil -} - -func conf(flags *genericclioptions.ConfigFlags, host string, pod string) error { - var nginxConf string - var err error - if pod != "" { - nginxConf, err = request.NamedPodExec(flags, pod, []string{"/dbg", "conf"}) - } else { - nginxConf, err = request.IngressPodExec(flags, []string{"/dbg", "conf"}) - } - if err != nil { - return err - } - - if host != "" { - block, err := nginx.GetServerBlock(nginxConf, host) - if err != nil { - return err - } - - fmt.Println(strings.TrimRight(strings.Trim(block, " \n"), " \n\t")) - } else { - fmt.Print(nginxConf) - } - - return nil -} - -type ingressRow struct { - Namespace string - IngressName string - Host string - Path string - TLS bool - ServiceName string - ServicePort string - Address string -} - -func getIngressRows(ingresses *[]v1beta1.Ingress) []ingressRow { - rows := make([]ingressRow, 0) - - for _, ing := range *ingresses { - - address := "" - for _, lbIng := range ing.Status.LoadBalancer.Ingress { - if len(lbIng.IP) > 0 { - address = address + lbIng.IP + "," - } - if len(lbIng.Hostname) > 0 { - address = address + lbIng.Hostname + "," - } - } - if len(address) > 0 { - address = address[:len(address)-1] - } - - tlsHosts := make(map[string]struct{}) - for _, tls := range ing.Spec.TLS { - for _, host := range tls.Hosts { - tlsHosts[host] = struct{}{} - } - } - - defaultBackendService := "" - defaultBackendPort := "" - if ing.Spec.Backend != nil { - defaultBackendService = ing.Spec.Backend.ServiceName - defaultBackendPort = ing.Spec.Backend.ServicePort.String() - } - - // Handle catch-all ingress - if len(ing.Spec.Rules) == 0 && len(defaultBackendService) > 0 { - row := ingressRow{ - Namespace: ing.Namespace, - IngressName: ing.Name, - Host: "*", - ServiceName: defaultBackendService, - ServicePort: defaultBackendPort, - Address: address, - } - - rows = append(rows, row) - continue - } - - for _, rule := range ing.Spec.Rules { - _, hasTLS := tlsHosts[rule.Host] - - //Handle ingress with no paths - if rule.HTTP == nil { - row := ingressRow{ - Namespace: ing.Namespace, - IngressName: ing.Name, - Host: rule.Host, - Path: "", - TLS: hasTLS, - ServiceName: defaultBackendService, - ServicePort: defaultBackendPort, - Address: address, - } - rows = append(rows, row) - continue - } - - for _, path := range rule.HTTP.Paths { - row := ingressRow{ - Namespace: ing.Namespace, - IngressName: ing.Name, - Host: rule.Host, - Path: path.Path, - TLS: hasTLS, - ServiceName: path.Backend.ServiceName, - ServicePort: path.Backend.ServicePort.String(), - Address: address, - } - - rows = append(rows, row) - } - } - } - - return rows -} diff --git a/cmd/plugin/request/request.go b/cmd/plugin/request/request.go index 4d1145ee8e..25561dc234 100644 --- a/cmd/plugin/request/request.go +++ b/cmd/plugin/request/request.go @@ -17,173 +17,225 @@ limitations under the License. package request import ( - "bytes" "fmt" apiv1 "k8s.io/api/core/v1" "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/kubernetes/scheme" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" extensions "k8s.io/client-go/kubernetes/typed/extensions/v1beta1" - "k8s.io/client-go/tools/remotecommand" "k8s.io/ingress-nginx/cmd/plugin/util" ) -const ( - ingressPodName = "nginx-ingress-controller" - ingressServiceName = "ingress-nginx" -) +// ChoosePod finds a pod either by deployment or by name +func ChoosePod(flags *genericclioptions.ConfigFlags, podName string, deployment string) (apiv1.Pod, error) { + if podName != "" { + return GetNamedPod(flags, podName) + } -// NamedPodExec finds a pod with the given name, executes a command inside it, and returns stdout -func NamedPodExec(flags *genericclioptions.ConfigFlags, podName string, cmd []string) (string, error) { + return GetDeploymentPod(flags, deployment) +} + +// GetNamedPod finds a pod with the given name +func GetNamedPod(flags *genericclioptions.ConfigFlags, name string) (apiv1.Pod, error) { allPods, err := getPods(flags) if err != nil { - return "", err + return apiv1.Pod{}, err } for _, pod := range allPods { - if pod.Name == podName { - return podExec(flags, &pod, cmd) + if pod.Name == name { + return pod, nil } } - return "", fmt.Errorf("Pod %v not found in namespace %v", podName, util.GetNamespace(flags)) + return apiv1.Pod{}, fmt.Errorf("Pod %v not found in namespace %v", name, util.GetNamespace(flags)) } -// IngressPodExec finds an ingress-nginx pod in the given namespace, executes a command inside it, and returns stdout -func IngressPodExec(flags *genericclioptions.ConfigFlags, cmd []string) (string, error) { - ings, err := getIngressPods(flags) +// GetDeploymentPod finds a pod from a given deployment +func GetDeploymentPod(flags *genericclioptions.ConfigFlags, deployment string) (apiv1.Pod, error) { + ings, err := getDeploymentPods(flags, deployment) if err != nil { - return "", err + return apiv1.Pod{}, err } if len(ings) == 0 { - return "", fmt.Errorf("No ingress-nginx pods found in namespace %v", util.GetNamespace(flags)) + return apiv1.Pod{}, fmt.Errorf("No pods for deployment %v found in namespace %v", deployment, util.GetNamespace(flags)) } - return podExec(flags, &ings[0], cmd) + return ings[0], nil } -func podExec(flags *genericclioptions.ConfigFlags, pod *apiv1.Pod, cmd []string) (string, error) { - config, err := flags.ToRESTConfig() +// GetIngressDefinitions returns an array of Ingress resource definitions +func GetIngressDefinitions(flags *genericclioptions.ConfigFlags, namespace string) ([]v1beta1.Ingress, error) { + rawConfig, err := flags.ToRESTConfig() if err != nil { - return "", err + return make([]v1beta1.Ingress, 0), err } - client, err := corev1.NewForConfig(config) + api, err := extensions.NewForConfig(rawConfig) if err != nil { - return "", err + return make([]v1beta1.Ingress, 0), err } - namespace, _, err := flags.ToRawKubeConfigLoader().Namespace() + pods, err := api.Ingresses(namespace).List(metav1.ListOptions{}) if err != nil { - return "", err + return make([]v1beta1.Ingress, 0), err } - restClient := client.RESTClient() - - req := restClient.Post(). - Resource("pods"). - Name(pod.Name). - Namespace(namespace). - SubResource("exec"). - Param("container", ingressPodName) - - req.VersionedParams(&apiv1.PodExecOptions{ - Container: ingressPodName, - Command: cmd, - Stdin: false, - Stdout: true, - Stderr: false, - TTY: false, - }, scheme.ParameterCodec) - - exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) + return pods.Items, nil +} +// GetNumEndpoints counts the number of endpoints for the service with the given name +func GetNumEndpoints(flags *genericclioptions.ConfigFlags, namespace string, serviceName string) (*int, error) { + endpoints, err := GetEndpointsByName(flags, namespace, serviceName) if err != nil { - return "", err + return nil, err } - stdout := bytes.NewBuffer(make([]byte, 0)) - err = exec.Stream(remotecommand.StreamOptions{ - Stdout: stdout, - }) + if endpoints == nil { + return nil, nil + } - return stdout.String(), err + ret := 0 + for _, subset := range endpoints.Subsets { + ret += len(subset.Addresses) + } + return &ret, nil } -func getIngressPods(flags *genericclioptions.ConfigFlags) ([]apiv1.Pod, error) { - pods, err := getPods(flags) +// GetEndpointsByName returns the endpoints for the service with the given name +func GetEndpointsByName(flags *genericclioptions.ConfigFlags, namespace string, name string) (*apiv1.Endpoints, error) { + allEndpoints, err := getEndpoints(flags, namespace) if err != nil { - return make([]apiv1.Pod, 0), err + return nil, err } - ingressPods := make([]apiv1.Pod, 0) - for _, pod := range pods { - if pod.Spec.Containers[0].Name == ingressPodName { - ingressPods = append(ingressPods, pod) + for _, endpoints := range allEndpoints { + if endpoints.Name == name { + return &endpoints, nil } } - return ingressPods, nil + return nil, nil } -func getPods(flags *genericclioptions.ConfigFlags) ([]apiv1.Pod, error) { - namespace := util.GetNamespace(flags) +var endpointsCache = make(map[string]*[]apiv1.Endpoints) + +func getEndpoints(flags *genericclioptions.ConfigFlags, namespace string) ([]apiv1.Endpoints, error) { + cachedEndpoints, ok := endpointsCache[namespace] + if ok { + return *cachedEndpoints, nil + } + + if namespace != "" { + tryAllNamespacesEndpointsCache(flags) + } + + cachedEndpoints = tryFilteringEndpointsFromAllNamespacesCache(flags, namespace) + if cachedEndpoints != nil { + return *cachedEndpoints, nil + } rawConfig, err := flags.ToRESTConfig() if err != nil { - return make([]apiv1.Pod, 0), err + return nil, err } api, err := corev1.NewForConfig(rawConfig) if err != nil { - return make([]apiv1.Pod, 0), err + return nil, err } - pods, err := api.Pods(namespace).List(metav1.ListOptions{}) + endpointsList, err := api.Endpoints(namespace).List(metav1.ListOptions{}) if err != nil { - return make([]apiv1.Pod, 0), err + return nil, err } + endpoints := endpointsList.Items - return pods.Items, nil + endpointsCache[namespace] = &endpoints + return endpoints, nil } -// GetIngressDefinitions returns an array of Ingress resource definitions -func GetIngressDefinitions(flags *genericclioptions.ConfigFlags, namespace string) ([]v1beta1.Ingress, error) { +func tryAllNamespacesEndpointsCache(flags *genericclioptions.ConfigFlags) { + _, ok := endpointsCache[""] + if !ok { + _, err := getEndpoints(flags, "") + if err != nil { + endpointsCache[""] = nil + } + } +} + +func tryFilteringEndpointsFromAllNamespacesCache(flags *genericclioptions.ConfigFlags, namespace string) *[]apiv1.Endpoints { + allEndpoints, _ := endpointsCache[""] + if allEndpoints != nil { + endpoints := make([]apiv1.Endpoints, 0) + for _, thisEndpoints := range *allEndpoints { + if thisEndpoints.Namespace == namespace { + endpoints = append(endpoints, thisEndpoints) + } + } + endpointsCache[namespace] = &endpoints + return &endpoints + } + return nil +} + +// GetServiceByName finds and returns the service definition with the given name +func GetServiceByName(flags *genericclioptions.ConfigFlags, name string, services *[]apiv1.Service) (apiv1.Service, error) { + if services == nil { + servicesArray, err := getServices(flags) + if err != nil { + return apiv1.Service{}, err + } + services = &servicesArray + } + + for _, svc := range *services { + if svc.Name == name { + return svc, nil + } + } + + return apiv1.Service{}, fmt.Errorf("Could not find service %v in namespace %v", name, util.GetNamespace(flags)) +} + +func getPods(flags *genericclioptions.ConfigFlags) ([]apiv1.Pod, error) { + namespace := util.GetNamespace(flags) + rawConfig, err := flags.ToRESTConfig() if err != nil { - return make([]v1beta1.Ingress, 0), err + return make([]apiv1.Pod, 0), err } - api, err := extensions.NewForConfig(rawConfig) + api, err := corev1.NewForConfig(rawConfig) if err != nil { - return make([]v1beta1.Ingress, 0), err + return make([]apiv1.Pod, 0), err } - pods, err := api.Ingresses(namespace).List(metav1.ListOptions{}) + pods, err := api.Pods(namespace).List(metav1.ListOptions{}) if err != nil { - return make([]v1beta1.Ingress, 0), err + return make([]apiv1.Pod, 0), err } return pods.Items, nil } -// GetIngressService finds and returns the ingress-nginx service definition -func GetIngressService(flags *genericclioptions.ConfigFlags) (apiv1.Service, error) { - services, err := getServices(flags) +func getDeploymentPods(flags *genericclioptions.ConfigFlags, deployment string) ([]apiv1.Pod, error) { + pods, err := getPods(flags) if err != nil { - return apiv1.Service{}, err + return make([]apiv1.Pod, 0), err } - for _, svc := range services { - if svc.Name == ingressServiceName { - return svc, nil + ingressPods := make([]apiv1.Pod, 0) + for _, pod := range pods { + if pod.Spec.Containers[0].Name == deployment { + ingressPods = append(ingressPods, pod) } } - return apiv1.Service{}, fmt.Errorf("Could not find service %v in namespace %v", ingressServiceName, util.GetNamespace(flags)) + return ingressPods, nil } func getServices(flags *genericclioptions.ConfigFlags) ([]apiv1.Service, error) { diff --git a/cmd/plugin/util/util.go b/cmd/plugin/util/util.go index 3293fbf02e..8a0753e9f6 100644 --- a/cmd/plugin/util/util.go +++ b/cmd/plugin/util/util.go @@ -18,11 +18,17 @@ package util import ( "fmt" - + "github.com/spf13/cobra" apiv1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericclioptions" ) +// The default deployment and service names for ingress-nginx +const ( + DefaultIngressDeploymentName = "nginx-ingress-controller" + DefaultIngressServiceName = "ingress-nginx" +) + // PrintError receives an error value and prints it if it exists func PrintError(e error) { if e != nil { @@ -45,6 +51,20 @@ func printOrError(s string, e error) error { return nil } +// AddPodFlag adds a --pod flag to a cobra command +func AddPodFlag(cmd *cobra.Command) *string { + v := "" + cmd.Flags().StringVar(&v, "pod", "", "Query a particular ingress-nginx pod") + return &v +} + +// AddDeploymentFlag adds a --deployment flag to a cobra command +func AddDeploymentFlag(cmd *cobra.Command) *string { + v := "" + cmd.Flags().StringVar(&v, "deployment", DefaultIngressDeploymentName, "The name of the ingress-nginx deployment") + return &v +} + // GetNamespace takes a set of kubectl flag values and returns the namespace we should be operating in func GetNamespace(flags *genericclioptions.ConfigFlags) string { namespace, _, err := flags.ToRawKubeConfigLoader().Namespace() diff --git a/internal/nginx/main.go b/internal/nginx/main.go index 703f78dfb1..30dffd9676 100644 --- a/internal/nginx/main.go +++ b/internal/nginx/main.go @@ -91,7 +91,7 @@ func NewPostStatusRequest(path, contentType string, data interface{}) (int, []by // GetServerBlock takes an nginx.conf file and a host and tries to find the server block for that host func GetServerBlock(conf string, host string) (string, error) { - startMsg := fmt.Sprintf("## start server %v", host) + startMsg := fmt.Sprintf("## start server %v\n", host) endMsg := fmt.Sprintf("## end server %v", host) blockStart := strings.Index(conf, startMsg)