mirror of https://github.com/linkerd/linkerd2.git
1977 lines
71 KiB
Go
1977 lines
71 KiB
Go
package servicemirror
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha3"
|
|
"github.com/linkerd/linkerd2/controller/k8s"
|
|
consts "github.com/linkerd/linkerd2/pkg/k8s"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
logging "github.com/sirupsen/logrus"
|
|
corev1 "k8s.io/api/core/v1"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
"k8s.io/client-go/tools/cache"
|
|
"k8s.io/client-go/tools/record"
|
|
"k8s.io/client-go/util/workqueue"
|
|
)
|
|
|
|
const (
|
|
eventTypeSkipped = "ServiceMirroringSkipped"
|
|
|
|
reasonMirrored = "Mirrored"
|
|
reasonInvalidService = "InvalidService"
|
|
reasonError = "Error"
|
|
reasonMissingNamespace = "MissingNamespace"
|
|
)
|
|
|
|
type (
|
|
// RemoteClusterServiceWatcher is a watcher instantiated for every cluster that is being watched
|
|
// Its main job is to listen to events coming from the remote cluster and react accordingly, keeping
|
|
// the state of the mirrored services in sync. This is achieved by maintaining a SharedInformer
|
|
// on the remote cluster. The basic add/update/delete operations are mapped to a more domain specific
|
|
// events, put onto a work queue and handled by the processing loop. In case processing an event fails
|
|
// it can be requeued up to N times, to ensure that the failure is not due to some temporary network
|
|
// problems or general glitch in the Matrix.
|
|
RemoteClusterServiceWatcher struct {
|
|
serviceMirrorNamespace string
|
|
link *v1alpha3.Link
|
|
remoteAPIClient *k8s.API
|
|
localAPIClient *k8s.API
|
|
linksAPIClient *k8s.API
|
|
probeSvc string
|
|
stopper chan struct{}
|
|
eventBroadcaster record.EventBroadcaster
|
|
recorder record.EventRecorder
|
|
log *logging.Entry
|
|
eventsQueue workqueue.TypedRateLimitingInterface[any]
|
|
requeueLimit int
|
|
repairPeriod time.Duration
|
|
gatewayAlive bool
|
|
liveness chan bool
|
|
headlessServicesEnabled bool
|
|
namespaceCreationEnabled bool
|
|
|
|
informerHandlers
|
|
}
|
|
|
|
informerHandlers struct {
|
|
svcHandler cache.ResourceEventHandlerRegistration
|
|
epHandler cache.ResourceEventHandlerRegistration
|
|
nsHandler cache.ResourceEventHandlerRegistration
|
|
}
|
|
|
|
// RemoteServiceExported is generated whenever a remote service is created Observing
|
|
// this event means that the service in question is not mirrored atm
|
|
RemoteServiceExported struct {
|
|
service *corev1.Service
|
|
}
|
|
|
|
// CreateFederatedService is generated whenever a remote service joins a
|
|
// federated service and the local federated service does not exist yet.
|
|
CreateFederatedService struct {
|
|
service *corev1.Service
|
|
}
|
|
|
|
// RemoteExportedServiceUpdated is generated when we see something about an already
|
|
// mirrored service change on the remote cluster. In that case we need to
|
|
// reconcile. Most importantly we need to keep track of exposed ports
|
|
// and gateway association changes.
|
|
RemoteExportedServiceUpdated struct {
|
|
remoteUpdate *corev1.Service
|
|
}
|
|
|
|
// RemoteServiceJoinedFederatedService is generated when a remote server
|
|
// joins a federated service and the local federated service already exists.
|
|
RemoteServiceJoinsFederatedService struct {
|
|
remoteUpdate *corev1.Service
|
|
}
|
|
|
|
// RemoteServiceUnexported when a remote service is going away or it is not
|
|
// considered mirrored anymore
|
|
RemoteServiceUnexported struct {
|
|
Name string
|
|
Namespace string
|
|
}
|
|
|
|
// RemoteServiceLeavesFederatedService when a remote service is going away or
|
|
// it is no longer part of the federated service
|
|
RemoteServiceLeavesFederatedService struct {
|
|
Name string
|
|
Namespace string
|
|
}
|
|
|
|
// ClusterUnregistered is issued when this ClusterWatcher is shut down.
|
|
ClusterUnregistered struct{}
|
|
|
|
// OrphanedServicesGcTriggered is a self-triggered event which aims to delete any
|
|
// orphaned services that are no longer on the remote cluster. It is emitted every
|
|
// time a new remote cluster is registered for monitoring. The need for this arises
|
|
// because the following might happen.
|
|
//
|
|
// 1. A cluster is registered for monitoring
|
|
// 2. Services A,B,C are created and mirrored
|
|
// 3. Then this component crashes, leaving the mirrors around
|
|
// 4. In the meantime services B and C are deleted on the remote cluster
|
|
// 5. When the controller starts up again it registers to listen for mirrored services
|
|
// 6. It receives an ADD for A but not a DELETE for B and C
|
|
//
|
|
// This event indicates that we need to make a diff with all services on the remote
|
|
// cluster, ensuring that we do not keep any mirrors that are not relevant anymore
|
|
OrphanedServicesGcTriggered struct{}
|
|
|
|
// OnAddCalled is issued when the onAdd function of the
|
|
// shared informer is called
|
|
OnAddCalled struct {
|
|
svc *corev1.Service
|
|
}
|
|
|
|
// OnAddEndpointsCalled is issued when the onAdd function of the Endpoints
|
|
// shared informer is called
|
|
OnAddEndpointsCalled struct {
|
|
ep *corev1.Endpoints
|
|
}
|
|
|
|
// OnUpdateCalled is issued when the onUpdate function of the
|
|
// shared informer is called
|
|
OnUpdateCalled struct {
|
|
svc *corev1.Service
|
|
}
|
|
|
|
// OnUpdateEndpointsCalled is issued when the onUpdate function of the
|
|
// shared Endpoints informer is called
|
|
OnUpdateEndpointsCalled struct {
|
|
ep *corev1.Endpoints
|
|
}
|
|
// OnDeleteCalled is issued when the onDelete function of the
|
|
// shared informer is called
|
|
OnDeleteCalled struct {
|
|
svc *corev1.Service
|
|
}
|
|
|
|
// RepairEndpoints is issued when the service mirror and mirror gateway
|
|
// endpoints should be resolved based on the remote gateway and updated.
|
|
RepairEndpoints struct{}
|
|
|
|
// OnLocalNamespaceAdded is issued when when a new namespace is added to
|
|
// the local cluster. This means that we should check the remote cluster
|
|
// for exported service in that namespace.
|
|
OnLocalNamespaceAdded struct {
|
|
ns *corev1.Namespace
|
|
}
|
|
|
|
// RetryableError is an error that should be retried through requeuing events
|
|
RetryableError struct{ Inner []error }
|
|
)
|
|
|
|
func (re RetryableError) Error() string {
|
|
var errorStrings []string
|
|
for _, err := range re.Inner {
|
|
errorStrings = append(errorStrings, err.Error())
|
|
}
|
|
return fmt.Sprintf("Inner errors:\n\t%s", strings.Join(errorStrings, "\n\t"))
|
|
}
|
|
|
|
// NewRemoteClusterServiceWatcher constructs a new cluster watcher
|
|
func NewRemoteClusterServiceWatcher(
|
|
ctx context.Context,
|
|
serviceMirrorNamespace string,
|
|
localAPI *k8s.API,
|
|
remoteAPI *k8s.API,
|
|
linksAPI *k8s.API,
|
|
probeSvc string,
|
|
link *v1alpha3.Link,
|
|
requeueLimit int,
|
|
repairPeriod time.Duration,
|
|
liveness chan bool,
|
|
enableHeadlessSvc bool,
|
|
enableNamespaceCreation bool,
|
|
) (*RemoteClusterServiceWatcher, error) {
|
|
_, err := remoteAPI.Client.Discovery().ServerVersion()
|
|
if err != nil {
|
|
remoteAPI.UnregisterGauges()
|
|
return nil, fmt.Errorf("cannot connect to api for target cluster %s: %w", link.Spec.TargetClusterName, err)
|
|
}
|
|
|
|
// Create k8s event recorder
|
|
eventBroadcaster := record.NewBroadcaster()
|
|
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{
|
|
Interface: remoteAPI.Client.CoreV1().Events(""),
|
|
})
|
|
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{
|
|
Component: fmt.Sprintf("linkerd-service-mirror-%s", link.Spec.TargetClusterName),
|
|
})
|
|
|
|
stopper := make(chan struct{})
|
|
return &RemoteClusterServiceWatcher{
|
|
serviceMirrorNamespace: serviceMirrorNamespace,
|
|
link: link,
|
|
remoteAPIClient: remoteAPI,
|
|
localAPIClient: localAPI,
|
|
linksAPIClient: linksAPI,
|
|
probeSvc: probeSvc,
|
|
stopper: stopper,
|
|
eventBroadcaster: eventBroadcaster,
|
|
recorder: recorder,
|
|
log: logging.WithFields(logging.Fields{
|
|
"cluster": link.Spec.TargetClusterName,
|
|
}),
|
|
eventsQueue: workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any]()),
|
|
requeueLimit: requeueLimit,
|
|
repairPeriod: repairPeriod,
|
|
liveness: liveness,
|
|
headlessServicesEnabled: enableHeadlessSvc,
|
|
namespaceCreationEnabled: enableNamespaceCreation,
|
|
// always instantiate the gatewayAlive=true to prevent unexpected service fail fast
|
|
gatewayAlive: true,
|
|
}, nil
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) mirrorServiceName(remoteName string) string {
|
|
return fmt.Sprintf("%s-%s", remoteName, rcsw.link.Spec.TargetClusterName)
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) federatedServiceName(remoteName string) string {
|
|
return fmt.Sprintf("%s-federated", remoteName)
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) targetResourceName(mirrorName string) string {
|
|
return strings.TrimSuffix(mirrorName, "-"+rcsw.link.Spec.TargetClusterName)
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) originalResourceName(mirroredName string) string {
|
|
return strings.TrimSuffix(mirroredName, fmt.Sprintf("-%s", rcsw.link.Spec.TargetClusterName))
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) isLabelExlucded(label string) bool {
|
|
if strings.HasPrefix(label, consts.SvcMirrorPrefix) {
|
|
return true
|
|
}
|
|
for _, excludedLabel := range rcsw.link.Spec.ExcludedLabels {
|
|
if strings.HasSuffix(excludedLabel, "/*") {
|
|
trimmed := strings.TrimSuffix(excludedLabel, "/*")
|
|
if strings.HasPrefix(label, trimmed) {
|
|
return true
|
|
}
|
|
} else if label == excludedLabel {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Provides labels for mirrored or federatedservice.
|
|
// Copies all labels from the remote service to local service (except labels
|
|
// with the "SvcMirrorPrefix").
|
|
func (rcsw *RemoteClusterServiceWatcher) getCommonServiceLabels(remoteService *corev1.Service) map[string]string {
|
|
labels := map[string]string{
|
|
consts.MirroredResourceLabel: "true",
|
|
}
|
|
|
|
for key, value := range remoteService.ObjectMeta.Labels {
|
|
if rcsw.isLabelExlucded(key) {
|
|
continue
|
|
}
|
|
labels[key] = value
|
|
}
|
|
|
|
return labels
|
|
}
|
|
|
|
// Provides labels for mirror service.
|
|
// Copies all labels from the remote service to mirrored service (except labels
|
|
// with the "SvcMirrorPrefix").
|
|
func (rcsw *RemoteClusterServiceWatcher) getMirrorServiceLabels(remoteService *corev1.Service) map[string]string {
|
|
labels := rcsw.getCommonServiceLabels(remoteService)
|
|
labels[consts.RemoteClusterNameLabel] = rcsw.link.Spec.TargetClusterName
|
|
|
|
if rcsw.isRemoteDiscovery(remoteService.Labels) {
|
|
labels[consts.RemoteDiscoveryLabel] = rcsw.link.Spec.TargetClusterName
|
|
labels[consts.RemoteServiceLabel] = remoteService.GetName()
|
|
}
|
|
|
|
return labels
|
|
}
|
|
|
|
// Provides labels for mirror endpoint. Copies all labels from the exported
|
|
// service to the mirror endpoint (except labels with the "SvcMirrorPrefix").
|
|
func (rcsw *RemoteClusterServiceWatcher) getMirrorEndpointLabels(exportedService *corev1.Service) map[string]string {
|
|
labels := rcsw.getCommonServiceLabels(exportedService)
|
|
|
|
labels[consts.RemoteClusterNameLabel] = rcsw.link.Spec.TargetClusterName
|
|
|
|
return labels
|
|
}
|
|
|
|
// Provides labels for federated services. Copies all labels from the remote
|
|
// service to the federated service (except labels with the "SvcMirrorPrefix").
|
|
func (rcsw *RemoteClusterServiceWatcher) getFederatedServiceLabels(remoteService *corev1.Service) map[string]string {
|
|
labels := rcsw.getCommonServiceLabels(remoteService)
|
|
|
|
return labels
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) isAnnotationExlucded(annotation string) bool {
|
|
// Topology aware hints are not multicluster aware.
|
|
if annotation == "service.kubernetes.io/topology-aware-hints" || annotation == "service.kubernetes.io/topology-mode" {
|
|
return true
|
|
}
|
|
for _, excludedAnnotation := range rcsw.link.Spec.ExcludedAnnotations {
|
|
if strings.HasSuffix(excludedAnnotation, "/*") {
|
|
trimmed := strings.TrimSuffix(excludedAnnotation, "/*")
|
|
if strings.HasPrefix(annotation, trimmed) {
|
|
return true
|
|
}
|
|
} else if annotation == excludedAnnotation {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Provides annotations for mirror or federated services
|
|
func (rcsw *RemoteClusterServiceWatcher) getCommonServiceAnnotations(remoteService *corev1.Service) map[string]string {
|
|
annotations := map[string]string{}
|
|
|
|
for key, value := range remoteService.ObjectMeta.Annotations {
|
|
if rcsw.isAnnotationExlucded(key) {
|
|
continue
|
|
}
|
|
annotations[key] = value
|
|
}
|
|
|
|
value, ok := remoteService.GetAnnotations()[consts.ProxyOpaquePortsAnnotation]
|
|
if ok {
|
|
annotations[consts.ProxyOpaquePortsAnnotation] = value
|
|
}
|
|
|
|
return annotations
|
|
}
|
|
|
|
// Provides annotations for mirror services
|
|
func (rcsw *RemoteClusterServiceWatcher) getMirrorServiceAnnotations(remoteService *corev1.Service) map[string]string {
|
|
annotations := rcsw.getCommonServiceAnnotations(remoteService)
|
|
|
|
annotations[consts.RemoteServiceFqName] = fmt.Sprintf("%s.%s.svc.%s", remoteService.Name, remoteService.Namespace, rcsw.link.Spec.TargetClusterDomain)
|
|
annotations[consts.RemoteResourceVersionAnnotation] = remoteService.ResourceVersion // needed to detect real changes
|
|
|
|
return annotations
|
|
}
|
|
|
|
// Provides annotations for federated service
|
|
func (rcsw *RemoteClusterServiceWatcher) getFederatedServiceAnnotations(remoteService *corev1.Service) map[string]string {
|
|
annotations := rcsw.getCommonServiceAnnotations(remoteService)
|
|
|
|
if rcsw.link.Spec.TargetClusterName == "" {
|
|
// Local discovery
|
|
annotations[consts.LocalDiscoveryAnnotation] = remoteService.Name
|
|
} else {
|
|
// Remote discovery
|
|
annotations[consts.RemoteDiscoveryAnnotation] = fmt.Sprintf("%s@%s", remoteService.Name, rcsw.link.Spec.TargetClusterName)
|
|
}
|
|
|
|
return annotations
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) mirrorNamespaceIfNecessary(ctx context.Context, namespace string) error {
|
|
// if the namespace is already present we do not need to change it.
|
|
// if we are creating it we want to put a label indicating this is a
|
|
// mirrored resource
|
|
if _, err := rcsw.localAPIClient.NS().Lister().Get(namespace); err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
// if the namespace is not found, we can just create it
|
|
ns := &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Labels: map[string]string{
|
|
consts.MirroredResourceLabel: "true",
|
|
consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName,
|
|
},
|
|
Name: namespace,
|
|
},
|
|
}
|
|
_, err := rcsw.localAPIClient.Client.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
|
|
if err != nil {
|
|
// something went wrong with the create, we can just retry as well
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
} else {
|
|
// something else went wrong, so we can just retry
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// This method takes care of port remapping. What it does essentially is get the one gateway port
|
|
// that we should send traffic to and create endpoint ports that bind to the mirrored service ports
|
|
// (same name, etc) but send traffic to the gateway port. This way we do not need to do any remapping
|
|
// on the service side of things. It all happens in the endpoints.
|
|
func (rcsw *RemoteClusterServiceWatcher) getEndpointsPorts(service *corev1.Service) ([]corev1.EndpointPort, error) {
|
|
gatewayPort, err := strconv.ParseInt(rcsw.link.Spec.GatewayPort, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var endpointsPorts []corev1.EndpointPort
|
|
for _, remotePort := range service.Spec.Ports {
|
|
endpointsPorts = append(endpointsPorts, corev1.EndpointPort{
|
|
Name: remotePort.Name,
|
|
Protocol: remotePort.Protocol,
|
|
Port: int32(gatewayPort),
|
|
})
|
|
}
|
|
return endpointsPorts, nil
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) cleanupOrphanedServices(ctx context.Context) error {
|
|
matchLabels := map[string]string{
|
|
consts.MirroredResourceLabel: "true",
|
|
consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName,
|
|
}
|
|
|
|
servicesOnLocalCluster, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector())
|
|
if err != nil {
|
|
innerErr := fmt.Errorf("failed to list services while cleaning up mirror services: %w", err)
|
|
if kerrors.IsNotFound(err) {
|
|
return innerErr
|
|
}
|
|
// if it is something else, we can just retry
|
|
return RetryableError{[]error{innerErr}}
|
|
}
|
|
|
|
var errors []error
|
|
for _, srv := range servicesOnLocalCluster {
|
|
mirroredName := srv.Name
|
|
// For headless services with cluster IPs representing the backing pods, the mirrored service name
|
|
// is the root headless service in the source cluster
|
|
if remoteHeadlessSvcName, headlessMirror := srv.Labels[consts.MirroredHeadlessSvcNameLabel]; headlessMirror {
|
|
mirroredName = remoteHeadlessSvcName
|
|
}
|
|
remoteServiceName := rcsw.originalResourceName(mirroredName)
|
|
_, err := rcsw.remoteAPIClient.Svc().Lister().Services(srv.Namespace).Get(remoteServiceName)
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
// service does not exist anymore. Need to delete
|
|
if err := rcsw.localAPIClient.Client.CoreV1().Services(srv.Namespace).Delete(ctx, srv.Name, metav1.DeleteOptions{}); err != nil {
|
|
// something went wrong with deletion, we need to retry
|
|
errors = append(errors, err)
|
|
} else {
|
|
rcsw.log.Infof("Deleted service %s/%s while cleaning up mirror services", srv.Namespace, srv.Name)
|
|
}
|
|
} else {
|
|
// something went wrong getting the service, we can retry
|
|
errors = append(errors, err)
|
|
}
|
|
}
|
|
}
|
|
if len(errors) > 0 {
|
|
return RetryableError{errors}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Whenever we stop watching a cluster, we need to cleanup everything that we have
|
|
// created. This piece of code is responsible for doing just that. It takes care of
|
|
// services, endpoints and namespaces (if needed)
|
|
func (rcsw *RemoteClusterServiceWatcher) cleanupMirroredResources(ctx context.Context) error {
|
|
matchLabels := map[string]string{
|
|
consts.MirroredResourceLabel: "true",
|
|
consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName,
|
|
}
|
|
|
|
services, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector())
|
|
if err != nil {
|
|
innerErr := fmt.Errorf("could not retrieve mirrored services that need cleaning up: %w", err)
|
|
if kerrors.IsNotFound(err) {
|
|
return innerErr
|
|
}
|
|
// if its not notFound then something else went wrong, so we can retry
|
|
return RetryableError{[]error{innerErr}}
|
|
}
|
|
|
|
var errors []error
|
|
for _, svc := range services {
|
|
if err := rcsw.localAPIClient.Client.CoreV1().Services(svc.Namespace).Delete(ctx, svc.Name, metav1.DeleteOptions{}); err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
continue
|
|
}
|
|
errors = append(errors, fmt.Errorf("Could not delete service %s/%s: %w", svc.Namespace, svc.Name, err))
|
|
} else {
|
|
rcsw.log.Infof("Deleted service %s/%s", svc.Namespace, svc.Name)
|
|
}
|
|
}
|
|
|
|
endpoints, err := rcsw.localAPIClient.Endpoint().Lister().List(labels.Set(matchLabels).AsSelector())
|
|
if err != nil {
|
|
innerErr := fmt.Errorf("could not retrieve endpoints that need cleaning up: %w", err)
|
|
if kerrors.IsNotFound(err) {
|
|
return innerErr
|
|
}
|
|
return RetryableError{[]error{innerErr}}
|
|
}
|
|
|
|
for _, endpoint := range endpoints {
|
|
if err := rcsw.localAPIClient.Client.CoreV1().Endpoints(endpoint.Namespace).Delete(ctx, endpoint.Name, metav1.DeleteOptions{}); err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
continue
|
|
}
|
|
errors = append(errors, fmt.Errorf("Could not delete endpoints %s/%s: %w", endpoint.Namespace, endpoint.Name, err))
|
|
} else {
|
|
rcsw.log.Infof("Deleted endpoints %s/%s", endpoint.Namespace, endpoint.Name)
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return RetryableError{errors}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Deletes a locally mirrored service as it is not present on the remote cluster anymore
|
|
func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceUnexported(ctx context.Context, ev *RemoteServiceUnexported) error {
|
|
rcsw.deleteLinkMirrorStatus(
|
|
ev.Name, ev.Namespace,
|
|
)
|
|
|
|
localServiceName := rcsw.mirrorServiceName(ev.Name)
|
|
localService, err := rcsw.localAPIClient.Svc().Lister().Services(ev.Namespace).Get(localServiceName)
|
|
var errors []error
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
rcsw.log.Debugf("Failed to delete mirror service %s/%s: %v", ev.Namespace, ev.Name, err)
|
|
return nil
|
|
}
|
|
errors = append(errors, fmt.Errorf("could not fetch service %s/%s: %w", ev.Namespace, localServiceName, err))
|
|
}
|
|
|
|
// If the mirror service is headless, also delete its endpoint mirror
|
|
// services.
|
|
if localService.Spec.ClusterIP == corev1.ClusterIPNone {
|
|
matchLabels := map[string]string{
|
|
consts.MirroredHeadlessSvcNameLabel: localServiceName,
|
|
}
|
|
endpointMirrorServices, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector())
|
|
if err != nil {
|
|
if !kerrors.IsNotFound(err) {
|
|
errors = append(errors, fmt.Errorf("could not fetch endpoint mirrors for mirror service %s/%s: %w", ev.Namespace, localServiceName, err))
|
|
}
|
|
}
|
|
|
|
for _, endpointMirror := range endpointMirrorServices {
|
|
err = rcsw.localAPIClient.Client.CoreV1().Services(endpointMirror.Namespace).Delete(ctx, endpointMirror.Name, metav1.DeleteOptions{})
|
|
if err != nil {
|
|
if !kerrors.IsNotFound(err) {
|
|
errors = append(errors, fmt.Errorf("could not delete endpoint mirror %s/%s: %w", endpointMirror.Namespace, endpointMirror.Name, err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
rcsw.log.Infof("Deleting mirrored service %s/%s", ev.Namespace, localServiceName)
|
|
if err := rcsw.localAPIClient.Client.CoreV1().Services(ev.Namespace).Delete(ctx, localServiceName, metav1.DeleteOptions{}); err != nil {
|
|
if !kerrors.IsNotFound(err) {
|
|
errors = append(errors, fmt.Errorf("could not delete service: %s/%s: %w", ev.Namespace, localServiceName, err))
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return RetryableError{errors}
|
|
}
|
|
|
|
rcsw.log.Infof("Successfully deleted service: %s/%s", ev.Namespace, localServiceName)
|
|
return nil
|
|
}
|
|
|
|
// Removes a remote service from a local federated service.
|
|
func (rcsw *RemoteClusterServiceWatcher) handleFederatedServiceLeave(ctx context.Context, ev *RemoteServiceLeavesFederatedService) error {
|
|
rcsw.deleteLinkFederatedStatus(
|
|
ev.Name, ev.Namespace,
|
|
)
|
|
|
|
localServiceName := rcsw.federatedServiceName(ev.Name)
|
|
localService, err := rcsw.localAPIClient.Svc().Lister().Services(ev.Namespace).Get(localServiceName)
|
|
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
rcsw.log.Debugf("Failed to update federated service %s/%s: %v", ev.Namespace, ev.Name, err)
|
|
return nil
|
|
}
|
|
return RetryableError{[]error{fmt.Errorf("could not fetch service %s/%s: %w", ev.Namespace, localServiceName, err)}}
|
|
}
|
|
|
|
if rcsw.link.Spec.TargetClusterName == "" {
|
|
// Local discovery
|
|
delete(localService.Annotations, consts.LocalDiscoveryAnnotation)
|
|
} else {
|
|
remoteTarget := fmt.Sprintf("%s@%s", ev.Name, rcsw.link.Spec.TargetClusterName)
|
|
if !remoteDiscoveryContains(localService.Annotations[consts.RemoteDiscoveryAnnotation], remoteTarget) {
|
|
return nil
|
|
}
|
|
|
|
remoteDiscoveryList := strings.Split(localService.Annotations[consts.RemoteDiscoveryAnnotation], ",")
|
|
newRemoteDiscoveryList := []string{}
|
|
for _, member := range remoteDiscoveryList {
|
|
if member == remoteTarget {
|
|
continue
|
|
}
|
|
newRemoteDiscoveryList = append(newRemoteDiscoveryList, member)
|
|
}
|
|
localService.Annotations[consts.RemoteDiscoveryAnnotation] = strings.Join(newRemoteDiscoveryList, ",")
|
|
}
|
|
|
|
if len(localService.Annotations[consts.RemoteDiscoveryAnnotation]) == 0 && len(localService.Annotations[consts.LocalDiscoveryAnnotation]) == 0 {
|
|
rcsw.log.Infof("Deleting federated service %s/%s", ev.Namespace, localServiceName)
|
|
if err := rcsw.localAPIClient.Client.CoreV1().Services(ev.Namespace).Delete(ctx, localServiceName, metav1.DeleteOptions{}); err != nil {
|
|
if !kerrors.IsNotFound(err) {
|
|
return RetryableError{[]error{fmt.Errorf("could not delete service: %s/%s: %w", ev.Namespace, localServiceName, err)}}
|
|
}
|
|
}
|
|
rcsw.log.Infof("Successfully deleted service: %s/%s", ev.Namespace, localServiceName)
|
|
rcsw.deleteLinkFederatedStatus(
|
|
ev.Name, ev.Namespace,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
if _, err := rcsw.localAPIClient.Client.CoreV1().Services(ev.Namespace).Update(ctx, localService, metav1.UpdateOptions{}); err != nil {
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Updates a locally mirrored service. There might have been some pretty fundamental changes such as
|
|
// new gateway being assigned or additional ports exposed. This method takes care of that.
|
|
func (rcsw *RemoteClusterServiceWatcher) handleRemoteExportedServiceUpdated(ctx context.Context, ev *RemoteExportedServiceUpdated) error {
|
|
rcsw.log.Infof("Updating mirror service for %s/%s", ev.remoteUpdate.Namespace, ev.remoteUpdate.Name)
|
|
|
|
mirrorName := rcsw.mirrorServiceName(ev.remoteUpdate.Name)
|
|
localService, err := rcsw.localAPIClient.Svc().Lister().Services(ev.remoteUpdate.Namespace).Get(mirrorName)
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
rcsw.log.Infof("Mirror service %s/%s not found, re-creating", ev.remoteUpdate.Namespace, mirrorName)
|
|
rcsw.eventsQueue.Add(&RemoteServiceExported{
|
|
service: ev.remoteUpdate,
|
|
})
|
|
return nil
|
|
}
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
|
|
localEndpoints, err := rcsw.localAPIClient.Endpoint().Lister().Endpoints(localService.Namespace).Get(mirrorName)
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
localEndpoints = nil
|
|
} else {
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
}
|
|
|
|
if rcsw.isRemoteDiscovery(ev.remoteUpdate.Labels) {
|
|
// The service is mirrored in remote discovery mode and any local
|
|
// endpoints for it should be deleted if they exist.
|
|
if localEndpoints != nil {
|
|
err := rcsw.localAPIClient.Client.CoreV1().Endpoints(localService.Namespace).Delete(ctx, localService.Name, metav1.DeleteOptions{})
|
|
if err != nil {
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.remoteUpdate.GetName(), ev.remoteUpdate.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to delete mirror endpoints: %s", err), nil),
|
|
)
|
|
return RetryableError{[]error{
|
|
fmt.Errorf("failed to delete mirror endpoints for %s/%s: %w", localService.Namespace, localService.Name, err),
|
|
}}
|
|
}
|
|
}
|
|
} else if localEndpoints == nil {
|
|
// The service is mirrored in gateway mode and gateway endpoints should
|
|
// be created for it.
|
|
err := rcsw.createGatewayEndpoints(ctx, ev.remoteUpdate)
|
|
if err != nil {
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.remoteUpdate.GetName(), ev.remoteUpdate.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to create mirror endpoints: %s", err), nil),
|
|
)
|
|
return err
|
|
}
|
|
} else {
|
|
// The service is mirrored in gateway mode and gateway endpoints already
|
|
// exist for it but may need to be updated.
|
|
gatewayAddresses, err := rcsw.resolveGatewayAddress()
|
|
if err != nil {
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.remoteUpdate.GetName(), ev.remoteUpdate.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to get gateway address: %s", err), nil),
|
|
)
|
|
return err
|
|
}
|
|
|
|
copiedEndpoints := localEndpoints.DeepCopy()
|
|
ports, err := rcsw.getEndpointsPorts(ev.remoteUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
copiedEndpoints.Subsets = []corev1.EndpointSubset{
|
|
{
|
|
Addresses: gatewayAddresses,
|
|
Ports: ports,
|
|
},
|
|
}
|
|
|
|
if copiedEndpoints.Annotations == nil {
|
|
copiedEndpoints.Annotations = make(map[string]string)
|
|
}
|
|
copiedEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.Spec.GatewayIdentity
|
|
|
|
err = rcsw.updateMirrorEndpoints(ctx, copiedEndpoints)
|
|
if err != nil {
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.remoteUpdate.GetName(), ev.remoteUpdate.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to update mirror endpoints: %s", err), nil),
|
|
)
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
}
|
|
|
|
localService.Labels = rcsw.getMirrorServiceLabels(ev.remoteUpdate)
|
|
localService.Annotations = rcsw.getMirrorServiceAnnotations(ev.remoteUpdate)
|
|
localService.Spec.Ports = remapRemoteServicePorts(ev.remoteUpdate.Spec.Ports)
|
|
|
|
if _, err := rcsw.localAPIClient.Client.CoreV1().Services(localService.Namespace).Update(ctx, localService, metav1.UpdateOptions{}); err != nil {
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.remoteUpdate.GetName(), ev.remoteUpdate.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to update mirror service: %s", err), nil),
|
|
)
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.remoteUpdate.GetName(), ev.remoteUpdate.GetNamespace(),
|
|
mirrorStatusCondition(true, reasonMirrored, "", localService),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// Updates a federated service to include the remote service as a member.
|
|
func (rcsw *RemoteClusterServiceWatcher) handleFederatedServiceJoin(ctx context.Context, ev *RemoteServiceJoinsFederatedService) error {
|
|
federatedName := rcsw.federatedServiceName(ev.remoteUpdate.Name)
|
|
rcsw.log.Infof("Updating federated service %s/%s", ev.remoteUpdate.Namespace, federatedName)
|
|
|
|
localService, err := rcsw.localAPIClient.Svc().Lister().Services(ev.remoteUpdate.Namespace).Get(federatedName)
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
rcsw.eventsQueue.Add(&CreateFederatedService{
|
|
service: ev.remoteUpdate,
|
|
})
|
|
return nil
|
|
}
|
|
rcsw.updateLinkFederatedStatus(
|
|
ev.remoteUpdate.Name, ev.remoteUpdate.Namespace,
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to get federated service: %s", err), nil),
|
|
)
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
|
|
previous := localService.DeepCopy()
|
|
|
|
if ev.remoteUpdate.Spec.ClusterIP == corev1.ClusterIPNone {
|
|
rcsw.updateLinkFederatedStatus(
|
|
ev.remoteUpdate.GetName(), ev.remoteUpdate.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonInvalidService, "Headless service cannot join federated service", nil),
|
|
)
|
|
return fmt.Errorf("headless service %s/%s cannot join federated service", ev.remoteUpdate.GetNamespace(), ev.remoteUpdate.GetName())
|
|
}
|
|
|
|
// We treat the member with the oldest Link as the source of truth for Service
|
|
// metadata such as labels and annotations.
|
|
var isOldest bool
|
|
if rcsw.link.Spec.TargetClusterName == "" {
|
|
// Always treat the local cluster as the oldest.
|
|
isOldest = true
|
|
} else if _, localDiscoveryExists := localService.Annotations[consts.LocalDiscoveryAnnotation]; localDiscoveryExists {
|
|
// The local cluster is older than us.
|
|
isOldest = false
|
|
} else {
|
|
// The local cluster is not a member of the federated service. Therefore,
|
|
// we check the remote discovery to see if we are the oldest.
|
|
isOldest = true
|
|
members := strings.Split(localService.Annotations[consts.RemoteDiscoveryAnnotation], ",")
|
|
for _, member := range members {
|
|
target := strings.Split(member, "@")
|
|
if len(target) != 2 {
|
|
rcsw.log.Errorf("Invalid federated service member: %s", member)
|
|
continue
|
|
}
|
|
cluster := target[1]
|
|
link, err := rcsw.linksAPIClient.Link().Lister().Links(rcsw.serviceMirrorNamespace).Get(cluster)
|
|
if err != nil {
|
|
rcsw.log.Errorf("Failed to get link %s: %s", cluster, err)
|
|
continue
|
|
}
|
|
if link.CreationTimestamp.Before(&rcsw.link.CreationTimestamp) {
|
|
rcsw.log.Debugf("ignoring metadata from %s because %s is older", rcsw.link.Spec.TargetClusterName, cluster)
|
|
isOldest = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if isOldest {
|
|
preservedAnnotations := map[string]string{}
|
|
for _, k := range []string{consts.RemoteDiscoveryAnnotation, consts.LocalDiscoveryAnnotation} {
|
|
if v, ok := localService.Annotations[k]; ok {
|
|
preservedAnnotations[k] = v
|
|
}
|
|
}
|
|
localService.Labels = rcsw.getFederatedServiceLabels(ev.remoteUpdate)
|
|
localService.Annotations = rcsw.getFederatedServiceAnnotations(ev.remoteUpdate)
|
|
for k, v := range preservedAnnotations {
|
|
localService.Annotations[k] = v
|
|
}
|
|
|
|
localService.Spec.Ports = remapRemoteServicePorts(ev.remoteUpdate.Spec.Ports)
|
|
}
|
|
|
|
if rcsw.link.Spec.TargetClusterName == "" {
|
|
// Local discovery
|
|
localService.Annotations[consts.LocalDiscoveryAnnotation] = ev.remoteUpdate.Name
|
|
} else {
|
|
// Remote discovery
|
|
remoteTarget := fmt.Sprintf("%s@%s", ev.remoteUpdate.Name, rcsw.link.Spec.TargetClusterName)
|
|
if !remoteDiscoveryContains(localService.Annotations[consts.RemoteDiscoveryAnnotation], remoteTarget) {
|
|
if localService.Annotations[consts.RemoteDiscoveryAnnotation] == "" {
|
|
localService.Annotations[consts.RemoteDiscoveryAnnotation] = remoteTarget
|
|
} else {
|
|
localService.Annotations[consts.RemoteDiscoveryAnnotation] = fmt.Sprintf(
|
|
"%s,%s",
|
|
localService.Annotations[consts.RemoteDiscoveryAnnotation],
|
|
remoteTarget,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't update the service if it has not changed.
|
|
if reflect.DeepEqual(previous, localService) {
|
|
return nil
|
|
}
|
|
|
|
if _, err := rcsw.localAPIClient.Client.CoreV1().Services(localService.Namespace).Update(ctx, localService, metav1.UpdateOptions{}); err != nil {
|
|
rcsw.updateLinkFederatedStatus(
|
|
ev.remoteUpdate.GetName(), ev.remoteUpdate.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to update federated service: %s", err), nil),
|
|
)
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
rcsw.updateLinkFederatedStatus(
|
|
ev.remoteUpdate.GetName(), ev.remoteUpdate.GetNamespace(),
|
|
mirrorStatusCondition(true, reasonMirrored, "", localService),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func remoteDiscoveryContains(remoteDiscoveryList string, remoteTarget string) bool {
|
|
for _, member := range strings.Split(remoteDiscoveryList, ",") {
|
|
if member == remoteTarget {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func remapRemoteServicePorts(ports []corev1.ServicePort) []corev1.ServicePort {
|
|
// We ignore the NodePort here as its not relevant
|
|
// to the local cluster
|
|
var newPorts []corev1.ServicePort
|
|
for _, port := range ports {
|
|
newPorts = append(newPorts, corev1.ServicePort{
|
|
Name: port.Name,
|
|
Protocol: port.Protocol,
|
|
Port: port.Port,
|
|
TargetPort: port.TargetPort,
|
|
})
|
|
}
|
|
return newPorts
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceExported(ctx context.Context, ev *RemoteServiceExported) error {
|
|
remoteService := ev.service.DeepCopy()
|
|
if rcsw.headlessServicesEnabled && remoteService.Spec.ClusterIP == corev1.ClusterIPNone {
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.service.GetName(), ev.service.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonInvalidService, "Headless mirror services are disabled", nil),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
serviceInfo := fmt.Sprintf("%s/%s", remoteService.Namespace, remoteService.Name)
|
|
localServiceName := rcsw.mirrorServiceName(remoteService.Name)
|
|
|
|
if rcsw.namespaceCreationEnabled {
|
|
if err := rcsw.mirrorNamespaceIfNecessary(ctx, remoteService.Namespace); err != nil {
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.service.GetName(), ev.service.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to create namespace: %s", err), nil),
|
|
)
|
|
return err
|
|
}
|
|
} else {
|
|
// Ensure the namespace exists, and skip mirroring if it doesn't
|
|
if _, err := rcsw.localAPIClient.Client.CoreV1().Namespaces().Get(ctx, remoteService.Namespace, metav1.GetOptions{}); err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
rcsw.recorder.Event(remoteService, corev1.EventTypeNormal, eventTypeSkipped, "Skipped mirroring service: namespace does not exist")
|
|
rcsw.log.Warnf("Skipping mirroring of service %s: namespace %s does not exist", serviceInfo, remoteService.Namespace)
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.service.GetName(), ev.service.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonMissingNamespace, "Namespace does not exist", nil),
|
|
)
|
|
return nil
|
|
}
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.service.GetName(), ev.service.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed get namespace: %s", err), nil),
|
|
)
|
|
// something else went wrong, so we can just retry
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
}
|
|
|
|
serviceToCreate := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: localServiceName,
|
|
Namespace: remoteService.Namespace,
|
|
Annotations: rcsw.getMirrorServiceAnnotations(remoteService),
|
|
Labels: rcsw.getMirrorServiceLabels(remoteService),
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: remapRemoteServicePorts(remoteService.Spec.Ports),
|
|
},
|
|
}
|
|
|
|
rcsw.log.Infof("Creating a new service mirror for %s", serviceInfo)
|
|
if _, err := rcsw.localAPIClient.Client.CoreV1().Services(remoteService.Namespace).Create(ctx, serviceToCreate, metav1.CreateOptions{}); err != nil {
|
|
if !kerrors.IsAlreadyExists(err) {
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.service.GetName(), ev.service.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to create mirror service: %s", err), nil),
|
|
)
|
|
// we might have created it during earlier attempt, if that is not the case, we retry
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
}
|
|
|
|
if rcsw.isRemoteDiscovery(remoteService.Labels) {
|
|
// For remote discovery services, skip creating gateway endpoints.
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.service.GetName(), ev.service.GetNamespace(),
|
|
mirrorStatusCondition(true, reasonMirrored, "", serviceToCreate),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
err := rcsw.createGatewayEndpoints(ctx, remoteService)
|
|
if err != nil {
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.service.GetName(), ev.service.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to create mirror endpoints: %s", err), nil),
|
|
)
|
|
return err
|
|
}
|
|
|
|
rcsw.updateLinkMirrorStatus(
|
|
ev.service.GetName(), ev.service.GetNamespace(),
|
|
mirrorStatusCondition(true, reasonMirrored, "", serviceToCreate),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) handleCreateFederatedService(ctx context.Context, ev *CreateFederatedService) error {
|
|
remoteService := ev.service.DeepCopy()
|
|
serviceInfo := fmt.Sprintf("%s/%s", remoteService.Namespace, remoteService.Name)
|
|
|
|
if remoteService.Spec.ClusterIP == corev1.ClusterIPNone {
|
|
rcsw.updateLinkFederatedStatus(
|
|
remoteService.GetName(), remoteService.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonInvalidService, "Headless service cannot join federated service", nil),
|
|
)
|
|
return fmt.Errorf("headless service %s cannot join federated service", serviceInfo)
|
|
}
|
|
|
|
localServiceName := rcsw.federatedServiceName(remoteService.Name)
|
|
|
|
if rcsw.namespaceCreationEnabled {
|
|
if err := rcsw.mirrorNamespaceIfNecessary(ctx, remoteService.Namespace); err != nil {
|
|
rcsw.updateLinkFederatedStatus(
|
|
remoteService.GetName(), remoteService.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to create namespace: %s", err), nil),
|
|
)
|
|
return err
|
|
}
|
|
} else {
|
|
// Ensure the namespace exists, and skip mirroring if it doesn't
|
|
if _, err := rcsw.localAPIClient.Client.CoreV1().Namespaces().Get(ctx, remoteService.Namespace, metav1.GetOptions{}); err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
rcsw.recorder.Event(remoteService, corev1.EventTypeNormal, eventTypeSkipped, "Skipped mirroring service: namespace does not exist")
|
|
rcsw.log.Warnf("Skipping mirroring of service %s: namespace %s does not exist", serviceInfo, remoteService.Namespace)
|
|
rcsw.updateLinkFederatedStatus(
|
|
remoteService.GetName(), remoteService.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonMissingNamespace, "Namespace does not exist", nil),
|
|
)
|
|
return nil
|
|
}
|
|
// something else went wrong, so we can just retry
|
|
rcsw.updateLinkFederatedStatus(
|
|
remoteService.GetName(), remoteService.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed get namespace: %s", err), nil),
|
|
)
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
}
|
|
|
|
serviceToCreate := &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: localServiceName,
|
|
Namespace: remoteService.Namespace,
|
|
Annotations: rcsw.getFederatedServiceAnnotations(remoteService),
|
|
Labels: rcsw.getFederatedServiceLabels(remoteService),
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Ports: remapRemoteServicePorts(remoteService.Spec.Ports),
|
|
},
|
|
}
|
|
|
|
rcsw.log.Infof("Creating a new federated service for %s", serviceInfo)
|
|
if _, err := rcsw.localAPIClient.Client.CoreV1().Services(remoteService.Namespace).Create(ctx, serviceToCreate, metav1.CreateOptions{}); err != nil {
|
|
if !kerrors.IsAlreadyExists(err) {
|
|
// we might have created it during earlier attempt, if that is not the case, we retry
|
|
rcsw.updateLinkFederatedStatus(
|
|
remoteService.GetName(), remoteService.GetNamespace(),
|
|
mirrorStatusCondition(false, reasonError, fmt.Sprintf("Failed to create federated service: %s", err), nil),
|
|
)
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
}
|
|
|
|
rcsw.updateLinkFederatedStatus(
|
|
remoteService.GetName(), remoteService.GetNamespace(),
|
|
mirrorStatusCondition(true, reasonMirrored, "", serviceToCreate),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) handleLocalNamespaceAdded(ns *corev1.Namespace) error {
|
|
// When a local namespace is added, we issue a create event for all the services in the corresponding namespace in
|
|
// case any of them are exported and need to be mirrored.
|
|
svcs, err := rcsw.remoteAPIClient.Svc().Lister().Services(ns.Name).List(labels.Everything())
|
|
if err != nil {
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
for _, svc := range svcs {
|
|
rcsw.eventsQueue.Add(&OnAddCalled{
|
|
svc: svc,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// isEmptyService returns true if any of these conditions are true:
|
|
// - svc's Endpoint is not found
|
|
// - svc's Endpoint has no Subsets (happens when there's no associated Pod)
|
|
// - svc's Endpoint has Subsets, but none have addresses (only notReadyAddresses,
|
|
// when the pod is not ready yet)
|
|
func (rcsw *RemoteClusterServiceWatcher) isEmptyService(svc *corev1.Service) (bool, error) {
|
|
ep, err := rcsw.remoteAPIClient.Endpoint().Lister().Endpoints(svc.Namespace).Get(svc.Name)
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
rcsw.log.Debugf("target endpoint %s/%s not found", svc.Namespace, svc.Name)
|
|
return true, nil
|
|
}
|
|
|
|
return true, err
|
|
}
|
|
return rcsw.isEmptyEndpoints(ep), nil
|
|
}
|
|
|
|
// isEmptyEndpoints returns true if any of these conditions are true:
|
|
// - The Endpoint is not found
|
|
// - The Endpoint has no Subsets (happens when there's no associated Pod)
|
|
// - The Endpoint has Subsets, but none have addresses (only notReadyAddresses,
|
|
// when the pod is not ready yet)
|
|
func (rcsw *RemoteClusterServiceWatcher) isEmptyEndpoints(ep *corev1.Endpoints) bool {
|
|
if len(ep.Subsets) == 0 {
|
|
rcsw.log.Debugf("endpoint %s/%s has no Subsets", ep.Namespace, ep.Name)
|
|
return true
|
|
}
|
|
for _, subset := range ep.Subsets {
|
|
if len(subset.Addresses) > 0 {
|
|
return false
|
|
}
|
|
}
|
|
rcsw.log.Debugf("endpoint %s/%s has no ready addresses", ep.Namespace, ep.Name)
|
|
return true
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) createGatewayEndpoints(ctx context.Context, exportedService *corev1.Service) error {
|
|
empty, err := rcsw.isEmptyService(exportedService)
|
|
if err != nil {
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
|
|
gatewayAddresses, err := rcsw.resolveGatewayAddress()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
localServiceName := rcsw.mirrorServiceName(exportedService.Name)
|
|
serviceInfo := fmt.Sprintf("%s/%s", exportedService.Namespace, exportedService.Name)
|
|
endpointsToCreate := &corev1.Endpoints{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: localServiceName,
|
|
Namespace: exportedService.Namespace,
|
|
Labels: rcsw.getMirrorEndpointLabels(exportedService),
|
|
Annotations: map[string]string{
|
|
consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.%s", exportedService.Name, exportedService.Namespace, rcsw.link.Spec.TargetClusterDomain),
|
|
},
|
|
},
|
|
}
|
|
|
|
rcsw.log.Infof("Resolved gateway [%v:%s] for %s", gatewayAddresses, rcsw.link.Spec.GatewayPort, serviceInfo)
|
|
|
|
ports, err := rcsw.getEndpointsPorts(exportedService)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !empty && len(gatewayAddresses) > 0 {
|
|
|
|
endpointsToCreate.Subsets = []corev1.EndpointSubset{
|
|
{
|
|
Addresses: gatewayAddresses,
|
|
Ports: ports,
|
|
},
|
|
}
|
|
} else if !empty {
|
|
endpointsToCreate.Subsets = []corev1.EndpointSubset{
|
|
{
|
|
NotReadyAddresses: gatewayAddresses,
|
|
Ports: ports,
|
|
},
|
|
}
|
|
rcsw.log.Warnf("could not resolve gateway addresses for %s; setting endpoint subsets to not ready", serviceInfo)
|
|
} else {
|
|
rcsw.log.Warnf("exported service %s is empty", serviceInfo)
|
|
}
|
|
|
|
if rcsw.link.Spec.GatewayIdentity != "" {
|
|
endpointsToCreate.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.Spec.GatewayIdentity
|
|
}
|
|
|
|
rcsw.log.Infof("Creating a new endpoints for %s", serviceInfo)
|
|
err = rcsw.createMirrorEndpoints(ctx, endpointsToCreate)
|
|
if err != nil {
|
|
if svcErr := rcsw.localAPIClient.Client.CoreV1().Services(exportedService.Namespace).Delete(ctx, localServiceName, metav1.DeleteOptions{}); svcErr != nil {
|
|
rcsw.log.Errorf("Failed to delete service %s after endpoints creation failed: %s", localServiceName, svcErr)
|
|
}
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// this method is common to both CREATE and UPDATE because if we have been
|
|
// offline for some time due to a crash a CREATE for a service that we have
|
|
// observed before is simply a case of UPDATE
|
|
func (rcsw *RemoteClusterServiceWatcher) createOrUpdateService(service *corev1.Service) error {
|
|
mirrorName := rcsw.mirrorServiceName(service.Name)
|
|
|
|
if rcsw.isExported(service.Labels) || rcsw.isRemoteDiscovery(service.Labels) {
|
|
// The desired state is that the local mirror service should exist.
|
|
localService, err := rcsw.localAPIClient.Svc().Lister().Services(service.Namespace).Get(mirrorName)
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
rcsw.eventsQueue.Add(&RemoteServiceExported{
|
|
service: service,
|
|
})
|
|
return nil
|
|
}
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
// if we have the local service present, we need to issue an update
|
|
lastMirroredRemoteVersion, ok := localService.Annotations[consts.RemoteResourceVersionAnnotation]
|
|
if ok && lastMirroredRemoteVersion != service.ResourceVersion {
|
|
rcsw.eventsQueue.Add(&RemoteExportedServiceUpdated{
|
|
remoteUpdate: service,
|
|
})
|
|
}
|
|
} else {
|
|
// The desired state is that the local mirror service should not exist.
|
|
localSvc, err := rcsw.localAPIClient.Svc().Lister().Services(service.Namespace).Get(mirrorName)
|
|
if err == nil {
|
|
if localSvc.Labels != nil {
|
|
_, isMirroredRes := localSvc.Labels[consts.MirroredResourceLabel]
|
|
clusterName := localSvc.Labels[consts.RemoteClusterNameLabel]
|
|
if isMirroredRes && (clusterName == rcsw.link.Spec.TargetClusterName) {
|
|
rcsw.eventsQueue.Add(&RemoteServiceUnexported{
|
|
Name: service.Name,
|
|
Namespace: service.Namespace,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
federatedName := rcsw.federatedServiceName(service.Name)
|
|
|
|
if rcsw.isFederatedServiceMember(service.Labels) {
|
|
// The desired state is that the local federated service should exist.
|
|
rcsw.eventsQueue.Add(&RemoteServiceJoinsFederatedService{
|
|
remoteUpdate: service,
|
|
})
|
|
} else {
|
|
// The desired state is that the local federated service should not
|
|
// include the remote service.
|
|
localSvc, err := rcsw.localAPIClient.Svc().Lister().Services(service.Namespace).Get(federatedName)
|
|
if err == nil {
|
|
if localSvc.Labels != nil {
|
|
_, isMirroredRes := localSvc.Labels[consts.MirroredResourceLabel]
|
|
if isMirroredRes {
|
|
rcsw.eventsQueue.Add(&RemoteServiceLeavesFederatedService{
|
|
Name: service.Name,
|
|
Namespace: service.Namespace,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) getMirrorServices() (*corev1.ServiceList, error) {
|
|
matchLabels := map[string]string{
|
|
consts.MirroredResourceLabel: "true",
|
|
consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName,
|
|
}
|
|
services, err := rcsw.localAPIClient.Client.CoreV1().Services("").List(context.Background(), metav1.ListOptions{LabelSelector: labels.SelectorFromSet(matchLabels).String()})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return services, nil
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) handleOnDelete(service *corev1.Service) {
|
|
if rcsw.isExported(service.Labels) || rcsw.isRemoteDiscovery(service.Labels) {
|
|
rcsw.eventsQueue.Add(&RemoteServiceUnexported{
|
|
Name: service.Name,
|
|
Namespace: service.Namespace,
|
|
})
|
|
}
|
|
if rcsw.isFederatedServiceMember(service.Labels) {
|
|
rcsw.eventsQueue.Add(&RemoteServiceLeavesFederatedService{
|
|
Name: service.Name,
|
|
Namespace: service.Namespace,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) processNextEvent(ctx context.Context) (bool, interface{}, error) {
|
|
event, done := rcsw.eventsQueue.Get()
|
|
if event != nil {
|
|
rcsw.log.Infof("Received: %s", event)
|
|
} else if done {
|
|
rcsw.log.Infof("Received: Stop")
|
|
}
|
|
|
|
var err error
|
|
switch ev := event.(type) {
|
|
case *OnAddCalled:
|
|
err = rcsw.createOrUpdateService(ev.svc)
|
|
case *OnAddEndpointsCalled:
|
|
err = rcsw.handleCreateOrUpdateEndpoints(ctx, ev.ep)
|
|
case *OnUpdateCalled:
|
|
err = rcsw.createOrUpdateService(ev.svc)
|
|
case *OnUpdateEndpointsCalled:
|
|
err = rcsw.handleCreateOrUpdateEndpoints(ctx, ev.ep)
|
|
case *OnDeleteCalled:
|
|
rcsw.handleOnDelete(ev.svc)
|
|
case *RemoteServiceExported:
|
|
err = rcsw.handleRemoteServiceExported(ctx, ev)
|
|
case *RemoteExportedServiceUpdated:
|
|
err = rcsw.handleRemoteExportedServiceUpdated(ctx, ev)
|
|
case *RemoteServiceUnexported:
|
|
err = rcsw.handleRemoteServiceUnexported(ctx, ev)
|
|
case *CreateFederatedService:
|
|
err = rcsw.handleCreateFederatedService(ctx, ev)
|
|
case *RemoteServiceJoinsFederatedService:
|
|
err = rcsw.handleFederatedServiceJoin(ctx, ev)
|
|
case *RemoteServiceLeavesFederatedService:
|
|
err = rcsw.handleFederatedServiceLeave(ctx, ev)
|
|
case *ClusterUnregistered:
|
|
err = rcsw.cleanupMirroredResources(ctx)
|
|
case *OrphanedServicesGcTriggered:
|
|
err = rcsw.cleanupOrphanedServices(ctx)
|
|
case *RepairEndpoints:
|
|
err = rcsw.repairEndpoints(ctx)
|
|
case *OnLocalNamespaceAdded:
|
|
err = rcsw.handleLocalNamespaceAdded(ev.ns)
|
|
default:
|
|
if ev != nil || !done { // we get a nil in case we are shutting down...
|
|
rcsw.log.Warnf("Received unknown event: %v", ev)
|
|
}
|
|
}
|
|
|
|
return done, event, err
|
|
|
|
}
|
|
|
|
// the main processing loop in which we handle more domain specific events
|
|
// and deal with retries
|
|
func (rcsw *RemoteClusterServiceWatcher) processEvents(ctx context.Context) {
|
|
for {
|
|
done, event, err := rcsw.processNextEvent(ctx)
|
|
rcsw.eventsQueue.Done(event)
|
|
// the logic here is that there might have been an API
|
|
// connectivity glitch or something. So its not a bad idea to requeue
|
|
// the event and try again up to a number of limits, just to ensure
|
|
// that we are not diverging in states due to bad luck...
|
|
if err == nil {
|
|
rcsw.eventsQueue.Forget(event)
|
|
} else {
|
|
var re RetryableError
|
|
if errors.As(err, &re) {
|
|
rcsw.log.Warnf("Requeues: %d, Limit: %d for event %s", rcsw.eventsQueue.NumRequeues(event), rcsw.requeueLimit, event)
|
|
if (rcsw.eventsQueue.NumRequeues(event) < rcsw.requeueLimit) && !done {
|
|
rcsw.log.Errorf("Error processing %s (will retry): %s", event, re)
|
|
rcsw.eventsQueue.AddRateLimited(event)
|
|
} else {
|
|
rcsw.log.Errorf("Error processing %s (giving up): %s", event, re)
|
|
rcsw.eventsQueue.Forget(event)
|
|
}
|
|
} else {
|
|
rcsw.log.Errorf("Error processing %s (will not retry): %s", event, err)
|
|
}
|
|
}
|
|
if done {
|
|
rcsw.log.Infof("Shutting down events processor")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start starts watching the remote cluster
|
|
func (rcsw *RemoteClusterServiceWatcher) Start(ctx context.Context) error {
|
|
rcsw.remoteAPIClient.Sync(rcsw.stopper)
|
|
rcsw.eventsQueue.Add(&OrphanedServicesGcTriggered{})
|
|
var err error
|
|
rcsw.svcHandler, err = rcsw.remoteAPIClient.Svc().Informer().AddEventHandler(
|
|
cache.ResourceEventHandlerFuncs{
|
|
AddFunc: func(svc interface{}) {
|
|
rcsw.eventsQueue.Add(&OnAddCalled{svc.(*corev1.Service)})
|
|
},
|
|
DeleteFunc: func(obj interface{}) {
|
|
service, ok := obj.(*corev1.Service)
|
|
if !ok {
|
|
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
|
if !ok {
|
|
rcsw.log.Errorf("couldn't get object from DeletedFinalStateUnknown %#v", obj)
|
|
return
|
|
}
|
|
service, ok = tombstone.Obj.(*corev1.Service)
|
|
if !ok {
|
|
rcsw.log.Errorf("DeletedFinalStateUnknown contained object that is not a Service %#v", obj)
|
|
return
|
|
}
|
|
}
|
|
rcsw.eventsQueue.Add(&OnDeleteCalled{service})
|
|
},
|
|
UpdateFunc: func(_, new interface{}) {
|
|
rcsw.eventsQueue.Add(&OnUpdateCalled{new.(*corev1.Service)})
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rcsw.epHandler, err = rcsw.remoteAPIClient.Endpoint().Informer().AddEventHandler(
|
|
cache.ResourceEventHandlerFuncs{
|
|
// AddFunc only relevant for exported headless endpoints
|
|
AddFunc: func(obj interface{}) {
|
|
ep, ok := obj.(*corev1.Endpoints)
|
|
if !ok {
|
|
rcsw.log.Errorf("error processing endpoints object: got %#v, expected *corev1.Endpoints", ep)
|
|
return
|
|
}
|
|
|
|
if !rcsw.isExported(ep.Labels) {
|
|
rcsw.log.Debugf("skipped processing endpoints object %s/%s: missing %s label", ep.Namespace, ep.Name, consts.DefaultExportedServiceSelector)
|
|
return
|
|
}
|
|
|
|
if !isHeadlessEndpoints(ep, rcsw.log) {
|
|
return
|
|
}
|
|
|
|
rcsw.eventsQueue.Add(&OnAddEndpointsCalled{obj.(*corev1.Endpoints)})
|
|
},
|
|
// AddFunc relevant for all kind of exported endpoints
|
|
UpdateFunc: func(_, new interface{}) {
|
|
epNew, ok := new.(*corev1.Endpoints)
|
|
if !ok {
|
|
rcsw.log.Errorf("error processing endpoints object: got %#v, expected *corev1.Endpoints", epNew)
|
|
return
|
|
}
|
|
if !rcsw.isExported(epNew.Labels) {
|
|
rcsw.log.Debugf("skipped processing endpoints object %s/%s: missing %s label", epNew.Namespace, epNew.Name, consts.DefaultExportedServiceSelector)
|
|
return
|
|
}
|
|
if rcsw.isRemoteDiscovery(epNew.Labels) {
|
|
rcsw.log.Debugf("skipped processing endpoints object %s/%s (service labeled for remote-discovery mode)", epNew.Namespace, epNew.Name)
|
|
return
|
|
}
|
|
rcsw.eventsQueue.Add(&OnUpdateEndpointsCalled{epNew})
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rcsw.nsHandler, err = rcsw.localAPIClient.NS().Informer().AddEventHandler(
|
|
cache.ResourceEventHandlerFuncs{
|
|
AddFunc: func(obj interface{}) {
|
|
rcsw.eventsQueue.Add(&OnLocalNamespaceAdded{obj.(*corev1.Namespace)})
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
go rcsw.processEvents(ctx)
|
|
|
|
// If no gateway address is present, do not repair endpoints
|
|
if rcsw.link.Spec.GatewayAddress == "" {
|
|
return nil
|
|
}
|
|
|
|
// We need to issue a RepairEndpoints immediately to populate the gateway
|
|
// mirror endpoints.
|
|
ev := RepairEndpoints{}
|
|
rcsw.eventsQueue.Add(&ev)
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(rcsw.repairPeriod)
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
ev := RepairEndpoints{}
|
|
rcsw.eventsQueue.Add(&ev)
|
|
case alive := <-rcsw.liveness:
|
|
rcsw.log.Debugf("gateway liveness change from %t to %t", rcsw.gatewayAlive, alive)
|
|
rcsw.gatewayAlive = alive
|
|
ev := RepairEndpoints{}
|
|
rcsw.eventsQueue.Add(&ev)
|
|
case <-rcsw.stopper:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop stops watching the cluster and cleans up all mirrored resources
|
|
func (rcsw *RemoteClusterServiceWatcher) Stop(cleanupState bool) {
|
|
close(rcsw.stopper)
|
|
if cleanupState {
|
|
rcsw.eventsQueue.Add(&ClusterUnregistered{})
|
|
}
|
|
rcsw.eventsQueue.ShutDown()
|
|
rcsw.eventBroadcaster.Shutdown()
|
|
|
|
if rcsw.svcHandler != nil {
|
|
if err := rcsw.remoteAPIClient.Svc().Informer().RemoveEventHandler(rcsw.svcHandler); err != nil {
|
|
rcsw.log.Warnf("error removing service informer handler: %s", err)
|
|
}
|
|
}
|
|
if rcsw.epHandler != nil {
|
|
if err := rcsw.remoteAPIClient.Endpoint().Informer().RemoveEventHandler(rcsw.epHandler); err != nil {
|
|
rcsw.log.Warnf("error removing service informer handler: %s", err)
|
|
}
|
|
}
|
|
if rcsw.nsHandler != nil {
|
|
if err := rcsw.localAPIClient.NS().Informer().RemoveEventHandler(rcsw.nsHandler); err != nil {
|
|
rcsw.log.Warnf("error removing service informer handler: %s", err)
|
|
}
|
|
}
|
|
|
|
if rcsw.remoteAPIClient != nil {
|
|
rcsw.remoteAPIClient.UnregisterGauges()
|
|
}
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) resolveGatewayAddress() ([]corev1.EndpointAddress, error) {
|
|
var gatewayEndpoints []corev1.EndpointAddress
|
|
var errors []error
|
|
for _, addr := range strings.Split(rcsw.link.Spec.GatewayAddress, ",") {
|
|
ipAddrs, err := net.LookupIP(addr)
|
|
if err != nil {
|
|
err = fmt.Errorf("Error resolving '%s': %w", addr, err)
|
|
rcsw.log.Warn(err)
|
|
errors = append(errors, err)
|
|
continue
|
|
}
|
|
|
|
for _, ipAddr := range ipAddrs {
|
|
gatewayEndpoints = append(gatewayEndpoints, corev1.EndpointAddress{
|
|
IP: ipAddr.String(),
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(gatewayEndpoints) == 0 {
|
|
return nil, RetryableError{errors}
|
|
}
|
|
|
|
sort.SliceStable(gatewayEndpoints, func(i, j int) bool {
|
|
return gatewayEndpoints[i].IP < gatewayEndpoints[j].IP
|
|
})
|
|
return gatewayEndpoints, nil
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) error {
|
|
endpointRepairCounter.With(prometheus.Labels{
|
|
gatewayClusterName: rcsw.link.Spec.TargetClusterName,
|
|
}).Inc()
|
|
|
|
// Create or update the gateway mirror endpoints responsible for driving
|
|
// the cluster watcher's gateway liveness status.
|
|
gatewayAddresses, err := rcsw.resolveGatewayAddress()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = rcsw.createOrUpdateGatewayEndpoints(ctx, gatewayAddresses)
|
|
if err != nil {
|
|
rcsw.log.Errorf("Failed to create/update gateway mirror endpoints: %s", err)
|
|
}
|
|
|
|
// Repair mirror service endpoints.
|
|
mirrorServices, err := rcsw.getMirrorServices()
|
|
if err != nil {
|
|
return RetryableError{[]error{fmt.Errorf("Failed to list mirror services: %w", err)}}
|
|
}
|
|
for _, svc := range mirrorServices.Items {
|
|
svc := svc
|
|
|
|
// Mirrors for headless services are also headless, and their
|
|
// Endpoints point to auxiliary services instead of pointing to
|
|
// the gateway, so they're skipped.
|
|
if svc.Spec.ClusterIP == corev1.ClusterIPNone {
|
|
rcsw.log.Debugf("Skipped repairing endpoints for headless mirror %s/%s", svc.Namespace, svc.Name)
|
|
continue
|
|
}
|
|
|
|
if _, ok := svc.Labels[consts.RemoteDiscoveryLabel]; ok {
|
|
rcsw.log.Debugf("Skipped repairing endpoints for service in remote-discovery mode %s/%s", svc.Namespace, svc.Name)
|
|
continue
|
|
}
|
|
|
|
endpoints, err := rcsw.localAPIClient.Endpoint().Lister().Endpoints(svc.Namespace).Get(svc.Name)
|
|
if err != nil {
|
|
if !kerrors.IsNotFound(err) {
|
|
rcsw.log.Errorf("Failed to list local endpoints: %s", err)
|
|
continue
|
|
}
|
|
endpoints, err = rcsw.localAPIClient.Client.CoreV1().Endpoints(svc.Namespace).Get(ctx, svc.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
rcsw.log.Errorf("Failed to get local endpoints %s/%s: %s", svc.Namespace, svc.Name, err)
|
|
continue
|
|
}
|
|
}
|
|
updatedEndpoints := endpoints.DeepCopy()
|
|
ports, err := rcsw.getEndpointsPorts(&svc)
|
|
if err != nil {
|
|
rcsw.log.Errorf("Failed to get endpoints ports: %s", err)
|
|
continue
|
|
}
|
|
updatedEndpoints.Subsets = []corev1.EndpointSubset{
|
|
{
|
|
Addresses: gatewayAddresses,
|
|
Ports: ports,
|
|
},
|
|
}
|
|
|
|
// We want to skip this service empty check for auxiliary services --
|
|
// services which are not headless but do belong to a headless
|
|
// mirrored service. This is because they do not have a corresponding
|
|
// endpoint on the target cluster, only a pod. If we attempt to find
|
|
// endpoints for services like this, they'll always be set to empty.
|
|
if _, found := svc.Labels[consts.MirroredHeadlessSvcNameLabel]; !found {
|
|
targetService := svc.DeepCopy()
|
|
targetService.Name = rcsw.targetResourceName(svc.Name)
|
|
empty, err := rcsw.isEmptyService(targetService)
|
|
if err != nil {
|
|
rcsw.log.Errorf("Could not check service emptiness: %s", err)
|
|
continue
|
|
}
|
|
if empty {
|
|
rcsw.log.Warnf("Exported service %s/%s is empty", targetService.Namespace, targetService.Name)
|
|
updatedEndpoints.Subsets = []corev1.EndpointSubset{}
|
|
}
|
|
}
|
|
|
|
if updatedEndpoints.Annotations == nil {
|
|
updatedEndpoints.Annotations = make(map[string]string)
|
|
}
|
|
updatedEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.Spec.GatewayIdentity
|
|
|
|
err = rcsw.updateMirrorEndpoints(ctx, updatedEndpoints)
|
|
if err != nil {
|
|
rcsw.log.Error(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createOrUpdateGatewayEndpoints will create or update the gateway mirror
|
|
// endpoints for a remote cluster. These endpoints are required for the probe
|
|
// worker responsible for probing gateway liveness, so these endpoints are
|
|
// never in a not ready state.
|
|
func (rcsw *RemoteClusterServiceWatcher) createOrUpdateGatewayEndpoints(ctx context.Context, addressses []corev1.EndpointAddress) error {
|
|
probePort, err := strconv.ParseInt(rcsw.link.Spec.ProbeSpec.Port, 10, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse probe port: %w", err)
|
|
}
|
|
endpoints := &corev1.Endpoints{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: rcsw.probeSvc,
|
|
Namespace: rcsw.serviceMirrorNamespace,
|
|
Labels: map[string]string{
|
|
consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName,
|
|
},
|
|
Annotations: map[string]string{
|
|
consts.RemoteGatewayIdentity: rcsw.link.Spec.GatewayIdentity,
|
|
},
|
|
},
|
|
Subsets: []corev1.EndpointSubset{
|
|
{
|
|
Addresses: addressses,
|
|
Ports: []corev1.EndpointPort{
|
|
{
|
|
Name: "mc-probe",
|
|
Port: int32(probePort),
|
|
Protocol: "TCP",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
_, err = rcsw.localAPIClient.Client.CoreV1().Endpoints(endpoints.Namespace).Get(ctx, endpoints.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
if !kerrors.IsNotFound(err) {
|
|
return err
|
|
}
|
|
|
|
// Mirror endpoints for the gateway do not exist so they need to be
|
|
// created. As mentioned above, these endpoints are required for the
|
|
// probe worker and therefore should never be put in a not ready
|
|
// state.
|
|
_, err = rcsw.localAPIClient.Client.CoreV1().Endpoints(endpoints.Namespace).Create(ctx, endpoints, metav1.CreateOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Mirror endpoints for the gateway already exist so they need to be
|
|
// updated. As mentioned above, these endpoints are required for the probe
|
|
// worker and therefore should never be put in a not ready state.
|
|
_, err = rcsw.localAPIClient.Client.CoreV1().Endpoints(endpoints.Namespace).Update(ctx, endpoints, metav1.UpdateOptions{})
|
|
return err
|
|
}
|
|
|
|
// handleCreateOrUpdateEndpoints forwards the call to
|
|
// createOrUpdateHeadlessEndpoints when adding/updating exported headless
|
|
// endpoints. Otherwise, it handles updates to endpoints to check if they've
|
|
// become empty/filled since their creation, in order to empty/fill the
|
|
// mirrored endpoints as well
|
|
func (rcsw *RemoteClusterServiceWatcher) handleCreateOrUpdateEndpoints(
|
|
ctx context.Context,
|
|
exportedEndpoints *corev1.Endpoints,
|
|
) error {
|
|
if isHeadlessEndpoints(exportedEndpoints, rcsw.log) {
|
|
if rcsw.headlessServicesEnabled {
|
|
return rcsw.createOrUpdateHeadlessEndpoints(ctx, exportedEndpoints)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
localServiceName := rcsw.mirrorServiceName(exportedEndpoints.Name)
|
|
ep, err := rcsw.localAPIClient.Endpoint().Lister().Endpoints(exportedEndpoints.Namespace).Get(localServiceName)
|
|
if err != nil {
|
|
return RetryableError{[]error{err}}
|
|
}
|
|
|
|
if (rcsw.isEmptyEndpoints(ep) && rcsw.isEmptyEndpoints(exportedEndpoints)) ||
|
|
(!rcsw.isEmptyEndpoints(ep) && !rcsw.isEmptyEndpoints(exportedEndpoints)) {
|
|
return nil
|
|
}
|
|
|
|
rcsw.log.Infof("Updating subsets for mirror endpoint %s/%s", exportedEndpoints.Namespace, exportedEndpoints.Name)
|
|
if rcsw.isEmptyEndpoints(exportedEndpoints) {
|
|
ep.Subsets = []corev1.EndpointSubset{}
|
|
} else {
|
|
exportedService, err := rcsw.remoteAPIClient.Svc().Lister().Services(exportedEndpoints.Namespace).Get(exportedEndpoints.Name)
|
|
if err != nil {
|
|
return RetryableError{[]error{
|
|
fmt.Errorf("error retrieving exported service %s/%s: %w", exportedEndpoints.Namespace, exportedEndpoints.Name, err),
|
|
}}
|
|
}
|
|
gatewayAddresses, err := rcsw.resolveGatewayAddress()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ports, err := rcsw.getEndpointsPorts(exportedService)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ep.Subsets = []corev1.EndpointSubset{
|
|
{
|
|
Addresses: gatewayAddresses,
|
|
Ports: ports,
|
|
},
|
|
}
|
|
}
|
|
return rcsw.updateMirrorEndpoints(ctx, ep)
|
|
}
|
|
|
|
// createMirrorEndpoints will create endpoints based off gateway liveness. If
|
|
// the gateway is not alive, then the addresses in each subset will be set to
|
|
// not ready.
|
|
func (rcsw *RemoteClusterServiceWatcher) createMirrorEndpoints(ctx context.Context, endpoints *corev1.Endpoints) error {
|
|
rcsw.updateReadiness(endpoints)
|
|
_, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(endpoints.Namespace).Create(ctx, endpoints, metav1.CreateOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create mirror endpoints for %s/%s: %w", endpoints.Namespace, endpoints.Name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// updateMirrorEndpoints will update endpoints based off gateway liveness. If
|
|
// the gateway is not alive, then the addresses in each subset will be set to
|
|
// not ready. Future calls to updateMirrorEndpoints can set the addresses back
|
|
// to ready if the gateway is alive.
|
|
func (rcsw *RemoteClusterServiceWatcher) updateMirrorEndpoints(ctx context.Context, endpoints *corev1.Endpoints) error {
|
|
rcsw.updateReadiness(endpoints)
|
|
_, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(endpoints.Namespace).Update(ctx, endpoints, metav1.UpdateOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update mirror endpoints for %s/%s: %w", endpoints.Namespace, endpoints.Name, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) updateReadiness(endpoints *corev1.Endpoints) {
|
|
if !rcsw.gatewayAlive {
|
|
rcsw.log.Warnf("gateway for %s/%s does not have ready addresses; setting addresses to not ready", endpoints.Namespace, endpoints.Name)
|
|
for i := range endpoints.Subsets {
|
|
endpoints.Subsets[i].NotReadyAddresses = append(endpoints.Subsets[i].NotReadyAddresses, endpoints.Subsets[i].Addresses...)
|
|
endpoints.Subsets[i].Addresses = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) isExported(l map[string]string) bool {
|
|
// Treat an empty selector as "Nothing" instead of "Everything" so that
|
|
// when the selector field is unset, we don't export all Services.
|
|
if rcsw.link.Spec.Selector == nil {
|
|
return false
|
|
}
|
|
if len(rcsw.link.Spec.Selector.MatchExpressions)+len(rcsw.link.Spec.Selector.MatchLabels) == 0 {
|
|
return false
|
|
}
|
|
selector, err := metav1.LabelSelectorAsSelector(rcsw.link.Spec.Selector)
|
|
if err != nil {
|
|
rcsw.log.Errorf("Invalid selector: %s", err)
|
|
return false
|
|
}
|
|
return selector.Matches(labels.Set(l))
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) isRemoteDiscovery(l map[string]string) bool {
|
|
// Treat an empty remoteDiscoverySelector as "Nothing" instead of
|
|
// "Everything" so that when the remoteDiscoverySelector field is unset, we
|
|
// don't export all Services.
|
|
if rcsw.link.Spec.RemoteDiscoverySelector == nil {
|
|
return false
|
|
}
|
|
if len(rcsw.link.Spec.RemoteDiscoverySelector.MatchExpressions)+len(rcsw.link.Spec.RemoteDiscoverySelector.MatchLabels) == 0 {
|
|
return false
|
|
}
|
|
remoteDiscoverySelector, err := metav1.LabelSelectorAsSelector(rcsw.link.Spec.RemoteDiscoverySelector)
|
|
if err != nil {
|
|
rcsw.log.Errorf("Invalid selector: %s", err)
|
|
return false
|
|
}
|
|
|
|
return remoteDiscoverySelector.Matches(labels.Set(l))
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) isFederatedServiceMember(l map[string]string) bool {
|
|
// Treat an empty federatedServiceSelector as "Nothing" instead of
|
|
// "Everything" so that when the federatedServiceSelector field is unset, we
|
|
// don't export all Services.
|
|
if rcsw.link.Spec.FederatedServiceSelector == nil {
|
|
return false
|
|
}
|
|
if len(rcsw.link.Spec.FederatedServiceSelector.MatchExpressions)+len(rcsw.link.Spec.FederatedServiceSelector.MatchLabels) == 0 {
|
|
return false
|
|
}
|
|
federatedServiceSelector, err := metav1.LabelSelectorAsSelector(rcsw.link.Spec.FederatedServiceSelector)
|
|
if err != nil {
|
|
rcsw.log.Errorf("Invalid selector: %s", err)
|
|
return false
|
|
}
|
|
|
|
return federatedServiceSelector.Matches(labels.Set(l))
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) updateLinkMirrorStatus(remoteName, namespace string, condition v1alpha3.LinkCondition) {
|
|
if rcsw.link.Spec.TargetClusterName == "" {
|
|
// The local cluster has no Link resource.
|
|
return
|
|
}
|
|
link, err := rcsw.linksAPIClient.L5dClient.LinkV1alpha3().Links(rcsw.link.GetNamespace()).Get(context.Background(), rcsw.link.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
rcsw.log.Errorf("Failed to get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err)
|
|
return
|
|
}
|
|
link.Status.MirrorServices = updateServiceStatus(remoteName, namespace, condition, link.Status.MirrorServices)
|
|
rcsw.patchLinkStatus(link.Status)
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) updateLinkFederatedStatus(remoteName, namespace string, condition v1alpha3.LinkCondition) {
|
|
if rcsw.link.Spec.TargetClusterName == "" {
|
|
// The local cluster has no Link resource.
|
|
return
|
|
}
|
|
link, err := rcsw.linksAPIClient.L5dClient.LinkV1alpha3().Links(rcsw.link.GetNamespace()).Get(context.Background(), rcsw.link.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
rcsw.log.Errorf("Failed to get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err)
|
|
}
|
|
link.Status.FederatedServices = updateServiceStatus(remoteName, namespace, condition, link.Status.FederatedServices)
|
|
rcsw.patchLinkStatus(link.Status)
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) deleteLinkMirrorStatus(remoteName, namespace string) {
|
|
if rcsw.link.Spec.TargetClusterName == "" {
|
|
// The local cluster has no Link resource.
|
|
return
|
|
}
|
|
link, err := rcsw.linksAPIClient.L5dClient.LinkV1alpha3().Links(rcsw.link.GetNamespace()).Get(context.Background(), rcsw.link.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
rcsw.log.Errorf("Failed to get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err)
|
|
}
|
|
link.Status.MirrorServices = deleteServiceStatus(remoteName, namespace, link.Status.MirrorServices)
|
|
rcsw.patchLinkStatus(link.Status)
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) deleteLinkFederatedStatus(remoteName, namespace string) {
|
|
if rcsw.link.Spec.TargetClusterName == "" {
|
|
// The local cluster has no Link resource.
|
|
return
|
|
}
|
|
link, err := rcsw.linksAPIClient.L5dClient.LinkV1alpha3().Links(rcsw.link.GetNamespace()).Get(context.Background(), rcsw.link.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
rcsw.log.Errorf("Failed to get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err)
|
|
}
|
|
link.Status.FederatedServices = deleteServiceStatus(remoteName, namespace, link.Status.FederatedServices)
|
|
rcsw.patchLinkStatus(link.Status)
|
|
}
|
|
|
|
func (rcsw *RemoteClusterServiceWatcher) patchLinkStatus(status v1alpha3.LinkStatus) {
|
|
rcsw.log.Infof("patching link status %s/%s", rcsw.link.Namespace, rcsw.link.Name)
|
|
statusBytes, err := json.Marshal(status)
|
|
if err != nil {
|
|
rcsw.log.Errorf("Failed to marshal link status: %s", err)
|
|
}
|
|
_, err = rcsw.linksAPIClient.L5dClient.LinkV1alpha3().Links(rcsw.link.GetNamespace()).Patch(
|
|
context.Background(),
|
|
rcsw.link.Name,
|
|
types.MergePatchType,
|
|
[]byte(fmt.Sprintf(`{"status": %s}`, string(statusBytes))),
|
|
metav1.PatchOptions{},
|
|
"status",
|
|
)
|
|
if err != nil {
|
|
rcsw.log.Errorf("Failed to patch link status %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err)
|
|
}
|
|
}
|
|
|
|
func updateServiceStatus(remoteName, namespace string, condition v1alpha3.LinkCondition, statuses []v1alpha3.ServiceStatus) []v1alpha3.ServiceStatus {
|
|
foundStatus := false
|
|
for i, status := range statuses {
|
|
if status.RemoteRef.Name == remoteName && status.RemoteRef.Namespace == namespace {
|
|
foundStatus = true
|
|
status.Conditions = []v1alpha3.LinkCondition{condition}
|
|
statuses[i] = status
|
|
}
|
|
}
|
|
if !foundStatus {
|
|
statuses = append(statuses, v1alpha3.ServiceStatus{
|
|
ControllerName: "linkerd.io/service-mirror",
|
|
RemoteRef: v1alpha3.ObjectRef{
|
|
Name: remoteName,
|
|
Namespace: namespace,
|
|
Kind: "Service",
|
|
Group: corev1.GroupName,
|
|
},
|
|
Conditions: []v1alpha3.LinkCondition{condition},
|
|
})
|
|
}
|
|
return statuses
|
|
}
|
|
|
|
func deleteServiceStatus(remoteName, namespace string, statuses []v1alpha3.ServiceStatus) []v1alpha3.ServiceStatus {
|
|
newStatuses := make([]v1alpha3.ServiceStatus, 0)
|
|
for _, status := range statuses {
|
|
if status.RemoteRef.Name == remoteName && status.RemoteRef.Namespace == namespace {
|
|
continue
|
|
}
|
|
newStatuses = append(newStatuses, status)
|
|
}
|
|
return newStatuses
|
|
}
|
|
|
|
func mirrorStatusCondition(success bool, reason string, message string, localRef *corev1.Service) v1alpha3.LinkCondition {
|
|
status := metav1.ConditionTrue
|
|
if !success {
|
|
status = metav1.ConditionFalse
|
|
}
|
|
condition := v1alpha3.LinkCondition{
|
|
LastTransitionTime: metav1.Now(),
|
|
Message: message,
|
|
Reason: reason,
|
|
Status: status,
|
|
Type: "Mirrored",
|
|
}
|
|
if localRef != nil {
|
|
condition.LocalRef = &v1alpha3.ObjectRef{
|
|
Name: localRef.Name,
|
|
Namespace: localRef.Namespace,
|
|
Kind: "Service",
|
|
Group: corev1.GroupName,
|
|
}
|
|
}
|
|
return condition
|
|
}
|