diff --git a/go.mod b/go.mod index da0ef1f..0e3e701 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.1.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 github.com/aquilax/truncate v1.0.0 github.com/aws/aws-sdk-go-v2 v1.32.5 diff --git a/go.sum b/go.sum index 916a721..fcde27b 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsI github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.1.0 h1:Fd+iaEa+JBwzYo6OTWYSNqyvlPSLciMGsmsnYCKcXM0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.1.0/go.mod h1:ulHyBFJOI0ONiRL4vcJTmS7rx18jQQlEPmAgo80cRdM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c= diff --git a/internal/pkg/service/objectstorage/cloud/azure/container.go b/internal/pkg/service/objectstorage/cloud/azure/container.go new file mode 100644 index 0000000..9cea173 --- /dev/null +++ b/internal/pkg/service/objectstorage/cloud/azure/container.go @@ -0,0 +1,85 @@ +package azure + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage" + + "github.com/giantswarm/object-storage-operator/api/v1alpha1" +) + +func (s AzureObjectStorageAdapter) existsContainer(ctx context.Context, bucket *v1alpha1.Bucket, storageAccountName string) (bool, error) { + // Check BlobContainer name exists in StorageAccount + _, err := s.blobContainerClient.Get( + ctx, + s.cluster.GetResourceGroup(), + storageAccountName, + bucket.Spec.Name, + nil, + ) + + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) { + // If NOT FOUND error, that means the BlobContainer doesn't exist, so we return false + if respErr.StatusCode == http.StatusNotFound { + return false, nil + } + } + return false, err + } + return true, nil +} + +func (s AzureObjectStorageAdapter) upsertContainer(ctx context.Context, bucket *v1alpha1.Bucket, storageAccountName string) error { + existsContainer, err := s.existsContainer(ctx, bucket, storageAccountName) + if err != nil { + return err + } + if !existsContainer { + // Create Storage Container + _, err := s.blobContainerClient.Create( + ctx, + s.cluster.GetResourceGroup(), + storageAccountName, + bucket.Spec.Name, + armstorage.BlobContainer{ + ContainerProperties: &armstorage.ContainerProperties{ + PublicAccess: to.Ptr(armstorage.PublicAccessNone), + Metadata: s.getBucketTags(bucket), + }, + }, + nil, + ) + if err != nil { + s.logger.Error(err, fmt.Sprintf("failed to create storage container %s", bucket.Spec.Name)) + return err + } + s.logger.Info(fmt.Sprintf("storage container %s created", bucket.Spec.Name)) + } else { + _, err := s.blobContainerClient.Update( + ctx, + s.cluster.GetResourceGroup(), + storageAccountName, + bucket.Spec.Name, + armstorage.BlobContainer{ + ContainerProperties: &armstorage.ContainerProperties{ + Metadata: s.getBucketTags(bucket), + }, + }, + nil, + ) + if err != nil { + s.logger.Error(err, fmt.Sprintf("failed to update storage container %s", bucket.Spec.Name)) + return err + } + s.logger.Info(fmt.Sprintf("storage container %s updated", bucket.Spec.Name)) + } + + return nil +} diff --git a/internal/pkg/service/objectstorage/cloud/azure/privateendpoint.go b/internal/pkg/service/objectstorage/cloud/azure/privateendpoint.go new file mode 100644 index 0000000..e2443cb --- /dev/null +++ b/internal/pkg/service/objectstorage/cloud/azure/privateendpoint.go @@ -0,0 +1,106 @@ +package azure + +import ( + "context" + "fmt" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" + + "github.com/giantswarm/object-storage-operator/api/v1alpha1" +) + +var subnetID = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s/subnets/%s" + +func (s AzureObjectStorageAdapter) upsertPrivateEndpoint(ctx context.Context, bucket *v1alpha1.Bucket, storageAccountName string) (*armnetwork.PrivateEndpoint, error) { + // Create or Update Private endpoint + pollersResp, err := s.privateEndpointsClient.BeginCreateOrUpdate( + ctx, + s.cluster.GetResourceGroup(), + bucket.Spec.Name, + armnetwork.PrivateEndpoint{ + Location: to.Ptr(s.cluster.GetRegion()), + Properties: &armnetwork.PrivateEndpointProperties{ + CustomNetworkInterfaceName: to.Ptr(fmt.Sprintf("%s-nodes-nic", bucket.Spec.Name)), + PrivateLinkServiceConnections: []*armnetwork.PrivateLinkServiceConnection{ + { + Name: to.Ptr(bucket.Spec.Name), + Properties: &armnetwork.PrivateLinkServiceConnectionProperties{ + PrivateLinkServiceID: to.Ptr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts/%s", s.cluster.GetSubscriptionID(), s.cluster.GetResourceGroup(), storageAccountName)), + GroupIDs: []*string{to.Ptr("blob")}, + }, + }, + }, + Subnet: &armnetwork.Subnet{ + ID: to.Ptr(s.subnetID()), + }, + }, + Tags: s.getBucketTags(bucket), + }, + nil, + ) + + if err != nil { + return nil, err + } + resp, err := pollersResp.PollUntilDone(ctx, nil) + if err != nil { + return nil, err + } + + return &resp.PrivateEndpoint, nil +} + +func (s AzureObjectStorageAdapter) upsertPrivateZone(ctx context.Context, bucket *v1alpha1.Bucket) (*armprivatedns.PrivateZone, error) { + pollersResp, err := s.privateZonesClient.BeginCreateOrUpdate( + ctx, + s.cluster.GetResourceGroup(), + bucket.Spec.Name, + armprivatedns.PrivateZone{ + Location: to.Ptr(s.cluster.GetRegion()), + Tags: s.getBucketTags(bucket), + }, + nil, + ) + if err != nil { + return nil, err + } + resp, err := pollersResp.PollUntilDone(ctx, nil) + if err != nil { + return nil, err + } + return &resp.PrivateZone, nil +} + +func (s AzureObjectStorageAdapter) upsertVirtualNetworkLink(ctx context.Context, bucket *v1alpha1.Bucket) (*armprivatedns.VirtualNetworkLink, error) { + pollersResp, err := s.virtualNetworkLinksClient.BeginCreateOrUpdate( + ctx, + s.cluster.GetResourceGroup(), + bucket.Spec.Name, + bucket.Spec.Name, + armprivatedns.VirtualNetworkLink{ + Location: to.Ptr(s.cluster.GetRegion()), + Properties: &armprivatedns.VirtualNetworkLinkProperties{ + RegistrationEnabled: to.Ptr(true), + VirtualNetwork: &armprivatedns.SubResource{ + ID: to.Ptr(s.subnetID()), + }, + }, + Tags: s.getBucketTags(bucket), + }, + nil, + ) + if err != nil { + return nil, err + } + resp, err := pollersResp.PollUntilDone(ctx, nil) + if err != nil { + return nil, err + } + return &resp.VirtualNetworkLink, nil +} + +func (s AzureObjectStorageAdapter) subnetID() string { + return fmt.Sprintf(subnetID, s.cluster.GetSubscriptionID(), s.cluster.GetResourceGroup(), s.cluster.GetVNetName(), "node-subnet") +} diff --git a/internal/pkg/service/objectstorage/cloud/azure/service.go b/internal/pkg/service/objectstorage/cloud/azure/service.go index 367e302..80c1a64 100644 --- a/internal/pkg/service/objectstorage/cloud/azure/service.go +++ b/internal/pkg/service/objectstorage/cloud/azure/service.go @@ -7,6 +7,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage" "github.com/go-logr/logr" "github.com/pkg/errors" @@ -64,6 +65,12 @@ func (s AzureObjectStorageService) NewObjectStorageService(ctx context.Context, return nil, errors.WithStack(err) } + var privateZonesClientFactory *armprivatedns.ClientFactory + privateZonesClientFactory, err = armprivatedns.NewClientFactory(azureCredentials.SubscriptionID, cred, nil) + if err != nil { + return nil, errors.WithStack(err) + } + azurecluster, ok := cluster.(AzureCluster) if !ok { return nil, errors.New("Impossible to cast cluster into Azure cluster") @@ -73,6 +80,8 @@ func (s AzureObjectStorageService) NewObjectStorageService(ctx context.Context, storageClientFactory.NewBlobContainersClient(), storageClientFactory.NewManagementPoliciesClient(), networkClientFactory.NewPrivateEndpointsClient(), + privateZonesClientFactory.NewPrivateZonesClient(), + privateZonesClientFactory.NewVirtualNetworkLinksClient(), logger, azurecluster, client, diff --git a/internal/pkg/service/objectstorage/cloud/azure/storage.go b/internal/pkg/service/objectstorage/cloud/azure/storage.go index a2c1e1e..7577492 100644 --- a/internal/pkg/service/objectstorage/cloud/azure/storage.go +++ b/internal/pkg/service/objectstorage/cloud/azure/storage.go @@ -2,18 +2,12 @@ package azure import ( "context" - "errors" "fmt" - "net/http" - "strings" - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage" - "github.com/aquilax/truncate" "github.com/go-logr/logr" - sanitize "github.com/mrz1836/go-sanitize" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -28,13 +22,15 @@ const ( ) type AzureObjectStorageAdapter struct { - storageAccountClient *armstorage.AccountsClient - blobContainerClient *armstorage.BlobContainersClient - managementPoliciesClient *armstorage.ManagementPoliciesClient - privateEndpointsClient *armnetwork.PrivateEndpointsClient - logger logr.Logger - cluster AzureCluster - client client.Client + storageAccountClient *armstorage.AccountsClient + blobContainerClient *armstorage.BlobContainersClient + managementPoliciesClient *armstorage.ManagementPoliciesClient + privateEndpointsClient *armnetwork.PrivateEndpointsClient + privateZonesClient *armprivatedns.PrivateZonesClient + virtualNetworkLinksClient *armprivatedns.VirtualNetworkLinksClient + logger logr.Logger + cluster AzureCluster + client client.Client } // NewAzureStorageService creates a new instance of AzureObjectStorageAdapter. @@ -48,17 +44,21 @@ func NewAzureStorageService( blobContainerClient *armstorage.BlobContainersClient, managementPoliciesClient *armstorage.ManagementPoliciesClient, privateEndpointsClient *armnetwork.PrivateEndpointsClient, + privateZonesClient *armprivatedns.PrivateZonesClient, + virtualNetworkLinksClient *armprivatedns.VirtualNetworkLinksClient, logger logr.Logger, cluster AzureCluster, client client.Client) AzureObjectStorageAdapter { return AzureObjectStorageAdapter{ - storageAccountClient: storageAccountClient, - blobContainerClient: blobContainerClient, - managementPoliciesClient: managementPoliciesClient, - privateEndpointsClient: privateEndpointsClient, - logger: logger, - cluster: cluster, - client: client, + storageAccountClient: storageAccountClient, + blobContainerClient: blobContainerClient, + managementPoliciesClient: managementPoliciesClient, + privateEndpointsClient: privateEndpointsClient, + privateZonesClient: privateZonesClient, + virtualNetworkLinksClient: virtualNetworkLinksClient, + logger: logger, + cluster: cluster, + client: client, } } @@ -68,8 +68,10 @@ func NewAzureStorageService( // If the storage account exists, it then checks if the BlobContainer with the specified name exists in the storage account. // If the BlobContainer does not exist, it returns false. Otherwise, it returns true. func (s AzureObjectStorageAdapter) ExistsBucket(ctx context.Context, bucket *v1alpha1.Bucket) (bool, error) { + storageAccountName := sanitizeStorageAccountName(bucket.Spec.Name) + // Check if storage account exists on Azure - existsStorageAccount, err := s.existsStorageAccount(ctx, bucket.Spec.Name) + existsStorageAccount, err := s.existsStorageAccount(ctx, storageAccountName) if err != nil { return false, err } @@ -77,41 +79,8 @@ func (s AzureObjectStorageAdapter) ExistsBucket(ctx context.Context, bucket *v1a if !existsStorageAccount { return false, nil } - // Check BlobContainer name exists in StorageAccount - _, err = s.blobContainerClient.Get( - ctx, - s.cluster.GetResourceGroup(), - s.getStorageAccountName(bucket.Spec.Name), - bucket.Spec.Name, - nil) - if err != nil { - var respErr *azcore.ResponseError - if errors.As(err, &respErr) { - // If NOT FOUND error, that means the BlobContainer doesn't exist, so we return false - if respErr.StatusCode == http.StatusNotFound { - return false, nil - } - } - return false, err - } - return true, nil -} - -// existsStorageAccount checks if a storage account exists for the given bucket name in Azure Object Storage. -// It returns a boolean indicating whether the storage account exists or not, along with any error encountered. -func (s AzureObjectStorageAdapter) existsStorageAccount(ctx context.Context, bucketName string) (bool, error) { - availability, err := s.storageAccountClient.CheckNameAvailability( - ctx, - armstorage.AccountCheckNameAvailabilityParameters{ - Name: to.Ptr(s.getStorageAccountName(bucketName)), - Type: to.Ptr("Microsoft.Storage/storageAccounts"), - }, - nil) - if err != nil { - return false, err - } - return !*availability.NameAvailable, nil + return s.existsContainer(ctx, bucket, storageAccountName) } // CreateBucket creates the Storage Account if it not exists AND the Storage Container @@ -122,105 +91,30 @@ func (s AzureObjectStorageAdapter) existsStorageAccount(ctx context.Context, buc // The Secret is created in the same namespace as the bucket. // The function returns an error if any of the operations fail. func (s AzureObjectStorageAdapter) CreateBucket(ctx context.Context, bucket *v1alpha1.Bucket) error { - storageAccountName := s.getStorageAccountName(bucket.Spec.Name) - // Check if Storage Account exists on Azure - existsStorageAccount, err := s.existsStorageAccount(ctx, storageAccountName) - if err != nil { - return err - } + storageAccountName := sanitizeStorageAccountName(bucket.Spec.Name) - // Create or Update Storage Account - pollerStorageAccount, err := s.storageAccountClient.BeginCreate( - ctx, - s.cluster.GetResourceGroup(), - storageAccountName, - armstorage.AccountCreateParameters{ - Kind: to.Ptr(armstorage.KindBlobStorage), - SKU: &armstorage.SKU{ - Name: to.Ptr(armstorage.SKUNameStandardLRS), - }, - Location: to.Ptr(s.cluster.GetRegion()), - Properties: &armstorage.AccountPropertiesCreateParameters{ - AllowSharedKeyAccess: to.Ptr(true), - AccessTier: to.Ptr(armstorage.AccessTierHot), - Encryption: &armstorage.Encryption{ - Services: &armstorage.EncryptionServices{ - Blob: &armstorage.EncryptionService{ - KeyType: to.Ptr(armstorage.KeyTypeAccount), - Enabled: to.Ptr(true), - }, - }, - KeySource: to.Ptr(armstorage.KeySourceMicrosoftStorage), - }, - EnableHTTPSTrafficOnly: to.Ptr(true), - // TODO make sure this is not the case on gaggle or public installations - PublicNetworkAccess: to.Ptr(armstorage.PublicNetworkAccessDisabled), - }, - }, nil) - if err != nil { - return err - } - _, err = pollerStorageAccount.PollUntilDone(ctx, nil) - if err != nil { + if err := s.upsertStorageAccount(ctx, bucket, storageAccountName); err != nil { return err } - if !existsStorageAccount { - s.logger.Info(fmt.Sprintf("Storage Account %s created", storageAccountName)) - } else { - s.logger.Info(fmt.Sprintf("Storage Account %s updated", storageAccountName)) + // TODO make sure this is not the case on public installations + if _, err := s.upsertPrivateZone(ctx, bucket); err != nil { + return err } - // Create or Update Private endpoint - blobGroupID := "blob" - pollerPrivateEndpoint, err := s.privateEndpointsClient.BeginCreateOrUpdate( - ctx, - s.cluster.GetResourceGroup(), - bucket.Spec.Name, - armnetwork.PrivateEndpoint{ - Location: to.Ptr(s.cluster.GetRegion()), - Properties: &armnetwork.PrivateEndpointProperties{ - CustomNetworkInterfaceName: to.Ptr(fmt.Sprintf("%s-nodes-nic", bucket.Spec.Name)), - PrivateLinkServiceConnections: []*armnetwork.PrivateLinkServiceConnection{ - { - Name: to.Ptr(bucket.Spec.Name), - Properties: &armnetwork.PrivateLinkServiceConnectionProperties{ - PrivateLinkServiceID: to.Ptr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts/%s", s.cluster.GetSubscriptionID(), s.cluster.GetResourceGroup(), storageAccountName)), - GroupIDs: []*string{&blobGroupID}, - }, - }, - }, - Subnet: &armnetwork.Subnet{ - ID: to.Ptr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s/subnets/%s", s.cluster.GetSubscriptionID(), s.cluster.GetResourceGroup(), s.cluster.GetVNetName(), "node-subnet")), - }, - }, - Tags: s.getBucketTags(bucket), - }, nil) - if err != nil { + // TODO make sure this is not the case on public installations + if _, err := s.upsertVirtualNetworkLink(ctx, bucket); err != nil { return err } - _, err = pollerPrivateEndpoint.PollUntilDone(ctx, nil) - if err != nil { + + // TODO make sure this is not the case on public installations + if _, err := s.upsertPrivateEndpoint(ctx, bucket, storageAccountName); err != nil { return err } - // Create Storage Container - _, err = s.blobContainerClient.Create( - ctx, - s.cluster.GetResourceGroup(), - storageAccountName, - bucket.Spec.Name, - armstorage.BlobContainer{ - ContainerProperties: &armstorage.ContainerProperties{ - PublicAccess: to.Ptr(armstorage.PublicAccessNone), - }, - }, - nil, - ) - if err != nil { + if err := s.upsertContainer(ctx, bucket, storageAccountName); err != nil { return err } - s.logger.Info(fmt.Sprintf("Storage Container %s created", bucket.Spec.Name)) // Create a K8S Secret to store Storage Account Access Key // First, we retrieve Storage Account Access Key on Azure @@ -267,7 +161,7 @@ func (s AzureObjectStorageAdapter) CreateBucket(ctx context.Context, bucket *v1a return err } - s.logger.Info(fmt.Sprintf("created secret %s", bucket.Spec.Name)) + s.logger.Info(fmt.Sprintf("upserted secret %s", bucket.Spec.Name)) return nil } @@ -280,23 +174,15 @@ func (s AzureObjectStorageAdapter) UpdateBucket(ctx context.Context, bucket *v1a // Here, we decided to have a Storage Account dedicated to a Storage Container (relation 1 - 1) // We want to prevent the Storage Account from being used by anyone func (s AzureObjectStorageAdapter) DeleteBucket(ctx context.Context, bucket *v1alpha1.Bucket) error { - storageAccountName := s.getStorageAccountName(bucket.Spec.Name) - // We delete the Storage Account, which delete the Storage Container - _, err := s.storageAccountClient.Delete( - ctx, - s.cluster.GetResourceGroup(), - storageAccountName, - nil, - ) - if err != nil { - s.logger.Error(err, fmt.Sprintf("Error deleting Storage Account %s and Storage Container %s", storageAccountName, bucket.Spec.Name)) + storageAccountName := sanitizeStorageAccountName(bucket.Spec.Name) + + if err := s.deleteStorageAccount(ctx, bucket, storageAccountName); err != nil { return err } - s.logger.Info(fmt.Sprintf("Storage Account %s and Storage Container %s deleted", storageAccountName, bucket.Spec.Name)) // We delete the Azure Credentials secret var secret = v1.Secret{} - err = s.client.Get( + err := s.client.Get( ctx, types.NamespacedName{ Name: bucket.Spec.Name, @@ -328,135 +214,5 @@ func (s AzureObjectStorageAdapter) DeleteBucket(ctx context.Context, bucket *v1a // ConfigureBucket set lifecycle rules (expiration on blob) and tags on the Storage Container func (s AzureObjectStorageAdapter) ConfigureBucket(ctx context.Context, bucket *v1alpha1.Bucket) error { - var err error - err = s.setLifecycleRules(ctx, bucket) - if err != nil { - return err - } - - err = s.setTags(ctx, bucket) - return err -} - -// setLifecycleRules set a lifecycle rule on the Storage Account to delete Blobs older than X days -func (s AzureObjectStorageAdapter) setLifecycleRules(ctx context.Context, bucket *v1alpha1.Bucket) error { - storageAccountName := s.getStorageAccountName(bucket.Spec.Name) - if bucket.Spec.ExpirationPolicy != nil { - _, err := s.managementPoliciesClient.CreateOrUpdate( - ctx, - s.cluster.GetResourceGroup(), - storageAccountName, - armstorage.ManagementPolicyNameDefault, - armstorage.ManagementPolicy{ - Properties: &armstorage.ManagementPolicyProperties{ - Policy: &armstorage.ManagementPolicySchema{ - Rules: []*armstorage.ManagementPolicyRule{ - { - Enabled: to.Ptr(true), - Name: to.Ptr(LifecycleRuleName), - Type: to.Ptr(armstorage.RuleTypeLifecycle), - Definition: &armstorage.ManagementPolicyDefinition{ - Actions: &armstorage.ManagementPolicyAction{ - BaseBlob: &armstorage.ManagementPolicyBaseBlob{ - Delete: &armstorage.DateAfterModification{ - DaysAfterModificationGreaterThan: to.Ptr[float32](float32(bucket.Spec.ExpirationPolicy.Days)), - }, - }, - }, - Filters: &armstorage.ManagementPolicyFilter{ - BlobTypes: []*string{ - to.Ptr("blockBlob"), - }, - }, - }, - }, - }, - }, - }, - }, - nil, - ) - if err != nil { - s.logger.Error(err, fmt.Sprintf("Error creating/updating Policy Rule for Storage Account %s", storageAccountName)) - } - return err - } - - // No Lifecycle Policy defines in the bucket CR, we delete it in the Storage Account - _, err := s.managementPoliciesClient.Delete( - ctx, - s.cluster.GetResourceGroup(), - storageAccountName, - LifecycleRuleName, - nil, - ) - if err != nil { - var respErr *azcore.ResponseError - if errors.As(err, &respErr) { - // If the Lifecycle policy does not exists, it's not an error - if respErr.StatusCode == http.StatusNotFound { - return nil - } - } - } - return err -} - -func sanitizeTagKey(tagName string) string { - return strings.ReplaceAll(tagName, "-", "_") -} - -func (s AzureObjectStorageAdapter) getBucketTags(bucket *v1alpha1.Bucket) map[string]*string { - tags := make(map[string]*string) - for _, t := range bucket.Spec.Tags { - // We use this to avoid pointer issues in range loops. - tag := t - if tag.Key != "" && tag.Value != "" { - tags[sanitizeTagKey(tag.Key)] = &tag.Value - } - } - for k, v := range s.cluster.GetTags() { - // We use this to avoid pointer issues in range loops. - key := k - value := v - if key != "" && value != "" { - tags[sanitizeTagKey(key)] = &value - } - } - return tags -} - -// setTags set cluster additionalTags and bucket tags into Storage Container Metadata -func (s AzureObjectStorageAdapter) setTags(ctx context.Context, bucket *v1alpha1.Bucket) error { - storageAccountName := s.getStorageAccountName(bucket.Spec.Name) - - // Updating Storage Container Metadata with tags (cluster additionalTags + Bucket tags) - _, err := s.blobContainerClient.Update( - ctx, - s.cluster.GetResourceGroup(), - storageAccountName, - bucket.Spec.Name, - armstorage.BlobContainer{ - ContainerProperties: &armstorage.ContainerProperties{ - Metadata: s.getBucketTags(bucket), - }, - }, - nil, - ) - if err != nil { - s.logger.Error(err, fmt.Sprintf("Error updating Storage Container %s Metadata", bucket.Spec.Name)) - } - return err -} - -// getStorageAccountName returns the storage account name for the given bucket name. -// It sanitizes the bucket name and returns it. -func (s *AzureObjectStorageAdapter) getStorageAccountName(bucketName string) string { - return sanitizeAlphanumeric24(bucketName) -} - -// sanitizeAlphanumeric24 sanitizes the given name by removing any non-alphanumeric characters and truncating it to a maximum length of 24 characters. -// more details https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/get-properties?view=rest-storagerp-2023-01-01&tabs=HTTP#uri-parameters -func sanitizeAlphanumeric24(name string) string { - return truncate.Truncate(sanitize.AlphaNumeric(name, false), 24, "", truncate.PositionEnd) + return s.setLifecycleRules(ctx, bucket) } diff --git a/internal/pkg/service/objectstorage/cloud/azure/storage_test.go b/internal/pkg/service/objectstorage/cloud/azure/storage_test.go index 5b9b954..782c01f 100644 --- a/internal/pkg/service/objectstorage/cloud/azure/storage_test.go +++ b/internal/pkg/service/objectstorage/cloud/azure/storage_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/go-cmp/cmp" ) -func Test_sanitizeAlphanumeric24(t *testing.T) { +func Test_sanitizeStorageAccountName(t *testing.T) { testCases := []struct { name string inputString string @@ -34,7 +34,7 @@ func Test_sanitizeAlphanumeric24(t *testing.T) { t.Run(strconv.Itoa(i), func(t *testing.T) { t.Log(tc.name) - storageAccountName := sanitizeAlphanumeric24(tc.inputString) + storageAccountName := sanitizeStorageAccountName(tc.inputString) if !cmp.Equal(storageAccountName, tc.expectedString) { t.Fatalf("\n\n%s\n", cmp.Diff(tc.expectedString, storageAccountName)) diff --git a/internal/pkg/service/objectstorage/cloud/azure/storageaccount.go b/internal/pkg/service/objectstorage/cloud/azure/storageaccount.go new file mode 100644 index 0000000..4ad376b --- /dev/null +++ b/internal/pkg/service/objectstorage/cloud/azure/storageaccount.go @@ -0,0 +1,172 @@ +package azure + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage" + "github.com/aquilax/truncate" + sanitize "github.com/mrz1836/go-sanitize" + + "github.com/giantswarm/object-storage-operator/api/v1alpha1" +) + +func (s AzureObjectStorageAdapter) upsertStorageAccount(ctx context.Context, bucket *v1alpha1.Bucket, storageAccountName string) error { + // Check if Storage Account exists on Azure + existsStorageAccount, err := s.existsStorageAccount(ctx, storageAccountName) + if err != nil { + return err + } + + // Create or Update Storage Account + pollerStorageAccount, err := s.storageAccountClient.BeginCreate( + ctx, + s.cluster.GetResourceGroup(), + storageAccountName, + armstorage.AccountCreateParameters{ + Kind: to.Ptr(armstorage.KindBlobStorage), + SKU: &armstorage.SKU{ + Name: to.Ptr(armstorage.SKUNameStandardLRS), + }, + Location: to.Ptr(s.cluster.GetRegion()), + Properties: &armstorage.AccountPropertiesCreateParameters{ + AllowSharedKeyAccess: to.Ptr(true), + AccessTier: to.Ptr(armstorage.AccessTierHot), + Encryption: &armstorage.Encryption{ + Services: &armstorage.EncryptionServices{ + Blob: &armstorage.EncryptionService{ + KeyType: to.Ptr(armstorage.KeyTypeAccount), + Enabled: to.Ptr(true), + }, + }, + KeySource: to.Ptr(armstorage.KeySourceMicrosoftStorage), + }, + EnableHTTPSTrafficOnly: to.Ptr(true), + MinimumTLSVersion: to.Ptr(armstorage.MinimumTLSVersionTLS12), + // TODO make sure this is not the case on gaggle or public installations + PublicNetworkAccess: to.Ptr(armstorage.PublicNetworkAccessDisabled), + }, + Tags: s.getBucketTags(bucket), + }, nil) + if err != nil { + return err + } + _, err = pollerStorageAccount.PollUntilDone(ctx, nil) + if err != nil { + return err + } + + if !existsStorageAccount { + s.logger.Info(fmt.Sprintf("Storage Account %s created", storageAccountName)) + } else { + s.logger.Info(fmt.Sprintf("Storage Account %s updated", storageAccountName)) + } + return nil +} + +func (s AzureObjectStorageAdapter) deleteStorageAccount(ctx context.Context, bucket *v1alpha1.Bucket, storageAccountName string) error { + // Delete Storage Account + // We delete the Storage Account, which delete the Storage Container + _, err := s.storageAccountClient.Delete( + ctx, + s.cluster.GetResourceGroup(), + storageAccountName, + nil, + ) + if err != nil { + s.logger.Error(err, fmt.Sprintf("Error deleting Storage Account %s and Storage Container %s", storageAccountName, bucket.Spec.Name)) + return err + } + s.logger.Info(fmt.Sprintf("Storage Account %s and Storage Container %s deleted", storageAccountName, bucket.Spec.Name)) + return nil +} + +// existsStorageAccount checks if a storage account exists for the given bucket name in Azure Object Storage. +// It returns a boolean indicating whether the storage account exists or not, along with any error encountered. +func (s AzureObjectStorageAdapter) existsStorageAccount(ctx context.Context, storageAccountName string) (bool, error) { + availability, err := s.storageAccountClient.CheckNameAvailability( + ctx, + armstorage.AccountCheckNameAvailabilityParameters{ + Name: to.Ptr(storageAccountName), + Type: to.Ptr("Microsoft.Storage/storageAccounts"), + }, + nil) + if err != nil { + return false, err + } + return !*availability.NameAvailable, nil +} + +// setLifecycleRules set a lifecycle rule on the Storage Account to delete Blobs older than X days +func (s AzureObjectStorageAdapter) setLifecycleRules(ctx context.Context, bucket *v1alpha1.Bucket) error { + storageAccountName := sanitizeStorageAccountName(bucket.Spec.Name) + if bucket.Spec.ExpirationPolicy != nil { + _, err := s.managementPoliciesClient.CreateOrUpdate( + ctx, + s.cluster.GetResourceGroup(), + storageAccountName, + armstorage.ManagementPolicyNameDefault, + armstorage.ManagementPolicy{ + Properties: &armstorage.ManagementPolicyProperties{ + Policy: &armstorage.ManagementPolicySchema{ + Rules: []*armstorage.ManagementPolicyRule{ + { + Enabled: to.Ptr(true), + Name: to.Ptr(LifecycleRuleName), + Type: to.Ptr(armstorage.RuleTypeLifecycle), + Definition: &armstorage.ManagementPolicyDefinition{ + Actions: &armstorage.ManagementPolicyAction{ + BaseBlob: &armstorage.ManagementPolicyBaseBlob{ + Delete: &armstorage.DateAfterModification{ + DaysAfterModificationGreaterThan: to.Ptr[float32](float32(bucket.Spec.ExpirationPolicy.Days)), + }, + }, + }, + Filters: &armstorage.ManagementPolicyFilter{ + BlobTypes: []*string{ + to.Ptr("blockBlob"), + }, + }, + }, + }, + }, + }, + }, + }, + nil, + ) + if err != nil { + s.logger.Error(err, fmt.Sprintf("Error creating/updating Policy Rule for Storage Account %s", storageAccountName)) + } + return err + } + + // No Lifecycle Policy defines in the bucket CR, we delete it in the Storage Account + _, err := s.managementPoliciesClient.Delete( + ctx, + s.cluster.GetResourceGroup(), + storageAccountName, + LifecycleRuleName, + nil, + ) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) { + // If the Lifecycle policy does not exists, it's not an error + if respErr.StatusCode == http.StatusNotFound { + return nil + } + } + } + return err +} + +// sanitizeStorageAccountName sanitizes the given name by removing any non-alphanumeric characters and truncating it to a maximum length of 24 characters. +// more details https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/get-properties?view=rest-storagerp-2023-01-01&tabs=HTTP#uri-parameters +func sanitizeStorageAccountName(name string) string { + return truncate.Truncate(sanitize.AlphaNumeric(name, false), 24, "", truncate.PositionEnd) +} diff --git a/internal/pkg/service/objectstorage/cloud/azure/tags.go b/internal/pkg/service/objectstorage/cloud/azure/tags.go new file mode 100644 index 0000000..a56da1d --- /dev/null +++ b/internal/pkg/service/objectstorage/cloud/azure/tags.go @@ -0,0 +1,31 @@ +package azure + +import ( + "strings" + + "github.com/giantswarm/object-storage-operator/api/v1alpha1" +) + +func sanitizeTagKey(tagName string) string { + return strings.ReplaceAll(tagName, "-", "_") +} + +func (s AzureObjectStorageAdapter) getBucketTags(bucket *v1alpha1.Bucket) map[string]*string { + tags := make(map[string]*string) + for _, t := range bucket.Spec.Tags { + // We use this to avoid pointer issues in range loops. + tag := t + if tag.Key != "" && tag.Value != "" { + tags[sanitizeTagKey(tag.Key)] = &tag.Value + } + } + for k, v := range s.cluster.GetTags() { + // We use this to avoid pointer issues in range loops. + key := k + value := v + if key != "" && value != "" { + tags[sanitizeTagKey(key)] = &value + } + } + return tags +}