DockerCLI/internal/pkg/containerized/snapshot.go

167 lines
4.4 KiB
Go

package containerized
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/containerd/containerd"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/diff/apply"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/rootfs"
"github.com/containerd/containerd/snapshots"
"github.com/opencontainers/image-spec/identity"
)
const (
gcRoot = "containerd.io/gc.root"
timestampFormat = "01-02-2006-15:04:05"
previousRevision = "docker.com/revision.previous"
imageLabel = "docker.com/revision.image"
)
// ErrNoPreviousRevision returned if the container has to previous revision
var ErrNoPreviousRevision = errors.New("no previous revision")
// WithNewSnapshot creates a new snapshot managed by containerized
func WithNewSnapshot(i containerd.Image) containerd.NewContainerOpts {
return func(ctx context.Context, client *containerd.Client, c *containers.Container) error {
if c.Snapshotter == "" {
c.Snapshotter = containerd.DefaultSnapshotter
}
r, err := create(ctx, client, i, c.ID, "")
if err != nil {
return err
}
c.SnapshotKey = r.Key
c.Image = i.Name()
return nil
}
}
// WithUpgrade upgrades an existing container's image to a new one
func WithUpgrade(i containerd.Image) containerd.UpdateContainerOpts {
return func(ctx context.Context, client *containerd.Client, c *containers.Container) error {
revision, err := save(ctx, client, i, c)
if err != nil {
return err
}
c.Image = i.Name()
err = updateConfig(c.ID, c.Image)
if err != nil {
return err
}
c.SnapshotKey = revision.Key
return nil
}
}
// WithRollback rolls back to the previous container's revision
func WithRollback(ctx context.Context, client *containerd.Client, c *containers.Container) error {
prev, err := previous(ctx, client, c)
if err != nil {
return err
}
ss := client.SnapshotService(c.Snapshotter)
sInfo, err := ss.Stat(ctx, prev.Key)
if err != nil {
return err
}
snapshotImage, ok := sInfo.Labels[imageLabel]
if !ok {
return fmt.Errorf("snapshot %s does not have a service image label", prev.Key)
}
if snapshotImage == "" {
return fmt.Errorf("snapshot %s has an empty service image label", prev.Key)
}
c.Image = snapshotImage
err = updateConfig(c.ID, c.Image)
if err != nil {
return err
}
c.SnapshotKey = prev.Key
return nil
}
func newRevision(id string) *revision {
now := time.Now()
return &revision{
Timestamp: now,
Key: fmt.Sprintf("boss.io.%s.%s", id, now.Format(timestampFormat)),
}
}
type revision struct {
Timestamp time.Time
Key string
mounts []mount.Mount
}
// nolint: interfacer
func create(ctx context.Context, client *containerd.Client, i containerd.Image, id string, previous string) (*revision, error) {
diffIDs, err := i.RootFS(ctx)
if err != nil {
return nil, err
}
var (
parent = identity.ChainID(diffIDs).String()
r = newRevision(id)
)
labels := map[string]string{
gcRoot: r.Timestamp.Format(time.RFC3339),
imageLabel: i.Name(),
}
if previous != "" {
labels[previousRevision] = previous
}
mounts, err := client.SnapshotService(containerd.DefaultSnapshotter).Prepare(ctx, r.Key, parent, snapshots.WithLabels(labels))
if err != nil {
return nil, err
}
r.mounts = mounts
return r, nil
}
func save(ctx context.Context, client *containerd.Client, updatedImage containerd.Image, c *containers.Container) (*revision, error) {
snapshot, err := create(ctx, client, updatedImage, c.ID, c.SnapshotKey)
if err != nil {
return nil, err
}
service := client.SnapshotService(c.Snapshotter)
// create a diff from the existing snapshot
diff, err := rootfs.CreateDiff(ctx, c.SnapshotKey, service, client.DiffService())
if err != nil {
return nil, err
}
applier := apply.NewFileSystemApplier(client.ContentStore())
if _, err := applier.Apply(ctx, diff, snapshot.mounts); err != nil {
return nil, err
}
return snapshot, nil
}
// nolint: interfacer
func previous(ctx context.Context, client *containerd.Client, c *containers.Container) (*revision, error) {
service := client.SnapshotService(c.Snapshotter)
sInfo, err := service.Stat(ctx, c.SnapshotKey)
if err != nil {
return nil, err
}
key := sInfo.Labels[previousRevision]
if key == "" {
return nil, ErrNoPreviousRevision
}
parts := strings.Split(key, ".")
timestamp, err := time.Parse(timestampFormat, parts[len(parts)-1])
if err != nil {
return nil, err
}
return &revision{
Timestamp: timestamp,
Key: key,
}, nil
}