istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/injector/injector-list.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package injector 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "sort" 22 "strconv" 23 "strings" 24 "text/tabwriter" 25 26 "github.com/spf13/cobra" 27 admitv1 "k8s.io/api/admissionregistration/v1" 28 corev1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 api_pkg_labels "k8s.io/apimachinery/pkg/labels" 31 32 "istio.io/api/annotation" 33 "istio.io/api/label" 34 "istio.io/istio/istioctl/pkg/cli" 35 "istio.io/istio/istioctl/pkg/clioptions" 36 "istio.io/istio/istioctl/pkg/describe" 37 "istio.io/istio/istioctl/pkg/util/ambient" 38 "istio.io/istio/pkg/config/analysis/analyzers/injection" 39 analyzer_util "istio.io/istio/pkg/config/analysis/analyzers/util" 40 "istio.io/istio/pkg/config/resource" 41 "istio.io/istio/pkg/kube" 42 "istio.io/istio/pkg/kube/inject" 43 "istio.io/istio/pkg/slices" 44 "istio.io/istio/pkg/util/sets" 45 ) 46 47 type revisionCount struct { 48 // pods in a revision 49 pods int 50 // pods that are disabled from injection 51 disabled int 52 // pods that are enabled for injection, but whose revision doesn't match their namespace's revision 53 needsRestart int 54 } 55 56 func Cmd(cliContext cli.Context) *cobra.Command { 57 cmd := &cobra.Command{ 58 Use: "injector", 59 Short: "List sidecar injector and sidecar versions", 60 Long: `List sidecar injector and sidecar versions`, 61 Example: ` istioctl experimental injector list`, 62 Args: func(cmd *cobra.Command, args []string) error { 63 if len(args) != 0 { 64 return fmt.Errorf("unknown subcommand %q", args[0]) 65 } 66 return nil 67 }, 68 RunE: func(cmd *cobra.Command, args []string) error { 69 cmd.HelpFunc()(cmd, args) 70 return nil 71 }, 72 } 73 74 cmd.AddCommand(injectorListCommand(cliContext)) 75 return cmd 76 } 77 78 func injectorListCommand(ctx cli.Context) *cobra.Command { 79 var opts clioptions.ControlPlaneOptions 80 cmd := &cobra.Command{ 81 Use: "list", 82 Short: "List sidecar injector and sidecar versions", 83 Long: `List sidecar injector and sidecar versions`, 84 Example: ` istioctl experimental injector list`, 85 RunE: func(cmd *cobra.Command, args []string) error { 86 client, err := ctx.CLIClientWithRevision(opts.Revision) 87 if err != nil { 88 return fmt.Errorf("failed to create k8s client: %v", err) 89 } 90 91 nslist, err := getNamespaces(context.Background(), client, ctx.IstioNamespace()) 92 if err != nil { 93 return err 94 } 95 96 hooksList, err := client.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), metav1.ListOptions{}) 97 if err != nil { 98 return err 99 } 100 hooks := hooksList.Items 101 pods, err := getPods(context.Background(), client) 102 if err != nil { 103 return err 104 } 105 err = printNS(cmd.OutOrStdout(), nslist, hooks, pods) 106 if err != nil { 107 return err 108 } 109 fmt.Fprintln(cmd.OutOrStdout()) 110 injectedImages, err := getInjectedImages(context.Background(), client) 111 if err != nil { 112 return err 113 } 114 115 sort.Slice(hooks, func(i, j int) bool { 116 return hooks[i].Name < hooks[j].Name 117 }) 118 return printHooks(cmd.OutOrStdout(), nslist, hooks, injectedImages) 119 }, 120 } 121 122 return cmd 123 } 124 125 func filterSystemNamespaces(nss []corev1.Namespace, istioNamespace string) []corev1.Namespace { 126 filtered := make([]corev1.Namespace, 0) 127 for _, ns := range nss { 128 if inject.IgnoredNamespaces.Contains(ns.Name) || ns.Name == istioNamespace { 129 continue 130 } 131 filtered = append(filtered, ns) 132 } 133 return filtered 134 } 135 136 func getNamespaces(ctx context.Context, client kube.CLIClient, istioNamespace string) ([]corev1.Namespace, error) { 137 nslist, err := client.Kube().CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) 138 if err != nil { 139 return []corev1.Namespace{}, err 140 } 141 filtered := filterSystemNamespaces(nslist.Items, istioNamespace) 142 filtered = slices.Filter(filtered, func(namespace corev1.Namespace) bool { 143 return !ambient.InAmbient(&namespace) 144 }) 145 sort.Slice(filtered, func(i, j int) bool { 146 return filtered[i].Name < filtered[j].Name 147 }) 148 return filtered, nil 149 } 150 151 func printNS(writer io.Writer, namespaces []corev1.Namespace, hooks []admitv1.MutatingWebhookConfiguration, 152 allPods map[resource.Namespace][]corev1.Pod, 153 ) error { 154 outputCount := 0 155 156 w := new(tabwriter.Writer).Init(writer, 0, 8, 1, ' ', 0) 157 for _, namespace := range namespaces { 158 revision := getInjectedRevision(&namespace, hooks) 159 podCount := podCountByRevision(allPods[resource.Namespace(namespace.Name)], revision) 160 if len(podCount) == 0 { 161 // This namespace has no pods, but we wish to display it if new pods will be auto-injected 162 if revision != "" { 163 podCount[revision] = revisionCount{} 164 } 165 } 166 for injectedRevision, count := range podCount { 167 if outputCount == 0 { 168 fmt.Fprintln(w, "NAMESPACE\tISTIO-REVISION\tPOD-REVISIONS") 169 } 170 outputCount++ 171 172 fmt.Fprintf(w, "%s\t%s\t%s\n", namespace.Name, revision, renderCounts(injectedRevision, count)) 173 } 174 } 175 if outputCount == 0 { 176 fmt.Fprintf(writer, "No Istio injected namespaces present.\n") 177 } 178 179 return w.Flush() 180 } 181 182 func printHooks(writer io.Writer, namespaces []corev1.Namespace, hooks []admitv1.MutatingWebhookConfiguration, injectedImages map[string]string) error { 183 if len(hooks) == 0 { 184 fmt.Fprintf(writer, "No Istio injection hooks present.\n") 185 return nil 186 } 187 188 w := new(tabwriter.Writer).Init(writer, 0, 8, 1, ' ', 0) 189 fmt.Fprintln(w, "NAMESPACES\tINJECTOR-HOOK\tISTIO-REVISION\tSIDECAR-IMAGE") 190 for _, hook := range hooks { 191 revision := hook.ObjectMeta.GetLabels()[label.IoIstioRev.Name] 192 namespaces := getMatchingNamespaces(&hook, namespaces) 193 if len(namespaces) == 0 { 194 fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "DOES NOT AUTOINJECT", hook.Name, revision, injectedImages[revision]) 195 continue 196 } 197 for _, namespace := range namespaces { 198 fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", namespace.Name, hook.Name, revision, injectedImages[revision]) 199 } 200 } 201 return w.Flush() 202 } 203 204 func getInjector(namespace *corev1.Namespace, hooks []admitv1.MutatingWebhookConfiguration) *admitv1.MutatingWebhookConfiguration { 205 // find matching hook 206 for _, hook := range hooks { 207 for _, webhook := range hook.Webhooks { 208 nsSelector, err := metav1.LabelSelectorAsSelector(webhook.NamespaceSelector) 209 if err != nil { 210 continue 211 } 212 if nsSelector.Matches(api_pkg_labels.Set(namespace.ObjectMeta.Labels)) { 213 return &hook 214 } 215 } 216 } 217 return nil 218 } 219 220 func getInjectedRevision(namespace *corev1.Namespace, hooks []admitv1.MutatingWebhookConfiguration) string { 221 injector := getInjector(namespace, hooks) 222 if injector != nil { 223 return injector.ObjectMeta.GetLabels()[label.IoIstioRev.Name] 224 } 225 newRev := namespace.ObjectMeta.GetLabels()[label.IoIstioRev.Name] 226 oldLabel, ok := namespace.ObjectMeta.GetLabels()[analyzer_util.InjectionLabelName] 227 // If there is no istio-injection=disabled and no istio.io/rev, the namespace isn't injected 228 if newRev == "" && (ok && oldLabel == "disabled" || !ok) { 229 return "" 230 } 231 if newRev != "" { 232 return fmt.Sprintf("MISSING/%s", newRev) 233 } 234 return fmt.Sprintf("MISSING/%s", analyzer_util.InjectionLabelName) 235 } 236 237 func getMatchingNamespaces(hook *admitv1.MutatingWebhookConfiguration, namespaces []corev1.Namespace) []corev1.Namespace { 238 retval := make([]corev1.Namespace, 0, len(namespaces)) 239 seen := sets.String{} 240 for _, webhook := range hook.Webhooks { 241 nsSelector, err := metav1.LabelSelectorAsSelector(webhook.NamespaceSelector) 242 if err != nil { 243 return retval 244 } 245 246 for _, namespace := range namespaces { 247 if !seen.Contains(namespace.Name) && nsSelector.Matches(api_pkg_labels.Set(namespace.Labels)) { 248 retval = append(retval, namespace) 249 seen.Insert(namespace.Name) 250 } 251 } 252 } 253 return retval 254 } 255 256 func getPods(ctx context.Context, client kube.CLIClient) (map[resource.Namespace][]corev1.Pod, error) { 257 retval := map[resource.Namespace][]corev1.Pod{} 258 // All pods in all namespaces 259 pods, err := client.Kube().CoreV1().Pods("").List(ctx, metav1.ListOptions{}) 260 if err != nil { 261 return retval, err 262 } 263 for _, pod := range pods.Items { 264 retval[resource.Namespace(pod.GetNamespace())] = append(retval[resource.Namespace(pod.GetNamespace())], pod) 265 } 266 return retval, nil 267 } 268 269 // getInjectedImages() returns a map of revision->dockerimage 270 func getInjectedImages(ctx context.Context, client kube.CLIClient) (map[string]string, error) { 271 retval := map[string]string{} 272 273 // All configs in all namespaces that are Istio revisioned 274 configMaps, err := client.Kube().CoreV1().ConfigMaps("").List(ctx, metav1.ListOptions{LabelSelector: label.IoIstioRev.Name}) 275 if err != nil { 276 return retval, err 277 } 278 279 for _, configMap := range configMaps.Items { 280 image := injection.GetIstioProxyImage(&configMap) 281 if image != "" { 282 retval[configMap.ObjectMeta.GetLabels()[label.IoIstioRev.Name]] = image 283 } 284 } 285 286 return retval, nil 287 } 288 289 // podCountByRevision() returns a map of revision->pods, with "<non-Istio>" as the dummy "revision" for uninjected pods 290 func podCountByRevision(pods []corev1.Pod, expectedRevision string) map[string]revisionCount { 291 retval := map[string]revisionCount{} 292 for _, pod := range pods { 293 revision := extractRevisionFromPod(&pod) 294 revisionLabel := revision 295 if revision == "" { 296 revisionLabel = "<non-Istio>" 297 } 298 counts := retval[revisionLabel] 299 counts.pods++ 300 if injectionDisabled(&pod) { 301 counts.disabled++ 302 } else if revision != expectedRevision { 303 counts.needsRestart++ 304 } 305 retval[revisionLabel] = counts 306 } 307 return retval 308 } 309 310 func extractRevisionFromPod(pod *corev1.Pod) string { 311 return describe.GetRevisionFromPodAnnotation(pod.GetAnnotations()) 312 } 313 314 func injectionDisabled(pod *corev1.Pod) bool { 315 inject := pod.ObjectMeta.GetAnnotations()[annotation.SidecarInject.Name] 316 if lbl, labelPresent := pod.ObjectMeta.GetLabels()[label.SidecarInject.Name]; labelPresent { 317 inject = lbl 318 } 319 return strings.EqualFold(inject, "false") 320 } 321 322 func renderCounts(injectedRevision string, counts revisionCount) string { 323 if counts.pods == 0 { 324 return "<no pods>" 325 } 326 327 podText := strconv.Itoa(counts.pods) 328 if counts.disabled > 0 { 329 podText += fmt.Sprintf(" (injection disabled: %d)", counts.disabled) 330 } 331 if counts.needsRestart > 0 { 332 podText += fmt.Sprintf(" NEEDS RESTART: %d", counts.needsRestart) 333 } 334 return fmt.Sprintf("%s: %s", injectedRevision, podText) 335 }