cmd/dist: add ability to edit content

This allows one to edit content in the content store with their favorite
editor. It is as simple as this:

```console
$ dist content edit sha256:58e1a1bb75db1b5a24a462dd5e2915277ea06438c3f105138f97eb53149673c4
```

The above will pop up your $EDITOR, where you can make changes to the content.
When you are done, save and the new version will be added to the content store.
The digest of the new content will be printed to stdout:

```console
sha256:247f30ac320db65f3314b63b908a3aeaac5813eade6cabc9198b5883b22807bc
```

We can then retrieve the content quite easily:

```console
$ dist content get sha256:247f30ac320db65f3314b63b908a3aeaac5813eade6cabc9198b5883b22807bc
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
   "config": {
      "mediaType": "application/vnd.docker.container.image.v1+json",
      "size": 1278,
      "digest": "sha256:4a415e3663882fbc554ee830889c68a33b3585503892cc718a4698e91ef2a526"
   },
   "annotations": {},
   "layers": [
      {
         "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
         "size": 1905270,
         "digest": "sha256:627beaf3eaaff1c0bc3311d60fb933c17ad04fe377e1043d9593646d8ae3bfe1"
      }
   ]
}

```

In this case, an annotations field was added to the original manifest.

While this implementation is very simple, we can add all sorts of validation
and tooling to allow one to edit images inline. Coupled with declaring the
mediatype, we could return specific errors that can allow a user to craft
valid, working modifications to images for testing and profit.

Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
Stephen J Day 2017-04-28 15:17:35 -07:00
parent 19edcb72b5
commit cec8578ff0
No known key found for this signature in database
GPG Key ID: 67B3DED84EDC823F
4 changed files with 121 additions and 2 deletions

113
cmd/dist/edit.go vendored Normal file
View File

@ -0,0 +1,113 @@
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
contentapi "github.com/containerd/containerd/api/services/content"
contentservice "github.com/containerd/containerd/services/content"
digest "github.com/opencontainers/go-digest"
"github.com/urfave/cli"
)
var editCommand = cli.Command{
Name: "edit",
Usage: "edit a blob and return a new digest.",
ArgsUsage: "[flags] <digest>",
Description: `Edit a blob and return a new digest.`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "validate",
Usage: "validate the result against a format (json, mediatype, etc.)",
},
},
Action: func(context *cli.Context) error {
var (
ctx = background
validate = context.String("validate")
object = context.Args().First()
)
if validate != "" {
return errors.New("validating the edit result not supported")
}
// TODO(stevvooe): Support looking up objects by a reference through
// the image metadata storage.
dgst, err := digest.Parse(object)
if err != nil {
return err
}
conn, err := connectGRPC(context)
if err != nil {
return err
}
provider := contentservice.NewProviderFromClient(contentapi.NewContentClient(conn))
ingester := contentservice.NewIngesterFromClient(contentapi.NewContentClient(conn))
rc, err := provider.Reader(ctx, dgst)
if err != nil {
return err
}
defer rc.Close()
nrc, err := edit(rc)
if err != nil {
return err
}
wr, err := ingester.Writer(ctx, "edit-"+object, 0, "") // TODO(stevvooe): Choose a better key?
if err != nil {
return err
}
if _, err := io.Copy(wr, nrc); err != nil {
return err
}
if err := wr.Commit(0, wr.Digest()); err != nil {
return err
}
fmt.Println(wr.Digest())
return nil
},
}
func edit(rd io.Reader) (io.ReadCloser, error) {
tmp, err := ioutil.TempFile("", "edit-")
if err != nil {
return nil, err
}
if _, err := io.Copy(tmp, rd); err != nil {
tmp.Close()
return nil, err
}
cmd := exec.Command("sh", "-c", "$EDITOR "+tmp.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
if err := cmd.Run(); err != nil {
tmp.Close()
return nil, err
}
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
tmp.Close()
return nil, err
}
return tmp, nil
}

1
cmd/dist/main.go vendored
View File

@ -106,6 +106,7 @@ var contentCommand = cli.Command{
ingestCommand,
activeCommand,
getCommand,
editCommand,
deleteCommand,
},
}

View File

@ -127,6 +127,9 @@ func (rw *remoteWriter) Write(p []byte) (n int, err error) {
}
rw.offset += int64(n)
if resp.Digest != "" {
rw.digest = resp.Digest
}
return
}
@ -149,6 +152,8 @@ func (rw *remoteWriter) Commit(size int64, expected digest.Digest) error {
return errors.Errorf("unexpected digest: %v != %v", resp.Digest, expected)
}
rw.digest = resp.Digest
rw.offset = resp.Offset
return nil
}

View File

@ -304,9 +304,9 @@ func (s *Service) Write(session api.Content_WriteServer) (err error) {
if err := wr.Commit(total, expected); err != nil {
return err
}
msg.Digest = wr.Digest()
}
msg.Digest = wr.Digest()
case api.WriteActionAbort:
return s.store.Abort(ref)
}