diff --git a/cmd/kpod/diff.go b/cmd/kpod/diff.go new file mode 100644 index 00000000..82f260e0 --- /dev/null +++ b/cmd/kpod/diff.go @@ -0,0 +1,89 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/containers/storage/pkg/archive" + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +type diffJSONOutput struct { + Changed []string `json:"changed,omitempty"` + Added []string `json:"added,omitempty"` + Deleted []string `json:"deleted,omitempty"` +} + +var ( + diffFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "archive", + Usage: "Save the diff as a tar archive", + Hidden: true, + }, + cli.BoolFlag{ + Name: "json", + Usage: "Format output as JSON", + }, + } + diffDescription = fmt.Sprint(`Displays changes on a container or image's filesystem. The + container or image will be compared to its parent layer`) + + diffCommand = cli.Command{ + Name: "diff", + Usage: "Inspect changes on container's file systems", + Description: diffDescription, + Flags: diffFlags, + Action: diffCmd, + ArgsUsage: "ID-NAME", + } +) + +func diffCmd(c *cli.Context) error { + if len(c.Args()) != 1 { + return errors.Errorf("container, layer, or image name must be specified: kpod diff [options [...]] ID-NAME") + } + config, err := getConfig(c) + if err != nil { + return errors.Wrapf(err, "could not get config") + } + + server, err := libkpod.New(config) + if err != nil { + return errors.Wrapf(err, "could not get container server") + } + + to := c.Args().Get(0) + changes, err := server.GetDiff("", to) + if err != nil { + return errors.Wrapf(err, "could not get changes for %q", to) + } + + if c.Bool("json") { + jsonStruct := diffJSONOutput{} + for _, change := range changes { + if change.Kind == archive.ChangeModify { + jsonStruct.Changed = append(jsonStruct.Changed, change.Path) + } else if change.Kind == archive.ChangeAdd { + jsonStruct.Added = append(jsonStruct.Added, change.Path) + } else if change.Kind == archive.ChangeDelete { + jsonStruct.Deleted = append(jsonStruct.Deleted, change.Path) + } else { + return errors.Errorf("change kind %q not recognized", change.Kind.String()) + } + } + b, err := json.MarshalIndent(jsonStruct, "", " ") + if err != nil { + return errors.Wrapf(err, "could not marshal json for %+v", jsonStruct) + } + fmt.Println(string(b)) + } else { + for _, change := range changes { + fmt.Println(change.String()) + } + } + + return nil +} diff --git a/cmd/kpod/main.go b/cmd/kpod/main.go index 7e91bd3e..8fe1a2ff 100644 --- a/cmd/kpod/main.go +++ b/cmd/kpod/main.go @@ -22,6 +22,7 @@ func main() { app.Version = Version app.Commands = []cli.Command{ + diffCommand, exportCommand, historyCommand, imagesCommand, diff --git a/docs/kpod-diff.1.md b/docs/kpod-diff.1.md new file mode 100644 index 00000000..1978687e --- /dev/null +++ b/docs/kpod-diff.1.md @@ -0,0 +1,45 @@ +% kpod(1) kpod-diff - Inspect changes on a container or image's filesystem +% Dan Walsh +# kpod-diff "1" "August 2017" "kpod" + +## NAME +kpod diff - Inspect changes on a container or image's filesystem + +## SYNOPSIS +**kpod** **diff** [*options* [...]] NAME + +## DESCRIPTION +Displays changes on a container or image's filesystem. The container or image will be compared to its parent layer + +## OPTIONS + +**--json** + +Format output as json + + +## EXAMPLE + +kpod diff redis:alpine +C /usr +C /usr/local +C /usr/local/bin +A /usr/local/bin/docker-entrypoint.sh + +kpod diff --json redis:alpine +{ + "changed": [ + "/usr", + "/usr/local", + "/usr/local/bin" + ], + "added": [ + "/usr/local/bin/docker-entrypoint.sh" + ] +} + +## SEE ALSO +kpod(1) + +## HISTORY +August 2017, Originally compiled by Ryan Cole diff --git a/libkpod/diff.go b/libkpod/diff.go new file mode 100644 index 00000000..cbc2f6f6 --- /dev/null +++ b/libkpod/diff.go @@ -0,0 +1,54 @@ +package libkpod + +import ( + "github.com/containers/storage/pkg/archive" + "github.com/kubernetes-incubator/cri-o/libkpod/image" + "github.com/kubernetes-incubator/cri-o/libkpod/layer" + "github.com/pkg/errors" +) + +// GetDiff returns the differences betwen the two images, layers, or containers +func (c *ContainerServer) GetDiff(from, to string) ([]archive.Change, error) { + toLayer, err := c.getLayerID(to) + if err != nil { + return nil, err + } + fromLayer := "" + if from != "" { + fromLayer, err = c.getLayerID(from) + if err != nil { + return nil, err + } + } + return c.Store().Changes(fromLayer, toLayer) +} + +// GetLayerID gets a full layer id given a full or partial id +// If the id matches a container or image, the id of the top layer is returned +// If the id matches a layer, the top layer id is returned +func (c *ContainerServer) getLayerID(id string) (string, error) { + var toLayer string + toImage, err := image.FindImage(c.store, id) + if err != nil { + toCtr, err := c.store.Container(id) + if err != nil { + toLayer, err = layer.FullID(c.store, id) + if err != nil { + return "", errors.Errorf("layer, image, or container %s does not exist", id) + } + } else { + toLayer = toCtr.LayerID + } + } else { + toLayer = toImage.TopLayer + } + return toLayer, nil +} + +func (c *ContainerServer) getLayerParent(layerID string) (string, error) { + layer, err := c.store.Layer(layerID) + if err != nil { + return "", err + } + return layer.Parent, nil +} diff --git a/libkpod/layer/layer.go b/libkpod/layer/layer.go new file mode 100644 index 00000000..a7996350 --- /dev/null +++ b/libkpod/layer/layer.go @@ -0,0 +1,12 @@ +package layer + +import cstorage "github.com/containers/storage" + +// FullID gets the full id of a layer given a partial id or name +func FullID(store cstorage.Store, id string) (string, error) { + layer, err := store.Layer(id) + if err != nil { + return "", err + } + return layer.ID, nil +} diff --git a/test/kpod_diff.bats b/test/kpod_diff.bats new file mode 100644 index 00000000..4af6a429 --- /dev/null +++ b/test/kpod_diff.bats @@ -0,0 +1,37 @@ +#/usr/bin/env bats + +load helpers + +IMAGE="alpine:latest" +ROOT="$TESTDIR/crio" +RUNROOT="$TESTDIR/crio-run" +KPOD_OPTIONS="--root $ROOT --runroot $RUNROOT --storage-driver vfs" + +function teardown() { + cleanup_test +} + +@test "test diff of image and parent" { + run ${KPOD_BINARY} $KPOD_OPTIONS pull $IMAGE + [ "$status" -eq 0 ] + run ${KPOD_BINARY} $KPOD_OPTIONS diff $IMAGE + [ "$status" -eq 0 ] + echo "$output" + run ${KKPOD_BINARY} $KPOD_OPTIONS rmi $IMAGE +} + +@test "test diff on non-existent layer" { + run ${KPOD_BINARY} $KPOD_OPTIONS diff "abc123" + [ "$status" -ne 0 ] + echo "$output" +} + +@test "test diff with json output" { + run ${KPOD_BINARY} $KPOD_OPTIONS pull $IMAGE + [ "$status" -eq 0 ] + # run bash -c "${KPOD_BINARY} ${KPOD_OPTIONS} diff --json $IMAGE | python -m json.tool" + run ${KPOD_BINARY} $KPOD_OPTIONS diff --json $IMAGE + [ "$status" -eq 0 ] + echo "$output" + run ${KKPOD_BINARY} $KPOD_OPTIONS rmi $IMAGE +}