Add kubectl plugin

This commit is contained in:
Alex Kursell 2019-02-25 15:54:00 -05:00
parent e8bfa9a587
commit 9e424a4a6a
52 changed files with 7212 additions and 2 deletions

View file

@ -0,0 +1,40 @@
apiVersion: krew.googlecontainertools.github.com/v1alpha2
kind: Plugin
metadata:
name: ingress-nginx
spec:
shortDescription: Interact with ingress-nginx
description: |
The official kubectl plugin for ingress-nginx.
version: %%%tag%%%
platforms:
- uri: https://github.com/kubernetes/ingress-nginx/releases/download/nginx-%%%tag%%%/kubectl-ingress_nginx-darwin-amd64.tar.gz
sha256: %%%shasum_darwin_amd64%%%
files:
- from: "*"
to: "."
bin: "./kubectl-ingress_nginx"
selector:
matchLabels:
os: darwin
arch: amd64
- uri: https://github.com/kubernetes/ingress-nginx/releases/download/nginx-%%%tag%%%/kubectl-ingress_nginx-linux-amd64.tar.gz
sha256: %%%shasum_linux_amd64%%%
files:
- from: "*"
to: "."
bin: "./kubectl-ingress_nginx"
selector:
matchLabels:
os: linux
arch: amd64
- uri: https://github.com/kubernetes/ingress-nginx/releases/download/nginx-%%%tag%%%/kubectl-ingress_nginx-windows-amd64.tar.gz
sha256: %%%shasum_windows_amd64%%%
files:
- from: "*"
to: "."
bin: "./kubectl-ingress_nginx"
selector:
matchLabels:
os: windows
arch: amd64

416
cmd/plugin/main.go Normal file
View file

@ -0,0 +1,416 @@
/*
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 main
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"
)
func main() {
rootCmd := &cobra.Command{
Use: "ingress-nginx",
Short: "A kubectl plugin for inspecting your ingress-nginx deployments",
}
// Respect some basic kubectl flags like --namespace
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)
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
}

View file

@ -0,0 +1,209 @@
/*
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 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"
)
// 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) {
allPods, err := getPods(flags)
if err != nil {
return "", err
}
for _, pod := range allPods {
if pod.Name == podName {
return podExec(flags, &pod, cmd)
}
}
return "", fmt.Errorf("Pod %v not found in namespace %v", podName, 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)
if err != nil {
return "", err
}
if len(ings) == 0 {
return "", fmt.Errorf("No ingress-nginx pods found in namespace %v", util.GetNamespace(flags))
}
return podExec(flags, &ings[0], cmd)
}
func podExec(flags *genericclioptions.ConfigFlags, pod *apiv1.Pod, cmd []string) (string, error) {
config, err := flags.ToRESTConfig()
if err != nil {
return "", err
}
client, err := corev1.NewForConfig(config)
if err != nil {
return "", err
}
namespace, _, err := flags.ToRawKubeConfigLoader().Namespace()
if err != nil {
return "", 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())
if err != nil {
return "", err
}
stdout := bytes.NewBuffer(make([]byte, 0))
err = exec.Stream(remotecommand.StreamOptions{
Stdout: stdout,
})
return stdout.String(), err
}
func getIngressPods(flags *genericclioptions.ConfigFlags) ([]apiv1.Pod, error) {
pods, err := getPods(flags)
if err != nil {
return make([]apiv1.Pod, 0), err
}
ingressPods := make([]apiv1.Pod, 0)
for _, pod := range pods {
if pod.Spec.Containers[0].Name == ingressPodName {
ingressPods = append(ingressPods, pod)
}
}
return ingressPods, nil
}
func getPods(flags *genericclioptions.ConfigFlags) ([]apiv1.Pod, error) {
namespace := util.GetNamespace(flags)
rawConfig, err := flags.ToRESTConfig()
if err != nil {
return make([]apiv1.Pod, 0), err
}
api, err := corev1.NewForConfig(rawConfig)
if err != nil {
return make([]apiv1.Pod, 0), err
}
pods, err := api.Pods(namespace).List(metav1.ListOptions{})
if err != nil {
return make([]apiv1.Pod, 0), err
}
return pods.Items, nil
}
// 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 make([]v1beta1.Ingress, 0), err
}
api, err := extensions.NewForConfig(rawConfig)
if err != nil {
return make([]v1beta1.Ingress, 0), err
}
pods, err := api.Ingresses(namespace).List(metav1.ListOptions{})
if err != nil {
return make([]v1beta1.Ingress, 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)
if err != nil {
return apiv1.Service{}, err
}
for _, svc := range services {
if svc.Name == ingressServiceName {
return svc, nil
}
}
return apiv1.Service{}, fmt.Errorf("Could not find service %v in namespace %v", ingressServiceName, util.GetNamespace(flags))
}
func getServices(flags *genericclioptions.ConfigFlags) ([]apiv1.Service, error) {
namespace := util.GetNamespace(flags)
rawConfig, err := flags.ToRESTConfig()
if err != nil {
return make([]apiv1.Service, 0), err
}
api, err := corev1.NewForConfig(rawConfig)
if err != nil {
return make([]apiv1.Service, 0), err
}
services, err := api.Services(namespace).List(metav1.ListOptions{})
if err != nil {
return make([]apiv1.Service, 0), err
}
return services.Items, nil
}

55
cmd/plugin/util/util.go Normal file
View file

@ -0,0 +1,55 @@
/*
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 util
import (
"fmt"
apiv1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
// PrintError receives an error value and prints it if it exists
func PrintError(e error) {
if e != nil {
fmt.Println(e)
}
}
func printWithError(s string, e error) {
if e != nil {
fmt.Println(e)
}
fmt.Print(s)
}
func printOrError(s string, e error) error {
if e != nil {
return e
}
fmt.Print(s)
return nil
}
// 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()
if err != nil || len(namespace) == 0 {
namespace = apiv1.NamespaceDefault
}
return namespace
}