internal/api/acrsdk.go (240 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package api import ( "bytes" "context" "io/ioutil" "strings" "time" // The autorest generated SDK is used, this file is just a wrapper to it. acrapi "github.com/Azure/acr-cli/acr" "github.com/Azure/acr-cli/auth/oras" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" "github.com/golang-jwt/jwt/v4" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) // Constants that are used throughout this file. const ( prefixHTTPS = "https://" registryURL = ".azurecr.io" manifestTagFetchCount = 100 manifestORASArtifactContentType = "application/vnd.cncf.oras.artifact.manifest.v1+json" manifestOCIArtifactContentType = "application/vnd.oci.artifact.manifest.v1+json" manifestOCIImageContentType = v1.MediaTypeImageManifest manifestOCIImageIndexContentType = v1.MediaTypeImageIndex manifestImageContentType = "application/vnd.docker.distribution.manifest.v2+json" manifestListContentType = "application/vnd.docker.distribution.manifest.list.v2+json" manifestAcceptHeader = "*/*, " + manifestORASArtifactContentType + ", " + manifestOCIArtifactContentType + ", " + manifestOCIImageContentType + ", " + manifestOCIImageIndexContentType + ", " + manifestImageContentType + ", " + manifestListContentType ) // The AcrCLIClient is the struct that will be in charge of doing the http requests to the registry. // it implements the AcrCLIClientInterface. type AcrCLIClient struct { AutorestClient acrapi.BaseClient // manifestTagFetchCount refers to how many tags or manifests can be retrieved in a single http request. manifestTagFetchCount int32 loginURL string // token refers to an ACR access token for use with bearer authentication. token *adal.Token // accessTokenExp refers to the expiration time for the access token, it is in a unix time format represented by a // 64 bit integer. accessTokenExp int64 } // LoginURL returns the FQDN for a registry. func LoginURL(registryName string) string { // TODO: if the registry is in another cloud (i.e. dogfood) a full FQDN for the registry should be specified. if strings.Contains(registryName, ".") { return registryName } return registryName + registryURL } // LoginURLWithPrefix return the hostname of a registry. func LoginURLWithPrefix(loginURL string) string { urlWithPrefix := loginURL if !strings.HasPrefix(loginURL, prefixHTTPS) { urlWithPrefix = prefixHTTPS + loginURL } return urlWithPrefix } // newAcrCLIClient creates a client that does not have any authentication. func newAcrCLIClient(loginURL string) AcrCLIClient { loginURLPrefix := LoginURLWithPrefix(loginURL) return AcrCLIClient{ AutorestClient: acrapi.NewWithoutDefaults(loginURLPrefix), // The manifestTagFetchCount is set to the default which is 100 manifestTagFetchCount: manifestTagFetchCount, loginURL: loginURL, } } // newAcrCLIClientWithBasicAuth creates a client that uses basic authentication. func newAcrCLIClientWithBasicAuth(loginURL string, username string, password string) AcrCLIClient { newAcrCLIClient := newAcrCLIClient(loginURL) newAcrCLIClient.AutorestClient.Authorizer = autorest.NewBasicAuthorizer(username, password) return newAcrCLIClient } // newAcrCLIClientWithBearerAuth creates a client that uses bearer token authentication. func newAcrCLIClientWithBearerAuth(loginURL string, refreshToken string) (AcrCLIClient, error) { newAcrCLIClient := newAcrCLIClient(loginURL) ctx := context.Background() accessTokenResponse, err := newAcrCLIClient.AutorestClient.GetAcrAccessToken(ctx, loginURL, "registry:catalog:* repository:*:*", refreshToken) if err != nil { return newAcrCLIClient, err } token := &adal.Token{ AccessToken: *accessTokenResponse.AccessToken, RefreshToken: refreshToken, } newAcrCLIClient.token = token newAcrCLIClient.AutorestClient.Authorizer = autorest.NewBearerAuthorizer(token) // The expiration time is stored in the struct to make it easy to determine if a token is expired. exp, err := getExpiration(token.AccessToken) if err != nil { return newAcrCLIClient, err } newAcrCLIClient.accessTokenExp = exp return newAcrCLIClient, nil } // GetAcrCLIClientWithAuth obtains a client that has authentication for making ACR http requests func GetAcrCLIClientWithAuth(loginURL string, username string, password string, configs []string) (*AcrCLIClient, error) { if username == "" && password == "" { // If both username and password are empty then the docker config file will be used, it can be found in the default // location or in a location specified by the configs string array store, err := oras.NewStore(configs...) if err != nil { return nil, errors.Wrap(err, "error resolving authentication") } cred, err := store.Credential(context.Background(), loginURL) if err != nil { return nil, errors.Wrap(err, "error resolving authentication") } username = cred.Username password = cred.Password // fallback to refresh token if it is available if password == "" && cred.RefreshToken != "" { password = cred.RefreshToken } } // If the password is empty then the authentication failed. if password == "" { return nil, errors.New("unable to resolve authentication, missing identity token or password") } var acrClient AcrCLIClient if username == "" || username == "00000000-0000-0000-0000-000000000000" { // If the username is empty an ACR refresh token was used. var err error acrClient, err = newAcrCLIClientWithBearerAuth(loginURL, password) if err != nil { return nil, errors.Wrap(err, "error resolving authentication") } return &acrClient, nil } // if both the username and password were specified basic authentication can be assumed. acrClient = newAcrCLIClientWithBasicAuth(loginURL, username, password) return &acrClient, nil } // refreshAcrCLIClientToken obtains a new token and gets its expiration time. func refreshAcrCLIClientToken(ctx context.Context, c *AcrCLIClient) error { accessTokenResponse, err := c.AutorestClient.GetAcrAccessToken(ctx, c.loginURL, "repository:*:*", c.token.RefreshToken) if err != nil { return err } token := &adal.Token{ AccessToken: *accessTokenResponse.AccessToken, RefreshToken: c.token.RefreshToken, } c.token = token c.AutorestClient.Authorizer = autorest.NewBearerAuthorizer(token) exp, err := getExpiration(token.AccessToken) if err != nil { return err } c.accessTokenExp = exp return nil } // getExpiration is used to obtain the expiration out of a jwt token. func getExpiration(token string) (int64, error) { parser := jwt.Parser{SkipClaimsValidation: true} mapC := jwt.MapClaims{} // Since we only need the expiration time there is no need for verifying the signature of the token. _, _, err := parser.ParseUnverified(token, mapC) if err != nil { return 0, err } if fExp, ok := mapC["exp"].(float64); ok { return int64(fExp), nil } return 0, errors.New("unable to obtain expiration date for token") } // isExpired return true when the token inside an acrClient is expired and a new should be requested. func (c *AcrCLIClient) isExpired() bool { if c.token == nil { // there is no token so basic auth can be assumed. return false } // 5 minutes are subtracted to make sure that there won't be a case were a client with an expired token tries doing a request. return (time.Now().Add(5 * time.Minute)).Unix() > c.accessTokenExp } // GetAcrTags list the tags of a repository with their attributes. func (c *AcrCLIClient) GetAcrTags(ctx context.Context, repoName string, orderBy string, last string) (*acrapi.RepositoryTagsType, error) { if c.isExpired() { if err := refreshAcrCLIClientToken(ctx, c); err != nil { return nil, err } } tags, err := c.AutorestClient.GetAcrTags(ctx, repoName, last, &c.manifestTagFetchCount, orderBy, "") if err != nil { // tags might contain information such as status codes, so it a pointer to it is returned instead of nil. return &tags, err } return &tags, nil } // DeleteAcrTag deletes the tag by reference. func (c *AcrCLIClient) DeleteAcrTag(ctx context.Context, repoName string, reference string) (*autorest.Response, error) { if c.isExpired() { if err := refreshAcrCLIClientToken(ctx, c); err != nil { return nil, err } } resp, err := c.AutorestClient.DeleteAcrTag(ctx, repoName, reference) if err != nil { return &resp, err } return &resp, nil } // GetAcrManifests list all the manifest in a repository with their attributes. func (c *AcrCLIClient) GetAcrManifests(ctx context.Context, repoName string, orderBy string, last string) (*acrapi.Manifests, error) { if c.isExpired() { if err := refreshAcrCLIClientToken(ctx, c); err != nil { return nil, err } } manifests, err := c.AutorestClient.GetAcrManifests(ctx, repoName, last, &c.manifestTagFetchCount, orderBy) if err != nil { return &manifests, err } return &manifests, nil } // DeleteManifest deletes a manifest using the digest as a reference. func (c *AcrCLIClient) DeleteManifest(ctx context.Context, repoName string, reference string) (*autorest.Response, error) { if c.isExpired() { if err := refreshAcrCLIClientToken(ctx, c); err != nil { return nil, err } } resp, err := c.AutorestClient.DeleteManifest(ctx, repoName, reference) if err != nil { return &resp, err } return &resp, nil } // GetManifest fetches a manifest (could be a Manifest List or a v2 manifest) and returns it as a byte array. // This is used when a manifest list is wanted, first the bytes are obtained and then unmarshalled into a new struct. func (c *AcrCLIClient) GetManifest(ctx context.Context, repoName string, reference string) ([]byte, error) { if c.isExpired() { if err := refreshAcrCLIClientToken(ctx, c); err != nil { return nil, err } } var result acrapi.SetObject req, err := c.AutorestClient.GetManifestPreparer(ctx, repoName, reference, manifestAcceptHeader) if err != nil { err = autorest.NewErrorWithError(err, "acr.BaseClient", "GetManifest", nil, "Failure preparing request") return nil, err } resp, err := c.AutorestClient.GetManifestSender(req) if err != nil { result.Response = autorest.Response{Response: resp} err = autorest.NewErrorWithError(err, "acr.BaseClient", "GetManifest", resp, "Failure sending request") return nil, err } var manifestBytes []byte if resp.Body != nil { manifestBytes, err = ioutil.ReadAll(resp.Body) if err != nil { return nil, err } } resp.Body = ioutil.NopCloser(bytes.NewBuffer(manifestBytes)) _, err = c.AutorestClient.GetManifestResponder(resp) if err != nil { err = autorest.NewErrorWithError(err, "acr.BaseClient", "GetManifest", resp, "Failure responding to request") return nil, err } return manifestBytes, nil } // AcrCLIClientInterface defines the required methods that the acr-cli will need to use. type AcrCLIClientInterface interface { GetAcrTags(ctx context.Context, repoName string, orderBy string, last string) (*acrapi.RepositoryTagsType, error) DeleteAcrTag(ctx context.Context, repoName string, reference string) (*autorest.Response, error) GetAcrManifests(ctx context.Context, repoName string, orderBy string, last string) (*acrapi.Manifests, error) DeleteManifest(ctx context.Context, repoName string, reference string) (*autorest.Response, error) GetManifest(ctx context.Context, repoName string, reference string) ([]byte, error) }