istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/helmreconciler/prune.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  	"strings"
    21  
    22  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    23  	"k8s.io/apimachinery/pkg/api/meta"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    26  	klabels "k8s.io/apimachinery/pkg/labels"
    27  	"k8s.io/apimachinery/pkg/runtime/schema"
    28  	"k8s.io/apimachinery/pkg/selection"
    29  	"sigs.k8s.io/controller-runtime/pkg/client"
    30  
    31  	"istio.io/api/label"
    32  	"istio.io/api/operator/v1alpha1"
    33  	iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    34  	"istio.io/istio/operator/pkg/cache"
    35  	"istio.io/istio/operator/pkg/metrics"
    36  	"istio.io/istio/operator/pkg/name"
    37  	"istio.io/istio/operator/pkg/object"
    38  	"istio.io/istio/operator/pkg/translate"
    39  	"istio.io/istio/operator/pkg/util"
    40  	"istio.io/istio/pkg/config/constants"
    41  	"istio.io/istio/pkg/config/schema/gvk"
    42  	"istio.io/istio/pkg/kube"
    43  	"istio.io/istio/pkg/proxy"
    44  )
    45  
    46  var (
    47  	// ClusterResources are resource types the operator prunes, ordered by which types should be deleted, first to last.
    48  	ClusterResources = []schema.GroupVersionKind{
    49  		{Group: "admissionregistration.k8s.io", Version: "v1", Kind: name.MutatingWebhookConfigurationStr},
    50  		{Group: "admissionregistration.k8s.io", Version: "v1", Kind: name.ValidatingWebhookConfigurationStr},
    51  		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.ClusterRoleStr},
    52  		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.ClusterRoleBindingStr},
    53  		// Cannot currently prune CRDs because this will also wipe out user config.
    54  		// {Group: "apiextensions.k8s.io", Version: "v1beta1", Kind: name.CRDStr},
    55  	}
    56  	// ClusterCPResources lists cluster scope resources types which should be deleted during uninstall command.
    57  	ClusterCPResources = []schema.GroupVersionKind{
    58  		{Group: "admissionregistration.k8s.io", Version: "v1", Kind: name.MutatingWebhookConfigurationStr},
    59  		{Group: "admissionregistration.k8s.io", Version: "v1", Kind: name.ValidatingWebhookConfigurationStr},
    60  		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.ClusterRoleStr},
    61  		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.ClusterRoleBindingStr},
    62  	}
    63  	// AllClusterResources lists all cluster scope resources types which should be deleted in purge case, including CRD.
    64  	AllClusterResources = append(ClusterResources,
    65  		schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: name.CRDStr},
    66  		schema.GroupVersionKind{Group: "k8s.cni.cncf.io", Version: "v1", Kind: name.NetworkAttachmentDefinitionStr},
    67  	)
    68  )
    69  
    70  // NamespacedResources gets specific pruning resources based on the k8s version
    71  func NamespacedResources() []schema.GroupVersionKind {
    72  	res := []schema.GroupVersionKind{
    73  		{Group: "apps", Version: "v1", Kind: name.DeploymentStr},
    74  		{Group: "apps", Version: "v1", Kind: name.DaemonSetStr},
    75  		{Group: "", Version: "v1", Kind: name.ServiceStr},
    76  		{Group: "", Version: "v1", Kind: name.CMStr},
    77  		{Group: "", Version: "v1", Kind: name.PodStr},
    78  		{Group: "", Version: "v1", Kind: name.SecretStr},
    79  		{Group: "", Version: "v1", Kind: name.SAStr},
    80  		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.RoleBindingStr},
    81  		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.RoleStr},
    82  		{Group: "policy", Version: "v1", Kind: name.PDBStr},
    83  		{Group: "autoscaling", Version: "v2", Kind: name.HPAStr},
    84  		gvk.EnvoyFilter.Kubernetes(),
    85  	}
    86  	return res
    87  }
    88  
    89  // Prune removes any resources not specified in manifests generated by HelmReconciler h.
    90  func (h *HelmReconciler) Prune(manifests name.ManifestMap, all bool) error {
    91  	return h.runForAllTypes(func(labels map[string]string, objects *unstructured.UnstructuredList) error {
    92  		var errs util.Errors
    93  		if all {
    94  			errs = util.AppendErr(errs, h.deleteResources(nil, labels, "", objects, all))
    95  		} else {
    96  			for cname, manifest := range manifests.Consolidated() {
    97  				errs = util.AppendErr(errs, h.deleteResources(object.AllObjectHashes(manifest), labels, cname, objects, all))
    98  			}
    99  		}
   100  		return errs.ToError()
   101  	})
   102  }
   103  
   104  // PruneControlPlaneByRevisionWithController is called to remove specific control plane revision
   105  // during reconciliation process of controller.
   106  // It returns the install status and any error encountered.
   107  func (h *HelmReconciler) PruneControlPlaneByRevisionWithController(iopSpec *v1alpha1.IstioOperatorSpec) (*v1alpha1.InstallStatus, error) {
   108  	ns := iopv1alpha1.Namespace(iopSpec)
   109  	if ns == "" {
   110  		ns = constants.IstioSystemNamespace
   111  	}
   112  	errStatus := &v1alpha1.InstallStatus{Status: v1alpha1.InstallStatus_ERROR}
   113  	enabledComponents, err := translate.GetEnabledComponents(iopSpec)
   114  	if err != nil {
   115  		return errStatus,
   116  			fmt.Errorf("failed to get enabled components: %v", err)
   117  	}
   118  	pilotEnabled := false
   119  	// check whether the istiod is enabled
   120  	for _, c := range enabledComponents {
   121  		if c == string(name.PilotComponentName) {
   122  			pilotEnabled = true
   123  			break
   124  		}
   125  	}
   126  	// If istiod is enabled, check if it has any proxies connected.
   127  	if pilotEnabled {
   128  		cfg := h.kubeClient.RESTConfig()
   129  		kubeClient, err := kube.NewCLIClient(kube.NewClientConfigForRestConfig(cfg), kube.WithRevision(iopSpec.Revision))
   130  		if err != nil {
   131  			return errStatus, err
   132  		}
   133  
   134  		pilotExists, err := h.pilotExists(kubeClient, ns)
   135  		if err != nil {
   136  			return errStatus, fmt.Errorf("failed to check istiod extist: %v", err)
   137  		}
   138  
   139  		if pilotExists {
   140  			// TODO(ramaraochavali): Find a better alternative instead of using debug interface
   141  			// of istiod as it is typically not recommended in production environments.
   142  			pids, err := proxy.GetIDsFromProxyInfo(kubeClient, ns)
   143  			if err != nil {
   144  				return errStatus, fmt.Errorf("failed to check proxy infos: %v", err)
   145  			}
   146  			if len(pids) != 0 {
   147  				msg := fmt.Sprintf("there are proxies still pointing to the pruned control plane: %s.",
   148  					strings.Join(pids, " "))
   149  				st := &v1alpha1.InstallStatus{Status: v1alpha1.InstallStatus_ACTION_REQUIRED, Message: msg}
   150  				return st, nil
   151  			}
   152  		}
   153  	}
   154  
   155  	for _, c := range enabledComponents {
   156  		uslist, err := h.GetPrunedResources(iopSpec.Revision, false, c)
   157  		if err != nil {
   158  			return errStatus, err
   159  		}
   160  		err = h.DeleteObjectsList(uslist, c)
   161  		if err != nil {
   162  			return errStatus, err
   163  		}
   164  	}
   165  	return &v1alpha1.InstallStatus{Status: v1alpha1.InstallStatus_HEALTHY}, nil
   166  }
   167  
   168  func (h *HelmReconciler) pilotExists(cliClient kube.CLIClient, istioNamespace string) (bool, error) {
   169  	istiodPods, err := cliClient.GetIstioPods(context.TODO(), istioNamespace, metav1.ListOptions{
   170  		LabelSelector: "app=istiod",
   171  		FieldSelector: "status.phase=Running",
   172  	})
   173  	if err != nil {
   174  		return false, err
   175  	}
   176  
   177  	return len(istiodPods) > 0, nil
   178  }
   179  
   180  // DeleteObjectsList removed resources that are in the slice of UnstructuredList.
   181  func (h *HelmReconciler) DeleteObjectsList(objectsList []*unstructured.UnstructuredList, componentName string) error {
   182  	var errs util.Errors
   183  	deletedObjects := make(map[string]bool)
   184  	for _, ul := range objectsList {
   185  		for _, o := range ul.Items {
   186  			obj := object.NewK8sObject(&o, nil, nil)
   187  			oh := obj.Hash()
   188  
   189  			// kube client does not differentiate API version when listing, added this check to deduplicate.
   190  			if deletedObjects[oh] {
   191  				continue
   192  			}
   193  			if err := h.deleteResource(obj, componentName, oh); err != nil {
   194  				errs = append(errs, err)
   195  			}
   196  			deletedObjects[oh] = true
   197  		}
   198  	}
   199  
   200  	return errs.ToError()
   201  }
   202  
   203  // GetPrunedResources get the list of resources to be removed
   204  // 1. if includeClusterResources is false, we list the namespaced resources by matching revision and component labels.
   205  // 2. if includeClusterResources is true, we list the namespaced and cluster resources by component labels only.
   206  // If componentName is not empty, only resources associated with specific components would be returned
   207  // UnstructuredList of objects and corresponding list of name kind hash of k8sObjects would be returned
   208  func (h *HelmReconciler) GetPrunedResources(revision string, includeClusterResources bool, componentName string) (
   209  	[]*unstructured.UnstructuredList, error,
   210  ) {
   211  	var usList []*unstructured.UnstructuredList
   212  	labels := make(map[string]string)
   213  	if revision != "" {
   214  		labels[label.IoIstioRev.Name] = revision
   215  	}
   216  	if componentName != "" {
   217  		labels[IstioComponentLabelStr] = componentName
   218  	}
   219  	if h.iop.GetName() != "" {
   220  		labels[OwningResourceName] = h.iop.GetName()
   221  	}
   222  	if h.iop.GetNamespace() != "" {
   223  		labels[OwningResourceNamespace] = h.iop.GetNamespace()
   224  	}
   225  	selector := klabels.Set(labels).AsSelectorPreValidated()
   226  	resources := NamespacedResources()
   227  	gvkList := append(resources, ClusterCPResources...)
   228  	if includeClusterResources {
   229  		gvkList = append(resources, AllClusterResources...)
   230  		// Cleanup IstioOperator, which may be used with in-cluster operator.
   231  		if ioplist := h.getIstioOperatorCR(); ioplist != nil && len(ioplist.Items) > 0 {
   232  			usList = append(usList, ioplist)
   233  		}
   234  	}
   235  	for _, gvk := range gvkList {
   236  		objects := &unstructured.UnstructuredList{}
   237  		objects.SetGroupVersionKind(gvk)
   238  		componentRequirement, err := klabels.NewRequirement(IstioComponentLabelStr, selection.Exists, nil)
   239  		if err != nil {
   240  			return usList, err
   241  		}
   242  		if includeClusterResources {
   243  			s := klabels.NewSelector()
   244  			err = h.client.List(context.TODO(), objects,
   245  				client.MatchingLabelsSelector{Selector: s.Add(*componentRequirement)})
   246  		} else {
   247  			// do not prune base components or unknown components
   248  			includeCN := []string{
   249  				string(name.PilotComponentName),
   250  				string(name.IngressComponentName), string(name.EgressComponentName),
   251  				string(name.CNIComponentName), string(name.IstioOperatorComponentName),
   252  				string(name.IstiodRemoteComponentName),
   253  				string(name.ZtunnelComponentName),
   254  			}
   255  			includeRequirement, err := klabels.NewRequirement(IstioComponentLabelStr, selection.In, includeCN)
   256  			if err != nil {
   257  				return usList, err
   258  			}
   259  			if err = h.client.List(context.TODO(), objects,
   260  				client.MatchingLabelsSelector{
   261  					Selector: selector.Add(*includeRequirement, *componentRequirement),
   262  				},
   263  			); err != nil {
   264  				continue
   265  			}
   266  		}
   267  		if err != nil {
   268  			continue
   269  		}
   270  		for _, obj := range objects.Items {
   271  			objName := fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())
   272  			metrics.AddResource(objName, gvk.GroupKind())
   273  		}
   274  		if len(objects.Items) == 0 {
   275  			continue
   276  		}
   277  		usList = append(usList, objects)
   278  	}
   279  
   280  	return usList, nil
   281  }
   282  
   283  // getIstioOperatorCR is a helper function to get IstioOperator CR during purge,
   284  // otherwise the resources would be reconciled back later if there is in-cluster operator deployment.
   285  // And it is needed to remove the IstioOperator CRD.
   286  func (h *HelmReconciler) getIstioOperatorCR() *unstructured.UnstructuredList {
   287  	iopGVR := iopv1alpha1.IstioOperatorGVR
   288  	objects, err := h.kubeClient.Dynamic().Resource(iopGVR).List(context.TODO(), metav1.ListOptions{})
   289  	if err != nil {
   290  		if kerrors.IsNotFound(err) {
   291  			return nil
   292  		}
   293  		scope.Errorf("failed to list IstioOperator CR: %v", err)
   294  	}
   295  	return objects
   296  }
   297  
   298  // runForAllTypes will collect all existing resource types we care about. For each type, the callback function
   299  // will be called with the labels used to select this type, and all objects.
   300  // This is in internal function meant to support prune and delete
   301  func (h *HelmReconciler) runForAllTypes(callback func(labels map[string]string, objects *unstructured.UnstructuredList) error) error {
   302  	var errs util.Errors
   303  	// Ultimately, we want to prune based on component labels. Each of these share a common set of labels
   304  	// Rather than do N List() calls for each component, we will just filter for the common subset here
   305  	// and each component will do its own filtering
   306  	// Because we are filtering by the core labels, List() will only return items that some components will care
   307  	// about, so we are not querying for an overly broad set of resources.
   308  	labels, err := h.getCoreOwnerLabels()
   309  	if err != nil {
   310  		return err
   311  	}
   312  	selector := klabels.Set(labels).AsSelectorPreValidated()
   313  	componentRequirement, err := klabels.NewRequirement(IstioComponentLabelStr, selection.Exists, nil)
   314  	if err != nil {
   315  		return err
   316  	}
   317  	selector = selector.Add(*componentRequirement)
   318  
   319  	resources := PrunedResourcesSchemas()
   320  	for _, gvk := range resources {
   321  		// First, we collect all objects for the provided GVK
   322  		objects := &unstructured.UnstructuredList{}
   323  		objects.SetGroupVersionKind(gvk)
   324  		if err := h.client.List(context.TODO(), objects, client.MatchingLabelsSelector{Selector: selector}); err != nil {
   325  			// we only want to retrieve resources clusters
   326  			if !(h.opts.DryRun && meta.IsNoMatchError(err)) {
   327  				scope.Debugf("retrieving resources to prune type %s: %s", gvk.String(), err)
   328  			}
   329  			continue
   330  		}
   331  		for _, obj := range objects.Items {
   332  			objName := fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())
   333  			metrics.AddResource(objName, gvk.GroupKind())
   334  		}
   335  		errs = util.AppendErr(errs, callback(labels, objects))
   336  	}
   337  	return errs.ToError()
   338  }
   339  
   340  func PrunedResourcesSchemas() []schema.GroupVersionKind {
   341  	return append(NamespacedResources(), ClusterResources...)
   342  }
   343  
   344  // deleteResources delete any resources from the given component that are not in the excluded map. Resource
   345  // labels are used to identify the resources belonging to the component.
   346  func (h *HelmReconciler) deleteResources(excluded map[string]bool, coreLabels map[string]string,
   347  	componentName string, objects *unstructured.UnstructuredList, all bool,
   348  ) error {
   349  	var errs util.Errors
   350  	labels := h.addComponentLabels(coreLabels, componentName)
   351  	selector := klabels.Set(labels).AsSelectorPreValidated()
   352  	for _, o := range objects.Items {
   353  		obj := object.NewK8sObject(&o, nil, nil)
   354  		oh := obj.Hash()
   355  		if !all {
   356  			// Label mismatch. Provided objects don't select against the component, so this likely means the object
   357  			// is for another component.
   358  			if !selector.Matches(klabels.Set(o.GetLabels())) {
   359  				continue
   360  			}
   361  			if excluded[oh] {
   362  				continue
   363  			}
   364  			if o.GetLabels()[OwningResourceNotPruned] == "true" {
   365  				continue
   366  			}
   367  		}
   368  		if err := h.deleteResource(obj, componentName, oh); err != nil {
   369  			errs = append(errs, err)
   370  		}
   371  	}
   372  	if all {
   373  		cache.FlushObjectCaches()
   374  	}
   375  
   376  	return errs.ToError()
   377  }
   378  
   379  func (h *HelmReconciler) deleteResource(obj *object.K8sObject, componentName, oh string) error {
   380  	if h.opts.DryRun {
   381  		h.opts.Log.LogAndPrintf("Not pruning object %s because of dry run.", oh)
   382  		return nil
   383  	}
   384  	u := obj.UnstructuredObject()
   385  	if u.GetKind() == name.IstioOperatorStr {
   386  		u.SetFinalizers([]string{})
   387  		if err := h.client.Patch(context.TODO(), u, client.Merge); err != nil {
   388  			scope.Errorf("failed to patch IstioOperator CR: %s, %v", u.GetName(), err)
   389  		}
   390  	}
   391  	err := h.client.Delete(context.TODO(), u, client.PropagationPolicy(metav1.DeletePropagationBackground))
   392  	scope.Debugf("Deleting %s (%s/%v)", oh, h.iop.Name, h.iop.Spec.Revision)
   393  	objGvk := u.GroupVersionKind()
   394  	if err != nil {
   395  		if !kerrors.IsNotFound(err) {
   396  			return err
   397  		}
   398  		// do not return error if resources are not found
   399  		h.opts.Log.LogAndPrintf("object: %s is not being deleted because it no longer exists", obj.Hash())
   400  		return nil
   401  	}
   402  	if componentName != "" {
   403  		h.removeFromObjectCache(componentName, oh)
   404  	} else {
   405  		cache.FlushObjectCaches()
   406  	}
   407  	metrics.ResourceDeletionTotal.
   408  		With(metrics.ResourceKindLabel.Value(util.GKString(objGvk.GroupKind()))).
   409  		Increment()
   410  	h.addPrunedKind(objGvk.GroupKind())
   411  	metrics.RemoveResource(obj.FullName(), objGvk.GroupKind())
   412  	h.opts.Log.LogAndPrintf("  Removed %s.", oh)
   413  	return nil
   414  }
   415  
   416  // RemoveObject removes object with objHash in componentName from the object cache.
   417  func (h *HelmReconciler) removeFromObjectCache(componentName, objHash string) {
   418  	crHash, err := h.getCRHash(componentName)
   419  	if err != nil {
   420  		scope.Error(err.Error())
   421  	}
   422  	cache.RemoveObject(crHash, objHash)
   423  	scope.Infof("Removed object %s from Cache.", objHash)
   424  }