Skip to content

Commit

Permalink
img inspect command: dump the JSON for the image config to stdout (#…
Browse files Browse the repository at this point in the history
…324)

* inspect CLI: dump the JSON for the image config to stdout

This CLI mimics `docker inspect`: it will print the OCI image config to
stdout as raw JSON. This can be used with `jq` or other tools to extract
useful metadata about an image.

Similar to `img pull`, `img inspect` only takes _one_ image (unlike
`docker inspect` which can take multiple and outputs an array). I felt
it was more important to act like img than it was to act like Docker.

* inspect: update help output to note incompatibility with docker inspect
  • Loading branch information
mitchellh committed Jan 14, 2021
1 parent 54d0ca9 commit 91d49f9
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 0 deletions.
72 changes: 72 additions & 0 deletions client/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package client

import (
"context"
"encoding/json"
"errors"
"fmt"

"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/docker/distribution/reference"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

// InspectImage returns the metadata about a given image.
func (c *Client) InspectImage(ctx context.Context, name string) (*ocispec.Image, error) {
// Parse the image name and tag for the src image.
named, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, fmt.Errorf("parsing image name %q failed: %v", name, err)
}

// Add the latest lag if they did not provide one.
named = reference.TagNameOnly(named)
name = named.String()

// Create the worker opts.
opt, err := c.createWorkerOpt(false)
if err != nil {
return nil, fmt.Errorf("creating worker opt failed: %v", err)
}

if opt.ImageStore == nil {
return nil, errors.New("image store is nil")
}

// Get the source image.
image, err := opt.ImageStore.Get(ctx, name)
if err != nil {
return nil, fmt.Errorf("getting image %s from image store failed: %v", name, err)
}

var result ocispec.Image
if err := images.Walk(ctx, images.Handlers(
images.ChildrenHandler(opt.ContentStore),
inspectHandler(opt.ContentStore, &result),
), image.Target); err != nil {
return nil, fmt.Errorf("error reading image %s: %v", name, err)
}

return &result, nil
}

func inspectHandler(provider content.Provider, result *ocispec.Image) images.Handler {
return images.HandlerFunc(func(
ctx context.Context,
desc ocispec.Descriptor,
) ([]ocispec.Descriptor, error) {
// We only want the image config
if desc.MediaType != images.MediaTypeDockerSchema2Config &&
desc.MediaType != ocispec.MediaTypeImageConfig {
return nil, nil
}

p, err := content.ReadBlob(ctx, provider, desc)
if err != nil {
return nil, err
}

return nil, json.Unmarshal(p, result)
})
}
79 changes: 79 additions & 0 deletions inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package main

import (
"context"
"encoding/json"
"fmt"
"github.com/spf13/cobra"

"github.com/containerd/containerd/namespaces"
"github.com/genuinetools/img/client"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/session"
)

const inspectUsageShortHelp = `Return the JSON-encoded OCI image config. The output format is not compatible with "docker inspect".`
const inspectUsageLongHelp = `Return the JSON-encoded OCI image config. The output format is not compatible with "docker inspect".`

func newInspectCommand() *cobra.Command {
inspect := &inspectCommand{}

cmd := &cobra.Command{
Use: "inspect NAME[:TAG]",
DisableFlagsInUseLine: true,
SilenceUsage: true,
Short: inspectUsageShortHelp,
Long: inspectUsageLongHelp,
Args: inspect.ValidateArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return inspect.Run(args)
},
}

return cmd
}

type inspectCommand struct {
image string
}

func (cmd *inspectCommand) ValidateArgs(c *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("must pass an image to inspect")
}

return nil
}

func (cmd *inspectCommand) Run(args []string) (err error) {
reexec()

// Get the specified image and target.
cmd.image = args[0]

// Create the context.
id := identity.NewID()
ctx := session.NewContext(context.Background(), id)
ctx = namespaces.WithNamespace(ctx, "buildkit")

// Create the client.
c, err := client.New(stateDir, backend, nil)
if err != nil {
return err
}
defer c.Close()

image, err := c.InspectImage(ctx, cmd.image)
if err != nil {
return err
}

fmted, err := json.MarshalIndent(image, "", "\t")
if err != nil {
return err
}

fmt.Println(string(fmted))

return nil
}
27 changes: 27 additions & 0 deletions inspect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"encoding/json"
"reflect"
"testing"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

func TestInspectImage(t *testing.T) {
runBuild(t, "inspectthing", withDockerfile(`
FROM busybox
ENTRYPOINT ["echo"]
`))

out := run(t, "inspect", "inspectthing")

var image ocispec.Image
if err := json.Unmarshal([]byte(out), &image); err != nil {
t.Fatalf("error decoding JSON: %s", err)
}

if !reflect.DeepEqual(image.Config.Entrypoint, []string{"echo"}) {
t.Fatalf("expected entrypoint to be set: %#v", image)
}
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func main() {
cmd.AddCommand(
newBuildCommand(),
newDiskUsageCommand(),
newInspectCommand(),
newListCommand(),
newLoginCommand(),
newLogoutCommand(),
Expand Down

0 comments on commit 91d49f9

Please sign in to comment.