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  }