Merge pull request #322 from mtrmac/x-registry-supports-signatures

Add support for the X-Registry-Supports-Signatures API extension
This commit is contained in:
Miloslav Trmač
2017-04-03 16:40:01 +02:00
committed by GitHub
6 changed files with 321 additions and 54 deletions

View File

@@ -473,6 +473,64 @@ func (s *CopySuite) TestCopyDockerSigstore(c *check.C) {
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy, "--registries.d", registriesDir, "copy", ourRegistry+"public/busybox", dirDest)
}
// atomic: and docker: X-Registry-Supports-Signatures works and interoperates
func (s *CopySuite) TestCopyAtomicExtension(c *check.C) {
mech, _, err := signature.NewEphemeralGPGSigningMechanism([]byte{})
c.Assert(err, check.IsNil)
defer mech.Close()
if err := mech.SupportsSigning(); err != nil { // FIXME? Test that the reading/writing works using signatures from fixtures
c.Skip(fmt.Sprintf("Signing not supported: %v", err))
}
topDir, err := ioutil.TempDir("", "atomic-extension")
c.Assert(err, check.IsNil)
defer os.RemoveAll(topDir)
for _, subdir := range []string{"dirAA", "dirAD", "dirDA", "dirDD", "registries.d"} {
err := os.MkdirAll(filepath.Join(topDir, subdir), 0755)
c.Assert(err, check.IsNil)
}
registriesDir := filepath.Join(topDir, "registries.d")
dirDest := "dir:" + topDir
policy := fileFromFixture(c, "fixtures/policy.json", map[string]string{"@keydir@": s.gpgHome})
defer os.Remove(policy)
// Get an image to work with to an atomic: destination. Also verifies that we can use Docker repositories without X-Registry-Supports-Signatures
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--registries.d", registriesDir, "copy", "docker://busybox", "atomic:localhost:5000/myns/extension:unsigned")
// Pulling an unsigned image using atomic: fails.
assertSkopeoFails(c, ".*Source image rejected: A signature was required, but no signature exists.*",
"--tls-verify=false", "--policy", policy,
"copy", "atomic:localhost:5000/myns/extension:unsigned", dirDest+"/dirAA")
// The same when pulling using docker:
assertSkopeoFails(c, ".*Source image rejected: A signature was required, but no signature exists.*",
"--tls-verify=false", "--policy", policy, "--registries.d", registriesDir,
"copy", "docker://localhost:5000/myns/extension:unsigned", dirDest+"/dirAD")
// Sign the image using atomic:
assertSkopeoSucceeds(c, "", "--tls-verify=false",
"copy", "--sign-by", "personal@example.com", "atomic:localhost:5000/myns/extension:unsigned", "atomic:localhost:5000/myns/extension:atomic")
// Pulling the image using atomic: now succeeds.
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy,
"copy", "atomic:localhost:5000/myns/extension:atomic", dirDest+"/dirAA")
// The same when pulling using docker:
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy, "--registries.d", registriesDir,
"copy", "docker://localhost:5000/myns/extension:atomic", dirDest+"/dirAD")
// Both access methods result in the same data.
destructiveCheckDirImagesAreEqual(c, filepath.Join(topDir, "dirAA"), filepath.Join(topDir, "dirAD"))
// Get another image (different so that they don't share signatures, and sign it using docker://)
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--registries.d", registriesDir,
"copy", "--sign-by", "personal@example.com", "docker://estesp/busybox:ppc64le", "atomic:localhost:5000/myns/extension:extension")
c.Logf("%s", combinedOutputOfCommand(c, "oc", "get", "istag", "extension:extension", "-o", "json"))
// Pulling the image using atomic: succeeds.
assertSkopeoSucceeds(c, "", "--debug", "--tls-verify=false", "--policy", policy,
"copy", "atomic:localhost:5000/myns/extension:extension", dirDest+"/dirDA")
// The same when pulling using docker:
assertSkopeoSucceeds(c, "", "--debug", "--tls-verify=false", "--policy", policy, "--registries.d", registriesDir,
"copy", "docker://localhost:5000/myns/extension:extension", dirDest+"/dirDD")
// Both access methods result in the same data.
destructiveCheckDirImagesAreEqual(c, filepath.Join(topDir, "dirDA"), filepath.Join(topDir, "dirDD"))
}
func (s *SkopeoSuite) TestCopySrcWithAuth(c *check.C) {
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "--dest-creds=testuser:testpassword", "docker://busybox", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url))
dir1, err := ioutil.TempDir("", "copy-1")

