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 }