From a031b83a09a8628435317a03f199cdc18b78262f Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Wed, 1 Nov 2017 11:24:59 -0400 Subject: Initial checkin from CRI-O repo Signed-off-by: Matthew Heon --- cmd/kpod/README.md | 16 + cmd/kpod/common.go | 135 +++++++++ cmd/kpod/common_test.go | 51 ++++ cmd/kpod/diff.go | 128 ++++++++ cmd/kpod/docker/types.go | 271 +++++++++++++++++ cmd/kpod/export.go | 106 +++++++ cmd/kpod/formats/formats.go | 143 +++++++++ cmd/kpod/formats/templates.go | 78 +++++ cmd/kpod/history.go | 243 +++++++++++++++ cmd/kpod/images.go | 330 +++++++++++++++++++++ cmd/kpod/info.go | 200 +++++++++++++ cmd/kpod/inspect.go | 120 ++++++++ cmd/kpod/kill.go | 74 +++++ cmd/kpod/load.go | 116 ++++++++ cmd/kpod/login.go | 110 +++++++ cmd/kpod/logout.go | 69 +++++ cmd/kpod/logs.go | 92 ++++++ cmd/kpod/main.go | 129 ++++++++ cmd/kpod/mount.go | 121 ++++++++ cmd/kpod/pause.go | 58 ++++ cmd/kpod/ps.go | 665 ++++++++++++++++++++++++++++++++++++++++++ cmd/kpod/pull.go | 118 ++++++++ cmd/kpod/push.go | 132 +++++++++ cmd/kpod/rename.go | 49 ++++ cmd/kpod/rm.go | 69 +++++ cmd/kpod/rmi.go | 56 ++++ cmd/kpod/save.go | 98 +++++++ cmd/kpod/stats.go | 245 ++++++++++++++++ cmd/kpod/stop.go | 77 +++++ cmd/kpod/tag.go | 77 +++++ cmd/kpod/umount.go | 41 +++ cmd/kpod/unpause.go | 58 ++++ cmd/kpod/version.go | 48 +++ cmd/kpod/wait.go | 62 ++++ 34 files changed, 4385 insertions(+) create mode 100644 cmd/kpod/README.md create mode 100644 cmd/kpod/common.go create mode 100644 cmd/kpod/common_test.go create mode 100644 cmd/kpod/diff.go create mode 100644 cmd/kpod/docker/types.go create mode 100644 cmd/kpod/export.go create mode 100644 cmd/kpod/formats/formats.go create mode 100644 cmd/kpod/formats/templates.go create mode 100644 cmd/kpod/history.go create mode 100644 cmd/kpod/images.go create mode 100644 cmd/kpod/info.go create mode 100644 cmd/kpod/inspect.go create mode 100644 cmd/kpod/kill.go create mode 100644 cmd/kpod/load.go create mode 100644 cmd/kpod/login.go create mode 100644 cmd/kpod/logout.go create mode 100644 cmd/kpod/logs.go create mode 100644 cmd/kpod/main.go create mode 100644 cmd/kpod/mount.go create mode 100644 cmd/kpod/pause.go create mode 100644 cmd/kpod/ps.go create mode 100644 cmd/kpod/pull.go create mode 100644 cmd/kpod/push.go create mode 100644 cmd/kpod/rename.go create mode 100644 cmd/kpod/rm.go create mode 100644 cmd/kpod/rmi.go create mode 100644 cmd/kpod/save.go create mode 100644 cmd/kpod/stats.go create mode 100644 cmd/kpod/stop.go create mode 100644 cmd/kpod/tag.go create mode 100644 cmd/kpod/umount.go create mode 100644 cmd/kpod/unpause.go create mode 100644 cmd/kpod/version.go create mode 100644 cmd/kpod/wait.go (limited to 'cmd/kpod') diff --git a/cmd/kpod/README.md b/cmd/kpod/README.md new file mode 100644 index 000000000..7a79e4893 --- /dev/null +++ b/cmd/kpod/README.md @@ -0,0 +1,16 @@ +# kpod - Simple debugging tool for pods and images +kpod is a simple client only tool to help with debugging issues when daemons such as CRI runtime and the kubelet are not responding or +failing. A shared API layer could be created to share code between the daemon and kpod. kpod does not require any daemon running. kpod +utilizes the same underlying components that crio uses i.e. containers/image, container/storage, oci-runtime-tool/generate, runc or +any other OCI compatible runtime. kpod shares state with crio and so has the capability to debug pods/images created by crio. + +## Use cases +1. List pods. +2. Launch simple pods (that require no daemon support). +3. Exec commands in a container in a pod. +4. Launch additional containers in a pod. +5. List images. +6. Remove images not in use. +7. Pull images. +8. Check image size. +9. Report pod disk resource usage. diff --git a/cmd/kpod/common.go b/cmd/kpod/common.go new file mode 100644 index 000000000..f77b3fd1e --- /dev/null +++ b/cmd/kpod/common.go @@ -0,0 +1,135 @@ +package main + +import ( + "os" + "reflect" + "regexp" + "strings" + + is "github.com/containers/image/storage" + "github.com/containers/storage" + "github.com/fatih/camelcase" + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/kubernetes-incubator/cri-o/libpod" + "github.com/kubernetes-incubator/cri-o/server" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + stores = make(map[storage.Store]struct{}) +) + +func getStore(c *libkpod.Config) (storage.Store, error) { + options := storage.DefaultStoreOptions + options.GraphRoot = c.Root + options.RunRoot = c.RunRoot + options.GraphDriverName = c.Storage + options.GraphDriverOptions = c.StorageOptions + + store, err := storage.GetStore(options) + if err != nil { + return nil, err + } + is.Transport.SetStore(store) + stores[store] = struct{}{} + return store, nil +} + +func getRuntime(c *cli.Context) (*libpod.Runtime, error) { + + config, err := getConfig(c) + if err != nil { + return nil, errors.Wrapf(err, "could not get config") + } + + options := storage.DefaultStoreOptions + options.GraphRoot = config.Root + options.RunRoot = config.RunRoot + options.GraphDriverName = config.Storage + options.GraphDriverOptions = config.StorageOptions + + return libpod.NewRuntime(libpod.WithStorageConfig(options)) +} + +func shutdownStores() { + for store := range stores { + if _, err := store.Shutdown(false); err != nil { + break + } + } +} + +func getConfig(c *cli.Context) (*libkpod.Config, error) { + config := libkpod.DefaultConfig() + var configFile string + if c.GlobalIsSet("config") { + configFile = c.GlobalString("config") + } else if _, err := os.Stat(server.CrioConfigPath); err == nil { + configFile = server.CrioConfigPath + } + // load and merge the configfile from the commandline or use + // the default crio config file + if configFile != "" { + err := config.UpdateFromFile(configFile) + if err != nil { + return config, err + } + } + if c.GlobalIsSet("root") { + config.Root = c.GlobalString("root") + } + if c.GlobalIsSet("runroot") { + config.RunRoot = c.GlobalString("runroot") + } + + if c.GlobalIsSet("storage-driver") { + config.Storage = c.GlobalString("storage-driver") + } + if c.GlobalIsSet("storage-opt") { + opts := c.GlobalStringSlice("storage-opt") + if len(opts) > 0 { + config.StorageOptions = opts + } + } + if c.GlobalIsSet("runtime") { + config.Runtime = c.GlobalString("runtime") + } + return config, nil +} + +func splitCamelCase(src string) string { + entries := camelcase.Split(src) + return strings.Join(entries, " ") +} + +// validateFlags searches for StringFlags or StringSlice flags that never had +// a value set. This commonly occurs when the CLI mistakenly takes the next +// option and uses it as a value. +func validateFlags(c *cli.Context, flags []cli.Flag) error { + for _, flag := range flags { + switch reflect.TypeOf(flag).String() { + case "cli.StringSliceFlag": + { + f := flag.(cli.StringSliceFlag) + name := strings.Split(f.Name, ",") + val := c.StringSlice(name[0]) + for _, v := range val { + if ok, _ := regexp.MatchString("^-.+", v); ok { + return errors.Errorf("option --%s requires a value", name[0]) + } + } + } + case "cli.StringFlag": + { + f := flag.(cli.StringFlag) + name := strings.Split(f.Name, ",") + val := c.String(name[0]) + if ok, _ := regexp.MatchString("^-.+", val); ok { + return errors.Errorf("option --%s requires a value", name[0]) + } + } + } + } + return nil +} diff --git a/cmd/kpod/common_test.go b/cmd/kpod/common_test.go new file mode 100644 index 000000000..663bc41e1 --- /dev/null +++ b/cmd/kpod/common_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "os/exec" + "os/user" + "testing" + + "flag" + + "github.com/urfave/cli" +) + +func TestGetStore(t *testing.T) { + t.Skip("FIX THIS!") + + //cmd/kpod/common_test.go:27: cannot use c (type *cli.Context) as type *libkpod.Config in argument to getStore + + // Make sure the tests are running as root + skipTestIfNotRoot(t) + + set := flag.NewFlagSet("test", 0) + globalSet := flag.NewFlagSet("test", 0) + globalSet.String("root", "", "path to the root directory in which data, including images, is stored") + globalCtx := cli.NewContext(nil, globalSet, nil) + command := cli.Command{Name: "imagesCommand"} + c := cli.NewContext(nil, set, globalCtx) + c.Command = command + + //_, err := getStore(c) + //if err != nil { + //t.Error(err) + //} +} + +func skipTestIfNotRoot(t *testing.T) { + u, err := user.Current() + if err != nil { + t.Skip("Could not determine user. Running without root may cause tests to fail") + } else if u.Uid != "0" { + t.Skip("tests will fail unless run as root") + } +} + +func pullTestImage(name string) error { + cmd := exec.Command("crioctl", "image", "pull", name) + err := cmd.Run() + if err != nil { + return err + } + return nil +} diff --git a/cmd/kpod/diff.go b/cmd/kpod/diff.go new file mode 100644 index 000000000..c28bdfce6 --- /dev/null +++ b/cmd/kpod/diff.go @@ -0,0 +1,128 @@ +package main + +import ( + "fmt" + + "github.com/containers/storage/pkg/archive" + "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" + "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"` +} + +type diffOutputParams struct { + Change archive.ChangeType + Path string +} + +type stdoutStruct struct { + output []diffOutputParams +} + +func (so stdoutStruct) Out() error { + for _, d := range so.output { + fmt.Printf("%s %s\n", d.Change, d.Path) + } + return nil +} + +var ( + diffFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "archive", + Usage: "Save the diff as a tar archive", + Hidden: true, + }, + cli.StringFlag{ + Name: "format", + Usage: "Change the output format.", + }, + } + 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 formatJSON(output []diffOutputParams) (diffJSONOutput, error) { + jsonStruct := diffJSONOutput{} + for _, output := range output { + switch output.Change { + case archive.ChangeModify: + jsonStruct.Changed = append(jsonStruct.Changed, output.Path) + case archive.ChangeAdd: + jsonStruct.Added = append(jsonStruct.Added, output.Path) + case archive.ChangeDelete: + jsonStruct.Deleted = append(jsonStruct.Deleted, output.Path) + default: + return jsonStruct, errors.Errorf("output kind %q not recognized", output.Change.String()) + } + } + return jsonStruct, nil +} + +func diffCmd(c *cli.Context) error { + if err := validateFlags(c, diffFlags); err != nil { + return err + } + + if len(c.Args()) != 1 { + return errors.Errorf("container, image, or layer name must be specified: kpod diff [options [...]] ID-NAME") + } + + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + to := c.Args().Get(0) + changes, err := runtime.GetDiff("", to) + if err != nil { + return errors.Wrapf(err, "could not get changes for %q", to) + } + + diffOutput := []diffOutputParams{} + outputFormat := c.String("format") + + for _, change := range changes { + + params := diffOutputParams{ + Change: change.Kind, + Path: change.Path, + } + diffOutput = append(diffOutput, params) + } + + var out formats.Writer + + if outputFormat != "" { + switch outputFormat { + case formats.JSONString: + data, err := formatJSON(diffOutput) + if err != nil { + return err + } + out = formats.JSONStruct{Output: data} + default: + return errors.New("only valid format for diff is 'json'") + } + } else { + out = stdoutStruct{output: diffOutput} + } + formats.Writer(out).Out() + + return nil +} diff --git a/cmd/kpod/docker/types.go b/cmd/kpod/docker/types.go new file mode 100644 index 000000000..a7e456554 --- /dev/null +++ b/cmd/kpod/docker/types.go @@ -0,0 +1,271 @@ +package docker + +// +// Types extracted from Docker +// + +import ( + "time" + + "github.com/containers/image/pkg/strslice" + "github.com/opencontainers/go-digest" +) + +// TypeLayers github.com/docker/docker/image/rootfs.go +const TypeLayers = "layers" + +// V2S2MediaTypeManifest github.com/docker/distribution/manifest/schema2/manifest.go +const V2S2MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json" + +// V2S2MediaTypeImageConfig github.com/docker/distribution/manifest/schema2/manifest.go +const V2S2MediaTypeImageConfig = "application/vnd.docker.container.image.v1+json" + +// V2S2MediaTypeLayer github.com/docker/distribution/manifest/schema2/manifest.go +const V2S2MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip" + +// V2S2MediaTypeUncompressedLayer github.com/docker/distribution/manifest/schema2/manifest.go +const V2S2MediaTypeUncompressedLayer = "application/vnd.docker.image.rootfs.diff.tar" + +// 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/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/docker/docker/image/image.go +type V2S2History struct { + // Created is the timestamp at which the image was created + Created time.Time `json:"created"` + // Author is the name of the author that was specified when committing the image + Author string `json:"author,omitempty"` + // CreatedBy keeps the Dockerfile command used while building the image + CreatedBy string `json:"created_by,omitempty"` + // Comment is the commit message that was set when committing the image + Comment string `json:"comment,omitempty"` + // EmptyLayer is set to true if this history item did not generate a + // layer. Otherwise, the history item is associated with the next + // layer in the RootFS section. + EmptyLayer bool `json:"empty_layer,omitempty"` +} + +// ID is the content-addressable ID of an image. +// github.com/docker/docker/image/image.go +type ID digest.Digest + +// HealthConfig holds configuration settings for the HEALTHCHECK feature. +// 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. + // The options are: + // {} : inherit healthcheck + // {"NONE"} : disable healthcheck + // {"CMD", args...} : exec arguments directly + // {"CMD-SHELL", command} : run command with system's default shell + Test []string `json:",omitempty"` + + // Zero means to inherit. Durations are expressed as integer nanoseconds. + Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks. + Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung. + + // Retries is the number of consecutive failures needed to consider a container as unhealthy. + // Zero means inherit. + Retries int `json:",omitempty"` +} + +// PortSet is a collection of structs indexed by Port +// github.com/docker/go-connections/nat/nat.go +type PortSet map[Port]struct{} + +// Port is a string containing port number and protocol in the format "80/tcp" +// github.com/docker/go-connections/nat/nat.go +type Port string + +// Config contains the configuration data about a container. +// It should hold only portable information about the container. +// Here, "portable" means "independent from the host we are running on". +// 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/docker/docker/api/types/container/config.go +type Config struct { + Hostname string // Hostname + Domainname string // Domainname + User string // User that will run the command(s) inside the container, also support user:group + AttachStdin bool // Attach the standard input, makes possible user interaction + AttachStdout bool // Attach the standard output + AttachStderr bool // Attach the standard error + ExposedPorts PortSet `json:",omitempty"` // List of exposed ports + Tty bool // Attach standard streams to a tty, including stdin if it is not closed. + OpenStdin bool // Open stdin + StdinOnce bool // If true, close stdin after the 1 attached client disconnects. + Env []string // List of environment variable to set in the container + Cmd strslice.StrSlice // Command to run when starting the container + Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy + ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (Windows specific) + Image string // Name of the image as it was passed by the operator (e.g. could be symbolic) + Volumes map[string]struct{} // List of volumes (mounts) used for the container + WorkingDir string // Current directory (PWD) in the command will be launched + Entrypoint strslice.StrSlice // Entrypoint to run when starting the container + NetworkDisabled bool `json:",omitempty"` // Is network disabled + MacAddress string `json:",omitempty"` // Mac Address of the container + OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile + Labels map[string]string // List of labels set to this container + StopSignal string `json:",omitempty"` // Signal to stop a container + StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container + Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT +} + +// V1Compatibility - For non-top-level layers, create fake V1Compatibility +// strings that fit the format and don't collide with anything else, but +// don't result in runnable images on their own. +// github.com/docker/distribution/manifest/schema1/config_builder.go +type V1Compatibility struct { + ID string `json:"id"` + Parent string `json:"parent,omitempty"` + Comment string `json:"comment,omitempty"` + Created time.Time `json:"created"` + ContainerConfig struct { + Cmd []string + } `json:"container_config,omitempty"` + Author string `json:"author,omitempty"` + ThrowAway bool `json:"throwaway,omitempty"` +} + +// V1Image stores the V1 image configuration. +// 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"` + // Parent is the ID of the parent image + Parent string `json:"parent,omitempty"` + // Comment is the commit message that was set when committing the image + Comment string `json:"comment,omitempty"` + // Created is the timestamp at which the image was created + Created time.Time `json:"created"` + // Container is the id of the container used to commit + Container string `json:"container,omitempty"` + // ContainerConfig is the configuration of the container that is committed into the image + ContainerConfig Config `json:"container_config,omitempty"` + // DockerVersion specifies the version of Docker that was used to build the image + DockerVersion string `json:"docker_version,omitempty"` + // Author is the name of the author that was specified when committing the image + Author string `json:"author,omitempty"` + // Config is the configuration of the container received from the client + Config *Config `json:"config,omitempty"` + // Architecture is the hardware that the image is build and runs on + Architecture string `json:"architecture,omitempty"` + // OS is the operating system used to build and run the image + OS string `json:"os,omitempty"` + // Size is the total size of the image including all layers it is composed of + Size int64 `json:",omitempty"` +} + +// V2Image stores the image configuration +// github.com/docker/docker/image/image.go +type V2Image struct { + V1Image + Parent ID `json:"parent,omitempty"` + RootFS *V2S2RootFS `json:"rootfs,omitempty"` + History []V2S2History `json:"history,omitempty"` + OSVersion string `json:"os.version,omitempty"` + OSFeatures []string `json:"os.features,omitempty"` + + // rawJSON caches the immutable JSON associated with this image. + //rawJSON []byte + + // computedID is the ID computed from the hash of the image config. + // Not to be confused with the legacy V1 ID in V1Image. + //computedID ID +} + +// V2Versioned provides a struct with the manifest schemaVersion and mediaType. +// Incoming content with unknown schema version can be decoded against this +// struct to check the version. +// github.com/docker/distribution/manifest/versioned.go +type V2Versioned struct { + // SchemaVersion is the image manifest schema that this image follows + SchemaVersion int `json:"schemaVersion"` + + // MediaType is the media type of this schema. + MediaType string `json:"mediaType,omitempty"` +} + +// V2S1FSLayer is a container struct for BlobSums defined in an image manifest +// github.com/docker/distribution/manifest/schema1/manifest.go +type V2S1FSLayer struct { + // BlobSum is the tarsum of the referenced filesystem image layer + BlobSum digest.Digest `json:"blobSum"` +} + +// V2S1History stores unstructured v1 compatibility information +// github.com/docker/distribution/manifest/schema1/manifest.go +type V2S1History struct { + // V1Compatibility is the raw v1 compatibility information + V1Compatibility string `json:"v1Compatibility"` +} + +// V2S1Manifest provides the base accessible fields for working with V2 image +// format in the registry. +// github.com/docker/distribution/manifest/schema1/manifest.go +type V2S1Manifest struct { + V2Versioned + + // Name is the name of the image's repository + Name string `json:"name"` + + // Tag is the tag of the image specified by this manifest + Tag string `json:"tag"` + + // Architecture is the host architecture on which this image is intended to + // run + Architecture string `json:"architecture"` + + // FSLayers is a list of filesystem layer blobSums contained in this image + FSLayers []V2S1FSLayer `json:"fsLayers"` + + // History is a list of unstructured historical data for v1 compatibility + History []V2S1History `json:"history"` +} + +// V2S2Descriptor describes targeted content. Used in conjunction with a blob +// store, a descriptor can be used to fetch, store and target any kind of +// blob. The struct also describes the wire protocol format. Fields should +// only be added but never changed. +// github.com/docker/distribution/blobs.go +type V2S2Descriptor struct { + // MediaType describe the type of the content. All text based formats are + // encoded as utf-8. + MediaType string `json:"mediaType,omitempty"` + + // Size in bytes of content. + Size int64 `json:"size,omitempty"` + + // Digest uniquely identifies the content. A byte stream can be verified + // against against this digest. + Digest digest.Digest `json:"digest,omitempty"` + + // URLs contains the source URLs of this content. + URLs []string `json:"urls,omitempty"` + + // NOTE: Before adding a field here, please ensure that all + // other options have been exhausted. Much of the type relationships + // depend on the simplicity of this type. +} + +// V2S2Manifest defines a schema2 manifest. +// github.com/docker/distribution/manifest/schema2/manifest.go +type V2S2Manifest struct { + V2Versioned + + // Config references the image configuration as a blob. + Config V2S2Descriptor `json:"config"` + + // Layers lists descriptors for the layers referenced by the + // configuration. + Layers []V2S2Descriptor `json:"layers"` +} diff --git a/cmd/kpod/export.go b/cmd/kpod/export.go new file mode 100644 index 000000000..94f05ce10 --- /dev/null +++ b/cmd/kpod/export.go @@ -0,0 +1,106 @@ +package main + +import ( + "io" + "os" + + "fmt" + + "github.com/containers/storage" + "github.com/containers/storage/pkg/archive" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +type exportOptions struct { + output string + container string +} + +var ( + exportFlags = []cli.Flag{ + cli.StringFlag{ + Name: "output, o", + Usage: "Write to a file, default is STDOUT", + Value: "/dev/stdout", + }, + } + exportDescription = "Exports container's filesystem contents as a tar archive" + + " and saves it on the local machine." + exportCommand = cli.Command{ + Name: "export", + Usage: "Export container's filesystem contents as a tar archive", + Description: exportDescription, + Flags: exportFlags, + Action: exportCmd, + ArgsUsage: "CONTAINER", + } +) + +// exportCmd saves a container to a tarball on disk +func exportCmd(c *cli.Context) error { + args := c.Args() + if len(args) == 0 { + return errors.Errorf("container id must be specified") + } + if len(args) > 1 { + return errors.Errorf("too many arguments given, need 1 at most.") + } + container := args[0] + if err := validateFlags(c, exportFlags); err != nil { + return err + } + + config, err := getConfig(c) + if err != nil { + return errors.Wrapf(err, "could not get config") + } + store, err := getStore(config) + if err != nil { + return err + } + + output := c.String("output") + if output == "/dev/stdout" { + file := os.Stdout + if logrus.IsTerminal(file) { + return errors.Errorf("refusing to export to terminal. Use -o flag or redirect") + } + } + + opts := exportOptions{ + output: output, + container: container, + } + + return exportContainer(store, opts) +} + +// exportContainer exports the contents of a container and saves it as +// a tarball on disk +func exportContainer(store storage.Store, opts exportOptions) error { + mountPoint, err := store.Mount(opts.container, "") + if err != nil { + return errors.Wrapf(err, "error finding container %q", opts.container) + } + defer func() { + if err := store.Unmount(opts.container); err != nil { + fmt.Printf("error unmounting container %q: %v\n", opts.container, err) + } + }() + + input, err := archive.Tar(mountPoint, archive.Uncompressed) + if err != nil { + return errors.Wrapf(err, "error reading container directory %q", opts.container) + } + + outFile, err := os.Create(opts.output) + if err != nil { + return errors.Wrapf(err, "error creating file %q", opts.output) + } + defer outFile.Close() + + _, err = io.Copy(outFile, input) + return err +} diff --git a/cmd/kpod/formats/formats.go b/cmd/kpod/formats/formats.go new file mode 100644 index 000000000..6e5dd2425 --- /dev/null +++ b/cmd/kpod/formats/formats.go @@ -0,0 +1,143 @@ +package formats + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "text/tabwriter" + "text/template" + + "bytes" + "github.com/ghodss/yaml" + "github.com/pkg/errors" +) + +const ( + // JSONString const to save on duplicate variable names + JSONString = "json" + // IDString const to save on duplicates for Go templates + IDString = "{{.ID}}" +) + +// Writer interface for outputs +type Writer interface { + Out() error +} + +// JSONStructArray for JSON output +type JSONStructArray struct { + Output []interface{} +} + +// StdoutTemplateArray for Go template output +type StdoutTemplateArray struct { + Output []interface{} + Template string + Fields map[string]string +} + +// JSONStruct for JSON output +type JSONStruct struct { + Output interface{} +} + +// StdoutTemplate for Go template output +type StdoutTemplate struct { + Output interface{} + Template string + Fields map[string]string +} + +// YAMLStruct for YAML output +type YAMLStruct struct { + Output interface{} +} + +// Out method for JSON Arrays +func (j JSONStructArray) Out() error { + data, err := json.MarshalIndent(j.Output, "", " ") + if err != nil { + return err + } + + // JSON returns a byte array with a literal null [110 117 108 108] in it + // if it is passed empty data. We used bytes.Compare to see if that is + // the case. + if diff := bytes.Compare(data, []byte("null")); diff == 0 { + data = []byte("[]") + } + + // If the we did get NULL back, we should spit out {} which is + // at least valid JSON for the consumer. + fmt.Printf("%s\n", data) + return nil +} + +// Out method for Go templates +func (t StdoutTemplateArray) Out() error { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + if strings.HasPrefix(t.Template, "table") { + // replace any spaces with tabs in template so that tabwriter can align it + t.Template = strings.Replace(strings.TrimSpace(t.Template[5:]), " ", "\t", -1) + headerTmpl, err := template.New("header").Funcs(headerFunctions).Parse(t.Template) + if err != nil { + return errors.Wrapf(err, "Template parsing error") + } + err = headerTmpl.Execute(w, t.Fields) + if err != nil { + return err + } + fmt.Fprintln(w, "") + } + t.Template = strings.Replace(t.Template, " ", "\t", -1) + tmpl, err := template.New("image").Funcs(basicFunctions).Parse(t.Template) + if err != nil { + return errors.Wrapf(err, "Template parsing error") + } + for _, img := range t.Output { + basicTmpl := tmpl.Funcs(basicFunctions) + err = basicTmpl.Execute(w, img) + if err != nil { + return err + } + fmt.Fprintln(w, "") + } + return w.Flush() +} + +// Out method for JSON struct +func (j JSONStruct) Out() error { + data, err := json.MarshalIndent(j.Output, "", " ") + if err != nil { + return err + } + fmt.Printf("%s\n", data) + return nil +} + +//Out method for Go templates +func (t StdoutTemplate) Out() error { + tmpl, err := template.New("image").Parse(t.Template) + if err != nil { + return errors.Wrapf(err, "template parsing error") + } + err = tmpl.Execute(os.Stdout, t.Output) + if err != nil { + return err + } + fmt.Println() + return nil +} + +// Out method for YAML +func (y YAMLStruct) Out() error { + var buf []byte + var err error + buf, err = yaml.Marshal(y.Output) + if err != nil { + return err + } + fmt.Println(string(buf)) + return nil +} diff --git a/cmd/kpod/formats/templates.go b/cmd/kpod/formats/templates.go new file mode 100644 index 000000000..c2582552a --- /dev/null +++ b/cmd/kpod/formats/templates.go @@ -0,0 +1,78 @@ +package formats + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" +) + +// basicFunctions are the set of initial +// functions provided to every template. +var basicFunctions = template.FuncMap{ + "json": func(v interface{}) string { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + _ = enc.Encode(v) + // Remove the trailing new line added by the encoder + return strings.TrimSpace(buf.String()) + }, + "split": strings.Split, + "join": strings.Join, + "title": strings.Title, + "lower": strings.ToLower, + "upper": strings.ToUpper, + "pad": padWithSpace, + "truncate": truncateWithLength, +} + +// HeaderFunctions are used to created headers of a table. +// This is a replacement of basicFunctions for header generation +// because we want the header to remain intact. +// Some functions like `split` are irrelevant so not added. +var headerFunctions = template.FuncMap{ + "json": func(v string) string { + return v + }, + "title": func(v string) string { + return v + }, + "lower": func(v string) string { + return v + }, + "upper": func(v string) string { + return v + }, + "truncate": func(v string, l int) string { + return v + }, +} + +// Parse creates a new anonymous template with the basic functions +// and parses the given format. +func Parse(format string) (*template.Template, error) { + return NewParse("", format) +} + +// NewParse creates a new tagged template with the basic functions +// and parses the given format. +func NewParse(tag, format string) (*template.Template, error) { + return template.New(tag).Funcs(basicFunctions).Parse(format) +} + +// padWithSpace adds whitespace to the input if the input is non-empty +func padWithSpace(source string, prefix, suffix int) string { + if source == "" { + return source + } + return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix) +} + +// truncateWithLength truncates the source string up to the length provided by the input +func truncateWithLength(source string, length int) string { + if len(source) < length { + return source + } + return source[:length] +} diff --git a/cmd/kpod/history.go b/cmd/kpod/history.go new file mode 100644 index 000000000..dd0da38a6 --- /dev/null +++ b/cmd/kpod/history.go @@ -0,0 +1,243 @@ +package main + +import ( + "reflect" + "strconv" + "strings" + "time" + + "github.com/containers/image/types" + units "github.com/docker/go-units" + "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" + "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +const ( + createdByTruncLength = 45 + idTruncLength = 13 +) + +// historyTemplateParams stores info about each layer +type historyTemplateParams struct { + ID string + Created string + CreatedBy string + Size string + Comment string +} + +// historyJSONParams is only used when the JSON format is specified, +// and is better for data processing from JSON. +// historyJSONParams will be populated by data from v1.History and types.BlobInfo, +// the members of the struct are the sama data types as their sources. +type historyJSONParams struct { + ID string `json:"id"` + Created *time.Time `json:"created"` + CreatedBy string `json:"createdBy"` + Size int64 `json:"size"` + Comment string `json:"comment"` +} + +// historyOptions stores cli flag values +type historyOptions struct { + human bool + noTrunc bool + quiet bool + format string +} + +var ( + historyFlags = []cli.Flag{ + cli.BoolTFlag{ + Name: "human, H", + Usage: "Display sizes and dates in human readable format", + }, + cli.BoolFlag{ + Name: "no-trunc, notruncate", + Usage: "Do not truncate the output", + }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "Display the numeric IDs only", + }, + cli.StringFlag{ + Name: "format", + Usage: "Change the output to JSON or a Go template", + }, + } + + historyDescription = "Displays the history of an image. The information can be printed out in an easy to read, " + + "or user specified format, and can be truncated." + historyCommand = cli.Command{ + Name: "history", + Usage: "Show history of a specified image", + Description: historyDescription, + Flags: historyFlags, + Action: historyCmd, + ArgsUsage: "", + } +) + +func historyCmd(c *cli.Context) error { + if err := validateFlags(c, historyFlags); err != nil { + return err + } + + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "Could not get config") + } + defer runtime.Shutdown(false) + + format := genHistoryFormat(c.Bool("quiet")) + if c.IsSet("format") { + format = c.String("format") + } + + args := c.Args() + if len(args) == 0 { + return errors.Errorf("an image name must be specified") + } + if len(args) > 1 { + return errors.Errorf("Kpod history takes at most 1 argument") + } + imgName := args[0] + + opts := historyOptions{ + human: c.BoolT("human"), + noTrunc: c.Bool("no-trunc"), + quiet: c.Bool("quiet"), + format: format, + } + + history, layers, imageID, err := runtime.GetHistory(imgName) + if err != nil { + return errors.Wrapf(err, "error getting history of image %q", imgName) + } + + return generateHistoryOutput(history, layers, imageID, opts) +} + +func genHistoryFormat(quiet bool) (format string) { + if quiet { + return formats.IDString + } + return "table {{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\t" +} + +// historyToGeneric makes an empty array of interfaces for output +func historyToGeneric(templParams []historyTemplateParams, JSONParams []historyJSONParams) (genericParams []interface{}) { + if len(templParams) > 0 { + for _, v := range templParams { + genericParams = append(genericParams, interface{}(v)) + } + return + } + for _, v := range JSONParams { + genericParams = append(genericParams, interface{}(v)) + } + return +} + +// generate the header based on the template provided +func (h *historyTemplateParams) headerMap() map[string]string { + v := reflect.Indirect(reflect.ValueOf(h)) + values := make(map[string]string) + for h := 0; h < v.NumField(); h++ { + key := v.Type().Field(h).Name + value := key + values[key] = strings.ToUpper(splitCamelCase(value)) + } + return values +} + +// getHistorytemplateOutput gets the modified history information to be printed in human readable format +func getHistoryTemplateOutput(history []v1.History, layers []types.BlobInfo, imageID string, opts historyOptions) (historyOutput []historyTemplateParams) { + var ( + outputSize string + createdTime string + createdBy string + count = 1 + ) + for i := len(history) - 1; i >= 0; i-- { + if i != len(history)-1 { + imageID = "" + } + if !opts.noTrunc && i == len(history)-1 { + imageID = imageID[:idTruncLength] + } + + var size int64 + if !history[i].EmptyLayer { + size = layers[len(layers)-count].Size + count++ + } + + if opts.human { + createdTime = units.HumanDuration(time.Since((*history[i].Created))) + " ago" + outputSize = units.HumanSize(float64(size)) + } else { + createdTime = (history[i].Created).Format(time.RFC3339) + outputSize = strconv.FormatInt(size, 10) + } + + createdBy = strings.Join(strings.Fields(history[i].CreatedBy), " ") + if !opts.noTrunc && len(createdBy) > createdByTruncLength { + createdBy = createdBy[:createdByTruncLength-3] + "..." + } + + params := historyTemplateParams{ + ID: imageID, + Created: createdTime, + CreatedBy: createdBy, + Size: outputSize, + Comment: history[i].Comment, + } + historyOutput = append(historyOutput, params) + } + return +} + +// getHistoryJSONOutput returns the history information in its raw form +func getHistoryJSONOutput(history []v1.History, layers []types.BlobInfo, imageID string) (historyOutput []historyJSONParams) { + count := 1 + for i := len(history) - 1; i >= 0; i-- { + var size int64 + if !history[i].EmptyLayer { + size = layers[len(layers)-count].Size + count++ + } + + params := historyJSONParams{ + ID: imageID, + Created: history[i].Created, + CreatedBy: history[i].CreatedBy, + Size: size, + Comment: history[i].Comment, + } + historyOutput = append(historyOutput, params) + } + return +} + +// generateHistoryOutput generates the history based on the format given +func generateHistoryOutput(history []v1.History, layers []types.BlobInfo, imageID string, opts historyOptions) error { + if len(history) == 0 { + return nil + } + + var out formats.Writer + + switch opts.format { + case formats.JSONString: + historyOutput := getHistoryJSONOutput(history, layers, imageID) + out = formats.JSONStructArray{Output: historyToGeneric([]historyTemplateParams{}, historyOutput)} + default: + historyOutput := getHistoryTemplateOutput(history, layers, imageID, opts) + out = formats.StdoutTemplateArray{Output: historyToGeneric(historyOutput, []historyJSONParams{}), Template: opts.format, Fields: historyOutput[0].headerMap()} + } + + return formats.Writer(out).Out() +} diff --git a/cmd/kpod/images.go b/cmd/kpod/images.go new file mode 100644 index 000000000..d7824ba3f --- /dev/null +++ b/cmd/kpod/images.go @@ -0,0 +1,330 @@ +package main + +import ( + "fmt" + "reflect" + "strings" + "time" + + "github.com/containers/image/types" + "github.com/containers/storage" + "github.com/docker/go-units" + "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" + "github.com/kubernetes-incubator/cri-o/libpod" + "github.com/kubernetes-incubator/cri-o/libpod/common" + digest "github.com/opencontainers/go-digest" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +type imagesTemplateParams struct { + ID string + Name string + Digest digest.Digest + CreatedAt string + Size string +} + +type imagesJSONParams struct { + ID string `json:"id"` + Name []string `json:"names"` + Digest digest.Digest `json:"digest"` + CreatedAt time.Time `json:"created"` + Size int64 `json:"size"` +} + +type imagesOptions struct { + quiet bool + noHeading bool + noTrunc bool + digests bool + format string +} + +var ( + imagesFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "quiet, q", + Usage: "display only image IDs", + }, + cli.BoolFlag{ + Name: "noheading, n", + Usage: "do not print column headings", + }, + cli.BoolFlag{ + Name: "no-trunc, notruncate", + Usage: "do not truncate output", + }, + cli.BoolFlag{ + Name: "digests", + Usage: "show digests", + }, + cli.StringFlag{ + Name: "format", + Usage: "Change the output format to JSON or a Go template", + }, + cli.StringFlag{ + Name: "filter, f", + Usage: "filter output based on conditions provided (default [])", + }, + } + + imagesDescription = "lists locally stored images." + imagesCommand = cli.Command{ + Name: "images", + Usage: "list images in local storage", + Description: imagesDescription, + Flags: imagesFlags, + Action: imagesCmd, + ArgsUsage: "", + } +) + +func imagesCmd(c *cli.Context) error { + if err := validateFlags(c, imagesFlags); err != nil { + return err + } + + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "Could not get runtime") + } + defer runtime.Shutdown(false) + + var format string + if c.IsSet("format") { + format = c.String("format") + } else { + format = genImagesFormat(c.Bool("quiet"), c.Bool("noheading"), c.Bool("digests")) + } + + opts := imagesOptions{ + quiet: c.Bool("quiet"), + noHeading: c.Bool("noheading"), + noTrunc: c.Bool("no-trunc"), + digests: c.Bool("digests"), + format: format, + } + + var imageInput string + if len(c.Args()) == 1 { + imageInput = c.Args().Get(0) + } + if len(c.Args()) > 1 { + return errors.New("'kpod images' requires at most 1 argument") + } + + params, err := runtime.ParseImageFilter(imageInput, c.String("filter")) + if err != nil { + return errors.Wrapf(err, "error parsing filter") + } + + // generate the different filters + labelFilter := generateImagesFilter(params, "label") + beforeImageFilter := generateImagesFilter(params, "before-image") + sinceImageFilter := generateImagesFilter(params, "since-image") + danglingFilter := generateImagesFilter(params, "dangling") + referenceFilter := generateImagesFilter(params, "reference") + imageInputFilter := generateImagesFilter(params, "image-input") + + images, err := runtime.GetImages(params, labelFilter, beforeImageFilter, sinceImageFilter, danglingFilter, referenceFilter, imageInputFilter) + if err != nil { + return errors.Wrapf(err, "could not get list of images matching filter") + } + + return generateImagesOutput(runtime, images, opts) +} + +func genImagesFormat(quiet, noHeading, digests bool) (format string) { + if quiet { + return formats.IDString + } + format = "table {{.ID}}\t{{.Name}}\t" + if noHeading { + format = "{{.ID}}\t{{.Name}}\t" + } + if digests { + format += "{{.Digest}}\t" + } + format += "{{.CreatedAt}}\t{{.Size}}\t" + return +} + +// imagesToGeneric creates an empty array of interfaces for output +func imagesToGeneric(templParams []imagesTemplateParams, JSONParams []imagesJSONParams) (genericParams []interface{}) { + if len(templParams) > 0 { + for _, v := range templParams { + genericParams = append(genericParams, interface{}(v)) + } + return + } + for _, v := range JSONParams { + genericParams = append(genericParams, interface{}(v)) + } + return +} + +// generate the header based on the template provided +func (i *imagesTemplateParams) headerMap() map[string]string { + v := reflect.Indirect(reflect.ValueOf(i)) + values := make(map[string]string) + + for i := 0; i < v.NumField(); i++ { + key := v.Type().Field(i).Name + value := key + if value == "ID" || value == "Name" { + value = "Image" + value + } + values[key] = strings.ToUpper(splitCamelCase(value)) + } + return values +} + +// getImagesTemplateOutput returns the images information to be printed in human readable format +func getImagesTemplateOutput(runtime *libpod.Runtime, images []*storage.Image, opts imagesOptions) (imagesOutput []imagesTemplateParams) { + var ( + lastID string + ) + for _, img := range images { + if opts.quiet && lastID == img.ID { + continue // quiet should not show the same ID multiple times + } + createdTime := img.Created + + imageID := img.ID + if !opts.noTrunc { + imageID = imageID[:idTruncLength] + } + + imageName := "" + if len(img.Names) > 0 { + imageName = img.Names[0] + } + + info, imageDigest, size, _ := runtime.InfoAndDigestAndSize(*img) + if info != nil { + createdTime = info.Created + } + + params := imagesTemplateParams{ + ID: imageID, + Name: imageName, + Digest: imageDigest, + CreatedAt: units.HumanDuration(time.Since((createdTime))) + " ago", + Size: units.HumanSize(float64(size)), + } + imagesOutput = append(imagesOutput, params) + } + return +} + +// getImagesJSONOutput returns the images information in its raw form +func getImagesJSONOutput(runtime *libpod.Runtime, images []*storage.Image) (imagesOutput []imagesJSONParams) { + for _, img := range images { + createdTime := img.Created + + info, imageDigest, size, _ := runtime.InfoAndDigestAndSize(*img) + if info != nil { + createdTime = info.Created + } + + params := imagesJSONParams{ + ID: img.ID, + Name: img.Names, + Digest: imageDigest, + CreatedAt: createdTime, + Size: size, + } + imagesOutput = append(imagesOutput, params) + } + return +} + +// generateImagesOutput generates the images based on the format provided +func generateImagesOutput(runtime *libpod.Runtime, images []*storage.Image, opts imagesOptions) error { + if len(images) == 0 { + return nil + } + + var out formats.Writer + + switch opts.format { + case formats.JSONString: + imagesOutput := getImagesJSONOutput(runtime, images) + out = formats.JSONStructArray{Output: imagesToGeneric([]imagesTemplateParams{}, imagesOutput)} + default: + imagesOutput := getImagesTemplateOutput(runtime, images, opts) + out = formats.StdoutTemplateArray{Output: imagesToGeneric(imagesOutput, []imagesJSONParams{}), Template: opts.format, Fields: imagesOutput[0].headerMap()} + + } + + return formats.Writer(out).Out() +} + +// generateImagesFilter returns an ImageFilter based on filterType +// to add more filters, define a new case and write what the ImageFilter function should do +func generateImagesFilter(params *libpod.ImageFilterParams, filterType string) libpod.ImageFilter { + switch filterType { + case "label": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.Label == "" { + return true + } + + pair := strings.SplitN(params.Label, "=", 2) + if val, ok := info.Labels[pair[0]]; ok { + if len(pair) == 2 && val == pair[1] { + return true + } + if len(pair) == 1 { + return true + } + } + return false + } + case "before-image": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.BeforeImage.IsZero() { + return true + } + return info.Created.Before(params.BeforeImage) + } + case "since-image": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.SinceImage.IsZero() { + return true + } + return info.Created.After(params.SinceImage) + } + case "dangling": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.Dangling == "" { + return true + } + if common.IsFalse(params.Dangling) && params.ImageName != "" { + return true + } + if common.IsTrue(params.Dangling) && params.ImageName == "" { + return true + } + return false + } + case "reference": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.ReferencePattern == "" { + return true + } + return libpod.MatchesReference(params.ImageName, params.ReferencePattern) + } + case "image-input": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.ImageInput == "" { + return true + } + return libpod.MatchesReference(params.ImageName, params.ImageInput) + } + default: + fmt.Println("invalid filter type", filterType) + return nil + } +} diff --git a/cmd/kpod/info.go b/cmd/kpod/info.go new file mode 100644 index 000000000..22ca74c73 --- /dev/null +++ b/cmd/kpod/info.go @@ -0,0 +1,200 @@ +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "runtime" + + "github.com/docker/docker/pkg/system" + "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + infoDescription = "display system information" + infoCommand = cli.Command{ + Name: "info", + Usage: infoDescription, + Description: `Information display here pertain to the host, current storage stats, and build of kpod. Useful for the user and when reporting issues.`, + Flags: infoFlags, + Action: infoCmd, + ArgsUsage: "", + } + infoFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "debug, D", + Usage: "display additional debug information", + }, + cli.StringFlag{ + Name: "format", + Usage: "Change the output format to JSON or a Go template", + }, + } +) + +func infoCmd(c *cli.Context) error { + if err := validateFlags(c, infoFlags); err != nil { + return err + } + info := map[string]interface{}{} + + infoGivers := []infoGiverFunc{ + storeInfo, + hostInfo, + } + + if c.Bool("debug") { + infoGivers = append(infoGivers, debugInfo) + } + + for _, giver := range infoGivers { + thisName, thisInfo, err := giver(c) + if err != nil { + info[thisName] = infoErr(err) + continue + } + info[thisName] = thisInfo + } + + var out formats.Writer + infoOutputFormat := c.String("format") + switch infoOutputFormat { + case formats.JSONString: + out = formats.JSONStruct{Output: info} + case "": + out = formats.YAMLStruct{Output: info} + default: + out = formats.StdoutTemplate{Output: info, Template: infoOutputFormat} + } + + formats.Writer(out).Out() + + return nil +} + +func infoErr(err error) map[string]interface{} { + return map[string]interface{}{ + "error": err.Error(), + } +} + +type infoGiverFunc func(c *cli.Context) (name string, info map[string]interface{}, err error) + +// top-level "debug" info +func debugInfo(c *cli.Context) (string, map[string]interface{}, error) { + info := map[string]interface{}{} + info["compiler"] = runtime.Compiler + info["go version"] = runtime.Version() + info["kpod version"] = c.App.Version + info["git commit"] = gitCommit + return "debug", info, nil +} + +// top-level "host" info +func hostInfo(c *cli.Context) (string, map[string]interface{}, error) { + // lets say OS, arch, number of cpus, amount of memory, maybe os distribution/version, hostname, kernel version, uptime + info := map[string]interface{}{} + info["os"] = runtime.GOOS + info["arch"] = runtime.GOARCH + info["cpus"] = runtime.NumCPU() + mi, err := system.ReadMemInfo() + if err != nil { + info["meminfo"] = infoErr(err) + } else { + // TODO this might be a place for github.com/dustin/go-humanize + info["MemTotal"] = mi.MemTotal + info["MemFree"] = mi.MemFree + info["SwapTotal"] = mi.SwapTotal + info["SwapFree"] = mi.SwapFree + } + if kv, err := readKernelVersion(); err != nil { + info["kernel"] = infoErr(err) + } else { + info["kernel"] = kv + } + + if up, err := readUptime(); err != nil { + info["uptime"] = infoErr(err) + } else { + info["uptime"] = up + } + if host, err := os.Hostname(); err != nil { + info["hostname"] = infoErr(err) + } else { + info["hostname"] = host + } + return "host", info, nil +} + +// top-level "store" info +func storeInfo(c *cli.Context) (string, map[string]interface{}, error) { + storeStr := "store" + config, err := getConfig(c) + if err != nil { + return storeStr, nil, errors.Wrapf(err, "Could not get config") + } + store, err := getStore(config) + if err != nil { + return storeStr, nil, err + } + + // lets say storage driver in use, number of images, number of containers + info := map[string]interface{}{} + info["GraphRoot"] = store.GraphRoot() + info["RunRoot"] = store.RunRoot() + info["GraphDriverName"] = store.GraphDriverName() + info["GraphOptions"] = store.GraphOptions() + statusPairs, err := store.Status() + if err != nil { + return storeStr, nil, err + } + status := map[string]string{} + for _, pair := range statusPairs { + status[pair[0]] = pair[1] + } + info["GraphStatus"] = status + images, err := store.Images() + if err != nil { + info["ImageStore"] = infoErr(err) + } else { + info["ImageStore"] = map[string]interface{}{ + "number": len(images), + } + } + containers, err := store.Containers() + if err != nil { + info["ContainerStore"] = infoErr(err) + } else { + info["ContainerStore"] = map[string]interface{}{ + "number": len(containers), + } + } + return storeStr, info, nil +} + +func readKernelVersion() (string, error) { + buf, err := ioutil.ReadFile("/proc/version") + if err != nil { + return "", err + } + f := bytes.Fields(buf) + if len(f) < 2 { + return string(bytes.TrimSpace(buf)), nil + } + return string(f[2]), nil +} + +func readUptime() (string, error) { + buf, err := ioutil.ReadFile("/proc/uptime") + if err != nil { + return "", err + } + f := bytes.Fields(buf) + if len(f) < 1 { + return "", fmt.Errorf("invalid uptime") + } + return string(f[0]), nil +} diff --git a/cmd/kpod/inspect.go b/cmd/kpod/inspect.go new file mode 100644 index 000000000..45e9d7e18 --- /dev/null +++ b/cmd/kpod/inspect.go @@ -0,0 +1,120 @@ +package main + +import ( + "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/kubernetes-incubator/cri-o/libpod/images" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +const ( + 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", + Usage: "Change the output format to a 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") + } + if err := validateFlags(c, inspectFlags); err != nil { + return err + } + + itemType := c.String("type") + size := c.Bool("size") + + switch itemType { + case inspectTypeContainer: + case inspectTypeImage: + case inspectAll: + default: + return errors.Errorf("the only recognized types are %q, %q, and %q", inspectTypeContainer, inspectTypeImage, inspectAll) + } + + name := args[0] + + 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") + } + defer server.Shutdown() + if err = server.Update(); err != nil { + return errors.Wrapf(err, "could not update list of containers") + } + + outputFormat := c.String("format") + var data interface{} + switch itemType { + case inspectTypeContainer: + data, err = server.GetContainerData(name, size) + if err != nil { + return errors.Wrapf(err, "error parsing container data") + } + case inspectTypeImage: + data, err = images.GetData(server.Store(), name) + if err != nil { + return errors.Wrapf(err, "error parsing image data") + } + case inspectAll: + ctrData, err := server.GetContainerData(name, size) + if err != nil { + imgData, err := images.GetData(server.Store(), name) + if err != nil { + return errors.Wrapf(err, "error parsing container or image data") + } + data = imgData + + } else { + data = ctrData + } + } + + var out formats.Writer + if outputFormat != "" && outputFormat != formats.JSONString { + //template + out = formats.StdoutTemplate{Output: data, Template: outputFormat} + } else { + // default is json output + out = formats.JSONStruct{Output: data} + } + + formats.Writer(out).Out() + return nil +} diff --git a/cmd/kpod/kill.go b/cmd/kpod/kill.go new file mode 100644 index 000000000..8a5500031 --- /dev/null +++ b/cmd/kpod/kill.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "os" + + "github.com/docker/docker/pkg/signal" + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + killFlags = []cli.Flag{ + cli.StringFlag{ + Name: "signal, s", + Usage: "Signal to send to the container", + Value: "KILL", + }, + } + killDescription = "The main process inside each container specified will be sent SIGKILL, or any signal specified with option --signal." + killCommand = cli.Command{ + Name: "kill", + Usage: "Kill one or more running containers with a specific signal", + Description: killDescription, + Flags: killFlags, + Action: killCmd, + ArgsUsage: "[CONTAINER_NAME_OR_ID]", + } +) + +// killCmd kills one or more containers with a signal +func killCmd(c *cli.Context) error { + args := c.Args() + if len(args) == 0 { + return errors.Errorf("specify one or more containers to kill") + } + if err := validateFlags(c, killFlags); err != nil { + return err + } + 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") + } + killSignal := c.String("signal") + // Check if the signalString provided by the user is valid + // Invalid signals will return err + sysSignal, err := signal.ParseSignal(killSignal) + if err != nil { + return err + } + defer server.Shutdown() + err = server.Update() + if err != nil { + return errors.Wrapf(err, "could not update list of containers") + } + var lastError error + for _, container := range c.Args() { + id, err := server.ContainerKill(container, sysSignal) + if err != nil { + if lastError != nil { + fmt.Fprintln(os.Stderr, lastError) + } + lastError = errors.Wrapf(err, "unable to kill %v", container) + } else { + fmt.Println(id) + } + } + return lastError +} diff --git a/cmd/kpod/load.go b/cmd/kpod/load.go new file mode 100644 index 000000000..b1cff1e88 --- /dev/null +++ b/cmd/kpod/load.go @@ -0,0 +1,116 @@ +package main + +import ( + "io" + "io/ioutil" + "os" + + "github.com/kubernetes-incubator/cri-o/libpod" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + loadFlags = []cli.Flag{ + cli.StringFlag{ + Name: "input, i", + Usage: "Read from archive file, default is STDIN", + Value: "/dev/stdin", + }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "Suppress the output", + }, + cli.StringFlag{ + Name: "signature-policy", + Usage: "`pathname` of signature policy file (not usually used)", + }, + } + loadDescription = "Loads the image from docker-archive stored on the local machine." + loadCommand = cli.Command{ + Name: "load", + Usage: "load an image from docker archive", + Description: loadDescription, + Flags: loadFlags, + Action: loadCmd, + ArgsUsage: "", + } +) + +// loadCmd gets the image/file to be loaded from the command line +// and calls loadImage to load the image to containers-storage +func loadCmd(c *cli.Context) error { + + args := c.Args() + var image string + if len(args) == 1 { + image = args[0] + } + if len(args) > 1 { + return errors.New("too many arguments. Requires exactly 1") + } + if err := validateFlags(c, loadFlags); err != nil { + return err + } + + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + input := c.String("input") + + if input == "/dev/stdin" { + fi, err := os.Stdin.Stat() + if err != nil { + return err + } + // checking if loading from pipe + if !fi.Mode().IsRegular() { + outFile, err := ioutil.TempFile("/var/tmp", "kpod") + if err != nil { + return errors.Errorf("error creating file %v", err) + } + defer outFile.Close() + defer os.Remove(outFile.Name()) + + inFile, err := os.OpenFile(input, 0, 0666) + if err != nil { + return errors.Errorf("error reading file %v", err) + } + defer inFile.Close() + + _, err = io.Copy(outFile, inFile) + if err != nil { + return errors.Errorf("error copying file %v", err) + } + + input = outFile.Name() + } + } + + var writer io.Writer + if !c.Bool("quiet") { + writer = os.Stdout + } + + options := libpod.CopyOptions{ + SignaturePolicyPath: c.String("signature-policy"), + Writer: writer, + } + + src := libpod.DockerArchive + ":" + input + if err := runtime.PullImage(src, options); err != nil { + src = libpod.OCIArchive + ":" + input + // generate full src name with specified image:tag + if image != "" { + src = src + ":" + image + } + if err := runtime.PullImage(src, options); err != nil { + return errors.Wrapf(err, "error pulling %q", src) + } + } + + return nil +} diff --git a/cmd/kpod/login.go b/cmd/kpod/login.go new file mode 100644 index 000000000..17880f7a7 --- /dev/null +++ b/cmd/kpod/login.go @@ -0,0 +1,110 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + + "github.com/containers/image/docker" + "github.com/containers/image/pkg/docker/config" + "github.com/kubernetes-incubator/cri-o/libpod/common" + "github.com/pkg/errors" + "github.com/urfave/cli" + "golang.org/x/crypto/ssh/terminal" +) + +var ( + loginFlags = []cli.Flag{ + cli.StringFlag{ + Name: "password, p", + Usage: "Password for registry", + }, + cli.StringFlag{ + Name: "username, u", + Usage: "Username for registry", + }, + cli.StringFlag{ + Name: "authfile", + Usage: "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + }, + } + loginDescription = "Login to a container registry on a specified server." + loginCommand = cli.Command{ + Name: "login", + Usage: "login to a container registry", + Description: loginDescription, + Flags: loginFlags, + Action: loginCmd, + ArgsUsage: "REGISTRY", + } +) + +// loginCmd uses the authentication package to store a user's authenticated credentials +// in an auth.json file for future use +func loginCmd(c *cli.Context) error { + args := c.Args() + if len(args) > 1 { + return errors.Errorf("too many arguments, login takes only 1 argument") + } + if len(args) == 0 { + return errors.Errorf("registry must be given") + } + var server string + if len(args) == 1 { + server = args[0] + } + + sc := common.GetSystemContext("", c.String("authfile")) + + // username of user logged in to server (if one exists) + userFromAuthFile := config.GetUserLoggedIn(sc, server) + username, password, err := getUserAndPass(c.String("username"), c.String("password"), userFromAuthFile) + if err != nil { + return errors.Wrapf(err, "error getting username and password") + } + + if err = docker.CheckAuth(context.TODO(), sc, username, password, server); err == nil { + if err := config.SetAuthentication(sc, server, username, password); err != nil { + return err + } + } + switch err { + case nil: + fmt.Println("Login Succeeded!") + return nil + case docker.ErrUnauthorizedForCredentials: + return errors.Errorf("error logging into %q: invalid username/password\n", server) + default: + return errors.Wrapf(err, "error authenticating creds for %q", server) + } +} + +// getUserAndPass gets the username and password from STDIN if not given +// using the -u and -p flags +func getUserAndPass(username, password, userFromAuthFile string) (string, string, error) { + var err error + reader := bufio.NewReader(os.Stdin) + if username == "" { + if userFromAuthFile != "" { + fmt.Printf("Username (%s): ", userFromAuthFile) + } else { + fmt.Print("Username: ") + } + username, err = reader.ReadString('\n') + if err != nil { + return "", "", errors.Wrapf(err, "error reading username") + } + } + if password == "" { + fmt.Print("Password: ") + pass, err := terminal.ReadPassword(0) + if err != nil { + return "", "", errors.Wrapf(err, "error reading password") + } + password = string(pass) + fmt.Println() + } + return strings.TrimSpace(username), password, err +} diff --git a/cmd/kpod/logout.go b/cmd/kpod/logout.go new file mode 100644 index 000000000..587346151 --- /dev/null +++ b/cmd/kpod/logout.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + + "github.com/containers/image/pkg/docker/config" + "github.com/kubernetes-incubator/cri-o/libpod/common" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + logoutFlags = []cli.Flag{ + cli.StringFlag{ + Name: "authfile", + Usage: "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + }, + cli.BoolFlag{ + Name: "all, a", + Usage: "Remove the cached credentials for all registries in the auth file", + }, + } + logoutDescription = "Remove the cached username and password for the registry." + logoutCommand = cli.Command{ + Name: "logout", + Usage: "logout of a container registry", + Description: logoutDescription, + Flags: logoutFlags, + Action: logoutCmd, + ArgsUsage: "REGISTRY", + } +) + +// logoutCmd uses the authentication package to remove the authenticated of a registry +// stored in the auth.json file +func logoutCmd(c *cli.Context) error { + args := c.Args() + if len(args) > 1 { + return errors.Errorf("too many arguments, logout takes only 1 argument") + } + if len(args) == 0 { + return errors.Errorf("registry must be given") + } + var server string + if len(args) == 1 { + server = args[0] + } + + sc := common.GetSystemContext("", c.String("authfile")) + + if c.Bool("all") { + if err := config.RemoveAllAuthentication(sc); err != nil { + return err + } + fmt.Println("Remove login credentials for all registries") + return nil + } + + err := config.RemoveAuthentication(sc, server) + switch err { + case nil: + fmt.Printf("Remove login credentials for %s\n", server) + return nil + case config.ErrNotLoggedIn: + return errors.Errorf("Not logged into %s\n", server) + default: + return errors.Wrapf(err, "error logging out of %q", server) + } +} diff --git a/cmd/kpod/logs.go b/cmd/kpod/logs.go new file mode 100644 index 000000000..60be4792e --- /dev/null +++ b/cmd/kpod/logs.go @@ -0,0 +1,92 @@ +package main + +import ( + "fmt" + "time" + + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + logsFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "details", + Usage: "Show extra details provided to the logs", + Hidden: true, + }, + cli.BoolFlag{ + Name: "follow, f", + Usage: "Follow log output. The default is false", + }, + cli.StringFlag{ + Name: "since", + Usage: "Show logs since TIMESTAMP", + }, + cli.Uint64Flag{ + Name: "tail", + Usage: "Output the specified number of LINES at the end of the logs. Defaults to 0, which prints all lines", + }, + } + logsDescription = "The kpod logs command batch-retrieves whatever logs are present for a container at the time of execution. This does not guarantee execution" + + "order when combined with kpod run (i.e. your run may not have generated any logs at the time you execute kpod logs" + logsCommand = cli.Command{ + Name: "logs", + Usage: "Fetch the logs of a container", + Description: logsDescription, + Flags: logsFlags, + Action: logsCmd, + ArgsUsage: "CONTAINER", + } +) + +func logsCmd(c *cli.Context) error { + args := c.Args() + if len(args) != 1 { + return errors.Errorf("'kpod logs' requires exactly one container name/ID") + } + if err := validateFlags(c, logsFlags); err != nil { + return err + } + container := c.Args().First() + var opts libkpod.LogOptions + opts.Details = c.Bool("details") + opts.Follow = c.Bool("follow") + opts.SinceTime = time.Time{} + if c.IsSet("since") { + // parse time, error out if something is wrong + since, err := time.Parse("2006-01-02T15:04:05.999999999-07:00", c.String("since")) + if err != nil { + return errors.Wrapf(err, "could not parse time: %q", c.String("since")) + } + opts.SinceTime = since + } + opts.Tail = c.Uint64("tail") + + 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 create container server") + } + defer server.Shutdown() + err = server.Update() + if err != nil { + return errors.Wrapf(err, "could not update list of containers") + } + logs := make(chan string) + go func() { + err = server.GetLogs(container, logs, opts) + }() + printLogs(logs) + return err +} + +func printLogs(logs chan string) { + for line := range logs { + fmt.Println(line) + } +} diff --git a/cmd/kpod/main.go b/cmd/kpod/main.go new file mode 100644 index 000000000..7745fbf3d --- /dev/null +++ b/cmd/kpod/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + "os" + + "github.com/containers/storage/pkg/reexec" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +// This is populated by the Makefile from the VERSION file +// in the repository +var kpodVersion = "" + +func main() { + debug := false + + if reexec.Init() { + return + } + + app := cli.NewApp() + app.Name = "kpod" + app.Usage = "manage pods and images" + + var v string + if kpodVersion != "" { + v = kpodVersion + } + app.Version = v + + app.Commands = []cli.Command{ + diffCommand, + exportCommand, + historyCommand, + imagesCommand, + infoCommand, + inspectCommand, + killCommand, + loadCommand, + loginCommand, + logoutCommand, + logsCommand, + mountCommand, + pauseCommand, + psCommand, + pullCommand, + pushCommand, + renameCommand, + rmCommand, + rmiCommand, + saveCommand, + statsCommand, + stopCommand, + tagCommand, + umountCommand, + unpauseCommand, + versionCommand, + waitCommand, + } + app.Before = func(c *cli.Context) error { + logLevel := c.GlobalString("log-level") + if logLevel != "" { + level, err := logrus.ParseLevel(logLevel) + if err != nil { + return err + } + + logrus.SetLevel(level) + } + + if logLevel == "debug" { + debug = true + + } + + return nil + } + app.After = func(*cli.Context) error { + // called by Run() when the command handler succeeds + shutdownStores() + return nil + } + cli.OsExiter = func(code int) { + // called by Run() when the command fails, bypassing After() + shutdownStores() + os.Exit(code) + } + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "config, c", + Usage: "path of a config file detailing container server configuration options", + }, + cli.StringFlag{ + Name: "log-level", + Usage: "log messages above specified level: debug, info, warn, error (default), fatal or panic", + Value: "error", + }, + cli.StringFlag{ + Name: "root", + Usage: "path to the root directory in which data, including images, is stored", + }, + cli.StringFlag{ + Name: "runroot", + Usage: "path to the 'run directory' where all state information is stored", + }, + cli.StringFlag{ + Name: "runtime", + Usage: "path to the OCI-compatible binary used to run containers, default is /usr/bin/runc", + }, + cli.StringFlag{ + Name: "storage-driver, s", + Usage: "select which storage driver is used to manage storage of images and containers (default is overlay)", + }, + cli.StringSliceFlag{ + Name: "storage-opt", + Usage: "used to pass an option to the storage driver", + }, + } + if err := app.Run(os.Args); err != nil { + if debug { + logrus.Errorf(err.Error()) + } else { + fmt.Fprintln(os.Stderr, err.Error()) + } + cli.OsExiter(1) + } +} diff --git a/cmd/kpod/mount.go b/cmd/kpod/mount.go new file mode 100644 index 000000000..a711bedea --- /dev/null +++ b/cmd/kpod/mount.go @@ -0,0 +1,121 @@ +package main + +import ( + js "encoding/json" + "fmt" + + of "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + mountDescription = ` + kpod mount + Lists all mounted containers mount points + + kpod mount CONTAINER-NAME-OR-ID + Mounts the specified container and outputs the mountpoint +` + + mountFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "notruncate", + Usage: "do not truncate output", + }, + cli.StringFlag{ + Name: "label", + Usage: "SELinux label for the mount point", + }, + cli.StringFlag{ + Name: "format", + Usage: "Change the output format to Go template", + }, + } + mountCommand = cli.Command{ + Name: "mount", + Usage: "Mount a working container's root filesystem", + Description: mountDescription, + Action: mountCmd, + ArgsUsage: "[CONTAINER-NAME-OR-ID]", + Flags: mountFlags, + } +) + +// MountOutputParams stores info about each layer +type jsonMountPoint struct { + ID string `json:"id"` + Names []string `json:"names"` + MountPoint string `json:"mountpoint"` +} + +func mountCmd(c *cli.Context) error { + formats := map[string]bool{ + "": true, + of.JSONString: true, + } + + args := c.Args() + json := c.String("format") == of.JSONString + if !formats[c.String("format")] { + return errors.Errorf("%q is not a supported format", c.String("format")) + } + + if len(args) > 1 { + return errors.Errorf("too many arguments specified") + } + if err := validateFlags(c, mountFlags); err != nil { + return err + } + config, err := getConfig(c) + if err != nil { + return errors.Wrapf(err, "Could not get config") + } + store, err := getStore(config) + if err != nil { + return errors.Wrapf(err, "error getting store") + } + if len(args) == 1 { + if json { + return errors.Wrapf(err, "json option can not be used with a container id") + } + mountPoint, err := store.Mount(args[0], c.String("label")) + if err != nil { + return errors.Wrapf(err, "error finding container %q", args[0]) + } + fmt.Printf("%s\n", mountPoint) + } else { + jsonMountPoints := []jsonMountPoint{} + containers, err2 := store.Containers() + if err2 != nil { + return errors.Wrapf(err2, "error reading list of all containers") + } + for _, container := range containers { + layer, err := store.Layer(container.LayerID) + if err != nil { + return errors.Wrapf(err, "error finding layer %q for container %q", container.LayerID, container.ID) + } + if layer.MountPoint == "" { + continue + } + if json { + jsonMountPoints = append(jsonMountPoints, jsonMountPoint{ID: container.ID, Names: container.Names, MountPoint: layer.MountPoint}) + continue + } + + if c.Bool("notruncate") { + fmt.Printf("%-64s %s\n", container.ID, layer.MountPoint) + } else { + fmt.Printf("%-12.12s %s\n", container.ID, layer.MountPoint) + } + } + if json { + data, err := js.MarshalIndent(jsonMountPoints, "", " ") + if err != nil { + return err + } + fmt.Printf("%s\n", data) + } + } + return nil +} diff --git a/cmd/kpod/pause.go b/cmd/kpod/pause.go new file mode 100644 index 000000000..5a8229ebe --- /dev/null +++ b/cmd/kpod/pause.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/pkg/errors" + "github.com/urfave/cli" + "os" +) + +var ( + pauseDescription = ` + kpod pause + + Pauses one or more running containers. The container name or ID can be used. +` + pauseCommand = cli.Command{ + Name: "pause", + Usage: "Pauses all the processes in one or more containers", + Description: pauseDescription, + Action: pauseCmd, + ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]", + } +) + +func pauseCmd(c *cli.Context) error { + args := c.Args() + if len(args) < 1 { + return errors.Errorf("you must provide at least one container name or id") + } + + 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") + } + defer server.Shutdown() + if err := server.Update(); err != nil { + return errors.Wrapf(err, "could not update list of containers") + } + var lastError error + for _, container := range c.Args() { + cid, err := server.ContainerPause(container) + if err != nil { + if lastError != nil { + fmt.Fprintln(os.Stderr, lastError) + } + lastError = errors.Wrapf(err, "failed to pause container %v", container) + } else { + fmt.Println(cid) + } + } + + return lastError +} diff --git a/cmd/kpod/ps.go b/cmd/kpod/ps.go new file mode 100644 index 000000000..76bc8b8b4 --- /dev/null +++ b/cmd/kpod/ps.go @@ -0,0 +1,665 @@ +package main + +import ( + "os" + "path/filepath" + "reflect" + "regexp" + "strconv" + "strings" + "time" + + "github.com/docker/go-units" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" + + "k8s.io/apimachinery/pkg/fields" + + "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/kubernetes-incubator/cri-o/oci" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +type psOptions struct { + all bool + filter string + format string + last int + latest bool + noTrunc bool + quiet bool + size bool + label string + namespace bool +} + +type psTemplateParams struct { + ID string + Image string + Command string + CreatedAt string + RunningFor string + Status string + Ports string + Size string + Names string + Labels string + Mounts string + PID int + Cgroup string + IPC string + MNT string + NET string + PIDNS string + User string + UTS string +} + +// psJSONParams is only used when the JSON format is specified, +// and is better for data processing from JSON. +// psJSONParams will be populated by data from libkpod.ContainerData, +// the members of the struct are the sama data types as their sources. +type psJSONParams struct { + ID string `json:"id"` + Image string `json:"image"` + ImageID string `json:"image_id"` + Command string `json:"command"` + CreatedAt time.Time `json:"createdAt"` + RunningFor time.Duration `json:"runningFor"` + Status string `json:"status"` + Ports map[string]struct{} `json:"ports"` + Size uint `json:"size"` + Names string `json:"names"` + Labels fields.Set `json:"labels"` + Mounts []specs.Mount `json:"mounts"` + ContainerRunning bool `json:"ctrRunning"` + Namespaces *namespace `json:"namespace,omitempty"` +} + +type namespace struct { + PID string `json:"pid,omitempty"` + Cgroup string `json:"cgroup,omitempty"` + IPC string `json:"ipc,omitempty"` + MNT string `json:"mnt,omitempty"` + NET string `json:"net,omitempty"` + PIDNS string `json:"pidns,omitempty"` + User string `json:"user,omitempty"` + UTS string `json:"uts,omitempty"` +} + +var ( + psFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "all, a", + Usage: "Show all the containers, default is only running containers", + }, + cli.StringFlag{ + Name: "filter, f", + Usage: "Filter output based on conditions given", + }, + cli.StringFlag{ + Name: "format", + Usage: "Pretty-print containers to JSON or using a Go template", + }, + cli.IntFlag{ + Name: "last, n", + Usage: "Print the n last created containers (all states)", + Value: -1, + }, + cli.BoolFlag{ + Name: "latest, l", + Usage: "Show the latest container created (all states)", + }, + cli.BoolFlag{ + Name: "no-trunc", + Usage: "Display the extended information", + }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "Print the numeric IDs of the containers only", + }, + cli.BoolFlag{ + Name: "size, s", + Usage: "Display the total file sizes", + }, + cli.BoolFlag{ + Name: "namespace, ns", + Usage: "Display namespace information", + }, + } + psDescription = "Prints out information about the containers" + psCommand = cli.Command{ + Name: "ps", + Usage: "List containers", + Description: psDescription, + Flags: psFlags, + Action: psCmd, + ArgsUsage: "", + } +) + +func psCmd(c *cli.Context) error { + if err := validateFlags(c, psFlags); err != nil { + return err + } + 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, "error creating server") + } + if err := server.Update(); err != nil { + return errors.Wrapf(err, "error updating list of containers") + } + + if len(c.Args()) > 0 { + return errors.Errorf("too many arguments, ps takes no arguments") + } + + format := genPsFormat(c.Bool("quiet"), c.Bool("size"), c.Bool("namespace")) + if c.IsSet("format") { + format = c.String("format") + } + + opts := psOptions{ + all: c.Bool("all"), + filter: c.String("filter"), + format: format, + last: c.Int("last"), + latest: c.Bool("latest"), + noTrunc: c.Bool("no-trunc"), + quiet: c.Bool("quiet"), + size: c.Bool("size"), + namespace: c.Bool("namespace"), + } + + // all, latest, and last are mutually exclusive. Only one flag can be used at a time + exclusiveOpts := 0 + if opts.last >= 0 { + exclusiveOpts++ + } + if opts.latest { + exclusiveOpts++ + } + if opts.all { + exclusiveOpts++ + } + if exclusiveOpts > 1 { + return errors.Errorf("Last, latest and all are mutually exclusive") + } + + containers, err := server.ListContainers() + if err != nil { + return errors.Wrapf(err, "error getting containers from server") + } + var params *FilterParamsPS + if opts.filter != "" { + params, err = parseFilter(opts.filter, containers) + if err != nil { + return errors.Wrapf(err, "error parsing filter") + } + } else { + params = nil + } + + containerList := getContainersMatchingFilter(containers, params, server) + + return generatePsOutput(containerList, server, opts) +} + +// generate the template based on conditions given +func genPsFormat(quiet, size, namespace bool) (format string) { + if quiet { + return formats.IDString + } + if namespace { + format = "table {{.ID}}\t{{.Names}}\t{{.PID}}\t{{.Cgroup}}\t{{.IPC}}\t{{.MNT}}\t{{.NET}}\t{{.PIDNS}}\t{{.User}}\t{{.UTS}}\t" + return + } + format = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}\t" + if size { + format += "{{.Size}}\t" + } + return +} + +func psToGeneric(templParams []psTemplateParams, JSONParams []psJSONParams) (genericParams []interface{}) { + if len(templParams) > 0 { + for _, v := range templParams { + genericParams = append(genericParams, interface{}(v)) + } + return + } + for _, v := range JSONParams { + genericParams = append(genericParams, interface{}(v)) + } + return +} + +// generate the accurate header based on template given +func (p *psTemplateParams) headerMap() map[string]string { + v := reflect.Indirect(reflect.ValueOf(p)) + values := make(map[string]string) + + for i := 0; i < v.NumField(); i++ { + key := v.Type().Field(i).Name + value := key + if value == "ID" { + value = "Container" + value + } + values[key] = strings.ToUpper(splitCamelCase(value)) + } + return values +} + +// getContainers gets the containers that match the flags given +func getContainers(containers []*libkpod.ContainerData, opts psOptions) []*libkpod.ContainerData { + var containersOutput []*libkpod.ContainerData + if opts.last >= 0 && opts.last < len(containers) { + for i := 0; i < opts.last; i++ { + containersOutput = append(containersOutput, containers[i]) + } + return containersOutput + } + if opts.latest { + return []*libkpod.ContainerData{containers[0]} + } + if opts.all || opts.last >= len(containers) { + return containers + } + for _, ctr := range containers { + if ctr.State.Status == oci.ContainerStateRunning { + containersOutput = append(containersOutput, ctr) + } + } + return containersOutput +} + +// getTemplateOutput returns the modified container information +func getTemplateOutput(containers []*libkpod.ContainerData, opts psOptions) (psOutput []psTemplateParams) { + var status string + for _, ctr := range containers { + ctrID := ctr.ID + runningFor := units.HumanDuration(time.Since(ctr.State.Created)) + createdAt := runningFor + " ago" + command := getStrFromSquareBrackets(ctr.ImageCreatedBy) + imageName := ctr.FromImage + mounts := getMounts(ctr.Mounts, opts.noTrunc) + ports := getPorts(ctr.Config.ExposedPorts) + size := units.HumanSize(float64(ctr.SizeRootFs)) + labels := getLabels(ctr.Labels) + + ns := getNamespaces(ctr.State.Pid) + + switch ctr.State.Status { + case oci.ContainerStateStopped: + status = "Exited (" + strconv.FormatInt(int64(ctr.State.ExitCode), 10) + ") " + runningFor + " ago" + case oci.ContainerStateRunning: + status = "Up " + runningFor + " ago" + case oci.ContainerStatePaused: + status = "Paused" + default: + status = "Created" + } + + if !opts.noTrunc { + ctrID = ctr.ID[:idTruncLength] + imageName = getImageName(ctr.FromImage) + } + + params := psTemplateParams{ + ID: ctrID, + Image: imageName, + Command: command, + CreatedAt: createdAt, + RunningFor: runningFor, + Status: status, + Ports: ports, + Size: size, + Names: ctr.Name, + Labels: labels, + Mounts: mounts, + PID: ctr.State.Pid, + Cgroup: ns.Cgroup, + IPC: ns.IPC, + MNT: ns.MNT, + NET: ns.NET, + PIDNS: ns.PID, + User: ns.User, + UTS: ns.UTS, + } + psOutput = append(psOutput, params) + } + return +} + +func getNamespaces(pid int) *namespace { + ctrPID := strconv.Itoa(pid) + cgroup, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "cgroup")) + ipc, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "ipc")) + mnt, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "mnt")) + net, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "net")) + pidns, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "pid")) + user, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "user")) + uts, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "uts")) + + return &namespace{ + PID: ctrPID, + Cgroup: cgroup, + IPC: ipc, + MNT: mnt, + NET: net, + PIDNS: pidns, + User: user, + UTS: uts, + } +} + +func getNamespaceInfo(path string) (string, error) { + val, err := os.Readlink(path) + if err != nil { + return "", errors.Wrapf(err, "error getting info from %q", path) + } + return getStrFromSquareBrackets(val), nil +} + +// getJSONOutput returns the container info in its raw form +func getJSONOutput(containers []*libkpod.ContainerData, nSpace bool) (psOutput []psJSONParams) { + var ns *namespace + for _, ctr := range containers { + if nSpace { + ns = getNamespaces(ctr.State.Pid) + } + + params := psJSONParams{ + ID: ctr.ID, + Image: ctr.FromImage, + ImageID: ctr.FromImageID, + Command: getStrFromSquareBrackets(ctr.ImageCreatedBy), + CreatedAt: ctr.State.Created, + RunningFor: time.Since(ctr.State.Created), + Status: ctr.State.Status, + Ports: ctr.Config.ExposedPorts, + Size: ctr.SizeRootFs, + Names: ctr.Name, + Labels: ctr.Labels, + Mounts: ctr.Mounts, + ContainerRunning: ctr.State.Status == oci.ContainerStateRunning, + Namespaces: ns, + } + psOutput = append(psOutput, params) + } + return +} + +func generatePsOutput(containers []*libkpod.ContainerData, server *libkpod.ContainerServer, opts psOptions) error { + containersOutput := getContainers(containers, opts) + // In the case of JSON, we want to continue so we at least pass + // {} --valid JSON-- to the consumer + if len(containersOutput) == 0 && opts.format != formats.JSONString { + return nil + } + + var out formats.Writer + + switch opts.format { + case formats.JSONString: + psOutput := getJSONOutput(containersOutput, opts.namespace) + out = formats.JSONStructArray{Output: psToGeneric([]psTemplateParams{}, psOutput)} + default: + psOutput := getTemplateOutput(containersOutput, opts) + out = formats.StdoutTemplateArray{Output: psToGeneric(psOutput, []psJSONParams{}), Template: opts.format, Fields: psOutput[0].headerMap()} + } + + return formats.Writer(out).Out() +} + +// getStrFromSquareBrackets gets the string inside [] from a string +func getStrFromSquareBrackets(cmd string) string { + reg, err := regexp.Compile(".*\\[|\\].*") + if err != nil { + return "" + } + arr := strings.Split(reg.ReplaceAllLiteralString(cmd, ""), ",") + return strings.Join(arr, ",") +} + +// getImageName shortens the image name +func getImageName(img string) string { + arr := strings.Split(img, "/") + if arr[0] == "docker.io" && arr[1] == "library" { + img = strings.Join(arr[2:], "/") + } else if arr[0] == "docker.io" { + img = strings.Join(arr[1:], "/") + } + return img +} + +// getLabels converts the labels to a string of the form "key=value, key2=value2" +func getLabels(labels fields.Set) string { + var arr []string + if len(labels) > 0 { + for key, val := range labels { + temp := key + "=" + val + arr = append(arr, temp) + } + return strings.Join(arr, ",") + } + return "" +} + +// getMounts converts the volumes mounted to a string of the form "mount1, mount2" +// it truncates it if noTrunc is false +func getMounts(mounts []specs.Mount, noTrunc bool) string { + var arr []string + if len(mounts) == 0 { + return "" + } + for _, mount := range mounts { + if noTrunc { + arr = append(arr, mount.Source) + continue + } + tempArr := strings.SplitAfter(mount.Source, "/") + if len(tempArr) >= 3 { + arr = append(arr, strings.Join(tempArr[:3], "")) + } else { + arr = append(arr, mount.Source) + } + } + return strings.Join(arr, ",") +} + +// getPorts converts the ports used to a string of the from "port1, port2" +func getPorts(ports map[string]struct{}) string { + var arr []string + if len(ports) == 0 { + return "" + } + for key := range ports { + arr = append(arr, key) + } + return strings.Join(arr, ",") +} + +// FilterParamsPS contains the filter options for ps +type FilterParamsPS struct { + id string + label string + name string + exited int32 + status string + ancestor string + before time.Time + since time.Time + volume string +} + +// parseFilter takes a filter string and a list of containers and filters it +func parseFilter(filter string, containers []*oci.Container) (*FilterParamsPS, error) { + params := new(FilterParamsPS) + allFilters := strings.Split(filter, ",") + + for _, param := range allFilters { + pair := strings.SplitN(param, "=", 2) + switch strings.TrimSpace(pair[0]) { + case "id": + params.id = pair[1] + case "label": + params.label = pair[1] + case "name": + params.name = pair[1] + case "exited": + exitedCode, err := strconv.ParseInt(pair[1], 10, 32) + if err != nil { + return nil, errors.Errorf("exited code out of range %q", pair[1]) + } + params.exited = int32(exitedCode) + case "status": + params.status = pair[1] + case "ancestor": + params.ancestor = pair[1] + case "before": + if ctr, err := findContainer(containers, pair[1]); err == nil { + params.before = ctr.CreatedAt() + } else { + return nil, errors.Wrapf(err, "no such container %q", pair[1]) + } + case "since": + if ctr, err := findContainer(containers, pair[1]); err == nil { + params.before = ctr.CreatedAt() + } else { + return nil, errors.Wrapf(err, "no such container %q", pair[1]) + } + case "volume": + params.volume = pair[1] + default: + return nil, errors.Errorf("invalid filter %q", pair[0]) + } + } + return params, nil +} + +// findContainer finds a container with a specific name or id from a list of containers +func findContainer(containers []*oci.Container, ref string) (*oci.Container, error) { + for _, ctr := range containers { + if strings.HasPrefix(ctr.ID(), ref) || ctr.Name() == ref { + return ctr, nil + } + } + return nil, errors.Errorf("could not find container") +} + +// matchesFilter checks if a container matches all the filter parameters +func matchesFilter(ctrData *libkpod.ContainerData, params *FilterParamsPS) bool { + if params == nil { + return true + } + if params.id != "" && !matchesID(ctrData, params.id) { + return false + } + if params.name != "" && !matchesName(ctrData, params.name) { + return false + } + if !params.before.IsZero() && !matchesBeforeContainer(ctrData, params.before) { + return false + } + if !params.since.IsZero() && !matchesSinceContainer(ctrData, params.since) { + return false + } + if params.exited > 0 && !matchesExited(ctrData, params.exited) { + return false + } + if params.status != "" && !matchesStatus(ctrData, params.status) { + return false + } + if params.ancestor != "" && !matchesAncestor(ctrData, params.ancestor) { + return false + } + if params.label != "" && !matchesLabel(ctrData, params.label) { + return false + } + if params.volume != "" && !matchesVolume(ctrData, params.volume) { + return false + } + return true +} + +// GetContainersMatchingFilter returns a slice of all the containers that match the provided filter parameters +func getContainersMatchingFilter(containers []*oci.Container, filter *FilterParamsPS, server *libkpod.ContainerServer) []*libkpod.ContainerData { + var filteredCtrs []*libkpod.ContainerData + for _, ctr := range containers { + ctrData, err := server.GetContainerData(ctr.ID(), true) + if err != nil { + logrus.Warn("unable to get container data for matched container") + } + if filter == nil || matchesFilter(ctrData, filter) { + filteredCtrs = append(filteredCtrs, ctrData) + } + } + return filteredCtrs +} + +// matchesID returns true if the id's match +func matchesID(ctrData *libkpod.ContainerData, id string) bool { + return strings.HasPrefix(ctrData.ID, id) +} + +// matchesBeforeContainer returns true if the container was created before the filter image +func matchesBeforeContainer(ctrData *libkpod.ContainerData, beforeTime time.Time) bool { + return ctrData.State.Created.Before(beforeTime) +} + +// matchesSincecontainer returns true if the container was created since the filter image +func matchesSinceContainer(ctrData *libkpod.ContainerData, sinceTime time.Time) bool { + return ctrData.State.Created.After(sinceTime) +} + +// matchesLabel returns true if the container label matches that of the filter label +func matchesLabel(ctrData *libkpod.ContainerData, label string) bool { + pair := strings.SplitN(label, "=", 2) + if val, ok := ctrData.Labels[pair[0]]; ok { + if len(pair) == 2 && val == pair[1] { + return true + } + if len(pair) == 1 { + return true + } + return false + } + return false +} + +// matchesName returns true if the names are identical +func matchesName(ctrData *libkpod.ContainerData, name string) bool { + return ctrData.Name == name +} + +// matchesExited returns true if the exit codes are identical +func matchesExited(ctrData *libkpod.ContainerData, exited int32) bool { + return ctrData.State.ExitCode == exited +} + +// matchesStatus returns true if the container status matches that of filter status +func matchesStatus(ctrData *libkpod.ContainerData, status string) bool { + return ctrData.State.Status == status +} + +// matchesAncestor returns true if filter ancestor is in container image name +func matchesAncestor(ctrData *libkpod.ContainerData, ancestor string) bool { + return strings.Contains(ctrData.FromImage, ancestor) +} + +// matchesVolue returns true if the volume mounted or path to volue of the container matches that of filter volume +func matchesVolume(ctrData *libkpod.ContainerData, volume string) bool { + for _, vol := range ctrData.Mounts { + if strings.Contains(vol.Source, volume) { + return true + } + } + return false +} diff --git a/cmd/kpod/pull.go b/cmd/kpod/pull.go new file mode 100644 index 000000000..738221279 --- /dev/null +++ b/cmd/kpod/pull.go @@ -0,0 +1,118 @@ +package main + +import ( + "fmt" + "io" + "os" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/containers/image/types" + "github.com/kubernetes-incubator/cri-o/libpod" + "github.com/kubernetes-incubator/cri-o/libpod/common" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var ( + pullFlags = []cli.Flag{ + cli.StringFlag{ + Name: "authfile", + Usage: "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + }, + cli.StringFlag{ + Name: "cert-dir", + Usage: "`pathname` of a directory containing TLS certificates and keys", + }, + cli.StringFlag{ + Name: "creds", + Usage: "`credentials` (USERNAME:PASSWORD) to use for authenticating to a registry", + }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "Suppress output information when pulling images", + }, + cli.StringFlag{ + Name: "signature-policy", + Usage: "`pathname` of signature policy file (not usually used)", + }, + cli.BoolTFlag{ + Name: "tls-verify", + Usage: "require HTTPS and verify certificates when contacting registries (default: true)", + }, + } + + pullDescription = "Pulls an image from a registry and stores it locally.\n" + + "An image can be pulled using its tag or digest. If a tag is not\n" + + "specified, the image with the 'latest' tag (if it exists) is pulled." + pullCommand = cli.Command{ + Name: "pull", + Usage: "pull an image from a registry", + Description: pullDescription, + Flags: pullFlags, + Action: pullCmd, + ArgsUsage: "", + } +) + +// pullCmd gets the data from the command line and calls pullImage +// to copy an image from a registry to a local machine +func pullCmd(c *cli.Context) error { + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + args := c.Args() + if len(args) == 0 { + logrus.Errorf("an image name must be specified") + return nil + } + if len(args) > 1 { + logrus.Errorf("too many arguments. Requires exactly 1") + return nil + } + if err := validateFlags(c, pullFlags); err != nil { + return err + } + image := args[0] + + var registryCreds *types.DockerAuthConfig + if c.String("creds") != "" { + creds, err := common.ParseRegistryCreds(c.String("creds")) + if err != nil { + if err == common.ErrNoPassword { + fmt.Print("Password: ") + password, err := terminal.ReadPassword(0) + if err != nil { + return errors.Wrapf(err, "could not read password from terminal") + } + creds.Password = string(password) + } else { + return err + } + } + registryCreds = creds + } + + var writer io.Writer + if !c.Bool("quiet") { + writer = os.Stdout + } + + options := libpod.CopyOptions{ + SignaturePolicyPath: c.String("signature-policy"), + AuthFile: c.String("authfile"), + DockerRegistryOptions: common.DockerRegistryOptions{ + DockerRegistryCreds: registryCreds, + DockerCertPath: c.String("cert-dir"), + DockerInsecureSkipTLSVerify: !c.BoolT("tls-verify"), + }, + Writer: writer, + } + + return runtime.PullImage(image, options) + +} diff --git a/cmd/kpod/push.go b/cmd/kpod/push.go new file mode 100644 index 000000000..506d97f4a --- /dev/null +++ b/cmd/kpod/push.go @@ -0,0 +1,132 @@ +package main + +import ( + "fmt" + "io" + "os" + + "github.com/containers/image/types" + "github.com/containers/storage/pkg/archive" + "github.com/kubernetes-incubator/cri-o/libpod" + "github.com/kubernetes-incubator/cri-o/libpod/common" + "github.com/pkg/errors" + "github.com/urfave/cli" + "golang.org/x/crypto/ssh/terminal" +) + +var ( + pushFlags = []cli.Flag{ + cli.StringFlag{ + Name: "signature-policy", + Usage: "`pathname` of signature policy file (not usually used)", + Hidden: true, + }, + cli.StringFlag{ + Name: "creds", + Usage: "`credentials` (USERNAME:PASSWORD) to use for authenticating to a registry", + }, + cli.StringFlag{ + Name: "cert-dir", + Usage: "`pathname` of a directory containing TLS certificates and keys", + }, + cli.BoolTFlag{ + Name: "tls-verify", + Usage: "require HTTPS and verify certificates when contacting registries (default: true)", + }, + cli.BoolFlag{ + Name: "remove-signatures", + Usage: "discard any pre-existing signatures in the image", + }, + cli.StringFlag{ + Name: "sign-by", + Usage: "add a signature at the destination using the specified key", + }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "don't output progress information when pushing images", + }, + cli.StringFlag{ + Name: "authfile", + Usage: "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + }, + } + pushDescription = fmt.Sprintf(` + Pushes an image to a specified location. + The Image "DESTINATION" uses a "transport":"details" format. + See kpod-push(1) section "DESTINATION" for the expected format`) + + pushCommand = cli.Command{ + Name: "push", + Usage: "push an image to a specified destination", + Description: pushDescription, + Flags: pushFlags, + Action: pushCmd, + ArgsUsage: "IMAGE DESTINATION", + } +) + +func pushCmd(c *cli.Context) error { + var registryCreds *types.DockerAuthConfig + + args := c.Args() + if len(args) < 2 { + return errors.New("kpod push requires exactly 2 arguments") + } + if err := validateFlags(c, pushFlags); err != nil { + return err + } + srcName := c.Args().Get(0) + destName := c.Args().Get(1) + + registryCredsString := c.String("creds") + certPath := c.String("cert-dir") + skipVerify := !c.BoolT("tls-verify") + removeSignatures := c.Bool("remove-signatures") + signBy := c.String("sign-by") + + if registryCredsString != "" { + creds, err := common.ParseRegistryCreds(registryCredsString) + if err != nil { + if err == common.ErrNoPassword { + fmt.Print("Password: ") + password, err := terminal.ReadPassword(0) + if err != nil { + return errors.Wrapf(err, "could not read password from terminal") + } + creds.Password = string(password) + } else { + return err + } + } + registryCreds = creds + } + + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not create runtime") + } + defer runtime.Shutdown(false) + + var writer io.Writer + if !c.Bool("quiet") { + writer = os.Stdout + } + + options := libpod.CopyOptions{ + Compression: archive.Uncompressed, + SignaturePolicyPath: c.String("signature-policy"), + DockerRegistryOptions: common.DockerRegistryOptions{ + DockerRegistryCreds: registryCreds, + DockerCertPath: certPath, + DockerInsecureSkipTLSVerify: skipVerify, + }, + SigningOptions: common.SigningOptions{ + RemoveSignatures: removeSignatures, + SignBy: signBy, + }, + AuthFile: c.String("authfile"), + Writer: writer, + } + + return runtime.PushImage(srcName, destName, options) +} diff --git a/cmd/kpod/rename.go b/cmd/kpod/rename.go new file mode 100644 index 000000000..b638856ed --- /dev/null +++ b/cmd/kpod/rename.go @@ -0,0 +1,49 @@ +package main + +import ( + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + renameDescription = "Rename a container. Container may be created, running, paused, or stopped" + renameFlags = []cli.Flag{} + renameCommand = cli.Command{ + Name: "rename", + Usage: "rename a container", + Description: renameDescription, + Action: renameCmd, + ArgsUsage: "CONTAINER NEW-NAME", + Flags: renameFlags, + } +) + +func renameCmd(c *cli.Context) error { + if len(c.Args()) != 2 { + return errors.Errorf("Rename requires a src container name/ID and a dest container name") + } + if err := validateFlags(c, renameFlags); err != nil { + return err + } + + 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") + } + defer server.Shutdown() + err = server.Update() + if err != nil { + return errors.Wrapf(err, "could not update list of containers") + } + + err = server.ContainerRename(c.Args().Get(0), c.Args().Get(1)) + if err != nil { + return errors.Wrapf(err, "could not rename container") + } + return nil +} diff --git a/cmd/kpod/rm.go b/cmd/kpod/rm.go new file mode 100644 index 000000000..c40fa41c8 --- /dev/null +++ b/cmd/kpod/rm.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/pkg/errors" + "github.com/urfave/cli" + "golang.org/x/net/context" +) + +var ( + rmFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "force, f", + Usage: "Force removal of a running container. The default is false", + }, + } + rmDescription = "Remove one or more containers" + rmCommand = cli.Command{ + Name: "rm", + Usage: fmt.Sprintf(`kpod rm will remove one or more containers from the host. The container name or ID can be used. + This does not remove images. Running containers will not be removed without the -f option.`), + Description: rmDescription, + Flags: rmFlags, + Action: rmCmd, + ArgsUsage: "", + } +) + +// saveCmd saves the image to either docker-archive or oci +func rmCmd(c *cli.Context) error { + args := c.Args() + if len(args) == 0 { + return errors.Errorf("specify one or more containers to remove") + } + if err := validateFlags(c, rmFlags); err != nil { + return err + } + + 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") + } + defer server.Shutdown() + err = server.Update() + if err != nil { + return errors.Wrapf(err, "could not update list of containers") + } + force := c.Bool("force") + + for _, container := range c.Args() { + id, err2 := server.Remove(context.Background(), container, force) + if err2 != nil { + if err == nil { + err = err2 + } else { + err = errors.Wrapf(err, "%v. Stop the container before attempting removal or use -f\n", err2) + } + } else { + fmt.Println(id) + } + } + return err +} diff --git a/cmd/kpod/rmi.go b/cmd/kpod/rmi.go new file mode 100644 index 000000000..3713db454 --- /dev/null +++ b/cmd/kpod/rmi.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + rmiDescription = "removes one or more locally stored images." + rmiFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "force, f", + Usage: "force removal of the image", + }, + } + rmiCommand = cli.Command{ + Name: "rmi", + Usage: "removes one or more images from local storage", + Description: rmiDescription, + Action: rmiCmd, + ArgsUsage: "IMAGE-NAME-OR-ID [...]", + Flags: rmiFlags, + } +) + +func rmiCmd(c *cli.Context) error { + if err := validateFlags(c, rmiFlags); err != nil { + return err + } + + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + args := c.Args() + if len(args) == 0 { + return errors.Errorf("image name or ID must be specified") + } + + for _, arg := range args { + image, err := runtime.GetImage(arg) + if err != nil { + return errors.Wrapf(err, "could not get image %q", arg) + } + id, err := runtime.RemoveImage(image, c.Bool("force")) + if err != nil { + return errors.Wrapf(err, "error removing image %q", id) + } + fmt.Printf("%s\n", id) + } + return nil +} diff --git a/cmd/kpod/save.go b/cmd/kpod/save.go new file mode 100644 index 000000000..cfe90a95e --- /dev/null +++ b/cmd/kpod/save.go @@ -0,0 +1,98 @@ +package main + +import ( + "io" + "os" + + "github.com/kubernetes-incubator/cri-o/libpod" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var ( + saveFlags = []cli.Flag{ + cli.StringFlag{ + Name: "output, o", + Usage: "Write to a file, default is STDOUT", + Value: "/dev/stdout", + }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "Suppress the output", + }, + cli.StringFlag{ + Name: "format", + Usage: "Save image to oci-archive", + }, + } + saveDescription = ` + Save an image to docker-archive or oci-archive on the local machine. + Default is docker-archive` + + saveCommand = cli.Command{ + Name: "save", + Usage: "Save image to an archive", + Description: saveDescription, + Flags: saveFlags, + Action: saveCmd, + ArgsUsage: "", + } +) + +// saveCmd saves the image to either docker-archive or oci +func saveCmd(c *cli.Context) error { + args := c.Args() + if len(args) == 0 { + return errors.Errorf("need at least 1 argument") + } + if err := validateFlags(c, saveFlags); err != nil { + return err + } + + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not create runtime") + } + defer runtime.Shutdown(false) + + var writer io.Writer + if !c.Bool("quiet") { + writer = os.Stdout + } + + output := c.String("output") + if output == "/dev/stdout" { + fi := os.Stdout + if logrus.IsTerminal(fi) { + return errors.Errorf("refusing to save to terminal. Use -o flag or redirect") + } + } + + var dst string + switch c.String("format") { + case libpod.OCIArchive: + dst = libpod.OCIArchive + ":" + output + case libpod.DockerArchive: + fallthrough + case "": + dst = libpod.DockerArchive + ":" + output + default: + return errors.Errorf("unknown format option %q", c.String("format")) + } + + saveOpts := libpod.CopyOptions{ + SignaturePolicyPath: "", + Writer: writer, + } + + // only one image is supported for now + // future pull requests will fix this + for _, image := range args { + dest := dst + ":" + image + if err := runtime.PushImage(image, dest, saveOpts); err != nil { + return errors.Wrapf(err, "unable to save %q", image) + } + } + return nil +} diff --git a/cmd/kpod/stats.go b/cmd/kpod/stats.go new file mode 100644 index 000000000..ac81212a1 --- /dev/null +++ b/cmd/kpod/stats.go @@ -0,0 +1,245 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "text/template" + "time" + + "github.com/docker/go-units" + + tm "github.com/buger/goterm" + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/kubernetes-incubator/cri-o/oci" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var printf func(format string, a ...interface{}) (n int, err error) +var println func(a ...interface{}) (n int, err error) + +type statsOutputParams struct { + Container string + ID string + CPUPerc string + MemUsage string + MemPerc string + NetIO string + BlockIO string + PIDs uint64 +} + +var ( + statsFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "all, a", + Usage: "show all containers. Only running containers are shown by default. The default is false", + }, + cli.BoolFlag{ + Name: "no-stream", + Usage: "disable streaming stats and only pull the first result, default setting is false", + }, + cli.StringFlag{ + Name: "format", + Usage: "pretty-print container statistics using a Go template", + }, + cli.BoolFlag{ + Name: "json", + Usage: "output container statistics in json format", + }, + } + + statsDescription = "display a live stream of one or more containers' resource usage statistics" + statsCommand = cli.Command{ + Name: "stats", + Usage: "Display percentage of CPU, memory, network I/O, block I/O and PIDs for one or more containers", + Description: statsDescription, + Flags: statsFlags, + Action: statsCmd, + ArgsUsage: "", + } +) + +func statsCmd(c *cli.Context) error { + if err := validateFlags(c, statsFlags); err != nil { + return err + } + config, err := getConfig(c) + if err != nil { + return errors.Wrapf(err, "could not read config") + } + containerServer, err := libkpod.New(config) + if err != nil { + return errors.Wrapf(err, "could not create container server") + } + defer containerServer.Shutdown() + err = containerServer.Update() + if err != nil { + return errors.Wrapf(err, "could not update list of containers") + } + times := -1 + if c.Bool("no-stream") { + times = 1 + } + statsChan := make(chan []*libkpod.ContainerStats) + // iterate over the channel until it is closed + go func() { + // print using goterm + printf = tm.Printf + println = tm.Println + for stats := range statsChan { + // Continually refresh statistics + tm.Clear() + tm.MoveCursor(1, 1) + outputStats(stats, c.String("format"), c.Bool("json")) + tm.Flush() + time.Sleep(time.Second) + } + }() + return getStats(containerServer, c.Args(), c.Bool("all"), statsChan, times) +} + +func getStats(server *libkpod.ContainerServer, args []string, all bool, statsChan chan []*libkpod.ContainerStats, times int) error { + ctrs, err := server.ListContainers(isRunning, ctrInList(args)) + if err != nil { + return err + } + containerStats := map[string]*libkpod.ContainerStats{} + for _, ctr := range ctrs { + initialStats, err := server.GetContainerStats(ctr, &libkpod.ContainerStats{}) + if err != nil { + return err + } + containerStats[ctr.ID()] = initialStats + } + step := 1 + if times == -1 { + times = 1 + step = 0 + } + for i := 0; i < times; i += step { + reportStats := []*libkpod.ContainerStats{} + for _, ctr := range ctrs { + id := ctr.ID() + if _, ok := containerStats[ctr.ID()]; !ok { + initialStats, err := server.GetContainerStats(ctr, &libkpod.ContainerStats{}) + if err != nil { + return err + } + containerStats[id] = initialStats + } + stats, err := server.GetContainerStats(ctr, containerStats[id]) + if err != nil { + return err + } + // replace the previous measurement with the current one + containerStats[id] = stats + reportStats = append(reportStats, stats) + } + statsChan <- reportStats + + err := server.Update() + if err != nil { + return err + } + ctrs, err = server.ListContainers(isRunning, ctrInList(args)) + if err != nil { + return err + } + } + return nil +} + +func outputStats(stats []*libkpod.ContainerStats, format string, json bool) error { + if format == "" { + outputStatsHeader() + } + if json { + return outputStatsAsJSON(stats) + } + var err error + for _, s := range stats { + if format == "" { + outputStatsUsingFormatString(s) + } else { + params := getStatsOutputParams(s) + err2 := outputStatsUsingTemplate(format, params) + if err2 != nil { + err = errors.Wrapf(err, err2.Error()) + } + } + } + return err +} + +func outputStatsHeader() { + printf("%-64s %-16s %-32s %-16s %-24s %-24s %s\n", "CONTAINER", "CPU %", "MEM USAGE / MEM LIMIT", "MEM %", "NET I/O", "BLOCK I/O", "PIDS") +} + +func outputStatsUsingFormatString(stats *libkpod.ContainerStats) { + printf("%-64s %-16s %-32s %-16s %-24s %-24s %d\n", stats.Container, floatToPercentString(stats.CPU), combineHumanValues(stats.MemUsage, stats.MemLimit), floatToPercentString(stats.MemPerc), combineHumanValues(stats.NetInput, stats.NetOutput), combineHumanValues(stats.BlockInput, stats.BlockOutput), stats.PIDs) +} + +func combineHumanValues(a, b uint64) string { + return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b))) +} + +func floatToPercentString(f float64) string { + return fmt.Sprintf("%.2f %s", f, "%") +} + +func getStatsOutputParams(stats *libkpod.ContainerStats) statsOutputParams { + return statsOutputParams{ + Container: stats.Container, + ID: stats.Container, + CPUPerc: floatToPercentString(stats.CPU), + MemUsage: combineHumanValues(stats.MemUsage, stats.MemLimit), + MemPerc: floatToPercentString(stats.MemPerc), + NetIO: combineHumanValues(stats.NetInput, stats.NetOutput), + BlockIO: combineHumanValues(stats.BlockInput, stats.BlockOutput), + PIDs: stats.PIDs, + } +} + +func outputStatsUsingTemplate(format string, params statsOutputParams) error { + tmpl, err := template.New("stats").Parse(format) + if err != nil { + return errors.Wrapf(err, "template parsing error") + } + + err = tmpl.Execute(os.Stdout, params) + if err != nil { + return err + } + println() + return nil +} + +func outputStatsAsJSON(stats []*libkpod.ContainerStats) error { + s, err := json.Marshal(stats) + if err != nil { + return err + } + println(s) + return nil +} + +func isRunning(ctr *oci.Container) bool { + return ctr.State().Status == "running" +} + +func ctrInList(idsOrNames []string) func(ctr *oci.Container) bool { + if len(idsOrNames) == 0 { + return func(*oci.Container) bool { return true } + } + return func(ctr *oci.Container) bool { + for _, idOrName := range idsOrNames { + if strings.HasPrefix(ctr.ID(), idOrName) || strings.HasSuffix(ctr.Name(), idOrName) { + return true + } + } + return false + } +} diff --git a/cmd/kpod/stop.go b/cmd/kpod/stop.go new file mode 100644 index 000000000..279f7b762 --- /dev/null +++ b/cmd/kpod/stop.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "os" + + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/pkg/errors" + "github.com/urfave/cli" + "golang.org/x/net/context" +) + +var ( + defaultTimeout int64 = 10 + stopFlags = []cli.Flag{ + cli.Int64Flag{ + Name: "timeout, t", + Usage: "Seconds to wait for stop before killing the container", + Value: defaultTimeout, + }, + } + stopDescription = ` + kpod stop + + Stops one or more running containers. The container name or ID can be used. + A timeout to forcibly stop the container can also be set but defaults to 10 + seconds otherwise. +` + + stopCommand = cli.Command{ + Name: "stop", + Usage: "Stop one or more containers", + Description: stopDescription, + Flags: stopFlags, + Action: stopCmd, + ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]", + } +) + +func stopCmd(c *cli.Context) error { + args := c.Args() + stopTimeout := c.Int64("timeout") + if len(args) < 1 { + return errors.Errorf("you must provide at least one container name or id") + } + if err := validateFlags(c, stopFlags); err != nil { + return err + } + + 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") + } + defer server.Shutdown() + err = server.Update() + if err != nil { + return errors.Wrapf(err, "could not update list of containers") + } + var lastError error + for _, container := range c.Args() { + cid, err := server.ContainerStop(context.Background(), container, stopTimeout) + if err != nil { + if lastError != nil { + fmt.Fprintln(os.Stderr, lastError) + } + lastError = errors.Wrapf(err, "failed to stop container %v", container) + } else { + fmt.Println(cid) + } + } + + return lastError +} diff --git a/cmd/kpod/tag.go b/cmd/kpod/tag.go new file mode 100644 index 000000000..b9c380607 --- /dev/null +++ b/cmd/kpod/tag.go @@ -0,0 +1,77 @@ +package main + +import ( + "github.com/containers/image/docker/reference" + "github.com/containers/storage" + "github.com/kubernetes-incubator/cri-o/libpod" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + tagDescription = "Adds one or more additional names to locally-stored image" + tagCommand = cli.Command{ + Name: "tag", + Usage: "Add an additional name to a local image", + Description: tagDescription, + Action: tagCmd, + ArgsUsage: "IMAGE-NAME [IMAGE-NAME ...]", + } +) + +func tagCmd(c *cli.Context) error { + args := c.Args() + if len(args) < 2 { + return errors.Errorf("image name and at least one new name must be specified") + } + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not create runtime") + } + defer runtime.Shutdown(false) + + img, err := runtime.GetImage(args[0]) + if err != nil { + return err + } + if img == nil { + return errors.New("null image") + } + err = addImageNames(runtime, img, args[1:]) + if err != nil { + return errors.Wrapf(err, "error adding names %v to image %q", args[1:], args[0]) + } + return nil +} + +func addImageNames(runtime *libpod.Runtime, image *storage.Image, addNames []string) error { + // Add tags to the names if applicable + names, err := expandedTags(addNames) + if err != nil { + return err + } + for _, name := range names { + if err := runtime.TagImage(image, name); err != nil { + return errors.Wrapf(err, "error adding name (%v) to image %q", name, image.ID) + } + } + return nil +} + +func expandedTags(tags []string) ([]string, error) { + expandedNames := []string{} + for _, tag := range tags { + var labelName string + name, err := reference.Parse(tag) + if err != nil { + return nil, errors.Wrapf(err, "error parsing tag %q", name) + } + if _, ok := name.(reference.NamedTagged); ok { + labelName = name.String() + } else { + labelName = name.String() + ":latest" + } + expandedNames = append(expandedNames, labelName) + } + return expandedNames, nil +} diff --git a/cmd/kpod/umount.go b/cmd/kpod/umount.go new file mode 100644 index 000000000..bad6752ab --- /dev/null +++ b/cmd/kpod/umount.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + umountCommand = cli.Command{ + Name: "umount", + Aliases: []string{"unmount"}, + Usage: "Unmount a working container's root filesystem", + Description: "Unmounts a working container's root filesystem", + Action: umountCmd, + ArgsUsage: "CONTAINER-NAME-OR-ID", + } +) + +func umountCmd(c *cli.Context) error { + args := c.Args() + if len(args) == 0 { + return errors.Errorf("container ID must be specified") + } + if len(args) > 1 { + return errors.Errorf("too many arguments specified") + } + config, err := getConfig(c) + if err != nil { + return errors.Wrapf(err, "Could not get config") + } + store, err := getStore(config) + if err != nil { + return err + } + + err = store.Unmount(args[0]) + if err != nil { + return errors.Wrapf(err, "error unmounting container %q", args[0]) + } + return nil +} diff --git a/cmd/kpod/unpause.go b/cmd/kpod/unpause.go new file mode 100644 index 000000000..a7b7db20f --- /dev/null +++ b/cmd/kpod/unpause.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/pkg/errors" + "github.com/urfave/cli" + "os" +) + +var ( + unpauseDescription = ` + kpod unpause + + Unpauses one or more running containers. The container name or ID can be used. +` + unpauseCommand = cli.Command{ + Name: "unpause", + Usage: "Unpause the processes in one or more containers", + Description: unpauseDescription, + Action: unpauseCmd, + ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]", + } +) + +func unpauseCmd(c *cli.Context) error { + args := c.Args() + if len(args) < 1 { + return errors.Errorf("you must provide at least one container name or id") + } + + 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") + } + defer server.Shutdown() + if err := server.Update(); err != nil { + return errors.Wrapf(err, "could not update list of containers") + } + var lastError error + for _, container := range c.Args() { + cid, err := server.ContainerUnpause(container) + if err != nil { + if lastError != nil { + fmt.Fprintln(os.Stderr, lastError) + } + lastError = errors.Wrapf(err, "failed to unpause container %v", container) + } else { + fmt.Println(cid) + } + } + + return lastError +} diff --git a/cmd/kpod/version.go b/cmd/kpod/version.go new file mode 100644 index 000000000..586c41da6 --- /dev/null +++ b/cmd/kpod/version.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "runtime" + "strconv" + "time" + + "github.com/urfave/cli" +) + +// Overwritten at build time +var ( + // gitCommit is the commit that the binary is being built from. + // It will be populated by the Makefile. + gitCommit string + // buildInfo is the time at which the binary was built + // It will be populated by the Makefile. + buildInfo string +) + +// versionCmd gets and prints version info for version command +func versionCmd(c *cli.Context) error { + fmt.Println("Version: ", c.App.Version) + fmt.Println("Go Version: ", runtime.Version()) + if gitCommit != "" { + fmt.Println("Git Commit: ", gitCommit) + } + if buildInfo != "" { + // Converts unix time from string to int64 + buildTime, err := strconv.ParseInt(buildInfo, 10, 64) + if err != nil { + return err + } + // Prints out the build time in readable format + fmt.Println("Built: ", time.Unix(buildTime, 0).Format(time.ANSIC)) + } + fmt.Println("OS/Arch: ", runtime.GOOS+"/"+runtime.GOARCH) + + return nil +} + +// Cli command to print out the full version of kpod +var versionCommand = cli.Command{ + Name: "version", + Usage: "Display the KPOD Version Information", + Action: versionCmd, +} diff --git a/cmd/kpod/wait.go b/cmd/kpod/wait.go new file mode 100644 index 000000000..b166e3306 --- /dev/null +++ b/cmd/kpod/wait.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "os" + + "github.com/kubernetes-incubator/cri-o/libkpod" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + waitDescription = ` + kpod wait + + Block until one or more containers stop and then print their exit codes +` + + waitCommand = cli.Command{ + Name: "wait", + Usage: "Block on one or more containers", + Description: waitDescription, + Action: waitCmd, + ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]", + } +) + +func waitCmd(c *cli.Context) error { + args := c.Args() + if len(args) < 1 { + return errors.Errorf("you must provide at least one container name or id") + } + + 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") + } + defer server.Shutdown() + err = server.Update() + if err != nil { + return errors.Wrapf(err, "could not update list of containers") + } + + var lastError error + for _, container := range c.Args() { + returnCode, err := server.ContainerWait(container) + if err != nil { + if lastError != nil { + fmt.Fprintln(os.Stderr, lastError) + } + lastError = errors.Wrapf(err, "failed to wait for the container %v", container) + } else { + fmt.Println(returnCode) + } + } + + return lastError +} -- cgit v1.2.3-54-g00ecf From c13f61798aa7bcf7b4de7ee31aa30148a3b08d97 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Wed, 1 Nov 2017 13:22:04 -0400 Subject: Prune Server package. Convert to new github location. Signed-off-by: Matthew Heon --- CONTRIBUTING.md | 8 +- Dockerfile | 4 +- Makefile | 4 +- cmd/kpod/common.go | 11 +- cmd/kpod/diff.go | 2 +- cmd/kpod/history.go | 2 +- cmd/kpod/images.go | 6 +- cmd/kpod/info.go | 2 +- cmd/kpod/inspect.go | 6 +- cmd/kpod/kill.go | 2 +- cmd/kpod/load.go | 2 +- cmd/kpod/login.go | 2 +- cmd/kpod/logout.go | 2 +- cmd/kpod/logs.go | 2 +- cmd/kpod/mount.go | 2 +- cmd/kpod/pause.go | 2 +- cmd/kpod/ps.go | 6 +- cmd/kpod/pull.go | 4 +- cmd/kpod/push.go | 4 +- cmd/kpod/rename.go | 2 +- cmd/kpod/rm.go | 2 +- cmd/kpod/save.go | 2 +- cmd/kpod/stats.go | 4 +- cmd/kpod/stop.go | 2 +- cmd/kpod/tag.go | 2 +- cmd/kpod/unpause.go | 2 +- cmd/kpod/wait.go | 2 +- contrib/rpm/Makefile | 14 - contrib/rpm/crio.spec | 76 -- contrib/systemd/crio-shutdown.service | 14 - contrib/systemd/crio.service | 24 - contrib/test/integration/README.md | 21 - contrib/test/integration/ansible.cfg | 359 ------ contrib/test/integration/build/bats.yml | 17 - contrib/test/integration/build/cri-o.yml | 79 -- contrib/test/integration/build/cri-tools.yml | 16 - contrib/test/integration/build/kubernetes.yml | 63 - contrib/test/integration/build/plugins.yml | 50 - contrib/test/integration/build/runc.yml | 23 - .../test/integration/callback_plugins/default.py | 156 --- contrib/test/integration/e2e.yml | 57 - contrib/test/integration/golang.yml | 51 - contrib/test/integration/main.yml | 58 - contrib/test/integration/results.yml | 62 - contrib/test/integration/system.yml | 117 -- contrib/test/integration/test.yml | 25 - contrib/test/integration/vars.yml | 8 - contrib/test/requirements.txt | 54 - contrib/test/venv-ansible-playbook.sh | 106 -- kubernetes.md | 105 -- libkpod/config.go | 2 +- libkpod/container.go | 6 +- libkpod/container_data.go | 6 +- libkpod/container_server.go | 10 +- libkpod/kill.go | 4 +- libkpod/pause.go | 2 +- libkpod/remove.go | 2 +- libkpod/rename.go | 4 +- libkpod/sandbox/sandbox.go | 2 +- libkpod/stats.go | 2 +- libkpod/stop.go | 2 +- libkpod/wait.go | 2 +- libpod/container.go | 2 +- libpod/diff.go | 2 +- libpod/images/image_data.go | 2 +- libpod/in_memory_state.go | 2 +- libpod/oci.go | 2 +- libpod/runtime_img.go | 2 +- oci/oci.go | 2 +- server/apparmor/aaparser.go | 89 -- server/apparmor/apparmor_common.go | 14 - server/apparmor/apparmor_supported.go | 145 --- server/apparmor/apparmor_unsupported.go | 18 - server/apparmor/template.go | 45 - server/config.go | 112 -- server/container_attach.go | 147 --- server/container_create.go | 1215 -------------------- server/container_exec.go | 108 -- server/container_execsync.go | 46 - server/container_list.go | 112 -- server/container_portforward.go | 91 -- server/container_remove.go | 20 - server/container_start.go | 43 - server/container_stats.go | 14 - server/container_stats_list.go | 13 - server/container_status.go | 102 -- server/container_stop.go | 19 - server/container_updateruntimeconfig.go | 11 - server/image_fs_info.go | 13 - server/image_list.go | 41 - server/image_pull.go | 108 -- server/image_remove.go | 52 - server/image_status.go | 53 - server/inspect.go | 105 -- server/inspect_test.go | 235 ---- server/naming.go | 86 -- server/runtime_status.go | 41 - server/sandbox_list.go | 94 -- server/sandbox_network.go | 70 -- server/sandbox_remove.go | 98 -- server/sandbox_run.go | 615 ---------- server/sandbox_status.go | 41 - server/sandbox_stop.go | 114 -- server/seccomp/seccomp.go | 165 --- server/seccomp/seccomp_unsupported.go | 20 - server/seccomp/types.go | 93 -- server/secrets.go | 162 --- server/server.go | 423 ------- server/utils.go | 183 --- server/version.go | 27 - tutorial.md | 425 ------- 111 files changed, 75 insertions(+), 7257 deletions(-) delete mode 100644 contrib/rpm/Makefile delete mode 100644 contrib/rpm/crio.spec delete mode 100644 contrib/systemd/crio-shutdown.service delete mode 100644 contrib/systemd/crio.service delete mode 100644 contrib/test/integration/README.md delete mode 100644 contrib/test/integration/ansible.cfg delete mode 100644 contrib/test/integration/build/bats.yml delete mode 100644 contrib/test/integration/build/cri-o.yml delete mode 100644 contrib/test/integration/build/cri-tools.yml delete mode 100644 contrib/test/integration/build/kubernetes.yml delete mode 100644 contrib/test/integration/build/plugins.yml delete mode 100644 contrib/test/integration/build/runc.yml delete mode 100644 contrib/test/integration/callback_plugins/default.py delete mode 100644 contrib/test/integration/e2e.yml delete mode 100644 contrib/test/integration/golang.yml delete mode 100644 contrib/test/integration/main.yml delete mode 100644 contrib/test/integration/results.yml delete mode 100644 contrib/test/integration/system.yml delete mode 100644 contrib/test/integration/test.yml delete mode 100644 contrib/test/integration/vars.yml delete mode 100644 contrib/test/requirements.txt delete mode 100755 contrib/test/venv-ansible-playbook.sh delete mode 100644 kubernetes.md delete mode 100644 server/apparmor/aaparser.go delete mode 100644 server/apparmor/apparmor_common.go delete mode 100644 server/apparmor/apparmor_supported.go delete mode 100644 server/apparmor/apparmor_unsupported.go delete mode 100644 server/apparmor/template.go delete mode 100644 server/config.go delete mode 100644 server/container_attach.go delete mode 100644 server/container_create.go delete mode 100644 server/container_exec.go delete mode 100644 server/container_execsync.go delete mode 100644 server/container_list.go delete mode 100644 server/container_portforward.go delete mode 100644 server/container_remove.go delete mode 100644 server/container_start.go delete mode 100644 server/container_stats.go delete mode 100644 server/container_stats_list.go delete mode 100644 server/container_status.go delete mode 100644 server/container_stop.go delete mode 100644 server/container_updateruntimeconfig.go delete mode 100644 server/image_fs_info.go delete mode 100644 server/image_list.go delete mode 100644 server/image_pull.go delete mode 100644 server/image_remove.go delete mode 100644 server/image_status.go delete mode 100644 server/inspect.go delete mode 100644 server/inspect_test.go delete mode 100644 server/naming.go delete mode 100644 server/runtime_status.go delete mode 100644 server/sandbox_list.go delete mode 100644 server/sandbox_network.go delete mode 100644 server/sandbox_remove.go delete mode 100644 server/sandbox_run.go delete mode 100644 server/sandbox_status.go delete mode 100644 server/sandbox_stop.go delete mode 100644 server/seccomp/seccomp.go delete mode 100644 server/seccomp/seccomp_unsupported.go delete mode 100644 server/seccomp/types.go delete mode 100644 server/secrets.go delete mode 100644 server/server.go delete mode 100644 server/utils.go delete mode 100644 server/version.go delete mode 100644 tutorial.md (limited to 'cmd/kpod') diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc549116d..c121ac416 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to CRI-O +# Contributing to Libpod We'd love to have you join the community! Below summarizes the processes that we follow. @@ -13,7 +13,7 @@ that we follow. ## Reporting Issues Before reporting an issue, check our backlog of -[open issues](https://github.com/kubernetes-incubator/cri-o/issues) +[open issues](https://github.com/projectatomic/libpod/issues) to see if someone else has already reported it. If so, feel free to add your scenario, or additional information, to the discussion. Or simply "subscribe" to it to be notified when it is updated. @@ -120,9 +120,9 @@ IRC group on `irc.freenode.net` called `cri-o` that has been setup. For discussions around issues/bugs and features, you can use the github -[issues](https://github.com/kubernetes-incubator/cri-o/issues) +[issues](https://github.com/projectatomic/libpod/issues) and -[PRs](https://github.com/kubernetes-incubator/cri-o/pulls) +[PRs](https://github.com/projectatomic/libpod/pulls) tracking system.