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 }