| // Copyright Istio 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 cmd |
| |
| import ( |
| "context" |
| "fmt" |
| "io" |
| "sort" |
| "strconv" |
| "strings" |
| "text/tabwriter" |
| ) |
| |
| import ( |
| "github.com/spf13/cobra" |
| "istio.io/api/annotation" |
| "istio.io/api/label" |
| admit_v1 "k8s.io/api/admissionregistration/v1" |
| v1 "k8s.io/api/core/v1" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| api_pkg_labels "k8s.io/apimachinery/pkg/labels" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/istioctl/pkg/clioptions" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/analysis/analyzers/injection" |
| analyzer_util "github.com/apache/dubbo-go-pixiu/pkg/config/analysis/analyzers/util" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/resource" |
| "github.com/apache/dubbo-go-pixiu/pkg/kube" |
| ) |
| |
| type revisionCount struct { |
| // pods in a revision |
| pods int |
| // pods that are disabled from injection |
| disabled int |
| // pods that are enabled for injection, but whose revision doesn't match their namespace's revision |
| needsRestart int |
| } |
| |
| func injectorCommand() *cobra.Command { |
| cmd := &cobra.Command{ |
| Use: "injector", |
| Short: "List sidecar injector and sidecar versions", |
| Long: `List sidecar injector and sidecar versions`, |
| Example: ` istioctl experimental injector list`, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if len(args) != 0 { |
| return fmt.Errorf("unknown subcommand %q", args[0]) |
| } |
| return nil |
| }, |
| RunE: func(cmd *cobra.Command, args []string) error { |
| cmd.HelpFunc()(cmd, args) |
| return nil |
| }, |
| } |
| |
| cmd.AddCommand(injectorListCommand()) |
| return cmd |
| } |
| |
| func injectorListCommand() *cobra.Command { |
| var opts clioptions.ControlPlaneOptions |
| cmd := &cobra.Command{ |
| Use: "list", |
| Short: "List sidecar injector and sidecar versions", |
| Long: `List sidecar injector and sidecar versions`, |
| Example: ` istioctl experimental injector list`, |
| RunE: func(cmd *cobra.Command, args []string) error { |
| client, err := kubeClientWithRevision(kubeconfig, configContext, opts.Revision) |
| if err != nil { |
| return fmt.Errorf("failed to create k8s client: %v", err) |
| } |
| ctx := context.Background() |
| |
| nslist, err := getNamespaces(ctx, client) |
| if err != nil { |
| return err |
| } |
| sort.Slice(nslist, func(i, j int) bool { |
| return nslist[i].Name < nslist[j].Name |
| }) |
| |
| hooks, err := getWebhooks(ctx, client) |
| if err != nil { |
| return err |
| } |
| pods, err := getPods(ctx, client) |
| if err != nil { |
| return err |
| } |
| err = printNS(cmd.OutOrStdout(), nslist, hooks, pods) |
| if err != nil { |
| return err |
| } |
| cmd.Println() |
| injectedImages, err := getInjectedImages(ctx, client) |
| if err != nil { |
| return err |
| } |
| |
| sort.Slice(hooks, func(i, j int) bool { |
| return hooks[i].Name < hooks[j].Name |
| }) |
| return printHooks(cmd.OutOrStdout(), nslist, hooks, injectedImages) |
| }, |
| } |
| |
| return cmd |
| } |
| |
| func getNamespaces(ctx context.Context, client kube.ExtendedClient) ([]v1.Namespace, error) { |
| nslist, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) |
| if err != nil { |
| return []v1.Namespace{}, err |
| } |
| return nslist.Items, nil |
| } |
| |
| func printNS(writer io.Writer, namespaces []v1.Namespace, hooks []admit_v1.MutatingWebhookConfiguration, |
| allPods map[resource.Namespace][]v1.Pod) error { |
| outputCount := 0 |
| |
| w := new(tabwriter.Writer).Init(writer, 0, 8, 1, ' ', 0) |
| for _, namespace := range namespaces { |
| if hideFromOutput(resource.Namespace(namespace.Name)) { |
| continue |
| } |
| |
| revision := getInjectedRevision(&namespace, hooks) |
| podCount := podCountByRevision(allPods[resource.Namespace(namespace.Name)], revision) |
| if len(podCount) == 0 { |
| // This namespace has no pods, but we wish to display it if new pods will be auto-injected |
| if revision != "" { |
| podCount[revision] = revisionCount{} |
| } |
| } |
| for injectedRevision, count := range podCount { |
| if outputCount == 0 { |
| fmt.Fprintln(w, "NAMESPACE\tISTIO-REVISION\tPOD-REVISIONS") |
| } |
| outputCount++ |
| |
| fmt.Fprintf(w, "%s\t%s\t%s\n", namespace.Name, revision, renderCounts(injectedRevision, count)) |
| } |
| } |
| if outputCount == 0 { |
| fmt.Fprintf(writer, "No Istio injected namespaces present.\n") |
| } |
| |
| return w.Flush() |
| } |
| |
| func getWebhooks(ctx context.Context, client kube.ExtendedClient) ([]admit_v1.MutatingWebhookConfiguration, error) { |
| hooks, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().List(ctx, metav1.ListOptions{}) |
| if err != nil { |
| return []admit_v1.MutatingWebhookConfiguration{}, err |
| } |
| return hooks.Items, nil |
| } |
| |
| func printHooks(writer io.Writer, namespaces []v1.Namespace, hooks []admit_v1.MutatingWebhookConfiguration, injectedImages map[string]string) error { |
| if len(hooks) == 0 { |
| fmt.Fprintf(writer, "No Istio injection hooks present.\n") |
| return nil |
| } |
| |
| w := new(tabwriter.Writer).Init(writer, 0, 8, 1, ' ', 0) |
| fmt.Fprintln(w, "NAMESPACES\tINJECTOR-HOOK\tISTIO-REVISION\tSIDECAR-IMAGE") |
| for _, hook := range hooks { |
| revision := hook.ObjectMeta.GetLabels()[label.IoIstioRev.Name] |
| namespaces := getMatchingNamespaces(&hook, namespaces) |
| if len(namespaces) == 0 { |
| fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "DOES NOT AUTOINJECT", hook.Name, revision, injectedImages[revision]) |
| continue |
| } |
| for _, namespace := range namespaces { |
| fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", namespace.Name, hook.Name, revision, injectedImages[revision]) |
| } |
| } |
| return w.Flush() |
| } |
| |
| func getInjector(namespace *v1.Namespace, hooks []admit_v1.MutatingWebhookConfiguration) *admit_v1.MutatingWebhookConfiguration { |
| // find matching hook |
| for _, hook := range hooks { |
| for _, webhook := range hook.Webhooks { |
| nsSelector, err := metav1.LabelSelectorAsSelector(webhook.NamespaceSelector) |
| if err != nil { |
| continue |
| } |
| if nsSelector.Matches(api_pkg_labels.Set(namespace.ObjectMeta.Labels)) { |
| return &hook |
| } |
| } |
| } |
| return nil |
| } |
| |
| func getInjectedRevision(namespace *v1.Namespace, hooks []admit_v1.MutatingWebhookConfiguration) string { |
| injector := getInjector(namespace, hooks) |
| if injector != nil { |
| return injector.ObjectMeta.GetLabels()[label.IoIstioRev.Name] |
| } |
| newRev := namespace.ObjectMeta.GetLabels()[label.IoIstioRev.Name] |
| oldLabel, ok := namespace.ObjectMeta.GetLabels()[analyzer_util.InjectionLabelName] |
| // If there is no istio-injection=disabled and no istio.io/rev, the namespace isn't injected |
| if newRev == "" && (ok && oldLabel == "disabled" || !ok) { |
| return "" |
| } |
| if newRev != "" { |
| return fmt.Sprintf("MISSING/%s", newRev) |
| } |
| return fmt.Sprintf("MISSING/%s", analyzer_util.InjectionLabelName) |
| } |
| |
| func getMatchingNamespaces(hook *admit_v1.MutatingWebhookConfiguration, namespaces []v1.Namespace) []v1.Namespace { |
| retval := make([]v1.Namespace, 0) |
| for _, webhook := range hook.Webhooks { |
| nsSelector, err := metav1.LabelSelectorAsSelector(webhook.NamespaceSelector) |
| if err != nil { |
| return retval |
| } |
| |
| for _, namespace := range namespaces { |
| if nsSelector.Matches(api_pkg_labels.Set(namespace.Labels)) { |
| retval = append(retval, namespace) |
| } |
| } |
| } |
| return retval |
| } |
| |
| func getPods(ctx context.Context, client kube.ExtendedClient) (map[resource.Namespace][]v1.Pod, error) { |
| retval := map[resource.Namespace][]v1.Pod{} |
| // All pods in all namespaces |
| pods, err := client.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) |
| if err != nil { |
| return retval, err |
| } |
| for _, pod := range pods.Items { |
| podList, ok := retval[resource.Namespace(pod.GetNamespace())] |
| if !ok { |
| retval[resource.Namespace(pod.GetNamespace())] = []v1.Pod{pod} |
| } else { |
| retval[resource.Namespace(pod.GetNamespace())] = append(podList, pod) |
| } |
| } |
| return retval, nil |
| } |
| |
| // getInjectedImages() returns a map of revision->dockerimage |
| func getInjectedImages(ctx context.Context, client kube.ExtendedClient) (map[string]string, error) { |
| retval := map[string]string{} |
| |
| // All configs in all namespaces that are Istio revisioned |
| configMaps, err := client.CoreV1().ConfigMaps("").List(ctx, metav1.ListOptions{LabelSelector: label.IoIstioRev.Name}) |
| if err != nil { |
| return retval, err |
| } |
| |
| for _, configMap := range configMaps.Items { |
| image := injection.GetIstioProxyImage(&configMap) |
| if image != "" { |
| retval[configMap.ObjectMeta.GetLabels()[label.IoIstioRev.Name]] = image |
| } |
| } |
| |
| return retval, nil |
| } |
| |
| // podCountByRevision() returns a map of revision->pods, with "<non-Istio>" as the dummy "revision" for uninjected pods |
| func podCountByRevision(pods []v1.Pod, expectedRevision string) map[string]revisionCount { |
| retval := map[string]revisionCount{} |
| for _, pod := range pods { |
| revision := pod.ObjectMeta.GetLabels()[label.IoIstioRev.Name] |
| revisionLabel := revision |
| if revision == "" { |
| revisionLabel = "<non-Istio>" |
| } |
| counts := retval[revisionLabel] |
| counts.pods++ |
| if injectionDisabled(&pod) { |
| counts.disabled++ |
| } else if revision != expectedRevision { |
| counts.needsRestart++ |
| } |
| retval[revisionLabel] = counts |
| } |
| return retval |
| } |
| |
| func hideFromOutput(ns resource.Namespace) bool { |
| return (analyzer_util.IsSystemNamespace(ns) || ns == resource.Namespace(istioNamespace)) |
| } |
| |
| func injectionDisabled(pod *v1.Pod) bool { |
| inject := pod.ObjectMeta.GetAnnotations()[annotation.SidecarInject.Name] |
| return strings.EqualFold(inject, "false") |
| } |
| |
| func renderCounts(injectedRevision string, counts revisionCount) string { |
| if counts.pods == 0 { |
| return "<no pods>" |
| } |
| |
| podText := strconv.Itoa(counts.pods) |
| if counts.disabled > 0 { |
| podText += fmt.Sprintf(" (injection disabled: %d)", counts.disabled) |
| } |
| if counts.needsRestart > 0 { |
| podText += fmt.Sprintf(" NEEDS RESTART: %d", counts.needsRestart) |
| } |
| return fmt.Sprintf("%s: %s", injectedRevision, podText) |
| } |