View File

@@ -13,6 +13,13 @@
"keyPath": "@keydir@/personal-pubkey.gpg"
}
],
"localhost:5000/myns/extension": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "@keydir@/personal-pubkey.gpg"
}
],
"docker.io/openshift": [
{
"type": "insecureAcceptAnything"
@@ -85,6 +92,13 @@
"keyType": "GPGKeys",
"keyPath": "@keydir@/personal-pubkey.gpg"
}
],
"localhost:5000/myns/extension": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "@keydir@/personal-pubkey.gpg"
}
]
}
}

View File

@@ -18,8 +18,10 @@ import (
"github.com/containers/image/docker/reference"
"github.com/containers/image/types"
"github.com/containers/storage/pkg/homedir"
"github.com/docker/distribution/registry/client"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
@@ -32,20 +34,38 @@ const (
dockerCfgFileName = "config.json"
dockerCfgObsolete = ".dockercfg"
baseURL = "%s://%s/v2/"
baseURLV1 = "%s://%s/v1/_ping"
tagsURL = "%s/tags/list"
manifestURL = "%s/manifests/%s"
blobsURL = "%s/blobs/%s"
blobUploadURL = "%s/blobs/uploads/"
resolvedPingV2URL = "%s://%s/v2/"
resolvedPingV1URL = "%s://%s/v1/_ping"
tagsPath = "/v2/%s/tags/list"
manifestPath = "/v2/%s/manifests/%s"
blobsPath = "/v2/%s/blobs/%s"
blobUploadPath = "/v2/%s/blobs/uploads/"
extensionsSignaturePath = "/extensions/v2/%s/signatures/%s"
minimumTokenLifetimeSeconds = 60
extensionSignatureSchemaVersion = 2 // extensionSignature.Version
extensionSignatureTypeAtomic = "atomic" // extensionSignature.Type
)
// ErrV1NotSupported is returned when we're trying to talk to a
// docker V1 registry.
var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
// extensionSignature and extensionSignatureList come from github.com/openshift/origin/pkg/dockerregistry/server/signaturedispatcher.go:
// signature represents a Docker image signature.
type extensionSignature struct {
Version int `json:"schemaVersion"` // Version specifies the schema version
Name string `json:"name"` // Name must be in "sha256:<digest>@signatureName" format
Type string `json:"type"` // Type is optional, of not set it will be defaulted to "AtomicImageV1"
Content []byte `json:"content"` // Content contains the signature
}
// signatureList represents list of Docker image signatures.
type extensionSignatureList struct {
Signatures []extensionSignature `json:"signatures"`
}
type bearerToken struct {
Token string `json:"token"`
ExpiresIn int `json:"expires_in"`
@@ -54,15 +74,20 @@ type bearerToken struct {
// dockerClient is configuration for dealing with a single Docker registry.
type dockerClient struct {
ctx *types.SystemContext
registry string
username string
password string
scheme string // Cache of a value returned by a successful ping() if not empty
client *http.Client
signatureBase signatureStorageBase
challenges []challenge
scope authScope
// The following members are set by newDockerClient and do not change afterwards.
ctx *types.SystemContext
registry string
username string
password string
client *http.Client
signatureBase signatureStorageBase
scope authScope
// The following members are detected registry properties:
// They are set after a successful detectProperties(), and never change afterwards.
scheme string // Empty value also used to indicate detectProperties() has not yet succeeded.
challenges []challenge
supportsSignatures bool
// The following members are private state for setupRequestAuth, both are valid if token != nil.
token *bearerToken
tokenExpiration time.Time
}
@@ -209,15 +234,13 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool,
}
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
// url is NOT an absolute URL, but a path relative to the /v2/ top-level API path. The host name and schema is taken from the client or autodetected.
func (c *dockerClient) makeRequest(method, url string, headers map[string][]string, stream io.Reader) (*http.Response, error) {
if c.scheme == "" {
if err := c.ping(); err != nil {
return nil, err
}
// The host name and schema is taken from the client or autodetected, and the path is relative to it, i.e. the path usually starts with /v2/.
func (c *dockerClient) makeRequest(method, path string, headers map[string][]string, stream io.Reader) (*http.Response, error) {
if err := c.detectProperties(); err != nil {
return nil, err
}
url = fmt.Sprintf(baseURL, c.scheme, c.registry) + url
url := fmt.Sprintf("%s://%s%s", c.scheme, c.registry, path)
return c.makeRequestToResolvedURL(method, url, headers, stream, -1, true)
}
@@ -398,21 +421,28 @@ func getAuth(ctx *types.SystemContext, registry string) (string, string, error)
return "", "", nil
}
func (c *dockerClient) ping() error {
// detectProperties detects various properties of the registry.
// See the dockerClient documentation for members which are affected by this.
func (c *dockerClient) detectProperties() error {
if c.scheme != "" {
return nil
}
ping := func(scheme string) error {
url := fmt.Sprintf(baseURL, scheme, c.registry)
url := fmt.Sprintf(resolvedPingV2URL, scheme, c.registry)
resp, err := c.makeRequestToResolvedURL("GET", url, nil, nil, -1, true)
logrus.Debugf("Ping %s err %#v", url, err)
if err != nil {
return err
}
defer resp.Body.Close()
logrus.Debugf("Ping %s status %d", scheme+"://"+c.registry+"/v2/", resp.StatusCode)
logrus.Debugf("Ping %s status %d", url, resp.StatusCode)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
return errors.Errorf("error pinging repository, response code %d", resp.StatusCode)
}
c.challenges = parseAuthHeader(resp.Header)
c.scheme = scheme
c.supportsSignatures = resp.Header.Get("X-Registry-Supports-Signatures") == "1"
return nil
}
err := ping("https")
@@ -426,14 +456,14 @@ func (c *dockerClient) ping() error {
}
// best effort to understand if we're talking to a V1 registry
pingV1 := func(scheme string) bool {
url := fmt.Sprintf(baseURLV1, scheme, c.registry)
url := fmt.Sprintf(resolvedPingV1URL, scheme, c.registry)
resp, err := c.makeRequestToResolvedURL("GET", url, nil, nil, -1, true)
logrus.Debugf("Ping %s err %#v", url, err)
if err != nil {
return false
}
defer resp.Body.Close()
logrus.Debugf("Ping %s status %d", scheme+"://"+c.registry+"/v1/_ping", resp.StatusCode)
logrus.Debugf("Ping %s status %d", url, resp.StatusCode)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
return false
}
@@ -450,6 +480,30 @@ func (c *dockerClient) ping() error {
return err
}
// getExtensionsSignatures returns signatures from the X-Registry-Supports-Signatures API extension,
// using the original data structures.
func (c *dockerClient) getExtensionsSignatures(ref dockerReference, manifestDigest digest.Digest) (*extensionSignatureList, error) {
path := fmt.Sprintf(extensionsSignaturePath, reference.Path(ref.ref), manifestDigest)
res, err := c.makeRequest("GET", path, nil, nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, client.HandleErrorResponse(res)
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
var parsedBody extensionSignatureList
if err := json.Unmarshal(body, &parsedBody); err != nil {
return nil, errors.Wrapf(err, "Error decoding signature list")
}
return &parsedBody, nil
}
func getDefaultConfigDir(confPath string) string {
return filepath.Join(homedir.Get(), confPath)
}

View File

@@ -40,8 +40,8 @@ func (i *Image) SourceRefFullName() string {
// GetRepositoryTags list all tags available in the repository. Note that this has no connection with the tag(s) used for this specific image, if any.
func (i *Image) GetRepositoryTags() ([]string, error) {
url := fmt.Sprintf(tagsURL, reference.Path(i.src.ref.ref))
res, err := i.src.c.makeRequest("GET", url, nil, nil)
path := fmt.Sprintf(tagsPath, reference.Path(i.src.ref.ref))
res, err := i.src.c.makeRequest("GET", path, nil, nil)
if err != nil {
return nil, err
}

View File

@@ -2,6 +2,8 @@ package docker
import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@@ -70,7 +72,17 @@ func (d *dockerImageDestination) SupportedManifestMIMETypes() []string {
// SupportsSignatures returns an error (to be displayed to the user) if the destination certainly can't store signatures.
// Note: It is still possible for PutSignatures to fail if SupportsSignatures returns nil.
func (d *dockerImageDestination) SupportsSignatures() error {
return errors.Errorf("Pushing signatures to a Docker Registry is not supported")
if err := d.c.detectProperties(); err != nil {
return err
}
switch {
case d.c.signatureBase != nil:
return nil
case d.c.supportsSignatures:
return nil
default:
return errors.Errorf("X-Registry-Supports-Signatures extension not supported, and lookaside is not configured")
}
}
// ShouldCompressLayers returns true iff it is desirable to compress layer blobs written to this destination.
@@ -111,16 +123,16 @@ func (d *dockerImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobI
}
// FIXME? Chunked upload, progress reporting, etc.
uploadURL := fmt.Sprintf(blobUploadURL, reference.Path(d.ref.ref))
logrus.Debugf("Uploading %s", uploadURL)
res, err := d.c.makeRequest("POST", uploadURL, nil, nil)
uploadPath := fmt.Sprintf(blobUploadPath, reference.Path(d.ref.ref))
logrus.Debugf("Uploading %s", uploadPath)
res, err := d.c.makeRequest("POST", uploadPath, nil, nil)
if err != nil {
return types.BlobInfo{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusAccepted {
logrus.Debugf("Error initiating layer upload, response %#v", *res)
return types.BlobInfo{}, errors.Errorf("Error initiating layer upload to %s, status %d", uploadURL, res.StatusCode)
return types.BlobInfo{}, errors.Errorf("Error initiating layer upload to %s, status %d", uploadPath, res.StatusCode)
}
uploadLocation, err := res.Location()
if err != nil {
@@ -167,10 +179,10 @@ func (d *dockerImageDestination) HasBlob(info types.BlobInfo) (bool, int64, erro
if info.Digest == "" {
return false, -1, errors.Errorf(`"Can not check for a blob with unknown digest`)
}
checkURL := fmt.Sprintf(blobsURL, reference.Path(d.ref.ref), info.Digest.String())
checkPath := fmt.Sprintf(blobsPath, reference.Path(d.ref.ref), info.Digest.String())
logrus.Debugf("Checking %s", checkURL)
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil)
logrus.Debugf("Checking %s", checkPath)
res, err := d.c.makeRequest("HEAD", checkPath, nil, nil)
if err != nil {
return false, -1, err
}
@@ -205,14 +217,14 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
if err != nil {
return err
}
url := fmt.Sprintf(manifestURL, reference.Path(d.ref.ref), refTail)
path := fmt.Sprintf(manifestPath, reference.Path(d.ref.ref), refTail)
headers := map[string][]string{}
mimeType := manifest.GuessMIMEType(m)
if mimeType != "" {
headers["Content-Type"] = []string{mimeType}
}
res, err := d.c.makeRequest("PUT", url, headers, bytes.NewReader(m))
res, err := d.c.makeRequest("PUT", path, headers, bytes.NewReader(m))
if err != nil {
return err
}
@@ -223,12 +235,32 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
logrus.Debugf("Error body %s", string(body))
}
logrus.Debugf("Error uploading manifest, status %d, %#v", res.StatusCode, res)
return errors.Errorf("Error uploading manifest to %s, status %d", url, res.StatusCode)
return errors.Errorf("Error uploading manifest to %s, status %d", path, res.StatusCode)
}
return nil
}
func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error {
// Do not fail if we dont really need to support signatures.
if len(signatures) == 0 {
return nil
}
if err := d.c.detectProperties(); err != nil {
return err
}
switch {
case d.c.signatureBase != nil:
return d.putSignaturesToLookaside(signatures)
case d.c.supportsSignatures:
return d.putSignaturesToAPIExtension(signatures)
default:
return errors.Errorf("X-Registry-Supports-Signatures extension not supported, and lookaside is not configured")
}
}
// putSignaturesToLookaside implements PutSignatures() from the lookaside location configured in s.c.signatureBase,
// which is not nil.
func (d *dockerImageDestination) putSignaturesToLookaside(signatures [][]byte) error {
// FIXME? This overwrites files one at a time, definitely not atomic.
// A failure when updating signatures with a reordered copy could lose some of them.
@@ -236,9 +268,6 @@ func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error {
if len(signatures) == 0 {
return nil
}
if d.c.signatureBase == nil {
return errors.Errorf("Pushing signatures to a Docker Registry is not supported, and there is no applicable signature storage configured")
}
if d.manifestDigest.String() == "" {
// This shouldnt happen, ImageDestination users are required to call PutManifest before PutSignatures
@@ -318,6 +347,82 @@ func (c *dockerClient) deleteOneSignature(url *url.URL) (missing bool, err error
}
}
// putSignaturesToAPIExtension implements PutSignatures() using the X-Registry-Supports-Signatures API extension.
func (d *dockerImageDestination) putSignaturesToAPIExtension(signatures [][]byte) error {
// Skip dealing with the manifest digest, or reading the old state, if not necessary.
if len(signatures) == 0 {
return nil
}
if d.manifestDigest.String() == "" {
// This shouldnt happen, ImageDestination users are required to call PutManifest before PutSignatures
return errors.Errorf("Unknown manifest digest, can't add signatures")
}
// Because image signatures are a shared resource in Atomic Registry, the default upload
// always adds signatures. Eventually we should also allow removing signatures,
// but the X-Registry-Supports-Signatures API extension does not support that yet.
existingSignatures, err := d.c.getExtensionsSignatures(d.ref, d.manifestDigest)
if err != nil {
return err
}
existingSigNames := map[string]struct{}{}
for _, sig := range existingSignatures.Signatures {
existingSigNames[sig.Name] = struct{}{}
}
sigExists:
for _, newSig := range signatures {
for _, existingSig := range existingSignatures.Signatures {
if existingSig.Version == extensionSignatureSchemaVersion && existingSig.Type == extensionSignatureTypeAtomic && bytes.Equal(existingSig.Content, newSig) {
continue sigExists
}
}
// The API expect us to invent a new unique name. This is racy, but hopefully good enough.
var signatureName string
for {
randBytes := make([]byte, 16)
n, err := rand.Read(randBytes)
if err != nil || n != 16 {
return errors.Wrapf(err, "Error generating random signature len %d", n)
}
signatureName = fmt.Sprintf("%s@%032x", d.manifestDigest.String(), randBytes)
if _, ok := existingSigNames[signatureName]; !ok {
break
}
}
sig := extensionSignature{
Version: extensionSignatureSchemaVersion,
Name: signatureName,
Type: extensionSignatureTypeAtomic,
Content: newSig,
}
body, err := json.Marshal(sig)
if err != nil {
return err
}
path := fmt.Sprintf(extensionsSignaturePath, reference.Path(d.ref.ref), d.manifestDigest.String())
res, err := d.c.makeRequest("PUT", path, nil, bytes.NewReader(body))
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
body, err := ioutil.ReadAll(res.Body)
if err == nil {
logrus.Debugf("Error body %s", string(body))
}
logrus.Debugf("Error uploading signature, status %d, %#v", res.StatusCode, res)
return errors.Errorf("Error uploading signature to %s, status %d", path, res.StatusCode)
}
}
return nil
}
// Commit marks the process of storing the image as successful and asks for the image to be persisted.
// WARNING: This does not have any transactional semantics:
// - Uploaded data MAY be visible to others before Commit() is called

View File

@@ -93,10 +93,10 @@ func (s *dockerImageSource) GetManifest() ([]byte, string, error) {
}
func (s *dockerImageSource) fetchManifest(tagOrDigest string) ([]byte, string, error) {
url := fmt.Sprintf(manifestURL, reference.Path(s.ref.ref), tagOrDigest)
path := fmt.Sprintf(manifestPath, reference.Path(s.ref.ref), tagOrDigest)
headers := make(map[string][]string)
headers["Accept"] = s.requestedManifestMIMETypes
res, err := s.c.makeRequest("GET", url, headers, nil)
res, err := s.c.makeRequest("GET", path, headers, nil)
if err != nil {
return nil, "", err
}
@@ -179,9 +179,9 @@ func (s *dockerImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64,
return s.getExternalBlob(info.URLs)
}
url := fmt.Sprintf(blobsURL, reference.Path(s.ref.ref), info.Digest.String())
logrus.Debugf("Downloading %s", url)
res, err := s.c.makeRequest("GET", url, nil, nil)
path := fmt.Sprintf(blobsPath, reference.Path(s.ref.ref), info.Digest.String())
logrus.Debugf("Downloading %s", path)
res, err := s.c.makeRequest("GET", path, nil, nil)
if err != nil {
return nil, 0, err
}
@@ -193,10 +193,22 @@ func (s *dockerImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64,
}
func (s *dockerImageSource) GetSignatures() ([][]byte, error) {
if s.c.signatureBase == nil { // Skip dealing with the manifest digest if not necessary.
if err := s.c.detectProperties(); err != nil {
return nil, err
}
switch {
case s.c.signatureBase != nil:
return s.getSignaturesFromLookaside()
case s.c.supportsSignatures:
return s.getSignaturesFromAPIExtension()
default:
return [][]byte{}, nil
}
}
// getSignaturesFromLookaside implements GetSignatures() from the lookaside location configured in s.c.signatureBase,
// which is not nil.
func (s *dockerImageSource) getSignaturesFromLookaside() ([][]byte, error) {
if err := s.ensureManifestIsLoaded(); err != nil {
return nil, err
}
@@ -261,6 +273,30 @@ func (s *dockerImageSource) getOneSignature(url *url.URL) (signature []byte, mis
}
}
// getSignaturesFromAPIExtension implements GetSignatures() using the X-Registry-Supports-Signatures API extension.
func (s *dockerImageSource) getSignaturesFromAPIExtension() ([][]byte, error) {
if err := s.ensureManifestIsLoaded(); err != nil {
return nil, err
}
manifestDigest, err := manifest.Digest(s.cachedManifest)
if err != nil {
return nil, err
}
parsedBody, err := s.c.getExtensionsSignatures(s.ref, manifestDigest)
if err != nil {
return nil, err
}
var sigs [][]byte
for _, sig := range parsedBody.Signatures {
if sig.Version == extensionSignatureSchemaVersion && sig.Type == extensionSignatureTypeAtomic {
sigs = append(sigs, sig.Content)
}
}
return sigs, nil
}
// deleteImage deletes the named image from the registry, if supported.
func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
c, err := newDockerClient(ctx, ref, true, "push")
@@ -277,8 +313,8 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
if err != nil {
return err
}
getURL := fmt.Sprintf(manifestURL, reference.Path(ref.ref), refTail)
get, err := c.makeRequest("GET", getURL, headers, nil)
getPath := fmt.Sprintf(manifestPath, reference.Path(ref.ref), refTail)
get, err := c.makeRequest("GET", getPath, headers, nil)
if err != nil {
return err
}
@@ -296,11 +332,11 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
}
digest := get.Header.Get("Docker-Content-Digest")
deleteURL := fmt.Sprintf(manifestURL, reference.Path(ref.ref), digest)
deletePath := fmt.Sprintf(manifestPath, reference.Path(ref.ref), digest)
// When retrieving the digest from a registry >= 2.3 use the following header:
// "Accept": "application/vnd.docker.distribution.manifest.v2+json"
delete, err := c.makeRequest("DELETE", deleteURL, headers, nil)
delete, err := c.makeRequest("DELETE", deletePath, headers, nil)
if err != nil {
return err
}
@@ -311,7 +347,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
return err
}
if delete.StatusCode != http.StatusAccepted {
return errors.Errorf("Failed to delete %v: %s (%v)", deleteURL, string(body), delete.Status)
return errors.Errorf("Failed to delete %v: %s (%v)", deletePath, string(body), delete.Status)
}
if c.signatureBase != nil {