istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/operator/switch_cr_test.go (about)

     1  //go:build integ
     2  // +build integ
     3  
     4  // Copyright Istio Authors
     5  //
     6  // Licensed under the Apache License, Version 2.0 (the "License");
     7  // you may not use this file except in compliance with the License.
     8  // You may obtain a copy of the License at
     9  //
    10  //     http://www.apache.org/licenses/LICENSE-2.0
    11  //
    12  // Unless required by applicable law or agreed to in writing, software
    13  // distributed under the License is distributed on an "AS IS" BASIS,
    14  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15  // See the License for the specific language governing permissions and
    16  // limitations under the License.
    17  
    18  package operator
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/hashicorp/go-multierror"
    30  	corev1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  
    33  	"istio.io/api/label"
    34  	api "istio.io/api/operator/v1alpha1"
    35  	iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    36  	"istio.io/istio/operator/pkg/object"
    37  	"istio.io/istio/operator/pkg/util"
    38  	"istio.io/istio/pkg/config/schema/gvr"
    39  	istioKube "istio.io/istio/pkg/kube"
    40  	"istio.io/istio/pkg/log"
    41  	"istio.io/istio/pkg/test/env"
    42  	"istio.io/istio/pkg/test/framework"
    43  	"istio.io/istio/pkg/test/framework/components/cluster"
    44  	"istio.io/istio/pkg/test/framework/components/istioctl"
    45  	"istio.io/istio/pkg/test/framework/resource"
    46  	kube2 "istio.io/istio/pkg/test/kube"
    47  	"istio.io/istio/pkg/test/scopes"
    48  	"istio.io/istio/pkg/test/util/retry"
    49  	"istio.io/istio/pkg/util/protomarshal"
    50  	"istio.io/istio/tests/util/sanitycheck"
    51  )
    52  
    53  const (
    54  	IstioNamespace    = "istio-system"
    55  	OperatorNamespace = "istio-operator"
    56  	retryDelay        = time.Second
    57  	retryTimeOut      = 5 * time.Minute
    58  	nsDeletionTimeout = 5 * time.Minute
    59  )
    60  
    61  var (
    62  	// ManifestPath is path of local manifests which istioctl operator init refers to.
    63  	ManifestPath = filepath.Join(env.IstioSrc, "manifests")
    64  	// ManifestPathContainer is path of manifests in the operator container for controller to work with.
    65  	ManifestPathContainer = "/var/lib/istio/manifests"
    66  	iopCRFile             = ""
    67  )
    68  
    69  func TestController(t *testing.T) {
    70  	framework.
    71  		NewTest(t).
    72  		Run(func(t framework.TestContext) {
    73  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
    74  			workDir, err := t.CreateTmpDirectory("operator-controller-test")
    75  			if err != nil {
    76  				t.Fatal("failed to create test directory")
    77  			}
    78  			cs := t.Clusters().Default()
    79  			cleanupInClusterCRs(t, cs)
    80  			t.Cleanup(func() {
    81  				cleanupIstioResources(t, cs, istioCtl)
    82  			})
    83  			s := t.Settings()
    84  			// Operator has no --variant flag, hack it with --tag
    85  			tag := s.Image.Tag
    86  			if v := s.Image.Variant; v != "" {
    87  				tag += "-" + v
    88  			}
    89  			initCmd := []string{
    90  				"operator", "init",
    91  				"--hub=" + s.Image.Hub,
    92  				"--tag=" + tag,
    93  				"--manifests=" + ManifestPath,
    94  			}
    95  			// install istio with default config for the first time by running operator init command
    96  			istioCtl.InvokeOrFail(t, initCmd)
    97  			t.TrackResource(&operatorDumper{rev: ""})
    98  
    99  			if _, err := cs.Kube().CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{
   100  				ObjectMeta: metav1.ObjectMeta{
   101  					Name: IstioNamespace,
   102  				},
   103  			}, metav1.CreateOptions{}); err != nil {
   104  				_, err := cs.Kube().CoreV1().Namespaces().Get(context.TODO(), IstioNamespace, metav1.GetOptions{})
   105  				if err == nil {
   106  					log.Info("istio namespace already exist")
   107  				} else {
   108  					t.Errorf("failed to create istio namespace: %v", err)
   109  				}
   110  			}
   111  			iopCRFile = filepath.Join(workDir, "iop_cr.yaml")
   112  			// later just run `kubectl apply -f newcr.yaml` to apply new installation cr files and verify.
   113  			installWithCRFile(t, t, cs, istioCtl, "demo", "")
   114  
   115  			initCmd = []string{
   116  				"operator", "init",
   117  				"--hub=" + s.Image.Hub,
   118  				"--tag=" + tag,
   119  				"--manifests=" + ManifestPath,
   120  				"--revision=" + "v2",
   121  			}
   122  			// install second operator deployment with different revision
   123  			istioCtl.InvokeOrFail(t, initCmd)
   124  			t.TrackResource(&operatorDumper{rev: "v2"})
   125  
   126  			initCmd = []string{
   127  				"operator", "init",
   128  				"--hub=" + s.Image.Hub,
   129  				"--tag=" + tag,
   130  				"--manifests=" + ManifestPath,
   131  				"--revision=" + "v3",
   132  			}
   133  			// install third operator deployment with different revision
   134  			istioCtl.InvokeOrFail(t, initCmd)
   135  			t.TrackResource(&operatorDumper{rev: "v3"})
   136  
   137  			installWithCRFile(t, t, cs, istioCtl, "default", "v2")
   138  			installWithCRFile(t, t, cs, istioCtl, "default", "")
   139  
   140  			// istio control plane resources expected to be deleted after deleting CRs
   141  			cleanupInClusterCRs(t, cs)
   142  
   143  			// test operator remove command
   144  			scopes.Framework.Infof("checking operator remove command for default revision")
   145  			removeCmd := []string{
   146  				"operator", "remove",
   147  				"--skip-confirmation",
   148  				"--revision", "default",
   149  			}
   150  			istioCtl.InvokeOrFail(t, removeCmd)
   151  
   152  			retry.UntilSuccessOrFail(t, func() error {
   153  				// check the revision of operator which should to be removed
   154  				if svc, _ := cs.Kube().CoreV1().Services(OperatorNamespace).Get(context.TODO(), "istio-operator", metav1.GetOptions{}); svc.Name != "" {
   155  					return fmt.Errorf("got operator service: %s from cluster, expected to be removed", svc.Name)
   156  				}
   157  				if dp, _ := cs.Kube().AppsV1().Deployments(OperatorNamespace).Get(context.TODO(), "istio-operator", metav1.GetOptions{}); dp.Name != "" {
   158  					return fmt.Errorf("got operator deployment %s from cluster, expected to be removed", dp.Name)
   159  				}
   160  
   161  				// check the revision of operator which should not to be removed
   162  				for _, n := range []string{"istio-operator-v2", "istio-operator-v3"} {
   163  					if svc, _ := cs.Kube().CoreV1().Services(OperatorNamespace).Get(context.TODO(), n, metav1.GetOptions{}); svc.Name == "" {
   164  						return fmt.Errorf("don't got operator service: %s from cluster, expected not to be removed", svc.Name)
   165  					}
   166  					if dp, _ := cs.Kube().AppsV1().Deployments(OperatorNamespace).Get(context.TODO(), n, metav1.GetOptions{}); dp.Name == "" {
   167  						return fmt.Errorf("don't got operator deployment %s from cluster, expected not to be removed", dp.Name)
   168  					}
   169  				}
   170  				return nil
   171  			}, retry.Timeout(retryTimeOut), retry.Delay(retryDelay))
   172  
   173  			// test operator remove command by purge
   174  			scopes.Framework.Infof("checking operator remove command")
   175  			removeCmd = []string{
   176  				"operator", "remove",
   177  				"--skip-confirmation",
   178  				"--purge",
   179  			}
   180  			istioCtl.InvokeOrFail(t, removeCmd)
   181  
   182  			retry.UntilSuccessOrFail(t, func() error {
   183  				for _, n := range []string{"istio-operator-v2", "istio-operator-v3"} {
   184  					if svc, _ := cs.Kube().CoreV1().Services(OperatorNamespace).Get(context.TODO(), n, metav1.GetOptions{}); svc.Name != "" {
   185  						return fmt.Errorf("got operator service: %s from cluster, expected to be removed", svc.Name)
   186  					}
   187  					if dp, _ := cs.Kube().AppsV1().Deployments(OperatorNamespace).Get(context.TODO(), n, metav1.GetOptions{}); dp.Name != "" {
   188  						return fmt.Errorf("got operator deployment %s from cluster, expected to be removed", dp.Name)
   189  					}
   190  				}
   191  				return nil
   192  			}, retry.Timeout(retryTimeOut), retry.Delay(retryDelay))
   193  		})
   194  }
   195  
   196  func cleanupIstioResources(t framework.TestContext, cs cluster.Cluster, istioCtl istioctl.Instance) {
   197  	scopes.Framework.Infof("cleaning up resources")
   198  	// clean up Istio control plane
   199  	unInstallCmd := []string{
   200  		"uninstall", "--purge", "--skip-confirmation",
   201  	}
   202  	out, _ := istioCtl.InvokeOrFail(t, unInstallCmd)
   203  	t.Logf("uninstall command output: %s", out)
   204  	// clean up operator namespace
   205  	if err := cs.Kube().CoreV1().Namespaces().Delete(context.TODO(), OperatorNamespace,
   206  		kube2.DeleteOptionsForeground()); err != nil {
   207  		t.Logf("failed to delete operator namespace: %v", err)
   208  	}
   209  	if err := kube2.WaitForNamespaceDeletion(cs.Kube(), OperatorNamespace, retry.Timeout(nsDeletionTimeout)); err != nil {
   210  		t.Logf("failed waiting for operator namespace to be deleted: %v", err)
   211  	}
   212  	var err error
   213  	// clean up dynamically created secret and configmaps
   214  	if e := cs.Kube().CoreV1().Secrets(IstioNamespace).DeleteCollection(
   215  		context.Background(), metav1.DeleteOptions{}, metav1.ListOptions{}); e != nil {
   216  		err = multierror.Append(err, e)
   217  	}
   218  	if e := cs.Kube().CoreV1().ConfigMaps(IstioNamespace).DeleteCollection(
   219  		context.Background(), metav1.DeleteOptions{}, metav1.ListOptions{}); e != nil {
   220  		err = multierror.Append(err, e)
   221  	}
   222  	if err != nil {
   223  		scopes.Framework.Errorf("failed to cleanup dynamically created resources: %v", err)
   224  	}
   225  }
   226  
   227  // checkInstallStatus check the status of IstioOperator CR from the cluster
   228  func checkInstallStatus(cs istioKube.CLIClient, revision string) error {
   229  	scopes.Framework.Infof("checking IstioOperator CR status")
   230  	gvr := iopv1alpha1.IstioOperatorGVR
   231  
   232  	var unhealthyCN []string
   233  	retryFunc := func() error {
   234  		us, err := cs.Dynamic().Resource(gvr).Namespace(IstioNamespace).Get(context.TODO(), revName("test-istiocontrolplane", revision), metav1.GetOptions{})
   235  		if err != nil {
   236  			return fmt.Errorf("failed to get istioOperator resource: %v", err)
   237  		}
   238  		usIOPStatus := us.UnstructuredContent()["status"]
   239  		if usIOPStatus == nil {
   240  			if _, err := cs.Kube().CoreV1().Services(OperatorNamespace).Get(context.TODO(), revName("istio-operator", revision),
   241  				metav1.GetOptions{}); err != nil {
   242  				return fmt.Errorf("istio operator svc is not ready: %v", err)
   243  			}
   244  			if _, err := kube2.CheckPodsAreReady(kube2.NewPodFetch(cs, OperatorNamespace, "")); err != nil {
   245  				return fmt.Errorf("istio operator pod is not ready: %v", err)
   246  			}
   247  
   248  			return fmt.Errorf("status not found from the istioOperator resource")
   249  		}
   250  		usIOPStatus = usIOPStatus.(map[string]any)
   251  		iopStatusString, err := json.Marshal(usIOPStatus)
   252  		if err != nil {
   253  			return fmt.Errorf("failed to marshal istioOperator status: %v", err)
   254  		}
   255  		status := &api.InstallStatus{}
   256  		if err := protomarshal.UnmarshalAllowUnknown(iopStatusString, status); err != nil {
   257  			return fmt.Errorf("failed to unmarshal istioOperator status: %v", err)
   258  		}
   259  		errs := util.Errors{}
   260  		unhealthyCN = []string{}
   261  		if status.Status != api.InstallStatus_HEALTHY {
   262  			errs = util.AppendErr(errs, fmt.Errorf("got IstioOperator status: %v", status.Status))
   263  		}
   264  
   265  		for cn, cnstatus := range status.ComponentStatus {
   266  			if cnstatus.Status != api.InstallStatus_HEALTHY {
   267  				unhealthyCN = append(unhealthyCN, cn)
   268  				errs = util.AppendErr(errs, fmt.Errorf("got component: %s status: %v", cn, cnstatus.Status))
   269  			}
   270  		}
   271  		return errs.ToError()
   272  	}
   273  	scopes.Framework.Infof("waiting for IOP to become healthy")
   274  	err := retry.UntilSuccess(retryFunc, retry.Timeout(retryTimeOut), retry.Delay(retryDelay))
   275  	if err != nil {
   276  		return fmt.Errorf("istioOperator status is not healthy: %v", err)
   277  	}
   278  	return nil
   279  }
   280  
   281  func cleanupInClusterCRs(t framework.TestContext, cs cluster.Cluster) {
   282  	// clean up hanging installed-state CR from previous tests, failing for errors is not needed here.
   283  	scopes.Framework.Info("cleaning up in-cluster CRs")
   284  	gvr := iopv1alpha1.IstioOperatorGVR
   285  	crList, err := cs.Dynamic().Resource(gvr).Namespace(IstioNamespace).List(context.TODO(),
   286  		metav1.ListOptions{})
   287  	if err == nil {
   288  		for _, obj := range crList.Items {
   289  			t.Logf("deleting CR %v", obj.GetName())
   290  			if err := cs.Dynamic().Resource(gvr).Namespace(IstioNamespace).Delete(context.TODO(), obj.GetName(),
   291  				metav1.DeleteOptions{}); err != nil {
   292  				t.Logf("failed to delete existing CR: %v", err)
   293  			}
   294  		}
   295  	} else {
   296  		t.Logf("failed to list existing CR: %v", err.Error())
   297  	}
   298  
   299  	scopes.Framework.Infof("waiting for workloads in istio-system to be deleted")
   300  	// wait for workloads in istio-system to be deleted
   301  	err = retry.UntilSuccess(func() error {
   302  		deployList, err := cs.Kube().AppsV1().Deployments(IstioNamespace).List(context.TODO(), metav1.ListOptions{
   303  			LabelSelector: label.IoIstioRev.Name,
   304  		})
   305  		if err != nil {
   306  			return err
   307  		}
   308  		if len(deployList.Items) > 0 {
   309  			return fmt.Errorf("workloads still remain in %s", IstioNamespace)
   310  		}
   311  		dsList, err := cs.Kube().AppsV1().DaemonSets(IstioNamespace).List(context.TODO(), metav1.ListOptions{
   312  			LabelSelector: label.IoIstioRev.Name,
   313  		})
   314  		if err != nil {
   315  			return err
   316  		}
   317  		if len(dsList.Items) > 0 {
   318  			return fmt.Errorf("workloads still remain in %s", IstioNamespace)
   319  		}
   320  		return nil
   321  	}, retry.Timeout(retryTimeOut), retry.Delay(retryDelay))
   322  
   323  	if err != nil {
   324  		t.Logf("failed to delete pods in %s: %v", IstioNamespace, err)
   325  	} else {
   326  		t.Logf("all pods in istio-system deleted")
   327  	}
   328  }
   329  
   330  func installWithCRFile(t framework.TestContext, ctx resource.Context, cs cluster.Cluster,
   331  	istioCtl istioctl.Instance, profileName string, revision string,
   332  ) {
   333  	scopes.Framework.Infof(fmt.Sprintf("=== install istio with profile: %s===\n", profileName))
   334  	metadataYAML := `
   335  apiVersion: install.istio.io/v1alpha1
   336  kind: IstioOperator
   337  metadata:
   338    name: %s
   339    namespace: istio-system
   340  spec:
   341  `
   342  	if revision != "" {
   343  		metadataYAML += "  revision: " + revision + "\n"
   344  	}
   345  
   346  	metadataYAML += `
   347    profile: %s
   348    installPackagePath: %s
   349    hub: %s
   350    tag: %s
   351    values:
   352      global:
   353        variant: %q
   354        imagePullPolicy: %s
   355  `
   356  	s := ctx.Settings()
   357  	overlayYAML := fmt.Sprintf(metadataYAML, revName("test-istiocontrolplane", revision), profileName, ManifestPathContainer,
   358  		s.Image.Hub, s.Image.Tag, s.Image.Variant, s.Image.PullPolicy)
   359  
   360  	scopes.Framework.Infof("=== installing with IOP: ===\n%s\n", overlayYAML)
   361  
   362  	if err := os.WriteFile(iopCRFile, []byte(overlayYAML), os.ModePerm); err != nil {
   363  		t.Fatalf("failed to write iop cr file: %v", err)
   364  	}
   365  
   366  	if err := cs.ApplyYAMLFiles(IstioNamespace, iopCRFile); err != nil {
   367  		t.Fatalf("failed to apply IstioOperator CR file: %s, %v", iopCRFile, err)
   368  	}
   369  
   370  	verifyInstallation(t, ctx, istioCtl, profileName, revision, cs)
   371  }
   372  
   373  // verifyInstallation verify IOP CR status and compare in-cluster resources with generated ones.
   374  // It also returns the expected K8sObjects generated by manifest generate command.
   375  func verifyInstallation(t framework.TestContext, ctx resource.Context,
   376  	istioCtl istioctl.Instance, profileName string, revision string, cs cluster.Cluster,
   377  ) object.K8sObjects {
   378  	scopes.Framework.Infof("=== verifying istio installation revision %s === ", revision)
   379  	if err := checkInstallStatus(cs, revision); err != nil {
   380  		t.Fatalf("IstioOperator status not healthy: %v", err)
   381  	}
   382  
   383  	if _, err := kube2.CheckPodsAreReady(kube2.NewSinglePodFetch(cs, IstioNamespace, "app=istiod")); err != nil {
   384  		t.Fatalf("istiod pod is not ready: %v", err)
   385  	}
   386  
   387  	// get manifests by running `manifest generate`
   388  	generateCmd := []string{
   389  		"manifest", "generate",
   390  		"--manifests", ManifestPath,
   391  	}
   392  	if profileName != "" {
   393  		generateCmd = append(generateCmd, "--set", fmt.Sprintf("profile=%s", profileName))
   394  	}
   395  	if revision != "" {
   396  		generateCmd = append(generateCmd, "--revision", revision)
   397  	}
   398  	genManifests, _ := istioCtl.InvokeOrFail(t, generateCmd)
   399  	K8SObjects, err := object.ParseK8sObjectsFromYAMLManifest(genManifests)
   400  	if err != nil {
   401  		t.Errorf("failed to parse generated manifest: %v", err)
   402  	}
   403  
   404  	compareInClusterAndGeneratedResources(t, cs, K8SObjects, false)
   405  	sanitycheck.RunTrafficTest(t, ctx)
   406  	scopes.Framework.Infof("=== succeeded ===")
   407  	return K8SObjects
   408  }
   409  
   410  func compareInClusterAndGeneratedResources(t framework.TestContext, cs cluster.Cluster, k8sObjects object.K8sObjects,
   411  	expectRemoved bool,
   412  ) {
   413  	// nolint:staticcheck
   414  	if k8sObjects == nil {
   415  		t.Fatalf("expected K8sObjects is nil")
   416  	}
   417  
   418  	// nolint:staticcheck
   419  	for _, genK8SObject := range k8sObjects {
   420  		kind := genK8SObject.Kind
   421  		ns := genK8SObject.Namespace
   422  		name := genK8SObject.Name
   423  		scopes.Framework.Infof("checking kind: %s, namespace: %s, name: %s", kind, ns, name)
   424  		retry.UntilSuccessOrFail(t, func() error {
   425  			var err error
   426  			switch kind {
   427  			case "Service":
   428  				_, err = cs.Kube().CoreV1().Services(ns).Get(context.TODO(), name, metav1.GetOptions{})
   429  			case "ServiceAccount":
   430  				_, err = cs.Kube().CoreV1().ServiceAccounts(ns).Get(context.TODO(), name, metav1.GetOptions{})
   431  			case "Deployment":
   432  				_, err = cs.Kube().AppsV1().Deployments(IstioNamespace).Get(context.TODO(), name,
   433  					metav1.GetOptions{})
   434  			case "ConfigMap":
   435  				_, err = cs.Kube().CoreV1().ConfigMaps(ns).Get(context.TODO(), name, metav1.GetOptions{})
   436  			case "ValidatingWebhookConfiguration":
   437  				_, err = cs.Kube().AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(),
   438  					name, metav1.GetOptions{})
   439  			case "MutatingWebhookConfiguration":
   440  				_, err = cs.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(),
   441  					name, metav1.GetOptions{})
   442  			case "CustomResourceDefinition":
   443  				_, err = cs.Ext().ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name,
   444  					metav1.GetOptions{})
   445  			case "EnvoyFilter":
   446  				_, err = cs.Dynamic().Resource(gvr.EnvoyFilter).Namespace(ns).Get(context.TODO(), name,
   447  					metav1.GetOptions{})
   448  			case "PodDisruptionBudget":
   449  				// policy/v1 is available on >=1.21
   450  				if cs.MinKubeVersion(21) {
   451  					_, err = cs.Kube().PolicyV1().PodDisruptionBudgets(ns).Get(context.TODO(), name,
   452  						metav1.GetOptions{})
   453  				} else {
   454  					_, err = cs.Kube().PolicyV1beta1().PodDisruptionBudgets(ns).Get(context.TODO(), name,
   455  						metav1.GetOptions{})
   456  				}
   457  			case "HorizontalPodAutoscaler":
   458  				// autoscaling v2 API is available on >=1.23
   459  				if cs.MinKubeVersion(23) {
   460  					_, err = cs.Kube().AutoscalingV2().HorizontalPodAutoscalers(ns).Get(context.TODO(), name,
   461  						metav1.GetOptions{})
   462  				} else {
   463  					_, err = cs.Kube().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).Get(context.TODO(), name,
   464  						metav1.GetOptions{})
   465  				}
   466  			}
   467  			if err != nil && !expectRemoved {
   468  				return fmt.Errorf("failed to get expected %s: %s from cluster", kind, name)
   469  			}
   470  			if err == nil && expectRemoved && kind != "CustomResourceDefinition" {
   471  				return fmt.Errorf("%s: %s expected to be removed from cluster but still exists", kind, name)
   472  			}
   473  			return nil
   474  		}, retry.Timeout(time.Second*300), retry.Delay(time.Millisecond*100))
   475  	}
   476  }
   477  
   478  func revName(name, revision string) string {
   479  	if revision == "" {
   480  		return name
   481  	}
   482  	return name + "-" + revision
   483  }