From cec8578ff0e33855b056b6d6754a71f2dd0afd81 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Fri, 28 Apr 2017 15:17:35 -0700 Subject: [PATCH] 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 --- cmd/dist/edit.go | 113 +++++++++++++++++++++++++++++++++++ cmd/dist/main.go | 1 + services/content/ingester.go | 5 ++ services/content/service.go | 4 +- 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 cmd/dist/edit.go diff --git a/cmd/dist/edit.go b/cmd/dist/edit.go new file mode 100644 index 000000000..c7f655e20 --- /dev/null +++ b/cmd/dist/edit.go @@ -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] ", + 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 +} diff --git a/cmd/dist/main.go b/cmd/dist/main.go index f9acfc859..0d1f946c4 100644 --- a/cmd/dist/main.go +++ b/cmd/dist/main.go @@ -106,6 +106,7 @@ var contentCommand = cli.Command{ ingestCommand, activeCommand, getCommand, + editCommand, deleteCommand, }, } diff --git a/services/content/ingester.go b/services/content/ingester.go index 191626bdf..a4182cc4f 100644 --- a/services/content/ingester.go +++ b/services/content/ingester.go @@ -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 } diff --git a/services/content/service.go b/services/content/service.go index 128104fa0..c138184ff 100644 --- a/services/content/service.go +++ b/services/content/service.go @@ -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) }