istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/analysis/analyzers/annotations/annotations.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 annotations 16 17 import ( 18 "strings" 19 20 "istio.io/api/annotation" 21 "istio.io/api/label" 22 "istio.io/istio/pkg/config" 23 "istio.io/istio/pkg/config/analysis" 24 "istio.io/istio/pkg/config/analysis/analyzers/maturity" 25 "istio.io/istio/pkg/config/analysis/analyzers/util" 26 "istio.io/istio/pkg/config/analysis/msg" 27 "istio.io/istio/pkg/config/resource" 28 "istio.io/istio/pkg/config/schema/gvk" 29 "istio.io/istio/pkg/kube/inject" 30 "istio.io/istio/pkg/slices" 31 ) 32 33 // K8sAnalyzer checks for misplaced and invalid Istio annotations in K8s resources 34 type K8sAnalyzer struct{} 35 36 var istioAnnotations = annotation.AllResourceAnnotations() 37 38 // Metadata implements analyzer.Analyzer 39 func (*K8sAnalyzer) Metadata() analysis.Metadata { 40 return analysis.Metadata{ 41 Name: "annotations.K8sAnalyzer", 42 Description: "Checks for misplaced and invalid Istio annotations in Kubernetes resources", 43 Inputs: []config.GroupVersionKind{ 44 gvk.Namespace, 45 gvk.Service, 46 gvk.Pod, 47 gvk.Deployment, 48 }, 49 } 50 } 51 52 // Analyze implements analysis.Analyzer 53 func (fa *K8sAnalyzer) Analyze(ctx analysis.Context) { 54 ctx.ForEach(gvk.Namespace, func(r *resource.Instance) bool { 55 fa.allowAnnotations(r, ctx, "Namespace", gvk.Namespace) 56 return true 57 }) 58 ctx.ForEach(gvk.Service, func(r *resource.Instance) bool { 59 fa.allowAnnotations(r, ctx, "Service", gvk.Service) 60 return true 61 }) 62 ctx.ForEach(gvk.Pod, func(r *resource.Instance) bool { 63 fa.allowAnnotations(r, ctx, "Pod", gvk.Pod) 64 return true 65 }) 66 ctx.ForEach(gvk.Deployment, func(r *resource.Instance) bool { 67 fa.allowAnnotations(r, ctx, "Deployment", gvk.Deployment) 68 return true 69 }) 70 } 71 72 var deprecationExtraMessages = map[string]string{ 73 annotation.SidecarInject.Name: ` in favor of the "sidecar.istio.io/inject" label`, 74 } 75 76 func (*K8sAnalyzer) allowAnnotations(r *resource.Instance, ctx analysis.Context, kind string, collectionType config.GroupVersionKind) { 77 if len(r.Metadata.Annotations) == 0 { 78 return 79 } 80 81 // It is fine if the annotation is kubectl.kubernetes.io/last-applied-configuration. 82 outer: 83 for ann, value := range r.Metadata.Annotations { 84 if !istioAnnotation(ann) { 85 continue 86 } 87 88 if maturity.AlwaysIgnoredAnnotations[ann] { 89 continue 90 } 91 92 annotationDef := lookupAnnotation(ann) 93 if annotationDef == nil { 94 m := msg.NewUnknownAnnotation(r, ann) 95 util.AddLineNumber(r, ann, m) 96 97 ctx.Report(collectionType, m) 98 continue 99 } 100 101 if annotationDef.Deprecated { 102 if _, f := r.Metadata.Labels[label.SidecarInject.Name]; f && ann == annotation.SidecarInject.Name { 103 // Skip to avoid noise; the user has the deprecated annotation but they also have the replacement 104 // This means they are likely aware its deprecated, but are keeping both variants around for maximum 105 // compatibility 106 } else { 107 m := msg.NewDeprecatedAnnotation(r, ann, deprecationExtraMessages[annotationDef.Name]) 108 util.AddLineNumber(r, ann, m) 109 110 ctx.Report(collectionType, m) 111 } 112 } 113 114 // If the annotation def attaches to Any, exit early 115 for _, rt := range annotationDef.Resources { 116 if rt == annotation.Any { 117 continue outer 118 } 119 } 120 121 attachesTo := resourceTypesAsStrings(annotationDef.Resources) 122 if !slices.Contains(attachesTo, kind) { 123 m := msg.NewMisplacedAnnotation(r, ann, strings.Join(attachesTo, ", ")) 124 util.AddLineNumber(r, ann, m) 125 126 ctx.Report(collectionType, m) 127 continue 128 } 129 130 validationFunction := inject.AnnotationValidation[ann] 131 if validationFunction != nil { 132 if err := validationFunction(value); err != nil { 133 m := msg.NewInvalidAnnotation(r, ann, err.Error()) 134 util.AddLineNumber(r, ann, m) 135 136 ctx.Report(collectionType, m) 137 continue 138 } 139 } 140 } 141 } 142 143 // istioAnnotation is true if the annotation is in Istio's namespace 144 func istioAnnotation(ann string) bool { 145 // We document this Kubernetes annotation, we should analyze it as well 146 if ann == "kubernetes.io/ingress.class" { 147 return true 148 } 149 150 parts := strings.Split(ann, "/") 151 if len(parts) == 0 { 152 return false 153 } 154 155 if !strings.HasSuffix(parts[0], "istio.io") { 156 return false 157 } 158 159 return true 160 } 161 162 func lookupAnnotation(ann string) *annotation.Instance { 163 for _, candidate := range istioAnnotations { 164 if candidate.Name == ann { 165 return candidate 166 } 167 } 168 169 return nil 170 } 171 172 func resourceTypesAsStrings(resourceTypes []annotation.ResourceTypes) []string { 173 retval := []string{} 174 for _, resourceType := range resourceTypes { 175 if s := resourceType.String(); s != "Unknown" { 176 retval = append(retval, s) 177 } 178 } 179 return retval 180 }