diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go new file mode 100644 index 00000000..89d2f21d --- /dev/null +++ b/cmd/skopeo/delete.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" +) + +func deleteHandler(context *cli.Context) { + if len(context.Args()) != 1 { + logrus.Fatal("Usage: delete imageReference") + } + + image, err := parseImageSource(context, context.Args()[0]) + if err != nil { + logrus.Fatal(err.Error()) + } + + if err := image.Delete(); err != nil { + logrus.Fatal(err) + } +} + +var deleteCmd = cli.Command{ + Name: "delete", + Usage: "Delete given image", + Action: deleteHandler, +} diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index 55bf2b77..66ce2f6e 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -63,6 +63,7 @@ func main() { copyCmd, inspectCmd, layersCmd, + deleteCmd, standaloneSignCmd, standaloneVerifyCmd, } diff --git a/directory/directory.go b/directory/directory.go index 547e9942..5fd84875 100644 --- a/directory/directory.go +++ b/directory/directory.go @@ -111,3 +111,7 @@ func (s *dirImageSource) GetSignatures() ([][]byte, error) { } return signatures, nil } + +func (s *dirImageSource) Delete() error { + return fmt.Errorf("directory#dirImageSource.Delete() not implmented") +} diff --git a/docker/docker_image_src.go b/docker/docker_image_src.go index 65d6e76f..961f7bc1 100644 --- a/docker/docker_image_src.go +++ b/docker/docker_image_src.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/Sirupsen/logrus" + "github.com/projectatomic/skopeo/docker/utils" "github.com/projectatomic/skopeo/reference" "github.com/projectatomic/skopeo/types" ) @@ -94,3 +95,51 @@ func (s *dockerImageSource) GetLayer(digest string) (io.ReadCloser, error) { func (s *dockerImageSource) GetSignatures() ([][]byte, error) { return [][]byte{}, nil } + +func (s *dockerImageSource) Delete() error { + var body []byte + + // When retrieving the digest from a registry >= 2.3 use the following header: + // "Accept": "application/vnd.docker.distribution.manifest.v2+json" + headers := make(map[string][]string) + headers["Accept"] = []string{utils.DockerV2Schema2MIMEType} + + getURL := fmt.Sprintf(manifestURL, s.ref.RemoteName(), s.tag) + get, err := s.c.makeRequest("GET", getURL, headers, nil) + if err != nil { + return err + } + defer get.Body.Close() + body, err = ioutil.ReadAll(get.Body) + if err != nil { + return err + } + switch get.StatusCode { + case http.StatusOK: + case http.StatusNotFound: + return fmt.Errorf("Unable to delete %v. Image may not exist or is not stored with a v2 Schema in a v2 registry.", s.ref) + default: + return fmt.Errorf("Failed to delete %v: %v (%v)", s.ref, body, get.Status) + } + + digest := get.Header.Get("Docker-Content-Digest") + deleteURL := fmt.Sprintf(manifestURL, s.ref.RemoteName(), digest) + + // When retrieving the digest from a registry >= 2.3 use the following header: + // "Accept": "application/vnd.docker.distribution.manifest.v2+json" + delete, err := s.c.makeRequest("DELETE", deleteURL, headers, nil) + if err != nil { + return err + } + defer delete.Body.Close() + + body, err = ioutil.ReadAll(delete.Body) + if err != nil { + return err + } + if delete.StatusCode != http.StatusAccepted { + return fmt.Errorf("Failed to delete %v: %v (%v)", deleteURL, body, delete.Status) + } + + return nil +} diff --git a/man1/skopeo.1 b/man1/skopeo.1 index b13804bd..9be13f08 100644 --- a/man1/skopeo.1 +++ b/man1/skopeo.1 @@ -10,6 +10,8 @@ skopeo \(em Inspect Docker images and repositories on registries .SH SYNOPSIS \fBskopeo copy\fR [\fB--sign-by=\fRkey-ID] source-location destination-location .PP +\fBskopeo delete\fR source-location +.PP \fBskopeo inspect\fR image-name [\fB--raw\fR] .PP \fBskopeo layers\fR image-name @@ -54,12 +56,22 @@ Copy an image (manifest, filesystem layers, signatures) from one location to ano .B source-location and .B destination-location -can be \fBdocker://\fRdocker-reference, \fBdir:\fRlocal-path, or \fBatomic:\fRimagestream-name\fB:\fRtag . +can be \fB\%docker://\fRdocker-reference, \fBdir:\fRlocal-path, or \fBatomic:\fRimagestream-name\fB:\fRtag . .sp \fB\-\-sign\-by=\fRkey-id Add a signature by the specified key ID for image name corresponding to \fBdestination-location\fR. Existing signatures, if any, are preserved as well. .TP +.B delete +Mark an image for deletion. You must then run docker registry garabage collection to recover the disk space. E.g., +.sp +\fBdocker exec -it registry bin/registry \\ +.br +garbage-collect /etc/docker/registry/config.yml\fR +.sp 2 +Additionally, the registry must allow deletions by setting \fB\%REGISTRY_STORAGE_DELETE_ENABLED=true\fR +for the registry daemon. +.TP .B inspect Return low-level information on images in a registry .sp diff --git a/openshift/openshift.go b/openshift/openshift.go index f9a647e9..07006c12 100644 --- a/openshift/openshift.go +++ b/openshift/openshift.go @@ -396,3 +396,7 @@ type status struct { // Details *StatusDetails `json:"details,omitempty"` Code int32 `json:"code,omitempty"` } + +func (s *openshiftImageSource) Delete() error { + return fmt.Errorf("openshift#openshiftImageSource.Delete() not implmented") +} diff --git a/types/types.go b/types/types.go index 9667a00c..aac979af 100644 --- a/types/types.go +++ b/types/types.go @@ -33,6 +33,8 @@ type ImageSource interface { GetLayer(digest string) (io.ReadCloser, error) // GetSignatures returns the image's signatures. It may use a remote (= slow) service. GetSignatures() ([][]byte, error) + // Delete image from registry, if operation is supported + Delete() error } // ImageDestination is a service, possibly remote (= slow), to store components of a single image.