istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/helmreconciler/reconciler.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 helmreconciler
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	"k8s.io/apimachinery/pkg/api/meta"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  	"sigs.k8s.io/yaml"
    33  
    34  	"istio.io/api/label"
    35  	"istio.io/api/operator/v1alpha1"
    36  	revtag "istio.io/istio/istioctl/pkg/tag"
    37  	"istio.io/istio/istioctl/pkg/util/formatting"
    38  	istioV1Alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    39  	"istio.io/istio/operator/pkg/helm"
    40  	"istio.io/istio/operator/pkg/metrics"
    41  	"istio.io/istio/operator/pkg/name"
    42  	"istio.io/istio/operator/pkg/object"
    43  	"istio.io/istio/operator/pkg/util"
    44  	"istio.io/istio/operator/pkg/util/clog"
    45  	"istio.io/istio/operator/pkg/util/progress"
    46  	"istio.io/istio/pkg/config"
    47  	"istio.io/istio/pkg/config/analysis"
    48  	"istio.io/istio/pkg/config/analysis/analyzers/webhook"
    49  	"istio.io/istio/pkg/config/analysis/diag"
    50  	"istio.io/istio/pkg/config/analysis/local"
    51  	"istio.io/istio/pkg/config/resource"
    52  	"istio.io/istio/pkg/config/schema/gvk"
    53  	"istio.io/istio/pkg/config/schema/gvr"
    54  	"istio.io/istio/pkg/kube"
    55  	"istio.io/istio/pkg/version"
    56  )
    57  
    58  // HelmReconciler reconciles resources rendered by a set of helm charts.
    59  type HelmReconciler struct {
    60  	client     client.Client
    61  	kubeClient kube.Client
    62  	iop        *istioV1Alpha1.IstioOperator
    63  	opts       *Options
    64  	// copy of the last generated manifests.
    65  	manifests name.ManifestMap
    66  	// dependencyWaitCh is a map of signaling channels. A parent with children ch1...chN will signal
    67  	// dependencyWaitCh[ch1]...dependencyWaitCh[chN] when it's completely installed.
    68  	dependencyWaitCh map[name.ComponentName]chan struct{}
    69  
    70  	// The fields below are for metrics and reporting
    71  	countLock     *sync.Mutex
    72  	prunedKindSet map[schema.GroupKind]struct{}
    73  }
    74  
    75  // Options are options for HelmReconciler.
    76  type Options struct {
    77  	// DryRun executes all actions but does not write anything to the cluster.
    78  	DryRun bool
    79  	// Log is a console logger for user visible CLI output.
    80  	Log clog.Logger
    81  	// Wait determines if we will wait for resources to be fully applied. Only applies to components that have no
    82  	// dependencies.
    83  	Wait bool
    84  	// WaitTimeout controls the amount of time to wait for resources in a component to become ready before giving up.
    85  	WaitTimeout time.Duration
    86  	// Log tracks the installation progress for all components.
    87  	ProgressLog *progress.Log
    88  	// Force ignores validation errors
    89  	Force bool
    90  	// SkipPrune will skip pruning
    91  	SkipPrune bool
    92  }
    93  
    94  var defaultOptions = &Options{
    95  	Log:         clog.NewDefaultLogger(),
    96  	ProgressLog: progress.NewLog(),
    97  }
    98  
    99  // NewHelmReconciler creates a HelmReconciler and returns a ptr to it
   100  func NewHelmReconciler(client client.Client, kubeClient kube.Client, iop *istioV1Alpha1.IstioOperator, opts *Options) (*HelmReconciler, error) {
   101  	if opts == nil {
   102  		opts = defaultOptions
   103  	}
   104  	if opts.ProgressLog == nil {
   105  		opts.ProgressLog = progress.NewLog()
   106  	}
   107  	if int64(opts.WaitTimeout) == 0 {
   108  		if waitForResourcesTimeoutStr, found := os.LookupEnv("WAIT_FOR_RESOURCES_TIMEOUT"); found {
   109  			if waitForResourcesTimeout, err := time.ParseDuration(waitForResourcesTimeoutStr); err == nil {
   110  				opts.WaitTimeout = waitForResourcesTimeout
   111  			} else {
   112  				scope.Warnf("invalid env variable value: %s for 'WAIT_FOR_RESOURCES_TIMEOUT'! falling back to default value...", waitForResourcesTimeoutStr)
   113  				// fallback to default wait resource timeout
   114  				opts.WaitTimeout = defaultWaitResourceTimeout
   115  			}
   116  		} else {
   117  			// fallback to default wait resource timeout
   118  			opts.WaitTimeout = defaultWaitResourceTimeout
   119  		}
   120  	}
   121  	if iop == nil {
   122  		// allows controller code to function for cases where IOP is not provided (e.g. operator remove).
   123  		iop = &istioV1Alpha1.IstioOperator{}
   124  		iop.Spec = &v1alpha1.IstioOperatorSpec{}
   125  	}
   126  	return &HelmReconciler{
   127  		client:           client,
   128  		kubeClient:       kubeClient,
   129  		iop:              iop,
   130  		opts:             opts,
   131  		dependencyWaitCh: initDependencies(),
   132  		countLock:        &sync.Mutex{},
   133  		prunedKindSet:    make(map[schema.GroupKind]struct{}),
   134  	}, nil
   135  }
   136  
   137  // initDependencies initializes the dependencies channel tree.
   138  func initDependencies() map[name.ComponentName]chan struct{} {
   139  	ret := make(map[name.ComponentName]chan struct{})
   140  	for _, parent := range ComponentDependencies {
   141  		for _, child := range parent {
   142  			ret[child] = make(chan struct{}, 1)
   143  		}
   144  	}
   145  	return ret
   146  }
   147  
   148  // Reconcile reconciles the associated resources.
   149  func (h *HelmReconciler) Reconcile() (*v1alpha1.InstallStatus, error) {
   150  	if err := util.CreateNamespace(h.kubeClient.Kube(), istioV1Alpha1.Namespace(h.iop.Spec), h.networkName(), h.opts.DryRun); err != nil {
   151  		return nil, err
   152  	}
   153  	manifestMap, err := h.RenderCharts()
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	err = h.analyzeWebhooks(manifestMap[name.PilotComponentName])
   159  	if err != nil {
   160  		if h.opts.Force {
   161  			scope.Error("invalid webhook configs; continuing because of --force")
   162  		} else {
   163  			return nil, err
   164  		}
   165  	}
   166  	status := h.processRecursive(manifestMap)
   167  
   168  	var pruneErr error
   169  	if !h.opts.SkipPrune && !h.opts.DryRun {
   170  		h.opts.ProgressLog.SetState(progress.StatePruning)
   171  		pruneErr = h.Prune(manifestMap, false)
   172  		h.reportPrunedObjectKind()
   173  	}
   174  	return status, pruneErr
   175  }
   176  
   177  // processRecursive processes the given manifests in an order of dependencies defined in h. Dependencies are a tree,
   178  // where a child must wait for the parent to complete before starting.
   179  func (h *HelmReconciler) processRecursive(manifests name.ManifestMap) *v1alpha1.InstallStatus {
   180  	componentStatus := make(map[string]*v1alpha1.InstallStatus_VersionStatus)
   181  
   182  	// mu protects the shared InstallStatus componentStatus across goroutines
   183  	var mu sync.Mutex
   184  	// wg waits for all manifest processing goroutines to finish
   185  	var wg sync.WaitGroup
   186  
   187  	for c, ms := range manifests {
   188  		c, ms := c, ms
   189  		wg.Add(1)
   190  		go func() {
   191  			var appliedResult AppliedResult
   192  			defer wg.Done()
   193  			if s := h.dependencyWaitCh[c]; s != nil {
   194  				scope.Infof("%s is waiting on dependency...", c)
   195  				<-s
   196  				scope.Infof("Dependency for %s has completed, proceeding.", c)
   197  			}
   198  
   199  			// Possible paths for status are RECONCILING -> {NONE, ERROR, HEALTHY}. NONE means component has no resources.
   200  			// In NONE case, the component is not shown in overall status.
   201  			mu.Lock()
   202  			setStatus(componentStatus, c, v1alpha1.InstallStatus_RECONCILING, nil)
   203  			mu.Unlock()
   204  
   205  			status := v1alpha1.InstallStatus_NONE
   206  			var err error
   207  			if len(ms) != 0 {
   208  				m := name.Manifest{
   209  					Name:    c,
   210  					Content: name.MergeManifestSlices(ms),
   211  				}
   212  				appliedResult, err = h.ApplyManifest(m)
   213  				if err != nil {
   214  					status = v1alpha1.InstallStatus_ERROR
   215  				} else if appliedResult.Succeed() {
   216  					status = v1alpha1.InstallStatus_HEALTHY
   217  				}
   218  			}
   219  
   220  			mu.Lock()
   221  			setStatus(componentStatus, c, status, err)
   222  			mu.Unlock()
   223  
   224  			// Signal all the components that depend on us.
   225  			for _, ch := range ComponentDependencies[c] {
   226  				scope.Infof("Unblocking dependency %s.", ch)
   227  				h.dependencyWaitCh[ch] <- struct{}{}
   228  			}
   229  		}()
   230  	}
   231  	wg.Wait()
   232  
   233  	metrics.ReportOwnedResourceCounts()
   234  
   235  	out := &v1alpha1.InstallStatus{
   236  		Status:          overallStatus(componentStatus),
   237  		ComponentStatus: componentStatus,
   238  	}
   239  
   240  	return out
   241  }
   242  
   243  // Delete resources associated with the custom resource instance
   244  func (h *HelmReconciler) Delete() error {
   245  	defer func() {
   246  		metrics.ReportOwnedResourceCounts()
   247  		h.reportPrunedObjectKind()
   248  	}()
   249  	iop := h.iop
   250  	if iop.Spec.Revision == "" {
   251  		err := h.Prune(nil, true)
   252  		return err
   253  	}
   254  	// Delete IOP with revision:
   255  	// for this case we update the status field to pending if there are still proxies pointing to this revision
   256  	// and we do not prune shared resources, same effect as `istioctl uninstall --revision foo` command.
   257  	status, err := h.PruneControlPlaneByRevisionWithController(iop.Spec)
   258  	if err != nil {
   259  		return err
   260  	}
   261  
   262  	// check status here because terminating iop's status can't be updated.
   263  	if status.Status == v1alpha1.InstallStatus_ACTION_REQUIRED {
   264  		return fmt.Errorf("action is required before deleting the iop instance: %s", status.Message)
   265  	}
   266  
   267  	// updating status taking no effect for terminating resources.
   268  	if err := h.SetStatusComplete(status); err != nil {
   269  		return err
   270  	}
   271  	return nil
   272  }
   273  
   274  func (h *HelmReconciler) DeleteIOPInClusterIfExists(iop *istioV1Alpha1.IstioOperator) {
   275  	// Delete the previous IstioOperator CR if it exists.
   276  	objectKey := client.ObjectKeyFromObject(iop)
   277  	receiver := &unstructured.Unstructured{}
   278  	receiver.SetGroupVersionKind(istioV1Alpha1.IstioOperatorGVK)
   279  	if err := h.client.Get(context.TODO(), objectKey, receiver); err == nil {
   280  		_ = h.client.Delete(context.TODO(), receiver)
   281  	}
   282  }
   283  
   284  // SetStatusBegin updates the status field on the IstioOperator instance before reconciling.
   285  func (h *HelmReconciler) SetStatusBegin() error {
   286  	isop := &istioV1Alpha1.IstioOperator{}
   287  	if err := h.getClient().Get(context.TODO(), config.NamespacedName(h.iop), isop); err != nil {
   288  		if runtime.IsNotRegisteredError(err) {
   289  			// CRD not yet installed in cluster, nothing to update.
   290  			return nil
   291  		}
   292  		return fmt.Errorf("failed to get IstioOperator before updating status due to %v", err)
   293  	}
   294  	if isop.Status == nil {
   295  		isop.Status = &v1alpha1.InstallStatus{Status: v1alpha1.InstallStatus_RECONCILING}
   296  	} else {
   297  		cs := isop.Status.ComponentStatus
   298  		for cn := range cs {
   299  			cs[cn] = &v1alpha1.InstallStatus_VersionStatus{
   300  				Status: v1alpha1.InstallStatus_RECONCILING,
   301  			}
   302  		}
   303  		isop.Status.Status = v1alpha1.InstallStatus_RECONCILING
   304  	}
   305  	return h.getClient().Status().Update(context.TODO(), isop)
   306  }
   307  
   308  // SetStatusComplete updates the status field on the IstioOperator instance based on the resulting err parameter.
   309  func (h *HelmReconciler) SetStatusComplete(status *v1alpha1.InstallStatus) error {
   310  	iop := &istioV1Alpha1.IstioOperator{}
   311  	if err := h.getClient().Get(context.TODO(), config.NamespacedName(h.iop), iop); err != nil {
   312  		return fmt.Errorf("failed to get IstioOperator before updating status due to %v", err)
   313  	}
   314  	iop.Status = status
   315  	return h.getClient().Status().Update(context.TODO(), iop)
   316  }
   317  
   318  // setStatus sets the status for the component with the given name, which is a key in the given map.
   319  // If the status is InstallStatus_NONE, the component name is deleted from the map.
   320  // Otherwise, if the map key/value is missing, one is created.
   321  func setStatus(s map[string]*v1alpha1.InstallStatus_VersionStatus, componentName name.ComponentName, status v1alpha1.InstallStatus_Status, err error) {
   322  	cn := string(componentName)
   323  	if status == v1alpha1.InstallStatus_NONE {
   324  		delete(s, cn)
   325  		return
   326  	}
   327  	if _, ok := s[cn]; !ok {
   328  		s[cn] = &v1alpha1.InstallStatus_VersionStatus{}
   329  	}
   330  	s[cn].Status = status
   331  	if err != nil {
   332  		s[cn].Error = err.Error()
   333  	}
   334  }
   335  
   336  // overallStatus returns the summary status over all components.
   337  // - If all components are HEALTHY, overall status is HEALTHY.
   338  // - If one or more components are RECONCILING and others are HEALTHY, overall status is RECONCILING.
   339  // - If one or more components are UPDATING and others are HEALTHY, overall status is UPDATING.
   340  // - If components are a mix of RECONCILING, UPDATING and HEALTHY, overall status is UPDATING.
   341  // - If any component is in ERROR state, overall status is ERROR.
   342  func overallStatus(componentStatus map[string]*v1alpha1.InstallStatus_VersionStatus) v1alpha1.InstallStatus_Status {
   343  	ret := v1alpha1.InstallStatus_HEALTHY
   344  	for _, cs := range componentStatus {
   345  		if cs.Status == v1alpha1.InstallStatus_ERROR {
   346  			ret = v1alpha1.InstallStatus_ERROR
   347  			break
   348  		} else if cs.Status == v1alpha1.InstallStatus_UPDATING {
   349  			ret = v1alpha1.InstallStatus_UPDATING
   350  			break
   351  		} else if cs.Status == v1alpha1.InstallStatus_RECONCILING {
   352  			ret = v1alpha1.InstallStatus_RECONCILING
   353  			break
   354  		}
   355  	}
   356  	return ret
   357  }
   358  
   359  // getCoreOwnerLabels returns a map of labels for associating installation resources. This is the common
   360  // labels shared between all resources; see getOwnerLabels to get labels per-component labels
   361  func (h *HelmReconciler) getCoreOwnerLabels() (map[string]string, error) {
   362  	crName, err := h.getCRName()
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  	crNamespace, err := h.getCRNamespace()
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	labels := make(map[string]string)
   371  
   372  	labels[operatorLabelStr] = operatorReconcileStr
   373  	if crName != "" {
   374  		labels[OwningResourceName] = crName
   375  	}
   376  	if crNamespace != "" {
   377  		labels[OwningResourceNamespace] = crNamespace
   378  	}
   379  	labels[istioVersionLabelStr] = version.Info.Version
   380  
   381  	revision := ""
   382  	if h.iop != nil {
   383  		revision = h.iop.Spec.Revision
   384  	}
   385  	if revision == "" {
   386  		revision = "default"
   387  	}
   388  	labels[label.IoIstioRev.Name] = revision
   389  
   390  	return labels, nil
   391  }
   392  
   393  func (h *HelmReconciler) addComponentLabels(coreLabels map[string]string, componentName string) map[string]string {
   394  	labels := map[string]string{}
   395  	for k, v := range coreLabels {
   396  		labels[k] = v
   397  	}
   398  
   399  	labels[IstioComponentLabelStr] = componentName
   400  
   401  	return labels
   402  }
   403  
   404  // getOwnerLabels returns a map of labels for the given component name, revision and owning CR resource name.
   405  func (h *HelmReconciler) getOwnerLabels(componentName string) (map[string]string, error) {
   406  	labels, err := h.getCoreOwnerLabels()
   407  	if err != nil {
   408  		return nil, err
   409  	}
   410  
   411  	return h.addComponentLabels(labels, componentName), nil
   412  }
   413  
   414  // applyLabelsAndAnnotations applies owner labels and annotations to the object.
   415  func (h *HelmReconciler) applyLabelsAndAnnotations(obj runtime.Object, componentName string) error {
   416  	labels, err := h.getOwnerLabels(componentName)
   417  	if err != nil {
   418  		return err
   419  	}
   420  
   421  	for k, v := range labels {
   422  		err := util.SetLabel(obj, k, v)
   423  		if err != nil {
   424  			return err
   425  		}
   426  	}
   427  	return nil
   428  }
   429  
   430  // getCRName returns the name of the CR associated with h.
   431  func (h *HelmReconciler) getCRName() (string, error) {
   432  	if h.iop == nil {
   433  		return "", nil
   434  	}
   435  	objAccessor, err := meta.Accessor(h.iop)
   436  	if err != nil {
   437  		return "", err
   438  	}
   439  	return objAccessor.GetName(), nil
   440  }
   441  
   442  // getCRHash returns the cluster unique hash of the CR associated with h.
   443  func (h *HelmReconciler) getCRHash(componentName string) (string, error) {
   444  	crName, err := h.getCRName()
   445  	if err != nil {
   446  		return "", err
   447  	}
   448  	crNamespace, err := h.getCRNamespace()
   449  	if err != nil {
   450  		return "", err
   451  	}
   452  	var host string
   453  	if h.kubeClient != nil && h.kubeClient.RESTConfig() != nil {
   454  		host = h.kubeClient.RESTConfig().Host
   455  	}
   456  	return strings.Join([]string{crName, crNamespace, componentName, host}, "-"), nil
   457  }
   458  
   459  // getCRNamespace returns the namespace of the CR associated with h.
   460  func (h *HelmReconciler) getCRNamespace() (string, error) {
   461  	if h.iop == nil {
   462  		return "", nil
   463  	}
   464  	objAccessor, err := meta.Accessor(h.iop)
   465  	if err != nil {
   466  		return "", err
   467  	}
   468  	return objAccessor.GetNamespace(), nil
   469  }
   470  
   471  // getClient returns the kubernetes client associated with this HelmReconciler
   472  func (h *HelmReconciler) getClient() client.Client {
   473  	return h.client
   474  }
   475  
   476  func (h *HelmReconciler) addPrunedKind(gk schema.GroupKind) {
   477  	h.countLock.Lock()
   478  	defer h.countLock.Unlock()
   479  	h.prunedKindSet[gk] = struct{}{}
   480  }
   481  
   482  func (h *HelmReconciler) reportPrunedObjectKind() {
   483  	h.countLock.Lock()
   484  	defer h.countLock.Unlock()
   485  	for gvk := range h.prunedKindSet {
   486  		metrics.ResourcePruneTotal.
   487  			With(metrics.ResourceKindLabel.Value(util.GKString(gvk))).
   488  			Increment()
   489  	}
   490  }
   491  
   492  func (h *HelmReconciler) analyzeWebhooks(whs []string) error {
   493  	if len(whs) == 0 {
   494  		return nil
   495  	}
   496  
   497  	// Add webhook manifests to be applied
   498  	var localWebhookYAMLReaders []local.ReaderSource
   499  	var parsedK8sObjects object.K8sObjects
   500  	exists := revtag.PreviousInstallExists(context.Background(), h.kubeClient.Kube())
   501  	for i, wh := range whs {
   502  		k8sObjects, err := object.ParseK8sObjectsFromYAMLManifest(wh)
   503  		if err != nil {
   504  			return err
   505  		}
   506  		objYaml, err := k8sObjects.YAMLManifest()
   507  		if err != nil {
   508  			return err
   509  		}
   510  		// Here if we need to create a default tag, we need to skip the webhooks that are going to be deactivated.
   511  		if !DetectIfTagWebhookIsNeeded(h.iop, exists) {
   512  			whReaderSource := local.ReaderSource{
   513  				Name:   fmt.Sprintf("installed-webhook-%d", i),
   514  				Reader: strings.NewReader(objYaml),
   515  			}
   516  			localWebhookYAMLReaders = append(localWebhookYAMLReaders, whReaderSource)
   517  		}
   518  		parsedK8sObjects = append(parsedK8sObjects, k8sObjects...)
   519  	}
   520  
   521  	sa := local.NewSourceAnalyzer(analysis.Combine("webhook", &webhook.Analyzer{
   522  		SkipServiceCheck:             true,
   523  		SkipDefaultRevisionedWebhook: DetectIfTagWebhookIsNeeded(h.iop, exists),
   524  	}), resource.Namespace(h.iop.Spec.GetNamespace()), resource.Namespace(istioV1Alpha1.Namespace(h.iop.Spec)), nil)
   525  
   526  	// Add in-cluster webhooks
   527  	objects := &unstructured.UnstructuredList{}
   528  	objects.SetGroupVersionKind(gvk.MutatingWebhookConfiguration.Kubernetes())
   529  	err := h.client.List(context.Background(), objects, &client.ListOptions{})
   530  	if err != nil {
   531  		return err
   532  	}
   533  	for i, obj := range objects.Items {
   534  		objYAML, err := object.NewK8sObject(&obj, nil, nil).YAML()
   535  		if err != nil {
   536  			return err
   537  		}
   538  		whReaderSource := local.ReaderSource{
   539  			Name:   fmt.Sprintf("in-cluster-webhook-%d", i),
   540  			Reader: strings.NewReader(string(objYAML)),
   541  		}
   542  		err = sa.AddReaderKubeSource([]local.ReaderSource{whReaderSource})
   543  		if err != nil {
   544  			return err
   545  		}
   546  	}
   547  
   548  	err = sa.AddReaderKubeSource(localWebhookYAMLReaders)
   549  	if err != nil {
   550  		return err
   551  	}
   552  
   553  	// Analyze webhooks
   554  	res, err := sa.Analyze(make(chan struct{}))
   555  	if err != nil {
   556  		return err
   557  	}
   558  	relevantMessages := filterOutBasedOnResources(res.Messages, parsedK8sObjects)
   559  	if len(relevantMessages) > 0 {
   560  		o, err := formatting.Print(relevantMessages, formatting.LogFormat, false)
   561  		if err != nil {
   562  			return err
   563  		}
   564  		return fmt.Errorf("creating default tag would conflict:\n%v", o)
   565  	}
   566  	return nil
   567  }
   568  
   569  func filterOutBasedOnResources(ms diag.Messages, resources object.K8sObjects) diag.Messages {
   570  	outputMessages := diag.Messages{}
   571  	for _, m := range ms {
   572  		for _, rs := range resources {
   573  			if rs.Name == m.Resource.Metadata.FullName.Name.String() {
   574  				outputMessages = append(outputMessages, m)
   575  				break
   576  			}
   577  		}
   578  	}
   579  	return outputMessages
   580  }
   581  
   582  func (h *HelmReconciler) networkName() string {
   583  	if h.iop.Spec.GetValues() == nil {
   584  		return ""
   585  	}
   586  	globalI := h.iop.Spec.Values.AsMap()["global"]
   587  	global, ok := globalI.(map[string]any)
   588  	if !ok {
   589  		return ""
   590  	}
   591  	nw, ok := global["network"].(string)
   592  	if !ok {
   593  		return ""
   594  	}
   595  	return nw
   596  }
   597  
   598  type ProcessDefaultWebhookOptions struct {
   599  	Namespace string
   600  	DryRun    bool
   601  }
   602  
   603  func DetectIfTagWebhookIsNeeded(iop *istioV1Alpha1.IstioOperator, exists bool) bool {
   604  	rev := iop.Spec.Revision
   605  	isDefaultInstallation := rev == "" && iop.Spec.Components.Pilot != nil && iop.Spec.Components.Pilot.Enabled.Value
   606  	operatorManageWebhooks := operatorManageWebhooks(iop)
   607  	return !operatorManageWebhooks && (!exists || isDefaultInstallation)
   608  }
   609  
   610  func ProcessDefaultWebhook(client kube.Client, iop *istioV1Alpha1.IstioOperator, exists bool, opt *ProcessDefaultWebhookOptions) (processed bool, err error) {
   611  	// Detect whether previous installation exists prior to performing the installation.
   612  	if !DetectIfTagWebhookIsNeeded(iop, exists) {
   613  		return false, nil
   614  	}
   615  	rev := iop.Spec.Revision
   616  	if rev == "" {
   617  		rev = revtag.DefaultRevisionName
   618  	}
   619  	autoInjectNamespaces := validateEnableNamespacesByDefault(iop)
   620  
   621  	ignorePruneLabel := map[string]string{
   622  		OwningResourceNotPruned: "true",
   623  	}
   624  
   625  	o := &revtag.GenerateOptions{
   626  		Tag:                  revtag.DefaultRevisionName,
   627  		Revision:             rev,
   628  		Overwrite:            true,
   629  		AutoInjectNamespaces: autoInjectNamespaces,
   630  		CustomLabels:         ignorePruneLabel,
   631  		Generate:             opt.DryRun,
   632  	}
   633  	// If tag cannot be created could be remote cluster install, don't fail out.
   634  	tagManifests, err := revtag.Generate(context.Background(), client, o, opt.Namespace)
   635  	if err == nil && !opt.DryRun {
   636  		if err = applyManifests(client, tagManifests); err != nil {
   637  			return false, err
   638  		}
   639  	}
   640  	return true, nil
   641  }
   642  
   643  func applyManifests(kubeClient kube.Client, manifests string) error {
   644  	yamls := strings.Split(manifests, helm.YAMLSeparator)
   645  	for _, yml := range yamls {
   646  		if strings.TrimSpace(yml) == "" {
   647  			continue
   648  		}
   649  
   650  		obj := &unstructured.Unstructured{}
   651  
   652  		if err := yaml.Unmarshal([]byte(yml), obj); err != nil {
   653  			return fmt.Errorf("failed to unmarshal YAML: %w", err)
   654  		}
   655  		var ogvr schema.GroupVersionResource
   656  		if obj.GetKind() == name.MutatingWebhookConfigurationStr {
   657  			ogvr = gvr.MutatingWebhookConfiguration
   658  		} else if obj.GetKind() == name.ValidatingWebhookConfigurationStr {
   659  			ogvr = gvr.ValidatingWebhookConfiguration
   660  		}
   661  
   662  		t := true
   663  		_, err := kubeClient.Dynamic().Resource(ogvr).Namespace(obj.GetNamespace()).Patch(
   664  			context.TODO(), obj.GetName(), types.ApplyPatchType, []byte(yml), metav1.PatchOptions{
   665  				Force:        &t,
   666  				FieldManager: fieldOwnerOperator,
   667  			})
   668  		if err != nil {
   669  			return fmt.Errorf("failed to apply YAML: %w", err)
   670  		}
   671  	}
   672  	return nil
   673  }
   674  
   675  // operatorManageWebhooks returns .Values.global.operatorManageWebhooks from the Istio Operator.
   676  func operatorManageWebhooks(iop *istioV1Alpha1.IstioOperator) bool {
   677  	if iop.Spec.GetValues() == nil {
   678  		return false
   679  	}
   680  	globalValues := iop.Spec.Values.AsMap()["global"]
   681  	global, ok := globalValues.(map[string]any)
   682  	if !ok {
   683  		return false
   684  	}
   685  	omw, ok := global["operatorManageWebhooks"].(bool)
   686  	if !ok {
   687  		return false
   688  	}
   689  	return omw
   690  }
   691  
   692  // validateEnableNamespacesByDefault checks whether there is .Values.sidecarInjectorWebhook.enableNamespacesByDefault set in the Istio Operator.
   693  // Should be used in installer when deciding whether to enable an automatic sidecar injection in all namespaces.
   694  func validateEnableNamespacesByDefault(iop *istioV1Alpha1.IstioOperator) bool {
   695  	if iop == nil || iop.Spec == nil || iop.Spec.Values == nil {
   696  		return false
   697  	}
   698  	sidecarValues := iop.Spec.Values.AsMap()["sidecarInjectorWebhook"]
   699  	sidecarMap, ok := sidecarValues.(map[string]any)
   700  	if !ok {
   701  		return false
   702  	}
   703  	autoInjectNamespaces, ok := sidecarMap["enableNamespacesByDefault"].(bool)
   704  	if !ok {
   705  		return false
   706  	}
   707  
   708  	return autoInjectNamespaces
   709  }