istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/helmreconciler/reconciler.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 "os" 21 "strings" 22 "sync" 23 "time" 24 25 "k8s.io/apimachinery/pkg/api/meta" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/apimachinery/pkg/runtime/schema" 30 "k8s.io/apimachinery/pkg/types" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 "sigs.k8s.io/yaml" 33 34 "istio.io/api/label" 35 "istio.io/api/operator/v1alpha1" 36 revtag "istio.io/istio/istioctl/pkg/tag" 37 "istio.io/istio/istioctl/pkg/util/formatting" 38 istioV1Alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" 39 "istio.io/istio/operator/pkg/helm" 40 "istio.io/istio/operator/pkg/metrics" 41 "istio.io/istio/operator/pkg/name" 42 "istio.io/istio/operator/pkg/object" 43 "istio.io/istio/operator/pkg/util" 44 "istio.io/istio/operator/pkg/util/clog" 45 "istio.io/istio/operator/pkg/util/progress" 46 "istio.io/istio/pkg/config" 47 "istio.io/istio/pkg/config/analysis" 48 "istio.io/istio/pkg/config/analysis/analyzers/webhook" 49 "istio.io/istio/pkg/config/analysis/diag" 50 "istio.io/istio/pkg/config/analysis/local" 51 "istio.io/istio/pkg/config/resource" 52 "istio.io/istio/pkg/config/schema/gvk" 53 "istio.io/istio/pkg/config/schema/gvr" 54 "istio.io/istio/pkg/kube" 55 "istio.io/istio/pkg/version" 56 ) 57 58 // HelmReconciler reconciles resources rendered by a set of helm charts. 59 type HelmReconciler struct { 60 client client.Client 61 kubeClient kube.Client 62 iop *istioV1Alpha1.IstioOperator 63 opts *Options 64 // copy of the last generated manifests. 65 manifests name.ManifestMap 66 // dependencyWaitCh is a map of signaling channels. A parent with children ch1...chN will signal 67 // dependencyWaitCh[ch1]...dependencyWaitCh[chN] when it's completely installed. 68 dependencyWaitCh map[name.ComponentName]chan struct{} 69 70 // The fields below are for metrics and reporting 71 countLock *sync.Mutex 72 prunedKindSet map[schema.GroupKind]struct{} 73 } 74 75 // Options are options for HelmReconciler. 76 type Options struct { 77 // DryRun executes all actions but does not write anything to the cluster. 78 DryRun bool 79 // Log is a console logger for user visible CLI output. 80 Log clog.Logger 81 // Wait determines if we will wait for resources to be fully applied. Only applies to components that have no 82 // dependencies. 83 Wait bool 84 // WaitTimeout controls the amount of time to wait for resources in a component to become ready before giving up. 85 WaitTimeout time.Duration 86 // Log tracks the installation progress for all components. 87 ProgressLog *progress.Log 88 // Force ignores validation errors 89 Force bool 90 // SkipPrune will skip pruning 91 SkipPrune bool 92 } 93 94 var defaultOptions = &Options{ 95 Log: clog.NewDefaultLogger(), 96 ProgressLog: progress.NewLog(), 97 } 98 99 // NewHelmReconciler creates a HelmReconciler and returns a ptr to it 100 func NewHelmReconciler(client client.Client, kubeClient kube.Client, iop *istioV1Alpha1.IstioOperator, opts *Options) (*HelmReconciler, error) { 101 if opts == nil { 102 opts = defaultOptions 103 } 104 if opts.ProgressLog == nil { 105 opts.ProgressLog = progress.NewLog() 106 } 107 if int64(opts.WaitTimeout) == 0 { 108 if waitForResourcesTimeoutStr, found := os.LookupEnv("WAIT_FOR_RESOURCES_TIMEOUT"); found { 109 if waitForResourcesTimeout, err := time.ParseDuration(waitForResourcesTimeoutStr); err == nil { 110 opts.WaitTimeout = waitForResourcesTimeout 111 } else { 112 scope.Warnf("invalid env variable value: %s for 'WAIT_FOR_RESOURCES_TIMEOUT'! falling back to default value...", waitForResourcesTimeoutStr) 113 // fallback to default wait resource timeout 114 opts.WaitTimeout = defaultWaitResourceTimeout 115 } 116 } else { 117 // fallback to default wait resource timeout 118 opts.WaitTimeout = defaultWaitResourceTimeout 119 } 120 } 121 if iop == nil { 122 // allows controller code to function for cases where IOP is not provided (e.g. operator remove). 123 iop = &istioV1Alpha1.IstioOperator{} 124 iop.Spec = &v1alpha1.IstioOperatorSpec{} 125 } 126 return &HelmReconciler{ 127 client: client, 128 kubeClient: kubeClient, 129 iop: iop, 130 opts: opts, 131 dependencyWaitCh: initDependencies(), 132 countLock: &sync.Mutex{}, 133 prunedKindSet: make(map[schema.GroupKind]struct{}), 134 }, nil 135 } 136 137 // initDependencies initializes the dependencies channel tree. 138 func initDependencies() map[name.ComponentName]chan struct{} { 139 ret := make(map[name.ComponentName]chan struct{}) 140 for _, parent := range ComponentDependencies { 141 for _, child := range parent { 142 ret[child] = make(chan struct{}, 1) 143 } 144 } 145 return ret 146 } 147 148 // Reconcile reconciles the associated resources. 149 func (h *HelmReconciler) Reconcile() (*v1alpha1.InstallStatus, error) { 150 if err := util.CreateNamespace(h.kubeClient.Kube(), istioV1Alpha1.Namespace(h.iop.Spec), h.networkName(), h.opts.DryRun); err != nil { 151 return nil, err 152 } 153 manifestMap, err := h.RenderCharts() 154 if err != nil { 155 return nil, err 156 } 157 158 err = h.analyzeWebhooks(manifestMap[name.PilotComponentName]) 159 if err != nil { 160 if h.opts.Force { 161 scope.Error("invalid webhook configs; continuing because of --force") 162 } else { 163 return nil, err 164 } 165 } 166 status := h.processRecursive(manifestMap) 167 168 var pruneErr error 169 if !h.opts.SkipPrune && !h.opts.DryRun { 170 h.opts.ProgressLog.SetState(progress.StatePruning) 171 pruneErr = h.Prune(manifestMap, false) 172 h.reportPrunedObjectKind() 173 } 174 return status, pruneErr 175 } 176 177 // processRecursive processes the given manifests in an order of dependencies defined in h. Dependencies are a tree, 178 // where a child must wait for the parent to complete before starting. 179 func (h *HelmReconciler) processRecursive(manifests name.ManifestMap) *v1alpha1.InstallStatus { 180 componentStatus := make(map[string]*v1alpha1.InstallStatus_VersionStatus) 181 182 // mu protects the shared InstallStatus componentStatus across goroutines 183 var mu sync.Mutex 184 // wg waits for all manifest processing goroutines to finish 185 var wg sync.WaitGroup 186 187 for c, ms := range manifests { 188 c, ms := c, ms 189 wg.Add(1) 190 go func() { 191 var appliedResult AppliedResult 192 defer wg.Done() 193 if s := h.dependencyWaitCh[c]; s != nil { 194 scope.Infof("%s is waiting on dependency...", c) 195 <-s 196 scope.Infof("Dependency for %s has completed, proceeding.", c) 197 } 198 199 // Possible paths for status are RECONCILING -> {NONE, ERROR, HEALTHY}. NONE means component has no resources. 200 // In NONE case, the component is not shown in overall status. 201 mu.Lock() 202 setStatus(componentStatus, c, v1alpha1.InstallStatus_RECONCILING, nil) 203 mu.Unlock() 204 205 status := v1alpha1.InstallStatus_NONE 206 var err error 207 if len(ms) != 0 { 208 m := name.Manifest{ 209 Name: c, 210 Content: name.MergeManifestSlices(ms), 211 } 212 appliedResult, err = h.ApplyManifest(m) 213 if err != nil { 214 status = v1alpha1.InstallStatus_ERROR 215 } else if appliedResult.Succeed() { 216 status = v1alpha1.InstallStatus_HEALTHY 217 } 218 } 219 220 mu.Lock() 221 setStatus(componentStatus, c, status, err) 222 mu.Unlock() 223 224 // Signal all the components that depend on us. 225 for _, ch := range ComponentDependencies[c] { 226 scope.Infof("Unblocking dependency %s.", ch) 227 h.dependencyWaitCh[ch] <- struct{}{} 228 } 229 }() 230 } 231 wg.Wait() 232 233 metrics.ReportOwnedResourceCounts() 234 235 out := &v1alpha1.InstallStatus{ 236 Status: overallStatus(componentStatus), 237 ComponentStatus: componentStatus, 238 } 239 240 return out 241 } 242 243 // Delete resources associated with the custom resource instance 244 func (h *HelmReconciler) Delete() error { 245 defer func() { 246 metrics.ReportOwnedResourceCounts() 247 h.reportPrunedObjectKind() 248 }() 249 iop := h.iop 250 if iop.Spec.Revision == "" { 251 err := h.Prune(nil, true) 252 return err 253 } 254 // Delete IOP with revision: 255 // for this case we update the status field to pending if there are still proxies pointing to this revision 256 // and we do not prune shared resources, same effect as `istioctl uninstall --revision foo` command. 257 status, err := h.PruneControlPlaneByRevisionWithController(iop.Spec) 258 if err != nil { 259 return err 260 } 261 262 // check status here because terminating iop's status can't be updated. 263 if status.Status == v1alpha1.InstallStatus_ACTION_REQUIRED { 264 return fmt.Errorf("action is required before deleting the iop instance: %s", status.Message) 265 } 266 267 // updating status taking no effect for terminating resources. 268 if err := h.SetStatusComplete(status); err != nil { 269 return err 270 } 271 return nil 272 } 273 274 func (h *HelmReconciler) DeleteIOPInClusterIfExists(iop *istioV1Alpha1.IstioOperator) { 275 // Delete the previous IstioOperator CR if it exists. 276 objectKey := client.ObjectKeyFromObject(iop) 277 receiver := &unstructured.Unstructured{} 278 receiver.SetGroupVersionKind(istioV1Alpha1.IstioOperatorGVK) 279 if err := h.client.Get(context.TODO(), objectKey, receiver); err == nil { 280 _ = h.client.Delete(context.TODO(), receiver) 281 } 282 } 283 284 // SetStatusBegin updates the status field on the IstioOperator instance before reconciling. 285 func (h *HelmReconciler) SetStatusBegin() error { 286 isop := &istioV1Alpha1.IstioOperator{} 287 if err := h.getClient().Get(context.TODO(), config.NamespacedName(h.iop), isop); err != nil { 288 if runtime.IsNotRegisteredError(err) { 289 // CRD not yet installed in cluster, nothing to update. 290 return nil 291 } 292 return fmt.Errorf("failed to get IstioOperator before updating status due to %v", err) 293 } 294 if isop.Status == nil { 295 isop.Status = &v1alpha1.InstallStatus{Status: v1alpha1.InstallStatus_RECONCILING} 296 } else { 297 cs := isop.Status.ComponentStatus 298 for cn := range cs { 299 cs[cn] = &v1alpha1.InstallStatus_VersionStatus{ 300 Status: v1alpha1.InstallStatus_RECONCILING, 301 } 302 } 303 isop.Status.Status = v1alpha1.InstallStatus_RECONCILING 304 } 305 return h.getClient().Status().Update(context.TODO(), isop) 306 } 307 308 // SetStatusComplete updates the status field on the IstioOperator instance based on the resulting err parameter. 309 func (h *HelmReconciler) SetStatusComplete(status *v1alpha1.InstallStatus) error { 310 iop := &istioV1Alpha1.IstioOperator{} 311 if err := h.getClient().Get(context.TODO(), config.NamespacedName(h.iop), iop); err != nil { 312 return fmt.Errorf("failed to get IstioOperator before updating status due to %v", err) 313 } 314 iop.Status = status 315 return h.getClient().Status().Update(context.TODO(), iop) 316 } 317 318 // setStatus sets the status for the component with the given name, which is a key in the given map. 319 // If the status is InstallStatus_NONE, the component name is deleted from the map. 320 // Otherwise, if the map key/value is missing, one is created. 321 func setStatus(s map[string]*v1alpha1.InstallStatus_VersionStatus, componentName name.ComponentName, status v1alpha1.InstallStatus_Status, err error) { 322 cn := string(componentName) 323 if status == v1alpha1.InstallStatus_NONE { 324 delete(s, cn) 325 return 326 } 327 if _, ok := s[cn]; !ok { 328 s[cn] = &v1alpha1.InstallStatus_VersionStatus{} 329 } 330 s[cn].Status = status 331 if err != nil { 332 s[cn].Error = err.Error() 333 } 334 } 335 336 // overallStatus returns the summary status over all components. 337 // - If all components are HEALTHY, overall status is HEALTHY. 338 // - If one or more components are RECONCILING and others are HEALTHY, overall status is RECONCILING. 339 // - If one or more components are UPDATING and others are HEALTHY, overall status is UPDATING. 340 // - If components are a mix of RECONCILING, UPDATING and HEALTHY, overall status is UPDATING. 341 // - If any component is in ERROR state, overall status is ERROR. 342 func overallStatus(componentStatus map[string]*v1alpha1.InstallStatus_VersionStatus) v1alpha1.InstallStatus_Status { 343 ret := v1alpha1.InstallStatus_HEALTHY 344 for _, cs := range componentStatus { 345 if cs.Status == v1alpha1.InstallStatus_ERROR { 346 ret = v1alpha1.InstallStatus_ERROR 347 break 348 } else if cs.Status == v1alpha1.InstallStatus_UPDATING { 349 ret = v1alpha1.InstallStatus_UPDATING 350 break 351 } else if cs.Status == v1alpha1.InstallStatus_RECONCILING { 352 ret = v1alpha1.InstallStatus_RECONCILING 353 break 354 } 355 } 356 return ret 357 } 358 359 // getCoreOwnerLabels returns a map of labels for associating installation resources. This is the common 360 // labels shared between all resources; see getOwnerLabels to get labels per-component labels 361 func (h *HelmReconciler) getCoreOwnerLabels() (map[string]string, error) { 362 crName, err := h.getCRName() 363 if err != nil { 364 return nil, err 365 } 366 crNamespace, err := h.getCRNamespace() 367 if err != nil { 368 return nil, err 369 } 370 labels := make(map[string]string) 371 372 labels[operatorLabelStr] = operatorReconcileStr 373 if crName != "" { 374 labels[OwningResourceName] = crName 375 } 376 if crNamespace != "" { 377 labels[OwningResourceNamespace] = crNamespace 378 } 379 labels[istioVersionLabelStr] = version.Info.Version 380 381 revision := "" 382 if h.iop != nil { 383 revision = h.iop.Spec.Revision 384 } 385 if revision == "" { 386 revision = "default" 387 } 388 labels[label.IoIstioRev.Name] = revision 389 390 return labels, nil 391 } 392 393 func (h *HelmReconciler) addComponentLabels(coreLabels map[string]string, componentName string) map[string]string { 394 labels := map[string]string{} 395 for k, v := range coreLabels { 396 labels[k] = v 397 } 398 399 labels[IstioComponentLabelStr] = componentName 400 401 return labels 402 } 403 404 // getOwnerLabels returns a map of labels for the given component name, revision and owning CR resource name. 405 func (h *HelmReconciler) getOwnerLabels(componentName string) (map[string]string, error) { 406 labels, err := h.getCoreOwnerLabels() 407 if err != nil { 408 return nil, err 409 } 410 411 return h.addComponentLabels(labels, componentName), nil 412 } 413 414 // applyLabelsAndAnnotations applies owner labels and annotations to the object. 415 func (h *HelmReconciler) applyLabelsAndAnnotations(obj runtime.Object, componentName string) error { 416 labels, err := h.getOwnerLabels(componentName) 417 if err != nil { 418 return err 419 } 420 421 for k, v := range labels { 422 err := util.SetLabel(obj, k, v) 423 if err != nil { 424 return err 425 } 426 } 427 return nil 428 } 429 430 // getCRName returns the name of the CR associated with h. 431 func (h *HelmReconciler) getCRName() (string, error) { 432 if h.iop == nil { 433 return "", nil 434 } 435 objAccessor, err := meta.Accessor(h.iop) 436 if err != nil { 437 return "", err 438 } 439 return objAccessor.GetName(), nil 440 } 441 442 // getCRHash returns the cluster unique hash of the CR associated with h. 443 func (h *HelmReconciler) getCRHash(componentName string) (string, error) { 444 crName, err := h.getCRName() 445 if err != nil { 446 return "", err 447 } 448 crNamespace, err := h.getCRNamespace() 449 if err != nil { 450 return "", err 451 } 452 var host string 453 if h.kubeClient != nil && h.kubeClient.RESTConfig() != nil { 454 host = h.kubeClient.RESTConfig().Host 455 } 456 return strings.Join([]string{crName, crNamespace, componentName, host}, "-"), nil 457 } 458 459 // getCRNamespace returns the namespace of the CR associated with h. 460 func (h *HelmReconciler) getCRNamespace() (string, error) { 461 if h.iop == nil { 462 return "", nil 463 } 464 objAccessor, err := meta.Accessor(h.iop) 465 if err != nil { 466 return "", err 467 } 468 return objAccessor.GetNamespace(), nil 469 } 470 471 // getClient returns the kubernetes client associated with this HelmReconciler 472 func (h *HelmReconciler) getClient() client.Client { 473 return h.client 474 } 475 476 func (h *HelmReconciler) addPrunedKind(gk schema.GroupKind) { 477 h.countLock.Lock() 478 defer h.countLock.Unlock() 479 h.prunedKindSet[gk] = struct{}{} 480 } 481 482 func (h *HelmReconciler) reportPrunedObjectKind() { 483 h.countLock.Lock() 484 defer h.countLock.Unlock() 485 for gvk := range h.prunedKindSet { 486 metrics.ResourcePruneTotal. 487 With(metrics.ResourceKindLabel.Value(util.GKString(gvk))). 488 Increment() 489 } 490 } 491 492 func (h *HelmReconciler) analyzeWebhooks(whs []string) error { 493 if len(whs) == 0 { 494 return nil 495 } 496 497 // Add webhook manifests to be applied 498 var localWebhookYAMLReaders []local.ReaderSource 499 var parsedK8sObjects object.K8sObjects 500 exists := revtag.PreviousInstallExists(context.Background(), h.kubeClient.Kube()) 501 for i, wh := range whs { 502 k8sObjects, err := object.ParseK8sObjectsFromYAMLManifest(wh) 503 if err != nil { 504 return err 505 } 506 objYaml, err := k8sObjects.YAMLManifest() 507 if err != nil { 508 return err 509 } 510 // Here if we need to create a default tag, we need to skip the webhooks that are going to be deactivated. 511 if !DetectIfTagWebhookIsNeeded(h.iop, exists) { 512 whReaderSource := local.ReaderSource{ 513 Name: fmt.Sprintf("installed-webhook-%d", i), 514 Reader: strings.NewReader(objYaml), 515 } 516 localWebhookYAMLReaders = append(localWebhookYAMLReaders, whReaderSource) 517 } 518 parsedK8sObjects = append(parsedK8sObjects, k8sObjects...) 519 } 520 521 sa := local.NewSourceAnalyzer(analysis.Combine("webhook", &webhook.Analyzer{ 522 SkipServiceCheck: true, 523 SkipDefaultRevisionedWebhook: DetectIfTagWebhookIsNeeded(h.iop, exists), 524 }), resource.Namespace(h.iop.Spec.GetNamespace()), resource.Namespace(istioV1Alpha1.Namespace(h.iop.Spec)), nil) 525 526 // Add in-cluster webhooks 527 objects := &unstructured.UnstructuredList{} 528 objects.SetGroupVersionKind(gvk.MutatingWebhookConfiguration.Kubernetes()) 529 err := h.client.List(context.Background(), objects, &client.ListOptions{}) 530 if err != nil { 531 return err 532 } 533 for i, obj := range objects.Items { 534 objYAML, err := object.NewK8sObject(&obj, nil, nil).YAML() 535 if err != nil { 536 return err 537 } 538 whReaderSource := local.ReaderSource{ 539 Name: fmt.Sprintf("in-cluster-webhook-%d", i), 540 Reader: strings.NewReader(string(objYAML)), 541 } 542 err = sa.AddReaderKubeSource([]local.ReaderSource{whReaderSource}) 543 if err != nil { 544 return err 545 } 546 } 547 548 err = sa.AddReaderKubeSource(localWebhookYAMLReaders) 549 if err != nil { 550 return err 551 } 552 553 // Analyze webhooks 554 res, err := sa.Analyze(make(chan struct{})) 555 if err != nil { 556 return err 557 } 558 relevantMessages := filterOutBasedOnResources(res.Messages, parsedK8sObjects) 559 if len(relevantMessages) > 0 { 560 o, err := formatting.Print(relevantMessages, formatting.LogFormat, false) 561 if err != nil { 562 return err 563 } 564 return fmt.Errorf("creating default tag would conflict:\n%v", o) 565 } 566 return nil 567 } 568 569 func filterOutBasedOnResources(ms diag.Messages, resources object.K8sObjects) diag.Messages { 570 outputMessages := diag.Messages{} 571 for _, m := range ms { 572 for _, rs := range resources { 573 if rs.Name == m.Resource.Metadata.FullName.Name.String() { 574 outputMessages = append(outputMessages, m) 575 break 576 } 577 } 578 } 579 return outputMessages 580 } 581 582 func (h *HelmReconciler) networkName() string { 583 if h.iop.Spec.GetValues() == nil { 584 return "" 585 } 586 globalI := h.iop.Spec.Values.AsMap()["global"] 587 global, ok := globalI.(map[string]any) 588 if !ok { 589 return "" 590 } 591 nw, ok := global["network"].(string) 592 if !ok { 593 return "" 594 } 595 return nw 596 } 597 598 type ProcessDefaultWebhookOptions struct { 599 Namespace string 600 DryRun bool 601 } 602 603 func DetectIfTagWebhookIsNeeded(iop *istioV1Alpha1.IstioOperator, exists bool) bool { 604 rev := iop.Spec.Revision 605 isDefaultInstallation := rev == "" && iop.Spec.Components.Pilot != nil && iop.Spec.Components.Pilot.Enabled.Value 606 operatorManageWebhooks := operatorManageWebhooks(iop) 607 return !operatorManageWebhooks && (!exists || isDefaultInstallation) 608 } 609 610 func ProcessDefaultWebhook(client kube.Client, iop *istioV1Alpha1.IstioOperator, exists bool, opt *ProcessDefaultWebhookOptions) (processed bool, err error) { 611 // Detect whether previous installation exists prior to performing the installation. 612 if !DetectIfTagWebhookIsNeeded(iop, exists) { 613 return false, nil 614 } 615 rev := iop.Spec.Revision 616 if rev == "" { 617 rev = revtag.DefaultRevisionName 618 } 619 autoInjectNamespaces := validateEnableNamespacesByDefault(iop) 620 621 ignorePruneLabel := map[string]string{ 622 OwningResourceNotPruned: "true", 623 } 624 625 o := &revtag.GenerateOptions{ 626 Tag: revtag.DefaultRevisionName, 627 Revision: rev, 628 Overwrite: true, 629 AutoInjectNamespaces: autoInjectNamespaces, 630 CustomLabels: ignorePruneLabel, 631 Generate: opt.DryRun, 632 } 633 // If tag cannot be created could be remote cluster install, don't fail out. 634 tagManifests, err := revtag.Generate(context.Background(), client, o, opt.Namespace) 635 if err == nil && !opt.DryRun { 636 if err = applyManifests(client, tagManifests); err != nil { 637 return false, err 638 } 639 } 640 return true, nil 641 } 642 643 func applyManifests(kubeClient kube.Client, manifests string) error { 644 yamls := strings.Split(manifests, helm.YAMLSeparator) 645 for _, yml := range yamls { 646 if strings.TrimSpace(yml) == "" { 647 continue 648 } 649 650 obj := &unstructured.Unstructured{} 651 652 if err := yaml.Unmarshal([]byte(yml), obj); err != nil { 653 return fmt.Errorf("failed to unmarshal YAML: %w", err) 654 } 655 var ogvr schema.GroupVersionResource 656 if obj.GetKind() == name.MutatingWebhookConfigurationStr { 657 ogvr = gvr.MutatingWebhookConfiguration 658 } else if obj.GetKind() == name.ValidatingWebhookConfigurationStr { 659 ogvr = gvr.ValidatingWebhookConfiguration 660 } 661 662 t := true 663 _, err := kubeClient.Dynamic().Resource(ogvr).Namespace(obj.GetNamespace()).Patch( 664 context.TODO(), obj.GetName(), types.ApplyPatchType, []byte(yml), metav1.PatchOptions{ 665 Force: &t, 666 FieldManager: fieldOwnerOperator, 667 }) 668 if err != nil { 669 return fmt.Errorf("failed to apply YAML: %w", err) 670 } 671 } 672 return nil 673 } 674 675 // operatorManageWebhooks returns .Values.global.operatorManageWebhooks from the Istio Operator. 676 func operatorManageWebhooks(iop *istioV1Alpha1.IstioOperator) bool { 677 if iop.Spec.GetValues() == nil { 678 return false 679 } 680 globalValues := iop.Spec.Values.AsMap()["global"] 681 global, ok := globalValues.(map[string]any) 682 if !ok { 683 return false 684 } 685 omw, ok := global["operatorManageWebhooks"].(bool) 686 if !ok { 687 return false 688 } 689 return omw 690 } 691 692 // validateEnableNamespacesByDefault checks whether there is .Values.sidecarInjectorWebhook.enableNamespacesByDefault set in the Istio Operator. 693 // Should be used in installer when deciding whether to enable an automatic sidecar injection in all namespaces. 694 func validateEnableNamespacesByDefault(iop *istioV1Alpha1.IstioOperator) bool { 695 if iop == nil || iop.Spec == nil || iop.Spec.Values == nil { 696 return false 697 } 698 sidecarValues := iop.Spec.Values.AsMap()["sidecarInjectorWebhook"] 699 sidecarMap, ok := sidecarValues.(map[string]any) 700 if !ok { 701 return false 702 } 703 autoInjectNamespaces, ok := sidecarMap["enableNamespacesByDefault"].(bool) 704 if !ok { 705 return false 706 } 707 708 return autoInjectNamespaces 709 }