github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/docker/registry/internal/base_manifests.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package internal
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"net/http"
    10  	"strings"
    11  
    12  	"github.com/juju/errors"
    13  )
    14  
    15  //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/manifests_mock.go github.com/juju/juju/docker/registry/internal ArchitectureGetter
    16  
    17  type manifestsResponseV1 struct {
    18  	SchemaVersion int    `json:"schemaVersion"`
    19  	Architecture  string `json:"architecture"`
    20  }
    21  
    22  type manifestsResponseV2Config struct {
    23  	Digest string `json:"digest"`
    24  }
    25  
    26  type manifestsResponseV2 struct {
    27  	SchemaVersion int                       `json:"schemaVersion"`
    28  	Config        manifestsResponseV2Config `json:"config"`
    29  }
    30  
    31  // ManifestsResult is the result of GetManifests.
    32  type ManifestsResult struct {
    33  	Architecture string
    34  	Digest       string
    35  }
    36  
    37  // BlobsResponse is the result of GetBlobs.
    38  type BlobsResponse struct {
    39  	Architecture string `json:"architecture"`
    40  }
    41  
    42  // ArchitectureGetter defines manifests and blob APIs.
    43  type ArchitectureGetter interface {
    44  	GetManifests(imageName, tag string) (*ManifestsResult, error)
    45  	GetBlobs(imageName, digest string) (*BlobsResponse, error)
    46  }
    47  
    48  // GetArchitecture returns the archtecture of the image for the specified tag.
    49  func (c baseClient) GetArchitecture(imageName, tag string) (string, error) {
    50  	return getArchitecture(imageName, tag, c)
    51  }
    52  
    53  func getArchitecture(imageName, tag string, client ArchitectureGetter) (string, error) {
    54  	manifests, err := client.GetManifests(imageName, tag)
    55  	if err != nil {
    56  		return "", errors.Annotatef(err, "can not get manifests for %s:%s", imageName, tag)
    57  	}
    58  	if manifests.Architecture == "" && manifests.Digest == "" {
    59  		return "", errors.New(fmt.Sprintf("faild to get manifests for %q %q", imageName, tag))
    60  	}
    61  	if manifests.Architecture != "" {
    62  		return manifests.Architecture, nil
    63  	}
    64  	blobs, err := client.GetBlobs(imageName, manifests.Digest)
    65  	if err != nil {
    66  		return "", errors.Trace(err)
    67  	}
    68  	return blobs.Architecture, nil
    69  }
    70  
    71  // GetManifests returns the manifests of the image for the specified tag.
    72  func (c baseClient) GetManifests(imageName, tag string) (*ManifestsResult, error) {
    73  	repo := getRepositoryOnly(c.ImageRepoDetails().Repository)
    74  	url := c.url("/%s/%s/manifests/%s", repo, imageName, tag)
    75  	return c.GetManifestsCommon(url)
    76  }
    77  
    78  // GetManifestsCommon returns manifests result for the provided url.
    79  func (c baseClient) GetManifestsCommon(url string) (*ManifestsResult, error) {
    80  	resp, err := c.client.Get(url)
    81  	if err != nil {
    82  		return nil, errors.Trace(unwrapNetError(err))
    83  	}
    84  	defer resp.Body.Close()
    85  	return processManifestsResponse(resp)
    86  }
    87  
    88  const (
    89  	manifestContentTypeV1 = "application/vnd.docker.distribution.manifest.v1"
    90  	manifestContentTypeV2 = "application/vnd.docker.distribution.manifest.v2"
    91  )
    92  
    93  func processManifestsResponse(resp *http.Response) (*ManifestsResult, error) {
    94  	contentTypes := resp.Header[http.CanonicalHeaderKey("Content-Type")]
    95  	if len(contentTypes) == 0 {
    96  		return nil, errors.NotSupportedf(`no "Content-Type" found in header of registry API response`)
    97  	}
    98  	contentType := contentTypes[0]
    99  	notSupportedAPIVersionError := errors.NotSupportedf("manifest response version %q", contentType)
   100  	parts := strings.Split(contentType, "+")
   101  	if len(parts) != 2 {
   102  		return nil, notSupportedAPIVersionError
   103  	}
   104  	switch parts[0] {
   105  	case manifestContentTypeV1:
   106  		var data manifestsResponseV1
   107  		if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
   108  			return nil, errors.Trace(err)
   109  		}
   110  		return &ManifestsResult{Architecture: data.Architecture}, nil
   111  	case manifestContentTypeV2:
   112  		var data manifestsResponseV2
   113  		if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
   114  			return nil, errors.Trace(err)
   115  		}
   116  		return &ManifestsResult{Digest: data.Config.Digest}, nil
   117  	default:
   118  		return nil, notSupportedAPIVersionError
   119  	}
   120  }
   121  
   122  // GetBlobs gets the archtecture of the image for the specified tag via blobs API.
   123  func (c baseClient) GetBlobs(imageName, digest string) (*BlobsResponse, error) {
   124  	repo := getRepositoryOnly(c.ImageRepoDetails().Repository)
   125  	url := c.url("/%s/%s/blobs/%s", repo, imageName, digest)
   126  	return c.GetBlobsCommon(url)
   127  }
   128  
   129  // GetBlobsCommon returns blobs result for the provided url.
   130  func (c baseClient) GetBlobsCommon(url string) (*BlobsResponse, error) {
   131  	resp, err := c.client.Get(url)
   132  	logger.Tracef("getting blobs for %q, err %v", url, err)
   133  	if err != nil {
   134  		return nil, errors.Trace(unwrapNetError(err))
   135  	}
   136  	defer resp.Body.Close()
   137  	var result BlobsResponse
   138  	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
   139  		return nil, errors.Trace(err)
   140  	}
   141  	return &result, nil
   142  }