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 }