diff --git a/README.md b/README.md index 2e5661d3..25fae28f 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ It is currently in active development in the Kubernetes community through the [d | [kpod(1)](/docs/kpod.1.md) | Simple management tool for pods and images | | [kpod-history(1)](/docs/kpod-history.1.md)] | Shows the history of an image | | [kpod-images(1)](/docs/kpod-images.1.md) | List images in local storage | +| [kpod-inspect(1)](/docs/kpod-inspect.1.md) | Display the configuration of a container or image | | [kpod-pull(1)](/docs/kpod-pull.1.md) | Pull an image from a registry | | [kpod-push(1)](/docs/kpod-push.1.md) | Push an image to a specified destination | | [kpod-rmi(1)](/docs/kpod-rmi.1.md) | Removes one or more images | diff --git a/cmd/kpod/common.go b/cmd/kpod/common.go index 98c9e60d..75eebc18 100644 --- a/cmd/kpod/common.go +++ b/cmd/kpod/common.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "fmt" "io" "strings" "time" @@ -78,6 +77,35 @@ func getStore(c *cli.Context) (storage.Store, error) { return store, nil } +func parseMetadata(image storage.Image) (imageMetadata, error) { + var im imageMetadata + + dec := json.NewDecoder(strings.NewReader(image.Metadata)) + if err := dec.Decode(&im); err != nil { + return imageMetadata{}, err + } + return im, nil +} + +func findImage(store storage.Store, image string) (*storage.Image, error) { + var img *storage.Image + ref, err := is.Transport.ParseStoreReference(store, image) + if err == nil { + img, err = is.Transport.GetStoreImage(store, ref) + } + if err != nil { + img2, err2 := store.Image(image) + if err2 != nil { + if ref == nil { + return nil, errors.Wrapf(err, "error parsing reference to image %q", image) + } + return nil, errors.Wrapf(err, "unable to locate image %q", image) + } + img = img2 + } + return img, nil +} + func getCopyOptions(reportWriter io.Writer, signaturePolicyPath string, srcDockerRegistry, destDockerRegistry *dockerRegistryOptions, signing signingOptions) *cp.Options { if srcDockerRegistry == nil { srcDockerRegistry = &dockerRegistryOptions{} @@ -96,33 +124,20 @@ func getCopyOptions(reportWriter io.Writer, signaturePolicyPath string, srcDocke } } -func getPolicyContext(path string) (*signature.PolicyContext, error) { - policy, err := signature.DefaultPolicy(&types.SystemContext{SignaturePolicyPath: path}) +func findContainer(store storage.Store, container string) (*storage.Container, error) { + ctrStore, err := store.ContainerStore() if err != nil { return nil, err } - return signature.NewPolicyContext(policy) + return ctrStore.Get(container) } -func findImage(store storage.Store, image string) (*storage.Image, error) { - var img *storage.Image - ref, err := is.Transport.ParseStoreReference(store, image) - if err == nil { - img, err := is.Transport.GetStoreImage(store, ref) - if err != nil { - return nil, err - } - return img, nil +func getContainerTopLayerID(store storage.Store, containerID string) (string, error) { + ctr, err := findContainer(store, containerID) + if err != nil { + return "", err } - img2, err2 := store.Image(image) - if err2 != nil { - if ref == nil { - return nil, errors.Wrapf(err, "error parsing reference to image %q", image) - } - return nil, errors.Wrapf(err, "unable to locate image %q", image) - } - img = img2 - return img, nil + return ctr.LayerID, nil } func getSystemContext(signaturePolicyPath string) *types.SystemContext { @@ -133,37 +148,6 @@ func getSystemContext(signaturePolicyPath string) *types.SystemContext { return sc } -func parseMetadata(image storage.Image) (imageMetadata, error) { - var im imageMetadata - - dec := json.NewDecoder(strings.NewReader(image.Metadata)) - if err := dec.Decode(&im); err != nil { - return imageMetadata{}, err - } - return im, nil -} - -func getSize(image storage.Image, store storage.Store) (int64, error) { - - is.Transport.SetStore(store) - storeRef, err := is.Transport.ParseStoreReference(store, "@"+image.ID) - if err != nil { - fmt.Println(err) - return -1, err - } - img, err := storeRef.NewImage(nil) - if err != nil { - fmt.Println("Error with NewImage") - return -1, err - } - imgSize, err := img.Size() - if err != nil { - fmt.Println("Error getting size") - return -1, err - } - return imgSize, nil -} - func copyStringStringMap(m map[string]string) map[string]string { n := map[string]string{} for k, v := range m { @@ -172,16 +156,140 @@ func copyStringStringMap(m map[string]string) map[string]string { return n } -func (o dockerRegistryOptions) getSystemContext(signaturePolicyPath string) *types.SystemContext { - sc := &types.SystemContext{ - SignaturePolicyPath: signaturePolicyPath, - DockerAuthConfig: o.DockerRegistryCreds, - DockerCertPath: o.DockerCertPath, - DockerInsecureSkipTLSVerify: o.DockerInsecureSkipTLSVerify, +// A container FS is split into two parts. The first is the top layer, a +// mutable layer, and the rest is the RootFS: the set of immutable layers +// that make up the image on which the container is based +func getRootFsSize(store storage.Store, containerID string) (int64, error) { + ctrStore, err := store.ContainerStore() + if err != nil { + return 0, err } - return sc + container, err := ctrStore.Get(containerID) + if err != nil { + return 0, err + } + lstore, err := store.LayerStore() + if err != nil { + return 0, err + } + + // Ignore the size of the top layer. The top layer is a mutable RW layer + // and is not considered a part of the rootfs + rwLayer, err := lstore.Get(container.LayerID) + if err != nil { + return 0, err + } + layer, err := lstore.Get(rwLayer.Parent) + if err != nil { + return 0, err + } + + size := int64(0) + for layer.Parent != "" { + layerSize, err := lstore.DiffSize(layer.Parent, layer.ID) + if err != nil { + return size, errors.Wrapf(err, "getting diffsize of layer %q and its parent %q", layer.ID, layer.Parent) + } + size += layerSize + layer, err = lstore.Get(layer.Parent) + if err != nil { + return 0, err + } + } + // Get the size of the last layer. Has to be outside of the loop + // because the parent of the last layer is "", andlstore.Get("") + // will return an error + layerSize, err := lstore.DiffSize(layer.Parent, layer.ID) + return size + layerSize, err } +func getContainerRwSize(store storage.Store, containerID string) (int64, error) { + ctrStore, err := store.ContainerStore() + if err != nil { + return 0, err + } + container, err := ctrStore.Get(containerID) + if err != nil { + return 0, err + } + lstore, err := store.LayerStore() + if err != nil { + return 0, err + } + + // Get the size of the top layer by calculating the size of the diff + // between the layer and its parent. The top layer of a container is + // the only RW layer, all others are immutable + layer, err := lstore.Get(container.LayerID) + if err != nil { + return 0, err + } + return lstore.DiffSize(layer.Parent, layer.ID) +} + +func isTrue(str string) bool { + return str == "true" +} + +func isFalse(str string) bool { + return str == "false" +} + +func isValidBool(str string) bool { + return isTrue(str) || isFalse(str) +} + +func getDriverName(store storage.Store) (string, error) { + driver, err := store.GraphDriver() + if err != nil { + return "", err + } + return driver.String(), nil +} + +func getDriverMetadata(store storage.Store, layerID string) (map[string]string, error) { + driver, err := store.GraphDriver() + if err != nil { + return nil, err + } + return driver.Metadata(layerID) +} + +func getImageSize(image storage.Image, store storage.Store) (int64, error) { + is.Transport.SetStore(store) + storeRef, err := is.Transport.ParseStoreReference(store, "@"+image.ID) + if err != nil { + return -1, err + } + img, err := storeRef.NewImage(nil) + if err != nil { + return -1, err + } + imgSize, err := img.Size() + if err != nil { + return -1, err + } + return imgSize, nil +} + +func getImageTopLayer(image storage.Image) (string, error) { + metadata, err := parseMetadata(image) + if err != nil { + return "", err + } + // Get the digest of the first blob + digest := string(metadata.Blobs[0].Digest) + // Return the first layer associated with the given digest + return metadata.Layers[digest][0], nil +} + +func getPolicyContext(path string) (*signature.PolicyContext, error) { + policy, err := signature.DefaultPolicy(&types.SystemContext{SignaturePolicyPath: path}) + if err != nil { + return nil, err + } + return signature.NewPolicyContext(policy) +} func parseRegistryCreds(creds string) (*types.DockerAuthConfig, error) { if creds == "" { return nil, errors.New("no credentials supplied") @@ -196,3 +304,13 @@ func parseRegistryCreds(creds string) (*types.DockerAuthConfig, error) { } return cfg, nil } + +func (o dockerRegistryOptions) getSystemContext(signaturePolicyPath string) *types.SystemContext { + sc := &types.SystemContext{ + SignaturePolicyPath: signaturePolicyPath, + DockerAuthConfig: o.DockerRegistryCreds, + DockerCertPath: o.DockerCertPath, + DockerInsecureSkipTLSVerify: o.DockerInsecureSkipTLSVerify, + } + return sc +} diff --git a/cmd/kpod/common_test.go b/cmd/kpod/common_test.go index dd1ab62c..3ec33658 100644 --- a/cmd/kpod/common_test.go +++ b/cmd/kpod/common_test.go @@ -70,7 +70,7 @@ func TestGetSize(t *testing.T) { t.Fatalf("Error reading images: %v", err) } - _, err = getSize(images[0], store) + _, err = getImageSize(images[0], store) if err != nil { t.Error(err) } diff --git a/cmd/kpod/containerData.go b/cmd/kpod/containerData.go new file mode 100644 index 00000000..2ecc00e9 --- /dev/null +++ b/cmd/kpod/containerData.go @@ -0,0 +1,219 @@ +package main + +import ( + "encoding/json" + "time" + + "k8s.io/apimachinery/pkg/fields" + pb "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" + + "github.com/containers/storage" + "github.com/kubernetes-incubator/cri-o/cmd/kpod/docker" + "github.com/kubernetes-incubator/cri-o/oci" + "github.com/kubernetes-incubator/cri-o/pkg/annotations" + "github.com/kubernetes-incubator/cri-o/server" + "github.com/opencontainers/image-spec/specs-go/v1" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +type containerData struct { + ID string + Name string + LogPath string + Labels fields.Set + Annotations fields.Set + State *oci.ContainerState + Metadata *pb.ContainerMetadata + BundlePath string + StopSignal string + Type string `json:"type"` + FromImage string `json:"image,omitempty"` + FromImageID string `json:"image-id"` + MountPoint string `json:"mountpoint,omitempty"` + MountLabel string + Mounts []specs.Mount + AppArmorProfile string + ImageAnnotations map[string]string `json:"annotations,omitempty"` + ImageCreatedBy string `json:"created-by,omitempty"` + OCIv1 v1.Image `json:"ociv1,omitempty"` + Docker docker.V2Image `json:"docker,omitempty"` + SizeRw uint `json:"SizeRw,omitempty"` + SizeRootFs uint `json:"SizeRootFs,omitempty"` + Args []string + ResolvConfPath string + HostnamePath string + HostsPath string + GraphDriver driverData +} + +type driverData struct { + Name string + Data map[string]string +} + +func getContainerData(store storage.Store, name string, size bool) (*containerData, error) { + ctr, err := inspectContainer(store, name) + if err != nil { + return nil, errors.Wrapf(err, "error reading build container %q", name) + } + cid, err := openContainer(store, name) + if err != nil { + return nil, errors.Wrapf(err, "error reading container image data") + } + config, err := store.FromContainerDirectory(ctr.ID(), "config.json") + if err != nil { + return nil, err + } + + var m specs.Spec + if err = json.Unmarshal(config, &m); err != nil { + return nil, err + } + + driverName, err := getDriverName(store) + if err != nil { + return nil, err + } + topLayer, err := getContainerTopLayerID(store, ctr.ID()) + if err != nil { + return nil, err + } + driverMetadata, err := getDriverMetadata(store, topLayer) + if err != nil { + return nil, err + } + data := &containerData{ + ID: ctr.ID(), + Name: ctr.Name(), + LogPath: ctr.LogPath(), + Labels: ctr.Labels(), + Annotations: ctr.Annotations(), + State: ctr.State(), + Metadata: ctr.Metadata(), + BundlePath: ctr.BundlePath(), + StopSignal: ctr.GetStopSignal(), + Args: m.Process.Args, + Type: cid.Type, + FromImage: cid.FromImage, + FromImageID: cid.FromImageID, + MountPoint: cid.MountPoint, + ImageAnnotations: cid.ImageAnnotations, + ImageCreatedBy: cid.ImageCreatedBy, + OCIv1: cid.OCIv1, + Docker: cid.Docker, + GraphDriver: driverData{ + Name: driverName, + Data: driverMetadata, + }, + MountLabel: m.Linux.MountLabel, + Mounts: m.Mounts, + AppArmorProfile: m.Process.ApparmorProfile, + ResolvConfPath: "", + HostnamePath: "", + HostsPath: "", + } + + if size { + sizeRootFs, err := getRootFsSize(store, data.ID) + if err != nil { + + return nil, errors.Wrapf(err, "error reading size for container %q", name) + } + data.SizeRootFs = uint(sizeRootFs) + sizeRw, err := getContainerRwSize(store, data.ID) + if err != nil { + return nil, errors.Wrapf(err, "error reading RWSize for container %q", name) + } + data.SizeRw = uint(sizeRw) + } + + return data, nil +} + +// Get an oci.Container and update its status +func inspectContainer(store storage.Store, container string) (*oci.Container, error) { + ociCtr, err := getOCIContainer(store, container) + if err != nil { + return nil, err + } + // call oci.New() to get the runtime + runtime, err := getOCIRuntime(store, container) + if err != nil { + return nil, err + } + // call runtime.UpdateStatus() + err = runtime.UpdateStatus(ociCtr) + if err != nil { + return nil, err + } + return ociCtr, nil +} + +// get an oci.Container instance for a given container ID +func getOCIContainer(store storage.Store, container string) (*oci.Container, error) { + ctr, err := findContainer(store, container) + if err != nil { + return nil, err + } + config, err := store.FromContainerDirectory(ctr.ID, "config.json") + if err != nil { + return nil, err + } + + var m specs.Spec + if err = json.Unmarshal(config, &m); err != nil { + return nil, err + } + + labels := make(map[string]string) + err = json.Unmarshal([]byte(m.Annotations[annotations.Labels]), &labels) + if len(m.Annotations[annotations.Labels]) > 0 && err != nil { + return nil, err + } + name := ctr.Names[0] + + var metadata pb.ContainerMetadata + err = json.Unmarshal([]byte(m.Annotations[annotations.Metadata]), &metadata) + if len(m.Annotations[annotations.Metadata]) > 0 && err != nil { + return nil, err + } + + tty := isTrue(m.Annotations[annotations.TTY]) + stdin := isTrue(m.Annotations[annotations.Stdin]) + stdinOnce := isTrue(m.Annotations[annotations.StdinOnce]) + + containerPath, err := store.ContainerRunDirectory(ctr.ID) + if err != nil { + return nil, err + } + + containerDir, err := store.ContainerDirectory(ctr.ID) + if err != nil { + return nil, err + } + + img, _ := m.Annotations[annotations.Image] + + kubeAnnotations := make(map[string]string) + err = json.Unmarshal([]byte(m.Annotations[annotations.Annotations]), &kubeAnnotations) + if len(m.Annotations[annotations.Annotations]) > 0 && err != nil { + return nil, err + } + + created := time.Time{} + if len(m.Annotations[annotations.Created]) > 0 { + created, err = time.Parse(time.RFC3339Nano, m.Annotations[annotations.Created]) + if err != nil { + return nil, err + } + } + + // create a new OCI Container. kpod currently doesn't deal with pod sandboxes, so the fields for netns, privileged, and trusted are left empty + return oci.NewContainer(ctr.ID, name, containerPath, m.Annotations[annotations.LogPath], nil, labels, kubeAnnotations, img, &metadata, ctr.ImageID, tty, stdin, stdinOnce, false, false, containerDir, created, m.Annotations["org.opencontainers.image.stopSignal"]) +} + +func getOCIRuntime(store storage.Store, container string) (*oci.Runtime, error) { + config := server.DefaultConfig() + return oci.New(config.Runtime, config.RuntimeUntrustedWorkload, config.DefaultWorkloadTrust, config.Conmon, config.ConmonEnv, config.CgroupManager) +} diff --git a/cmd/kpod/imagePushData.go b/cmd/kpod/containerImageData.go similarity index 57% rename from cmd/kpod/imagePushData.go rename to cmd/kpod/containerImageData.go index 1134f366..768044a0 100644 --- a/cmd/kpod/imagePushData.go +++ b/cmd/kpod/containerImageData.go @@ -3,6 +3,8 @@ package main import ( "encoding/json" "fmt" + "io/ioutil" + "os" "path/filepath" "runtime" "time" @@ -12,7 +14,8 @@ import ( "github.com/containers/image/types" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" - "github.com/kubernetes-incubator/cri-o/cmd/kpod/docker" // Get rid of this eventually + "github.com/docker/docker/pkg/ioutils" + "github.com/kubernetes-incubator/cri-o/cmd/kpod/docker" digest "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go/v1" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -20,142 +23,159 @@ import ( ) const ( + // Package is used to identify working containers + Package = "kpod" + containerType = Package + " 0.0.1" + stateFile = Package + ".json" // OCIv1ImageManifest is the MIME type of an OCIv1 image manifest, // suitable for specifying as a value of the PreferredManifestType // member of a CommitOptions structure. It is also the default. OCIv1ImageManifest = v1.MediaTypeImageManifest ) -type imagePushData struct { +type containerImageData struct { store storage.Store - // Type is used to help a build container's metadata + + // Type is used to help identify a build container's metadata. It + // should not be modified. Type string `json:"type"` - // FromImage is the name of the source image which ws used to create - // the container, if one was used + // FromImage is the name of the source image which was used to create + // the container, if one was used. It should not be modified. FromImage string `json:"image,omitempty"` - // FromImageID is the id of the source image - FromImageID string `json:"imageid"` - // Config is the source image's configuration + // FromImageID is the ID of the source image which was used to create + // the container, if one was used. It should not be modified. + FromImageID string `json:"image-id"` + // Config is the source image's configuration. It should not be + // modified. Config []byte `json:"config,omitempty"` - // Manifest is the source image's manifest + // Manifest is the source image's manifest. It should not be modified. Manifest []byte `json:"manifest,omitempty"` + + // Container is the name of the build container. It should not be modified. + Container string `json:"container-name,omitempty"` + // ContainerID is the ID of the build container. It should not be modified. + ContainerID string `json:"container-id,omitempty"` + // MountPoint is the last location where the container's root + // filesystem was mounted. It should not be modified. + MountPoint string `json:"mountpoint,omitempty"` + // ImageAnnotations is a set of key-value pairs which is stored in the - // image's manifest + // image's manifest. ImageAnnotations map[string]string `json:"annotations,omitempty"` - // ImageCreatedBy is a description of how this container was built + // ImageCreatedBy is a description of how this container was built. ImageCreatedBy string `json:"created-by,omitempty"` - // Image metadata and runtime settings, in multiple formats - OCIv1 ociv1.Image `json:"ociv1,omitempty"` + // Image metadata and runtime settings, in multiple formats. + OCIv1 v1.Image `json:"ociv1,omitempty"` Docker docker.V2Image `json:"docker,omitempty"` } -func (i *imagePushData) initConfig() { +func (c *containerImageData) initConfig() { image := ociv1.Image{} dimage := docker.V2Image{} - if len(i.Config) > 0 { + if len(c.Config) > 0 { // Try to parse the image config. If we fail, try to start over from scratch - if err := json.Unmarshal(i.Config, &dimage); err == nil && dimage.DockerVersion != "" { + if err := json.Unmarshal(c.Config, &dimage); err == nil && dimage.DockerVersion != "" { image, err = makeOCIv1Image(&dimage) if err != nil { image = ociv1.Image{} } } else { - if err := json.Unmarshal(i.Config, &image); err != nil { + if err := json.Unmarshal(c.Config, &image); err != nil { if dimage, err = makeDockerV2S2Image(&image); err != nil { dimage = docker.V2Image{} } } } - i.OCIv1 = image - i.Docker = dimage + c.OCIv1 = image + c.Docker = dimage } else { // Try to dig out the image configuration from the manifest manifest := docker.V2S1Manifest{} - if err := json.Unmarshal(i.Manifest, &manifest); err == nil && manifest.SchemaVersion == 1 { + if err := json.Unmarshal(c.Manifest, &manifest); err == nil && manifest.SchemaVersion == 1 { if dimage, err = makeDockerV2S1Image(manifest); err == nil { if image, err = makeOCIv1Image(&dimage); err != nil { image = ociv1.Image{} } } } - i.OCIv1 = image - i.Docker = dimage + c.OCIv1 = image + c.Docker = dimage } - if len(i.Manifest) > 0 { + if len(c.Manifest) > 0 { // Attempt to recover format-specific data from the manifest v1Manifest := ociv1.Manifest{} - if json.Unmarshal(i.Manifest, &v1Manifest) == nil { - i.ImageAnnotations = v1Manifest.Annotations + if json.Unmarshal(c.Manifest, &v1Manifest) == nil { + c.ImageAnnotations = v1Manifest.Annotations } } - i.fixupConfig() + c.fixupConfig() } -func (i *imagePushData) fixupConfig() { - if i.Docker.Config != nil { +func (c *containerImageData) fixupConfig() { + if c.Docker.Config != nil { // Prefer image-level settings over those from the container it was built from - i.Docker.ContainerConfig = *i.Docker.Config + c.Docker.ContainerConfig = *c.Docker.Config } - i.Docker.Config = &i.Docker.ContainerConfig - i.Docker.DockerVersion = "" + c.Docker.Config = &c.Docker.ContainerConfig + c.Docker.DockerVersion = "" now := time.Now().UTC() - if i.Docker.Created.IsZero() { - i.Docker.Created = now + if c.Docker.Created.IsZero() { + c.Docker.Created = now } - if i.OCIv1.Created.IsZero() { - i.OCIv1.Created = &now + if c.OCIv1.Created.IsZero() { + c.OCIv1.Created = &now } - if i.OS() == "" { - i.SetOS(runtime.GOOS) + if c.OS() == "" { + c.SetOS(runtime.GOOS) } - if i.Architecture() == "" { - i.SetArchitecture(runtime.GOARCH) + if c.Architecture() == "" { + c.SetArchitecture(runtime.GOARCH) } - if i.WorkDir() == "" { - i.SetWorkDir(string(filepath.Separator)) + if c.WorkDir() == "" { + c.SetWorkDir(string(filepath.Separator)) } } // OS returns a name of the OS on which a container built using this image //is intended to be run. -func (i *imagePushData) OS() string { - return i.OCIv1.OS +func (c *containerImageData) OS() string { + return c.OCIv1.OS } // SetOS sets the name of the OS on which a container built using this image // is intended to be run. -func (i *imagePushData) SetOS(os string) { - i.OCIv1.OS = os - i.Docker.OS = os +func (c *containerImageData) SetOS(os string) { + c.OCIv1.OS = os + c.Docker.OS = os } // Architecture returns a name of the architecture on which a container built // using this image is intended to be run. -func (i *imagePushData) Architecture() string { - return i.OCIv1.Architecture +func (c *containerImageData) Architecture() string { + return c.OCIv1.Architecture } // SetArchitecture sets the name of the architecture on which ta container built // using this image is intended to be run. -func (i *imagePushData) SetArchitecture(arch string) { - i.OCIv1.Architecture = arch - i.Docker.Architecture = arch +func (c *containerImageData) SetArchitecture(arch string) { + c.OCIv1.Architecture = arch + c.Docker.Architecture = arch } // WorkDir returns the default working directory for running commands in a container // built using this image. -func (i *imagePushData) WorkDir() string { - return i.OCIv1.Config.WorkingDir +func (c *containerImageData) WorkDir() string { + return c.OCIv1.Config.WorkingDir } // SetWorkDir sets the location of the default working directory for running commands // in a container built using this image. -func (i *imagePushData) SetWorkDir(there string) { - i.OCIv1.Config.WorkingDir = there - i.Docker.Config.WorkingDir = there +func (c *containerImageData) SetWorkDir(there string) { + c.OCIv1.Config.WorkingDir = there + c.Docker.Config.WorkingDir = there } // makeOCIv1Image builds the best OCIv1 image structure we can from the @@ -318,11 +338,172 @@ func makeDockerV2S1Image(manifest docker.V2S1Manifest) (docker.V2Image, error) { return dimage, nil } -func (i *imagePushData) Annotations() map[string]string { - return copyStringStringMap(i.ImageAnnotations) +func (c *containerImageData) Annotations() map[string]string { + return copyStringStringMap(c.ImageAnnotations) } -func (i *imagePushData) makeImageRef(manifestType string, compress archive.Compression, names []string, layerID string, historyTimestamp *time.Time) (types.ImageReference, error) { +func (c *containerImageData) Save() error { + buildstate, err := json.Marshal(c) + if err != nil { + return err + } + cdir, err := c.store.ContainerDirectory(c.ContainerID) + if err != nil { + return err + } + return ioutils.AtomicWriteFile(filepath.Join(cdir, stateFile), buildstate, 0600) + +} + +func openContainer(store storage.Store, name string) (*containerImageData, error) { + var data *containerImageData + var err error + if name != "" { + data, err = openContainerImageData(store, name) + if os.IsNotExist(err) { + data, err = importContainerImageData(store, name, "") + } + } + if err != nil { + return nil, errors.Wrapf(err, "error reading build container") + } + if data == nil { + return nil, errors.Errorf("error finding build container") + } + return data, nil + +} + +func openImage(store storage.Store, image string) (*containerImageData, error) { + if image == "" { + return nil, errors.Errorf("image name must be specified") + } + img, err := findImage(store, image) + if err != nil { + return nil, errors.Wrapf(err, "error locating image %q for importing settings", image) + } + + systemContext := getSystemContext("") + data, err := importContainerImageDataFromImage(store, systemContext, img.ID, "", "") + if err != nil { + return nil, errors.Wrapf(err, "error reading image") + } + if data == nil { + return nil, errors.Errorf("error mocking up build configuration") + } + return data, nil + +} + +func importContainerImageData(store storage.Store, container, signaturePolicyPath string) (*containerImageData, error) { + if container == "" { + return nil, errors.Errorf("container name must be specified") + } + + c, err := store.Container(container) + if err != nil { + return nil, err + } + + systemContext := getSystemContext(signaturePolicyPath) + + data, err := importContainerImageDataFromImage(store, systemContext, c.ImageID, container, c.ID) + if err != nil { + return nil, err + } + + if data.FromImageID != "" { + if d, err2 := digest.Parse(data.FromImageID); err2 == nil { + data.Docker.Parent = docker.ID(d) + } else { + data.Docker.Parent = docker.ID(digest.NewDigestFromHex(digest.Canonical.String(), data.FromImageID)) + } + } + if data.FromImage != "" { + data.Docker.ContainerConfig.Image = data.FromImage + } + + err = data.Save() + if err != nil { + return nil, errors.Wrapf(err, "error saving containerImageData state") + } + + return data, nil +} + +func openContainerImageData(store storage.Store, container string) (*containerImageData, error) { + cdir, err := store.ContainerDirectory(container) + if err != nil { + return nil, err + } + buildstate, err := ioutil.ReadFile(filepath.Join(cdir, stateFile)) + if err != nil { + return nil, err + } + c := &containerImageData{} + err = json.Unmarshal(buildstate, &c) + if err != nil { + return nil, err + } + if c.Type != containerType { + return nil, errors.Errorf("container is not a %s container", Package) + } + c.store = store + c.fixupConfig() + return c, nil + +} + +func importContainerImageDataFromImage(store storage.Store, systemContext *types.SystemContext, imageID, containerName, containerID string) (*containerImageData, error) { + manifest := []byte{} + config := []byte{} + imageName := "" + + if imageID != "" { + ref, err := is.Transport.ParseStoreReference(store, "@"+imageID) + if err != nil { + return nil, errors.Wrapf(err, "no such image %q", "@"+imageID) + } + src, err2 := ref.NewImage(systemContext) + if err2 != nil { + return nil, errors.Wrapf(err2, "error instantiating image") + } + defer src.Close() + config, err = src.ConfigBlob() + if err != nil { + return nil, errors.Wrapf(err, "error reading image configuration") + } + manifest, _, err = src.Manifest() + if err != nil { + return nil, errors.Wrapf(err, "error reading image manifest") + } + if img, err3 := store.Image(imageID); err3 == nil { + if len(img.Names) > 0 { + imageName = img.Names[0] + } + } + } + + data := &containerImageData{ + store: store, + Type: containerType, + FromImage: imageName, + FromImageID: imageID, + Config: config, + Manifest: manifest, + Container: containerName, + ContainerID: containerID, + ImageAnnotations: map[string]string{}, + ImageCreatedBy: "", + } + + data.initConfig() + + return data, nil + +} + +func (c *containerImageData) makeImageRef(manifestType string, compress archive.Compression, names []string, layerID string, historyTimestamp *time.Time) (types.ImageReference, error) { var name reference.Named if len(names) > 0 { if parsed, err := reference.ParseNamed(names[0]); err == nil { @@ -332,11 +513,11 @@ func (i *imagePushData) makeImageRef(manifestType string, compress archive.Compr if manifestType == "" { manifestType = OCIv1ImageManifest } - oconfig, err := json.Marshal(&i.OCIv1) + oconfig, err := json.Marshal(&c.OCIv1) if err != nil { return nil, errors.Wrapf(err, "error encoding OCI-format image configuration") } - dconfig, err := json.Marshal(&i.Docker) + dconfig, err := json.Marshal(&c.Docker) if err != nil { return nil, errors.Wrapf(err, "error encoding docker-format image configuration") } @@ -345,7 +526,7 @@ func (i *imagePushData) makeImageRef(manifestType string, compress archive.Compr created = historyTimestamp.UTC() } ref := &containerImageRef{ - store: i.store, + store: c.store, compression: compress, name: name, names: names, @@ -354,53 +535,10 @@ func (i *imagePushData) makeImageRef(manifestType string, compress archive.Compr oconfig: oconfig, dconfig: dconfig, created: created, - createdBy: i.ImageCreatedBy, - annotations: i.ImageAnnotations, + createdBy: c.ImageCreatedBy, + annotations: c.ImageAnnotations, preferredManifestType: manifestType, exporting: true, } return ref, nil } - -func importImagePushDataFromImage(store storage.Store, img *storage.Image, systemContext *types.SystemContext) (*imagePushData, error) { - manifest := []byte{} - config := []byte{} - imageName := "" - - if img.ID != "" { - ref, err := is.Transport.ParseStoreReference(store, "@"+img.ID) - if err != nil { - return nil, errors.Wrapf(err, "no such image %q", "@"+img.ID) - } - src, err2 := ref.NewImage(systemContext) - if err2 != nil { - return nil, errors.Wrapf(err2, "error reading image configuration") - } - defer src.Close() - config, err = src.ConfigBlob() - if err != nil { - return nil, errors.Wrapf(err, "error reading image manfest") - } - manifest, _, err = src.Manifest() - if err != nil { - return nil, errors.Wrapf(err, "error reading image manifest") - } - if len(img.Names) > 0 { - imageName = img.Names[0] - } - } - - ipd := &imagePushData{ - store: store, - FromImage: imageName, - FromImageID: img.ID, - Config: config, - Manifest: manifest, - ImageAnnotations: map[string]string{}, - ImageCreatedBy: "", - } - - ipd.initConfig() - - return ipd, nil -} diff --git a/cmd/kpod/docker/types.go b/cmd/kpod/docker/types.go index 03757f95..a7e45655 100644 --- a/cmd/kpod/docker/types.go +++ b/cmd/kpod/docker/types.go @@ -11,7 +11,7 @@ import ( "github.com/opencontainers/go-digest" ) -// TypeLayers github.com/moby/moby/image/rootfs.go +// TypeLayers github.com/docker/docker/image/rootfs.go const TypeLayers = "layers" // V2S2MediaTypeManifest github.com/docker/distribution/manifest/schema2/manifest.go @@ -29,14 +29,14 @@ const V2S2MediaTypeUncompressedLayer = "application/vnd.docker.image.rootfs.diff // V2S2RootFS describes images root filesystem // This is currently a placeholder that only supports layers. In the future // this can be made into an interface that supports different implementations. -// github.com/moby/moby/image/rootfs.go +// github.com/docker/docker/image/rootfs.go type V2S2RootFS struct { Type string `json:"type"` DiffIDs []digest.Digest `json:"diff_ids,omitempty"` } // V2S2History stores build commands that were used to create an image -// github.com/moby/moby/image/image.go +// github.com/docker/docker/image/image.go type V2S2History struct { // Created is the timestamp at which the image was created Created time.Time `json:"created"` @@ -53,11 +53,11 @@ type V2S2History struct { } // ID is the content-addressable ID of an image. -// github.com/moby/moby/image/image.go +// github.com/docker/docker/image/image.go type ID digest.Digest // HealthConfig holds configuration settings for the HEALTHCHECK feature. -// github.com/moby/moby/api/types/container/config.go +// github.com/docker/docker/api/types/container/config.go type HealthConfig struct { // Test is the test to perform to check that the container is healthy. // An empty slice means to inherit the default. @@ -91,7 +91,7 @@ type Port string // Non-portable information *should* appear in HostConfig. // All fields added to this struct must be marked `omitempty` to keep getting // predictable hashes from the old `v1Compatibility` configuration. -// github.com/moby/moby/api/types/container/config.go +// github.com/docker/docker/api/types/container/config.go type Config struct { Hostname string // Hostname Domainname string // Domainname @@ -137,7 +137,7 @@ type V1Compatibility struct { } // V1Image stores the V1 image configuration. -// github.com/moby/moby/image/image.go +// github.com/docker/docker/image/image.go type V1Image struct { // ID is a unique 64 character identifier of the image ID string `json:"id,omitempty"` @@ -166,7 +166,7 @@ type V1Image struct { } // V2Image stores the image configuration -// github.com/moby/moby/image/image.go +// github.com/docker/docker/image/image.go type V2Image struct { V1Image Parent ID `json:"parent,omitempty"` diff --git a/cmd/kpod/imageData.go b/cmd/kpod/imageData.go new file mode 100644 index 00000000..1419ecd9 --- /dev/null +++ b/cmd/kpod/imageData.go @@ -0,0 +1,162 @@ +package main + +import ( + "encoding/json" + "time" + + "github.com/containers/storage" + digest "github.com/opencontainers/go-digest" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +type imageData struct { + ID string + Names []string + Digests []digest.Digest + Parent string + Comment string + Created *time.Time + Container string + ContainerConfig containerConfig + Author string + Config ociv1.ImageConfig + Architecture string + OS string + Size uint + VirtualSize uint + GraphDriver driverData + RootFS ociv1.RootFS +} + +type containerConfig struct { + Hostname string + Domainname string + User string + AttachStdin bool + AttachStdout bool + AttachStderr bool + Tty bool + OpenStdin bool + StdinOnce bool + Env []string + Cmd []string + ArgsEscaped bool + Image digest.Digest + Volumes map[string]interface{} + WorkingDir string + Entrypoint []string + Labels interface{} + OnBuild []string +} + +type rootFS struct { + Type string + Layers []string +} + +func getImageData(store storage.Store, name string) (*imageData, error) { + img, err := findImage(store, name) + if err != nil { + return nil, errors.Wrapf(err, "error reading image %q", name) + } + + cid, err := openImage(store, name) + if err != nil { + return nil, errors.Wrapf(err, "error reading image %q", name) + } + digests, err := getDigests(*img) + if err != nil { + return nil, err + } + + var bigData interface{} + ctrConfig := containerConfig{} + container := "" + if len(digests) > 0 { + bd, err := store.ImageBigData(img.ID, string(digests[len(digests)-1])) + if err != nil { + return nil, err + } + err = json.Unmarshal(bd, &bigData) + if err != nil { + return nil, err + } + + container = (bigData.(map[string]interface{})["container"]).(string) + cc, err := json.MarshalIndent((bigData.(map[string]interface{})["container_config"]).(map[string]interface{}), "", " ") + if err != nil { + return nil, err + } + err = json.Unmarshal(cc, &ctrConfig) + if err != nil { + return nil, err + } + } + + driverName, err := getDriverName(store) + if err != nil { + return nil, err + } + + topLayerID, err := getImageTopLayer(*img) + if err != nil { + return nil, err + } + driverMetadata, err := getDriverMetadata(store, topLayerID) + if err != nil { + return nil, err + } + + lstore, err := store.LayerStore() + if err != nil { + return nil, err + } + layer, err := lstore.Get(topLayerID) + if err != nil { + return nil, err + } + size, err := lstore.DiffSize(layer.Parent, layer.ID) + if err != nil { + return nil, err + } + + virtualSize, err := getImageSize(*img, store) + if err != nil { + return nil, err + } + + return &imageData{ + ID: img.ID, + Names: img.Names, + Digests: digests, + Parent: string(cid.Docker.Parent), + Comment: cid.OCIv1.History[0].Comment, + Created: cid.OCIv1.Created, + Container: container, + ContainerConfig: ctrConfig, + Author: cid.OCIv1.Author, + Config: cid.OCIv1.Config, + Architecture: cid.OCIv1.Architecture, + OS: cid.OCIv1.OS, + Size: uint(size), + VirtualSize: uint(virtualSize), + GraphDriver: driverData{ + Name: driverName, + Data: driverMetadata, + }, + RootFS: cid.OCIv1.RootFS, + }, nil +} + +func getDigests(img storage.Image) ([]digest.Digest, error) { + metadata, err := parseMetadata(img) + if err != nil { + return nil, err + } + digests := []digest.Digest{} + for _, blob := range metadata.Blobs { + digests = append(digests, blob.Digest) + } + return digests, nil +} diff --git a/cmd/kpod/imagePushData_test.go b/cmd/kpod/imagePushData_test.go deleted file mode 100644 index a6c412cc..00000000 --- a/cmd/kpod/imagePushData_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "bytes" - "fmt" -) - -// We have to compare the structs manually because they contain -// []byte variables, which cannot be compared with "==" -func compareImagePushData(a, b *imagePushData) bool { - if a.store != b.store { - fmt.Println("store") - return false - } else if a.Type != b.Type { - fmt.Println("type") - return false - } else if a.FromImage != b.FromImage { - fmt.Println("FromImage") - return false - } else if a.FromImageID != b.FromImageID { - fmt.Println("FromImageID") - return false - } else if !bytes.Equal(a.Config, b.Config) { - fmt.Println("Config") - return false - } else if !bytes.Equal(a.Manifest, b.Manifest) { - fmt.Println("Manifest") - return false - } else if fmt.Sprint(a.ImageAnnotations) != fmt.Sprint(b.ImageAnnotations) { - fmt.Println("Annotations") - return false - } else if a.ImageCreatedBy != b.ImageCreatedBy { - fmt.Println("ImageCreatedBy") - return false - } else if fmt.Sprintf("%+v", a.OCIv1) != fmt.Sprintf("%+v", b.OCIv1) { - fmt.Println("OCIv1") - return false - } - return true -} diff --git a/cmd/kpod/images.go b/cmd/kpod/images.go index 2c46c1c6..d2a8dc48 100644 --- a/cmd/kpod/images.go +++ b/cmd/kpod/images.go @@ -133,7 +133,7 @@ func parseFilter(images []storage.Image, filter string) (*filterParams, error) { pair := strings.SplitN(param, "=", 2) switch strings.TrimSpace(pair[0]) { case "dangling": - if pair[1] == "true" || pair[1] == "false" { + if isValidBool(pair[1]) { params.dangling = pair[1] } else { return nil, fmt.Errorf("invalid filter: '%s=[%s]'", pair[0], pair[1]) @@ -200,7 +200,7 @@ func outputImages(images []storage.Image, format string, store storage.Store, fi if len(imageMetadata.Blobs) > 0 { digest = string(imageMetadata.Blobs[0].Digest) } - size, _ := getSize(image, store) + size, _ := getImageSize(image, store) names := []string{""} if len(image.Names) > 0 { @@ -259,9 +259,9 @@ func matchesFilter(image storage.Image, store storage.Store, name string, params } func matchesDangling(name string, dangling string) bool { - if dangling == "false" && name != "" { + if isFalse(dangling) && name != "" { return true - } else if dangling == "true" && name == "" { + } else if isTrue(dangling) && name == "" { return true } return false diff --git a/cmd/kpod/inspect.go b/cmd/kpod/inspect.go new file mode 100644 index 00000000..b6f61e39 --- /dev/null +++ b/cmd/kpod/inspect.go @@ -0,0 +1,123 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "text/template" + + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +const ( + defaultFormat = `Container: {{.Container}} +ID: {{.ContainerID}} +` + inspectTypeContainer = "container" + inspectTypeImage = "image" + inspectAll = "all" +) + +var ( + inspectFlags = []cli.Flag{ + cli.StringFlag{ + Name: "type, t", + Value: inspectAll, + Usage: "Return JSON for specified type, (e.g image, container or task)", + }, + cli.StringFlag{ + Name: "format, f", + Value: defaultFormat, + Usage: "Format the output using the given go template", + }, + cli.BoolFlag{ + Name: "size", + Usage: "Display total file size if the type is container", + }, + } + inspectDescription = "This displays the low-level information on containers and images identified by name or ID. By default, this will render all results in a JSON array. If the container and image have the same name, this will return container JSON for unspecified type." + inspectCommand = cli.Command{ + Name: "inspect", + Usage: "Displays the configuration of a container or image", + Description: inspectDescription, + Flags: inspectFlags, + Action: inspectCmd, + ArgsUsage: "CONTAINER-OR-IMAGE", + } +) + +func inspectCmd(c *cli.Context) error { + args := c.Args() + if len(args) == 0 { + return errors.Errorf("container or image name must be specified: kpod inspect [options [...]] name") + } + if len(args) > 1 { + return errors.Errorf("too many arguments specified") + } + + itemType := c.String("type") + size := c.Bool("size") + format := defaultFormat + if c.String("format") != "" { + format = c.String("format") + } + + switch itemType { + case inspectTypeContainer: + case inspectTypeImage: + case inspectAll: + default: + return errors.Errorf("the only recognized types are %q, %q, and %q", inspectTypeContainer, inspectTypeImage, inspectAll) + } + + t := template.Must(template.New("format").Parse(format)) + + name := args[0] + + store, err := getStore(c) + if err != nil { + return err + } + + var data interface{} + switch itemType { + case inspectTypeContainer: + data, err = getContainerData(store, name, size) + if err != nil { + return errors.Wrapf(err, "error parsing container data") + } + case inspectTypeImage: + data, err = getImageData(store, name) + if err != nil { + return errors.Wrapf(err, "error parsing image data") + } + case inspectAll: + ctrData, err := getContainerData(store, name, size) + if err != nil { + imgData, err := getImageData(store, name) + if err != nil { + return errors.Wrapf(err, "error parsing image data") + } + data = imgData + + } else { + data = ctrData + } + } + + if c.IsSet("format") { + if err = t.Execute(os.Stdout, data); err != nil { + return err + } + fmt.Println() + return nil + } + + d, err := json.MarshalIndent(data, "", " ") + if err != nil { + return errors.Wrapf(err, "error encoding build container as json") + } + _, err = fmt.Println(string(d)) + return err +} diff --git a/cmd/kpod/main.go b/cmd/kpod/main.go index 6057f85e..7c1e7ac9 100644 --- a/cmd/kpod/main.go +++ b/cmd/kpod/main.go @@ -25,6 +25,7 @@ func main() { historyCommand, imagesCommand, infoCommand, + inspectCommand, pullCommand, pushCommand, rmiCommand, diff --git a/cmd/kpod/push.go b/cmd/kpod/push.go index e878d4f9..33eecc4a 100644 --- a/cmd/kpod/push.go +++ b/cmd/kpod/push.go @@ -171,16 +171,16 @@ func pushImage(srcName, destName string, options pushOptions) error { return errors.Wrapf(err, "error locating image %q for importing settings", srcName) } systemContext := getSystemContext(options.SignaturePolicyPath) - ipd, err := importImagePushDataFromImage(options.Store, img, systemContext) + cid, err := importContainerImageDataFromImage(options.Store, systemContext, img.ID, "", "") if err != nil { return err } // Give the image we're producing the same ancestors as its source image - ipd.FromImage = ipd.Docker.ContainerConfig.Image - ipd.FromImageID = string(ipd.Docker.Parent) + cid.FromImage = cid.Docker.ContainerConfig.Image + cid.FromImageID = string(cid.Docker.Parent) // Prep the layers and manifest for export - src, err := ipd.makeImageRef(manifest.GuessMIMEType(ipd.Manifest), options.Compression, img.Names, img.TopLayer, nil) + src, err := cid.makeImageRef(manifest.GuessMIMEType(cid.Manifest), options.Compression, img.Names, img.TopLayer, nil) if err != nil { return errors.Wrapf(err, "error copying layers and metadata") } diff --git a/cmd/kpod/push_test.go b/cmd/kpod/push_test.go deleted file mode 100644 index 2705ebe6..00000000 --- a/cmd/kpod/push_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "os/user" - "testing" - - is "github.com/containers/image/storage" -) - -func TestImportImagePushDataFromImage(t *testing.T) { - u, err := user.Current() - if err != nil { - t.Log("Could not determine user. Running as root may cause tests to fail") - } else if u.Uid != "0" { - t.Fatal("tests will fail unless run as root") - } - // Get Store - store, err := getStoreForTests() - if err != nil { - t.Fatalf("could not get store: %q", err) - } - // Pull an image and save it to the store - testImageName := "docker.io/library/busybox:1.26" - err = pullTestImage(testImageName) - if err != nil { - t.Fatalf("could not pull test image: %q", err) - } - img, err := findImage(store, testImageName) - if err != nil { - t.Fatalf("could not find image in store: %q", err) - } - // Get System Context - systemContext := getSystemContext("") - // Call importImagePushDataFromImage - ipd, err := importImagePushDataFromImage(store, img, systemContext) - if err != nil { - t.Fatalf("could not get ImagePushData: %q", err) - } - // Get ref and from it, get the config and the manifest - ref, err := is.Transport.ParseStoreReference(store, "@"+img.ID) - if err != nil { - t.Fatalf("no such image %q", "@"+img.ID) - } - src, err := ref.NewImage(systemContext) - if err != nil { - t.Fatalf("error creating new image from system context: %q", err) - } - defer src.Close() - config, err := src.ConfigBlob() - if err != nil { - t.Fatalf("error reading image config: %q", err) - } - manifest, _, err := src.Manifest() - if err != nil { - t.Fatalf("error reading image manifest: %q", err) - } - //Create "expected" ipd struct - expectedIpd := &imagePushData{ - store: store, - FromImage: testImageName, - FromImageID: img.ID, - Config: config, - Manifest: manifest, - ImageAnnotations: map[string]string{}, - ImageCreatedBy: "", - } - expectedIpd.initConfig() - //Compare structs, error if they are not the same - if !compareImagePushData(ipd, expectedIpd) { - t.Errorf("imagePushData did not match expected imagePushData") - } -} diff --git a/completions/bash/kpod b/completions/bash/kpod index 687f7e48..0d5a728b 100644 --- a/completions/bash/kpod +++ b/completions/bash/kpod @@ -6,6 +6,28 @@ __kpod_list_images() { COMPREPLY=($(compgen -W "$(kpod images -q)" -- $cur)) } +_kpod_history() { + local options_with_args=" + --format + " + local boolean_options=" + --human -H + --no-trunc + --quiet -q + --json + " + _complete_ "$options_with_args" "$boolean_options" + + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + __kpod_list_images + ;; + esac +} + _kpod_images() { local boolean_options=" --help @@ -125,28 +147,6 @@ _complete_() { esac } -_kpod_history() { - local options_with_args=" - --format - " - local boolean_options=" - --human -H - --no-trunc - --quiet -q - --json - " - _complete_ "$options_with_args" "$boolean_options" - - case "$cur" in - -*) - COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) - ;; - *) - __kpod_list_images - ;; - esac -} - _kpod_kpod() { local options_with_args=" " diff --git a/docs/kpod-inspect.1.md b/docs/kpod-inspect.1.md new file mode 100644 index 00000000..3d5fbccb --- /dev/null +++ b/docs/kpod-inspect.1.md @@ -0,0 +1,169 @@ +## kpod-inspect "1" "July 2017" "kpod" + +## NAME +kpod inspect - display a container or image's configuration + +## SYNOPSIS +**kpod** **inspect** [*options* [...]] name + +## DESCRIPTION +This displays the low-level information on containers and images identified by name or ID. By default, this will render all results in a JSON array. If the container and image have the same name, this will return container JSON for unspecified type. If a format is specified, the given template will be executed for each result. + +## OPTIONS + +**--type, t="TYPE"** + +Return data on items of the specified type. Type can be 'container', 'image' or 'all' (default: all) + +**--format, -f="FORMAT"** + +Format the output using the given Go template + +**--size** + +Display the total file size if the type is a container + + +## EXAMPLE + +kpod inspect redis:alpine + +{ + "ArgsEscaped": true, + "AttachStderr": false, + "AttachStdin": false, + "AttachStdout": false, + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "CMD [\"redis-server\"]" + ], + "Domainname": "", + "Entrypoint": [ + "entrypoint.sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "REDIS_VERSION=3.2.9", + "REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-3.2.9.tar.gz", + "REDIS_DOWNLOAD_SHA=6eaacfa983b287e440d0839ead20c2231749d5d6b78bbe0e0ffa3a890c59ff26" + ], + "ExposedPorts": { + "6379/tcp": {} + }, + "Hostname": "e1ede117fb1e", + "Image": "sha256:75e877aa15b534396de82d385386cc4dda7819d5cbb018b9f97b77aeb8f4b55a", + "Labels": {}, + "OnBuild": [], + "OpenStdin": false, + "StdinOnce": false, + "Tty": false, + "User": "", + "Volumes": { + "/data": {} + }, + "WorkingDir": "/data" +} +{ + "ID": "b3f2436bdb978c1d33b1387afb5d7ba7e3243ed2ce908db431ac0069da86cb45", + "Names": [ + "docker.io/library/redis:alpine" + ], + "Digests": [ + "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926", + "sha256:07b1ac6c7a5068201d8b63a09bb15358ec1616b813ef3942eb8cc12ae191227f", + "sha256:91e2e140ea27b3e89f359cd9fab4ec45647dda2a8e5fb0c78633217d9dca87b5", + "sha256:08957ceaa2b3be874cde8d7fa15c274300f47185acd62bca812a2ffb6228482d", + "sha256:acd3d12a6a79f772961a771f678c1a39e1f370e7baeb9e606ad8f1b92572f4ab", + "sha256:4ad88df090801e8faa8cf0be1f403b77613d13e11dad73f561461d482f79256c", + "sha256:159ac12c79e1a8d85dfe61afff8c64b96881719139730012a9697f432d6b739a" + ], + "Parent": "", + "Comment": "", + "Created": "2017-06-28T22:14:36.35280993Z", + "Container": "ba8d6c6b0d7fdd201fce404236136b44f3bfdda883466531a3d1a1f87906770b", + "ContainerConfig": { + "Hostname": "e1ede117fb1e", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "REDIS_VERSION=3.2.9", + "REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-3.2.9.tar.gz", + "REDIS_DOWNLOAD_SHA=6eaacfa983b287e440d0839ead20c2231749d5d6b78bbe0e0ffa3a890c59ff26" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "CMD [\"redis-server\"]" + ], + "ArgsEscaped": true, + "Image": "sha256:75e877aa15b534396de82d385386cc4dda7819d5cbb018b9f97b77aeb8f4b55a", + "Volumes": { + "/data": {} + }, + "WorkingDir": "/data", + "Entrypoint": [ + "entrypoint.sh" + ], + "Labels": {}, + "OnBuild": [] + }, + "Author": "", + "Config": { + "ExposedPorts": { + "6379/tcp": {} + }, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "REDIS_VERSION=3.2.9", + "REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-3.2.9.tar.gz", + "REDIS_DOWNLOAD_SHA=6eaacfa983b287e440d0839ead20c2231749d5d6b78bbe0e0ffa3a890c59ff26" + ], + "Entrypoint": [ + "entrypoint.sh" + ], + "Cmd": [ + "redis-server" + ], + "Volumes": { + "/data": {} + }, + "WorkingDir": "/data" + }, + "Architecture": "amd64", + "OS": "linux", + "Size": 3965955, + "VirtualSize": 19808086, + "GraphDriver": { + "Name": "overlay2", + "Data": { + "MergedDir": "/var/lib/containers/storage/overlay2/2059d805c90e034cb773d9722232ef018a72143dd31113b470fb876baeccd700/merged", + "UpperDir": "/var/lib/containers/storage/overlay2/2059d805c90e034cb773d9722232ef018a72143dd31113b470fb876baeccd700/diff", + "WorkDir": "/var/lib/containers/storage/overlay2/2059d805c90e034cb773d9722232ef018a72143dd31113b470fb876baeccd700/work" + } + }, + "RootFS": { + "type": "layers", + "diff_ids": [ + "sha256:5bef08742407efd622d243692b79ba0055383bbce12900324f75e56f589aedb0", + "sha256:c92a8fc997217611d0bfc9ff14d7ec00350ca564aef0ecbf726624561d7872d7", + "sha256:d4c406dea37a107b0cccb845611266a146725598be3e82ba31c55c08d1583b5a", + "sha256:8b4fa064e2b6c03a6c37089b0203f167375a8b49259c0ad7cb47c8c1e58b3fa0", + "sha256:c393e3d0b00ddf6b4166f1e2ad68245e08e9e3be0a0567a36d0a43854f03bfd6", + "sha256:38047b4117cb8bb3bba82991daf9a4e14ba01f9f66c1434d4895a7e96f67d8ba" + ] + } +} + + +## SEE ALSO +kpod(1) diff --git a/oci/container.go b/oci/container.go index 01ec4d2f..7907856f 100644 --- a/oci/container.go +++ b/oci/container.go @@ -177,3 +177,10 @@ func (c *Container) NetNsPath() (string, error) { func (c *Container) Metadata() *pb.ContainerMetadata { return c.metadata } + +// State returns the state of the running container +func (c *Container) State() *ContainerState { + c.opLock.Lock() + defer c.opLock.Unlock() + return c.state +} diff --git a/test/kpod.bats b/test/kpod.bats index 7e8fce32..8fb19a11 100644 --- a/test/kpod.bats +++ b/test/kpod.bats @@ -190,3 +190,39 @@ function teardown() { run crioctl image remove "$IMAGE" stop_crio } + +@test "kpod inspect image" { + run ${KPOD_BINARY} $KPOD_OPTIONS pull redis:alpine + [ "$status" -eq 0 ] + run bash -c "${KPOD_BINARY} $KPOD_OPTIONS inspect redis:alpine | python -m json.tool" + echo "$output" + [ "$status" -eq 0 ] +} + run ${KPOD_BINARY} $KPOD_OPTIONS rmi redis:alpine + +@test "kpod inspect non-existent container" { + run ${KPOD_BINARY} $KPOD_OPTIONS inspect 14rcole/non-existent + echo "$output" + [ "$status" -ne 0 ] +} + +@test "kpod inspect with format" { + run ${KPOD_BINARY} $KPOD_OPTIONS pull redis:alpine + [ "$status" -eq 0 ] + run ${KPOD_BINARY} $KPOD_OPTIONS --format {{.ID}} inspect redis:alpine + [ "$status" -eq 0] + inspectOutput="$output" + run ${KPOD_BINARY} $KPOD_OPTIONS images --quiet redis:alpine + [ "$status" -eq 0] + [ "$output" -eq "$inspectOutput" ] + run ${KPOD_BINARY} $KPOD_OPTIONS rmi redis:alpine +} + +@test "kpod inspect specified type" { + run ${KPOD_BINARY} $KPOD_OPTIONS pull redis:alpine + [ "$status" -eq 0 ] + run bash -c "${KPOD_BINARY} $KPOD_OPTIONS inspect --type image redis:alpine | python -m json.tool" + echo "$output" + [ "$status" -eq 0] + run ${KPOD_BINARY} $KPOD_OPTIONS rmi redis:alpine +}