istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/controller/istiocontrolplane/istiocontrolplane_controller.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 istiocontrolplane 16 17 import ( 18 "context" 19 "fmt" 20 "os" 21 "strings" 22 23 "k8s.io/apimachinery/pkg/api/errors" 24 "k8s.io/apimachinery/pkg/api/meta" 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/apimachinery/pkg/runtime/schema" 29 "k8s.io/apimachinery/pkg/types" 30 "k8s.io/client-go/rest" 31 cache2 "sigs.k8s.io/controller-runtime/pkg/cache" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 "sigs.k8s.io/controller-runtime/pkg/controller" 34 "sigs.k8s.io/controller-runtime/pkg/event" 35 "sigs.k8s.io/controller-runtime/pkg/handler" 36 "sigs.k8s.io/controller-runtime/pkg/manager" 37 "sigs.k8s.io/controller-runtime/pkg/predicate" 38 "sigs.k8s.io/controller-runtime/pkg/reconcile" 39 "sigs.k8s.io/controller-runtime/pkg/source" 40 "sigs.k8s.io/yaml" 41 42 "istio.io/api/operator/v1alpha1" 43 revtag "istio.io/istio/istioctl/pkg/tag" 44 "istio.io/istio/operator/pkg/apis/istio" 45 iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" 46 "istio.io/istio/operator/pkg/cache" 47 "istio.io/istio/operator/pkg/helm" 48 "istio.io/istio/operator/pkg/helmreconciler" 49 "istio.io/istio/operator/pkg/metrics" 50 "istio.io/istio/operator/pkg/name" 51 "istio.io/istio/operator/pkg/object" 52 "istio.io/istio/operator/pkg/tpath" 53 "istio.io/istio/operator/pkg/translate" 54 "istio.io/istio/operator/pkg/util" 55 "istio.io/istio/operator/pkg/util/clog" 56 "istio.io/istio/operator/pkg/util/progress" 57 "istio.io/istio/pkg/config/constants" 58 "istio.io/istio/pkg/kube" 59 "istio.io/istio/pkg/log" 60 "istio.io/istio/pkg/util/sets" 61 "istio.io/istio/pkg/version" 62 ) 63 64 const ( 65 finalizer = "istio-finalizer.install.istio.io" 66 // finalizerMaxRetries defines the maximum number of attempts to remove the finalizer. 67 finalizerMaxRetries = 1 68 // IgnoreReconcileAnnotation is annotation of IstioOperator CR so it would be ignored during Reconcile loop. 69 IgnoreReconcileAnnotation = "install.istio.io/ignoreReconcile" 70 ) 71 72 var ( 73 scope = log.RegisterScope("installer", "installer") 74 restConfig *rest.Config 75 ) 76 77 type Options struct { 78 Force bool 79 MaxConcurrentReconciles int 80 } 81 82 // watchedResources contains all resources we will watch and reconcile when changed 83 // Ideally this would also contain Istio CRDs, but there is a race condition here - we cannot watch 84 // a type that does not yet exist. 85 func watchedResources() []schema.GroupVersionKind { 86 res := []schema.GroupVersionKind{ 87 {Group: "apps", Version: "v1", Kind: name.DeploymentStr}, 88 {Group: "apps", Version: "v1", Kind: name.DaemonSetStr}, 89 {Group: "", Version: "v1", Kind: name.ServiceStr}, 90 // Endpoints should not be pruned because these are generated and not in the manifest. 91 // {Group: "", Version: "v1", Kind: name.EndpointStr}, 92 {Group: "", Version: "v1", Kind: name.CMStr}, 93 {Group: "", Version: "v1", Kind: name.PodStr}, 94 {Group: "", Version: "v1", Kind: name.SecretStr}, 95 {Group: "", Version: "v1", Kind: name.SAStr}, 96 {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.RoleBindingStr}, 97 {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.RoleStr}, 98 {Group: "admissionregistration.k8s.io", Version: "v1", Kind: name.MutatingWebhookConfigurationStr}, 99 {Group: "admissionregistration.k8s.io", Version: "v1", Kind: name.ValidatingWebhookConfigurationStr}, 100 {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.ClusterRoleStr}, 101 {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: name.ClusterRoleBindingStr}, 102 {Group: "apiextensions.k8s.io", Version: "v1", Kind: name.CRDStr}, 103 {Group: "policy", Version: "v1", Kind: name.PDBStr}, 104 {Group: "autoscaling", Version: "v2", Kind: name.HPAStr}, 105 } 106 return res 107 } 108 109 var ( 110 ownedResourcePredicates = predicate.TypedFuncs[*unstructured.Unstructured]{ 111 CreateFunc: func(_ event.TypedCreateEvent[*unstructured.Unstructured]) bool { 112 // no action 113 return false 114 }, 115 GenericFunc: func(_ event.TypedGenericEvent[*unstructured.Unstructured]) bool { 116 // no action 117 return false 118 }, 119 DeleteFunc: func(e event.TypedDeleteEvent[*unstructured.Unstructured]) bool { 120 obj, err := meta.Accessor(e.Object) 121 if err != nil { 122 return false 123 } 124 scope.Debugf("got delete event for %s.%s", obj.GetName(), obj.GetNamespace()) 125 unsObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(e.Object) 126 if err != nil { 127 return false 128 } 129 if isOperatorCreatedResource(obj) { 130 crName := obj.GetLabels()[helmreconciler.OwningResourceName] 131 crNamespace := obj.GetLabels()[helmreconciler.OwningResourceNamespace] 132 componentName := obj.GetLabels()[helmreconciler.IstioComponentLabelStr] 133 var host string 134 if restConfig != nil { 135 host = restConfig.Host 136 } 137 crHash := strings.Join([]string{crName, crNamespace, componentName, host}, "-") 138 oh := object.NewK8sObject(&unstructured.Unstructured{Object: unsObj}, nil, nil).Hash() 139 cache.RemoveObject(crHash, oh) 140 return true 141 } 142 return false 143 }, 144 UpdateFunc: func(e event.TypedUpdateEvent[*unstructured.Unstructured]) bool { 145 // no action 146 return false 147 }, 148 } 149 150 operatorPredicates = predicate.TypedFuncs[*iopv1alpha1.IstioOperator]{ 151 CreateFunc: func(e event.TypedCreateEvent[*iopv1alpha1.IstioOperator]) bool { 152 metrics.IncrementReconcileRequest("create") 153 return true 154 }, 155 DeleteFunc: func(e event.TypedDeleteEvent[*iopv1alpha1.IstioOperator]) bool { 156 metrics.IncrementReconcileRequest("delete") 157 return true 158 }, 159 UpdateFunc: func(e event.TypedUpdateEvent[*iopv1alpha1.IstioOperator]) bool { 160 oldIOP := e.ObjectOld 161 newIOP := e.ObjectNew 162 163 // If revision is updated in the IstioOperator resource, we must remove entries 164 // from the cache. If the IstioOperator resource is reverted back to match this operator's 165 // revision, a clean cache would ensure that the operator Reconcile the IstioOperator, 166 // and not skip it. 167 if oldIOP.Spec.Revision != newIOP.Spec.Revision { 168 var host string 169 if restConfig != nil { 170 host = restConfig.Host 171 } 172 for _, component := range name.AllComponentNames { 173 crHash := strings.Join([]string{newIOP.Name, newIOP.Namespace, string(component), host}, "-") 174 cache.RemoveCache(crHash) 175 } 176 } 177 178 if oldIOP.GetDeletionTimestamp() != newIOP.GetDeletionTimestamp() { 179 metrics.IncrementReconcileRequest("update_deletion_timestamp") 180 return true 181 } 182 183 if oldIOP.GetGeneration() != newIOP.GetGeneration() { 184 metrics.IncrementReconcileRequest("update_generation") 185 return true 186 } 187 188 // if generation unchanged, spec also unchanged 189 return false 190 }, 191 } 192 ) 193 194 // ReconcileIstioOperator reconciles a IstioOperator object 195 type ReconcileIstioOperator struct { 196 // This client, initialized using mgr.Client() above, is a split client 197 // that reads objects from the cache and writes to the apiserver 198 client client.Client 199 kubeClient kube.Client 200 scheme *runtime.Scheme 201 options *Options 202 } 203 204 // Reconcile reads that state of the cluster for a IstioOperator object and makes changes based on the state read 205 // and what is in the IstioOperator.Spec 206 // Note: 207 // The Controller will requeue the Request to be processed again if the returned error is non-nil or 208 // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. 209 func (r *ReconcileIstioOperator) Reconcile(_ context.Context, request reconcile.Request) (reconcile.Result, error) { 210 scope.Info("Reconciling IstioOperator") 211 212 ns, iopName := request.Namespace, request.Name 213 reqNamespacedName := types.NamespacedName{ 214 Name: request.Name, 215 Namespace: ns, 216 } 217 // declare read-only iop instance to create the reconciler 218 iop := &iopv1alpha1.IstioOperator{} 219 if err := r.client.Get(context.TODO(), reqNamespacedName, iop); err != nil { 220 if errors.IsNotFound(err) { 221 // Request object not found, could have been deleted after reconcile request. 222 // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 223 // Return and don't requeue 224 metrics.CRDeletionTotal.Increment() 225 return reconcile.Result{}, nil 226 } 227 // Error reading the object - requeue the request. 228 operatorFailedToGetObjectFromAPIServer.Log(scope).Warnf("error getting IstioOperator %s: %s", iopName, err) 229 metrics.CountCRFetchFail(errors.ReasonForError(err)) 230 return reconcile.Result{}, err 231 } 232 if iop.Spec == nil { 233 iop.Spec = &v1alpha1.IstioOperatorSpec{Profile: name.DefaultProfileName} 234 } 235 operatorRevision, _ := os.LookupEnv("REVISION") 236 if operatorRevision != "" && operatorRevision != iop.Spec.Revision { 237 scope.Infof("Ignoring IstioOperator CR %s with revision %s, since operator revision is %s.", iopName, iop.Spec.Revision, operatorRevision) 238 return reconcile.Result{}, nil 239 } 240 if iop.Annotations != nil { 241 if ir := iop.Annotations[IgnoreReconcileAnnotation]; ir == "true" { 242 scope.Infof("Ignoring the IstioOperator CR %s because it is annotated to be ignored for reconcile ", iopName) 243 return reconcile.Result{}, nil 244 } 245 } 246 if iop.Spec.Profile == "ambient" { 247 scope.Infof("Ignoring the IstioOperator CR %s because it is using the unsupported profile 'ambient'.", iopName) 248 return reconcile.Result{}, nil 249 } 250 251 var err error 252 iopMerged := &iopv1alpha1.IstioOperator{} 253 *iopMerged = *iop 254 // get the merged values in iop on top of the defaults for the profile given by iop.profile 255 iopMerged.Spec, err = mergeIOPSWithProfile(iopMerged) 256 if err != nil { 257 operatorFailedToMergeUserIOP.Log(scope).Errorf("failed to merge base profile with user IstioOperator CR %s, %s", iopName, err) 258 return reconcile.Result{}, err 259 } 260 261 deleted := iop.GetDeletionTimestamp() != nil 262 finalizers := sets.New(iop.GetFinalizers()...) 263 if deleted { 264 if !finalizers.Contains(finalizer) { 265 scope.Infof("IstioOperator %s deleted", iopName) 266 metrics.CRDeletionTotal.Increment() 267 return reconcile.Result{}, nil 268 } 269 scope.Infof("Deleting IstioOperator %s", iopName) 270 271 reconciler, err := helmreconciler.NewHelmReconciler(r.client, r.kubeClient, iopMerged, nil) 272 if err != nil { 273 return reconcile.Result{}, err 274 } 275 if err := reconciler.Delete(); err != nil { 276 scope.Errorf("Failed to delete resources with helm reconciler: %s.", err) 277 return reconcile.Result{}, err 278 } 279 280 finalizers.Delete(finalizer) 281 iop.SetFinalizers(sets.SortedList(finalizers)) 282 finalizerError := r.client.Update(context.TODO(), iop) 283 for retryCount := 0; errors.IsConflict(finalizerError) && retryCount < finalizerMaxRetries; retryCount++ { 284 scope.Info("API server conflict during finalizer removal, retrying.") 285 _ = r.client.Get(context.TODO(), request.NamespacedName, iop) 286 finalizers = sets.New(iop.GetFinalizers()...) 287 finalizers.Delete(finalizer) 288 iop.SetFinalizers(sets.SortedList(finalizers)) 289 finalizerError = r.client.Update(context.TODO(), iop) 290 } 291 if finalizerError != nil { 292 if errors.IsNotFound(finalizerError) { 293 scope.Infof("Did not remove finalizer from %s: the object was previously deleted.", iopName) 294 metrics.CRDeletionTotal.Increment() 295 return reconcile.Result{}, nil 296 } else if errors.IsConflict(finalizerError) { 297 scope.Infof("Could not remove finalizer from %s due to conflict. Operation will be retried in next reconcile attempt.", iopName) 298 return reconcile.Result{}, nil 299 } 300 operatorFailedToRemoveFinalizer.Log(scope).Errorf("error removing finalizer: %s", finalizerError) 301 return reconcile.Result{}, finalizerError 302 } 303 return reconcile.Result{}, nil 304 } else if !finalizers.Contains(finalizer) { 305 log.Infof("Adding finalizer %v to %v", finalizer, request) 306 finalizers.Insert(finalizer) 307 iop.SetFinalizers(sets.SortedList(finalizers)) 308 err := r.client.Update(context.TODO(), iop) 309 if err != nil { 310 if errors.IsNotFound(err) { 311 scope.Infof("Could not add finalizer to %s: the object was deleted.", iopName) 312 metrics.CRDeletionTotal.Increment() 313 return reconcile.Result{}, nil 314 } else if errors.IsConflict(err) { 315 scope.Infof("Could not add finalizer to %s due to conflict. Operation will be retried in next reconcile attempt.", iopName) 316 } 317 operatorFailedToAddFinalizer.Log(scope).Errorf("Failed to add finalizer to IstioOperator CR %s: %s", iopName, err) 318 return reconcile.Result{}, err 319 } 320 } 321 322 scope.Info("Updating IstioOperator") 323 val := iopMerged.Spec.Values.AsMap() 324 if _, ok := val["global"]; !ok { 325 val["global"] = make(map[string]any) 326 } 327 err = util.ValidateIOPCAConfig(r.kubeClient, iopMerged) 328 if err != nil { 329 operatorFailedToConfigure.Log(scope).Errorf("failed to apply IstioOperator resources. Error %s", err) 330 return reconcile.Result{}, err 331 } 332 helmReconcilerOptions := &helmreconciler.Options{ 333 Log: clog.NewDefaultLogger(), 334 ProgressLog: progress.NewLog(), 335 } 336 if r.options != nil { 337 helmReconcilerOptions.Force = r.options.Force 338 } 339 exists := revtag.PreviousInstallExists(context.Background(), r.kubeClient.Kube()) 340 reconciler, err := helmreconciler.NewHelmReconciler(r.client, r.kubeClient, iopMerged, helmReconcilerOptions) 341 if err != nil { 342 scope.Errorf("Error during reconcile. Error: %s", err) 343 return reconcile.Result{}, err 344 } 345 if err := reconciler.SetStatusBegin(); err != nil { 346 scope.Errorf("Error during reconcile, failed to update status to Begin. Error: %s", err) 347 return reconcile.Result{}, err 348 } 349 status, err := reconciler.Reconcile() 350 if err != nil { 351 scope.Errorf("Error during reconcile: %s", err) 352 } 353 if err = processDefaultWebhookAfterReconcile(iopMerged, r.kubeClient, exists); err != nil { 354 scope.Errorf("Error during reconcile: %s", err) 355 return reconcile.Result{}, err 356 } 357 358 if err := reconciler.SetStatusComplete(status); err != nil { 359 scope.Errorf("Error during reconcile, failed to update status to Complete. Error: %s", err) 360 return reconcile.Result{}, err 361 } 362 363 return reconcile.Result{}, err 364 } 365 366 func processDefaultWebhookAfterReconcile(iop *iopv1alpha1.IstioOperator, client kube.Client, exists bool) error { 367 var ns string 368 if configuredNamespace := iopv1alpha1.Namespace(iop.Spec); configuredNamespace != "" { 369 ns = configuredNamespace 370 } else { 371 ns = constants.IstioSystemNamespace 372 } 373 opts := &helmreconciler.ProcessDefaultWebhookOptions{ 374 Namespace: ns, 375 DryRun: false, 376 } 377 if _, err := helmreconciler.ProcessDefaultWebhook(client, iop, exists, opts); err != nil { 378 return fmt.Errorf("failed to process default webhook: %v", err) 379 } 380 return nil 381 } 382 383 // mergeIOPSWithProfile overlays the values in iop on top of the defaults for the profile given by iop.profile and 384 // returns the merged result. 385 func mergeIOPSWithProfile(iop *iopv1alpha1.IstioOperator) (*v1alpha1.IstioOperatorSpec, error) { 386 profileYAML, err := helm.GetProfileYAML(iop.Spec.InstallPackagePath, iop.Spec.Profile) 387 if err != nil { 388 metrics.CountCRMergeFail(metrics.CannotFetchProfileError) 389 return nil, err 390 } 391 392 // Due to the fact that base profile is compiled in before a tag can be created, we must allow an additional 393 // override from variables that are set during release build time. 394 hub := version.DockerInfo.Hub 395 tag := version.DockerInfo.Tag 396 if hub != "" && hub != "unknown" && tag != "" && tag != "unknown" { 397 buildHubTagOverlayYAML, err := helm.GenerateHubTagOverlay(hub, tag) 398 if err != nil { 399 metrics.CountCRMergeFail(metrics.OverlayError) 400 return nil, err 401 } 402 profileYAML, err = util.OverlayYAML(profileYAML, buildHubTagOverlayYAML) 403 if err != nil { 404 metrics.CountCRMergeFail(metrics.OverlayError) 405 return nil, err 406 } 407 } 408 409 overlayYAMLB, err := yaml.Marshal(iop) 410 if err != nil { 411 metrics.CountCRMergeFail(metrics.IOPFormatError) 412 return nil, err 413 } 414 overlayYAML := string(overlayYAMLB) 415 t := translate.NewReverseTranslator() 416 overlayYAML, err = t.TranslateK8SfromValueToIOP(overlayYAML) 417 if err != nil { 418 metrics.CountCRMergeFail(metrics.TranslateValuesError) 419 return nil, fmt.Errorf("could not overlay k8s settings from values to IOP: %s", err) 420 } 421 422 mergedYAML, err := util.OverlayIOP(profileYAML, overlayYAML) 423 if err != nil { 424 metrics.CountCRMergeFail(metrics.OverlayError) 425 return nil, err 426 } 427 428 mergedYAML, err = translate.OverlayValuesEnablement(mergedYAML, overlayYAML, "") 429 if err != nil { 430 metrics.CountCRMergeFail(metrics.TranslateValuesError) 431 return nil, err 432 } 433 434 mergedYAMLSpec, err := tpath.GetSpecSubtree(mergedYAML) 435 if err != nil { 436 metrics.CountCRMergeFail(metrics.InternalYAMLParseError) 437 return nil, err 438 } 439 440 return istio.UnmarshalAndValidateIOPS(mergedYAMLSpec) 441 } 442 443 // Add creates a new IstioOperator Controller and adds it to the Manager. The Manager will set fields on the Controller 444 // and Start it when the Manager is Started. It also provides additional options to modify internal reconciler behavior. 445 func Add(mgr manager.Manager, options *Options) error { 446 restConfig = mgr.GetConfig() 447 kubeClient, err := kube.NewClient(kube.NewClientConfigForRestConfig(restConfig), "") 448 if err != nil { 449 return fmt.Errorf("create Kubernetes client: %v", err) 450 } 451 return add(mgr, &ReconcileIstioOperator{client: mgr.GetClient(), scheme: mgr.GetScheme(), kubeClient: kubeClient, options: options}, options) 452 } 453 454 // add adds a new Controller to mgr with r as the reconcile.Reconciler along with options for additional configuration. 455 func add(mgr manager.Manager, r *ReconcileIstioOperator, options *Options) error { 456 scope.Info("Adding controller for IstioOperator.") 457 // Create a new controller 458 opts := controller.Options{Reconciler: r, MaxConcurrentReconciles: options.MaxConcurrentReconciles} 459 c, err := controller.New("istiocontrolplane-controller", mgr, opts) 460 if err != nil { 461 return err 462 } 463 464 // Watch for changes to primary resource IstioOperator 465 err = c.Watch(source.Kind( 466 mgr.GetCache(), 467 &iopv1alpha1.IstioOperator{}, 468 &handler.TypedEnqueueRequestForObject[*iopv1alpha1.IstioOperator]{}, 469 operatorPredicates, 470 )) 471 if err != nil { 472 return err 473 } 474 // watch for changes to Istio resources 475 err = watchIstioResources(mgr.GetCache(), c) 476 if err != nil { 477 return err 478 } 479 scope.Info("Controller added") 480 return nil 481 } 482 483 // Watch changes for Istio resources managed by the operator 484 func watchIstioResources(mgrCache cache2.Cache, c controller.Controller) error { 485 for _, t := range watchedResources() { 486 u := &unstructured.Unstructured{} 487 u.SetGroupVersionKind(schema.GroupVersionKind{ 488 Kind: t.Kind, 489 Group: t.Group, 490 Version: t.Version, 491 }) 492 handlerFunc := handler.TypedEnqueueRequestsFromMapFunc[*unstructured.Unstructured](func(_ context.Context, a *unstructured.Unstructured) []reconcile.Request { 493 scope.Infof("Watching a change for istio resource: %s/%s", a.GetNamespace(), a.GetName()) 494 return []reconcile.Request{ 495 {NamespacedName: types.NamespacedName{ 496 Name: a.GetLabels()[helmreconciler.OwningResourceName], 497 Namespace: a.GetLabels()[helmreconciler.OwningResourceNamespace], 498 }}, 499 } 500 }) 501 err := c.Watch(source.Kind(mgrCache, u, handlerFunc, ownedResourcePredicates)) 502 if err != nil { 503 scope.Errorf("Could not create watch for %s/%s/%s: %s.", t.Kind, t.Group, t.Version, err) 504 } 505 } 506 return nil 507 } 508 509 // Check if the specified object is created by operator 510 func isOperatorCreatedResource(obj metav1.Object) bool { 511 return obj.GetLabels()[helmreconciler.OwningResourceName] != "" && 512 obj.GetLabels()[helmreconciler.OwningResourceNamespace] != "" && 513 obj.GetLabels()[helmreconciler.IstioComponentLabelStr] != "" 514 }