istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/kube/controller/multicluster.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 controller
    16  
    17  import (
    18  	"context"
    19  	"strings"
    20  
    21  	"k8s.io/apimachinery/pkg/api/errors"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	"k8s.io/client-go/kubernetes"
    24  
    25  	"istio.io/api/annotation"
    26  	"istio.io/istio/pilot/pkg/config/kube/crdclient"
    27  	"istio.io/istio/pilot/pkg/features"
    28  	"istio.io/istio/pilot/pkg/keycertbundle"
    29  	"istio.io/istio/pilot/pkg/leaderelection"
    30  	"istio.io/istio/pilot/pkg/model"
    31  	"istio.io/istio/pilot/pkg/server"
    32  	"istio.io/istio/pilot/pkg/serviceregistry/aggregate"
    33  	"istio.io/istio/pilot/pkg/serviceregistry/provider"
    34  	"istio.io/istio/pilot/pkg/serviceregistry/serviceentry"
    35  	"istio.io/istio/pkg/backoff"
    36  	"istio.io/istio/pkg/config/schema/collection"
    37  	"istio.io/istio/pkg/config/schema/collections"
    38  	kubelib "istio.io/istio/pkg/kube"
    39  	"istio.io/istio/pkg/kube/multicluster"
    40  	"istio.io/istio/pkg/webhooks"
    41  )
    42  
    43  const (
    44  	// Name of the webhook config in the config - no need to change it.
    45  	webhookName = "sidecar-injector.istio.io"
    46  )
    47  
    48  type kubeController struct {
    49  	MeshServiceController *aggregate.Controller
    50  	*Controller
    51  	workloadEntryController *serviceentry.Controller
    52  	stop                    chan struct{}
    53  }
    54  
    55  func (k *kubeController) Close() {
    56  	close(k.stop)
    57  	clusterID := k.Controller.clusterID
    58  	k.MeshServiceController.UnRegisterHandlersForCluster(clusterID)
    59  	k.MeshServiceController.DeleteRegistry(clusterID, provider.Kubernetes)
    60  	if k.workloadEntryController != nil {
    61  		k.MeshServiceController.DeleteRegistry(clusterID, provider.External)
    62  	}
    63  	if err := k.Controller.Cleanup(); err != nil {
    64  		log.Warnf("failed cleaning up services in %s: %v", clusterID, err)
    65  	}
    66  	if k.opts.XDSUpdater != nil {
    67  		k.opts.XDSUpdater.ConfigUpdate(&model.PushRequest{Full: true, Reason: model.NewReasonStats(model.ClusterUpdate)})
    68  	}
    69  }
    70  
    71  // Multicluster structure holds the remote kube Controllers and multicluster specific attributes.
    72  type Multicluster struct {
    73  	// serverID of this pilot instance used for leader election
    74  	serverID string
    75  
    76  	// options to use when creating kube controllers
    77  	opts Options
    78  
    79  	// client for reading remote-secrets to initialize multicluster registries
    80  	client kubernetes.Interface
    81  	s      server.Instance
    82  
    83  	serviceEntryController *serviceentry.Controller
    84  	configController       model.ConfigStoreController
    85  	XDSUpdater             model.XDSUpdater
    86  
    87  	clusterLocal model.ClusterLocalProvider
    88  
    89  	startNsController bool
    90  	caBundleWatcher   *keycertbundle.Watcher
    91  	revision          string
    92  
    93  	// secretNamespace where we get cluster-access secrets
    94  	secretNamespace string
    95  	component       *multicluster.Component[*kubeController]
    96  }
    97  
    98  // NewMulticluster initializes data structure to store multicluster information
    99  func NewMulticluster(
   100  	serverID string,
   101  	kc kubernetes.Interface,
   102  	secretNamespace string,
   103  	opts Options,
   104  	serviceEntryController *serviceentry.Controller,
   105  	configController model.ConfigStoreController,
   106  	caBundleWatcher *keycertbundle.Watcher,
   107  	revision string,
   108  	startNsController bool,
   109  	clusterLocal model.ClusterLocalProvider,
   110  	s server.Instance,
   111  	controller *multicluster.Controller,
   112  ) *Multicluster {
   113  	mc := &Multicluster{
   114  		serverID:               serverID,
   115  		opts:                   opts,
   116  		serviceEntryController: serviceEntryController,
   117  		configController:       configController,
   118  		startNsController:      startNsController,
   119  		caBundleWatcher:        caBundleWatcher,
   120  		revision:               revision,
   121  		XDSUpdater:             opts.XDSUpdater,
   122  		clusterLocal:           clusterLocal,
   123  		secretNamespace:        secretNamespace,
   124  		client:                 kc,
   125  		s:                      s,
   126  	}
   127  	mc.component = multicluster.BuildMultiClusterComponent(controller, func(cluster *multicluster.Cluster) *kubeController {
   128  		stop := make(chan struct{})
   129  		client := cluster.Client
   130  		configCluster := opts.ClusterID == cluster.ID
   131  
   132  		options := opts
   133  		options.ClusterID = cluster.ID
   134  		if !configCluster {
   135  			options.SyncTimeout = features.RemoteClusterTimeout
   136  		}
   137  		log.Infof("Initializing Kubernetes service registry %q", options.ClusterID)
   138  		options.ConfigCluster = configCluster
   139  		kubeRegistry := NewController(client, options)
   140  		kubeController := &kubeController{
   141  			MeshServiceController: opts.MeshServiceController,
   142  			Controller:            kubeRegistry,
   143  			stop:                  stop,
   144  		}
   145  		mc.initializeCluster(cluster, kubeController, kubeRegistry, options, configCluster, stop)
   146  		return kubeController
   147  	})
   148  
   149  	return mc
   150  }
   151  
   152  // initializeCluster initializes the cluster by setting various handlers.
   153  func (m *Multicluster) initializeCluster(cluster *multicluster.Cluster, kubeController *kubeController, kubeRegistry *Controller,
   154  	options Options, configCluster bool, clusterStopCh <-chan struct{},
   155  ) {
   156  	client := cluster.Client
   157  
   158  	if m.serviceEntryController != nil && features.EnableServiceEntrySelectPods {
   159  		// Add an instance handler in the kubernetes registry to notify service entry store about pod events
   160  		kubeRegistry.AppendWorkloadHandler(m.serviceEntryController.WorkloadInstanceHandler)
   161  	}
   162  
   163  	if configCluster && m.serviceEntryController != nil && features.EnableEnhancedResourceScoping {
   164  		kubeRegistry.AppendNamespaceDiscoveryHandlers(m.serviceEntryController.NamespaceDiscoveryHandler)
   165  	}
   166  
   167  	// TODO implement deduping in aggregate registry to allow multiple k8s registries to handle WorkloadEntry
   168  	if features.EnableK8SServiceSelectWorkloadEntries {
   169  		if m.serviceEntryController != nil && configCluster {
   170  			// Add an instance handler in the service entry store to notify kubernetes about workload entry events
   171  			m.serviceEntryController.AppendWorkloadHandler(kubeRegistry.WorkloadInstanceHandler)
   172  		} else if features.WorkloadEntryCrossCluster {
   173  			// TODO only do this for non-remotes, can't guarantee CRDs in remotes (depends on https://github.com/istio/istio/pull/29824)
   174  			configStore := createWleConfigStore(client, m.revision, options)
   175  			kubeController.workloadEntryController = serviceentry.NewWorkloadEntryController(
   176  				configStore, options.XDSUpdater,
   177  				serviceentry.WithClusterID(cluster.ID),
   178  				serviceentry.WithNetworkIDCb(kubeRegistry.Network))
   179  			// Services can select WorkloadEntry from the same cluster. We only duplicate the Service to configure kube-dns.
   180  			kubeController.workloadEntryController.AppendWorkloadHandler(kubeRegistry.WorkloadInstanceHandler)
   181  			// ServiceEntry selects WorkloadEntry from remote cluster
   182  			kubeController.workloadEntryController.AppendWorkloadHandler(m.serviceEntryController.WorkloadInstanceHandler)
   183  			if features.EnableEnhancedResourceScoping {
   184  				kubeRegistry.AppendNamespaceDiscoveryHandlers(kubeController.workloadEntryController.NamespaceDiscoveryHandler)
   185  			}
   186  			m.opts.MeshServiceController.AddRegistryAndRun(kubeController.workloadEntryController, clusterStopCh)
   187  			go configStore.Run(clusterStopCh)
   188  		}
   189  	}
   190  
   191  	// run after WorkloadHandler is added
   192  	m.opts.MeshServiceController.AddRegistryAndRun(kubeRegistry, clusterStopCh)
   193  
   194  	go func() {
   195  		var shouldLead bool
   196  		if !configCluster {
   197  			shouldLead = m.checkShouldLead(client, options.SystemNamespace, clusterStopCh)
   198  			log.Infof("should join leader-election for cluster %s: %t", cluster.ID, shouldLead)
   199  		}
   200  		if m.startNsController && (shouldLead || configCluster) {
   201  			// Block server exit on graceful termination of the leader controller.
   202  			m.s.RunComponentAsyncAndWait("namespace controller", func(_ <-chan struct{}) error {
   203  				log.Infof("joining leader-election for %s in %s on cluster %s",
   204  					leaderelection.NamespaceController, options.SystemNamespace, options.ClusterID)
   205  				election := leaderelection.
   206  					NewLeaderElectionMulticluster(options.SystemNamespace, m.serverID, leaderelection.NamespaceController, m.revision, !configCluster, client).
   207  					AddRunFunction(func(leaderStop <-chan struct{}) {
   208  						log.Infof("starting namespace controller for cluster %s", cluster.ID)
   209  						nc := NewNamespaceController(client, m.caBundleWatcher)
   210  						// Start informers again. This fixes the case where informers for namespace do not start,
   211  						// as we create them only after acquiring the leader lock
   212  						// Note: stop here should be the overall pilot stop, NOT the leader election stop. We are
   213  						// basically lazy loading the informer, if we stop it when we lose the lock we will never
   214  						// recreate it again.
   215  						client.RunAndWait(clusterStopCh)
   216  						nc.Run(leaderStop)
   217  					})
   218  				election.Run(clusterStopCh)
   219  				return nil
   220  			})
   221  		}
   222  		// Set up injection webhook patching for remote clusters we are controlling.
   223  		// The config cluster has this patching set up elsewhere. We may eventually want to move it here.
   224  		// We can not use leader election for webhook patching because each revision needs to patch its own
   225  		// webhook.
   226  		if shouldLead && !configCluster && m.caBundleWatcher != nil {
   227  			// Patch injection webhook cert
   228  			// This requires RBAC permissions - a low-priv Istiod should not attempt to patch but rely on
   229  			// operator or CI/CD
   230  			if features.InjectionWebhookConfigName != "" {
   231  				log.Infof("initializing injection webhook cert patcher for cluster %s", cluster.ID)
   232  				patcher, err := webhooks.NewWebhookCertPatcher(client, m.revision, webhookName, m.caBundleWatcher)
   233  				if err != nil {
   234  					log.Errorf("could not initialize webhook cert patcher: %v", err)
   235  				} else {
   236  					go patcher.Run(clusterStopCh)
   237  				}
   238  			}
   239  		}
   240  	}()
   241  
   242  	// setting up the serviceexport controller if and only if it is turned on in the meshconfig.
   243  	if features.EnableMCSAutoExport {
   244  		log.Infof("joining leader-election for %s in %s on cluster %s",
   245  			leaderelection.ServiceExportController, options.SystemNamespace, options.ClusterID)
   246  		// Block server exit on graceful termination of the leader controller.
   247  		m.s.RunComponentAsyncAndWait("auto serviceexport controller", func(_ <-chan struct{}) error {
   248  			leaderelection.
   249  				NewLeaderElectionMulticluster(options.SystemNamespace, m.serverID, leaderelection.ServiceExportController, m.revision, !configCluster, client).
   250  				AddRunFunction(func(leaderStop <-chan struct{}) {
   251  					serviceExportController := newAutoServiceExportController(autoServiceExportOptions{
   252  						Client:       client,
   253  						ClusterID:    options.ClusterID,
   254  						DomainSuffix: options.DomainSuffix,
   255  						ClusterLocal: m.clusterLocal,
   256  					})
   257  					// Start informers again. This fixes the case where informers do not start,
   258  					// as we create them only after acquiring the leader lock
   259  					// Note: stop here should be the overall pilot stop, NOT the leader election stop. We are
   260  					// basically lazy loading the informer, if we stop it when we lose the lock we will never
   261  					// recreate it again.
   262  					client.RunAndWait(clusterStopCh)
   263  					serviceExportController.Run(leaderStop)
   264  				}).Run(clusterStopCh)
   265  			return nil
   266  		})
   267  	}
   268  }
   269  
   270  // checkShouldLead returns true if the caller should attempt leader election for a remote cluster.
   271  func (m *Multicluster) checkShouldLead(client kubelib.Client, systemNamespace string, stop <-chan struct{}) bool {
   272  	var res bool
   273  	if features.ExternalIstiod {
   274  		b := backoff.NewExponentialBackOff(backoff.DefaultOption())
   275  		ctx, cancel := context.WithCancel(context.Background())
   276  		go func() {
   277  			select {
   278  			case <-stop:
   279  				cancel()
   280  			case <-ctx.Done():
   281  			}
   282  		}()
   283  		defer cancel()
   284  		_ = b.RetryWithContext(ctx, func() error {
   285  			namespace, err := client.Kube().CoreV1().Namespaces().Get(context.TODO(), systemNamespace, metav1.GetOptions{})
   286  			if err != nil {
   287  				if errors.IsNotFound(err) {
   288  					return nil
   289  				}
   290  				return err
   291  			}
   292  			// found same system namespace on the remote cluster so check if we are a selected istiod to lead
   293  			istiodCluster, found := namespace.Annotations[annotation.TopologyControlPlaneClusters.Name]
   294  			if found {
   295  				localCluster := string(m.opts.ClusterID)
   296  				for _, cluster := range strings.Split(istiodCluster, ",") {
   297  					if cluster == "*" || cluster == localCluster {
   298  						res = true
   299  						return nil
   300  					}
   301  				}
   302  			}
   303  			return nil
   304  		})
   305  	}
   306  	return res
   307  }
   308  
   309  func createWleConfigStore(client kubelib.Client, revision string, opts Options) model.ConfigStoreController {
   310  	log.Infof("Creating WorkloadEntry only config store for %s", opts.ClusterID)
   311  	workloadEntriesSchemas := collection.NewSchemasBuilder().
   312  		MustAdd(collections.WorkloadEntry).
   313  		Build()
   314  	crdOpts := crdclient.Option{Revision: revision, DomainSuffix: opts.DomainSuffix, Identifier: "mc-workload-entry-controller"}
   315  	return crdclient.NewForSchemas(client, crdOpts, workloadEntriesSchemas)
   316  }