istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/analysis/local/istiod_analyze.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 local
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"strings"
    23  
    24  	"github.com/hashicorp/go-multierror"
    25  	"github.com/ryanuber/go-glob"
    26  	v1 "k8s.io/api/core/v1"
    27  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/fields"
    30  
    31  	"istio.io/api/annotation"
    32  	"istio.io/api/mesh/v1alpha1"
    33  	"istio.io/istio/pilot/pkg/config/aggregate"
    34  	"istio.io/istio/pilot/pkg/config/file"
    35  	"istio.io/istio/pilot/pkg/config/kube/crdclient"
    36  	"istio.io/istio/pilot/pkg/config/memory"
    37  	"istio.io/istio/pilot/pkg/leaderelection/k8sleaderelection/k8sresourcelock"
    38  	"istio.io/istio/pilot/pkg/model"
    39  	"istio.io/istio/pkg/cluster"
    40  	"istio.io/istio/pkg/config"
    41  	"istio.io/istio/pkg/config/analysis"
    42  	"istio.io/istio/pkg/config/analysis/diag"
    43  	"istio.io/istio/pkg/config/analysis/legacy/util/kuberesource"
    44  	"istio.io/istio/pkg/config/analysis/scope"
    45  	"istio.io/istio/pkg/config/mesh"
    46  	"istio.io/istio/pkg/config/resource"
    47  	"istio.io/istio/pkg/config/schema/collection"
    48  	"istio.io/istio/pkg/config/schema/collections"
    49  	"istio.io/istio/pkg/config/schema/gvk"
    50  	kubelib "istio.io/istio/pkg/kube"
    51  	"istio.io/istio/pkg/kube/inject"
    52  	"istio.io/istio/pkg/kube/kubetypes"
    53  	"istio.io/istio/pkg/util/sets"
    54  )
    55  
    56  // IstiodAnalyzer handles local analysis of k8s event sources, both live and file-based
    57  type IstiodAnalyzer struct {
    58  	// internalStore stores synthetic configs for analysis (mesh config, etc)
    59  	internalStore model.ConfigStore
    60  	// stores contains all the (non file) config sources to analyze
    61  	stores []model.ConfigStoreController
    62  	// multiClusterStores contains all the multi-cluster config sources to analyze
    63  	multiClusterStores map[cluster.ID]model.ConfigStoreController
    64  	// cluster is the cluster ID for the environment we are analyzing
    65  	cluster cluster.ID
    66  	// fileSource contains all file bases sources
    67  	fileSource *file.KubeSource
    68  
    69  	analyzer       analysis.CombinedAnalyzer
    70  	namespace      resource.Namespace
    71  	istioNamespace resource.Namespace
    72  
    73  	// List of code and resource suppressions to exclude messages on
    74  	suppressions []AnalysisSuppression
    75  
    76  	// Mesh config for this analyzer. This can come from multiple sources, and the last added version will take precedence.
    77  	meshCfg *v1alpha1.MeshConfig
    78  
    79  	// Mesh networks config for this analyzer.
    80  	meshNetworks *v1alpha1.MeshNetworks
    81  
    82  	// Which kube resources are used by this analyzer
    83  	// Derived from metadata and the specified analyzer and transformer providers
    84  	kubeResources collection.Schemas
    85  
    86  	// Hook function called when a collection is used in analysis
    87  	collectionReporter CollectionReporterFn
    88  
    89  	clientsToRun []kubelib.Client
    90  }
    91  
    92  // NewSourceAnalyzer is a drop-in replacement for the galley function, adapting to istiod analyzer.
    93  func NewSourceAnalyzer(analyzer analysis.CombinedAnalyzer, namespace, istioNamespace resource.Namespace, cr CollectionReporterFn) *IstiodAnalyzer {
    94  	return NewIstiodAnalyzer(analyzer, namespace, istioNamespace, cr)
    95  }
    96  
    97  // NewIstiodAnalyzer creates a new IstiodAnalyzer with no sources. Use the Add*Source
    98  // methods to add sources in ascending precedence order,
    99  // then execute Analyze to perform the analysis
   100  func NewIstiodAnalyzer(analyzer analysis.CombinedAnalyzer, namespace,
   101  	istioNamespace resource.Namespace, cr CollectionReporterFn,
   102  ) *IstiodAnalyzer {
   103  	// collectionReporter hook function defaults to no-op
   104  	if cr == nil {
   105  		cr = func(config.GroupVersionKind) {}
   106  	}
   107  
   108  	// Get the closure of all input collections for our analyzer, paying attention to transforms
   109  	kubeResources := kuberesource.ConvertInputsToSchemas(analyzer.Metadata().Inputs)
   110  
   111  	kubeResources = kubeResources.Union(kuberesource.DefaultExcludedSchemas())
   112  
   113  	mcfg := mesh.DefaultMeshConfig()
   114  	sa := &IstiodAnalyzer{
   115  		meshCfg:            mcfg,
   116  		meshNetworks:       mesh.DefaultMeshNetworks(),
   117  		analyzer:           analyzer,
   118  		namespace:          namespace,
   119  		cluster:            "default",
   120  		internalStore:      memory.Make(collection.SchemasFor(collections.MeshNetworks, collections.MeshConfig)),
   121  		istioNamespace:     istioNamespace,
   122  		kubeResources:      kubeResources,
   123  		collectionReporter: cr,
   124  		clientsToRun:       []kubelib.Client{},
   125  		multiClusterStores: make(map[cluster.ID]model.ConfigStoreController),
   126  	}
   127  
   128  	return sa
   129  }
   130  
   131  func (sa *IstiodAnalyzer) ReAnalyzeSubset(kinds sets.Set[config.GroupVersionKind], cancel <-chan struct{}) (AnalysisResult, error) {
   132  	subset := sa.analyzer.RelevantSubset(kinds)
   133  	return sa.internalAnalyze(subset, cancel)
   134  }
   135  
   136  // ReAnalyze loads the sources and executes the analysis, assuming init is already called
   137  func (sa *IstiodAnalyzer) ReAnalyze(cancel <-chan struct{}) (AnalysisResult, error) {
   138  	return sa.internalAnalyze(sa.analyzer, cancel)
   139  }
   140  
   141  func (sa *IstiodAnalyzer) internalAnalyze(a analysis.CombinedAnalyzer, cancel <-chan struct{}) (AnalysisResult, error) {
   142  	var schemas collection.Schemas
   143  	for _, store := range sa.multiClusterStores {
   144  		schemas = schemas.Union(store.Schemas())
   145  	}
   146  
   147  	var result AnalysisResult
   148  	result.ExecutedAnalyzers = a.AnalyzerNames()
   149  	result.SkippedAnalyzers = a.RemoveSkipped(schemas)
   150  	result.MappedMessages = make(map[string]diag.Messages, len(result.ExecutedAnalyzers))
   151  
   152  	for _, store := range sa.multiClusterStores {
   153  		kubelib.WaitForCacheSync("istiod analyzer", cancel, store.HasSynced)
   154  	}
   155  
   156  	stores := map[cluster.ID]model.ConfigStore{}
   157  	for k, v := range sa.multiClusterStores {
   158  		stores[k] = v
   159  	}
   160  	ctx := NewContext(stores, cancel, sa.collectionReporter)
   161  
   162  	a.Analyze(ctx)
   163  
   164  	// TODO(hzxuzhonghu): we do not need set here
   165  	namespaces := sets.New[resource.Namespace]()
   166  	if sa.namespace != "" {
   167  		namespaces.Insert(sa.namespace)
   168  	}
   169  	for _, analyzerName := range result.ExecutedAnalyzers {
   170  
   171  		// TODO: analysis is run for all namespaces, even if they are requested to be filtered.
   172  		msgs := filterMessages(ctx.(*istiodContext).GetMessages(analyzerName), namespaces, sa.suppressions)
   173  		result.MappedMessages[analyzerName] = msgs.SortedDedupedCopy()
   174  	}
   175  	msgs := filterMessages(ctx.(*istiodContext).GetMessages(), namespaces, sa.suppressions)
   176  	result.Messages = msgs.SortedDedupedCopy()
   177  
   178  	return result, nil
   179  }
   180  
   181  // Analyze loads the sources and executes the analysis
   182  func (sa *IstiodAnalyzer) Analyze(cancel <-chan struct{}) (AnalysisResult, error) {
   183  	err2 := sa.Init(cancel)
   184  	if err2 != nil {
   185  		return AnalysisResult{}, err2
   186  	}
   187  	return sa.ReAnalyze(cancel)
   188  }
   189  
   190  func (sa *IstiodAnalyzer) Init(cancel <-chan struct{}) error {
   191  	// We need at least one non-meshcfg source
   192  	if len(sa.stores) == 0 && sa.fileSource == nil {
   193  		return fmt.Errorf("at least one file and/or Kubernetes source must be provided")
   194  	}
   195  
   196  	// TODO: there's gotta be a better way to convert v1meshconfig to config.Config...
   197  	// Create a store containing mesh config. There should be exactly one.
   198  	_, err := sa.internalStore.Create(config.Config{
   199  		Meta: config.Meta{
   200  			Name:      "meshconfig",
   201  			Namespace: sa.istioNamespace.String(),
   202  
   203  			GroupVersionKind: gvk.MeshConfig,
   204  		},
   205  		Spec: sa.meshCfg,
   206  	})
   207  	if err != nil {
   208  		return fmt.Errorf("something unexpected happened while creating the meshconfig: %s", err)
   209  	}
   210  	// Create a store containing meshnetworks. There should be exactly one.
   211  	_, err = sa.internalStore.Create(config.Config{
   212  		Meta: config.Meta{
   213  			Name:             "meshnetworks",
   214  			Namespace:        sa.istioNamespace.String(),
   215  			GroupVersionKind: gvk.MeshNetworks,
   216  		},
   217  		Spec: sa.meshNetworks,
   218  	})
   219  	if err != nil {
   220  		return fmt.Errorf("something unexpected happened while creating the meshnetworks: %s", err)
   221  	}
   222  	allstores := append(sa.stores, dfCache{ConfigStore: sa.internalStore})
   223  	if sa.fileSource != nil {
   224  		// File source takes the highest precedence, since files are resources to be configured to in-cluster resources.
   225  		// The order here does matter - aggregated store takes the first available resource.
   226  		allstores = append([]model.ConfigStoreController{sa.fileSource}, allstores...)
   227  	}
   228  
   229  	for _, c := range sa.clientsToRun {
   230  		// TODO: this could be parallel
   231  		c.RunAndWait(cancel)
   232  	}
   233  
   234  	store, err := aggregate.MakeWriteableCache(allstores, nil)
   235  	if err != nil {
   236  		return err
   237  	}
   238  	sa.multiClusterStores[sa.cluster] = store
   239  	for _, mcs := range sa.multiClusterStores {
   240  		go mcs.Run(cancel)
   241  	}
   242  	return nil
   243  }
   244  
   245  type dfCache struct {
   246  	model.ConfigStore
   247  }
   248  
   249  func (d dfCache) RegisterEventHandler(kind config.GroupVersionKind, handler model.EventHandler) {
   250  	panic("implement me")
   251  }
   252  
   253  // Run intentionally left empty
   254  func (d dfCache) Run(_ <-chan struct{}) {
   255  }
   256  
   257  func (d dfCache) HasSynced() bool {
   258  	return true
   259  }
   260  
   261  // SetSuppressions will set the list of suppressions for the analyzer. Any
   262  // resource that matches the provided suppression will not be included in the
   263  // final message output.
   264  func (sa *IstiodAnalyzer) SetSuppressions(suppressions []AnalysisSuppression) {
   265  	sa.suppressions = suppressions
   266  }
   267  
   268  // AddTestReaderKubeSource adds a yaml source to the analyzer, which will analyze
   269  // runtime resources like pods and namespaces for use in tests.
   270  func (sa *IstiodAnalyzer) AddTestReaderKubeSource(readers []ReaderSource) error {
   271  	return sa.addReaderKubeSourceInternal(readers, true)
   272  }
   273  
   274  // AddReaderKubeSource adds a source based on the specified k8s yaml files to the current IstiodAnalyzer
   275  func (sa *IstiodAnalyzer) AddReaderKubeSource(readers []ReaderSource) error {
   276  	return sa.addReaderKubeSourceInternal(readers, false)
   277  }
   278  
   279  func (sa *IstiodAnalyzer) addReaderKubeSourceInternal(readers []ReaderSource, includeRuntimeResources bool) error {
   280  	var src *file.KubeSource
   281  	if sa.fileSource != nil {
   282  		src = sa.fileSource
   283  	} else {
   284  		var readerResources collection.Schemas
   285  		if includeRuntimeResources {
   286  			readerResources = sa.kubeResources
   287  		} else {
   288  			readerResources = sa.kubeResources.Remove(kuberesource.DefaultExcludedSchemas().All()...)
   289  		}
   290  		src = file.NewKubeSource(readerResources)
   291  		sa.fileSource = src
   292  	}
   293  	src.SetDefaultNamespace(sa.namespace)
   294  
   295  	src.SetNamespacesFilter(func(obj interface{}) bool {
   296  		cfg, ok := obj.(config.Config)
   297  		if !ok {
   298  			return false
   299  		}
   300  		meta := cfg.GetNamespace()
   301  		if cfg.Meta.GroupVersionKind.Kind == gvk.Namespace.Kind {
   302  			meta = cfg.GetName()
   303  		}
   304  		return !inject.IgnoredNamespaces.Contains(meta)
   305  	})
   306  
   307  	var errs error
   308  
   309  	// If we encounter any errors reading or applying files, track them but attempt to continue
   310  	for _, r := range readers {
   311  		by, err := io.ReadAll(r.Reader)
   312  		if err != nil {
   313  			errs = multierror.Append(errs, err)
   314  			continue
   315  		}
   316  
   317  		if err = src.ApplyContent(r.Name, string(by)); err != nil {
   318  			errs = multierror.Append(errs, err)
   319  		}
   320  	}
   321  	return errs
   322  }
   323  
   324  // AddRunningKubeSource adds a source based on a running k8s cluster to the current IstiodAnalyzer
   325  // Also tries to get mesh config from the running cluster, if it can
   326  func (sa *IstiodAnalyzer) AddRunningKubeSource(c kubelib.Client) {
   327  	sa.AddRunningKubeSourceWithRevision(c, "default", false)
   328  }
   329  
   330  func isIstioConfigMap(obj any) bool {
   331  	cObj, ok := obj.(*v1.ConfigMap)
   332  	if !ok {
   333  		return false
   334  	}
   335  	if _, ok = cObj.GetAnnotations()[k8sresourcelock.LeaderElectionRecordAnnotationKey]; ok {
   336  		return false
   337  	}
   338  	return strings.HasPrefix(cObj.GetName(), "istio")
   339  }
   340  
   341  var secretFieldSelector = fields.AndSelectors(
   342  	fields.OneTermNotEqualSelector("type", "helm.sh/release.v1"),
   343  	fields.OneTermNotEqualSelector("type", string(v1.SecretTypeServiceAccountToken))).String()
   344  
   345  func (sa *IstiodAnalyzer) GetFiltersByGVK() map[config.GroupVersionKind]kubetypes.Filter {
   346  	return map[config.GroupVersionKind]kubetypes.Filter{
   347  		gvk.ConfigMap: {
   348  			Namespace:    sa.istioNamespace.String(),
   349  			ObjectFilter: kubetypes.NewStaticObjectFilter(isIstioConfigMap),
   350  		},
   351  		gvk.Secret: {
   352  			FieldSelector: secretFieldSelector,
   353  		},
   354  	}
   355  }
   356  
   357  func (sa *IstiodAnalyzer) AddRunningKubeSourceWithRevision(c kubelib.Client, revision string, remote bool) {
   358  	// This makes the assumption we don't care about Helm secrets or SA token secrets - two common
   359  	// large secrets in clusters.
   360  	// This is a best effort optimization only; the code would behave correctly if we watched all secrets.
   361  
   362  	ignoredNamespacesSelectorForField := func(field string) string {
   363  		selectors := make([]fields.Selector, 0, len(inject.IgnoredNamespaces))
   364  		for _, ns := range inject.IgnoredNamespaces.UnsortedList() {
   365  			selectors = append(selectors, fields.OneTermNotEqualSelector(field, ns))
   366  		}
   367  		return fields.AndSelectors(selectors...).String()
   368  	}
   369  
   370  	namespaceFieldSelector := ignoredNamespacesSelectorForField("metadata.name")
   371  	generalSelectors := ignoredNamespacesSelectorForField("metadata.namespace")
   372  
   373  	// TODO: are either of these string constants intended to vary?
   374  	// We gets Istio CRD resources with a specific revision.
   375  	krs := sa.kubeResources.Remove(kuberesource.DefaultExcludedSchemas().All()...)
   376  	if remote {
   377  		krs = krs.Remove(kuberesource.DefaultRemoteClusterExcludedSchemas().All()...)
   378  	}
   379  	store := crdclient.NewForSchemas(c, crdclient.Option{
   380  		Revision:     revision,
   381  		DomainSuffix: "cluster.local",
   382  		Identifier:   "analysis-controller",
   383  		FiltersByGVK: map[config.GroupVersionKind]kubetypes.Filter{
   384  			gvk.ConfigMap: {
   385  				Namespace:    sa.istioNamespace.String(),
   386  				ObjectFilter: kubetypes.NewStaticObjectFilter(isIstioConfigMap),
   387  			},
   388  		},
   389  	}, krs)
   390  	sa.stores = append(sa.stores, store)
   391  
   392  	// We gets service discovery resources without a specific revision.
   393  	krs = sa.kubeResources.Intersect(kuberesource.DefaultExcludedSchemas())
   394  	if remote {
   395  		krs = krs.Remove(kuberesource.DefaultRemoteClusterExcludedSchemas().All()...)
   396  	}
   397  	store = crdclient.NewForSchemas(c, crdclient.Option{
   398  		DomainSuffix: "cluster.local",
   399  		Identifier:   "analysis-controller",
   400  		FiltersByGVK: map[config.GroupVersionKind]kubetypes.Filter{
   401  			gvk.Secret: {
   402  				FieldSelector: secretFieldSelector,
   403  			},
   404  			gvk.Namespace: {
   405  				FieldSelector: namespaceFieldSelector,
   406  			},
   407  			gvk.Service: {
   408  				FieldSelector: generalSelectors,
   409  			},
   410  			gvk.Pod: {
   411  				FieldSelector: generalSelectors,
   412  			},
   413  			gvk.Deployment: {
   414  				FieldSelector: generalSelectors,
   415  			},
   416  		},
   417  	}, krs)
   418  	// RunAndWait must be called after NewForSchema so that the informers are all created and started.
   419  	if remote {
   420  		clusterID := c.ClusterID()
   421  		if clusterID == "" {
   422  			clusterID = "default"
   423  		}
   424  		sa.multiClusterStores[clusterID] = store
   425  	} else {
   426  		sa.stores = append(sa.stores, store)
   427  	}
   428  	sa.clientsToRun = append(sa.clientsToRun, c)
   429  
   430  	// Since we're using a running k8s source, try to get meshconfig and meshnetworks from the configmap.
   431  	if err := sa.addRunningKubeIstioConfigMapSource(c); err != nil {
   432  		_, err := c.Kube().CoreV1().Namespaces().Get(context.TODO(), sa.istioNamespace.String(), metav1.GetOptions{})
   433  		if kerrors.IsNotFound(err) {
   434  			// An AnalysisMessage already show up to warn the absence of istio-system namespace, so making it debug level.
   435  			scope.Analysis.Debugf("%v namespace not found. Istio may not be installed in the target cluster. "+
   436  				"Using default mesh configuration values for analysis", sa.istioNamespace.String())
   437  		} else if err != nil {
   438  			scope.Analysis.Errorf("error getting mesh config from running kube source: %v", err)
   439  		}
   440  	}
   441  }
   442  
   443  // AddSource adds a source based on user supplied configstore to the current IstiodAnalyzer
   444  // Assumes that the source has same or subset of resource types that this analyzer is configured with.
   445  // This can be used by external users who import the analyzer as a module within their own controllers.
   446  func (sa *IstiodAnalyzer) AddSource(src model.ConfigStoreController) {
   447  	sa.stores = append(sa.stores, src)
   448  }
   449  
   450  // AddFileKubeMeshConfig gets mesh config from the specified yaml file
   451  func (sa *IstiodAnalyzer) AddFileKubeMeshConfig(file string) error {
   452  	by, err := os.ReadFile(file)
   453  	if err != nil {
   454  		return err
   455  	}
   456  
   457  	cfg, err := mesh.ApplyMeshConfigDefaults(string(by))
   458  	if err != nil {
   459  		return err
   460  	}
   461  
   462  	sa.meshCfg = cfg
   463  	return nil
   464  }
   465  
   466  // AddFileKubeMeshNetworks gets a file meshnetworks and add it to the analyzer.
   467  func (sa *IstiodAnalyzer) AddFileKubeMeshNetworks(file string) error {
   468  	mn, err := mesh.ReadMeshNetworks(file)
   469  	if err != nil {
   470  		return err
   471  	}
   472  
   473  	sa.meshNetworks = mn
   474  	return nil
   475  }
   476  
   477  // AddDefaultResources adds some basic dummy Istio resources, based on mesh configuration.
   478  // This is useful for files-only analysis cases where we don't expect the user to be including istio system resources
   479  // and don't want to generate false positives because they aren't there.
   480  // Respect mesh config when deciding which default resources should be generated
   481  func (sa *IstiodAnalyzer) AddDefaultResources() error {
   482  	var readers []ReaderSource
   483  
   484  	if sa.meshCfg.GetIngressControllerMode() != v1alpha1.MeshConfig_OFF {
   485  		ingressResources, err := getDefaultIstioIngressGateway(sa.istioNamespace.String(), sa.meshCfg.GetIngressService())
   486  		if err != nil {
   487  			return err
   488  		}
   489  		readers = append(readers, ReaderSource{Reader: strings.NewReader(ingressResources), Name: "internal-ingress"})
   490  	}
   491  
   492  	if len(readers) == 0 {
   493  		return nil
   494  	}
   495  
   496  	return sa.AddReaderKubeSource(readers)
   497  }
   498  
   499  func (sa *IstiodAnalyzer) RegisterEventHandler(kind config.GroupVersionKind, handler model.EventHandler) {
   500  	for _, store := range sa.stores {
   501  		store.RegisterEventHandler(kind, handler)
   502  	}
   503  }
   504  
   505  func (sa *IstiodAnalyzer) Schemas() collection.Schemas {
   506  	result := collection.NewSchemasBuilder()
   507  	for _, store := range sa.stores {
   508  		for _, schema := range store.Schemas().All() {
   509  			result.MustAdd(schema)
   510  		}
   511  	}
   512  	return result.Build()
   513  }
   514  
   515  func (sa *IstiodAnalyzer) addRunningKubeIstioConfigMapSource(client kubelib.Client) error {
   516  	meshConfigMap, err := client.Kube().CoreV1().ConfigMaps(string(sa.istioNamespace)).Get(context.TODO(), meshConfigMapName, metav1.GetOptions{})
   517  	if err != nil {
   518  		return fmt.Errorf("could not read configmap %q from namespace %q: %v", meshConfigMapName, sa.istioNamespace, err)
   519  	}
   520  
   521  	configYaml, ok := meshConfigMap.Data[meshConfigMapKey]
   522  	if !ok {
   523  		return fmt.Errorf("missing config map key %q", meshConfigMapKey)
   524  	}
   525  
   526  	cfg, err := mesh.ApplyMeshConfigDefaults(configYaml)
   527  	if err != nil {
   528  		return fmt.Errorf("error parsing mesh config: %v", err)
   529  	}
   530  
   531  	sa.meshCfg = cfg
   532  
   533  	meshNetworksYaml, ok := meshConfigMap.Data[meshNetworksMapKey]
   534  	if !ok {
   535  		return fmt.Errorf("missing config map key %q", meshNetworksMapKey)
   536  	}
   537  
   538  	mn, err := mesh.ParseMeshNetworks(meshNetworksYaml)
   539  	if err != nil {
   540  		return fmt.Errorf("error parsing mesh networks: %v", err)
   541  	}
   542  
   543  	sa.meshNetworks = mn
   544  	return nil
   545  }
   546  
   547  // AddSourceForCluster adds a source based on user supplied configstore to the current IstiodAnalyzer with cluster specified.
   548  // It functions like the same as AddSource, but it adds the source to the specified cluster.
   549  func (sa *IstiodAnalyzer) AddSourceForCluster(src model.ConfigStoreController, clusterName cluster.ID) {
   550  	sa.multiClusterStores[clusterName] = src
   551  }
   552  
   553  // CollectionReporterFn is a hook function called whenever a collection is accessed through the AnalyzingDistributor's context
   554  type CollectionReporterFn func(config.GroupVersionKind)
   555  
   556  // copied from processing/snapshotter/analyzingdistributor.go
   557  func filterMessages(messages diag.Messages, namespaces sets.Set[resource.Namespace], suppressions []AnalysisSuppression) diag.Messages {
   558  	nsNames := sets.New[string]()
   559  	for k := range namespaces {
   560  		nsNames.Insert(k.String())
   561  	}
   562  
   563  	var msgs diag.Messages
   564  FilterMessages:
   565  	for _, m := range messages {
   566  		// Only keep messages for resources in namespaces we want to analyze if the
   567  		// message doesn't have an origin (meaning we can't determine the
   568  		// namespace). Also kept are cluster-level resources where the namespace is
   569  		// the empty string. If no such limit is specified, keep them all.
   570  		if len(namespaces) > 0 && m.Resource != nil && m.Resource.Origin.Namespace() != "" {
   571  			if !nsNames.Contains(m.Resource.Origin.Namespace().String()) {
   572  				continue FilterMessages
   573  			}
   574  		}
   575  
   576  		// Filter out any messages on resources with suppression annotations.
   577  		if m.Resource != nil && m.Resource.Metadata.Annotations[annotation.GalleyAnalyzeSuppress.Name] != "" {
   578  			for _, code := range strings.Split(m.Resource.Metadata.Annotations[annotation.GalleyAnalyzeSuppress.Name], ",") {
   579  				if code == "*" || m.Type.Code() == code {
   580  					scope.Analysis.Debugf("Suppressing code %s on resource %s due to resource annotation", m.Type.Code(), m.Resource.Origin.FriendlyName())
   581  					continue FilterMessages
   582  				}
   583  			}
   584  		}
   585  
   586  		// Filter out any messages that match our suppressions.
   587  		for _, s := range suppressions {
   588  			if m.Resource == nil || s.Code != m.Type.Code() {
   589  				continue
   590  			}
   591  
   592  			if !glob.Glob(s.ResourceName, m.Resource.Origin.FriendlyName()) {
   593  				continue
   594  			}
   595  			scope.Analysis.Debugf("Suppressing code %s on resource %s due to suppressions list", m.Type.Code(), m.Resource.Origin.FriendlyName())
   596  			continue FilterMessages
   597  		}
   598  
   599  		msgs = append(msgs, m)
   600  	}
   601  	return msgs
   602  }
   603  
   604  // AnalysisSuppression describes a resource and analysis code to be suppressed
   605  // (e.g. ignored) during analysis. Used when a particular message code is to be
   606  // ignored for a specific resource.
   607  type AnalysisSuppression struct {
   608  	// Code is the analysis code to suppress (e.g. "IST0104").
   609  	Code string
   610  
   611  	// ResourceName is the name of the resource to suppress the message for. For
   612  	// K8s resources it has the same form as used by istioctl (e.g.
   613  	// "DestinationRule default.istio-system"). Note that globbing wildcards are
   614  	// supported (e.g. "DestinationRule *.istio-system").
   615  	ResourceName string
   616  }
   617  
   618  // ReaderSource is a tuple of a io.Reader and filepath.
   619  type ReaderSource struct {
   620  	// Name is the name of the source (commonly the path to a file, but can be "-" for sources read from stdin or "" if completely synthetic).
   621  	Name string
   622  	// Reader is the reader instance to use.
   623  	Reader io.Reader
   624  }