
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>
169 lines
3.5 KiB
Go
169 lines
3.5 KiB
Go
package content
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
|
|
contentapi "github.com/containerd/containerd/api/services/content"
|
|
"github.com/containerd/containerd/content"
|
|
|
|
digest "github.com/opencontainers/go-digest"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func NewIngesterFromClient(client contentapi.ContentClient) content.Ingester {
|
|
return &remoteIngester{
|
|
client: client,
|
|
}
|
|
}
|
|
|
|
type remoteIngester struct {
|
|
client contentapi.ContentClient
|
|
}
|
|
|
|
func (ri *remoteIngester) Writer(ctx context.Context, ref string, size int64, expected digest.Digest) (content.Writer, error) {
|
|
wrclient, offset, err := ri.negotiate(ctx, ref, size, expected)
|
|
if err != nil {
|
|
return nil, rewriteGRPCError(err)
|
|
}
|
|
|
|
return &remoteWriter{
|
|
client: wrclient,
|
|
offset: offset,
|
|
}, nil
|
|
}
|
|
|
|
func (ri *remoteIngester) negotiate(ctx context.Context, ref string, size int64, expected digest.Digest) (contentapi.Content_WriteClient, int64, error) {
|
|
wrclient, err := ri.client.Write(ctx)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if err := wrclient.Send(&contentapi.WriteRequest{
|
|
Action: contentapi.WriteActionStat,
|
|
Ref: ref,
|
|
Total: size,
|
|
Expected: expected,
|
|
}); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
resp, err := wrclient.Recv()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return wrclient, resp.Offset, nil
|
|
}
|
|
|
|
type remoteWriter struct {
|
|
ref string
|
|
client contentapi.Content_WriteClient
|
|
offset int64
|
|
digest digest.Digest
|
|
}
|
|
|
|
func newRemoteWriter(client contentapi.Content_WriteClient, ref string, offset int64) (*remoteWriter, error) {
|
|
return &remoteWriter{
|
|
ref: ref,
|
|
client: client,
|
|
offset: offset,
|
|
}, nil
|
|
}
|
|
|
|
// send performs a synchronous req-resp cycle on the client.
|
|
func (rw *remoteWriter) send(req *contentapi.WriteRequest) (*contentapi.WriteResponse, error) {
|
|
if err := rw.client.Send(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := rw.client.Recv()
|
|
|
|
if err == nil {
|
|
// try to keep these in sync
|
|
if resp.Digest != "" {
|
|
rw.digest = resp.Digest
|
|
}
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
func (rw *remoteWriter) Status() (content.Status, error) {
|
|
resp, err := rw.send(&contentapi.WriteRequest{
|
|
Action: contentapi.WriteActionStat,
|
|
})
|
|
if err != nil {
|
|
return content.Status{}, err
|
|
}
|
|
|
|
return content.Status{
|
|
Ref: rw.ref,
|
|
Offset: resp.Offset,
|
|
StartedAt: resp.StartedAt,
|
|
UpdatedAt: resp.UpdatedAt,
|
|
}, nil
|
|
}
|
|
|
|
func (rw *remoteWriter) Digest() digest.Digest {
|
|
return rw.digest
|
|
}
|
|
|
|
func (rw *remoteWriter) Write(p []byte) (n int, err error) {
|
|
offset := rw.offset
|
|
|
|
resp, err := rw.send(&contentapi.WriteRequest{
|
|
Action: contentapi.WriteActionWrite,
|
|
Offset: offset,
|
|
Data: p,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
n = int(resp.Offset - offset)
|
|
if n < len(p) {
|
|
err = io.ErrShortWrite
|
|
}
|
|
|
|
rw.offset += int64(n)
|
|
if resp.Digest != "" {
|
|
rw.digest = resp.Digest
|
|
}
|
|
return
|
|
}
|
|
|
|
func (rw *remoteWriter) Commit(size int64, expected digest.Digest) error {
|
|
resp, err := rw.send(&contentapi.WriteRequest{
|
|
Action: contentapi.WriteActionCommit,
|
|
Total: size,
|
|
Offset: rw.offset,
|
|
Expected: expected,
|
|
})
|
|
if err != nil {
|
|
return rewriteGRPCError(err)
|
|
}
|
|
|
|
if size != 0 && resp.Offset != size {
|
|
return errors.Errorf("unexpected size: %v != %v", resp.Offset, size)
|
|
}
|
|
|
|
if expected != "" && resp.Digest != expected {
|
|
return errors.Errorf("unexpected digest: %v != %v", resp.Digest, expected)
|
|
}
|
|
|
|
rw.digest = resp.Digest
|
|
rw.offset = resp.Offset
|
|
return nil
|
|
}
|
|
|
|
func (rw *remoteWriter) Truncate(size int64) error {
|
|
// This truncation won't actually be validated until a write is issued.
|
|
rw.offset = size
|
|
return nil
|
|
}
|
|
|
|
func (rw *remoteWriter) Close() error {
|
|
return rw.client.CloseSend()
|
|
}
|