istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/verifier/verifier.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 verifier
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"github.com/fatih/color"
    23  	appsv1 "k8s.io/api/apps/v1"
    24  	v1batch "k8s.io/api/batch/v1"
    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/cli-runtime/pkg/genericclioptions"
    29  	"k8s.io/cli-runtime/pkg/resource"
    30  	"k8s.io/client-go/dynamic"
    31  	"k8s.io/client-go/kubernetes/scheme"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  
    34  	"istio.io/api/label"
    35  	operatprv1alpha1 "istio.io/api/operator/v1alpha1"
    36  	"istio.io/istio/istioctl/pkg/clioptions"
    37  	operator_istio "istio.io/istio/operator/pkg/apis/istio"
    38  	"istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    39  	"istio.io/istio/operator/pkg/controlplane"
    40  	"istio.io/istio/operator/pkg/helmreconciler"
    41  	"istio.io/istio/operator/pkg/manifest"
    42  	"istio.io/istio/operator/pkg/translate"
    43  	"istio.io/istio/operator/pkg/util"
    44  	"istio.io/istio/operator/pkg/util/clog"
    45  	"istio.io/istio/pkg/kube"
    46  )
    47  
    48  // specialKinds is a map of special kinds to their corresponding kind names, which do not follow the
    49  // standard convention of pluralizing the kind name.
    50  var specialKinds = map[string]string{
    51  	"NetworkAttachmentDefinition": "network-attachment-definitions",
    52  }
    53  
    54  // StatusVerifier checks status of certain resources like deployment,
    55  // jobs and also verifies count of certain resource types.
    56  type StatusVerifier struct {
    57  	istioNamespace   string
    58  	manifestsPath    string
    59  	filenames        []string
    60  	controlPlaneOpts clioptions.ControlPlaneOptions
    61  	logger           clog.Logger
    62  	iop              *v1alpha1.IstioOperator
    63  	successMarker    string
    64  	failureMarker    string
    65  	client           kube.CLIClient
    66  	kclient          client.Client
    67  }
    68  
    69  type StatusVerifierOptions func(*StatusVerifier)
    70  
    71  func WithLogger(l clog.Logger) StatusVerifierOptions {
    72  	return func(s *StatusVerifier) {
    73  		s.logger = l
    74  	}
    75  }
    76  
    77  func WithIOP(iop *v1alpha1.IstioOperator) StatusVerifierOptions {
    78  	return func(s *StatusVerifier) {
    79  		s.iop = iop
    80  	}
    81  }
    82  
    83  // NewStatusVerifier creates a new instance of post-install verifier
    84  // which checks the status of various resources from the manifest.
    85  func NewStatusVerifier(kubeClient kube.CLIClient, client client.Client, istioNamespace, manifestsPath string,
    86  	filenames []string, controlPlaneOpts clioptions.ControlPlaneOptions,
    87  	options ...StatusVerifierOptions,
    88  ) (*StatusVerifier, error) {
    89  	verifier := StatusVerifier{
    90  		logger:           clog.NewDefaultLogger(),
    91  		successMarker:    "✅",
    92  		failureMarker:    "❌",
    93  		istioNamespace:   istioNamespace,
    94  		manifestsPath:    manifestsPath,
    95  		filenames:        filenames,
    96  		controlPlaneOpts: controlPlaneOpts,
    97  		client:           kubeClient,
    98  		kclient:          client,
    99  	}
   100  
   101  	for _, opt := range options {
   102  		opt(&verifier)
   103  	}
   104  
   105  	return &verifier, nil
   106  }
   107  
   108  func (v *StatusVerifier) Colorize() {
   109  	v.successMarker = color.New(color.FgGreen).Sprint(v.successMarker)
   110  	v.failureMarker = color.New(color.FgRed).Sprint(v.failureMarker)
   111  }
   112  
   113  // Verify implements Verifier interface. Here we check status of deployment
   114  // and jobs, count various resources for verification.
   115  func (v *StatusVerifier) Verify() error {
   116  	if v.iop != nil {
   117  		return v.verifyFinalIOP()
   118  	}
   119  	if len(v.filenames) == 0 {
   120  		return v.verifyInstallIOPRevision()
   121  	}
   122  	return v.verifyInstall()
   123  }
   124  
   125  // verifyInstallIOPRevision verifies the default installation of IstioOperator with the revision.
   126  func (v *StatusVerifier) verifyInstallIOPRevision() error {
   127  	var err error
   128  	if v.controlPlaneOpts.Revision == "" {
   129  		v.controlPlaneOpts.Revision, err = v.getRevision()
   130  		if err != nil {
   131  			return err
   132  		}
   133  	} else if v.controlPlaneOpts.Revision == "default" {
   134  		v.controlPlaneOpts.Revision = ""
   135  	}
   136  
   137  	emptyiops := &operatprv1alpha1.IstioOperatorSpec{Profile: "empty", Revision: v.controlPlaneOpts.Revision}
   138  	iop, err := translate.IOPStoIOP(emptyiops, "", "")
   139  	if err != nil {
   140  		return err
   141  	}
   142  	h, err := helmreconciler.NewHelmReconciler(v.kclient, v.client, iop, &helmreconciler.Options{})
   143  	if err != nil {
   144  		return err
   145  	}
   146  	resources, err := h.GetPrunedResources(v.controlPlaneOpts.Revision, true, "")
   147  	if err != nil {
   148  		return err
   149  	}
   150  	builder := resource.NewBuilder(v.client.UtilFactory()).ContinueOnError().Unstructured()
   151  	for i, re := range resources {
   152  		rj, err := re.MarshalJSON()
   153  		if err != nil {
   154  			continue
   155  		}
   156  		pseudoFilename := fmt.Sprintf("%d: generated from %s", i, "default")
   157  
   158  		reader := strings.NewReader(string(rj))
   159  		builder = builder.Stream(reader, pseudoFilename)
   160  	}
   161  	r := builder.Flatten().Do()
   162  	if r.Err() != nil {
   163  		return r.Err()
   164  	}
   165  	visitor := genericclioptions.ResourceFinderForResult(r).Do()
   166  	generatedCrds, generatedDeployments, generatedDaemonSets, err := v.verifyPostInstall(
   167  		visitor,
   168  		fmt.Sprintf("generated from %s", "default"))
   169  	if err != nil {
   170  		return err
   171  	}
   172  	return v.reportStatus(generatedCrds, generatedDeployments, generatedDaemonSets, nil)
   173  }
   174  
   175  func (v *StatusVerifier) getRevision() (string, error) {
   176  	var revision string
   177  	var revs string
   178  	revCount := 0
   179  	pods, err := v.client.PodsForSelector(context.TODO(), v.istioNamespace, "app=istiod")
   180  	if err != nil {
   181  		return "", fmt.Errorf("failed to fetch istiod pod, error: %v", err)
   182  	}
   183  	for _, pod := range pods.Items {
   184  		rev := pod.ObjectMeta.GetLabels()[label.IoIstioRev.Name]
   185  		revCount++
   186  		if rev == "default" {
   187  			continue
   188  		}
   189  		revision = rev
   190  	}
   191  	if revision == "" {
   192  		revs = "default"
   193  	} else {
   194  		revs = revision
   195  	}
   196  	v.logger.LogAndPrintf("%d Istio control planes detected, checking --revision %q only", revCount, revs)
   197  	return revision, nil
   198  }
   199  
   200  func (v *StatusVerifier) verifyFinalIOP() error {
   201  	crdCount, istioDeploymentCount, daemonSetCount, err := v.verifyPostInstallIstioOperator(
   202  		v.iop, fmt.Sprintf("IOP:%s", v.iop.GetName()))
   203  	return v.reportStatus(crdCount, istioDeploymentCount, daemonSetCount, err)
   204  }
   205  
   206  func (v *StatusVerifier) verifyInstall() error {
   207  	// This is not a pre-check.  Check that the supplied resources exist in the cluster
   208  	r := resource.NewBuilder(v.client.UtilFactory()).
   209  		Unstructured().
   210  		FilenameParam(false, &resource.FilenameOptions{Filenames: v.filenames}).
   211  		Flatten().
   212  		Do()
   213  	if r.Err() != nil {
   214  		return r.Err()
   215  	}
   216  	visitor := genericclioptions.ResourceFinderForResult(r).Do()
   217  	crdCount, istioDeploymentCount, generatedDaemonsets, err := v.verifyPostInstall(
   218  		visitor, strings.Join(v.filenames, ","))
   219  	return v.reportStatus(crdCount, istioDeploymentCount, generatedDaemonsets, err)
   220  }
   221  
   222  func (v *StatusVerifier) verifyPostInstallIstioOperator(iop *v1alpha1.IstioOperator, filename string) (int, int, int, error) {
   223  	t := translate.NewTranslator()
   224  	ver, err := v.client.GetKubernetesVersion()
   225  	if err != nil {
   226  		return 0, 0, 0, err
   227  	}
   228  	cp, err := controlplane.NewIstioControlPlane(iop.Spec, t, nil, ver)
   229  	if err != nil {
   230  		return 0, 0, 0, err
   231  	}
   232  	if err := cp.Run(); err != nil {
   233  		return 0, 0, 0, err
   234  	}
   235  
   236  	manifests, errs := cp.RenderManifest()
   237  	if len(errs) > 0 {
   238  		return 0, 0, 0, errs.ToError()
   239  	}
   240  
   241  	builder := resource.NewBuilder(v.client.UtilFactory()).ContinueOnError().Unstructured()
   242  	for cat, manifest := range manifests {
   243  		for i, manitem := range manifest {
   244  			reader := strings.NewReader(manitem)
   245  			pseudoFilename := fmt.Sprintf("%s:%d generated from %s", cat, i, filename)
   246  			builder = builder.Stream(reader, pseudoFilename)
   247  		}
   248  	}
   249  	r := builder.Flatten().Do()
   250  	if r.Err() != nil {
   251  		return 0, 0, 0, r.Err()
   252  	}
   253  	visitor := genericclioptions.ResourceFinderForResult(r).Do()
   254  	// Indirectly RECURSE back into verifyPostInstall with the manifest we just generated
   255  	generatedCrds, generatedDeployments, generatedDaemonSets, err := v.verifyPostInstall(
   256  		visitor,
   257  		fmt.Sprintf("generated from %s", filename))
   258  	if err != nil {
   259  		return generatedCrds, generatedDeployments, generatedDaemonSets, err
   260  	}
   261  
   262  	return generatedCrds, generatedDeployments, generatedDaemonSets, nil
   263  }
   264  
   265  func (v *StatusVerifier) verifyPostInstall(visitor resource.Visitor, filename string) (int, int, int, error) {
   266  	crdCount := 0
   267  	istioDeploymentCount := 0
   268  	daemonSetCount := 0
   269  	err := visitor.Visit(func(info *resource.Info, err error) error {
   270  		if err != nil {
   271  			return err
   272  		}
   273  		content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object)
   274  		if err != nil {
   275  			return err
   276  		}
   277  		un := &unstructured.Unstructured{Object: content}
   278  		kind := un.GetKind()
   279  		name := un.GetName()
   280  		namespace := un.GetNamespace()
   281  		kinds := resourceKinds(un)
   282  		if namespace == "" {
   283  			namespace = v.istioNamespace
   284  		}
   285  		switch kind {
   286  		case "Deployment":
   287  			deployment := &appsv1.Deployment{}
   288  			err = info.Client.
   289  				Get().
   290  				Resource(kinds).
   291  				Namespace(namespace).
   292  				Name(name).
   293  				VersionedParams(&metav1.GetOptions{}, scheme.ParameterCodec).
   294  				Do(context.TODO()).
   295  				Into(deployment)
   296  			if err != nil {
   297  				v.reportFailure(kind, name, namespace, err)
   298  				return err
   299  			}
   300  			if namespace == v.istioNamespace && strings.HasPrefix(name, "istio") {
   301  				istioDeploymentCount++
   302  			}
   303  			if err = verifyDeploymentStatus(deployment); err != nil {
   304  				ivf := istioVerificationFailureError(filename, err)
   305  				v.reportFailure(kind, name, namespace, ivf)
   306  				return ivf
   307  			}
   308  		case "Job":
   309  			job := &v1batch.Job{}
   310  			err = info.Client.
   311  				Get().
   312  				Resource(kinds).
   313  				Namespace(namespace).
   314  				Name(name).
   315  				VersionedParams(&metav1.GetOptions{}, scheme.ParameterCodec).
   316  				Do(context.TODO()).
   317  				Into(job)
   318  			if err != nil {
   319  				v.reportFailure(kind, name, namespace, err)
   320  				return err
   321  			}
   322  			if err := verifyJobPostInstall(job); err != nil {
   323  				ivf := istioVerificationFailureError(filename, err)
   324  				v.reportFailure(kind, name, namespace, ivf)
   325  				return ivf
   326  			}
   327  		case "IstioOperator":
   328  			// It is not a problem if the cluster does not include the IstioOperator
   329  			// we are checking.  Instead, verify the cluster has the things the
   330  			// IstioOperator specifies it should have.
   331  
   332  			// IstioOperator isn't part of pkg/config/schema/collections,
   333  			// usual conversion not available.  Convert unstructured to string
   334  			// and ask operator code to unmarshal.
   335  			fixTimestampRelatedUnmarshalIssues(un)
   336  
   337  			by := util.ToYAML(un)
   338  			unmergedIOP, err := operator_istio.UnmarshalIstioOperator(by, true)
   339  			if err != nil {
   340  				v.reportFailure(kind, name, namespace, err)
   341  				return err
   342  			}
   343  			profile := manifest.GetProfile(unmergedIOP)
   344  			iop, err := manifest.GetMergedIOP(by, profile, v.manifestsPath, v.controlPlaneOpts.Revision,
   345  				v.client, v.logger)
   346  			if err != nil {
   347  				v.reportFailure(kind, name, namespace, err)
   348  				return err
   349  			}
   350  			if v.manifestsPath != "" {
   351  				iop.Spec.InstallPackagePath = v.manifestsPath
   352  			}
   353  			if v1alpha1.Namespace(iop.Spec) == "" {
   354  				v1alpha1.SetNamespace(iop.Spec, v.istioNamespace)
   355  			}
   356  			generatedCrds, generatedDeployments, generatedDaemonSets, err := v.verifyPostInstallIstioOperator(iop, filename)
   357  			crdCount += generatedCrds
   358  			istioDeploymentCount += generatedDeployments
   359  			daemonSetCount += generatedDaemonSets
   360  			if err != nil {
   361  				return err
   362  			}
   363  		case "DaemonSet":
   364  			ds := &appsv1.DaemonSet{}
   365  			err = info.Client.
   366  				Get().
   367  				Resource(kinds).
   368  				Namespace(namespace).
   369  				Name(name).
   370  				VersionedParams(&metav1.GetOptions{}, scheme.ParameterCodec).
   371  				Do(context.TODO()).
   372  				Into(ds)
   373  			if err != nil {
   374  				v.reportFailure(kind, name, namespace, err)
   375  				return err
   376  			}
   377  			daemonSetCount++
   378  			if err = verifyDaemonSetStatus(ds); err != nil {
   379  				ivf := istioVerificationFailureError(filename, err)
   380  				v.reportFailure(kind, name, namespace, ivf)
   381  				return ivf
   382  			}
   383  		default:
   384  			result := info.Client.
   385  				Get().
   386  				Resource(kinds).
   387  				Name(name).
   388  				Do(context.TODO())
   389  			if result.Error() != nil {
   390  				result = info.Client.
   391  					Get().
   392  					Resource(kinds).
   393  					Namespace(namespace).
   394  					Name(name).
   395  					Do(context.TODO())
   396  				if result.Error() != nil {
   397  					v.reportFailure(kind, name, namespace, result.Error())
   398  					return istioVerificationFailureError(filename,
   399  						fmt.Errorf("the required %s:%s is not ready due to: %v",
   400  							kind, name, result.Error()))
   401  				}
   402  			}
   403  			if kind == "CustomResourceDefinition" {
   404  				crdCount++
   405  			}
   406  		}
   407  		v.logger.LogAndPrintf("%s %s: %s.%s checked successfully", v.successMarker, kind, name, namespace)
   408  		return nil
   409  	})
   410  	return crdCount, istioDeploymentCount, daemonSetCount, err
   411  }
   412  
   413  func resourceKinds(un *unstructured.Unstructured) string {
   414  	kinds := findResourceInSpec(un.GetObjectKind().GroupVersionKind())
   415  	if kinds == "" {
   416  		kinds = strings.ToLower(un.GetKind()) + "s"
   417  	}
   418  	// Override with special kind if it exists in the map
   419  	if specialKind, exists := specialKinds[un.GetKind()]; exists {
   420  		kinds = specialKind
   421  	}
   422  	return kinds
   423  }
   424  
   425  func (v *StatusVerifier) reportStatus(crdCount, istioDeploymentCount, daemonSetCount int, err error) error {
   426  	v.logger.LogAndPrintf("Checked %v custom resource definitions", crdCount)
   427  	v.logger.LogAndPrintf("Checked %v Istio Deployments", istioDeploymentCount)
   428  	if daemonSetCount > 0 {
   429  		v.logger.LogAndPrintf("Checked %v Istio Daemonsets", daemonSetCount)
   430  	}
   431  	if istioDeploymentCount == 0 {
   432  		if err != nil {
   433  			v.logger.LogAndPrintf("! No Istio installation found: %v", err)
   434  		} else {
   435  			v.logger.LogAndPrintf("! No Istio installation found")
   436  		}
   437  		return fmt.Errorf("no Istio installation found")
   438  	}
   439  	if err != nil {
   440  		// Don't return full error; it is usually an unwieldy aggregate
   441  		return fmt.Errorf("Istio installation failed") // nolint
   442  	}
   443  	v.logger.LogAndPrintf("%s Istio is installed and verified successfully", v.successMarker)
   444  	return nil
   445  }
   446  
   447  func fixTimestampRelatedUnmarshalIssues(un *unstructured.Unstructured) {
   448  	un.SetCreationTimestamp(metav1.Time{}) // UnmarshalIstioOperator chokes on these
   449  
   450  	// UnmarshalIstioOperator fails because managedFields could contain time
   451  	// and gogo/protobuf/jsonpb(v1.3.1) tries to unmarshal it as struct (the type
   452  	// meta_v1.Time is really a struct) and fails.
   453  	un.SetManagedFields([]metav1.ManagedFieldsEntry{})
   454  }
   455  
   456  // Find all IstioOperator in the cluster.
   457  func AllOperatorsInCluster(client dynamic.Interface) ([]*v1alpha1.IstioOperator, error) {
   458  	ul, err := client.
   459  		Resource(v1alpha1.IstioOperatorGVR).
   460  		List(context.TODO(), metav1.ListOptions{})
   461  	if err != nil {
   462  		return nil, err
   463  	}
   464  	retval := make([]*v1alpha1.IstioOperator, 0)
   465  	for _, un := range ul.Items {
   466  		fixTimestampRelatedUnmarshalIssues(&un)
   467  		by := util.ToYAML(un.Object)
   468  		iop, err := operator_istio.UnmarshalIstioOperator(by, true)
   469  		if err != nil {
   470  			return nil, err
   471  		}
   472  		retval = append(retval, iop)
   473  	}
   474  	return retval, nil
   475  }
   476  
   477  func istioVerificationFailureError(filename string, reason error) error {
   478  	return fmt.Errorf("Istio installation failed, incomplete or does not match \"%s\": %v", filename, reason) // nolint
   479  }
   480  
   481  func (v *StatusVerifier) reportFailure(kind, name, namespace string, err error) {
   482  	v.logger.LogAndPrintf("%s %s: %s.%s: %v", v.failureMarker, kind, name, namespace, err)
   483  }