From e5a9fa20046c34c227dd688a5a06a3de12da6318 Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Mon, 11 Nov 2024 12:15:50 -0500 Subject: [PATCH] Add a file watcher for Azure client cert auth Add a file watcher, utilizing fsnotify, to monitor the Azure client certificate authentication. If the file changes, the pod will need to be restarted since Azure SDK doesn't support hotloading a new certificate yet. Signed-off-by: Bryan Cox --- azure/scope/identity.go | 8 ++++ util/filewatcher/filewatcher.go | 67 +++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 util/filewatcher/filewatcher.go diff --git a/azure/scope/identity.go b/azure/scope/identity.go index 194cc2465b8..292c1fbb0b8 100644 --- a/azure/scope/identity.go +++ b/azure/scope/identity.go @@ -20,6 +20,7 @@ import ( "context" "os" "reflect" + "sigs.k8s.io/cluster-api-provider-azure/util/filewatcher" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" @@ -145,6 +146,13 @@ func (p *AzureCredentialsProvider) GetTokenCredential(ctx context.Context, resou if err != nil { return nil, errors.Wrap(err, "failed to parse certificate data") } + + // Watch the certificate for changes; if the certificate changes, the pod will be restarted + err = filewatcher.WatchFileForChanges(p.Identity.Spec.CertPath) + if err != nil { + return nil, errors.Wrap(err, "failed to watch certificate file") + } + cred, authErr = azidentity.NewClientCertificateCredential(p.GetTenantID(), p.Identity.Spec.ClientID, certs, key, &azidentity.ClientCertificateCredentialOptions{ ClientOptions: azcore.ClientOptions{ TracingProvider: tracingProvider, diff --git a/util/filewatcher/filewatcher.go b/util/filewatcher/filewatcher.go new file mode 100644 index 00000000000..9c4e8d14459 --- /dev/null +++ b/util/filewatcher/filewatcher.go @@ -0,0 +1,67 @@ +package filewatcher + +import ( + "os" + "path/filepath" + "sync" + + "github.com/fsnotify/fsnotify" + "k8s.io/klog/v2" +) + +var watchCertificateFileOnce sync.Once + +// WatchFileForChanges watches the file, fileToWatch, for changes. If the file contents have changed, the pod this +// function is running on will be restarted. +func WatchFileForChanges(fileToWatch string) error { + var err error + + // This starts only one occurrence of the file watcher, which watches the file, fileToWatch. + watchCertificateFileOnce.Do(func() { + klog.Infof("Starting the file change watcher on file, %s", fileToWatch) + + // Update the file path to watch in case this is a symlink + fileToWatch, err = filepath.EvalSymlinks(fileToWatch) + if err != nil { + return + } + klog.Infof("Watching file, %s", fileToWatch) + + // Start the file watcher to monitor file changes + go func() { + err = checkForFileChanges(fileToWatch) + }() + }) + return err +} + +// checkForFileChanges starts a new file watcher. If the file is changed, the pod running this function will exit. +func checkForFileChanges(path string) error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + go func() { + for { + select { + case event, ok := <-watcher.Events: + if ok && (event.Has(fsnotify.Write) || event.Has(fsnotify.Chmod) || event.Has(fsnotify.Remove)) { + klog.Infof("file, %s, was modified, exiting...", event.Name) + os.Exit(0) + } + case err, ok := <-watcher.Errors: + if ok { + klog.Errorf("file watcher error: %v", err) + } + } + } + }() + + err = watcher.Add(path) + if err != nil { + return err + } + + return nil +}