From 81279ebad5496cfadcb4c031da9431d06fbd0fa2 Mon Sep 17 00:00:00 2001 From: Sourav Gupta <98318303+souravgupta-msft@users.noreply.github.com> Date: Thu, 22 Sep 2022 15:08:20 +0530 Subject: [PATCH] Container soft delete (#19136) Adding code for container soft delete --- sdk/storage/azblob/container/client.go | 17 ++++++ sdk/storage/azblob/container/models.go | 7 +++ sdk/storage/azblob/container/responses.go | 3 + sdk/storage/azblob/service/client.go | 8 +++ sdk/storage/azblob/service/client_test.go | 61 +++++++++++++++++++++ sdk/storage/azblob/service/examples_test.go | 34 ++++++++++++ sdk/storage/azblob/service/models.go | 3 + sdk/storage/azblob/service/responses.go | 3 + 8 files changed, 136 insertions(+) diff --git a/sdk/storage/azblob/container/client.go b/sdk/storage/azblob/container/client.go index ea579e62af5a..aa58acdce1b8 100644 --- a/sdk/storage/azblob/container/client.go +++ b/sdk/storage/azblob/container/client.go @@ -160,6 +160,23 @@ func (c *Client) Delete(ctx context.Context, options *DeleteOptions) (DeleteResp return resp, err } +// Restore operation restore the contents and properties of a soft deleted container to a specified container. +// For more information, see https://docs.microsoft.com/en-us/rest/api/storageservices/restore-container. +func (c *Client) Restore(ctx context.Context, deletedContainerVersion string, options *RestoreOptions) (RestoreResponse, error) { + urlParts, err := blob.ParseURL(c.URL()) + if err != nil { + return RestoreResponse{}, err + } + + opts := &generated.ContainerClientRestoreOptions{ + DeletedContainerName: &urlParts.ContainerName, + DeletedContainerVersion: &deletedContainerVersion, + } + resp, err := c.generated().Restore(ctx, opts) + + return resp, err +} + // GetProperties returns the container's properties. // For more information, see https://docs.microsoft.com/rest/api/storageservices/get-container-metadata. func (c *Client) GetProperties(ctx context.Context, o *GetPropertiesOptions) (GetPropertiesResponse, error) { diff --git a/sdk/storage/azblob/container/models.go b/sdk/storage/azblob/container/models.go index 971a10a3dbef..eb65d5fc3c80 100644 --- a/sdk/storage/azblob/container/models.go +++ b/sdk/storage/azblob/container/models.go @@ -84,6 +84,13 @@ func (o *DeleteOptions) format() (*generated.ContainerClientDeleteOptions, *gene // --------------------------------------------------------------------------------------------------------------------- +// RestoreOptions contains the optional parameters for the Client.Restore method. +type RestoreOptions struct { + // placeholder for future options +} + +// --------------------------------------------------------------------------------------------------------------------- + // GetPropertiesOptions contains the optional parameters for the ContainerClient.GetProperties method. type GetPropertiesOptions struct { LeaseAccessConditions *LeaseAccessConditions diff --git a/sdk/storage/azblob/container/responses.go b/sdk/storage/azblob/container/responses.go index 261ee643ca99..9d8672b135be 100644 --- a/sdk/storage/azblob/container/responses.go +++ b/sdk/storage/azblob/container/responses.go @@ -16,6 +16,9 @@ type CreateResponse = generated.ContainerClientCreateResponse // DeleteResponse contains the response from method Client.Delete. type DeleteResponse = generated.ContainerClientDeleteResponse +// RestoreResponse contains the response from method Client.Restore. +type RestoreResponse = generated.ContainerClientRestoreResponse + // GetPropertiesResponse contains the response from method Client.GetProperties. type GetPropertiesResponse = generated.ContainerClientGetPropertiesResponse diff --git a/sdk/storage/azblob/service/client.go b/sdk/storage/azblob/service/client.go index 892140193783..69740e47680d 100644 --- a/sdk/storage/azblob/service/client.go +++ b/sdk/storage/azblob/service/client.go @@ -123,6 +123,14 @@ func (s *Client) DeleteContainer(ctx context.Context, containerName string, opti return containerDeleteResp, err } +// RestoreContainer restores soft-deleted container +// Operation will only be successful if used within the specified number of days set in the delete retention policy +func (s *Client) RestoreContainer(ctx context.Context, deletedContainerName string, deletedContainerVersion string, options *RestoreContainerOptions) (RestoreContainerResponse, error) { + containerClient := s.NewContainerClient(deletedContainerName) + containerRestoreResp, err := containerClient.Restore(ctx, deletedContainerVersion, options) + return containerRestoreResp, err +} + // GetAccountInfo provides account level information func (s *Client) GetAccountInfo(ctx context.Context, o *GetAccountInfoOptions) (GetAccountInfoResponse, error) { getAccountInfoOptions := o.format() diff --git a/sdk/storage/azblob/service/client_test.go b/sdk/storage/azblob/service/client_test.go index a3c8573bc9e3..758bc9180cf6 100644 --- a/sdk/storage/azblob/service/client_test.go +++ b/sdk/storage/azblob/service/client_test.go @@ -599,3 +599,64 @@ func (s *ServiceUnrecordedTestsSuite) TestSASContainerClient2() { //_, err = containerClient2.Create(ctx, nil) //_require.Nil(err) } + +// make sure that container soft delete is enabled +// TODO: convert this test to recorded +func (s *ServiceUnrecordedTestsSuite) TestContainerRestore() { + _require := require.New(s.T()) + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + testName := s.T().Name() + containerName := testcommon.GenerateContainerName(testName) + + _, err = svcClient.CreateContainer(context.Background(), containerName, nil) + _require.Nil(err) + + _, err = svcClient.DeleteContainer(context.Background(), containerName, nil) + _require.Nil(err) + + prefix := testcommon.ContainerPrefix + listOptions := service.ListContainersOptions{Prefix: &prefix, Include: service.ListContainersInclude{Metadata: true, Deleted: true}} + pager := svcClient.NewListContainersPager(&listOptions) + + contRestored := false + for pager.More() { + resp, err := pager.NextPage(context.Background()) + _require.Nil(err) + for _, cont := range resp.ContainerItems { + _require.NotNil(cont.Name) + + if *cont.Deleted && *cont.Name == containerName { + contRestored = true + _, err = svcClient.RestoreContainer(context.Background(), containerName, *cont.Version, nil) + _require.Nil(err) + break + } + } + if contRestored { + break + } + } + + _require.Equal(contRestored, true) + + _, err = svcClient.DeleteContainer(context.Background(), containerName, nil) + _require.Nil(err) +} + +// TODO: convert this test to recorded +func (s *ServiceUnrecordedTestsSuite) TestContainerRestoreFailures() { + _require := require.New(s.T()) + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + testName := s.T().Name() + containerName := testcommon.GenerateContainerName(testName) + + _, err = svcClient.RestoreContainer(context.Background(), containerName, "", nil) + testcommon.ValidateBlobErrorCode(_require, err, bloberror.MissingRequiredHeader) + + _, err = svcClient.RestoreContainer(context.Background(), "", "", &service.RestoreContainerOptions{}) + testcommon.ValidateBlobErrorCode(_require, err, bloberror.MissingRequiredHeader) +} diff --git a/sdk/storage/azblob/service/examples_test.go b/sdk/storage/azblob/service/examples_test.go index 57b15b8ed3ef..a552de0660cc 100644 --- a/sdk/storage/azblob/service/examples_test.go +++ b/sdk/storage/azblob/service/examples_test.go @@ -129,6 +129,40 @@ func Example_service_Client_DeleteContainer() { handleError(err) } +func Example_service_Client_RestoreContainer() { + accountName, ok := os.LookupEnv("AZURE_STORAGE_ACCOUNT_NAME") + if !ok { + panic("AZURE_STORAGE_ACCOUNT_NAME could not be found") + } + serviceURL := fmt.Sprintf("https://%s.blob.core.windows.net/", accountName) + + cred, err := azidentity.NewDefaultAzureCredential(nil) + handleError(err) + serviceClient, err := service.NewClient(serviceURL, cred, nil) + handleError(err) + + listOptions := service.ListContainersOptions{ + Include: service.ListContainersInclude{ + Metadata: true, // Include Metadata + Deleted: true, // Include deleted containers in the result as well + }, + } + pager := serviceClient.NewListContainersPager(&listOptions) + + for pager.More() { + resp, err := pager.NextPage(context.TODO()) + if err != nil { + log.Fatal(err) + } + for _, cont := range resp.ContainerItems { + if *cont.Deleted { + _, err = serviceClient.RestoreContainer(context.TODO(), *cont.Name, *cont.Version, nil) + handleError(err) + } + } + } +} + func Example_service_Client_ListContainers() { accountName, ok := os.LookupEnv("AZURE_STORAGE_ACCOUNT_NAME") if !ok { diff --git a/sdk/storage/azblob/service/models.go b/sdk/storage/azblob/service/models.go index 6667166c43fa..5c3c0f1dc7f0 100644 --- a/sdk/storage/azblob/service/models.go +++ b/sdk/storage/azblob/service/models.go @@ -36,6 +36,9 @@ type CreateContainerOptions = container.CreateOptions // DeleteContainerOptions contains the optional parameters for the container.Client.Delete method. type DeleteContainerOptions = container.DeleteOptions +// RestoreContainerOptions contains the optional parameters for the container.Client.Restore method. +type RestoreContainerOptions = container.RestoreOptions + // CorsRule - CORS is an HTTP feature that enables a web application running under one domain to access resources in another // domain. Web browsers implement a security restriction known as same-origin policy that // prevents a web page from calling APIs in a different domain; CORS provides a secure way to allow one domain (the origin diff --git a/sdk/storage/azblob/service/responses.go b/sdk/storage/azblob/service/responses.go index 1eb5ab3d5ffd..26e3c085bd90 100644 --- a/sdk/storage/azblob/service/responses.go +++ b/sdk/storage/azblob/service/responses.go @@ -16,6 +16,9 @@ type CreateContainerResponse = generated.ContainerClientCreateResponse // DeleteContainerResponse contains the response from method container.Client.Delete type DeleteContainerResponse = generated.ContainerClientDeleteResponse +// RestoreContainerResponse contains the response from method container.Client.Restore +type RestoreContainerResponse = generated.ContainerClientRestoreResponse + // GetAccountInfoResponse contains the response from method Client.GetAccountInfo. type GetAccountInfoResponse = generated.ServiceClientGetAccountInfoResponse