From 3a39031c46fc777d695eee1bab2ea95f9a5f82ed Mon Sep 17 00:00:00 2001 From: Chenjie Shi Date: Wed, 1 Feb 2023 20:07:33 +0800 Subject: [PATCH] ACR data plane module (#19554) * very early version of azacr * update ci * init blob client * do not export jwtOnlyWithExp * fix lint * change to use CGC as much as possible * move operation * rename module path * change back to public autorest/go version * finalize auth policy * add and refine example * add test * finish test, example and docs * loose test coverage criteria temporarily * update recording * make authenticationPolicy private * remove readAll from example and change digest calculation to buffered method * add blob related examples * remove endpoint schema check logic * remove unreferenced types * fix pageable could not get next page problem and add related tests * rollback some directives * refine with review * change coverage config * refine some naming * refine some naming * refine some naming * refine with latest review * add BlobDigestCalculator and refine samples and examples * update Audience value to consolidate with azcore * refine test and doc * add test of upload chunk by chunk * add test recording file * update autorest/go version and add some error path test case * refine readme.md * update uploadChunk API * update uploadChunk API * change options of chunk upload * upgrade core lib version * move recording file to remote * refine model name * fix lint and doc problem * remove package link for ci check * update changelog after running pre-release script * remove pkg link from url validation --- eng/config.json | 4 + .../azcontainerregistry/CHANGELOG.md | 5 + .../azcontainerregistry/LICENSE.txt | 21 + sdk/containers/azcontainerregistry/README.md | 147 ++++ .../azcontainerregistry/assets.json | 6 + .../authentication_client.go | 142 ++++ .../authentication_client_test.go | 54 ++ .../authentication_custom_client.go | 34 + .../authentication_custom_client_test.go | 17 + .../authentication_policy.go | 243 ++++++ .../authentication_policy_test.go | 167 ++++ .../azcontainerregistry/autorest.md | 459 ++++++++++ .../azcontainerregistry/blob_client.go | 607 +++++++++++++ .../blob_client_example_test.go | 89 ++ .../azcontainerregistry/blob_client_test.go | 146 ++++ .../azcontainerregistry/blob_custom_client.go | 113 +++ .../blob_custom_client_example_test.go | 50 ++ .../blob_custom_client_test.go | 99 +++ sdk/containers/azcontainerregistry/build.go | 10 + sdk/containers/azcontainerregistry/ci.yml | 28 + sdk/containers/azcontainerregistry/client.go | 804 ++++++++++++++++++ .../client_example_test.go | 168 ++++ .../azcontainerregistry/client_test.go | 444 ++++++++++ .../azcontainerregistry/cloud_config.go | 26 + .../azcontainerregistry/constants.go | 199 +++++ .../azcontainerregistry/custom_client.go | 60 ++ .../custom_client_example_test.go | 25 + .../azcontainerregistry/custom_client_test.go | 25 + .../azcontainerregistry/custom_constants.go | 12 + .../example_delete_images_test.go | 63 ++ .../example_list_repositories_test.go | 37 + .../example_list_tags_test.go | 32 + .../example_set_artifact_properties_test.go | 37 + .../example_upload_download_blob_test.go | 53 ++ .../example_upload_manifest_test.go | 92 ++ sdk/containers/azcontainerregistry/go.mod | 27 + sdk/containers/azcontainerregistry/go.sum | 47 + sdk/containers/azcontainerregistry/models.go | 375 ++++++++ .../azcontainerregistry/models_serde.go | 575 +++++++++++++ .../azcontainerregistry/response_types.go | 219 +++++ .../azcontainerregistry/time_rfc3339.go | 87 ++ .../azcontainerregistry/utils_test.go | 96 +++ 42 files changed, 5944 insertions(+) create mode 100644 sdk/containers/azcontainerregistry/CHANGELOG.md create mode 100644 sdk/containers/azcontainerregistry/LICENSE.txt create mode 100644 sdk/containers/azcontainerregistry/README.md create mode 100644 sdk/containers/azcontainerregistry/assets.json create mode 100644 sdk/containers/azcontainerregistry/authentication_client.go create mode 100644 sdk/containers/azcontainerregistry/authentication_client_test.go create mode 100644 sdk/containers/azcontainerregistry/authentication_custom_client.go create mode 100644 sdk/containers/azcontainerregistry/authentication_custom_client_test.go create mode 100644 sdk/containers/azcontainerregistry/authentication_policy.go create mode 100644 sdk/containers/azcontainerregistry/authentication_policy_test.go create mode 100644 sdk/containers/azcontainerregistry/autorest.md create mode 100644 sdk/containers/azcontainerregistry/blob_client.go create mode 100644 sdk/containers/azcontainerregistry/blob_client_example_test.go create mode 100644 sdk/containers/azcontainerregistry/blob_client_test.go create mode 100644 sdk/containers/azcontainerregistry/blob_custom_client.go create mode 100644 sdk/containers/azcontainerregistry/blob_custom_client_example_test.go create mode 100644 sdk/containers/azcontainerregistry/blob_custom_client_test.go create mode 100644 sdk/containers/azcontainerregistry/build.go create mode 100644 sdk/containers/azcontainerregistry/ci.yml create mode 100644 sdk/containers/azcontainerregistry/client.go create mode 100644 sdk/containers/azcontainerregistry/client_example_test.go create mode 100644 sdk/containers/azcontainerregistry/client_test.go create mode 100644 sdk/containers/azcontainerregistry/cloud_config.go create mode 100644 sdk/containers/azcontainerregistry/constants.go create mode 100644 sdk/containers/azcontainerregistry/custom_client.go create mode 100644 sdk/containers/azcontainerregistry/custom_client_example_test.go create mode 100644 sdk/containers/azcontainerregistry/custom_client_test.go create mode 100644 sdk/containers/azcontainerregistry/custom_constants.go create mode 100644 sdk/containers/azcontainerregistry/example_delete_images_test.go create mode 100644 sdk/containers/azcontainerregistry/example_list_repositories_test.go create mode 100644 sdk/containers/azcontainerregistry/example_list_tags_test.go create mode 100644 sdk/containers/azcontainerregistry/example_set_artifact_properties_test.go create mode 100644 sdk/containers/azcontainerregistry/example_upload_download_blob_test.go create mode 100644 sdk/containers/azcontainerregistry/example_upload_manifest_test.go create mode 100644 sdk/containers/azcontainerregistry/go.mod create mode 100644 sdk/containers/azcontainerregistry/go.sum create mode 100644 sdk/containers/azcontainerregistry/models.go create mode 100644 sdk/containers/azcontainerregistry/models_serde.go create mode 100644 sdk/containers/azcontainerregistry/response_types.go create mode 100644 sdk/containers/azcontainerregistry/time_rfc3339.go create mode 100644 sdk/containers/azcontainerregistry/utils_test.go diff --git a/eng/config.json b/eng/config.json index e6d730e048fa..a508d682cab2 100644 --- a/eng/config.json +++ b/eng/config.json @@ -8,6 +8,10 @@ "Name": "azblob", "CoverageGoal": 0.30 }, + { + "Name": "azcontainerregistry", + "CoverageGoal": 0.60 + }, { "Name": "azcore", "CoverageGoal": 0.92 diff --git a/sdk/containers/azcontainerregistry/CHANGELOG.md b/sdk/containers/azcontainerregistry/CHANGELOG.md new file mode 100644 index 000000000000..598afc83a573 --- /dev/null +++ b/sdk/containers/azcontainerregistry/CHANGELOG.md @@ -0,0 +1,5 @@ +# Release History + +## 0.1.0 (2023-02-07) + +* This is the initial release of the `azcontainerregistry` library diff --git a/sdk/containers/azcontainerregistry/LICENSE.txt b/sdk/containers/azcontainerregistry/LICENSE.txt new file mode 100644 index 000000000000..d1ca00f20a89 --- /dev/null +++ b/sdk/containers/azcontainerregistry/LICENSE.txt @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE \ No newline at end of file diff --git a/sdk/containers/azcontainerregistry/README.md b/sdk/containers/azcontainerregistry/README.md new file mode 100644 index 000000000000..9ba39c7d0da0 --- /dev/null +++ b/sdk/containers/azcontainerregistry/README.md @@ -0,0 +1,147 @@ +# Azure Container Registry client module for Go + +Azure Container Registry allows you to store and manage container images and artifacts in a private registry for all types of container deployments. + +Use the client library for Azure Container Registry to: + +- List images or artifacts in a registry +- Obtain metadata for images and artifacts, repositories and tags +- Set read/write/delete properties on registry items +- Delete images and artifacts, repositories and tags +- Upload and download images + +[Source code](https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/containers/azcontainerregistry) | [REST API documentation](https://docs.microsoft.com/rest/api/containerregistry/) | [Product documentation](https://docs.microsoft.com/azure/container-registry/) + +## Getting started + +### Install packages + +Install `azcontainerregistry` and `azidentity` with `go get`: +```Bash +go get github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry +go get github.com/Azure/azure-sdk-for-go/sdk/azidentity +``` +[azidentity](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity) is used for Azure Active Directory authentication as demonstrated below. + +### Prerequisites + +- An [Azure subscription](https://azure.microsoft.com/free/) +- A supported Go version (the Azure SDK supports the two most recent Go releases) +- A [Container Registry service instance](https://docs.microsoft.com/azure/container-registry/container-registry-intro) + +To create a new Container Registry, you can use the [Azure Portal](https://docs.microsoft.com/azure/container-registry/container-registry-get-started-portal), +[Azure PowerShell](https://docs.microsoft.com/azure/container-registry/container-registry-get-started-powershell), or the [Azure CLI](https://docs.microsoft.com/azure/container-registry/container-registry-get-started-azure-cli). +Here's an example using the Azure CLI: + +```Powershell +az acr create --name MyContainerRegistry --resource-group MyResourceGroup --location westus --sku Basic +``` +### Authentication + +This document demonstrates using [azidentity.NewDefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#NewDefaultAzureCredential) to authenticate. +This credential type works in both local development and production environments. +We recommend using a [managed identity](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) in production. + +Client and BlobClient accepts any [azidentity][https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity] credential. +See the [azidentity](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity) documentation for more information about other credential types. + +#### Create a client + +Constructing the client requires your Container Registry's endpoint URL, which you can get from the Azure CLI or the Azure Portal. + +```go +import ( + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "log" +) + +func main() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + + client, err := azcontainerregistry.NewClient("", cred, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } +} +``` + +## Key concepts + +A **registry** stores Docker images and [OCI Artifacts](https://opencontainers.org/). +An image or artifact consists of a **manifest** and **layers**. +An image's manifest describes the layers that make up the image, and is uniquely identified by its **digest**. +An image can also be "tagged" to give it a human-readable alias. +An image or artifact can have zero or more **tags** associated with it, and each tag uniquely identifies the image. +A collection of images that share the same name but have different tags, is referred to as a **repository**. + +For more information please see [Container Registry Concepts](https://docs.microsoft.com/azure/container-registry/container-registry-concepts). + +## Examples + +Get started with our examples in pkg.go.dev. + +## Troubleshooting + +### Error Handling + +All methods which send HTTP requests return `*azcore.ResponseError` when these requests fail. `ResponseError` has error details and the raw response from Container Registry. + +```go +import "github.com/Azure/azure-sdk-for-go/sdk/azcore" + +resp, err := client.GetRepositoryProperties(ctx, "library/hello-world", nil) +if err != nil { + var httpErr *azcore.ResponseError + if errors.As(err, &httpErr) { + // TODO: investigate httpErr + } else { + // TODO: not an HTTP error + } +} +``` + +### Logging + +This module uses the logging implementation in `azcore`. To turn on logging for all Azure SDK modules, set `AZURE_SDK_GO_LOGGING` to `all`. By default the logger writes to stderr. Use the `azcore/log` package to control log output. For example, logging only HTTP request and response events, and printing them to stdout: + +```go +import azlog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log" + +// Print log events to stdout +azlog.SetListener(func(cls azlog.Event, msg string) { + fmt.Println(msg) +}) + +// Includes only requests and responses in credential logs +azlog.SetEvents(azlog.EventRequest, azlog.EventResponse) +``` + +### Accessing `http.Response` + +You can access the raw `*http.Response` returned by Container Registry using the `runtime.WithCaptureResponse` method and a context passed to any client method. + +```go +import "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + +var response *http.Response +ctx := runtime.WithCaptureResponse(context.TODO(), &response) +_, err = client.GetRepositoryProperties(ctx, "library/hello-world", nil) +if err != nil { + // TODO: handle error +} +// TODO: do something with response +``` + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct][https://opensource.microsoft.com/codeofconduct/]. For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact opencode@microsoft.com with any additional questions or comments. + + diff --git a/sdk/containers/azcontainerregistry/assets.json b/sdk/containers/azcontainerregistry/assets.json new file mode 100644 index 000000000000..62c8af4862cb --- /dev/null +++ b/sdk/containers/azcontainerregistry/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "go", + "TagPrefix": "go/containers/azcontainerregistry", + "Tag": "go/containers/azcontainerregistry_c8ca813cd9" +} diff --git a/sdk/containers/azcontainerregistry/authentication_client.go b/sdk/containers/azcontainerregistry/authentication_client.go new file mode 100644 index 000000000000..de75e60e1fb0 --- /dev/null +++ b/sdk/containers/azcontainerregistry/authentication_client.go @@ -0,0 +1,142 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. +// DO NOT EDIT. + +package azcontainerregistry + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming" + "net/http" + "net/url" + "strings" +) + +// authenticationClient contains the methods for the Authentication group. +// Don't use this type directly, use newAuthenticationClient() instead. +type authenticationClient struct { + endpoint string + pl runtime.Pipeline +} + +// ExchangeAADAccessTokenForACRRefreshToken - Exchange AAD tokens for an ACR refresh Token +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - grantType - Can take a value of accesstokenrefreshtoken, or accesstoken, or refresh_token +// - service - Indicates the name of your Azure container registry. +// - options - authenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions contains the optional parameters for the +// authenticationClient.ExchangeAADAccessTokenForACRRefreshToken method. +func (client *authenticationClient) ExchangeAADAccessTokenForACRRefreshToken(ctx context.Context, grantType postContentSchemaGrantType, service string, options *authenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions) (authenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse, error) { + req, err := client.exchangeAADAccessTokenForACRRefreshTokenCreateRequest(ctx, grantType, service, options) + if err != nil { + return authenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return authenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return authenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{}, runtime.NewResponseError(resp) + } + return client.exchangeAADAccessTokenForACRRefreshTokenHandleResponse(resp) +} + +// exchangeAADAccessTokenForACRRefreshTokenCreateRequest creates the ExchangeAADAccessTokenForACRRefreshToken request. +func (client *authenticationClient) exchangeAADAccessTokenForACRRefreshTokenCreateRequest(ctx context.Context, grantType postContentSchemaGrantType, service string, options *authenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions) (*policy.Request, error) { + urlPath := "/oauth2/exchange" + req, err := runtime.NewRequest(ctx, http.MethodPost, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + formData := url.Values{} + formData.Set("grant_type", string(grantType)) + formData.Set("service", service) + if options != nil && options.Tenant != nil { + formData.Set("tenant", *options.Tenant) + } + if options != nil && options.RefreshToken != nil { + formData.Set("refresh_token", *options.RefreshToken) + } + if options != nil && options.AccessToken != nil { + formData.Set("access_token", *options.AccessToken) + } + body := streaming.NopCloser(strings.NewReader(formData.Encode())) + return req, req.SetBody(body, "application/x-www-form-urlencoded") +} + +// exchangeAADAccessTokenForACRRefreshTokenHandleResponse handles the ExchangeAADAccessTokenForACRRefreshToken response. +func (client *authenticationClient) exchangeAADAccessTokenForACRRefreshTokenHandleResponse(resp *http.Response) (authenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse, error) { + result := authenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.acrRefreshToken); err != nil { + return authenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{}, err + } + return result, nil +} + +// ExchangeACRRefreshTokenForACRAccessToken - Exchange ACR Refresh token for an ACR Access Token +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - service - Indicates the name of your Azure container registry. +// - scope - Which is expected to be a valid scope, and can be specified more than once for multiple scope requests. You obtained +// this from the Www-Authenticate response header from the challenge. +// - refreshToken - Must be a valid ACR refresh token +// - options - authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions contains the optional parameters for the +// authenticationClient.ExchangeACRRefreshTokenForACRAccessToken method. +func (client *authenticationClient) ExchangeACRRefreshTokenForACRAccessToken(ctx context.Context, service string, scope string, refreshToken string, options *authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions) (authenticationClientExchangeACRRefreshTokenForACRAccessTokenResponse, error) { + req, err := client.exchangeACRRefreshTokenForACRAccessTokenCreateRequest(ctx, service, scope, refreshToken, options) + if err != nil { + return authenticationClientExchangeACRRefreshTokenForACRAccessTokenResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return authenticationClientExchangeACRRefreshTokenForACRAccessTokenResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return authenticationClientExchangeACRRefreshTokenForACRAccessTokenResponse{}, runtime.NewResponseError(resp) + } + return client.exchangeACRRefreshTokenForACRAccessTokenHandleResponse(resp) +} + +// exchangeACRRefreshTokenForACRAccessTokenCreateRequest creates the ExchangeACRRefreshTokenForACRAccessToken request. +func (client *authenticationClient) exchangeACRRefreshTokenForACRAccessTokenCreateRequest(ctx context.Context, service string, scope string, refreshToken string, options *authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions) (*policy.Request, error) { + urlPath := "/oauth2/token" + req, err := runtime.NewRequest(ctx, http.MethodPost, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + formData := url.Values{} + formData.Set("service", service) + formData.Set("scope", scope) + formData.Set("refresh_token", refreshToken) + if options != nil && options.GrantType != nil { + formData.Set("grant_type", string(*options.GrantType)) + } + body := streaming.NopCloser(strings.NewReader(formData.Encode())) + return req, req.SetBody(body, "application/x-www-form-urlencoded") +} + +// exchangeACRRefreshTokenForACRAccessTokenHandleResponse handles the ExchangeACRRefreshTokenForACRAccessToken response. +func (client *authenticationClient) exchangeACRRefreshTokenForACRAccessTokenHandleResponse(resp *http.Response) (authenticationClientExchangeACRRefreshTokenForACRAccessTokenResponse, error) { + result := authenticationClientExchangeACRRefreshTokenForACRAccessTokenResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.acrAccessToken); err != nil { + return authenticationClientExchangeACRRefreshTokenForACRAccessTokenResponse{}, err + } + return result, nil +} diff --git a/sdk/containers/azcontainerregistry/authentication_client_test.go b/sdk/containers/azcontainerregistry/authentication_client_test.go new file mode 100644 index 000000000000..1057f75462c7 --- /dev/null +++ b/sdk/containers/azcontainerregistry/authentication_client_test.go @@ -0,0 +1,54 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/stretchr/testify/require" + "testing" +) + +func Test_authenticationClient_ExchangeAADAccessTokenForACRRefreshToken(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + client := newAuthenticationClient("https://azacrlivetest.azurecr.io", &authenticationClientOptions{ClientOptions: options}) + ctx := context.Background() + accessToken, err := cred.GetToken( + ctx, + policy.TokenRequestOptions{ + Scopes: []string{"https://management.core.windows.net/.default"}, + }) + require.NoError(t, err) + resp, err := client.ExchangeAADAccessTokenForACRRefreshToken(ctx, postContentSchemaGrantTypeAccessToken, "azacrlivetest.azurecr.io", &authenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions{ + AccessToken: &accessToken.Token, + }) + require.NoError(t, err) + require.NotEmpty(t, *resp.acrRefreshToken.RefreshToken) +} + +func Test_authenticationClient_ExchangeACRRefreshTokenForACRAccessToken(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + client := newAuthenticationClient("https://azacrlivetest.azurecr.io", &authenticationClientOptions{ClientOptions: options}) + ctx := context.Background() + accessToken, err := cred.GetToken( + ctx, + policy.TokenRequestOptions{ + Scopes: []string{"https://management.core.windows.net/.default"}, + }) + require.NoError(t, err) + refreshResp, err := client.ExchangeAADAccessTokenForACRRefreshToken(ctx, postContentSchemaGrantTypeAccessToken, "azacrlivetest.azurecr.io", &authenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions{ + AccessToken: &accessToken.Token, + }) + require.NoError(t, err) + require.NotEmpty(t, *refreshResp.acrRefreshToken.RefreshToken) + accessResp, err := client.ExchangeACRRefreshTokenForACRAccessToken(ctx, "azacrlivetest.azurecr.io", "registry:catalog:*", *refreshResp.acrRefreshToken.RefreshToken, &authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions{GrantType: to.Ptr(tokenGrantTypeRefreshToken)}) + require.NoError(t, err) + require.NotEmpty(t, *accessResp.acrAccessToken.AccessToken) +} diff --git a/sdk/containers/azcontainerregistry/authentication_custom_client.go b/sdk/containers/azcontainerregistry/authentication_custom_client.go new file mode 100644 index 000000000000..6083d6afa54f --- /dev/null +++ b/sdk/containers/azcontainerregistry/authentication_custom_client.go @@ -0,0 +1,34 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" +) + +// authenticationClientOptions contains the optional parameters for the newAuthenticationClient method. +type authenticationClientOptions struct { + azcore.ClientOptions +} + +// newAuthenticationClient creates a new instance of AuthenticationClient with the specified values. +// - endpoint - Registry login URL +// - options - Client options, pass nil to accept the default values. +func newAuthenticationClient(endpoint string, options *authenticationClientOptions) *authenticationClient { + if options == nil { + options = &authenticationClientOptions{} + } + + pipeline := runtime.NewPipeline(moduleName, moduleVersion, runtime.PipelineOptions{}, &options.ClientOptions) + + client := &authenticationClient{ + endpoint: endpoint, + pl: pipeline, + } + return client +} diff --git a/sdk/containers/azcontainerregistry/authentication_custom_client_test.go b/sdk/containers/azcontainerregistry/authentication_custom_client_test.go new file mode 100644 index 000000000000..133cfac0c60d --- /dev/null +++ b/sdk/containers/azcontainerregistry/authentication_custom_client_test.go @@ -0,0 +1,17 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func Test_newAuthenticationClient(t *testing.T) { + client := newAuthenticationClient("test", nil) + require.NotNil(t, client) +} diff --git a/sdk/containers/azcontainerregistry/authentication_policy.go b/sdk/containers/azcontainerregistry/authentication_policy.go new file mode 100644 index 000000000000..7b139f83a2bd --- /dev/null +++ b/sdk/containers/azcontainerregistry/authentication_policy.go @@ -0,0 +1,243 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "net/http" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/internal/temporal" +) + +const ( + headerAuthorization = "Authorization" + bearerHeader = "Bearer " +) + +type authenticationPolicyOptions struct { +} + +type authenticationPolicy struct { + mainResource *temporal.Resource[azcore.AccessToken, acquiringResourceState] + cred azcore.TokenCredential + aadScopes []string + acrScope string + acrService string + authClient *authenticationClient +} + +func newAuthenticationPolicy(cred azcore.TokenCredential, scopes []string, authClient *authenticationClient, opts *authenticationPolicyOptions) *authenticationPolicy { + return &authenticationPolicy{ + cred: cred, + aadScopes: scopes, + authClient: authClient, + mainResource: temporal.NewResource(acquire), + } +} + +func (p *authenticationPolicy) Do(req *policy.Request) (*http.Response, error) { + // send a copy of the original request without body content + challengeReq, err := p.getChallengeRequest(*req) + if err != nil { + return nil, err + } + resp, err := challengeReq.Next() + if err != nil { + return nil, err + } + + // do challenge process + if resp.StatusCode == 401 { + err := p.findServiceAndScope(resp) + if err != nil { + return nil, err + } + + accessToken, err := p.getAccessToken(req) + if err != nil { + return nil, err + } + + req.Raw().Header.Set( + headerAuthorization, + fmt.Sprintf("%s%s", bearerHeader, accessToken), + ) + + // send the original request with auth + return req.Next() + } + + return resp, nil +} + +func (p *authenticationPolicy) getAccessToken(req *policy.Request) (string, error) { + // anonymous access + if p.cred == nil { + resp, err := p.authClient.ExchangeACRRefreshTokenForACRAccessToken(req.Raw().Context(), p.acrService, p.acrScope, "", &authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions{GrantType: to.Ptr(tokenGrantTypePassword)}) + if err != nil { + return "", err + } + return *resp.acrAccessToken.AccessToken, nil + } + + // access with token + as := acquiringResourceState{ + policy: p, + req: req, + } + + // get refresh token from cache/request + refreshToken, err := p.mainResource.Get(as) + if err != nil { + return "", err + } + + // get access token from request + resp, err := p.authClient.ExchangeACRRefreshTokenForACRAccessToken(req.Raw().Context(), p.acrService, p.acrScope, refreshToken.Token, &authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions{GrantType: to.Ptr(tokenGrantTypeRefreshToken)}) + if err != nil { + return "", err + } + return *resp.acrAccessToken.AccessToken, nil +} + +func (p *authenticationPolicy) findServiceAndScope(resp *http.Response) error { + authHeader := resp.Header.Get("WWW-Authenticate") + if authHeader == "" { + return errors.New("response has no WWW-Authenticate header for challenge authentication") + } + + authHeader = strings.ReplaceAll(authHeader, "Bearer ", "") + parts := strings.Split(authHeader, "\",") + valuesMap := map[string]string{} + for _, part := range parts { + subParts := strings.Split(part, "=") + if len(subParts) == 2 { + valuesMap[subParts[0]] = strings.ReplaceAll(subParts[1], "\"", "") + } + } + + if v, ok := valuesMap["scope"]; ok { + p.acrScope = v + } + if p.acrScope == "" { + return errors.New("could not find a valid scope in the WWW-Authenticate header") + } + + if v, ok := valuesMap["service"]; ok { + p.acrService = v + } + if p.acrService == "" { + return errors.New("could not find a valid service in the WWW-Authenticate header") + } + + return nil +} + +func (p authenticationPolicy) getChallengeRequest(orig policy.Request) (*policy.Request, error) { + req, err := runtime.NewRequest(orig.Raw().Context(), orig.Raw().Method, orig.Raw().URL.String()) + if err != nil { + return nil, err + } + + req.Raw().Header = orig.Raw().Header + req.Raw().Header.Set("Content-Length", "0") + req.Raw().ContentLength = 0 + + copied := orig.Clone(orig.Raw().Context()) + copied.Raw().Body = req.Body() + copied.Raw().ContentLength = 0 + copied.Raw().Header.Set("Content-Length", "0") + err = copied.SetBody(streaming.NopCloser(bytes.NewReader([]byte{})), "application/json") + if err != nil { + return nil, err + } + copied.Raw().Header.Del("Content-Type") + + return copied, err +} + +type acquiringResourceState struct { + req *policy.Request + policy *authenticationPolicy +} + +// acquire acquires or updates the resource; only one +// thread/goroutine at a time ever calls this function +func acquire(state acquiringResourceState) (newResource azcore.AccessToken, newExpiration time.Time, err error) { + // get AAD token from credential + aadToken, err := state.policy.cred.GetToken( + state.req.Raw().Context(), + policy.TokenRequestOptions{ + Scopes: state.policy.aadScopes, + }, + ) + if err != nil { + return azcore.AccessToken{}, time.Time{}, err + } + + // exchange refresh token with AAD token + refreshResp, err := state.policy.authClient.ExchangeAADAccessTokenForACRRefreshToken(state.req.Raw().Context(), postContentSchemaGrantTypeAccessToken, state.policy.acrService, &authenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions{ + AccessToken: &aadToken.Token, + }) + if err != nil { + return azcore.AccessToken{}, time.Time{}, err + } + + refreshToken := azcore.AccessToken{ + Token: *refreshResp.acrRefreshToken.RefreshToken, + } + + // get refresh token expire time + refreshToken.ExpiresOn, err = getJWTExpireTime(*refreshResp.acrRefreshToken.RefreshToken) + if err != nil { + return azcore.AccessToken{}, time.Time{}, err + } + + // return refresh token + return refreshToken, refreshToken.ExpiresOn, nil +} + +func getJWTExpireTime(token string) (time.Time, error) { + values := strings.Split(token, ".") + if len(values) > 2 { + value := values[1] + padding := len(value) % 4 + if padding > 0 { + for i := 0; i < 4-padding; i++ { + value += "=" + } + } + parsedValue, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return time.Time{}, err + } + + var jsonValue *jwtOnlyWithExp + err = json.Unmarshal(parsedValue, &jsonValue) + if err != nil { + return time.Time{}, err + } + return time.Unix(jsonValue.Exp, 0), nil + } + + return time.Time{}, errors.New("could not parse refresh token expire time") +} + +type jwtOnlyWithExp struct { + Exp int64 `json:"exp"` +} diff --git a/sdk/containers/azcontainerregistry/authentication_policy_test.go b/sdk/containers/azcontainerregistry/authentication_policy_test.go new file mode 100644 index 000000000000..0c0613524f3d --- /dev/null +++ b/sdk/containers/azcontainerregistry/authentication_policy_test.go @@ -0,0 +1,167 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/internal/temporal" + "github.com/stretchr/testify/require" + "net/http" + "testing" + "time" +) + +func Test_getJWTExpireTime(t *testing.T) { + for _, test := range []struct { + name string + token string + expire time.Time + err bool + }{ + { + "test1", + ".ewogICJqdGkiOiAiMzY1ZTNiNWItODQ0ZS00YTIxLWEzOGMtNGQ4YWViZGQ2YTA2IiwKICAic3ViIjogInVzZXJAY29udG9zby5jb20iLAogICJuYmYiOiAxNDk3OTg4NzEyLAogICJleHAiOiAxNDk3OTkwODAxLAogICJpYXQiOiAxNDk3OTg4NzEyLAogICJpc3MiOiAiQXp1cmUgQ29udGFpbmVyIFJlZ2lzdHJ5IiwKICAiYXVkIjogImNvbnRvc29yZWdpc3RyeS5henVyZWNyLmlvIiwKICAidmVyc2lvbiI6ICIxLjAiLAogICJncmFudF90eXBlIjogInJlZnJlc2hfdG9rZW4iLAogICJ0ZW5hbnQiOiAiNDA5NTIwZDQtODEwMC00ZDFkLWFkNDctNzI0MzJkZGNjMTIwIiwKICAicGVybWlzc2lvbnMiOiB7CiAgICAiYWN0aW9ucyI6IFsKICAgICAgIioiCiAgICBdLAogICAgIm5vdEFjdGlvbnMiOiBbXQogIH0sCiAgInJvbGVzIjogW10KfQ==.", + time.Unix(1497990801, 0), + false, + }, + { + "test2", + ".eyJqdGkiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJuYmYiOjE2NzA0MTA1NDEsImV4cCI6MTY3MDQyMjI0MSwiaWF0IjoxNjcwNDEwNTQxLCJpc3MiOiJBenVyZSBDb250YWluZXIgUmVnaXN0cnkiLCJhdWQiOiJhemFjcmxpdmV0ZXN0LmF6dXJlY3IuaW8iLCJ2ZXJzaW9uIjoiMS4wIiwicmlkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJncmFudF90eXBlIjoicmVmcmVzaF90b2tlbiIsImFwcGlkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwicGVybWlzc2lvbnMiOnsiQWN0aW9ucyI6WyJyZWFkIiwid3JpdGUiLCJkZWxldGUiLCJkZWxldGVkL3JlYWQiLCJkZWxldGVkL3Jlc3RvcmUvYWN0aW9uIl0sIk5vdEFjdGlvbnMiOm51bGx9LCJyb2xlcyI6W119.", + time.Unix(1670422241, 0), + false, + }, + { + "test-padding", + ".ewogICJqdGkiOiAiMzY1ZTNiNWItODQ0ZS00YTIxLWEzOGMtNGQ4YWViZGQ2YTA2IiwKICAic3ViIjogInVzZXJAY29udG9zby5jb20iLAogICJuYmYiOiAxNDk3OTg4NzEyLAogICJleHAiOiAxNDk3OTkwODAxLAogICJpYXQiOiAxNDk3OTg4NzEyLAogICJpc3MiOiAiQXp1cmUgQ29udGFpbmVyIFJlZ2lzdHJ5IiwKICAiYXVkIjogImNvbnRvc29yZWdpc3RyeS5henVyZWNyLmlvIiwKICAidmVyc2lvbiI6ICIxLjAiLAogICJncmFudF90eXBlIjogInJlZnJlc2hfdG9rZW4iLAogICJ0ZW5hbnQiOiAiNDA5NTIwZDQtODEwMC00ZDFkLWFkNDctNzI0MzJkZGNjMTIwIiwKICAicGVybWlzc2lvbnMiOiB7CiAgICAiYWN0aW9ucyI6IFsKICAgICAgIioiCiAgICBdLAogICAgIm5vdEFjdGlvbnMiOiBbXQogIH0sCiAgInJvbGVzIjogW10KfQ=.", + time.Unix(1497990801, 0), + false, + }, + { + "test-error", + ".error.", + time.Unix(1497990801, 0), + true, + }, + { + "test-unmarshal-error", + ".ewogICJqdGkiOiAiMzY1ZTNiNWItODQ0ZS00YTIxLWEzOGMtNGQ4YWViZGQ2YTA2IiwKICAic3ViIjogInVzZXJAY29udG9zby5jb20iLAogICJuYmYiOiAxNDk3OTg4NzEyLAogICJleHAiOiAiMTQ5Nzk5MDgwMSIsCiAgImlhdCI6IDE0OTc5ODg3MTIsCiAgImlzcyI6ICJBenVyZSBDb250YWluZXIgUmVnaXN0cnkiLAogICJhdWQiOiAiY29udG9zb3JlZ2lzdHJ5LmF6dXJlY3IuaW8iLAogICJ2ZXJzaW9uIjogIjEuMCIsCiAgImdyYW50X3R5cGUiOiAicmVmcmVzaF90b2tlbiIsCiAgInRlbmFudCI6ICI0MDk1MjBkNC04MTAwLTRkMWQtYWQ0Ny03MjQzMmRkY2MxMjAiLAogICJwZXJtaXNzaW9ucyI6IHsKICAgICJhY3Rpb25zIjogWwogICAgICAiKiIKICAgIF0sCiAgICAibm90QWN0aW9ucyI6IFtdCiAgfSwKICAicm9sZXMiOiBbXQp9.", + time.Unix(1497990801, 0), + true, + }, + { + "test-length-error", + ".", + time.Unix(1497990801, 0), + true, + }, + } { + t.Run(test.name, func(t *testing.T) { + expire, err := getJWTExpireTime(test.token) + if test.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, test.expire, expire) + } + }) + } +} + +func Test_authenticationPolicy_findServiceAndScope(t *testing.T) { + resp1 := http.Response{} + resp1.Header = http.Header{} + resp1.Header.Set("WWW-Authenticate", "Bearer realm=\"https://contosoregistry.azurecr.io/oauth2/token\",service=\"contosoregistry.azurecr.io\",scope=\"registry:catalog:*\"") + + resp2 := http.Response{} + resp2.Header = http.Header{} + resp2.Header.Set("WWW-Authenticate", "Bearer realm=\"https://contosoregistry.azurecr.io/oauth2/token\",service=\"contosoregistry.azurecr.io\",scope=\"artifact-repository:repo:pull\"") + + for _, test := range []struct { + acrScope string + acrService string + resp *http.Response + err bool + }{ + {"registry:catalog:*", "contosoregistry.azurecr.io", &resp1, false}, + {"artifact-repository:repo:pull", "contosoregistry.azurecr.io", &resp2, false}, + {"error", "error", &http.Response{}, true}, + } { + t.Run(fmt.Sprintf("%s-%s", test.acrService, test.acrScope), func(t *testing.T) { + p := &authenticationPolicy{} + err := p.findServiceAndScope(test.resp) + if test.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, test.acrScope, p.acrScope) + require.Equal(t, test.acrService, p.acrService) + } + }) + } +} + +func Test_authenticationPolicy_getAccessToken_live(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + authClient := newAuthenticationClient("https://azacrlivetest.azurecr.io", &authenticationClientOptions{options}) + p := &authenticationPolicy{ + temporal.NewResource(acquire), + cred, + []string{"https://management.core.windows.net/.default"}, + "registry:catalog:*", + "azacrlivetest.azurecr.io", + authClient, + } + request, err := runtime.NewRequest(context.Background(), http.MethodGet, "https://test.com") + require.NoError(t, err) + token, err := p.getAccessToken(request) + require.NoError(t, err) + require.NotEmpty(t, token) +} + +func Test_authenticationPolicy_getAccessToken_live_anonymous(t *testing.T) { + startRecording(t) + _, options := getCredAndClientOptions(t) + authClient := newAuthenticationClient("https://azacrlivetest.azurecr.io", &authenticationClientOptions{options}) + p := &authenticationPolicy{ + temporal.NewResource(acquire), + nil, + nil, + "registry:catalog:*", + "azacrlivetest.azurecr.io", + authClient, + } + request, err := runtime.NewRequest(context.Background(), http.MethodGet, "https://test.com") + require.NoError(t, err) + token, err := p.getAccessToken(request) + require.NoError(t, err) + require.NotEmpty(t, token) +} + +func Test_authenticationPolicy_anonymousAccess(t *testing.T) { + startRecording(t) + _, options := getCredAndClientOptions(t) + client, err := NewClient("https://azacrlivetest.azurecr.io", nil, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + ctx := context.Background() + pager := client.NewListRepositoriesPager(nil) + repositoryName := "" + for pager.More() { + page, err := pager.NextPage(ctx) + require.NoError(t, err) + require.NotEmpty(t, page.Repositories.Names) + if repositoryName == "" { + repositoryName = *page.Repositories.Names[0] + } + } + require.NotEmpty(t, repositoryName) + _, err = client.UpdateRepositoryProperties(ctx, repositoryName, &ClientUpdateRepositoryPropertiesOptions{Value: &RepositoryWriteableProperties{CanDelete: to.Ptr(true)}}) + require.Error(t, err) +} diff --git a/sdk/containers/azcontainerregistry/autorest.md b/sdk/containers/azcontainerregistry/autorest.md new file mode 100644 index 000000000000..adf0ed333b76 --- /dev/null +++ b/sdk/containers/azcontainerregistry/autorest.md @@ -0,0 +1,459 @@ +# Autorest config for Azure Container Registry Go client + +> see https://aka.ms/autorest + +## Configuration + +```yaml +input-file: https://github.com/Azure/azure-rest-api-specs/blob/c8d9a26a2857828e095903efa72512cf3a76c15d/specification/containerregistry/data-plane/Azure.ContainerRegistry/stable/2021-07-01/containerregistry.json +license-header: MICROSOFT_MIT_NO_VERSION +go: true +clear-output-folder: false +export-clients: true +openapi-type: "data-plane" +output-folder: ../azcontainerregistry +use: "@autorest/go@4.0.0-preview.45" +honor-body-placement: true +remove-unreferenced-types: true +``` + +## Customizations + +See the [AutoRest samples](https://github.com/Azure/autorest/tree/master/Samples/3b-custom-transformations) +for more about how we're customizing things. + +### Remove response for "ContainerRegistry_DeleteRepository" operation + +so that the generated code doesn't return a response for the deleted repository operation. + +```yaml +directive: + - from: swagger-document + where: $["paths"]["/acr/v1/{name}"] + transform: > + delete $.delete["responses"]["202"].schema +``` + +### Remove response for "ContainerRegistryBlob_DeleteBlob" operation + +so that the generated code doesn't return a response for the deleted blob operation. + +```yaml +directive: + - from: swagger-document + where: $["paths"]["/v2/{name}/blobs/{digest}"] + transform: > + delete $.delete["responses"]["202"].schema +``` + +### Remove "Authentication_GetAcrAccessTokenFromLogin" operation + +as the service team discourage using username/password to authenticate. + +```yaml +directive: + - from: swagger-document + where: $["paths"]["/oauth2/token"] + transform: > + delete $.get +``` + +### Remove "ContainerRegistry_CheckDockerV2Support" operation + +```yaml +directive: + - from: swagger-document + where: $["paths"]["/v2/"] + transform: > + delete $.get +``` + +### Remove "definitions.TagAttributesBase.properties.signed" + +as we don't have customer scenario using it. + +```yaml +directive: + - from: swagger-document + where: $.definitions.TagAttributesBase + transform: > + delete $.properties.signed +``` + +### Remove "definitions.ManifestAttributesBase.properties.configMediaType" + +as we don't have customer scenario using it. + +```yaml +directive: + - from: swagger-document + where: $.definitions.ManifestAttributesBase + transform: > + delete $.properties.configMediaType +``` + +### Change "parameters.ApiVersionParameter.required" to true + +so that the API version could be removed from client parameter. + +```yaml +directive: + - from: swagger-document + where: $.parameters.ApiVersionParameter + transform: > + $.required = true +``` + +### Take stream as manifest body + +```yaml +directive: + from: swagger-document + where: $.parameters.ManifestBody + transform: > + $.schema = { + "type": "string", + "format": "binary" + } +``` + +### Change list order by param to enum + +```yaml +directive: + - from: containerregistry.json + where: $.paths["/acr/v1/{name}/_tags"].get + transform: > + $.parameters.splice(3, 1); + $.parameters.push({ + "name": "orderby", + "x-ms-client-name": "OrderBy", + "in": "query", + "required": false, + "x-ms-parameter-location": "method", + "type": "string", + "description": "Sort options for ordering tags in a collection.", + "enum": [ + "none", + "timedesc", + "timeasc" + ], + "x-ms-enum": { + "name": "ArtifactTagOrderBy", + "values": [ + { + "value": "none", + "name": "None", + "description": "Do not provide an orderby value in the request." + }, + { + "value": "timedesc", + "name": "LastUpdatedOnDescending", + "description": "Order tags by LastUpdatedOn field, from most recently updated to least recently updated." + }, + { + "value": "timeasc", + "name": "LastUpdatedOnAscending", + "description": "Order tags by LastUpdatedOn field, from least recently updated to most recently updated." + } + ] + } + }); + - from: containerregistry.json + where: $.paths["/acr/v1/{name}/_manifests"] + transform: > + $.get.parameters.splice(3, 1); + $.get.parameters.push({ + "name": "orderby", + "x-ms-client-name": "OrderBy", + "in": "query", + "required": false, + "x-ms-parameter-location": "method", + "type": "string", + "description": "Sort options for ordering manifests in a collection.", + "enum": [ + "none", + "timedesc", + "timeasc" + ], + "x-ms-enum": { + "name": "ArtifactManifestOrderBy", + "values": [ + { + "value": "none", + "name": "None", + "description": "Do not provide an orderby value in the request." + }, + { + "value": "timedesc", + "name": "LastUpdatedOnDescending", + "description": "Order manifests by LastUpdatedOn field, from most recently updated to least recently updated." + }, + { + "value": "timeasc", + "name": "LastUpdatedOnAscending", + "description": "Order manifest by LastUpdatedOn field, from least recently updated to most recently updated." + } + ] + } + }); +``` + +### Rename paged operations from Get* to List* + +```yaml +directive: + - rename-operation: + from: ContainerRegistry_GetManifests + to: ContainerRegistry_ListManifests + - rename-operation: + from: ContainerRegistry_GetRepositories + to: ContainerRegistry_ListRepositories + - rename-operation: + from: ContainerRegistry_GetTags + to: ContainerRegistry_ListTags +``` + +### Change ContainerRegistry_CreateManifest behaviour + +```yaml +directive: + from: swagger-document + where: $.paths["/v2/{name}/manifests/{reference}"].put + transform: > + $.consumes.push("application/vnd.oci.image.manifest.v1+json"); + delete $.responses["201"].schema; +``` + +### Change ContainerRegistry_GetManifest behaviour + +```yaml +directive: + from: swagger-document + where: $.paths["/v2/{name}/manifests/{reference}"].get.responses["200"] + transform: > + $.schema = { + type: "string", + format: "file" + }; + $.headers = { + "Docker-Content-Digest": { + "type": "string", + "description": "Digest of the targeted content for the request." + } + }; +``` + +### Remove generated constructors + +```yaml +directive: + - from: + - authentication_client.go + - client.go + - blob_client.go + where: $ + transform: return $.replace(/(?:\/\/.*\s)+func New.+Client.+\{\s(?:.+\s)+\}\s/, ""); +``` + +### Rename operations + +```yaml +directive: + - rename-operation: + from: ContainerRegistry_GetProperties + to: ContainerRegistry_GetRepositoryProperties + - rename-operation: + from: ContainerRegistry_UpdateProperties + to: ContainerRegistry_UpdateRepositoryProperties + - rename-operation: + from: ContainerRegistry_UpdateTagAttributes + to: ContainerRegistry_UpdateTagProperties + - rename-operation: + from: ContainerRegistry_CreateManifest + to: ContainerRegistry_UploadManifest +``` + +### Rename parameter name + +```yaml +directive: + from: swagger-document + where: $.parameters + transform: > + $.DigestReference["x-ms-client-name"] = "digest"; + $.TagReference["x-ms-client-name"] = "tag"; +``` + +### Add 202 response to ContainerRegistryBlob_MountBlob + +```yaml +directive: + from: swagger-document + where: $.paths["/v2/{name}/blobs/uploads/"] + transform: > + $.post["responses"]["202"] = $.post["responses"]["201"]; +``` + +### Extract and add endpoint for nextLink + +```yaml +directive: + - from: + - client.go + where: $ + transform: return $.replaceAll(/result\.Link = &val/g, "val = runtime.JoinPaths(client.endpoint, extractNextLink(val))\n\t\tresult.Link = &val"); +``` + +### Do not export Authentication client and related models + +```yaml +directive: + - from: + - authentication_client.go + where: $ + transform: return $.replaceAll(/ AuthenticationClient/g, " authenticationClient").replaceAll(/\*AuthenticationClient/g, "*authenticationClient").replace(/NewAuthenticationClient/, "newAuthenticationClient").replaceAll(/\(AuthenticationClient/g, "(authenticationClient"); + - from: + - authentication_client.go + where: $ + transform: return $.replace(/PostContentSchemaGrantType/, "postContentSchemaGrantType").replace(/PostContentSchemaGrantType/, "postContentSchemaGrantType"); + - from: + - authentication_client.go + where: $ + transform: return $.replace(/result\.AcrAccessToken/, "result.acrAccessToken").replace(/result\.AcrRefreshToken/, "result.acrRefreshToken"); + - from: + - response_types.go + where: $ + transform: return $.replaceAll(/AuthenticationClient/g, "authenticationClient").replace(/AcrRefreshToken\n/, "acrRefreshToken\n").replace(/AcrAccessToken\n/, "acrAccessToken\n"); + - from: + - models.go + where: $ + transform: return $.replaceAll(/AuthenticationClient/g, "authenticationClient").replace(/AcrRefreshToken struct/, "acrRefreshToken struct").replace(/AcrAccessToken struct/, "acrAccessToken struct"); + - from: + - models.go + where: $ + transform: return $.replace(/TokenGrantType/, "tokenGrantType"); + - from: + - constants.go + where: $ + transform: return $.replaceAll(/ TokenGrantType/g, " tokenGrantType").replaceAll(/\tTokenGrantType/g, "\ttokenGrantType").replaceAll(/\]TokenGrantType/g, "]tokenGrantType").replaceAll(/PossibleTokenGrantType/g, "possibleTokenGrantType"); + - from: + - constants.go + where: $ + transform: return $.replaceAll(/ PostContentSchemaGrantType/g, " postContentSchemaGrantType").replaceAll(/\tPostContentSchemaGrantType/g, "\tpostContentSchemaGrantType").replaceAll(/\]PostContentSchemaGrantType/g, "]postContentSchemaGrantType").replaceAll(/PossiblePostContentSchemaGrantType/g, "possiblePostContentSchemaGrantType"); + - from: + - models_serde.go + where: $ + transform: return $.replaceAll(/ AcrAccessToken/g, " acrAccessToken").replace(/\*AcrAccessToken/g, "*acrAccessToken").replaceAll(/ AcrRefreshToken/g, " acrRefreshToken").replace(/\*AcrRefreshToken/g, "*acrRefreshToken"); +``` + +### Rename all Acr to ACR + +```yaml +directive: + - from: + - "*.go" + where: $ + transform: return $.replaceAll(/Acr/g, "ACR"); +``` + +### Rename TagAttributesBase, ManifestAttributesBase, TagAttributeBases, Repositories, AcrManifests and QueryNum + +```yaml +directive: + - from: containerregistry.json + where: $.definitions + transform: > + $.TagAttributesBase["x-ms-client-name"] = "TagAttributes"; + - from: containerregistry.json + where: $.definitions + transform: > + $.ManifestAttributesBase["x-ms-client-name"] = "ManifestAttributes"; + - from: containerregistry.json + where: $.definitions.TagList + transform: > + delete $.properties.tags["x-ms-client-name"]; + - from: containerregistry.json + where: $.definitions.Repositories + transform: > + $.properties.repositories["x-ms-client-name"] = "Names"; + - from: containerregistry.json + where: $.definitions + transform: > + $.AcrManifests["x-ms-client-name"] = "Manifests"; + - from: containerregistry.json + where: $.definitions.AcrManifests + transform: > + $.properties.manifests["x-ms-client-name"] = "Attributes"; + - from: containerregistry.json + where: $.parameters + transform: > + $.QueryNum["x-ms-client-name"] = "MaxNum"; +``` + +### Rename binary request param and response property + +```yaml +directive: + - from: containerregistry.json + where: $.parameters + transform: > + $.RawData["x-ms-client-name"] = "chunkData"; + $.RawDataOptional["x-ms-client-name"] = "blobData"; + $.ManifestBody["x-ms-client-name"] = "manifestData"; + - from: + - blob_client.go + where: $ + transform: return $.replace(/BlobClientGetBlobResponse\{Body/, "BlobClientGetBlobResponse{BlobData").replace(/BlobClientGetChunkResponse\{Body/, "BlobClientGetChunkResponse{ChunkData"); + - from: + - client.go + where: $ + transform: return $.replace(/ClientGetManifestResponse\{Body/, "ClientGetManifestResponse{ManifestData"); + - from: + - response_types.go + where: $ + transform: return $.replace(/Body io\.ReadCloser/, "BlobData io.ReadCloser").replace(/Body io\.ReadCloser/, "ChunkData io.ReadCloser").replace(/Body io\.ReadCloser/, "ManifestData io.ReadCloser"); +``` + +### Hide original UploadChunk and CompleteUpload method +```yaml +directive: + - from: containerregistry.json + where: $.paths["/{nextBlobUuidLink}"] + transform: > + $.put.parameters.splice(1,1); + - from: + - blob_client.go + where: $ + transform: return $.replaceAll(/ UploadChunk/g, " uploadChunk").replace(/\.UploadChunk/, ".uploadChunk").replaceAll(/ CompleteUpload/g, " completeUpload").replace(/\.CompleteUpload/, ".completeUpload"); +``` + +### Add content-range parameters to upload chunk + +```yaml +directive: + - from: swagger-document + where: $.paths["/{nextBlobUuidLink}"].patch + transform: > + $.parameters.push({ + "name": "Content-Range", + "in": "header", + "type": "string", + "description": "Range of bytes identifying the desired block of content represented by the body. Start must the end offset retrieved via status check plus one. Note that this is a non-standard use of the Content-Range header." + }); + - from: + - blob_client.go + - models.go + where: $ + transform: return $.replaceAll(/BlobClientUploadChunkOptions/g, "blobClientUploadChunkOptions").replace(/BlobClient\.UploadChunk/, "BlobClient.uploadChunk"); +``` + +### Add description for ArtifactOperatingSystem + +```yaml +directive: + - from: swagger-document + where: $.definitions + transform: > + $.ArtifactOperatingSystem.description = "The artifact platform's operating system."; +``` diff --git a/sdk/containers/azcontainerregistry/blob_client.go b/sdk/containers/azcontainerregistry/blob_client.go new file mode 100644 index 000000000000..5c438d21a74e --- /dev/null +++ b/sdk/containers/azcontainerregistry/blob_client.go @@ -0,0 +1,607 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. +// DO NOT EDIT. + +package azcontainerregistry + +import ( + "context" + "errors" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "io" + "net/http" + "net/url" + "strconv" + "strings" +) + +// BlobClient contains the methods for the ContainerRegistryBlob group. +// Don't use this type directly, use NewBlobClient() instead. +type BlobClient struct { + endpoint string + pl runtime.Pipeline +} + +// CancelUpload - Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished +// uploads will eventually timeout. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - location - Link acquired from upload start or previous chunk. Note, do not include initial / (must do substring(1) ) +// - options - BlobClientCancelUploadOptions contains the optional parameters for the BlobClient.CancelUpload method. +func (client *BlobClient) CancelUpload(ctx context.Context, location string, options *BlobClientCancelUploadOptions) (BlobClientCancelUploadResponse, error) { + req, err := client.cancelUploadCreateRequest(ctx, location, options) + if err != nil { + return BlobClientCancelUploadResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientCancelUploadResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusNoContent) { + return BlobClientCancelUploadResponse{}, runtime.NewResponseError(resp) + } + return BlobClientCancelUploadResponse{}, nil +} + +// cancelUploadCreateRequest creates the CancelUpload request. +func (client *BlobClient) cancelUploadCreateRequest(ctx context.Context, location string, options *BlobClientCancelUploadOptions) (*policy.Request, error) { + urlPath := "/{nextBlobUuidLink}" + urlPath = strings.ReplaceAll(urlPath, "{nextBlobUuidLink}", location) + req, err := runtime.NewRequest(ctx, http.MethodDelete, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// CheckBlobExists - Same as GET, except only the headers are returned. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - digest - Digest of a BLOB +// - options - BlobClientCheckBlobExistsOptions contains the optional parameters for the BlobClient.CheckBlobExists method. +func (client *BlobClient) CheckBlobExists(ctx context.Context, name string, digest string, options *BlobClientCheckBlobExistsOptions) (BlobClientCheckBlobExistsResponse, error) { + req, err := client.checkBlobExistsCreateRequest(ctx, name, digest, options) + if err != nil { + return BlobClientCheckBlobExistsResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientCheckBlobExistsResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return BlobClientCheckBlobExistsResponse{}, runtime.NewResponseError(resp) + } + return client.checkBlobExistsHandleResponse(resp) +} + +// checkBlobExistsCreateRequest creates the CheckBlobExists request. +func (client *BlobClient) checkBlobExistsCreateRequest(ctx context.Context, name string, digest string, options *BlobClientCheckBlobExistsOptions) (*policy.Request, error) { + urlPath := "/v2/{name}/blobs/{digest}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if digest == "" { + return nil, errors.New("parameter digest cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{digest}", url.PathEscape(digest)) + req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// checkBlobExistsHandleResponse handles the CheckBlobExists response. +func (client *BlobClient) checkBlobExistsHandleResponse(resp *http.Response) (BlobClientCheckBlobExistsResponse, error) { + result := BlobClientCheckBlobExistsResponse{} + if val := resp.Header.Get("Content-Length"); val != "" { + contentLength, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return BlobClientCheckBlobExistsResponse{}, err + } + result.ContentLength = &contentLength + } + if val := resp.Header.Get("Docker-Content-Digest"); val != "" { + result.DockerContentDigest = &val + } + return result, nil +} + +// CheckChunkExists - Same as GET, except only the headers are returned. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - digest - Digest of a BLOB +// - rangeParam - Format : bytes=-, HTTP Range header specifying blob chunk. +// - options - BlobClientCheckChunkExistsOptions contains the optional parameters for the BlobClient.CheckChunkExists method. +func (client *BlobClient) CheckChunkExists(ctx context.Context, name string, digest string, rangeParam string, options *BlobClientCheckChunkExistsOptions) (BlobClientCheckChunkExistsResponse, error) { + req, err := client.checkChunkExistsCreateRequest(ctx, name, digest, rangeParam, options) + if err != nil { + return BlobClientCheckChunkExistsResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientCheckChunkExistsResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return BlobClientCheckChunkExistsResponse{}, runtime.NewResponseError(resp) + } + return client.checkChunkExistsHandleResponse(resp) +} + +// checkChunkExistsCreateRequest creates the CheckChunkExists request. +func (client *BlobClient) checkChunkExistsCreateRequest(ctx context.Context, name string, digest string, rangeParam string, options *BlobClientCheckChunkExistsOptions) (*policy.Request, error) { + urlPath := "/v2/{name}/blobs/{digest}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if digest == "" { + return nil, errors.New("parameter digest cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{digest}", url.PathEscape(digest)) + req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + req.Raw().Header["Range"] = []string{rangeParam} + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// checkChunkExistsHandleResponse handles the CheckChunkExists response. +func (client *BlobClient) checkChunkExistsHandleResponse(resp *http.Response) (BlobClientCheckChunkExistsResponse, error) { + result := BlobClientCheckChunkExistsResponse{} + if val := resp.Header.Get("Content-Length"); val != "" { + contentLength, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return BlobClientCheckChunkExistsResponse{}, err + } + result.ContentLength = &contentLength + } + if val := resp.Header.Get("Content-Range"); val != "" { + result.ContentRange = &val + } + return result, nil +} + +// completeUpload - Complete the upload, providing all the data in the body, if necessary. A request without a body will just +// complete the upload with previously uploaded content. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - digest - Digest of a BLOB +// - location - Link acquired from upload start or previous chunk. Note, do not include initial / (must do substring(1) ) +// - options - BlobClientCompleteUploadOptions contains the optional parameters for the BlobClient.completeUpload method. +func (client *BlobClient) completeUpload(ctx context.Context, digest string, location string, options *BlobClientCompleteUploadOptions) (BlobClientCompleteUploadResponse, error) { + req, err := client.completeUploadCreateRequest(ctx, digest, location, options) + if err != nil { + return BlobClientCompleteUploadResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientCompleteUploadResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusCreated) { + return BlobClientCompleteUploadResponse{}, runtime.NewResponseError(resp) + } + return client.completeUploadHandleResponse(resp) +} + +// completeUploadCreateRequest creates the completeUpload request. +func (client *BlobClient) completeUploadCreateRequest(ctx context.Context, digest string, location string, options *BlobClientCompleteUploadOptions) (*policy.Request, error) { + urlPath := "/{nextBlobUuidLink}" + urlPath = strings.ReplaceAll(urlPath, "{nextBlobUuidLink}", location) + req, err := runtime.NewRequest(ctx, http.MethodPut, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("digest", digest) + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// completeUploadHandleResponse handles the completeUpload response. +func (client *BlobClient) completeUploadHandleResponse(resp *http.Response) (BlobClientCompleteUploadResponse, error) { + result := BlobClientCompleteUploadResponse{} + if val := resp.Header.Get("Location"); val != "" { + result.Location = &val + } + if val := resp.Header.Get("Range"); val != "" { + result.Range = &val + } + if val := resp.Header.Get("Docker-Content-Digest"); val != "" { + result.DockerContentDigest = &val + } + return result, nil +} + +// DeleteBlob - Removes an already uploaded blob. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - digest - Digest of a BLOB +// - options - BlobClientDeleteBlobOptions contains the optional parameters for the BlobClient.DeleteBlob method. +func (client *BlobClient) DeleteBlob(ctx context.Context, name string, digest string, options *BlobClientDeleteBlobOptions) (BlobClientDeleteBlobResponse, error) { + req, err := client.deleteBlobCreateRequest(ctx, name, digest, options) + if err != nil { + return BlobClientDeleteBlobResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientDeleteBlobResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusAccepted) { + return BlobClientDeleteBlobResponse{}, runtime.NewResponseError(resp) + } + return client.deleteBlobHandleResponse(resp) +} + +// deleteBlobCreateRequest creates the DeleteBlob request. +func (client *BlobClient) deleteBlobCreateRequest(ctx context.Context, name string, digest string, options *BlobClientDeleteBlobOptions) (*policy.Request, error) { + urlPath := "/v2/{name}/blobs/{digest}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if digest == "" { + return nil, errors.New("parameter digest cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{digest}", url.PathEscape(digest)) + req, err := runtime.NewRequest(ctx, http.MethodDelete, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + return req, nil +} + +// deleteBlobHandleResponse handles the DeleteBlob response. +func (client *BlobClient) deleteBlobHandleResponse(resp *http.Response) (BlobClientDeleteBlobResponse, error) { + result := BlobClientDeleteBlobResponse{} + if val := resp.Header.Get("Docker-Content-Digest"); val != "" { + result.DockerContentDigest = &val + } + return result, nil +} + +// GetBlob - Retrieve the blob from the registry identified by digest. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - digest - Digest of a BLOB +// - options - BlobClientGetBlobOptions contains the optional parameters for the BlobClient.GetBlob method. +func (client *BlobClient) GetBlob(ctx context.Context, name string, digest string, options *BlobClientGetBlobOptions) (BlobClientGetBlobResponse, error) { + req, err := client.getBlobCreateRequest(ctx, name, digest, options) + if err != nil { + return BlobClientGetBlobResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientGetBlobResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return BlobClientGetBlobResponse{}, runtime.NewResponseError(resp) + } + return client.getBlobHandleResponse(resp) +} + +// getBlobCreateRequest creates the GetBlob request. +func (client *BlobClient) getBlobCreateRequest(ctx context.Context, name string, digest string, options *BlobClientGetBlobOptions) (*policy.Request, error) { + urlPath := "/v2/{name}/blobs/{digest}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if digest == "" { + return nil, errors.New("parameter digest cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{digest}", url.PathEscape(digest)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + runtime.SkipBodyDownload(req) + req.Raw().Header["Accept"] = []string{"application/octet-stream"} + return req, nil +} + +// getBlobHandleResponse handles the GetBlob response. +func (client *BlobClient) getBlobHandleResponse(resp *http.Response) (BlobClientGetBlobResponse, error) { + result := BlobClientGetBlobResponse{BlobData: resp.Body} + if val := resp.Header.Get("Content-Length"); val != "" { + contentLength, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return BlobClientGetBlobResponse{}, err + } + result.ContentLength = &contentLength + } + if val := resp.Header.Get("Docker-Content-Digest"); val != "" { + result.DockerContentDigest = &val + } + return result, nil +} + +// GetChunk - Retrieve the blob from the registry identified by digest. This endpoint may also support RFC7233 compliant range +// requests. Support can be detected by issuing a HEAD request. If the header +// Accept-Range: bytes is returned, range requests can be used to fetch partial content. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - digest - Digest of a BLOB +// - rangeParam - Format : bytes=-, HTTP Range header specifying blob chunk. +// - options - BlobClientGetChunkOptions contains the optional parameters for the BlobClient.GetChunk method. +func (client *BlobClient) GetChunk(ctx context.Context, name string, digest string, rangeParam string, options *BlobClientGetChunkOptions) (BlobClientGetChunkResponse, error) { + req, err := client.getChunkCreateRequest(ctx, name, digest, rangeParam, options) + if err != nil { + return BlobClientGetChunkResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientGetChunkResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusPartialContent) { + return BlobClientGetChunkResponse{}, runtime.NewResponseError(resp) + } + return client.getChunkHandleResponse(resp) +} + +// getChunkCreateRequest creates the GetChunk request. +func (client *BlobClient) getChunkCreateRequest(ctx context.Context, name string, digest string, rangeParam string, options *BlobClientGetChunkOptions) (*policy.Request, error) { + urlPath := "/v2/{name}/blobs/{digest}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if digest == "" { + return nil, errors.New("parameter digest cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{digest}", url.PathEscape(digest)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + runtime.SkipBodyDownload(req) + req.Raw().Header["Range"] = []string{rangeParam} + req.Raw().Header["Accept"] = []string{"application/octet-stream"} + return req, nil +} + +// getChunkHandleResponse handles the GetChunk response. +func (client *BlobClient) getChunkHandleResponse(resp *http.Response) (BlobClientGetChunkResponse, error) { + result := BlobClientGetChunkResponse{ChunkData: resp.Body} + if val := resp.Header.Get("Content-Length"); val != "" { + contentLength, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return BlobClientGetChunkResponse{}, err + } + result.ContentLength = &contentLength + } + if val := resp.Header.Get("Content-Range"); val != "" { + result.ContentRange = &val + } + return result, nil +} + +// GetUploadStatus - Retrieve status of upload identified by uuid. The primary purpose of this endpoint is to resolve the +// current status of a resumable upload. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - location - Link acquired from upload start or previous chunk. Note, do not include initial / (must do substring(1) ) +// - options - BlobClientGetUploadStatusOptions contains the optional parameters for the BlobClient.GetUploadStatus method. +func (client *BlobClient) GetUploadStatus(ctx context.Context, location string, options *BlobClientGetUploadStatusOptions) (BlobClientGetUploadStatusResponse, error) { + req, err := client.getUploadStatusCreateRequest(ctx, location, options) + if err != nil { + return BlobClientGetUploadStatusResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientGetUploadStatusResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusNoContent) { + return BlobClientGetUploadStatusResponse{}, runtime.NewResponseError(resp) + } + return client.getUploadStatusHandleResponse(resp) +} + +// getUploadStatusCreateRequest creates the GetUploadStatus request. +func (client *BlobClient) getUploadStatusCreateRequest(ctx context.Context, location string, options *BlobClientGetUploadStatusOptions) (*policy.Request, error) { + urlPath := "/{nextBlobUuidLink}" + urlPath = strings.ReplaceAll(urlPath, "{nextBlobUuidLink}", location) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// getUploadStatusHandleResponse handles the GetUploadStatus response. +func (client *BlobClient) getUploadStatusHandleResponse(resp *http.Response) (BlobClientGetUploadStatusResponse, error) { + result := BlobClientGetUploadStatusResponse{} + if val := resp.Header.Get("Range"); val != "" { + result.Range = &val + } + if val := resp.Header.Get("Docker-Upload-UUID"); val != "" { + result.DockerUploadUUID = &val + } + return result, nil +} + +// MountBlob - Mount a blob identified by the mount parameter from another repository. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - from - Name of the source repository. +// - mount - Digest of blob to mount from the source repository. +// - options - BlobClientMountBlobOptions contains the optional parameters for the BlobClient.MountBlob method. +func (client *BlobClient) MountBlob(ctx context.Context, name string, from string, mount string, options *BlobClientMountBlobOptions) (BlobClientMountBlobResponse, error) { + req, err := client.mountBlobCreateRequest(ctx, name, from, mount, options) + if err != nil { + return BlobClientMountBlobResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientMountBlobResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusCreated, http.StatusAccepted) { + return BlobClientMountBlobResponse{}, runtime.NewResponseError(resp) + } + return client.mountBlobHandleResponse(resp) +} + +// mountBlobCreateRequest creates the MountBlob request. +func (client *BlobClient) mountBlobCreateRequest(ctx context.Context, name string, from string, mount string, options *BlobClientMountBlobOptions) (*policy.Request, error) { + urlPath := "/v2/{name}/blobs/uploads/" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + req, err := runtime.NewRequest(ctx, http.MethodPost, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("from", from) + reqQP.Set("mount", mount) + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// mountBlobHandleResponse handles the MountBlob response. +func (client *BlobClient) mountBlobHandleResponse(resp *http.Response) (BlobClientMountBlobResponse, error) { + result := BlobClientMountBlobResponse{} + if val := resp.Header.Get("Location"); val != "" { + result.Location = &val + } + if val := resp.Header.Get("Docker-Upload-UUID"); val != "" { + result.DockerUploadUUID = &val + } + if val := resp.Header.Get("Docker-Content-Digest"); val != "" { + result.DockerContentDigest = &val + } + return result, nil +} + +// StartUpload - Initiate a resumable blob upload with an empty request body. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - options - BlobClientStartUploadOptions contains the optional parameters for the BlobClient.StartUpload method. +func (client *BlobClient) StartUpload(ctx context.Context, name string, options *BlobClientStartUploadOptions) (BlobClientStartUploadResponse, error) { + req, err := client.startUploadCreateRequest(ctx, name, options) + if err != nil { + return BlobClientStartUploadResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientStartUploadResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusAccepted) { + return BlobClientStartUploadResponse{}, runtime.NewResponseError(resp) + } + return client.startUploadHandleResponse(resp) +} + +// startUploadCreateRequest creates the StartUpload request. +func (client *BlobClient) startUploadCreateRequest(ctx context.Context, name string, options *BlobClientStartUploadOptions) (*policy.Request, error) { + urlPath := "/v2/{name}/blobs/uploads/" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + req, err := runtime.NewRequest(ctx, http.MethodPost, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// startUploadHandleResponse handles the StartUpload response. +func (client *BlobClient) startUploadHandleResponse(resp *http.Response) (BlobClientStartUploadResponse, error) { + result := BlobClientStartUploadResponse{} + if val := resp.Header.Get("Location"); val != "" { + result.Location = &val + } + if val := resp.Header.Get("Range"); val != "" { + result.Range = &val + } + if val := resp.Header.Get("Docker-Upload-UUID"); val != "" { + result.DockerUploadUUID = &val + } + return result, nil +} + +// uploadChunk - Upload a stream of data without completing the upload. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - location - Link acquired from upload start or previous chunk. Note, do not include initial / (must do substring(1) ) +// - chunkData - Raw data of blob +// - options - blobClientUploadChunkOptions contains the optional parameters for the BlobClient.uploadChunk method. +func (client *BlobClient) uploadChunk(ctx context.Context, location string, chunkData io.ReadSeekCloser, options *blobClientUploadChunkOptions) (BlobClientUploadChunkResponse, error) { + req, err := client.uploadChunkCreateRequest(ctx, location, chunkData, options) + if err != nil { + return BlobClientUploadChunkResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return BlobClientUploadChunkResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusAccepted) { + return BlobClientUploadChunkResponse{}, runtime.NewResponseError(resp) + } + return client.uploadChunkHandleResponse(resp) +} + +// uploadChunkCreateRequest creates the uploadChunk request. +func (client *BlobClient) uploadChunkCreateRequest(ctx context.Context, location string, chunkData io.ReadSeekCloser, options *blobClientUploadChunkOptions) (*policy.Request, error) { + urlPath := "/{nextBlobUuidLink}" + urlPath = strings.ReplaceAll(urlPath, "{nextBlobUuidLink}", location) + req, err := runtime.NewRequest(ctx, http.MethodPatch, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + if options != nil && options.ContentRange != nil { + req.Raw().Header["Content-Range"] = []string{*options.ContentRange} + } + req.Raw().Header["Accept"] = []string{"application/json"} + return req, req.SetBody(chunkData, "application/octet-stream") +} + +// uploadChunkHandleResponse handles the uploadChunk response. +func (client *BlobClient) uploadChunkHandleResponse(resp *http.Response) (BlobClientUploadChunkResponse, error) { + result := BlobClientUploadChunkResponse{} + if val := resp.Header.Get("Location"); val != "" { + result.Location = &val + } + if val := resp.Header.Get("Range"); val != "" { + result.Range = &val + } + if val := resp.Header.Get("Docker-Upload-UUID"); val != "" { + result.DockerUploadUUID = &val + } + return result, nil +} diff --git a/sdk/containers/azcontainerregistry/blob_client_example_test.go b/sdk/containers/azcontainerregistry/blob_client_example_test.go new file mode 100644 index 000000000000..12314bed7065 --- /dev/null +++ b/sdk/containers/azcontainerregistry/blob_client_example_test.go @@ -0,0 +1,89 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry_test + +import ( + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "log" +) + +var blobClient *azcontainerregistry.BlobClient + +func ExampleBlobClient_CancelUpload() { + _, err := blobClient.CancelUpload(context.TODO(), "v2/blobland/blobs/uploads/2b28c60d-d296-44b7-b2b4-1f01c63195c6?_nouploadcache=false&_state=VYABvUSCNW2yY5e5VabLHppXqwU0K7cvT0YUdq57KBt7Ik5hbWUiOiJibG9ibGFuZCIsIlVVSUQiOiIyYjI4YzYwZC1kMjk2LTQ0YjctYjJiNC0xZjAxYzYzMTk1YzYiLCJPZmZzZXQiOjAsIlN0YXJ0ZWRBdCI6IjIwMTktMDgtMjdUMjM6NTI6NDcuMDUzNjU2Mjg1WiJ9", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } +} + +func ExampleBlobClient_CheckBlobExists() { + res, err := blobClient.CheckBlobExists(context.TODO(), "prod/bash", "sha256:16463e0c481e161aabb735437d30b3c9c7391c2747cc564bb927e843b73dcb39", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("blob digest: %s", *res.DockerContentDigest) +} + +func ExampleBlobClient_CheckChunkExists() { + res, err := blobClient.CheckChunkExists(context.TODO(), "prod/bash", "sha256:16463e0c481e161aabb735437d30b3c9c7391c2747cc564bb927e843b73dcb39", "bytes=0-299", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("chunk size: %d", *res.ContentLength) + fmt.Printf("chunk range: %s", *res.ContentRange) +} + +func ExampleBlobClient_DeleteBlob() { + _, err := blobClient.DeleteBlob(context.TODO(), "prod/bash", "sha256:16463e0c481e161aabb735437d30b3c9c7391c2747cc564bb927e843b73dcb39", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } +} + +func ExampleBlobClient_GetBlob() { + res, err := blobClient.GetBlob(context.TODO(), "prod/bash", "sha256:16463e0c481e161aabb735437d30b3c9c7391c2747cc564bb927e843b73dcb39", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + // deal with the blob io + _ = res.BlobData +} + +func ExampleBlobClient_GetChunk() { + res, err := blobClient.GetChunk(context.TODO(), "prod/bash", "sha256:16463e0c481e161aabb735437d30b3c9c7391c2747cc564bb927e843b73dcb39", "bytes=0-299", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + // deal with the chunk io + _ = res.ChunkData +} + +func ExampleBlobClient_GetUploadStatus() { + res, err := blobClient.GetUploadStatus(context.TODO(), "v2/blobland/blobs/uploads/2b28c60d-d296-44b7-b2b4-1f01c63195c6?_nouploadcache=false&_state=VYABvUSCNW2yY5e5VabLHppXqwU0K7cvT0YUdq57KBt7Ik5hbWUiOiJibG9ibGFuZCIsIlVVSUQiOiIyYjI4YzYwZC1kMjk2LTQ0YjctYjJiNC0xZjAxYzYzMTk1YzYiLCJPZmZzZXQiOjAsIlN0YXJ0ZWRBdCI6IjIwMTktMDgtMjdUMjM6NTI6NDcuMDUzNjU2Mjg1WiJ9", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("upload UUID: %s", *res.DockerUploadUUID) +} + +func ExampleBlobClient_MountBlob() { + res, err := blobClient.MountBlob(context.TODO(), "newimage", "prod/bash", "sha256:16463e0c481e161aabb735437d30b3c9c7391c2747cc564bb927e843b73dcb39", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("upload location: %s", *res.Location) +} + +func ExampleBlobClient_StartUpload() { + res, err := blobClient.StartUpload(context.TODO(), "newimg", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("upload location: %s", *res.Location) +} diff --git a/sdk/containers/azcontainerregistry/blob_client_test.go b/sdk/containers/azcontainerregistry/blob_client_test.go new file mode 100644 index 000000000000..e31e76428714 --- /dev/null +++ b/sdk/containers/azcontainerregistry/blob_client_test.go @@ -0,0 +1,146 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "bytes" + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming" + "github.com/stretchr/testify/require" + "io" + "testing" +) + +func TestBlobClient_CancelUpload(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + startRes, err := client.StartUpload(ctx, "hello-world-test", nil) + require.NoError(t, err) + _, err = client.CancelUpload(ctx, *startRes.Location, nil) + require.NoError(t, err) +} + +func TestBlobClient_CheckBlobExists(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54" + res, err := client.CheckBlobExists(ctx, "hello-world", digest, nil) + require.NoError(t, err) + require.Equal(t, digest, *res.DockerContentDigest) +} + +func TestBlobClient_CheckChunkExists(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54" + res, err := client.CheckChunkExists(ctx, "hello-world", digest, "bytes=0-299", nil) + require.NoError(t, err) + require.NotEmpty(t, *res.ContentLength) +} + +func TestBlobClient_completeUpload_wrongDigest(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54" + getRes, err := client.GetBlob(ctx, "hello-world", digest, nil) + require.NoError(t, err) + blob, err := io.ReadAll(getRes.BlobData) + require.NoError(t, err) + startRes, err := client.StartUpload(ctx, "hello-world-test", nil) + require.NoError(t, err) + uploadResp, err := client.uploadChunk(ctx, *startRes.Location, streaming.NopCloser(bytes.NewReader(blob)), nil) + require.NoError(t, err) + _, err = client.completeUpload(ctx, "sha256:00000000", *uploadResp.Location, nil) + require.Error(t, err) +} + +func TestBlobClient_DeleteBlob(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54" + _, err = client.DeleteBlob(ctx, "hello-world-test", digest, nil) + require.NoError(t, err) +} + +func TestBlobClient_GetBlob(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54" + res, err := client.GetBlob(ctx, "hello-world", digest, nil) + require.NoError(t, err) + require.NotEmpty(t, *res.ContentLength) +} + +func TestBlobClient_GetChunk(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:feac5306138255e28a9862d3f3d29025d0a4d0648855afe1acd6131af07138ac" + res, err := client.GetChunk(ctx, "ubuntu", digest, "bytes=0-1000", nil) + require.NoError(t, err) + require.NotEmpty(t, *res.ContentLength) +} + +func TestBlobClient_GetUploadStatus(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + startRes, err := client.StartUpload(ctx, "hello-world-test", nil) + require.NoError(t, err) + checkResp, err := client.GetUploadStatus(ctx, *startRes.Location, nil) + require.NoError(t, err) + require.NotEmpty(t, *checkResp.DockerUploadUUID) + _, err = client.CancelUpload(ctx, *startRes.Location, nil) + require.NoError(t, err) +} + +func TestBlobClient_MountBlob(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54" + res, err := client.MountBlob(ctx, "hello-world", "hello-world-test", digest, nil) + require.NoError(t, err) + require.NotEmpty(t, res.Location) +} + +func TestBlobClient_StartUpload(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + startRes, err := client.StartUpload(ctx, "hello-world-test", nil) + require.NoError(t, err) + require.NotEmpty(t, *startRes.Location) + _, err = client.CancelUpload(ctx, *startRes.Location, nil) + require.NoError(t, err) +} diff --git a/sdk/containers/azcontainerregistry/blob_custom_client.go b/sdk/containers/azcontainerregistry/blob_custom_client.go new file mode 100644 index 000000000000..e1a0fc350177 --- /dev/null +++ b/sdk/containers/azcontainerregistry/blob_custom_client.go @@ -0,0 +1,113 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "hash" + "io" + "reflect" +) + +// BlobClientOptions contains the optional parameters for the NewBlobClient method. +type BlobClientOptions struct { + azcore.ClientOptions +} + +// NewBlobClient creates a new instance of BlobClient with the specified values. +// - endpoint - registry login URL +// - credential - used to authorize requests. Usually a credential from azidentity. +// - options - client options, pass nil to accept the default values. +func NewBlobClient(endpoint string, credential azcore.TokenCredential, options *BlobClientOptions) (*BlobClient, error) { + if options == nil { + options = &BlobClientOptions{} + } + + if reflect.ValueOf(options.Cloud).IsZero() { + options.Cloud = cloud.AzurePublic + } + c, ok := options.Cloud.Services[ServiceName] + if !ok || c.Audience == "" { + return nil, errors.New("provided Cloud field is missing Azure Container Registry configuration") + } + + authClient := newAuthenticationClient(endpoint, &authenticationClientOptions{ + options.ClientOptions, + }) + authPolicy := newAuthenticationPolicy( + credential, + []string{c.Audience + "/.default"}, + authClient, + nil, + ) + + pl := runtime.NewPipeline(moduleName, moduleVersion, runtime.PipelineOptions{PerRetry: []policy.Policy{authPolicy}}, &options.ClientOptions) + return &BlobClient{ + endpoint, + pl, + }, nil +} + +// BlobDigestCalculator help to calculate blob digest when uploading blob. +// Don't use this type directly, use NewBlobDigestCalculator() instead. +type BlobDigestCalculator struct { + h hash.Hash +} + +type wrappedReadSeeker struct { + io.Reader + io.Seeker +} + +// NewBlobDigestCalculator creates a new calculator to help to calculate blob digest when uploading blob. +func NewBlobDigestCalculator() *BlobDigestCalculator { + return &BlobDigestCalculator{ + h: sha256.New(), + } +} + +// BlobClientUploadChunkOptions contains the optional parameters for the BlobClient.UploadChunk method. +type BlobClientUploadChunkOptions struct { + // Start of range for the blob to be uploaded. + RangeStart *int32 + // End of range for the blob to be uploaded, inclusive. + RangeEnd *int32 +} + +// UploadChunk - Upload a stream of data without completing the upload. +// +// - location - Link acquired from upload start or previous chunk +// - chunkData - Raw data of blob +// - blobDigestCalculator - Calculator that help to calculate blob digest +// - options - BlobClientUploadChunkOptions contains the optional parameters for the BlobClient.UploadChunk method. +func (client *BlobClient) UploadChunk(ctx context.Context, location string, chunkData io.ReadSeeker, blobDigestCalculator *BlobDigestCalculator, options *BlobClientUploadChunkOptions) (BlobClientUploadChunkResponse, error) { + wrappedChunkData := &wrappedReadSeeker{Reader: io.TeeReader(chunkData, blobDigestCalculator.h), Seeker: chunkData} + var requestOptions *blobClientUploadChunkOptions + if options != nil && options.RangeStart != nil && options.RangeEnd != nil { + requestOptions = &blobClientUploadChunkOptions{ContentRange: to.Ptr(fmt.Sprintf("%d-%d", *options.RangeStart, *options.RangeEnd))} + } + return client.uploadChunk(ctx, location, streaming.NopCloser(wrappedChunkData), requestOptions) +} + +// CompleteUpload - Complete the upload with previously uploaded content. +// +// - digest - Digest of a BLOB +// - location - Link acquired from upload start or previous chunk +// - blobDigestCalculator - Calculator that help to calculate blob digest +// - options - BlobClientCompleteUploadOptions contains the optional parameters for the BlobClient.CompleteUpload method. +func (client *BlobClient) CompleteUpload(ctx context.Context, location string, blobDigestCalculator *BlobDigestCalculator, options *BlobClientCompleteUploadOptions) (BlobClientCompleteUploadResponse, error) { + return client.completeUpload(ctx, fmt.Sprintf("sha256:%x", blobDigestCalculator.h.Sum(nil)), location, options) +} diff --git a/sdk/containers/azcontainerregistry/blob_custom_client_example_test.go b/sdk/containers/azcontainerregistry/blob_custom_client_example_test.go new file mode 100644 index 000000000000..6707e0d99367 --- /dev/null +++ b/sdk/containers/azcontainerregistry/blob_custom_client_example_test.go @@ -0,0 +1,50 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry_test + +import ( + "bytes" + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "log" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" +) + +func ExampleNewBlobClient() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + blobClient, err = azcontainerregistry.NewBlobClient("https://example.azurecr.io", cred, nil) + if err != nil { + log.Fatalf("failed to create blob client: %v", err) + } + _ = blobClient +} + +func ExampleBlobClient_CompleteUpload() { + // calculator should be created when starting upload blob and passing to UploadChunk and CompleteUpload method + calculator := azcontainerregistry.NewBlobDigestCalculator() + res, err := blobClient.CompleteUpload(context.TODO(), "v2/blobland/blobs/uploads/2b28c60d-d296-44b7-b2b4-1f01c63195c6?_nouploadcache=false&_state=VYABvUSCNW2yY5e5VabLHppXqwU0K7cvT0YUdq57KBt7Ik5hbWUiOiJibG9ibGFuZCIsIlVVSUQiOiIyYjI4YzYwZC1kMjk2LTQ0YjctYjJiNC0xZjAxYzYzMTk1YzYiLCJPZmZzZXQiOjAsIlN0YXJ0ZWRBdCI6IjIwMTktMDgtMjdUMjM6NTI6NDcuMDUzNjU2Mjg1WiJ9", calculator, nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("content digest: %s", *res.DockerContentDigest) +} + +func ExampleBlobClient_UploadChunk() { + // calculator should be created when starting upload blob and passing to UploadChunk and CompleteUpload method + calculator := azcontainerregistry.NewBlobDigestCalculator() + res, err := blobClient.UploadChunk(context.TODO(), "v2/blobland/blobs/uploads/2b28c60d-d296-44b7-b2b4-1f01c63195c6?_nouploadcache=false&_state=VYABvUSCNW2yY5e5VabLHppXqwU0K7cvT0YUdq57KBt7Ik5hbWUiOiJibG9ibGFuZCIsIlVVSUQiOiIyYjI4YzYwZC1kMjk2LTQ0YjctYjJiNC0xZjAxYzYzMTk1YzYiLCJPZmZzZXQiOjAsIlN0YXJ0ZWRBdCI6IjIwMTktMDgtMjdUMjM6NTI6NDcuMDUzNjU2Mjg1WiJ9", streaming.NopCloser(bytes.NewReader([]byte("U29tZXRoaW5nRWxzZQ=="))), calculator, nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("upload location: %s", *res.Location) +} diff --git a/sdk/containers/azcontainerregistry/blob_custom_client_test.go b/sdk/containers/azcontainerregistry/blob_custom_client_test.go new file mode 100644 index 000000000000..fac57270a6cf --- /dev/null +++ b/sdk/containers/azcontainerregistry/blob_custom_client_test.go @@ -0,0 +1,99 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "bytes" + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/stretchr/testify/require" + "io" + "testing" +) + +func TestBlobClient_CompleteUpload(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54" + getRes, err := client.GetBlob(ctx, "hello-world", digest, nil) + require.NoError(t, err) + blob, err := io.ReadAll(getRes.BlobData) + require.NoError(t, err) + startRes, err := client.StartUpload(ctx, "hello-world-test", nil) + require.NoError(t, err) + calculator := NewBlobDigestCalculator() + uploadResp, err := client.UploadChunk(ctx, *startRes.Location, bytes.NewReader(blob), calculator, nil) + require.NoError(t, err) + completeResp, err := client.CompleteUpload(ctx, *uploadResp.Location, calculator, nil) + require.NoError(t, err) + require.NotEmpty(t, *completeResp.DockerContentDigest) +} + +func TestBlobClient_UploadChunk(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54" + getRes, err := client.GetBlob(ctx, "hello-world", digest, nil) + require.NoError(t, err) + blob, err := io.ReadAll(getRes.BlobData) + require.NoError(t, err) + startRes, err := client.StartUpload(ctx, "hello-world-test", nil) + require.NoError(t, err) + calculator := NewBlobDigestCalculator() + uploadResp, err := client.UploadChunk(ctx, *startRes.Location, bytes.NewReader(blob), calculator, nil) + require.NoError(t, err) + require.NotEmpty(t, *uploadResp.Location) + _, err = client.CancelUpload(ctx, *uploadResp.Location, nil) + require.NoError(t, err) +} + +func TestBlobClient_CompleteUpload_uploadByChunk(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewBlobClient("https://azacrlivetest.azurecr.io", cred, &BlobClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54" + getRes, err := client.GetBlob(ctx, "hello-world", digest, nil) + require.NoError(t, err) + blob, err := io.ReadAll(getRes.BlobData) + require.NoError(t, err) + startRes, err := client.StartUpload(ctx, "hello-world-test", nil) + require.NoError(t, err) + calculator := NewBlobDigestCalculator() + oriReader := bytes.NewReader(blob) + firstPart := io.NewSectionReader(oriReader, int64(0), int64(len(blob)/2)) + secondPart := io.NewSectionReader(oriReader, int64(len(blob)/2), int64(len(blob)-len(blob)/2)) + uploadResp, err := client.UploadChunk(ctx, *startRes.Location, firstPart, calculator, &BlobClientUploadChunkOptions{RangeStart: to.Ptr(int32(0)), RangeEnd: to.Ptr(int32(len(blob)/2 - 1))}) + require.NoError(t, err) + require.NotEmpty(t, *uploadResp.Location) + uploadResp, err = client.UploadChunk(ctx, *uploadResp.Location, secondPart, calculator, &BlobClientUploadChunkOptions{RangeStart: to.Ptr(int32(len(blob) / 2)), RangeEnd: to.Ptr(int32(len(blob) - 1))}) + require.NoError(t, err) + require.NotEmpty(t, *uploadResp.Location) + completeResp, err := client.CompleteUpload(ctx, *uploadResp.Location, calculator, nil) + require.NoError(t, err) + require.NotEmpty(t, *completeResp.DockerContentDigest) +} + +func TestNewBlobClient(t *testing.T) { + client, err := NewBlobClient("test", nil, nil) + require.NoError(t, err) + require.NotNil(t, client) + wrongCloudConfig := cloud.Configuration{ + ActiveDirectoryAuthorityHost: "test", Services: map[cloud.ServiceName]cloud.ServiceConfiguration{}, + } + _, err = NewBlobClient("test", nil, &BlobClientOptions{ClientOptions: azcore.ClientOptions{Cloud: wrongCloudConfig}}) + require.Errorf(t, err, "provided Cloud field is missing Azure Container Registry configuration") +} diff --git a/sdk/containers/azcontainerregistry/build.go b/sdk/containers/azcontainerregistry/build.go new file mode 100644 index 000000000000..4a04d3069d1d --- /dev/null +++ b/sdk/containers/azcontainerregistry/build.go @@ -0,0 +1,10 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +//go:generate autorest --version=3.8.2 --debug ./autorest.md +//go:generate gofmt -w . + +package azcontainerregistry diff --git a/sdk/containers/azcontainerregistry/ci.yml b/sdk/containers/azcontainerregistry/ci.yml new file mode 100644 index 000000000000..5650e01f5bfd --- /dev/null +++ b/sdk/containers/azcontainerregistry/ci.yml @@ -0,0 +1,28 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. +trigger: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/containers/azcontainerregistry + +pr: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/containers/azcontainerregistry + +stages: +- template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml + parameters: + ServiceDirectory: 'containers/azcontainerregistry' + RunLiveTests: true diff --git a/sdk/containers/azcontainerregistry/client.go b/sdk/containers/azcontainerregistry/client.go new file mode 100644 index 000000000000..2dd119a1c311 --- /dev/null +++ b/sdk/containers/azcontainerregistry/client.go @@ -0,0 +1,804 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. +// DO NOT EDIT. + +package azcontainerregistry + +import ( + "context" + "errors" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "io" + "net/http" + "net/url" + "strconv" + "strings" +) + +// Client contains the methods for the ContainerRegistry group. +// Don't use this type directly, use NewClient() instead. +type Client struct { + endpoint string + pl runtime.Pipeline +} + +// DeleteManifest - Delete the manifest identified by name and reference. Note that a manifest can only be deleted by digest. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - digest - Digest of a BLOB +// - options - ClientDeleteManifestOptions contains the optional parameters for the Client.DeleteManifest method. +func (client *Client) DeleteManifest(ctx context.Context, name string, digest string, options *ClientDeleteManifestOptions) (ClientDeleteManifestResponse, error) { + req, err := client.deleteManifestCreateRequest(ctx, name, digest, options) + if err != nil { + return ClientDeleteManifestResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientDeleteManifestResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusAccepted, http.StatusNotFound) { + return ClientDeleteManifestResponse{}, runtime.NewResponseError(resp) + } + return ClientDeleteManifestResponse{}, nil +} + +// deleteManifestCreateRequest creates the DeleteManifest request. +func (client *Client) deleteManifestCreateRequest(ctx context.Context, name string, digest string, options *ClientDeleteManifestOptions) (*policy.Request, error) { + urlPath := "/v2/{name}/manifests/{reference}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if digest == "" { + return nil, errors.New("parameter digest cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{reference}", url.PathEscape(digest)) + req, err := runtime.NewRequest(ctx, http.MethodDelete, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// DeleteRepository - Delete the repository identified by name +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - options - ClientDeleteRepositoryOptions contains the optional parameters for the Client.DeleteRepository method. +func (client *Client) DeleteRepository(ctx context.Context, name string, options *ClientDeleteRepositoryOptions) (ClientDeleteRepositoryResponse, error) { + req, err := client.deleteRepositoryCreateRequest(ctx, name, options) + if err != nil { + return ClientDeleteRepositoryResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientDeleteRepositoryResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusAccepted, http.StatusNotFound) { + return ClientDeleteRepositoryResponse{}, runtime.NewResponseError(resp) + } + return ClientDeleteRepositoryResponse{}, nil +} + +// deleteRepositoryCreateRequest creates the DeleteRepository request. +func (client *Client) deleteRepositoryCreateRequest(ctx context.Context, name string, options *ClientDeleteRepositoryOptions) (*policy.Request, error) { + urlPath := "/acr/v1/{name}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + req, err := runtime.NewRequest(ctx, http.MethodDelete, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// DeleteTag - Delete tag +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - tag - Tag name +// - options - ClientDeleteTagOptions contains the optional parameters for the Client.DeleteTag method. +func (client *Client) DeleteTag(ctx context.Context, name string, tag string, options *ClientDeleteTagOptions) (ClientDeleteTagResponse, error) { + req, err := client.deleteTagCreateRequest(ctx, name, tag, options) + if err != nil { + return ClientDeleteTagResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientDeleteTagResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusAccepted, http.StatusNotFound) { + return ClientDeleteTagResponse{}, runtime.NewResponseError(resp) + } + return ClientDeleteTagResponse{}, nil +} + +// deleteTagCreateRequest creates the DeleteTag request. +func (client *Client) deleteTagCreateRequest(ctx context.Context, name string, tag string, options *ClientDeleteTagOptions) (*policy.Request, error) { + urlPath := "/acr/v1/{name}/_tags/{reference}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if tag == "" { + return nil, errors.New("parameter tag cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{reference}", url.PathEscape(tag)) + req, err := runtime.NewRequest(ctx, http.MethodDelete, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// GetManifest - Get the manifest identified by name and reference where reference can be a tag or digest. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - reference - A tag or a digest, pointing to a specific image +// - options - ClientGetManifestOptions contains the optional parameters for the Client.GetManifest method. +func (client *Client) GetManifest(ctx context.Context, name string, reference string, options *ClientGetManifestOptions) (ClientGetManifestResponse, error) { + req, err := client.getManifestCreateRequest(ctx, name, reference, options) + if err != nil { + return ClientGetManifestResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientGetManifestResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ClientGetManifestResponse{}, runtime.NewResponseError(resp) + } + return client.getManifestHandleResponse(resp) +} + +// getManifestCreateRequest creates the GetManifest request. +func (client *Client) getManifestCreateRequest(ctx context.Context, name string, reference string, options *ClientGetManifestOptions) (*policy.Request, error) { + urlPath := "/v2/{name}/manifests/{reference}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if reference == "" { + return nil, errors.New("parameter reference cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{reference}", url.PathEscape(reference)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + runtime.SkipBodyDownload(req) + if options != nil && options.Accept != nil { + req.Raw().Header["accept"] = []string{*options.Accept} + } + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// getManifestHandleResponse handles the GetManifest response. +func (client *Client) getManifestHandleResponse(resp *http.Response) (ClientGetManifestResponse, error) { + result := ClientGetManifestResponse{ManifestData: resp.Body} + if val := resp.Header.Get("Docker-Content-Digest"); val != "" { + result.DockerContentDigest = &val + } + return result, nil +} + +// GetManifestProperties - Get manifest attributes +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - digest - Digest of a BLOB +// - options - ClientGetManifestPropertiesOptions contains the optional parameters for the Client.GetManifestProperties method. +func (client *Client) GetManifestProperties(ctx context.Context, name string, digest string, options *ClientGetManifestPropertiesOptions) (ClientGetManifestPropertiesResponse, error) { + req, err := client.getManifestPropertiesCreateRequest(ctx, name, digest, options) + if err != nil { + return ClientGetManifestPropertiesResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientGetManifestPropertiesResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ClientGetManifestPropertiesResponse{}, runtime.NewResponseError(resp) + } + return client.getManifestPropertiesHandleResponse(resp) +} + +// getManifestPropertiesCreateRequest creates the GetManifestProperties request. +func (client *Client) getManifestPropertiesCreateRequest(ctx context.Context, name string, digest string, options *ClientGetManifestPropertiesOptions) (*policy.Request, error) { + urlPath := "/acr/v1/{name}/_manifests/{digest}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if digest == "" { + return nil, errors.New("parameter digest cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{digest}", url.PathEscape(digest)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// getManifestPropertiesHandleResponse handles the GetManifestProperties response. +func (client *Client) getManifestPropertiesHandleResponse(resp *http.Response) (ClientGetManifestPropertiesResponse, error) { + result := ClientGetManifestPropertiesResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.ArtifactManifestProperties); err != nil { + return ClientGetManifestPropertiesResponse{}, err + } + return result, nil +} + +// GetRepositoryProperties - Get repository attributes +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - options - ClientGetRepositoryPropertiesOptions contains the optional parameters for the Client.GetRepositoryProperties +// method. +func (client *Client) GetRepositoryProperties(ctx context.Context, name string, options *ClientGetRepositoryPropertiesOptions) (ClientGetRepositoryPropertiesResponse, error) { + req, err := client.getRepositoryPropertiesCreateRequest(ctx, name, options) + if err != nil { + return ClientGetRepositoryPropertiesResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientGetRepositoryPropertiesResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ClientGetRepositoryPropertiesResponse{}, runtime.NewResponseError(resp) + } + return client.getRepositoryPropertiesHandleResponse(resp) +} + +// getRepositoryPropertiesCreateRequest creates the GetRepositoryProperties request. +func (client *Client) getRepositoryPropertiesCreateRequest(ctx context.Context, name string, options *ClientGetRepositoryPropertiesOptions) (*policy.Request, error) { + urlPath := "/acr/v1/{name}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// getRepositoryPropertiesHandleResponse handles the GetRepositoryProperties response. +func (client *Client) getRepositoryPropertiesHandleResponse(resp *http.Response) (ClientGetRepositoryPropertiesResponse, error) { + result := ClientGetRepositoryPropertiesResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.ContainerRepositoryProperties); err != nil { + return ClientGetRepositoryPropertiesResponse{}, err + } + return result, nil +} + +// GetTagProperties - Get tag attributes by tag +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - tag - Tag name +// - options - ClientGetTagPropertiesOptions contains the optional parameters for the Client.GetTagProperties method. +func (client *Client) GetTagProperties(ctx context.Context, name string, tag string, options *ClientGetTagPropertiesOptions) (ClientGetTagPropertiesResponse, error) { + req, err := client.getTagPropertiesCreateRequest(ctx, name, tag, options) + if err != nil { + return ClientGetTagPropertiesResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientGetTagPropertiesResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ClientGetTagPropertiesResponse{}, runtime.NewResponseError(resp) + } + return client.getTagPropertiesHandleResponse(resp) +} + +// getTagPropertiesCreateRequest creates the GetTagProperties request. +func (client *Client) getTagPropertiesCreateRequest(ctx context.Context, name string, tag string, options *ClientGetTagPropertiesOptions) (*policy.Request, error) { + urlPath := "/acr/v1/{name}/_tags/{reference}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if tag == "" { + return nil, errors.New("parameter tag cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{reference}", url.PathEscape(tag)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// getTagPropertiesHandleResponse handles the GetTagProperties response. +func (client *Client) getTagPropertiesHandleResponse(resp *http.Response) (ClientGetTagPropertiesResponse, error) { + result := ClientGetTagPropertiesResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.ArtifactTagProperties); err != nil { + return ClientGetTagPropertiesResponse{}, err + } + return result, nil +} + +// NewListManifestsPager - List manifests of a repository +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - options - ClientListManifestsOptions contains the optional parameters for the Client.NewListManifestsPager method. +func (client *Client) NewListManifestsPager(name string, options *ClientListManifestsOptions) *runtime.Pager[ClientListManifestsResponse] { + return runtime.NewPager(runtime.PagingHandler[ClientListManifestsResponse]{ + More: func(page ClientListManifestsResponse) bool { + return page.Link != nil && len(*page.Link) > 0 + }, + Fetcher: func(ctx context.Context, page *ClientListManifestsResponse) (ClientListManifestsResponse, error) { + var req *policy.Request + var err error + if page == nil { + req, err = client.listManifestsCreateRequest(ctx, name, options) + } else { + req, err = runtime.NewRequest(ctx, http.MethodGet, *page.Link) + } + if err != nil { + return ClientListManifestsResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientListManifestsResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ClientListManifestsResponse{}, runtime.NewResponseError(resp) + } + return client.listManifestsHandleResponse(resp) + }, + }) +} + +// listManifestsCreateRequest creates the ListManifests request. +func (client *Client) listManifestsCreateRequest(ctx context.Context, name string, options *ClientListManifestsOptions) (*policy.Request, error) { + urlPath := "/acr/v1/{name}/_manifests" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + if options != nil && options.Last != nil { + reqQP.Set("last", *options.Last) + } + if options != nil && options.MaxNum != nil { + reqQP.Set("n", strconv.FormatInt(int64(*options.MaxNum), 10)) + } + reqQP.Set("api-version", "2021-07-01") + if options != nil && options.OrderBy != nil { + reqQP.Set("orderby", string(*options.OrderBy)) + } + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// listManifestsHandleResponse handles the ListManifests response. +func (client *Client) listManifestsHandleResponse(resp *http.Response) (ClientListManifestsResponse, error) { + result := ClientListManifestsResponse{} + if val := resp.Header.Get("Link"); val != "" { + val = runtime.JoinPaths(client.endpoint, extractNextLink(val)) + result.Link = &val + } + if err := runtime.UnmarshalAsJSON(resp, &result.Manifests); err != nil { + return ClientListManifestsResponse{}, err + } + return result, nil +} + +// NewListRepositoriesPager - List repositories +// +// Generated from API version 2021-07-01 +// - options - ClientListRepositoriesOptions contains the optional parameters for the Client.NewListRepositoriesPager method. +func (client *Client) NewListRepositoriesPager(options *ClientListRepositoriesOptions) *runtime.Pager[ClientListRepositoriesResponse] { + return runtime.NewPager(runtime.PagingHandler[ClientListRepositoriesResponse]{ + More: func(page ClientListRepositoriesResponse) bool { + return page.Link != nil && len(*page.Link) > 0 + }, + Fetcher: func(ctx context.Context, page *ClientListRepositoriesResponse) (ClientListRepositoriesResponse, error) { + var req *policy.Request + var err error + if page == nil { + req, err = client.listRepositoriesCreateRequest(ctx, options) + } else { + req, err = runtime.NewRequest(ctx, http.MethodGet, *page.Link) + } + if err != nil { + return ClientListRepositoriesResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientListRepositoriesResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ClientListRepositoriesResponse{}, runtime.NewResponseError(resp) + } + return client.listRepositoriesHandleResponse(resp) + }, + }) +} + +// listRepositoriesCreateRequest creates the ListRepositories request. +func (client *Client) listRepositoriesCreateRequest(ctx context.Context, options *ClientListRepositoriesOptions) (*policy.Request, error) { + urlPath := "/acr/v1/_catalog" + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + if options != nil && options.Last != nil { + reqQP.Set("last", *options.Last) + } + if options != nil && options.MaxNum != nil { + reqQP.Set("n", strconv.FormatInt(int64(*options.MaxNum), 10)) + } + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// listRepositoriesHandleResponse handles the ListRepositories response. +func (client *Client) listRepositoriesHandleResponse(resp *http.Response) (ClientListRepositoriesResponse, error) { + result := ClientListRepositoriesResponse{} + if val := resp.Header.Get("Link"); val != "" { + val = runtime.JoinPaths(client.endpoint, extractNextLink(val)) + result.Link = &val + } + if err := runtime.UnmarshalAsJSON(resp, &result.Repositories); err != nil { + return ClientListRepositoriesResponse{}, err + } + return result, nil +} + +// NewListTagsPager - List tags of a repository +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - options - ClientListTagsOptions contains the optional parameters for the Client.NewListTagsPager method. +func (client *Client) NewListTagsPager(name string, options *ClientListTagsOptions) *runtime.Pager[ClientListTagsResponse] { + return runtime.NewPager(runtime.PagingHandler[ClientListTagsResponse]{ + More: func(page ClientListTagsResponse) bool { + return page.Link != nil && len(*page.Link) > 0 + }, + Fetcher: func(ctx context.Context, page *ClientListTagsResponse) (ClientListTagsResponse, error) { + var req *policy.Request + var err error + if page == nil { + req, err = client.listTagsCreateRequest(ctx, name, options) + } else { + req, err = runtime.NewRequest(ctx, http.MethodGet, *page.Link) + } + if err != nil { + return ClientListTagsResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientListTagsResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ClientListTagsResponse{}, runtime.NewResponseError(resp) + } + return client.listTagsHandleResponse(resp) + }, + }) +} + +// listTagsCreateRequest creates the ListTags request. +func (client *Client) listTagsCreateRequest(ctx context.Context, name string, options *ClientListTagsOptions) (*policy.Request, error) { + urlPath := "/acr/v1/{name}/_tags" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + if options != nil && options.Last != nil { + reqQP.Set("last", *options.Last) + } + if options != nil && options.MaxNum != nil { + reqQP.Set("n", strconv.FormatInt(int64(*options.MaxNum), 10)) + } + if options != nil && options.Digest != nil { + reqQP.Set("digest", *options.Digest) + } + reqQP.Set("api-version", "2021-07-01") + if options != nil && options.OrderBy != nil { + reqQP.Set("orderby", string(*options.OrderBy)) + } + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// listTagsHandleResponse handles the ListTags response. +func (client *Client) listTagsHandleResponse(resp *http.Response) (ClientListTagsResponse, error) { + result := ClientListTagsResponse{} + if val := resp.Header.Get("Link"); val != "" { + val = runtime.JoinPaths(client.endpoint, extractNextLink(val)) + result.Link = &val + } + if err := runtime.UnmarshalAsJSON(resp, &result.TagList); err != nil { + return ClientListTagsResponse{}, err + } + return result, nil +} + +// UpdateManifestProperties - Update properties of a manifest +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - digest - Digest of a BLOB +// - options - ClientUpdateManifestPropertiesOptions contains the optional parameters for the Client.UpdateManifestProperties +// method. +func (client *Client) UpdateManifestProperties(ctx context.Context, name string, digest string, options *ClientUpdateManifestPropertiesOptions) (ClientUpdateManifestPropertiesResponse, error) { + req, err := client.updateManifestPropertiesCreateRequest(ctx, name, digest, options) + if err != nil { + return ClientUpdateManifestPropertiesResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientUpdateManifestPropertiesResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ClientUpdateManifestPropertiesResponse{}, runtime.NewResponseError(resp) + } + return client.updateManifestPropertiesHandleResponse(resp) +} + +// updateManifestPropertiesCreateRequest creates the UpdateManifestProperties request. +func (client *Client) updateManifestPropertiesCreateRequest(ctx context.Context, name string, digest string, options *ClientUpdateManifestPropertiesOptions) (*policy.Request, error) { + urlPath := "/acr/v1/{name}/_manifests/{digest}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if digest == "" { + return nil, errors.New("parameter digest cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{digest}", url.PathEscape(digest)) + req, err := runtime.NewRequest(ctx, http.MethodPatch, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + if options != nil && options.Value != nil { + return req, runtime.MarshalAsJSON(req, *options.Value) + } + return req, nil +} + +// updateManifestPropertiesHandleResponse handles the UpdateManifestProperties response. +func (client *Client) updateManifestPropertiesHandleResponse(resp *http.Response) (ClientUpdateManifestPropertiesResponse, error) { + result := ClientUpdateManifestPropertiesResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.ArtifactManifestProperties); err != nil { + return ClientUpdateManifestPropertiesResponse{}, err + } + return result, nil +} + +// UpdateRepositoryProperties - Update the attribute identified by name where reference is the name of the repository. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - options - ClientUpdateRepositoryPropertiesOptions contains the optional parameters for the Client.UpdateRepositoryProperties +// method. +func (client *Client) UpdateRepositoryProperties(ctx context.Context, name string, options *ClientUpdateRepositoryPropertiesOptions) (ClientUpdateRepositoryPropertiesResponse, error) { + req, err := client.updateRepositoryPropertiesCreateRequest(ctx, name, options) + if err != nil { + return ClientUpdateRepositoryPropertiesResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientUpdateRepositoryPropertiesResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ClientUpdateRepositoryPropertiesResponse{}, runtime.NewResponseError(resp) + } + return client.updateRepositoryPropertiesHandleResponse(resp) +} + +// updateRepositoryPropertiesCreateRequest creates the UpdateRepositoryProperties request. +func (client *Client) updateRepositoryPropertiesCreateRequest(ctx context.Context, name string, options *ClientUpdateRepositoryPropertiesOptions) (*policy.Request, error) { + urlPath := "/acr/v1/{name}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + req, err := runtime.NewRequest(ctx, http.MethodPatch, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + if options != nil && options.Value != nil { + return req, runtime.MarshalAsJSON(req, *options.Value) + } + return req, nil +} + +// updateRepositoryPropertiesHandleResponse handles the UpdateRepositoryProperties response. +func (client *Client) updateRepositoryPropertiesHandleResponse(resp *http.Response) (ClientUpdateRepositoryPropertiesResponse, error) { + result := ClientUpdateRepositoryPropertiesResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.ContainerRepositoryProperties); err != nil { + return ClientUpdateRepositoryPropertiesResponse{}, err + } + return result, nil +} + +// UpdateTagProperties - Update tag attributes +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - tag - Tag name +// - options - ClientUpdateTagPropertiesOptions contains the optional parameters for the Client.UpdateTagProperties method. +func (client *Client) UpdateTagProperties(ctx context.Context, name string, tag string, options *ClientUpdateTagPropertiesOptions) (ClientUpdateTagPropertiesResponse, error) { + req, err := client.updateTagPropertiesCreateRequest(ctx, name, tag, options) + if err != nil { + return ClientUpdateTagPropertiesResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientUpdateTagPropertiesResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ClientUpdateTagPropertiesResponse{}, runtime.NewResponseError(resp) + } + return client.updateTagPropertiesHandleResponse(resp) +} + +// updateTagPropertiesCreateRequest creates the UpdateTagProperties request. +func (client *Client) updateTagPropertiesCreateRequest(ctx context.Context, name string, tag string, options *ClientUpdateTagPropertiesOptions) (*policy.Request, error) { + urlPath := "/acr/v1/{name}/_tags/{reference}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if tag == "" { + return nil, errors.New("parameter tag cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{reference}", url.PathEscape(tag)) + req, err := runtime.NewRequest(ctx, http.MethodPatch, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", "2021-07-01") + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + if options != nil && options.Value != nil { + return req, runtime.MarshalAsJSON(req, *options.Value) + } + return req, nil +} + +// updateTagPropertiesHandleResponse handles the UpdateTagProperties response. +func (client *Client) updateTagPropertiesHandleResponse(resp *http.Response) (ClientUpdateTagPropertiesResponse, error) { + result := ClientUpdateTagPropertiesResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.ArtifactTagProperties); err != nil { + return ClientUpdateTagPropertiesResponse{}, err + } + return result, nil +} + +// UploadManifest - Put the manifest identified by name and reference where reference can be a tag or digest. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2021-07-01 +// - name - Name of the image (including the namespace) +// - reference - A tag or a digest, pointing to a specific image +// - contentType - Upload file type +// - manifestData - Manifest body, can take v1 or v2 values depending on accept header +// - options - ClientUploadManifestOptions contains the optional parameters for the Client.UploadManifest method. +func (client *Client) UploadManifest(ctx context.Context, name string, reference string, contentType ContentType, manifestData io.ReadSeekCloser, options *ClientUploadManifestOptions) (ClientUploadManifestResponse, error) { + req, err := client.uploadManifestCreateRequest(ctx, name, reference, contentType, manifestData, options) + if err != nil { + return ClientUploadManifestResponse{}, err + } + resp, err := client.pl.Do(req) + if err != nil { + return ClientUploadManifestResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusCreated) { + return ClientUploadManifestResponse{}, runtime.NewResponseError(resp) + } + return client.uploadManifestHandleResponse(resp) +} + +// uploadManifestCreateRequest creates the UploadManifest request. +func (client *Client) uploadManifestCreateRequest(ctx context.Context, name string, reference string, contentType ContentType, manifestData io.ReadSeekCloser, options *ClientUploadManifestOptions) (*policy.Request, error) { + urlPath := "/v2/{name}/manifests/{reference}" + if name == "" { + return nil, errors.New("parameter name cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{name}", url.PathEscape(name)) + if reference == "" { + return nil, errors.New("parameter reference cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{reference}", url.PathEscape(reference)) + req, err := runtime.NewRequest(ctx, http.MethodPut, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + req.Raw().Header["Content-Type"] = []string{string(contentType)} + req.Raw().Header["Accept"] = []string{"application/json"} + return req, req.SetBody(manifestData, string(contentType)) +} + +// uploadManifestHandleResponse handles the UploadManifest response. +func (client *Client) uploadManifestHandleResponse(resp *http.Response) (ClientUploadManifestResponse, error) { + result := ClientUploadManifestResponse{} + if val := resp.Header.Get("Docker-Content-Digest"); val != "" { + result.DockerContentDigest = &val + } + if val := resp.Header.Get("Location"); val != "" { + result.Location = &val + } + if val := resp.Header.Get("Content-Length"); val != "" { + contentLength, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return ClientUploadManifestResponse{}, err + } + result.ContentLength = &contentLength + } + return result, nil +} diff --git a/sdk/containers/azcontainerregistry/client_example_test.go b/sdk/containers/azcontainerregistry/client_example_test.go new file mode 100644 index 000000000000..3186fcca8acf --- /dev/null +++ b/sdk/containers/azcontainerregistry/client_example_test.go @@ -0,0 +1,168 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry_test + +import ( + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "io" + "log" + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" +) + +var client *azcontainerregistry.Client + +func ExampleClient_DeleteManifest() { + resp, err := client.GetTagProperties(context.TODO(), "alpine", "3.7", nil) + if err != nil { + log.Fatalf("failed to get tag properties: %v", err) + } + _, err = client.DeleteManifest(context.TODO(), "alpine", *resp.Tag.Digest, nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } +} + +func ExampleClient_DeleteRepository() { + _, err := client.DeleteRepository(context.TODO(), "nanoserver", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } +} + +func ExampleClient_DeleteTag() { + _, err := client.DeleteTag(context.TODO(), "nanoserver", "4.7.2-20180905-nanoserver-1803", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } +} + +func ExampleClient_GetManifest() { + res, err := client.GetManifest(context.TODO(), "hello-world-dangling", "20190628-033033z", &azcontainerregistry.ClientGetManifestOptions{Accept: to.Ptr("application/vnd.docker.distribution.manifest.v2+json")}) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + manifest, err := io.ReadAll(res.ManifestData) + if err != nil { + log.Fatalf("failed to read manifest data: %v", err) + } + fmt.Printf("manifest content: %s\n", manifest) +} + +func ExampleClient_GetManifestProperties() { + res, err := client.GetManifestProperties(context.TODO(), "nanoserver", "sha256:110d2b6c84592561338aa040b1b14b7ab81c2f9edbd564c2285dd7d70d777086", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("manifest digest: %s\n", *res.Manifest.Digest) + fmt.Printf("manifest size: %d\n", *res.Manifest.Size) +} + +func ExampleClient_GetRepositoryProperties() { + res, err := client.GetRepositoryProperties(context.TODO(), "nanoserver", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("repository name: %s\n", *res.Name) + fmt.Printf("registry login server of the repository: %s\n", *res.RegistryLoginServer) + fmt.Printf("repository manifest count: %d\n", *res.ManifestCount) +} + +func ExampleClient_GetTagProperties() { + res, err := client.GetTagProperties(context.TODO(), "test/bash", "latest", nil) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("tag name: %s\n", *res.Tag.Name) + fmt.Printf("tag digest: %s\n", *res.Tag.Digest) +} + +func ExampleClient_NewListManifestsPager() { + pager := client.NewListManifestsPager("nanoserver", nil) + for pager.More() { + page, err := pager.NextPage(context.TODO()) + if err != nil { + log.Fatalf("failed to advance page: %v", err) + } + for i, v := range page.Manifests.Attributes { + fmt.Printf("manifest %d: %s\n", i+1, *v.Digest) + } + } +} + +func ExampleClient_NewListRepositoriesPager() { + pager := client.NewListRepositoriesPager(nil) + for pager.More() { + page, err := pager.NextPage(context.TODO()) + if err != nil { + log.Fatalf("failed to advance page: %v", err) + } + for i, v := range page.Repositories.Names { + fmt.Printf("repository %d: %s\n", i+1, *v) + } + } +} + +func ExampleClient_NewListTagsPager() { + pager := client.NewListTagsPager("nanoserver", nil) + for pager.More() { + page, err := pager.NextPage(context.TODO()) + if err != nil { + log.Fatalf("failed to advance page: %v", err) + } + for i, v := range page.Tags { + fmt.Printf("tag %d: %s\n", i+1, *v.Name) + } + } +} + +func ExampleClient_UpdateManifestProperties() { + res, err := client.UpdateManifestProperties(context.TODO(), "nanoserver", "sha256:110d2b6c84592561338aa040b1b14b7ab81c2f9edbd564c2285dd7d70d777086", &azcontainerregistry.ClientUpdateManifestPropertiesOptions{Value: &azcontainerregistry.ManifestWriteableProperties{ + CanWrite: to.Ptr(false), + }, + }) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("repository nanoserver - manifest sha256:110d2b6c84592561338aa040b1b14b7ab81c2f9edbd564c2285dd7d70d777086 - 'CanWrite' property: %t", *res.Manifest.ChangeableAttributes.CanWrite) +} +func ExampleClient_UpdateRepositoryProperties() { + res, err := client.UpdateRepositoryProperties(context.TODO(), "nanoserver", &azcontainerregistry.ClientUpdateRepositoryPropertiesOptions{Value: &azcontainerregistry.RepositoryWriteableProperties{ + CanWrite: to.Ptr(false), + }, + }) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("repository namoserver - 'CanWrite' property: %t\n", *res.ContainerRepositoryProperties.ChangeableAttributes.CanWrite) +} + +func ExampleClient_UpdateTagProperties() { + res, err := client.UpdateTagProperties(context.TODO(), "nanoserver", "4.7.2-20180905-nanoserver-1803", &azcontainerregistry.ClientUpdateTagPropertiesOptions{ + Value: &azcontainerregistry.TagWriteableProperties{ + CanWrite: to.Ptr(false), + }}) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("repository namoserver - tag 4.7.2-20180905-nanoserver-1803 - 'CanWrite' property: %t\n", *res.Tag.ChangeableAttributes.CanWrite) +} + +func ExampleClient_UploadManifest() { + payload, err := os.Open("example-manifest.json") + if err != nil { + log.Fatalf("failed to read manifest file: %v", err) + } + resp, err := client.UploadManifest(context.TODO(), "nanoserver", "test", "application/vnd.docker.distribution.manifest.v2+json", payload, nil) + if err != nil { + log.Fatalf("failed to upload manifest: %v", err) + } + fmt.Printf("uploaded manifest digest: %s", *resp.DockerContentDigest) +} diff --git a/sdk/containers/azcontainerregistry/client_test.go b/sdk/containers/azcontainerregistry/client_test.go new file mode 100644 index 000000000000..7bc8b68705af --- /dev/null +++ b/sdk/containers/azcontainerregistry/client_test.go @@ -0,0 +1,444 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "bytes" + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/stretchr/testify/require" + "io" + "testing" +) + +func TestClient_DeleteManifest(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + resp, err := client.GetTagProperties(ctx, "hello-world", "latest", nil) + require.NoError(t, err) + _, err = client.DeleteManifest(ctx, "hello-world", *resp.Tag.Digest, nil) + require.NoError(t, err) + _, err = client.DeleteManifest(ctx, "hello-world-test", "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4", nil) + require.NoError(t, err) +} + +func TestClient_DeleteManifest_wrongDigest(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + _, err = client.DeleteManifest(ctx, "hello-world", "error-digest", nil) + require.Error(t, err) +} + +func TestClient_DeleteRepository(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + _, err = client.DeleteRepository(ctx, "hello-world", nil) + require.NoError(t, err) +} + +func TestClient_DeleteTag(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + _, err = client.DeleteTag(ctx, "hello-world", "latest", nil) + require.NoError(t, err) +} + +func TestClient_GetManifest(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + res, err := client.GetManifest(ctx, "hello-world", "latest", &ClientGetManifestOptions{Accept: to.Ptr("application/vnd.docker.distribution.manifest.v2+json")}) + require.NoError(t, err) + manifest, err := io.ReadAll(res.ManifestData) + require.NoError(t, err) + require.NotEmpty(t, manifest) + fmt.Printf("manifest content: %s\n", manifest) +} + +func TestClient_GetManifest_wrongTag(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + _, err = client.GetManifest(ctx, "hello-world", "wrong-tag", &ClientGetManifestOptions{Accept: to.Ptr("application/vnd.docker.distribution.manifest.v2+json")}) + require.Error(t, err) +} + +func TestClient_GetManifestProperties(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4" + tag := "latest" + digestRes, err := client.GetManifestProperties(ctx, "hello-world", digest, nil) + require.NoError(t, err) + require.Equal(t, *digestRes.Manifest.Digest, digest) + resp, err := client.GetTagProperties(ctx, "hello-world", tag, nil) + require.NoError(t, err) + tagRes, err := client.GetManifestProperties(ctx, "hello-world", *resp.Tag.Digest, nil) + require.NoError(t, err) + require.Equal(t, *tagRes.Manifest.Digest, digest) +} + +func TestClient_GetManifestProperties_wrongDigest(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + _, err = client.GetManifestProperties(ctx, "hello-world", "wrong-digest", nil) + require.Error(t, err) +} + +func TestClient_GetRepositoryProperties(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + res, err := client.GetRepositoryProperties(ctx, "hello-world", nil) + require.NoError(t, err) + require.NotEmpty(t, *res.Name) + fmt.Printf("repository name: %s\n", *res.Name) + require.NotEmpty(t, *res.RegistryLoginServer) + fmt.Printf("registry login server of the repository: %s\n", *res.RegistryLoginServer) + require.NotEmpty(t, *res.ManifestCount) + fmt.Printf("repository manifest count: %d\n", *res.ManifestCount) +} + +func TestClient_GetRepositoryProperties_wrongName(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + _, err = client.GetRepositoryProperties(ctx, "wrong-name", nil) + require.Error(t, err) +} + +func TestClient_GetTagProperties(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + res, err := client.GetTagProperties(ctx, "hello-world", "latest", nil) + require.NoError(t, err) + require.NotEmpty(t, *res.Tag.Name) + fmt.Printf("tag name: %s\n", *res.Tag.Name) + require.NotEmpty(t, *res.Tag.Digest) + fmt.Printf("tag digest: %s\n", *res.Tag.Digest) +} + +func TestClient_GetTagProperties_wrongTag(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + _, err = client.GetTagProperties(ctx, "hello-world", "wrong-tag", nil) + require.Error(t, err) +} + +func TestClient_NewListManifestsPager(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + pager := client.NewListManifestsPager("hello-world", &ClientListManifestsOptions{ + MaxNum: to.Ptr[int32](1), + }) + pages := 0 + items := 0 + for pager.More() { + page, err := pager.NextPage(ctx) + require.NoError(t, err) + require.NotEmpty(t, page.Manifests.Attributes) + pages++ + for i, v := range page.Manifests.Attributes { + fmt.Printf("page %d manifest %d: %s\n", pages, i+1, *v.Digest) + items++ + } + } + require.Equal(t, pages, 2) + require.Equal(t, items, 2) + + pager = client.NewListManifestsPager("hello-world", &ClientListManifestsOptions{ + OrderBy: to.Ptr(ArtifactManifestOrderByLastUpdatedOnDescending), + }) + var descendingItems []*ManifestAttributes + for pager.More() { + page, err := pager.NextPage(ctx) + require.NoError(t, err) + require.NotEmpty(t, page.Manifests.Attributes) + for i, v := range page.Manifests.Attributes { + fmt.Printf("manifest order by last updated on descending %d: %s\n", i+1, *v.Digest) + descendingItems = append(descendingItems, v) + } + } + pager = client.NewListManifestsPager("hello-world", &ClientListManifestsOptions{ + OrderBy: to.Ptr(ArtifactManifestOrderByLastUpdatedOnAscending), + }) + var ascendingItems []*ManifestAttributes + for pager.More() { + page, err := pager.NextPage(ctx) + require.NoError(t, err) + require.NotEmpty(t, page.Manifests.Attributes) + for i, v := range page.Manifests.Attributes { + fmt.Printf("manifest order by last updated on descending %d: %s\n", i+1, *v.Digest) + ascendingItems = append(ascendingItems, v) + } + } + for i := range descendingItems { + require.Equal(t, descendingItems[i].Digest, ascendingItems[len(ascendingItems)-1-i].Digest) + } +} + +func TestClient_NewListManifestsPager_wrongRepositoryName(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + pager := client.NewListManifestsPager("wrong-name", nil) + for pager.More() { + _, err := pager.NextPage(ctx) + require.Error(t, err) + break + } +} + +func TestClient_NewListRepositoriesPager(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + pager := client.NewListRepositoriesPager(&ClientListRepositoriesOptions{ + MaxNum: to.Ptr[int32](1), + }) + pages := 0 + items := 0 + for pager.More() { + page, err := pager.NextPage(ctx) + require.NoError(t, err) + require.NotEmpty(t, page.Repositories.Names) + pages++ + for i, v := range page.Repositories.Names { + fmt.Printf("page %d repository %d: %s\n", pages, i+1, *v) + items++ + } + } + require.Equal(t, pages, 3) + require.Equal(t, items, 3) +} + +func TestClient_NewListTagsPager(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + pager := client.NewListTagsPager("hello-world", &ClientListTagsOptions{ + MaxNum: to.Ptr[int32](1), + }) + pages := 0 + items := 0 + for pager.More() { + page, err := pager.NextPage(ctx) + require.NoError(t, err) + require.NotEmpty(t, page.Tags) + pages++ + require.Equal(t, len(page.Tags), 1) + for i, v := range page.Tags { + fmt.Printf("page %d tag %d: %s\n", pages, i+1, *v.Name) + items++ + } + } + require.Equal(t, pages, 3) + require.Equal(t, items, 3) + + pager = client.NewListTagsPager("hello-world", &ClientListTagsOptions{ + OrderBy: to.Ptr(ArtifactTagOrderByLastUpdatedOnDescending), + }) + var descendingItems []*TagAttributes + for pager.More() { + page, err := pager.NextPage(ctx) + require.NoError(t, err) + require.NotEmpty(t, page.Tags) + for i, v := range page.Tags { + fmt.Printf("tag order by last updated on descending %d: %s\n", i+1, *v.Name) + descendingItems = append(descendingItems, v) + } + } + pager = client.NewListTagsPager("hello-world", &ClientListTagsOptions{ + OrderBy: to.Ptr(ArtifactTagOrderByLastUpdatedOnAscending), + }) + var ascendingItems []*TagAttributes + for pager.More() { + page, err := pager.NextPage(ctx) + require.NoError(t, err) + require.NotEmpty(t, page.Tags) + for i, v := range page.Tags { + fmt.Printf("tag order by last updated on descending %d: %s\n", i+1, *v.Name) + ascendingItems = append(ascendingItems, v) + } + } + for i := range descendingItems { + require.Equal(t, descendingItems[i].Name, ascendingItems[len(ascendingItems)-1-i].Name) + } +} + +func TestClient_NewListTagsPager_wrongRepositoryName(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + pager := client.NewListTagsPager("wrong-name", nil) + for pager.More() { + _, err := pager.NextPage(ctx) + require.Error(t, err) + break + } +} + +func TestClient_UpdateManifestProperties(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + digest := "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4" + tag := "latest" + resp, err := client.GetTagProperties(ctx, "hello-world", tag, nil) + require.NoError(t, err) + res, err := client.UpdateManifestProperties(ctx, "hello-world", *resp.Tag.Digest, &ClientUpdateManifestPropertiesOptions{Value: &ManifestWriteableProperties{ + CanWrite: to.Ptr(false), + }, + }) + require.NoError(t, err) + require.Equal(t, *res.Manifest.ChangeableAttributes.CanWrite, false) + res, err = client.UpdateManifestProperties(ctx, "hello-world", digest, &ClientUpdateManifestPropertiesOptions{Value: &ManifestWriteableProperties{ + CanWrite: to.Ptr(true), + }, + }) + require.NoError(t, err) + require.Equal(t, *res.Manifest.ChangeableAttributes.CanWrite, true) +} + +func TestClient_UpdateManifestProperties_wrongDigest(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + _, err = client.GetTagProperties(ctx, "hello-world", "wrong-digest", nil) + require.Error(t, err) +} + +func TestClient_UpdateRepositoryProperties(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + res, err := client.UpdateRepositoryProperties(ctx, "hello-world", &ClientUpdateRepositoryPropertiesOptions{Value: &RepositoryWriteableProperties{ + CanWrite: to.Ptr(false), + }, + }) + require.NoError(t, err) + require.Equal(t, *res.ContainerRepositoryProperties.ChangeableAttributes.CanWrite, false) + res, err = client.UpdateRepositoryProperties(ctx, "hello-world", &ClientUpdateRepositoryPropertiesOptions{Value: &RepositoryWriteableProperties{ + CanWrite: to.Ptr(true), + }, + }) + require.NoError(t, err) + require.Equal(t, *res.ContainerRepositoryProperties.ChangeableAttributes.CanWrite, true) +} + +func TestClient_UpdateRepositoryProperties_wrongRepository(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + _, err = client.UpdateRepositoryProperties(ctx, "wrong-repository", &ClientUpdateRepositoryPropertiesOptions{Value: &RepositoryWriteableProperties{ + CanWrite: to.Ptr(false), + }, + }) + require.Error(t, err) +} + +func TestClient_UpdateTagProperties(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + res, err := client.UpdateTagProperties(ctx, "hello-world", "latest", &ClientUpdateTagPropertiesOptions{Value: &TagWriteableProperties{ + CanWrite: to.Ptr(false), + }, + }) + require.NoError(t, err) + require.Equal(t, *res.Tag.ChangeableAttributes.CanWrite, false) + res, err = client.UpdateTagProperties(ctx, "hello-world", "latest", &ClientUpdateTagPropertiesOptions{Value: &TagWriteableProperties{ + CanWrite: to.Ptr(true), + }, + }) + require.NoError(t, err) + require.Equal(t, *res.Tag.ChangeableAttributes.CanWrite, true) +} + +func TestClient_UpdateTagProperties_wrongTag(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + _, err = client.UpdateTagProperties(ctx, "hello-world", "wrong-tag", &ClientUpdateTagPropertiesOptions{Value: &TagWriteableProperties{ + CanWrite: to.Ptr(false), + }, + }) + require.Error(t, err) +} + +func TestClient_UploadManifest(t *testing.T) { + startRecording(t) + cred, options := getCredAndClientOptions(t) + ctx := context.Background() + client, err := NewClient("https://azacrlivetest.azurecr.io", cred, &ClientOptions{ClientOptions: options}) + require.NoError(t, err) + getRes, err := client.GetManifest(ctx, "hello-world", "latest", &ClientGetManifestOptions{Accept: to.Ptr("application/vnd.docker.distribution.manifest.v2+json")}) + require.NoError(t, err) + manifest, err := io.ReadAll(getRes.ManifestData) + require.NoError(t, err) + uploadRes, err := client.UploadManifest(ctx, "hello-world", "test", "application/vnd.docker.distribution.manifest.v2+json", streaming.NopCloser(bytes.NewReader(manifest)), nil) + require.NoError(t, err) + require.NotEmpty(t, *uploadRes.DockerContentDigest) + fmt.Printf("uploaded manifest digest: %s\n", *uploadRes.DockerContentDigest) +} diff --git a/sdk/containers/azcontainerregistry/cloud_config.go b/sdk/containers/azcontainerregistry/cloud_config.go new file mode 100644 index 000000000000..7687fa3c0f8c --- /dev/null +++ b/sdk/containers/azcontainerregistry/cloud_config.go @@ -0,0 +1,26 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + +const ( + // ServiceName is the cloud service name for Azure Container Registry + ServiceName cloud.ServiceName = "azcontainerregistry" +) + +func init() { + cloud.AzureChina.Services[ServiceName] = cloud.ServiceConfiguration{ + Audience: "https://management.core.chinacloudapi.cn", + } + cloud.AzureGovernment.Services[ServiceName] = cloud.ServiceConfiguration{ + Audience: "https://management.core.usgovcloudapi.net", + } + cloud.AzurePublic.Services[ServiceName] = cloud.ServiceConfiguration{ + Audience: "https://management.core.windows.net/", + } +} diff --git a/sdk/containers/azcontainerregistry/constants.go b/sdk/containers/azcontainerregistry/constants.go new file mode 100644 index 000000000000..304df483ffd1 --- /dev/null +++ b/sdk/containers/azcontainerregistry/constants.go @@ -0,0 +1,199 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. +// DO NOT EDIT. + +package azcontainerregistry + +// ArtifactArchitecture - The artifact platform's architecture. +type ArtifactArchitecture string + +const ( + // ArtifactArchitectureAmd64 - AMD64 + ArtifactArchitectureAmd64 ArtifactArchitecture = "amd64" + // ArtifactArchitectureArm - ARM + ArtifactArchitectureArm ArtifactArchitecture = "arm" + // ArtifactArchitectureArm64 - ARM64 + ArtifactArchitectureArm64 ArtifactArchitecture = "arm64" + // ArtifactArchitectureI386 - i386 + ArtifactArchitectureI386 ArtifactArchitecture = "386" + // ArtifactArchitectureMips - MIPS + ArtifactArchitectureMips ArtifactArchitecture = "mips" + // ArtifactArchitectureMips64 - MIPS64 + ArtifactArchitectureMips64 ArtifactArchitecture = "mips64" + // ArtifactArchitectureMips64Le - MIPS64LE + ArtifactArchitectureMips64Le ArtifactArchitecture = "mips64le" + // ArtifactArchitectureMipsLe - MIPSLE + ArtifactArchitectureMipsLe ArtifactArchitecture = "mipsle" + // ArtifactArchitecturePpc64 - PPC64 + ArtifactArchitecturePpc64 ArtifactArchitecture = "ppc64" + // ArtifactArchitecturePpc64Le - PPC64LE + ArtifactArchitecturePpc64Le ArtifactArchitecture = "ppc64le" + // ArtifactArchitectureRiscV64 - RISCv64 + ArtifactArchitectureRiscV64 ArtifactArchitecture = "riscv64" + // ArtifactArchitectureS390X - s390x + ArtifactArchitectureS390X ArtifactArchitecture = "s390x" + // ArtifactArchitectureWasm - Wasm + ArtifactArchitectureWasm ArtifactArchitecture = "wasm" +) + +// PossibleArtifactArchitectureValues returns the possible values for the ArtifactArchitecture const type. +func PossibleArtifactArchitectureValues() []ArtifactArchitecture { + return []ArtifactArchitecture{ + ArtifactArchitectureAmd64, + ArtifactArchitectureArm, + ArtifactArchitectureArm64, + ArtifactArchitectureI386, + ArtifactArchitectureMips, + ArtifactArchitectureMips64, + ArtifactArchitectureMips64Le, + ArtifactArchitectureMipsLe, + ArtifactArchitecturePpc64, + ArtifactArchitecturePpc64Le, + ArtifactArchitectureRiscV64, + ArtifactArchitectureS390X, + ArtifactArchitectureWasm, + } +} + +// ArtifactManifestOrderBy - Sort options for ordering manifests in a collection. +type ArtifactManifestOrderBy string + +const ( + // ArtifactManifestOrderByNone - Do not provide an orderby value in the request. + ArtifactManifestOrderByNone ArtifactManifestOrderBy = "none" + // ArtifactManifestOrderByLastUpdatedOnDescending - Order manifests by LastUpdatedOn field, from most recently updated to + // least recently updated. + ArtifactManifestOrderByLastUpdatedOnDescending ArtifactManifestOrderBy = "timedesc" + // ArtifactManifestOrderByLastUpdatedOnAscending - Order manifest by LastUpdatedOn field, from least recently updated to most + // recently updated. + ArtifactManifestOrderByLastUpdatedOnAscending ArtifactManifestOrderBy = "timeasc" +) + +// PossibleArtifactManifestOrderByValues returns the possible values for the ArtifactManifestOrderBy const type. +func PossibleArtifactManifestOrderByValues() []ArtifactManifestOrderBy { + return []ArtifactManifestOrderBy{ + ArtifactManifestOrderByNone, + ArtifactManifestOrderByLastUpdatedOnDescending, + ArtifactManifestOrderByLastUpdatedOnAscending, + } +} + +// ArtifactOperatingSystem - The artifact platform's operating system. +type ArtifactOperatingSystem string + +const ( + ArtifactOperatingSystemAix ArtifactOperatingSystem = "aix" + ArtifactOperatingSystemAndroid ArtifactOperatingSystem = "android" + ArtifactOperatingSystemDarwin ArtifactOperatingSystem = "darwin" + ArtifactOperatingSystemDragonfly ArtifactOperatingSystem = "dragonfly" + ArtifactOperatingSystemFreeBsd ArtifactOperatingSystem = "freebsd" + ArtifactOperatingSystemIOS ArtifactOperatingSystem = "ios" + ArtifactOperatingSystemIllumos ArtifactOperatingSystem = "illumos" + ArtifactOperatingSystemJS ArtifactOperatingSystem = "js" + ArtifactOperatingSystemLinux ArtifactOperatingSystem = "linux" + ArtifactOperatingSystemNetBsd ArtifactOperatingSystem = "netbsd" + ArtifactOperatingSystemOpenBsd ArtifactOperatingSystem = "openbsd" + ArtifactOperatingSystemPlan9 ArtifactOperatingSystem = "plan9" + ArtifactOperatingSystemSolaris ArtifactOperatingSystem = "solaris" + ArtifactOperatingSystemWindows ArtifactOperatingSystem = "windows" +) + +// PossibleArtifactOperatingSystemValues returns the possible values for the ArtifactOperatingSystem const type. +func PossibleArtifactOperatingSystemValues() []ArtifactOperatingSystem { + return []ArtifactOperatingSystem{ + ArtifactOperatingSystemAix, + ArtifactOperatingSystemAndroid, + ArtifactOperatingSystemDarwin, + ArtifactOperatingSystemDragonfly, + ArtifactOperatingSystemFreeBsd, + ArtifactOperatingSystemIOS, + ArtifactOperatingSystemIllumos, + ArtifactOperatingSystemJS, + ArtifactOperatingSystemLinux, + ArtifactOperatingSystemNetBsd, + ArtifactOperatingSystemOpenBsd, + ArtifactOperatingSystemPlan9, + ArtifactOperatingSystemSolaris, + ArtifactOperatingSystemWindows, + } +} + +// ArtifactTagOrderBy - Sort options for ordering tags in a collection. +type ArtifactTagOrderBy string + +const ( + // ArtifactTagOrderByNone - Do not provide an orderby value in the request. + ArtifactTagOrderByNone ArtifactTagOrderBy = "none" + // ArtifactTagOrderByLastUpdatedOnDescending - Order tags by LastUpdatedOn field, from most recently updated to least recently + // updated. + ArtifactTagOrderByLastUpdatedOnDescending ArtifactTagOrderBy = "timedesc" + // ArtifactTagOrderByLastUpdatedOnAscending - Order tags by LastUpdatedOn field, from least recently updated to most recently + // updated. + ArtifactTagOrderByLastUpdatedOnAscending ArtifactTagOrderBy = "timeasc" +) + +// PossibleArtifactTagOrderByValues returns the possible values for the ArtifactTagOrderBy const type. +func PossibleArtifactTagOrderByValues() []ArtifactTagOrderBy { + return []ArtifactTagOrderBy{ + ArtifactTagOrderByNone, + ArtifactTagOrderByLastUpdatedOnDescending, + ArtifactTagOrderByLastUpdatedOnAscending, + } +} + +// ContentType - Content type for upload +type ContentType string + +const ( + // ContentTypeApplicationVndDockerDistributionManifestV2JSON - Content Type 'application/vnd.docker.distribution.manifest.v2+json' + ContentTypeApplicationVndDockerDistributionManifestV2JSON ContentType = "application/vnd.docker.distribution.manifest.v2+json" + // ContentTypeApplicationVndOciImageManifestV1JSON - Content Type 'application/vnd.oci.image.manifest.v1+json' + ContentTypeApplicationVndOciImageManifestV1JSON ContentType = "application/vnd.oci.image.manifest.v1+json" +) + +// PossibleContentTypeValues returns the possible values for the ContentType const type. +func PossibleContentTypeValues() []ContentType { + return []ContentType{ + ContentTypeApplicationVndDockerDistributionManifestV2JSON, + ContentTypeApplicationVndOciImageManifestV1JSON, + } +} + +// postContentSchemaGrantType - Can take a value of accesstokenrefreshtoken, or accesstoken, or refresh_token +type postContentSchemaGrantType string + +const ( + postContentSchemaGrantTypeAccessToken postContentSchemaGrantType = "access_token" + postContentSchemaGrantTypeAccessTokenRefreshToken postContentSchemaGrantType = "access_token_refresh_token" + postContentSchemaGrantTypeRefreshToken postContentSchemaGrantType = "refresh_token" +) + +// possiblePostContentSchemaGrantTypeValues returns the possible values for the postContentSchemaGrantType const type. +func possiblePostContentSchemaGrantTypeValues() []postContentSchemaGrantType { + return []postContentSchemaGrantType{ + postContentSchemaGrantTypeAccessToken, + postContentSchemaGrantTypeAccessTokenRefreshToken, + postContentSchemaGrantTypeRefreshToken, + } +} + +// tokenGrantType - Grant type is expected to be refresh_token +type tokenGrantType string + +const ( + tokenGrantTypeRefreshToken tokenGrantType = "refresh_token" + tokenGrantTypePassword tokenGrantType = "password" +) + +// possibleTokenGrantTypeValues returns the possible values for the tokenGrantType const type. +func possibleTokenGrantTypeValues() []tokenGrantType { + return []tokenGrantType{ + tokenGrantTypeRefreshToken, + tokenGrantTypePassword, + } +} diff --git a/sdk/containers/azcontainerregistry/custom_client.go b/sdk/containers/azcontainerregistry/custom_client.go new file mode 100644 index 000000000000..310870b994eb --- /dev/null +++ b/sdk/containers/azcontainerregistry/custom_client.go @@ -0,0 +1,60 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "errors" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "reflect" + "strings" +) + +// ClientOptions contains the optional parameters for the NewClient method. +type ClientOptions struct { + azcore.ClientOptions +} + +// NewClient creates a new instance of Client with the specified values. +// - endpoint - registry login URL +// - credential - used to authorize requests. Usually a credential from azidentity. +// - options - client options, pass nil to accept the default values. +func NewClient(endpoint string, credential azcore.TokenCredential, options *ClientOptions) (*Client, error) { + if options == nil { + options = &ClientOptions{} + } + + if reflect.ValueOf(options.Cloud).IsZero() { + options.Cloud = cloud.AzurePublic + } + c, ok := options.Cloud.Services[ServiceName] + if !ok || c.Audience == "" { + return nil, errors.New("provided Cloud field is missing Azure Container Registry configuration") + } + + authClient := newAuthenticationClient(endpoint, &authenticationClientOptions{ + options.ClientOptions, + }) + authPolicy := newAuthenticationPolicy( + credential, + []string{c.Audience + "/.default"}, + authClient, + nil, + ) + + pl := runtime.NewPipeline(moduleName, moduleVersion, runtime.PipelineOptions{PerRetry: []policy.Policy{authPolicy}}, &options.ClientOptions) + return &Client{ + endpoint, + pl, + }, nil +} + +func extractNextLink(value string) string { + return value[1:strings.Index(value, ">")] +} diff --git a/sdk/containers/azcontainerregistry/custom_client_example_test.go b/sdk/containers/azcontainerregistry/custom_client_example_test.go new file mode 100644 index 000000000000..83af8eb3d42b --- /dev/null +++ b/sdk/containers/azcontainerregistry/custom_client_example_test.go @@ -0,0 +1,25 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry_test + +import ( + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "log" +) + +func ExampleNewClient() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + client, err = azcontainerregistry.NewClient("https://example.azurecr.io", cred, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + _ = client +} diff --git a/sdk/containers/azcontainerregistry/custom_client_test.go b/sdk/containers/azcontainerregistry/custom_client_test.go new file mode 100644 index 000000000000..de99a2aeb153 --- /dev/null +++ b/sdk/containers/azcontainerregistry/custom_client_test.go @@ -0,0 +1,25 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/stretchr/testify/require" + "testing" +) + +func TestNewClient(t *testing.T) { + client, err := NewClient("test", nil, nil) + require.NoError(t, err) + require.NotNil(t, client) + wrongCloudConfig := cloud.Configuration{ + ActiveDirectoryAuthorityHost: "test", Services: map[cloud.ServiceName]cloud.ServiceConfiguration{}, + } + _, err = NewClient("test", nil, &ClientOptions{ClientOptions: azcore.ClientOptions{Cloud: wrongCloudConfig}}) + require.Errorf(t, err, "provided Cloud field is missing Azure Container Registry configuration") +} diff --git a/sdk/containers/azcontainerregistry/custom_constants.go b/sdk/containers/azcontainerregistry/custom_constants.go new file mode 100644 index 000000000000..7509104164b5 --- /dev/null +++ b/sdk/containers/azcontainerregistry/custom_constants.go @@ -0,0 +1,12 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +const ( + moduleName = "azcontainerregistry" + moduleVersion = "v0.1.0" +) diff --git a/sdk/containers/azcontainerregistry/example_delete_images_test.go b/sdk/containers/azcontainerregistry/example_delete_images_test.go new file mode 100644 index 000000000000..69af07133ad1 --- /dev/null +++ b/sdk/containers/azcontainerregistry/example_delete_images_test.go @@ -0,0 +1,63 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry_test + +import ( + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "log" +) + +func Example_deleteImages() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + client, err := azcontainerregistry.NewClient("", cred, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + ctx := context.Background() + repositoryPager := client.NewListRepositoriesPager(nil) + for repositoryPager.More() { + repositoryPage, err := repositoryPager.NextPage(ctx) + if err != nil { + log.Fatalf("failed to advance repository page: %v", err) + } + for _, r := range repositoryPage.Repositories.Names { + manifestPager := client.NewListManifestsPager(*r, &azcontainerregistry.ClientListManifestsOptions{ + OrderBy: to.Ptr(azcontainerregistry.ArtifactManifestOrderByLastUpdatedOnDescending), + }) + for manifestPager.More() { + manifestPage, err := manifestPager.NextPage(ctx) + if err != nil { + log.Fatalf("failed to advance manifest page: %v", err) + } + imagesToKeep := 3 + for i, m := range manifestPage.Manifests.Attributes { + if i >= imagesToKeep { + for _, t := range m.Tags { + fmt.Printf("delete tag from image: %s", *t) + _, err := client.DeleteTag(ctx, *r, *t, nil) + if err != nil { + log.Fatalf("failed to delete tag: %v", err) + } + } + _, err := client.DeleteManifest(ctx, *r, *m.Digest, nil) + if err != nil { + log.Fatalf("failed to delete manifest: %v", err) + } + fmt.Printf("delete image with digest: %s", *m.Digest) + } + } + } + } + } +} diff --git a/sdk/containers/azcontainerregistry/example_list_repositories_test.go b/sdk/containers/azcontainerregistry/example_list_repositories_test.go new file mode 100644 index 000000000000..2b48a0230676 --- /dev/null +++ b/sdk/containers/azcontainerregistry/example_list_repositories_test.go @@ -0,0 +1,37 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry_test + +import ( + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "log" +) + +func Example_listRepositories() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + client, err := azcontainerregistry.NewClient("", cred, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + ctx := context.Background() + pager := client.NewListRepositoriesPager(nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + log.Fatalf("failed to advance page: %v", err) + } + for _, v := range page.Repositories.Names { + fmt.Printf("repository: %s\n", *v) + } + } +} diff --git a/sdk/containers/azcontainerregistry/example_list_tags_test.go b/sdk/containers/azcontainerregistry/example_list_tags_test.go new file mode 100644 index 000000000000..2fe573fa51d1 --- /dev/null +++ b/sdk/containers/azcontainerregistry/example_list_tags_test.go @@ -0,0 +1,32 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry_test + +import ( + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "log" +) + +func Example_listTagsWithAnonymousAccess() { + client, err := azcontainerregistry.NewClient("", nil, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + ctx := context.Background() + pager := client.NewListTagsPager("library/hello-world", nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + log.Fatalf("failed to advance page: %v", err) + } + for _, v := range page.Tags { + fmt.Printf("tag: %s\n", *v.Name) + } + } +} diff --git a/sdk/containers/azcontainerregistry/example_set_artifact_properties_test.go b/sdk/containers/azcontainerregistry/example_set_artifact_properties_test.go new file mode 100644 index 000000000000..24309e51e206 --- /dev/null +++ b/sdk/containers/azcontainerregistry/example_set_artifact_properties_test.go @@ -0,0 +1,37 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry_test + +import ( + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "log" +) + +func Example_setArtifactProperties() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + client, err := azcontainerregistry.NewClient("", cred, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + ctx := context.Background() + res, err := client.UpdateTagProperties(ctx, "library/hello-world", "latest", &azcontainerregistry.ClientUpdateTagPropertiesOptions{ + Value: &azcontainerregistry.TagWriteableProperties{ + CanWrite: to.Ptr(false), + CanDelete: to.Ptr(false), + }}) + if err != nil { + log.Fatalf("failed to finish the request: %v", err) + } + fmt.Printf("repository library/hello-world - tag latest: 'CanWrite' property: %t, 'CanDelete' property: %t\n", *res.Tag.ChangeableAttributes.CanWrite, *res.Tag.ChangeableAttributes.CanDelete) +} diff --git a/sdk/containers/azcontainerregistry/example_upload_download_blob_test.go b/sdk/containers/azcontainerregistry/example_upload_download_blob_test.go new file mode 100644 index 000000000000..a36fc19b3d13 --- /dev/null +++ b/sdk/containers/azcontainerregistry/example_upload_download_blob_test.go @@ -0,0 +1,53 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry_test + +import ( + "bytes" + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "io" + "log" +) + +func Example_uploadAndDownloadBlob() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + client, err := azcontainerregistry.NewBlobClient("", cred, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + ctx := context.Background() + blob := []byte("hello world") + startRes, err := client.StartUpload(ctx, "library/hello-world", nil) + if err != nil { + log.Fatalf("failed to start upload blob: %v", err) + } + calculator := azcontainerregistry.NewBlobDigestCalculator() + uploadResp, err := client.UploadChunk(ctx, *startRes.Location, bytes.NewReader(blob), calculator, nil) + if err != nil { + log.Fatalf("failed to upload blob: %v", err) + } + completeResp, err := client.CompleteUpload(ctx, *uploadResp.Location, calculator, nil) + if err != nil { + log.Fatalf("failed to complete upload: %v", err) + } + fmt.Printf("uploaded blob digest: %s", *completeResp.DockerContentDigest) + downloadRes, err := client.GetBlob(ctx, "library/hello-world", *completeResp.DockerContentDigest, nil) + if err != nil { + log.Fatalf("failed to download blob: %v", err) + } + downloadBlob, err := io.ReadAll(downloadRes.BlobData) + if err != nil { + log.Fatalf("failed to read blob: %v", err) + } + fmt.Printf("blob content: %s", downloadBlob) +} diff --git a/sdk/containers/azcontainerregistry/example_upload_manifest_test.go b/sdk/containers/azcontainerregistry/example_upload_manifest_test.go new file mode 100644 index 000000000000..ed3e1ee50c01 --- /dev/null +++ b/sdk/containers/azcontainerregistry/example_upload_manifest_test.go @@ -0,0 +1,92 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry_test + +import ( + "bytes" + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "log" +) + +func Example_uploadManifest() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a credential: %v", err) + } + client, err := azcontainerregistry.NewClient("", cred, nil) + if err != nil { + log.Fatalf("failed to create client: %v", err) + } + blobClient, err := azcontainerregistry.NewBlobClient("", cred, nil) + if err != nil { + log.Fatalf("failed to create blob client: %v", err) + } + ctx := context.Background() + layer := []byte("hello world") + startRes, err := blobClient.StartUpload(ctx, "library/hello-world", nil) + if err != nil { + log.Fatalf("failed to start upload layer: %v", err) + } + calculator := azcontainerregistry.NewBlobDigestCalculator() + uploadResp, err := blobClient.UploadChunk(ctx, *startRes.Location, bytes.NewReader(layer), calculator, nil) + if err != nil { + log.Fatalf("failed to upload layer: %v", err) + } + completeResp, err := blobClient.CompleteUpload(ctx, *uploadResp.Location, calculator, nil) + if err != nil { + log.Fatalf("failed to complete layer upload: %v", err) + } + layerDigest := *completeResp.DockerContentDigest + config := []byte(fmt.Sprintf(`{ + architecture: "amd64", + os: "windows", + rootfs: { + type: "layers", + diff_ids: [%s], + }, +}`, layerDigest)) + startRes, err = blobClient.StartUpload(ctx, "library/hello-world", nil) + if err != nil { + log.Fatalf("failed to start upload config: %v", err) + } + calculator = azcontainerregistry.NewBlobDigestCalculator() + uploadResp, err = blobClient.UploadChunk(ctx, *startRes.Location, bytes.NewReader(config), calculator, nil) + if err != nil { + log.Fatalf("failed to upload config: %v", err) + } + completeResp, err = blobClient.CompleteUpload(ctx, *uploadResp.Location, calculator, nil) + if err != nil { + log.Fatalf("failed to complete config upload: %v", err) + } + manifest := fmt.Sprintf(`{ + schemaVersion: 2, + config: { + mediaType: "application/vnd.oci.image.config.v1+json", + digest: %s, + size: %d, + }, + layers: [ + { + mediaType: "application/vnd.oci.image.layer.v1.tar", + digest: %s, + size: %d, + annotations: { + title: "artifact.txt", + }, + }, + ], +}`, layerDigest, len(config), *completeResp.DockerContentDigest, len(layer)) + uploadManifestRes, err := client.UploadManifest(ctx, "library/hello-world", "1.0.0", "application/vnd.oci.image.config.v1+json", streaming.NopCloser(bytes.NewReader([]byte(manifest))), nil) + if err != nil { + log.Fatalf("failed to upload manifest: %v", err) + } + fmt.Printf("digest of uploaded manifest: %s", *uploadManifestRes.DockerContentDigest) +} diff --git a/sdk/containers/azcontainerregistry/go.mod b/sdk/containers/azcontainerregistry/go.mod new file mode 100644 index 000000000000..ca85b0d29fce --- /dev/null +++ b/sdk/containers/azcontainerregistry/go.mod @@ -0,0 +1,27 @@ +module github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry + +go 1.18 + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 + github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 + github.com/stretchr/testify v1.8.1 +) + +require ( + github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dnaeon/go-vcr v1.1.0 // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 // indirect + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/sdk/containers/azcontainerregistry/go.sum b/sdk/containers/azcontainerregistry/go.sum new file mode 100644 index 000000000000..43383cfa4489 --- /dev/null +++ b/sdk/containers/azcontainerregistry/go.sum @@ -0,0 +1,47 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0 h1:VuHAcMq8pU1IWNT/m5yRaGqbK0BiQKHT8X4DTp9CHdI= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0/go.mod h1:tZoQYdDZNOiIjdSn0dVWVfl0NEPGOJqVLzSrcFk4Is0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 h1:t/W5MYAuQy81cvM8VUNfRLzhtKpXhVUAN7Cd7KVbTyc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0/go.mod h1:NBanQUfSWiWn3QEpWDTCU0IjBECKOYvl2R8xdRtMtiM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 h1:+5VZ72z0Qan5Bog5C+ZkgSqUbeVUd9wgtHOrIKuc5b8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 h1:VgSJlZH5u0k2qxSpqyghcFQKmvYckj46uymKK5XzkBM= +github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0/go.mod h1:BDJ5qMFKx9DugEg3+uQSDCdbYPr5s9vBTrL9P8TpqOU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 h1:Tgea0cVUD0ivh5ADBX4WwuI12DUd2to3nCYe2eayMIw= +golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sdk/containers/azcontainerregistry/models.go b/sdk/containers/azcontainerregistry/models.go new file mode 100644 index 000000000000..c678be4168ad --- /dev/null +++ b/sdk/containers/azcontainerregistry/models.go @@ -0,0 +1,375 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. +// DO NOT EDIT. + +package azcontainerregistry + +import "time" + +type acrAccessToken struct { + // The access token for performing authenticated requests + AccessToken *string `json:"access_token,omitempty"` +} + +type acrRefreshToken struct { + // The refresh token to be used for generating access tokens + RefreshToken *string `json:"refresh_token,omitempty"` +} + +// ArtifactManifestPlatform - The artifact's platform, consisting of operating system and architecture. +type ArtifactManifestPlatform struct { + // READ-ONLY; Manifest digest + Digest *string `json:"digest,omitempty" azure:"ro"` + + // READ-ONLY; CPU architecture + Architecture *ArtifactArchitecture `json:"architecture,omitempty" azure:"ro"` + + // READ-ONLY; Operating system + OperatingSystem *ArtifactOperatingSystem `json:"os,omitempty" azure:"ro"` +} + +// ArtifactManifestProperties - Manifest attributes details +type ArtifactManifestProperties struct { + // READ-ONLY; Manifest attributes + Manifest *ManifestAttributes `json:"manifest,omitempty" azure:"ro"` + + // READ-ONLY; Registry login server name. This is likely to be similar to {registry-name}.azurecr.io. + RegistryLoginServer *string `json:"registry,omitempty" azure:"ro"` + + // READ-ONLY; Repository name + RepositoryName *string `json:"imageName,omitempty" azure:"ro"` +} + +// ArtifactTagProperties - Tag attributes +type ArtifactTagProperties struct { + // READ-ONLY; Registry login server name. This is likely to be similar to {registry-name}.azurecr.io. + RegistryLoginServer *string `json:"registry,omitempty" azure:"ro"` + + // READ-ONLY; Image name + RepositoryName *string `json:"imageName,omitempty" azure:"ro"` + + // READ-ONLY; List of tag attribute details + Tag *TagAttributes `json:"tag,omitempty" azure:"ro"` +} + +// authenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions contains the optional parameters for the authenticationClient.ExchangeAADAccessTokenForACRRefreshToken +// method. +type authenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions struct { + // AAD access token, mandatory when granttype is accesstokenrefreshtoken or access_token. + AccessToken *string + // AAD refresh token, mandatory when granttype is accesstokenrefreshtoken or refresh_token + RefreshToken *string + // AAD tenant associated to the AAD credentials. + Tenant *string +} + +// authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions contains the optional parameters for the authenticationClient.ExchangeACRRefreshTokenForACRAccessToken +// method. +type authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions struct { + // Grant type is expected to be refresh_token + GrantType *tokenGrantType +} + +// BlobClientCancelUploadOptions contains the optional parameters for the BlobClient.CancelUpload method. +type BlobClientCancelUploadOptions struct { + // placeholder for future optional parameters +} + +// BlobClientCheckBlobExistsOptions contains the optional parameters for the BlobClient.CheckBlobExists method. +type BlobClientCheckBlobExistsOptions struct { + // placeholder for future optional parameters +} + +// BlobClientCheckChunkExistsOptions contains the optional parameters for the BlobClient.CheckChunkExists method. +type BlobClientCheckChunkExistsOptions struct { + // placeholder for future optional parameters +} + +// BlobClientCompleteUploadOptions contains the optional parameters for the BlobClient.CompleteUpload method. +type BlobClientCompleteUploadOptions struct { + // placeholder for future optional parameters +} + +// BlobClientDeleteBlobOptions contains the optional parameters for the BlobClient.DeleteBlob method. +type BlobClientDeleteBlobOptions struct { + // placeholder for future optional parameters +} + +// BlobClientGetBlobOptions contains the optional parameters for the BlobClient.GetBlob method. +type BlobClientGetBlobOptions struct { + // placeholder for future optional parameters +} + +// BlobClientGetChunkOptions contains the optional parameters for the BlobClient.GetChunk method. +type BlobClientGetChunkOptions struct { + // placeholder for future optional parameters +} + +// BlobClientGetUploadStatusOptions contains the optional parameters for the BlobClient.GetUploadStatus method. +type BlobClientGetUploadStatusOptions struct { + // placeholder for future optional parameters +} + +// BlobClientMountBlobOptions contains the optional parameters for the BlobClient.MountBlob method. +type BlobClientMountBlobOptions struct { + // placeholder for future optional parameters +} + +// BlobClientStartUploadOptions contains the optional parameters for the BlobClient.StartUpload method. +type BlobClientStartUploadOptions struct { + // placeholder for future optional parameters +} + +// blobClientUploadChunkOptions contains the optional parameters for the BlobClient.uploadChunk method. +type blobClientUploadChunkOptions struct { + // Range of bytes identifying the desired block of content represented by the body. Start must the end offset retrieved via + // status check plus one. Note that this is a non-standard use of the + // Content-Range header. + ContentRange *string +} + +// ClientDeleteManifestOptions contains the optional parameters for the Client.DeleteManifest method. +type ClientDeleteManifestOptions struct { + // placeholder for future optional parameters +} + +// ClientDeleteRepositoryOptions contains the optional parameters for the Client.DeleteRepository method. +type ClientDeleteRepositoryOptions struct { + // placeholder for future optional parameters +} + +// ClientDeleteTagOptions contains the optional parameters for the Client.DeleteTag method. +type ClientDeleteTagOptions struct { + // placeholder for future optional parameters +} + +// ClientGetManifestOptions contains the optional parameters for the Client.GetManifest method. +type ClientGetManifestOptions struct { + // Accept header string delimited by comma. For example, application/vnd.docker.distribution.manifest.v2+json + Accept *string +} + +// ClientGetManifestPropertiesOptions contains the optional parameters for the Client.GetManifestProperties method. +type ClientGetManifestPropertiesOptions struct { + // placeholder for future optional parameters +} + +// ClientGetRepositoryPropertiesOptions contains the optional parameters for the Client.GetRepositoryProperties method. +type ClientGetRepositoryPropertiesOptions struct { + // placeholder for future optional parameters +} + +// ClientGetTagPropertiesOptions contains the optional parameters for the Client.GetTagProperties method. +type ClientGetTagPropertiesOptions struct { + // placeholder for future optional parameters +} + +// ClientListManifestsOptions contains the optional parameters for the Client.NewListManifestsPager method. +type ClientListManifestsOptions struct { + // Query parameter for the last item in previous query. Result set will include values lexically after last. + Last *string + // query parameter for max number of items + MaxNum *int32 + // Sort options for ordering manifests in a collection. + OrderBy *ArtifactManifestOrderBy +} + +// ClientListRepositoriesOptions contains the optional parameters for the Client.NewListRepositoriesPager method. +type ClientListRepositoriesOptions struct { + // Query parameter for the last item in previous query. Result set will include values lexically after last. + Last *string + // query parameter for max number of items + MaxNum *int32 +} + +// ClientListTagsOptions contains the optional parameters for the Client.NewListTagsPager method. +type ClientListTagsOptions struct { + // filter by digest + Digest *string + // Query parameter for the last item in previous query. Result set will include values lexically after last. + Last *string + // query parameter for max number of items + MaxNum *int32 + // Sort options for ordering tags in a collection. + OrderBy *ArtifactTagOrderBy +} + +// ClientUpdateManifestPropertiesOptions contains the optional parameters for the Client.UpdateManifestProperties method. +type ClientUpdateManifestPropertiesOptions struct { + // Manifest attribute value + Value *ManifestWriteableProperties +} + +// ClientUpdateRepositoryPropertiesOptions contains the optional parameters for the Client.UpdateRepositoryProperties method. +type ClientUpdateRepositoryPropertiesOptions struct { + // Repository attribute value + Value *RepositoryWriteableProperties +} + +// ClientUpdateTagPropertiesOptions contains the optional parameters for the Client.UpdateTagProperties method. +type ClientUpdateTagPropertiesOptions struct { + // Tag attribute value + Value *TagWriteableProperties +} + +// ClientUploadManifestOptions contains the optional parameters for the Client.UploadManifest method. +type ClientUploadManifestOptions struct { + // placeholder for future optional parameters +} + +// ContainerRepositoryProperties - Properties of this repository. +type ContainerRepositoryProperties struct { + // REQUIRED; Writeable properties of the resource + ChangeableAttributes *RepositoryWriteableProperties `json:"changeableAttributes,omitempty"` + + // READ-ONLY; Image created time + CreatedOn *time.Time `json:"createdTime,omitempty" azure:"ro"` + + // READ-ONLY; Image last update time + LastUpdatedOn *time.Time `json:"lastUpdateTime,omitempty" azure:"ro"` + + // READ-ONLY; Number of the manifests + ManifestCount *int32 `json:"manifestCount,omitempty" azure:"ro"` + + // READ-ONLY; Image name + Name *string `json:"imageName,omitempty" azure:"ro"` + + // READ-ONLY; Registry login server name. This is likely to be similar to {registry-name}.azurecr.io. + RegistryLoginServer *string `json:"registry,omitempty" azure:"ro"` + + // READ-ONLY; Number of the tags + TagCount *int32 `json:"tagCount,omitempty" azure:"ro"` +} + +// ManifestAttributes - Manifest details +type ManifestAttributes struct { + // READ-ONLY; Created time + CreatedOn *time.Time `json:"createdTime,omitempty" azure:"ro"` + + // READ-ONLY; Manifest + Digest *string `json:"digest,omitempty" azure:"ro"` + + // READ-ONLY; Last update time + LastUpdatedOn *time.Time `json:"lastUpdateTime,omitempty" azure:"ro"` + + // Writeable properties of the resource + ChangeableAttributes *ManifestWriteableProperties `json:"changeableAttributes,omitempty"` + + // READ-ONLY; CPU architecture + Architecture *ArtifactArchitecture `json:"architecture,omitempty" azure:"ro"` + + // READ-ONLY; Operating system + OperatingSystem *ArtifactOperatingSystem `json:"os,omitempty" azure:"ro"` + + // READ-ONLY; List of artifacts that are referenced by this manifest list, with information about the platform each supports. + // This list will be empty if this is a leaf manifest and not a manifest list. + RelatedArtifacts []*ArtifactManifestPlatform `json:"references,omitempty" azure:"ro"` + + // READ-ONLY; Image size + Size *int64 `json:"imageSize,omitempty" azure:"ro"` + + // READ-ONLY; List of tags + Tags []*string `json:"tags,omitempty" azure:"ro"` +} + +// ManifestWriteableProperties - Changeable attributes +type ManifestWriteableProperties struct { + // Delete enabled + CanDelete *bool `json:"deleteEnabled,omitempty"` + + // List enabled + CanList *bool `json:"listEnabled,omitempty"` + + // Read enabled + CanRead *bool `json:"readEnabled,omitempty"` + + // Write enabled + CanWrite *bool `json:"writeEnabled,omitempty"` +} + +// Manifests - Manifest attributes +type Manifests struct { + // List of manifests + Attributes []*ManifestAttributes `json:"manifests,omitempty"` + Link *string `json:"link,omitempty"` + + // Registry login server name. This is likely to be similar to {registry-name}.azurecr.io. + RegistryLoginServer *string `json:"registry,omitempty"` + + // Image name + Repository *string `json:"imageName,omitempty"` +} + +// Repositories - List of repositories +type Repositories struct { + Link *string `json:"link,omitempty"` + + // Repository names + Names []*string `json:"repositories,omitempty"` +} + +// RepositoryWriteableProperties - Changeable attributes for Repository +type RepositoryWriteableProperties struct { + // Delete enabled + CanDelete *bool `json:"deleteEnabled,omitempty"` + + // List enabled + CanList *bool `json:"listEnabled,omitempty"` + + // Read enabled + CanRead *bool `json:"readEnabled,omitempty"` + + // Write enabled + CanWrite *bool `json:"writeEnabled,omitempty"` +} + +// TagAttributes - Tag attribute details +type TagAttributes struct { + // REQUIRED; Writeable properties of the resource + ChangeableAttributes *TagWriteableProperties `json:"changeableAttributes,omitempty"` + + // READ-ONLY; Tag created time + CreatedOn *time.Time `json:"createdTime,omitempty" azure:"ro"` + + // READ-ONLY; Tag digest + Digest *string `json:"digest,omitempty" azure:"ro"` + + // READ-ONLY; Tag last update time + LastUpdatedOn *time.Time `json:"lastUpdateTime,omitempty" azure:"ro"` + + // READ-ONLY; Tag name + Name *string `json:"name,omitempty" azure:"ro"` +} + +// TagList - List of tag details +type TagList struct { + // REQUIRED; Registry login server name. This is likely to be similar to {registry-name}.azurecr.io. + RegistryLoginServer *string `json:"registry,omitempty"` + + // REQUIRED; Image name + Repository *string `json:"imageName,omitempty"` + + // REQUIRED; List of tag attribute details + Tags []*TagAttributes `json:"tags,omitempty"` + Link *string `json:"link,omitempty"` +} + +// TagWriteableProperties - Changeable attributes +type TagWriteableProperties struct { + // Delete enabled + CanDelete *bool `json:"deleteEnabled,omitempty"` + + // List enabled + CanList *bool `json:"listEnabled,omitempty"` + + // Read enabled + CanRead *bool `json:"readEnabled,omitempty"` + + // Write enabled + CanWrite *bool `json:"writeEnabled,omitempty"` +} diff --git a/sdk/containers/azcontainerregistry/models_serde.go b/sdk/containers/azcontainerregistry/models_serde.go new file mode 100644 index 000000000000..51d41284caba --- /dev/null +++ b/sdk/containers/azcontainerregistry/models_serde.go @@ -0,0 +1,575 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. +// DO NOT EDIT. + +package azcontainerregistry + +import ( + "encoding/json" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "reflect" +) + +// MarshalJSON implements the json.Marshaller interface for type acrAccessToken. +func (a acrAccessToken) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "access_token", a.AccessToken) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type acrAccessToken. +func (a *acrAccessToken) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "access_token": + err = unpopulate(val, "AccessToken", &a.AccessToken) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type acrRefreshToken. +func (a acrRefreshToken) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "refresh_token", a.RefreshToken) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type acrRefreshToken. +func (a *acrRefreshToken) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "refresh_token": + err = unpopulate(val, "RefreshToken", &a.RefreshToken) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type ArtifactManifestPlatform. +func (a ArtifactManifestPlatform) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "architecture", a.Architecture) + populate(objectMap, "digest", a.Digest) + populate(objectMap, "os", a.OperatingSystem) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type ArtifactManifestPlatform. +func (a *ArtifactManifestPlatform) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "architecture": + err = unpopulate(val, "Architecture", &a.Architecture) + delete(rawMsg, key) + case "digest": + err = unpopulate(val, "Digest", &a.Digest) + delete(rawMsg, key) + case "os": + err = unpopulate(val, "OperatingSystem", &a.OperatingSystem) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type ArtifactManifestProperties. +func (a ArtifactManifestProperties) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "manifest", a.Manifest) + populate(objectMap, "registry", a.RegistryLoginServer) + populate(objectMap, "imageName", a.RepositoryName) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type ArtifactManifestProperties. +func (a *ArtifactManifestProperties) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "manifest": + err = unpopulate(val, "Manifest", &a.Manifest) + delete(rawMsg, key) + case "registry": + err = unpopulate(val, "RegistryLoginServer", &a.RegistryLoginServer) + delete(rawMsg, key) + case "imageName": + err = unpopulate(val, "RepositoryName", &a.RepositoryName) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type ArtifactTagProperties. +func (a ArtifactTagProperties) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "registry", a.RegistryLoginServer) + populate(objectMap, "imageName", a.RepositoryName) + populate(objectMap, "tag", a.Tag) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type ArtifactTagProperties. +func (a *ArtifactTagProperties) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "registry": + err = unpopulate(val, "RegistryLoginServer", &a.RegistryLoginServer) + delete(rawMsg, key) + case "imageName": + err = unpopulate(val, "RepositoryName", &a.RepositoryName) + delete(rawMsg, key) + case "tag": + err = unpopulate(val, "Tag", &a.Tag) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type ContainerRepositoryProperties. +func (c ContainerRepositoryProperties) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "changeableAttributes", c.ChangeableAttributes) + populateTimeRFC3339(objectMap, "createdTime", c.CreatedOn) + populateTimeRFC3339(objectMap, "lastUpdateTime", c.LastUpdatedOn) + populate(objectMap, "manifestCount", c.ManifestCount) + populate(objectMap, "imageName", c.Name) + populate(objectMap, "registry", c.RegistryLoginServer) + populate(objectMap, "tagCount", c.TagCount) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type ContainerRepositoryProperties. +func (c *ContainerRepositoryProperties) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", c, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "changeableAttributes": + err = unpopulate(val, "ChangeableAttributes", &c.ChangeableAttributes) + delete(rawMsg, key) + case "createdTime": + err = unpopulateTimeRFC3339(val, "CreatedOn", &c.CreatedOn) + delete(rawMsg, key) + case "lastUpdateTime": + err = unpopulateTimeRFC3339(val, "LastUpdatedOn", &c.LastUpdatedOn) + delete(rawMsg, key) + case "manifestCount": + err = unpopulate(val, "ManifestCount", &c.ManifestCount) + delete(rawMsg, key) + case "imageName": + err = unpopulate(val, "Name", &c.Name) + delete(rawMsg, key) + case "registry": + err = unpopulate(val, "RegistryLoginServer", &c.RegistryLoginServer) + delete(rawMsg, key) + case "tagCount": + err = unpopulate(val, "TagCount", &c.TagCount) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", c, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type ManifestAttributes. +func (m ManifestAttributes) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "architecture", m.Architecture) + populate(objectMap, "changeableAttributes", m.ChangeableAttributes) + populateTimeRFC3339(objectMap, "createdTime", m.CreatedOn) + populate(objectMap, "digest", m.Digest) + populateTimeRFC3339(objectMap, "lastUpdateTime", m.LastUpdatedOn) + populate(objectMap, "os", m.OperatingSystem) + populate(objectMap, "references", m.RelatedArtifacts) + populate(objectMap, "imageSize", m.Size) + populate(objectMap, "tags", m.Tags) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type ManifestAttributes. +func (m *ManifestAttributes) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", m, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "architecture": + err = unpopulate(val, "Architecture", &m.Architecture) + delete(rawMsg, key) + case "changeableAttributes": + err = unpopulate(val, "ChangeableAttributes", &m.ChangeableAttributes) + delete(rawMsg, key) + case "createdTime": + err = unpopulateTimeRFC3339(val, "CreatedOn", &m.CreatedOn) + delete(rawMsg, key) + case "digest": + err = unpopulate(val, "Digest", &m.Digest) + delete(rawMsg, key) + case "lastUpdateTime": + err = unpopulateTimeRFC3339(val, "LastUpdatedOn", &m.LastUpdatedOn) + delete(rawMsg, key) + case "os": + err = unpopulate(val, "OperatingSystem", &m.OperatingSystem) + delete(rawMsg, key) + case "references": + err = unpopulate(val, "RelatedArtifacts", &m.RelatedArtifacts) + delete(rawMsg, key) + case "imageSize": + err = unpopulate(val, "Size", &m.Size) + delete(rawMsg, key) + case "tags": + err = unpopulate(val, "Tags", &m.Tags) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", m, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type ManifestWriteableProperties. +func (m ManifestWriteableProperties) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "deleteEnabled", m.CanDelete) + populate(objectMap, "listEnabled", m.CanList) + populate(objectMap, "readEnabled", m.CanRead) + populate(objectMap, "writeEnabled", m.CanWrite) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type ManifestWriteableProperties. +func (m *ManifestWriteableProperties) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", m, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "deleteEnabled": + err = unpopulate(val, "CanDelete", &m.CanDelete) + delete(rawMsg, key) + case "listEnabled": + err = unpopulate(val, "CanList", &m.CanList) + delete(rawMsg, key) + case "readEnabled": + err = unpopulate(val, "CanRead", &m.CanRead) + delete(rawMsg, key) + case "writeEnabled": + err = unpopulate(val, "CanWrite", &m.CanWrite) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", m, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type Manifests. +func (m Manifests) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "manifests", m.Attributes) + populate(objectMap, "link", m.Link) + populate(objectMap, "registry", m.RegistryLoginServer) + populate(objectMap, "imageName", m.Repository) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type Manifests. +func (m *Manifests) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", m, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "manifests": + err = unpopulate(val, "Attributes", &m.Attributes) + delete(rawMsg, key) + case "link": + err = unpopulate(val, "Link", &m.Link) + delete(rawMsg, key) + case "registry": + err = unpopulate(val, "RegistryLoginServer", &m.RegistryLoginServer) + delete(rawMsg, key) + case "imageName": + err = unpopulate(val, "Repository", &m.Repository) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", m, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type Repositories. +func (r Repositories) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "link", r.Link) + populate(objectMap, "repositories", r.Names) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type Repositories. +func (r *Repositories) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", r, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "link": + err = unpopulate(val, "Link", &r.Link) + delete(rawMsg, key) + case "repositories": + err = unpopulate(val, "Names", &r.Names) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", r, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type RepositoryWriteableProperties. +func (r RepositoryWriteableProperties) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "deleteEnabled", r.CanDelete) + populate(objectMap, "listEnabled", r.CanList) + populate(objectMap, "readEnabled", r.CanRead) + populate(objectMap, "writeEnabled", r.CanWrite) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type RepositoryWriteableProperties. +func (r *RepositoryWriteableProperties) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", r, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "deleteEnabled": + err = unpopulate(val, "CanDelete", &r.CanDelete) + delete(rawMsg, key) + case "listEnabled": + err = unpopulate(val, "CanList", &r.CanList) + delete(rawMsg, key) + case "readEnabled": + err = unpopulate(val, "CanRead", &r.CanRead) + delete(rawMsg, key) + case "writeEnabled": + err = unpopulate(val, "CanWrite", &r.CanWrite) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", r, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type TagAttributes. +func (t TagAttributes) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "changeableAttributes", t.ChangeableAttributes) + populateTimeRFC3339(objectMap, "createdTime", t.CreatedOn) + populate(objectMap, "digest", t.Digest) + populateTimeRFC3339(objectMap, "lastUpdateTime", t.LastUpdatedOn) + populate(objectMap, "name", t.Name) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type TagAttributes. +func (t *TagAttributes) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", t, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "changeableAttributes": + err = unpopulate(val, "ChangeableAttributes", &t.ChangeableAttributes) + delete(rawMsg, key) + case "createdTime": + err = unpopulateTimeRFC3339(val, "CreatedOn", &t.CreatedOn) + delete(rawMsg, key) + case "digest": + err = unpopulate(val, "Digest", &t.Digest) + delete(rawMsg, key) + case "lastUpdateTime": + err = unpopulateTimeRFC3339(val, "LastUpdatedOn", &t.LastUpdatedOn) + delete(rawMsg, key) + case "name": + err = unpopulate(val, "Name", &t.Name) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", t, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type TagList. +func (t TagList) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "link", t.Link) + populate(objectMap, "registry", t.RegistryLoginServer) + populate(objectMap, "imageName", t.Repository) + populate(objectMap, "tags", t.Tags) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type TagList. +func (t *TagList) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", t, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "link": + err = unpopulate(val, "Link", &t.Link) + delete(rawMsg, key) + case "registry": + err = unpopulate(val, "RegistryLoginServer", &t.RegistryLoginServer) + delete(rawMsg, key) + case "imageName": + err = unpopulate(val, "Repository", &t.Repository) + delete(rawMsg, key) + case "tags": + err = unpopulate(val, "Tags", &t.Tags) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", t, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type TagWriteableProperties. +func (t TagWriteableProperties) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "deleteEnabled", t.CanDelete) + populate(objectMap, "listEnabled", t.CanList) + populate(objectMap, "readEnabled", t.CanRead) + populate(objectMap, "writeEnabled", t.CanWrite) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type TagWriteableProperties. +func (t *TagWriteableProperties) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", t, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "deleteEnabled": + err = unpopulate(val, "CanDelete", &t.CanDelete) + delete(rawMsg, key) + case "listEnabled": + err = unpopulate(val, "CanList", &t.CanList) + delete(rawMsg, key) + case "readEnabled": + err = unpopulate(val, "CanRead", &t.CanRead) + delete(rawMsg, key) + case "writeEnabled": + err = unpopulate(val, "CanWrite", &t.CanWrite) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", t, err) + } + } + return nil +} + +func populate(m map[string]any, k string, v any) { + if v == nil { + return + } else if azcore.IsNullValue(v) { + m[k] = nil + } else if !reflect.ValueOf(v).IsNil() { + m[k] = v + } +} + +func unpopulate(data json.RawMessage, fn string, v any) error { + if data == nil { + return nil + } + if err := json.Unmarshal(data, v); err != nil { + return fmt.Errorf("struct field %s: %v", fn, err) + } + return nil +} diff --git a/sdk/containers/azcontainerregistry/response_types.go b/sdk/containers/azcontainerregistry/response_types.go new file mode 100644 index 000000000000..8227369e5fdf --- /dev/null +++ b/sdk/containers/azcontainerregistry/response_types.go @@ -0,0 +1,219 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. +// DO NOT EDIT. + +package azcontainerregistry + +import "io" + +// authenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse contains the response from method authenticationClient.ExchangeAADAccessTokenForACRRefreshToken. +type authenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse struct { + acrRefreshToken +} + +// authenticationClientExchangeACRRefreshTokenForACRAccessTokenResponse contains the response from method authenticationClient.ExchangeACRRefreshTokenForACRAccessToken. +type authenticationClientExchangeACRRefreshTokenForACRAccessTokenResponse struct { + acrAccessToken +} + +// BlobClientCancelUploadResponse contains the response from method BlobClient.CancelUpload. +type BlobClientCancelUploadResponse struct { + // placeholder for future response values +} + +// BlobClientCheckBlobExistsResponse contains the response from method BlobClient.CheckBlobExists. +type BlobClientCheckBlobExistsResponse struct { + // ContentLength contains the information returned from the Content-Length header response. + ContentLength *int64 + + // DockerContentDigest contains the information returned from the Docker-Content-Digest header response. + DockerContentDigest *string +} + +// BlobClientCheckChunkExistsResponse contains the response from method BlobClient.CheckChunkExists. +type BlobClientCheckChunkExistsResponse struct { + // ContentLength contains the information returned from the Content-Length header response. + ContentLength *int64 + + // ContentRange contains the information returned from the Content-Range header response. + ContentRange *string +} + +// BlobClientCompleteUploadResponse contains the response from method BlobClient.CompleteUpload. +type BlobClientCompleteUploadResponse struct { + // DockerContentDigest contains the information returned from the Docker-Content-Digest header response. + DockerContentDigest *string + + // Location contains the information returned from the Location header response. + Location *string + + // Range contains the information returned from the Range header response. + Range *string +} + +// BlobClientDeleteBlobResponse contains the response from method BlobClient.DeleteBlob. +type BlobClientDeleteBlobResponse struct { + // DockerContentDigest contains the information returned from the Docker-Content-Digest header response. + DockerContentDigest *string +} + +// BlobClientGetBlobResponse contains the response from method BlobClient.GetBlob. +type BlobClientGetBlobResponse struct { + // Body contains the streaming response. + BlobData io.ReadCloser + + // ContentLength contains the information returned from the Content-Length header response. + ContentLength *int64 + + // DockerContentDigest contains the information returned from the Docker-Content-Digest header response. + DockerContentDigest *string +} + +// BlobClientGetChunkResponse contains the response from method BlobClient.GetChunk. +type BlobClientGetChunkResponse struct { + // Body contains the streaming response. + ChunkData io.ReadCloser + + // ContentLength contains the information returned from the Content-Length header response. + ContentLength *int64 + + // ContentRange contains the information returned from the Content-Range header response. + ContentRange *string +} + +// BlobClientGetUploadStatusResponse contains the response from method BlobClient.GetUploadStatus. +type BlobClientGetUploadStatusResponse struct { + // DockerUploadUUID contains the information returned from the Docker-Upload-UUID header response. + DockerUploadUUID *string + + // Range contains the information returned from the Range header response. + Range *string +} + +// BlobClientMountBlobResponse contains the response from method BlobClient.MountBlob. +type BlobClientMountBlobResponse struct { + // DockerContentDigest contains the information returned from the Docker-Content-Digest header response. + DockerContentDigest *string + + // DockerUploadUUID contains the information returned from the Docker-Upload-UUID header response. + DockerUploadUUID *string + + // Location contains the information returned from the Location header response. + Location *string +} + +// BlobClientStartUploadResponse contains the response from method BlobClient.StartUpload. +type BlobClientStartUploadResponse struct { + // DockerUploadUUID contains the information returned from the Docker-Upload-UUID header response. + DockerUploadUUID *string + + // Location contains the information returned from the Location header response. + Location *string + + // Range contains the information returned from the Range header response. + Range *string +} + +// BlobClientUploadChunkResponse contains the response from method BlobClient.UploadChunk. +type BlobClientUploadChunkResponse struct { + // DockerUploadUUID contains the information returned from the Docker-Upload-UUID header response. + DockerUploadUUID *string + + // Location contains the information returned from the Location header response. + Location *string + + // Range contains the information returned from the Range header response. + Range *string +} + +// ClientDeleteManifestResponse contains the response from method Client.DeleteManifest. +type ClientDeleteManifestResponse struct { + // placeholder for future response values +} + +// ClientDeleteRepositoryResponse contains the response from method Client.DeleteRepository. +type ClientDeleteRepositoryResponse struct { + // placeholder for future response values +} + +// ClientDeleteTagResponse contains the response from method Client.DeleteTag. +type ClientDeleteTagResponse struct { + // placeholder for future response values +} + +// ClientGetManifestPropertiesResponse contains the response from method Client.GetManifestProperties. +type ClientGetManifestPropertiesResponse struct { + ArtifactManifestProperties +} + +// ClientGetManifestResponse contains the response from method Client.GetManifest. +type ClientGetManifestResponse struct { + // Body contains the streaming response. + ManifestData io.ReadCloser + + // DockerContentDigest contains the information returned from the Docker-Content-Digest header response. + DockerContentDigest *string +} + +// ClientGetRepositoryPropertiesResponse contains the response from method Client.GetRepositoryProperties. +type ClientGetRepositoryPropertiesResponse struct { + ContainerRepositoryProperties +} + +// ClientGetTagPropertiesResponse contains the response from method Client.GetTagProperties. +type ClientGetTagPropertiesResponse struct { + ArtifactTagProperties +} + +// ClientListManifestsResponse contains the response from method Client.NewListManifestsPager. +type ClientListManifestsResponse struct { + Manifests + // Link contains the information returned from the Link header response. + Link *string +} + +// ClientListRepositoriesResponse contains the response from method Client.NewListRepositoriesPager. +type ClientListRepositoriesResponse struct { + Repositories + // Link contains the information returned from the Link header response. + Link *string +} + +// ClientListTagsResponse contains the response from method Client.NewListTagsPager. +type ClientListTagsResponse struct { + TagList + // Link contains the information returned from the Link header response. + Link *string +} + +// ClientUpdateManifestPropertiesResponse contains the response from method Client.UpdateManifestProperties. +type ClientUpdateManifestPropertiesResponse struct { + ArtifactManifestProperties +} + +// ClientUpdateRepositoryPropertiesResponse contains the response from method Client.UpdateRepositoryProperties. +type ClientUpdateRepositoryPropertiesResponse struct { + ContainerRepositoryProperties +} + +// ClientUpdateTagPropertiesResponse contains the response from method Client.UpdateTagProperties. +type ClientUpdateTagPropertiesResponse struct { + ArtifactTagProperties +} + +// ClientUploadManifestResponse contains the response from method Client.UploadManifest. +type ClientUploadManifestResponse struct { + // ContentLength contains the information returned from the Content-Length header response. + ContentLength *int64 + + // DockerContentDigest contains the information returned from the Docker-Content-Digest header response. + DockerContentDigest *string + + // Location contains the information returned from the Location header response. + Location *string +} diff --git a/sdk/containers/azcontainerregistry/time_rfc3339.go b/sdk/containers/azcontainerregistry/time_rfc3339.go new file mode 100644 index 000000000000..8ef526fa064c --- /dev/null +++ b/sdk/containers/azcontainerregistry/time_rfc3339.go @@ -0,0 +1,87 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. +// DO NOT EDIT. + +package azcontainerregistry + +import ( + "encoding/json" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "reflect" + "regexp" + "strings" + "time" +) + +const ( + utcLayoutJSON = `"2006-01-02T15:04:05.999999999"` + utcLayout = "2006-01-02T15:04:05.999999999" + rfc3339JSON = `"` + time.RFC3339Nano + `"` +) + +// Azure reports time in UTC but it doesn't include the 'Z' time zone suffix in some cases. +var tzOffsetRegex = regexp.MustCompile(`(Z|z|\+|-)(\d+:\d+)*"*$`) + +type timeRFC3339 time.Time + +func (t timeRFC3339) MarshalJSON() (json []byte, err error) { + tt := time.Time(t) + return tt.MarshalJSON() +} + +func (t timeRFC3339) MarshalText() (text []byte, err error) { + tt := time.Time(t) + return tt.MarshalText() +} + +func (t *timeRFC3339) UnmarshalJSON(data []byte) error { + layout := utcLayoutJSON + if tzOffsetRegex.Match(data) { + layout = rfc3339JSON + } + return t.Parse(layout, string(data)) +} + +func (t *timeRFC3339) UnmarshalText(data []byte) (err error) { + layout := utcLayout + if tzOffsetRegex.Match(data) { + layout = time.RFC3339Nano + } + return t.Parse(layout, string(data)) +} + +func (t *timeRFC3339) Parse(layout, value string) error { + p, err := time.Parse(layout, strings.ToUpper(value)) + *t = timeRFC3339(p) + return err +} + +func populateTimeRFC3339(m map[string]any, k string, t *time.Time) { + if t == nil { + return + } else if azcore.IsNullValue(t) { + m[k] = nil + return + } else if reflect.ValueOf(t).IsNil() { + return + } + m[k] = (*timeRFC3339)(t) +} + +func unpopulateTimeRFC3339(data json.RawMessage, fn string, t **time.Time) error { + if data == nil || strings.EqualFold(string(data), "null") { + return nil + } + var aux timeRFC3339 + if err := json.Unmarshal(data, &aux); err != nil { + return fmt.Errorf("struct field %s: %v", fn, err) + } + *t = (*time.Time)(&aux) + return nil +} diff --git a/sdk/containers/azcontainerregistry/utils_test.go b/sdk/containers/azcontainerregistry/utils_test.go new file mode 100644 index 000000000000..04b6604aeaf6 --- /dev/null +++ b/sdk/containers/azcontainerregistry/utils_test.go @@ -0,0 +1,96 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azcontainerregistry + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/internal/recording" + "github.com/stretchr/testify/require" + "os" + "testing" + "time" +) + +// FakeCredential is an empty credential for testing. +type FakeCredential struct { +} + +// GetToken provide a fake access token. +func (c *FakeCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { + return azcore.AccessToken{Token: "Sanitized", ExpiresOn: time.Now().Add(time.Hour * 24).UTC()}, nil +} + +// getCredAndClientOptions will create a credential and a client options for test application. +// The client options will initialize the transport for recording client add recording policy to the pipeline. +// In the record mode, the credential will be a DefaultAzureCredential which combines several common credentials. +// In the playback mode, the credential will be a fake credential which will bypass truly authorization. +func getCredAndClientOptions(t *testing.T) (azcore.TokenCredential, azcore.ClientOptions) { + transport, err := recording.NewRecordingHTTPClient(t, nil) + require.NoError(t, err) + + options := azcore.ClientOptions{ + Transport: transport, + } + + var cred azcore.TokenCredential + if recording.GetRecordMode() != recording.PlaybackMode { + cred, err = azidentity.NewDefaultAzureCredential(nil) + require.NoError(t, err) + } else { + cred = &FakeCredential{} + } + + return cred, options +} + +// startRecording starts the recording. +func startRecording(t *testing.T) { + err := recording.Start(t, "sdk/containers/azcontainerregistry/testdata", nil) + require.NoError(t, err) + t.Cleanup(func() { + err := recording.Stop(t, nil) + require.NoError(t, err) + }) +} + +func TestMain(m *testing.M) { + err := recording.ResetProxy(nil) + if err != nil { + panic(err) + } + if recording.GetRecordMode() == recording.RecordingMode { + defer func() { + err := recording.ResetProxy(nil) + if err != nil { + panic(err) + } + }() + // sanitizer for any uuid string, e.g., subscriptionID + err = recording.AddGeneralRegexSanitizer("00000000-0000-0000-0000-000000000000", `[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}`, nil) + if err != nil { + panic(err) + } + // sanitizer for authentication + err = recording.AddBodyRegexSanitizer("access_token=Sanitized&", "access_token=[^&]+&", nil) + if err != nil { + panic(err) + } + err = recording.AddBodyRegexSanitizer("\"refresh_token\":\".eyJqdGkiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJuYmYiOjQ2NzA0MTEyMTIsImV4cCI6NDY3MDQyMjkxMiwiaWF0Ijo0NjcwNDExMjEyLCJpc3MiOiJBenVyZSBDb250YWluZXIgUmVnaXN0cnkiLCJhdWQiOiJhemFjcmxpdmV0ZXN0LmF6dXJlY3IuaW8iLCJ2ZXJzaW9uIjoiMS4wIiwicmlkIjoiMDAwMCIsImdyYW50X3R5cGUiOiJyZWZyZXNoX3Rva2VuIiwiYXBwaWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJwZXJtaXNzaW9ucyI6eyJBY3Rpb25zIjpbInJlYWQiLCJ3cml0ZSIsImRlbGV0ZSIsImRlbGV0ZWQvcmVhZCIsImRlbGV0ZWQvcmVzdG9yZS9hY3Rpb24iXSwiTm90QWN0aW9ucyI6bnVsbH0sInJvbGVzIjpbXX0=.\"", "\"refresh_token\":\".+\"", nil) + if err != nil { + panic(err) + } + err = recording.AddBodyRegexSanitizer("refresh_token=.eyJqdGkiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJuYmYiOjQ2NzA0MTEyMTIsImV4cCI6NDY3MDQyMjkxMiwiaWF0Ijo0NjcwNDExMjEyLCJpc3MiOiJBenVyZSBDb250YWluZXIgUmVnaXN0cnkiLCJhdWQiOiJhemFjcmxpdmV0ZXN0LmF6dXJlY3IuaW8iLCJ2ZXJzaW9uIjoiMS4wIiwicmlkIjoiMDAwMCIsImdyYW50X3R5cGUiOiJyZWZyZXNoX3Rva2VuIiwiYXBwaWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJwZXJtaXNzaW9ucyI6eyJBY3Rpb25zIjpbInJlYWQiLCJ3cml0ZSIsImRlbGV0ZSIsImRlbGV0ZWQvcmVhZCIsImRlbGV0ZWQvcmVzdG9yZS9hY3Rpb24iXSwiTm90QWN0aW9ucyI6bnVsbH0sInJvbGVzIjpbXX0%3D.&", "refresh_token=[^&]+&", nil) + if err != nil { + panic(err) + } + } + code := m.Run() + os.Exit(code) +}