github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/certloader/loader.go (about)

     1  package certloader
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/kyma-incubator/compass/components/director/pkg/cert"
    10  
    11  	"github.com/kyma-incubator/compass/components/director/pkg/kubernetes"
    12  	"github.com/kyma-incubator/compass/components/director/pkg/namespacedname"
    13  
    14  	"github.com/pkg/errors"
    15  
    16  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    17  	v1 "k8s.io/api/core/v1"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	"k8s.io/apimachinery/pkg/watch"
    20  )
    21  
    22  const certsListLoaderCorrelationID = "cert-loader-id"
    23  
    24  // Loader provide mechanism to load certificate data into in-memory storage
    25  type Loader interface {
    26  	Run(ctx context.Context)
    27  }
    28  
    29  // Manager is a kubernetes secret manager that has methods to work with secret resources
    30  //go:generate mockery --name=Manager --output=automock --outpkg=automock --case=underscore --disable-version-string
    31  type Manager interface {
    32  	Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
    33  }
    34  
    35  type certificateLoader struct {
    36  	config            Config
    37  	certCache         *certificateCache
    38  	secretManagers    []Manager
    39  	secretNames       []string
    40  	reconnectInterval time.Duration
    41  }
    42  
    43  // NewCertificateLoader creates new certificate loader which is responsible to watch a secret containing client certificate
    44  // and update in-memory cache with that certificate if there is any change
    45  func NewCertificateLoader(config Config, certCache *certificateCache, secretManagers []Manager, secretNames []string, reconnectInterval time.Duration) Loader {
    46  	return &certificateLoader{
    47  		config:            config,
    48  		certCache:         certCache,
    49  		secretManagers:    secretManagers,
    50  		secretNames:       secretNames,
    51  		reconnectInterval: reconnectInterval,
    52  	}
    53  }
    54  
    55  // StartCertLoader prepares and run certificate loader goroutine
    56  func StartCertLoader(ctx context.Context, certLoaderConfig Config) (Cache, error) {
    57  	parsedCertSecret, err := namespacedname.Parse(certLoaderConfig.ExternalClientCertSecret)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  
    62  	parsedExtSvcCertSecret, err := namespacedname.Parse(certLoaderConfig.ExtSvcClientCertSecret)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	kubeConfig := kubernetes.Config{}
    68  	k8sClientSet, err := kubernetes.NewKubernetesClientSet(ctx, kubeConfig.PollInterval, kubeConfig.PollTimeout, kubeConfig.Timeout)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	certCache := NewCertificateCache()
    74  	secretManagers := []Manager{k8sClientSet.CoreV1().Secrets(parsedCertSecret.Namespace), k8sClientSet.CoreV1().Secrets(parsedExtSvcCertSecret.Namespace)}
    75  	secretNames := []string{parsedCertSecret.Name, parsedExtSvcCertSecret.Name}
    76  
    77  	certLoader := NewCertificateLoader(certLoaderConfig, certCache, secretManagers, secretNames, time.Second)
    78  	go certLoader.Run(ctx)
    79  
    80  	return certCache, nil
    81  }
    82  
    83  // Run uses kubernetes watch mechanism to listen for resource changes and update certificate cache
    84  func (cl *certificateLoader) Run(ctx context.Context) {
    85  	entry := log.C(ctx)
    86  	entry = entry.WithField(log.FieldRequestID, certsListLoaderCorrelationID)
    87  	ctx = log.ContextWithLogger(ctx, entry)
    88  
    89  	cl.startKubeWatch(ctx)
    90  }
    91  
    92  func (cl *certificateLoader) startKubeWatch(ctx context.Context) {
    93  	for {
    94  		select {
    95  		case <-ctx.Done():
    96  			log.C(ctx).Info("Context cancelled, stopping certificate watcher...")
    97  			return
    98  		default:
    99  		}
   100  		log.C(ctx).Info("Starting certificate watchers for secret changes...")
   101  
   102  		wg := &sync.WaitGroup{}
   103  		for idx, manager := range cl.secretManagers {
   104  			wg.Add(1)
   105  
   106  			go func(manager Manager, idx int) {
   107  				defer wg.Done()
   108  
   109  				watcher, err := manager.Watch(ctx, metav1.ListOptions{
   110  					FieldSelector: "metadata.name=" + cl.secretNames[idx],
   111  					Watch:         true,
   112  				})
   113  
   114  				if err != nil {
   115  					log.C(ctx).WithError(err).Errorf("Could not initialize watcher. Sleep for %s and try again... %v", cl.reconnectInterval.String(), err)
   116  					time.Sleep(cl.reconnectInterval)
   117  					return
   118  				}
   119  				log.C(ctx).Info("Waiting for certificate secret events...")
   120  
   121  				cl.processEvents(ctx, watcher.ResultChan(), cl.secretNames[idx])
   122  
   123  				// Cleanup any allocated resources
   124  				watcher.Stop()
   125  				time.Sleep(cl.reconnectInterval)
   126  			}(manager, idx)
   127  		}
   128  
   129  		wg.Wait()
   130  	}
   131  }
   132  
   133  func (cl *certificateLoader) processEvents(ctx context.Context, events <-chan watch.Event, secretName string) {
   134  	for {
   135  		select {
   136  		case <-ctx.Done():
   137  			return
   138  		case ev, ok := <-events:
   139  			if !ok {
   140  				return
   141  			}
   142  			switch ev.Type {
   143  			case watch.Added:
   144  				fallthrough
   145  			case watch.Modified:
   146  				log.C(ctx).Info("Updating the cache with certificate data...")
   147  				secret, ok := ev.Object.(*v1.Secret)
   148  				if !ok {
   149  					log.C(ctx).Error("Unexpected error: object is not secret. Try again")
   150  					continue
   151  				}
   152  				tlsCert, err := parseCertificate(ctx, secret.Data, cl.config)
   153  				if err != nil {
   154  					log.C(ctx).WithError(err).Error("Fail during certificate parsing")
   155  				}
   156  				cl.certCache.put(secretName, tlsCert)
   157  			case watch.Deleted:
   158  				log.C(ctx).Info("Removing certificate secret data from cache...")
   159  				cl.certCache.put(secretName, nil)
   160  			case watch.Error:
   161  				log.C(ctx).Error("Error event is received, stop certificate secret watcher and try again...")
   162  				return
   163  			}
   164  		}
   165  	}
   166  }
   167  
   168  func parseCertificate(ctx context.Context, secretData map[string][]byte, config Config) (*tls.Certificate, error) {
   169  	log.C(ctx).Info("Parsing provided certificate data...")
   170  	certChainBytes, existsCertKey := secretData[config.ExternalClientCertCertKey]
   171  	privateKeyBytes, existsKeyKey := secretData[config.ExternalClientCertKeyKey]
   172  
   173  	if existsCertKey && existsKeyKey {
   174  		return cert.ParseCertificateBytes(certChainBytes, privateKeyBytes)
   175  	}
   176  
   177  	extSvcCertChainBytes, existsExtSvcCertKey := secretData[config.ExtSvcClientCertCertKey]
   178  	extSvcPrivateKeyBytes, existsExtSvcKeyKey := secretData[config.ExtSvcClientCertKeyKey]
   179  
   180  	if existsExtSvcCertKey && existsExtSvcKeyKey {
   181  		return cert.ParseCertificateBytes(extSvcCertChainBytes, extSvcPrivateKeyBytes)
   182  	}
   183  
   184  	return nil, errors.New("There is no certificate data provided")
   185  }