diff --git a/go.mod b/go.mod index 7fb2f1f92..92e9e77af 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/IBM/platform-services-go-sdk v0.55.0 github.com/aws/aws-sdk-go v1.50.35 github.com/davecgh/go-spew v1.1.1 + github.com/fsnotify/fsnotify v1.7.0 github.com/ghodss/yaml v1.0.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-cmp v0.6.0 @@ -80,7 +81,6 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/errors v0.20.3 // indirect diff --git a/pkg/filewatcher/filewatcher.go b/pkg/filewatcher/filewatcher.go new file mode 100644 index 000000000..9c4e8d144 --- /dev/null +++ b/pkg/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 +} diff --git a/pkg/storage/azure/azure.go b/pkg/storage/azure/azure.go index 9f9a1b349..385eba4ce 100644 --- a/pkg/storage/azure/azure.go +++ b/pkg/storage/azure/azure.go @@ -38,6 +38,7 @@ import ( regopclient "github.com/openshift/cluster-image-registry-operator/pkg/client" "github.com/openshift/cluster-image-registry-operator/pkg/defaults" "github.com/openshift/cluster-image-registry-operator/pkg/envvar" + "github.com/openshift/cluster-image-registry-operator/pkg/filewatcher" "github.com/openshift/cluster-image-registry-operator/pkg/storage/azure/azureclient" "github.com/openshift/cluster-image-registry-operator/pkg/storage/util" ) @@ -370,17 +371,37 @@ func (d *driver) storageAccountsClient(cfg *Azure, environment autorestazure.Env cred azcore.TokenCredential err error ) - // MSI Override for ARO HCP - msi := os.Getenv("AZURE_MSI_AUTHENTICATION") - if msi == "true" { - options := azidentity.ManagedIdentityCredentialOptions{ + // Managed Identity Override for ARO HCP + managedIdentityClientID := os.Getenv("ARO_HCP_MI_CLIENT_ID") + if managedIdentityClientID != "" { + klog.V(2).Info("Using client certification Azure authentication for ARO HCP") + options := &azidentity.ClientCertificateCredentialOptions{ ClientOptions: azcore.ClientOptions{ Cloud: cloudConfig, }, + SendCertificateChain: true, } - var err error - cred, err = azidentity.NewManagedIdentityCredential(&options) + tenantID := os.Getenv("ARO_HCP_TENANT_ID") + certPath := os.Getenv("ARO_HCP_CLIENT_CERTIFICATE_PATH") + + certData, err := os.ReadFile(certPath) + if err != nil { + return storage.AccountsClient{}, fmt.Errorf(`failed to read certificate file "%s": %v`, certPath, err) + } + + certs, key, err := azidentity.ParseCertificates(certData, []byte{}) + if err != nil { + return storage.AccountsClient{}, fmt.Errorf(`failed to parse certificate data "%s": %v`, certPath, err) + } + + // Watch the certificate for changes; if the certificate changes, the pod will be restarted + err = filewatcher.WatchFileForChanges(certPath) + if err != nil { + return storage.AccountsClient{}, err + } + + cred, err = azidentity.NewClientCertificateCredential(tenantID, managedIdentityClientID, certs, key, options) if err != nil { return storage.AccountsClient{}, err } diff --git a/pkg/storage/azure/azureclient/azureclient.go b/pkg/storage/azure/azureclient/azureclient.go index 284eb148a..d5d8bd5fa 100644 --- a/pkg/storage/azure/azureclient/azureclient.go +++ b/pkg/storage/azure/azureclient/azureclient.go @@ -21,6 +21,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container" autorestazure "github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/to" + "github.com/openshift/cluster-image-registry-operator/pkg/filewatcher" + "k8s.io/klog/v2" ) const ( @@ -105,13 +107,34 @@ func (c *Client) getCreds() (azcore.TokenCredential, error) { // Managed Identity Override for ARO HCP managedIdentityClientID := os.Getenv("ARO_HCP_MI_CLIENT_ID") if managedIdentityClientID != "" { - options := azidentity.ManagedIdentityCredentialOptions{ + klog.V(2).Info("Using client certification Azure authentication for ARO HCP") + options := &azidentity.ClientCertificateCredentialOptions{ ClientOptions: azcore.ClientOptions{ Cloud: c.clientOpts.Cloud, }, - ID: azidentity.ClientID(managedIdentityClientID), + SendCertificateChain: true, } - creds, err = azidentity.NewManagedIdentityCredential(&options) + + tenantID := os.Getenv("ARO_HCP_TENANT_ID") + certPath := os.Getenv("ARO_HCP_CLIENT_CERTIFICATE_PATH") + + certData, err := os.ReadFile(certPath) + if err != nil { + return nil, fmt.Errorf(`failed to read certificate file "%s": %v`, certPath, err) + } + + certs, key, err := azidentity.ParseCertificates(certData, []byte{}) + if err != nil { + return nil, fmt.Errorf(`failed to parse certificate data "%s": %v`, certPath, err) + } + + // Watch the certificate for changes; if the certificate changes, the pod will be restarted + err = filewatcher.WatchFileForChanges(certPath) + if err != nil { + return nil, err + } + + creds, err = azidentity.NewClientCertificateCredential(tenantID, managedIdentityClientID, certs, key, options) if err != nil { return nil, err }