istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/controller/istiocontrolplane/istiocontrolplane_controller.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 istiocontrolplane
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"strings"
    22  
    23  	"k8s.io/apimachinery/pkg/api/errors"
    24  	"k8s.io/apimachinery/pkg/api/meta"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/client-go/rest"
    31  	cache2 "sigs.k8s.io/controller-runtime/pkg/cache"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  	"sigs.k8s.io/controller-runtime/pkg/controller"
    34  	"sigs.k8s.io/controller-runtime/pkg/event"
    35  	"sigs.k8s.io/controller-runtime/pkg/handler"
    36  	"sigs.k8s.io/controller-runtime/pkg/manager"
    37  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    38  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    39  	"sigs.k8s.io/controller-runtime/pkg/source"
    40  	"sigs.k8s.io/yaml"
    41  
    42  	"istio.io/api/operator/v1alpha1"
    43  	revtag "istio.io/istio/istioctl/pkg/tag"
    44  	"istio.io/istio/operator/pkg/apis/istio"
    45  	iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    46  	"istio.io/istio/operator/pkg/cache"
    47  	"istio.io/istio/operator/pkg/helm"
    48  	"istio.io/istio/operator/pkg/helmreconciler"
    49  	"istio.io/istio/operator/pkg/metrics"
    50  	"istio.io/istio/operator/pkg/name"
    51  	"istio.io/istio/operator/pkg/object"
    52  	"istio.io/istio/operator/pkg/tpath"
    53  	"istio.io/istio/operator/pkg/translate"
    54  	"istio.io/istio/operator/pkg/util"
    55  	"istio.io/istio/operator/pkg/util/clog"
    56  	"istio.io/istio/operator/pkg/util/progress"
    57  	"istio.io/istio/pkg/config/constants"
    58  	"istio.io/istio/pkg/kube"
    59  	"istio.io/istio/pkg/log"
    60  	"istio.io/istio/pkg/util/sets"
    61  	"istio.io/istio/pkg/version"
    62  )
    63  
    64  const (
    65  	finalizer = "istio-finalizer.install.istio.io"
    66  	// finalizerMaxRetries defines the maximum number of attempts to remove the finalizer.
    67  	finalizerMaxRetries = 1
    68  	// IgnoreReconcileAnnotation is annotation of IstioOperator CR so it would be ignored during Reconcile loop.
    69  	IgnoreReconcileAnnotation = "install.istio.io/ignoreReconcile"
    70  )
    71  
    72  var (
    73  	scope      = log.RegisterScope("installer", "installer")
    74  	restConfig *rest.Config
    75  )
    76  
    77  type Options struct {
    78  	Force                   bool
    79  	MaxConcurrentReconciles int
    80  }
    81  
    82  // watchedResources contains all resources we will watch and reconcile when changed
    83  // Ideally this would also contain Istio CRDs, but there is a race condition here - we cannot watch
    84  // a type that does not yet exist.
    85  func watchedResources() []schema.GroupVersionKind {
    86  	res := []schema.GroupVersionKind{
    87  		{Group: "apps", Version: "v1", Kind: name.DeploymentStr},
    88  		{Group: "apps", Version: "v1", Kind: name.DaemonSetStr},
    89  		{Group: "", Version: "v1", Kind: name.ServiceStr},
    90  		// Endpoints should not be pruned because these are generated and not in the manifest.
    91  		// {Group: "", Version: "v1", Kind: name.EndpointStr},
    92  		{Group: "", Version: "v1", Kind: name.CMStr},
    93  		{Group: "", Version: "v1", Kind: name.PodStr},
    94  		{Group: "", Version: "v1", Kind: name.SecretStr},
    95  		{Group: "", Version: "v1", Kind: name.SAStr},
    96  		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.RoleBindingStr},
    97  		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.RoleStr},
    98  		{Group: "admissionregistration.k8s.io", Version: "v1", Kind: name.MutatingWebhookConfigurationStr},
    99  		{Group: "admissionregistration.k8s.io", Version: "v1", Kind: name.ValidatingWebhookConfigurationStr},
   100  		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.ClusterRoleStr},
   101  		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.ClusterRoleBindingStr},
   102  		{Group: "apiextensions.k8s.io", Version: "v1", Kind: name.CRDStr},
   103  		{Group: "policy", Version: "v1", Kind: name.PDBStr},
   104  		{Group: "autoscaling", Version: "v2", Kind: name.HPAStr},
   105  	}
   106  	return res
   107  }
   108  
   109  var (
   110  	ownedResourcePredicates = predicate.TypedFuncs[*unstructured.Unstructured]{
   111  		CreateFunc: func(_ event.TypedCreateEvent[*unstructured.Unstructured]) bool {
   112  			// no action
   113  			return false
   114  		},
   115  		GenericFunc: func(_ event.TypedGenericEvent[*unstructured.Unstructured]) bool {
   116  			// no action
   117  			return false
   118  		},
   119  		DeleteFunc: func(e event.TypedDeleteEvent[*unstructured.Unstructured]) bool {
   120  			obj, err := meta.Accessor(e.Object)
   121  			if err != nil {
   122  				return false
   123  			}
   124  			scope.Debugf("got delete event for %s.%s", obj.GetName(), obj.GetNamespace())
   125  			unsObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(e.Object)
   126  			if err != nil {
   127  				return false
   128  			}
   129  			if isOperatorCreatedResource(obj) {
   130  				crName := obj.GetLabels()[helmreconciler.OwningResourceName]
   131  				crNamespace := obj.GetLabels()[helmreconciler.OwningResourceNamespace]
   132  				componentName := obj.GetLabels()[helmreconciler.IstioComponentLabelStr]
   133  				var host string
   134  				if restConfig != nil {
   135  					host = restConfig.Host
   136  				}
   137  				crHash := strings.Join([]string{crName, crNamespace, componentName, host}, "-")
   138  				oh := object.NewK8sObject(&unstructured.Unstructured{Object: unsObj}, nil, nil).Hash()
   139  				cache.RemoveObject(crHash, oh)
   140  				return true
   141  			}
   142  			return false
   143  		},
   144  		UpdateFunc: func(e event.TypedUpdateEvent[*unstructured.Unstructured]) bool {
   145  			// no action
   146  			return false
   147  		},
   148  	}
   149  
   150  	operatorPredicates = predicate.TypedFuncs[*iopv1alpha1.IstioOperator]{
   151  		CreateFunc: func(e event.TypedCreateEvent[*iopv1alpha1.IstioOperator]) bool {
   152  			metrics.IncrementReconcileRequest("create")
   153  			return true
   154  		},
   155  		DeleteFunc: func(e event.TypedDeleteEvent[*iopv1alpha1.IstioOperator]) bool {
   156  			metrics.IncrementReconcileRequest("delete")
   157  			return true
   158  		},
   159  		UpdateFunc: func(e event.TypedUpdateEvent[*iopv1alpha1.IstioOperator]) bool {
   160  			oldIOP := e.ObjectOld
   161  			newIOP := e.ObjectNew
   162  
   163  			// If revision is updated in the IstioOperator resource, we must remove entries
   164  			// from the cache. If the IstioOperator resource is reverted back to match this operator's
   165  			// revision, a clean cache would ensure that the operator Reconcile the IstioOperator,
   166  			// and not skip it.
   167  			if oldIOP.Spec.Revision != newIOP.Spec.Revision {
   168  				var host string
   169  				if restConfig != nil {
   170  					host = restConfig.Host
   171  				}
   172  				for _, component := range name.AllComponentNames {
   173  					crHash := strings.Join([]string{newIOP.Name, newIOP.Namespace, string(component), host}, "-")
   174  					cache.RemoveCache(crHash)
   175  				}
   176  			}
   177  
   178  			if oldIOP.GetDeletionTimestamp() != newIOP.GetDeletionTimestamp() {
   179  				metrics.IncrementReconcileRequest("update_deletion_timestamp")
   180  				return true
   181  			}
   182  
   183  			if oldIOP.GetGeneration() != newIOP.GetGeneration() {
   184  				metrics.IncrementReconcileRequest("update_generation")
   185  				return true
   186  			}
   187  
   188  			// if generation unchanged, spec also unchanged
   189  			return false
   190  		},
   191  	}
   192  )
   193  
   194  // ReconcileIstioOperator reconciles a IstioOperator object
   195  type ReconcileIstioOperator struct {
   196  	// This client, initialized using mgr.Client() above, is a split client
   197  	// that reads objects from the cache and writes to the apiserver
   198  	client     client.Client
   199  	kubeClient kube.Client
   200  	scheme     *runtime.Scheme
   201  	options    *Options
   202  }
   203  
   204  // Reconcile reads that state of the cluster for a IstioOperator object and makes changes based on the state read
   205  // and what is in the IstioOperator.Spec
   206  // Note:
   207  // The Controller will requeue the Request to be processed again if the returned error is non-nil or
   208  // Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
   209  func (r *ReconcileIstioOperator) Reconcile(_ context.Context, request reconcile.Request) (reconcile.Result, error) {
   210  	scope.Info("Reconciling IstioOperator")
   211  
   212  	ns, iopName := request.Namespace, request.Name
   213  	reqNamespacedName := types.NamespacedName{
   214  		Name:      request.Name,
   215  		Namespace: ns,
   216  	}
   217  	// declare read-only iop instance to create the reconciler
   218  	iop := &iopv1alpha1.IstioOperator{}
   219  	if err := r.client.Get(context.TODO(), reqNamespacedName, iop); err != nil {
   220  		if errors.IsNotFound(err) {
   221  			// Request object not found, could have been deleted after reconcile request.
   222  			// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
   223  			// Return and don't requeue
   224  			metrics.CRDeletionTotal.Increment()
   225  			return reconcile.Result{}, nil
   226  		}
   227  		// Error reading the object - requeue the request.
   228  		operatorFailedToGetObjectFromAPIServer.Log(scope).Warnf("error getting IstioOperator %s: %s", iopName, err)
   229  		metrics.CountCRFetchFail(errors.ReasonForError(err))
   230  		return reconcile.Result{}, err
   231  	}
   232  	if iop.Spec == nil {
   233  		iop.Spec = &v1alpha1.IstioOperatorSpec{Profile: name.DefaultProfileName}
   234  	}
   235  	operatorRevision, _ := os.LookupEnv("REVISION")
   236  	if operatorRevision != "" && operatorRevision != iop.Spec.Revision {
   237  		scope.Infof("Ignoring IstioOperator CR %s with revision %s, since operator revision is %s.", iopName, iop.Spec.Revision, operatorRevision)
   238  		return reconcile.Result{}, nil
   239  	}
   240  	if iop.Annotations != nil {
   241  		if ir := iop.Annotations[IgnoreReconcileAnnotation]; ir == "true" {
   242  			scope.Infof("Ignoring the IstioOperator CR %s because it is annotated to be ignored for reconcile ", iopName)
   243  			return reconcile.Result{}, nil
   244  		}
   245  	}
   246  	if iop.Spec.Profile == "ambient" {
   247  		scope.Infof("Ignoring the IstioOperator CR %s because it is using the unsupported profile 'ambient'.", iopName)
   248  		return reconcile.Result{}, nil
   249  	}
   250  
   251  	var err error
   252  	iopMerged := &iopv1alpha1.IstioOperator{}
   253  	*iopMerged = *iop
   254  	// get the merged values in iop on top of the defaults for the profile given by iop.profile
   255  	iopMerged.Spec, err = mergeIOPSWithProfile(iopMerged)
   256  	if err != nil {
   257  		operatorFailedToMergeUserIOP.Log(scope).Errorf("failed to merge base profile with user IstioOperator CR %s, %s", iopName, err)
   258  		return reconcile.Result{}, err
   259  	}
   260  
   261  	deleted := iop.GetDeletionTimestamp() != nil
   262  	finalizers := sets.New(iop.GetFinalizers()...)
   263  	if deleted {
   264  		if !finalizers.Contains(finalizer) {
   265  			scope.Infof("IstioOperator %s deleted", iopName)
   266  			metrics.CRDeletionTotal.Increment()
   267  			return reconcile.Result{}, nil
   268  		}
   269  		scope.Infof("Deleting IstioOperator %s", iopName)
   270  
   271  		reconciler, err := helmreconciler.NewHelmReconciler(r.client, r.kubeClient, iopMerged, nil)
   272  		if err != nil {
   273  			return reconcile.Result{}, err
   274  		}
   275  		if err := reconciler.Delete(); err != nil {
   276  			scope.Errorf("Failed to delete resources with helm reconciler: %s.", err)
   277  			return reconcile.Result{}, err
   278  		}
   279  
   280  		finalizers.Delete(finalizer)
   281  		iop.SetFinalizers(sets.SortedList(finalizers))
   282  		finalizerError := r.client.Update(context.TODO(), iop)
   283  		for retryCount := 0; errors.IsConflict(finalizerError) && retryCount < finalizerMaxRetries; retryCount++ {
   284  			scope.Info("API server conflict during finalizer removal, retrying.")
   285  			_ = r.client.Get(context.TODO(), request.NamespacedName, iop)
   286  			finalizers = sets.New(iop.GetFinalizers()...)
   287  			finalizers.Delete(finalizer)
   288  			iop.SetFinalizers(sets.SortedList(finalizers))
   289  			finalizerError = r.client.Update(context.TODO(), iop)
   290  		}
   291  		if finalizerError != nil {
   292  			if errors.IsNotFound(finalizerError) {
   293  				scope.Infof("Did not remove finalizer from %s: the object was previously deleted.", iopName)
   294  				metrics.CRDeletionTotal.Increment()
   295  				return reconcile.Result{}, nil
   296  			} else if errors.IsConflict(finalizerError) {
   297  				scope.Infof("Could not remove finalizer from %s due to conflict. Operation will be retried in next reconcile attempt.", iopName)
   298  				return reconcile.Result{}, nil
   299  			}
   300  			operatorFailedToRemoveFinalizer.Log(scope).Errorf("error removing finalizer: %s", finalizerError)
   301  			return reconcile.Result{}, finalizerError
   302  		}
   303  		return reconcile.Result{}, nil
   304  	} else if !finalizers.Contains(finalizer) {
   305  		log.Infof("Adding finalizer %v to %v", finalizer, request)
   306  		finalizers.Insert(finalizer)
   307  		iop.SetFinalizers(sets.SortedList(finalizers))
   308  		err := r.client.Update(context.TODO(), iop)
   309  		if err != nil {
   310  			if errors.IsNotFound(err) {
   311  				scope.Infof("Could not add finalizer to %s: the object was deleted.", iopName)
   312  				metrics.CRDeletionTotal.Increment()
   313  				return reconcile.Result{}, nil
   314  			} else if errors.IsConflict(err) {
   315  				scope.Infof("Could not add finalizer to %s due to conflict. Operation will be retried in next reconcile attempt.", iopName)
   316  			}
   317  			operatorFailedToAddFinalizer.Log(scope).Errorf("Failed to add finalizer to IstioOperator CR %s: %s", iopName, err)
   318  			return reconcile.Result{}, err
   319  		}
   320  	}
   321  
   322  	scope.Info("Updating IstioOperator")
   323  	val := iopMerged.Spec.Values.AsMap()
   324  	if _, ok := val["global"]; !ok {
   325  		val["global"] = make(map[string]any)
   326  	}
   327  	err = util.ValidateIOPCAConfig(r.kubeClient, iopMerged)
   328  	if err != nil {
   329  		operatorFailedToConfigure.Log(scope).Errorf("failed to apply IstioOperator resources. Error %s", err)
   330  		return reconcile.Result{}, err
   331  	}
   332  	helmReconcilerOptions := &helmreconciler.Options{
   333  		Log:         clog.NewDefaultLogger(),
   334  		ProgressLog: progress.NewLog(),
   335  	}
   336  	if r.options != nil {
   337  		helmReconcilerOptions.Force = r.options.Force
   338  	}
   339  	exists := revtag.PreviousInstallExists(context.Background(), r.kubeClient.Kube())
   340  	reconciler, err := helmreconciler.NewHelmReconciler(r.client, r.kubeClient, iopMerged, helmReconcilerOptions)
   341  	if err != nil {
   342  		scope.Errorf("Error during reconcile. Error: %s", err)
   343  		return reconcile.Result{}, err
   344  	}
   345  	if err := reconciler.SetStatusBegin(); err != nil {
   346  		scope.Errorf("Error during reconcile, failed to update status to Begin. Error: %s", err)
   347  		return reconcile.Result{}, err
   348  	}
   349  	status, err := reconciler.Reconcile()
   350  	if err != nil {
   351  		scope.Errorf("Error during reconcile: %s", err)
   352  	}
   353  	if err = processDefaultWebhookAfterReconcile(iopMerged, r.kubeClient, exists); err != nil {
   354  		scope.Errorf("Error during reconcile: %s", err)
   355  		return reconcile.Result{}, err
   356  	}
   357  
   358  	if err := reconciler.SetStatusComplete(status); err != nil {
   359  		scope.Errorf("Error during reconcile, failed to update status to Complete. Error: %s", err)
   360  		return reconcile.Result{}, err
   361  	}
   362  
   363  	return reconcile.Result{}, err
   364  }
   365  
   366  func processDefaultWebhookAfterReconcile(iop *iopv1alpha1.IstioOperator, client kube.Client, exists bool) error {
   367  	var ns string
   368  	if configuredNamespace := iopv1alpha1.Namespace(iop.Spec); configuredNamespace != "" {
   369  		ns = configuredNamespace
   370  	} else {
   371  		ns = constants.IstioSystemNamespace
   372  	}
   373  	opts := &helmreconciler.ProcessDefaultWebhookOptions{
   374  		Namespace: ns,
   375  		DryRun:    false,
   376  	}
   377  	if _, err := helmreconciler.ProcessDefaultWebhook(client, iop, exists, opts); err != nil {
   378  		return fmt.Errorf("failed to process default webhook: %v", err)
   379  	}
   380  	return nil
   381  }
   382  
   383  // mergeIOPSWithProfile overlays the values in iop on top of the defaults for the profile given by iop.profile and
   384  // returns the merged result.
   385  func mergeIOPSWithProfile(iop *iopv1alpha1.IstioOperator) (*v1alpha1.IstioOperatorSpec, error) {
   386  	profileYAML, err := helm.GetProfileYAML(iop.Spec.InstallPackagePath, iop.Spec.Profile)
   387  	if err != nil {
   388  		metrics.CountCRMergeFail(metrics.CannotFetchProfileError)
   389  		return nil, err
   390  	}
   391  
   392  	// Due to the fact that base profile is compiled in before a tag can be created, we must allow an additional
   393  	// override from variables that are set during release build time.
   394  	hub := version.DockerInfo.Hub
   395  	tag := version.DockerInfo.Tag
   396  	if hub != "" && hub != "unknown" && tag != "" && tag != "unknown" {
   397  		buildHubTagOverlayYAML, err := helm.GenerateHubTagOverlay(hub, tag)
   398  		if err != nil {
   399  			metrics.CountCRMergeFail(metrics.OverlayError)
   400  			return nil, err
   401  		}
   402  		profileYAML, err = util.OverlayYAML(profileYAML, buildHubTagOverlayYAML)
   403  		if err != nil {
   404  			metrics.CountCRMergeFail(metrics.OverlayError)
   405  			return nil, err
   406  		}
   407  	}
   408  
   409  	overlayYAMLB, err := yaml.Marshal(iop)
   410  	if err != nil {
   411  		metrics.CountCRMergeFail(metrics.IOPFormatError)
   412  		return nil, err
   413  	}
   414  	overlayYAML := string(overlayYAMLB)
   415  	t := translate.NewReverseTranslator()
   416  	overlayYAML, err = t.TranslateK8SfromValueToIOP(overlayYAML)
   417  	if err != nil {
   418  		metrics.CountCRMergeFail(metrics.TranslateValuesError)
   419  		return nil, fmt.Errorf("could not overlay k8s settings from values to IOP: %s", err)
   420  	}
   421  
   422  	mergedYAML, err := util.OverlayIOP(profileYAML, overlayYAML)
   423  	if err != nil {
   424  		metrics.CountCRMergeFail(metrics.OverlayError)
   425  		return nil, err
   426  	}
   427  
   428  	mergedYAML, err = translate.OverlayValuesEnablement(mergedYAML, overlayYAML, "")
   429  	if err != nil {
   430  		metrics.CountCRMergeFail(metrics.TranslateValuesError)
   431  		return nil, err
   432  	}
   433  
   434  	mergedYAMLSpec, err := tpath.GetSpecSubtree(mergedYAML)
   435  	if err != nil {
   436  		metrics.CountCRMergeFail(metrics.InternalYAMLParseError)
   437  		return nil, err
   438  	}
   439  
   440  	return istio.UnmarshalAndValidateIOPS(mergedYAMLSpec)
   441  }
   442  
   443  // Add creates a new IstioOperator Controller and adds it to the Manager. The Manager will set fields on the Controller
   444  // and Start it when the Manager is Started. It also provides additional options to modify internal reconciler behavior.
   445  func Add(mgr manager.Manager, options *Options) error {
   446  	restConfig = mgr.GetConfig()
   447  	kubeClient, err := kube.NewClient(kube.NewClientConfigForRestConfig(restConfig), "")
   448  	if err != nil {
   449  		return fmt.Errorf("create Kubernetes client: %v", err)
   450  	}
   451  	return add(mgr, &ReconcileIstioOperator{client: mgr.GetClient(), scheme: mgr.GetScheme(), kubeClient: kubeClient, options: options}, options)
   452  }
   453  
   454  // add adds a new Controller to mgr with r as the reconcile.Reconciler along with options for additional configuration.
   455  func add(mgr manager.Manager, r *ReconcileIstioOperator, options *Options) error {
   456  	scope.Info("Adding controller for IstioOperator.")
   457  	// Create a new controller
   458  	opts := controller.Options{Reconciler: r, MaxConcurrentReconciles: options.MaxConcurrentReconciles}
   459  	c, err := controller.New("istiocontrolplane-controller", mgr, opts)
   460  	if err != nil {
   461  		return err
   462  	}
   463  
   464  	// Watch for changes to primary resource IstioOperator
   465  	err = c.Watch(source.Kind(
   466  		mgr.GetCache(),
   467  		&iopv1alpha1.IstioOperator{},
   468  		&handler.TypedEnqueueRequestForObject[*iopv1alpha1.IstioOperator]{},
   469  		operatorPredicates,
   470  	))
   471  	if err != nil {
   472  		return err
   473  	}
   474  	// watch for changes to Istio resources
   475  	err = watchIstioResources(mgr.GetCache(), c)
   476  	if err != nil {
   477  		return err
   478  	}
   479  	scope.Info("Controller added")
   480  	return nil
   481  }
   482  
   483  // Watch changes for Istio resources managed by the operator
   484  func watchIstioResources(mgrCache cache2.Cache, c controller.Controller) error {
   485  	for _, t := range watchedResources() {
   486  		u := &unstructured.Unstructured{}
   487  		u.SetGroupVersionKind(schema.GroupVersionKind{
   488  			Kind:    t.Kind,
   489  			Group:   t.Group,
   490  			Version: t.Version,
   491  		})
   492  		handlerFunc := handler.TypedEnqueueRequestsFromMapFunc[*unstructured.Unstructured](func(_ context.Context, a *unstructured.Unstructured) []reconcile.Request {
   493  			scope.Infof("Watching a change for istio resource: %s/%s", a.GetNamespace(), a.GetName())
   494  			return []reconcile.Request{
   495  				{NamespacedName: types.NamespacedName{
   496  					Name:      a.GetLabels()[helmreconciler.OwningResourceName],
   497  					Namespace: a.GetLabels()[helmreconciler.OwningResourceNamespace],
   498  				}},
   499  			}
   500  		})
   501  		err := c.Watch(source.Kind(mgrCache, u, handlerFunc, ownedResourcePredicates))
   502  		if err != nil {
   503  			scope.Errorf("Could not create watch for %s/%s/%s: %s.", t.Kind, t.Group, t.Version, err)
   504  		}
   505  	}
   506  	return nil
   507  }
   508  
   509  // Check if the specified object is created by operator
   510  func isOperatorCreatedResource(obj metav1.Object) bool {
   511  	return obj.GetLabels()[helmreconciler.OwningResourceName] != "" &&
   512  		obj.GetLabels()[helmreconciler.OwningResourceNamespace] != "" &&
   513  		obj.GetLabels()[helmreconciler.IstioComponentLabelStr] != ""
   514  }