github.com/cilium/cilium@v1.16.2/pkg/clustermesh/remote_cluster.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package clustermesh
     5  
     6  import (
     7  	"context"
     8  	"path"
     9  
    10  	"github.com/sirupsen/logrus"
    11  
    12  	"github.com/cilium/cilium/api/v1/models"
    13  	"github.com/cilium/cilium/pkg/allocator"
    14  	"github.com/cilium/cilium/pkg/clustermesh/common"
    15  	cmtypes "github.com/cilium/cilium/pkg/clustermesh/types"
    16  	"github.com/cilium/cilium/pkg/clustermesh/wait"
    17  	identityCache "github.com/cilium/cilium/pkg/identity/cache"
    18  	"github.com/cilium/cilium/pkg/ipcache"
    19  	"github.com/cilium/cilium/pkg/kvstore"
    20  	"github.com/cilium/cilium/pkg/kvstore/store"
    21  	"github.com/cilium/cilium/pkg/lock"
    22  	"github.com/cilium/cilium/pkg/logging/logfields"
    23  	nodeStore "github.com/cilium/cilium/pkg/node/store"
    24  	serviceStore "github.com/cilium/cilium/pkg/service/store"
    25  )
    26  
    27  // remoteCluster implements the clustermesh business logic on top of
    28  // common.RemoteCluster.
    29  type remoteCluster struct {
    30  	// name is the name of the cluster
    31  	name string
    32  
    33  	// clusterID is the clusterID advertized by the remote cluster
    34  	clusterID uint32
    35  
    36  	// clusterConfigValidator validates the cluster configuration advertised
    37  	// by remote clusters.
    38  	clusterConfigValidator func(cmtypes.CiliumClusterConfig) error
    39  
    40  	usedIDs ClusterIDsManager
    41  
    42  	// mutex protects the following variables:
    43  	// - remoteIdentityCache
    44  	mutex lock.RWMutex
    45  
    46  	// store is the shared store representing all nodes in the remote cluster
    47  	remoteNodes store.WatchStore
    48  
    49  	// remoteServices is the shared store representing services in remote
    50  	// clusters
    51  	remoteServices store.WatchStore
    52  
    53  	// ipCacheWatcher is the watcher that notifies about IP<->identity
    54  	// changes in the remote cluster
    55  	ipCacheWatcher *ipcache.IPIdentityWatcher
    56  
    57  	// ipCacheWatcherExtraOpts returns extra options for watching ipcache entries.
    58  	ipCacheWatcherExtraOpts IPCacheWatcherOptsFn
    59  
    60  	// remoteIdentityWatcher allows watching remote identities.
    61  	remoteIdentityWatcher RemoteIdentityWatcher
    62  
    63  	// remoteIdentityCache is a locally cached copy of the identity
    64  	// allocations in the remote cluster
    65  	remoteIdentityCache *allocator.RemoteCache
    66  
    67  	// status is the function which fills the common part of the status.
    68  	status common.StatusFunc
    69  
    70  	storeFactory store.Factory
    71  
    72  	// synced tracks the initial synchronization with the remote cluster.
    73  	synced synced
    74  
    75  	log logrus.FieldLogger
    76  }
    77  
    78  func (rc *remoteCluster) Run(ctx context.Context, backend kvstore.BackendOperations, config cmtypes.CiliumClusterConfig, ready chan<- error) {
    79  	if err := rc.clusterConfigValidator(config); err != nil {
    80  		ready <- err
    81  		close(ready)
    82  		return
    83  	}
    84  
    85  	if err := rc.onUpdateConfig(config); err != nil {
    86  		ready <- err
    87  		close(ready)
    88  		return
    89  	}
    90  
    91  	remoteIdentityCache, err := rc.remoteIdentityWatcher.WatchRemoteIdentities(rc.name, rc.clusterID, backend, config.Capabilities.Cached)
    92  	if err != nil {
    93  		ready <- err
    94  		close(ready)
    95  		return
    96  	}
    97  
    98  	rc.mutex.Lock()
    99  	rc.remoteIdentityCache = remoteIdentityCache
   100  	rc.mutex.Unlock()
   101  
   102  	var mgr store.WatchStoreManager
   103  	if config.Capabilities.SyncedCanaries {
   104  		mgr = rc.storeFactory.NewWatchStoreManager(backend, rc.name)
   105  	} else {
   106  		mgr = store.NewWatchStoreManagerImmediate(rc.name)
   107  	}
   108  
   109  	adapter := func(prefix string) string { return prefix }
   110  	if config.Capabilities.Cached {
   111  		adapter = kvstore.StateToCachePrefix
   112  	}
   113  
   114  	mgr.Register(adapter(nodeStore.NodeStorePrefix), func(ctx context.Context) {
   115  		rc.remoteNodes.Watch(ctx, backend, path.Join(adapter(nodeStore.NodeStorePrefix), rc.name))
   116  	})
   117  
   118  	mgr.Register(adapter(serviceStore.ServiceStorePrefix), func(ctx context.Context) {
   119  		rc.remoteServices.Watch(ctx, backend, path.Join(adapter(serviceStore.ServiceStorePrefix), rc.name))
   120  	})
   121  
   122  	mgr.Register(adapter(ipcache.IPIdentitiesPath), func(ctx context.Context) {
   123  		rc.ipCacheWatcher.Watch(ctx, backend, rc.ipCacheWatcherOpts(&config)...)
   124  	})
   125  
   126  	mgr.Register(adapter(identityCache.IdentitiesPath), func(ctx context.Context) {
   127  		rc.remoteIdentityCache.Watch(ctx, func(context.Context) { rc.synced.identities.Done() })
   128  	})
   129  
   130  	close(ready)
   131  	mgr.Run(ctx)
   132  }
   133  
   134  func (rc *remoteCluster) Stop() {
   135  	rc.synced.Stop()
   136  }
   137  
   138  func (rc *remoteCluster) Remove(context.Context) {
   139  	// Draining shall occur only when the configuration for the remote cluster
   140  	// is removed, and not in case the agent is shutting down, otherwise we
   141  	// would break existing connections on restart.
   142  	rc.remoteNodes.Drain()
   143  	rc.remoteServices.Drain()
   144  	rc.ipCacheWatcher.Drain()
   145  
   146  	rc.remoteIdentityWatcher.RemoveRemoteIdentities(rc.name)
   147  
   148  	rc.usedIDs.ReleaseClusterID(rc.clusterID)
   149  }
   150  
   151  func (rc *remoteCluster) Status() *models.RemoteCluster {
   152  	status := rc.status()
   153  
   154  	rc.mutex.RLock()
   155  	defer rc.mutex.RUnlock()
   156  
   157  	status.NumNodes = int64(rc.remoteNodes.NumEntries())
   158  	status.NumSharedServices = int64(rc.remoteServices.NumEntries())
   159  	status.NumIdentities = int64(rc.remoteIdentityCache.NumEntries())
   160  	status.NumEndpoints = int64(rc.ipCacheWatcher.NumEntries())
   161  
   162  	status.Synced = &models.RemoteClusterSynced{
   163  		Nodes:      rc.remoteNodes.Synced(),
   164  		Services:   rc.remoteServices.Synced(),
   165  		Identities: rc.remoteIdentityCache.Synced(),
   166  		Endpoints:  rc.ipCacheWatcher.Synced(),
   167  	}
   168  
   169  	status.Ready = status.Ready &&
   170  		status.Synced.Nodes && status.Synced.Services &&
   171  		status.Synced.Identities && status.Synced.Endpoints
   172  
   173  	return status
   174  }
   175  
   176  func (rc *remoteCluster) onUpdateConfig(newConfig cmtypes.CiliumClusterConfig) error {
   177  	if newConfig.ID == rc.clusterID {
   178  		return nil
   179  	}
   180  
   181  	// Let's fully drain all previously known entries if the remote cluster changed
   182  	// the cluster ID. Although synthetic deletion events would be generated in any
   183  	// case upon initial listing (as the entries with the incorrect ID would not pass
   184  	// validation), that would leave a window of time in which there would still be
   185  	// stale entries for a Cluster ID that has already been released, potentially
   186  	// leading to inconsistencies if the same ID is acquired again in the meanwhile.
   187  	if rc.clusterID != cmtypes.ClusterIDUnset {
   188  		rc.log.WithField(logfields.ClusterID, newConfig.ID).
   189  			Info("Remote Cluster ID changed: draining all known entries before reconnecting. ",
   190  				"Expect connectivity disruption towards this cluster")
   191  		rc.remoteNodes.Drain()
   192  		rc.remoteServices.Drain()
   193  		rc.ipCacheWatcher.Drain()
   194  		rc.remoteIdentityWatcher.RemoveRemoteIdentities(rc.name)
   195  	}
   196  
   197  	if err := rc.usedIDs.ReserveClusterID(newConfig.ID); err != nil {
   198  		return err
   199  	}
   200  
   201  	rc.usedIDs.ReleaseClusterID(rc.clusterID)
   202  	rc.clusterID = newConfig.ID
   203  
   204  	return nil
   205  }
   206  
   207  func (rc *remoteCluster) ipCacheWatcherOpts(config *cmtypes.CiliumClusterConfig) []ipcache.IWOpt {
   208  	var opts []ipcache.IWOpt
   209  
   210  	if config != nil {
   211  		opts = append(opts, ipcache.WithCachedPrefix(config.Capabilities.Cached))
   212  	}
   213  
   214  	if rc.ipCacheWatcherExtraOpts != nil {
   215  		opts = append(opts, rc.ipCacheWatcherExtraOpts(config)...)
   216  	}
   217  
   218  	return opts
   219  }
   220  
   221  type synced struct {
   222  	wait.SyncedCommon
   223  	services   *lock.StoppableWaitGroup
   224  	nodes      chan struct{}
   225  	ipcache    chan struct{}
   226  	identities *lock.StoppableWaitGroup
   227  }
   228  
   229  func newSynced() synced {
   230  	// Use a StoppableWaitGroup for identities, instead of a plain channel to
   231  	// avoid having to deal with the possibility of a closed channel if already
   232  	// synced (as the callback is executed every time the etcd connection
   233  	// is restarted, differently from the other resource types).
   234  	idswg := lock.NewStoppableWaitGroup()
   235  	idswg.Add()
   236  	idswg.Stop()
   237  
   238  	return synced{
   239  		SyncedCommon: wait.NewSyncedCommon(),
   240  		services:     lock.NewStoppableWaitGroup(),
   241  		nodes:        make(chan struct{}),
   242  		ipcache:      make(chan struct{}),
   243  		identities:   idswg,
   244  	}
   245  }
   246  
   247  // Nodes returns after that the initial list of nodes has been received
   248  // from the remote cluster, and synchronized with the different subscribers,
   249  // the remote cluster is disconnected, or the given context is canceled.
   250  func (s *synced) Nodes(ctx context.Context) error {
   251  	return s.Wait(ctx, s.nodes)
   252  }
   253  
   254  // Services returns after that the initial list of shared services has been
   255  // received from the remote cluster, and synchronized with the BPF datapath,
   256  // the remote cluster is disconnected, or the given context is canceled.
   257  func (s *synced) Services(ctx context.Context) error {
   258  	return s.Wait(ctx, s.services.WaitChannel())
   259  }
   260  
   261  // IPIdentities returns after that the initial list of ipcache entries and
   262  // identities has been received from the remote cluster, and synchronized
   263  // with the BPF datapath, the remote cluster is disconnected, or the given
   264  // context is canceled. We additionally need to explicitly wait for nodes
   265  // synchronization because they also trigger the insertion of ipcache entries
   266  // (i.e., node addresses, health, ingress, ...).
   267  func (s *synced) IPIdentities(ctx context.Context) error {
   268  	return s.Wait(ctx, s.ipcache, s.identities.WaitChannel(), s.nodes)
   269  }