summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorMatthew Heon <matthew.heon@gmail.com>2017-11-01 11:24:59 -0400
committerMatthew Heon <matthew.heon@gmail.com>2017-11-01 11:24:59 -0400
commita031b83a09a8628435317a03f199cdc18b78262f (patch)
treebc017a96769ce6de33745b8b0b1304ccf38e9df0 /cmd
parent2b74391cd5281f6fdf391ff8ad50fd1490f6bf89 (diff)
downloadpodman-a031b83a09a8628435317a03f199cdc18b78262f.tar.gz
podman-a031b83a09a8628435317a03f199cdc18b78262f.tar.bz2
podman-a031b83a09a8628435317a03f199cdc18b78262f.zip
Initial checkin from CRI-O repo
Signed-off-by: Matthew Heon <matthew.heon@gmail.com>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/crio/config.go192
-rw-r--r--cmd/crio/daemon_linux.go20
-rw-r--r--cmd/crio/main.go532
-rw-r--r--cmd/crioctl/container.go653
-rw-r--r--cmd/crioctl/image.go173
-rw-r--r--cmd/crioctl/info.go31
-rw-r--r--cmd/crioctl/main.go113
-rw-r--r--cmd/crioctl/sandbox.go386
-rw-r--r--cmd/crioctl/system.go41
-rw-r--r--cmd/kpod/README.md16
-rw-r--r--cmd/kpod/common.go135
-rw-r--r--cmd/kpod/common_test.go51
-rw-r--r--cmd/kpod/diff.go128
-rw-r--r--cmd/kpod/docker/types.go271
-rw-r--r--cmd/kpod/export.go106
-rw-r--r--cmd/kpod/formats/formats.go143
-rw-r--r--cmd/kpod/formats/templates.go78
-rw-r--r--cmd/kpod/history.go243
-rw-r--r--cmd/kpod/images.go330
-rw-r--r--cmd/kpod/info.go200
-rw-r--r--cmd/kpod/inspect.go120
-rw-r--r--cmd/kpod/kill.go74
-rw-r--r--cmd/kpod/load.go116
-rw-r--r--cmd/kpod/login.go110
-rw-r--r--cmd/kpod/logout.go69
-rw-r--r--cmd/kpod/logs.go92
-rw-r--r--cmd/kpod/main.go129
-rw-r--r--cmd/kpod/mount.go121
-rw-r--r--cmd/kpod/pause.go58
-rw-r--r--cmd/kpod/ps.go665
-rw-r--r--cmd/kpod/pull.go118
-rw-r--r--cmd/kpod/push.go132
-rw-r--r--cmd/kpod/rename.go49
-rw-r--r--cmd/kpod/rm.go69
-rw-r--r--cmd/kpod/rmi.go56
-rw-r--r--cmd/kpod/save.go98
-rw-r--r--cmd/kpod/stats.go245
-rw-r--r--cmd/kpod/stop.go77
-rw-r--r--cmd/kpod/tag.go77
-rw-r--r--cmd/kpod/umount.go41
-rw-r--r--cmd/kpod/unpause.go58
-rw-r--r--cmd/kpod/version.go48
-rw-r--r--cmd/kpod/wait.go62
43 files changed, 6526 insertions, 0 deletions
diff --git a/cmd/crio/config.go b/cmd/crio/config.go
new file mode 100644
index 000000000..76e3361d4
--- /dev/null
+++ b/cmd/crio/config.go
@@ -0,0 +1,192 @@
+package main
+
+import (
+ "os"
+ "text/template"
+
+ "github.com/kubernetes-incubator/cri-o/server"
+ "github.com/urfave/cli"
+)
+
+var commentedConfigTemplate = template.Must(template.New("config").Parse(`
+# The "crio" table contains all of the server options.
+[crio]
+
+# root is a path to the "root directory". CRIO stores all of its data,
+# including container images, in this directory.
+root = "{{ .Root }}"
+
+# run is a path to the "run directory". CRIO stores all of its state
+# in this directory.
+runroot = "{{ .RunRoot }}"
+
+# storage_driver select which storage driver is used to manage storage
+# of images and containers.
+storage_driver = "{{ .Storage }}"
+
+# storage_option is used to pass an option to the storage driver.
+storage_option = [
+{{ range $opt := .StorageOptions }}{{ printf "\t%q,\n" $opt }}{{ end }}]
+
+# The "crio.api" table contains settings for the kubelet/gRPC
+# interface (which is also used by crioctl).
+[crio.api]
+
+# listen is the path to the AF_LOCAL socket on which crio will listen.
+listen = "{{ .Listen }}"
+
+# stream_address is the IP address on which the stream server will listen
+stream_address = "{{ .StreamAddress }}"
+
+# stream_port is the port on which the stream server will listen
+stream_port = "{{ .StreamPort }}"
+
+# file_locking is whether file-based locking will be used instead of
+# in-memory locking
+file_locking = {{ .FileLocking }}
+
+# The "crio.runtime" table contains settings pertaining to the OCI
+# runtime used and options for how to set up and manage the OCI runtime.
+[crio.runtime]
+
+# runtime is the OCI compatible runtime used for trusted container workloads.
+# This is a mandatory setting as this runtime will be the default one
+# and will also be used for untrusted container workloads if
+# runtime_untrusted_workload is not set.
+runtime = "{{ .Runtime }}"
+
+# runtime_untrusted_workload is the OCI compatible runtime used for untrusted
+# container workloads. This is an optional setting, except if
+# default_container_trust is set to "untrusted".
+runtime_untrusted_workload = "{{ .RuntimeUntrustedWorkload }}"
+
+# default_workload_trust is the default level of trust crio puts in container
+# workloads. It can either be "trusted" or "untrusted", and the default
+# is "trusted".
+# Containers can be run through different container runtimes, depending on
+# the trust hints we receive from kubelet:
+# - If kubelet tags a container workload as untrusted, crio will try first to
+# run it through the untrusted container workload runtime. If it is not set,
+# crio will use the trusted runtime.
+# - If kubelet does not provide any information about the container workload trust
+# level, the selected runtime will depend on the default_container_trust setting.
+# If it is set to "untrusted", then all containers except for the host privileged
+# ones, will be run by the runtime_untrusted_workload runtime. Host privileged
+# containers are by definition trusted and will always use the trusted container
+# runtime. If default_container_trust is set to "trusted", crio will use the trusted
+# container runtime for all containers.
+default_workload_trust = "{{ .DefaultWorkloadTrust }}"
+
+# no_pivot instructs the runtime to not use pivot_root, but instead use MS_MOVE
+no_pivot = {{ .NoPivot }}
+
+# conmon is the path to conmon binary, used for managing the runtime.
+conmon = "{{ .Conmon }}"
+
+# conmon_env is the environment variable list for conmon process,
+# used for passing necessary environment variable to conmon or runtime.
+conmon_env = [
+{{ range $env := .ConmonEnv }}{{ printf "\t%q,\n" $env }}{{ end }}]
+
+# selinux indicates whether or not SELinux will be used for pod
+# separation on the host. If you enable this flag, SELinux must be running
+# on the host.
+selinux = {{ .SELinux }}
+
+# seccomp_profile is the seccomp json profile path which is used as the
+# default for the runtime.
+seccomp_profile = "{{ .SeccompProfile }}"
+
+# apparmor_profile is the apparmor profile name which is used as the
+# default for the runtime.
+apparmor_profile = "{{ .ApparmorProfile }}"
+
+# cgroup_manager is the cgroup management implementation to be used
+# for the runtime.
+cgroup_manager = "{{ .CgroupManager }}"
+
+# hooks_dir_path is the oci hooks directory for automatically executed hooks
+hooks_dir_path = "{{ .HooksDirPath }}"
+
+# default_mounts is the mounts list to be mounted for the container when created
+default_mounts = [
+{{ range $mount := .DefaultMounts }}{{ printf "\t%q, \n" $mount }}{{ end }}]
+
+# pids_limit is the number of processes allowed in a container
+pids_limit = {{ .PidsLimit }}
+
+# log_size_max is the max limit for the container log size in bytes.
+# Negative values indicate that no limit is imposed.
+log_size_max = {{ .LogSizeMax }}
+
+# The "crio.image" table contains settings pertaining to the
+# management of OCI images.
+[crio.image]
+
+# default_transport is the prefix we try prepending to an image name if the
+# image name as we receive it can't be parsed as a valid source reference
+default_transport = "{{ .DefaultTransport }}"
+
+# pause_image is the image which we use to instantiate infra containers.
+pause_image = "{{ .PauseImage }}"
+
+# pause_command is the command to run in a pause_image to have a container just
+# sit there. If the image contains the necessary information, this value need
+# not be specified.
+pause_command = "{{ .PauseCommand }}"
+
+# signature_policy is the name of the file which decides what sort of policy we
+# use when deciding whether or not to trust an image that we've pulled.
+# Outside of testing situations, it is strongly advised that this be left
+# unspecified so that the default system-wide policy will be used.
+signature_policy = "{{ .SignaturePolicyPath }}"
+
+# image_volumes controls how image volumes are handled.
+# The valid values are mkdir and ignore.
+image_volumes = "{{ .ImageVolumes }}"
+
+# insecure_registries is used to skip TLS verification when pulling images.
+insecure_registries = [
+{{ range $opt := .InsecureRegistries }}{{ printf "\t%q,\n" $opt }}{{ end }}]
+
+# registries is used to specify a comma separated list of registries to be used
+# when pulling an unqualified image (e.g. fedora:rawhide).
+registries = [
+{{ range $opt := .Registries }}{{ printf "\t%q,\n" $opt }}{{ end }}]
+
+# The "crio.network" table contains settings pertaining to the
+# management of CNI plugins.
+[crio.network]
+
+# network_dir is is where CNI network configuration
+# files are stored.
+network_dir = "{{ .NetworkDir }}"
+
+# plugin_dir is is where CNI plugin binaries are stored.
+plugin_dir = "{{ .PluginDir }}"
+`))
+
+// TODO: Currently ImageDir isn't really used, so we haven't added it to this
+// template. Add it once the storage code has been merged.
+
+var configCommand = cli.Command{
+ Name: "config",
+ Usage: "generate crio configuration files",
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "default",
+ Usage: "output the default configuration",
+ },
+ },
+ Action: func(c *cli.Context) error {
+ // At this point, app.Before has already parsed the user's chosen
+ // config file. So no need to handle that here.
+ config := c.App.Metadata["config"].(*server.Config)
+ if c.Bool("default") {
+ config = server.DefaultConfig()
+ }
+
+ // Output the commented config.
+ return commentedConfigTemplate.ExecuteTemplate(os.Stdout, "config", config)
+ },
+}
diff --git a/cmd/crio/daemon_linux.go b/cmd/crio/daemon_linux.go
new file mode 100644
index 000000000..884d3f269
--- /dev/null
+++ b/cmd/crio/daemon_linux.go
@@ -0,0 +1,20 @@
+// +build linux
+
+package main
+
+import (
+ systemdDaemon "github.com/coreos/go-systemd/daemon"
+ "github.com/sirupsen/logrus"
+)
+
+func sdNotify() {
+ if _, err := systemdDaemon.SdNotify(true, "READY=1"); err != nil {
+ logrus.Warnf("Failed to sd_notify systemd: %v", err)
+ }
+}
+
+// notifySystem sends a message to the host when the server is ready to be used
+func notifySystem() {
+ // Tell the init daemon we are accepting requests
+ go sdNotify()
+}
diff --git a/cmd/crio/main.go b/cmd/crio/main.go
new file mode 100644
index 000000000..e58adb114
--- /dev/null
+++ b/cmd/crio/main.go
@@ -0,0 +1,532 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/http"
+ _ "net/http/pprof"
+ "os"
+ "os/signal"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/containers/storage/pkg/reexec"
+ "github.com/kubernetes-incubator/cri-o/libkpod"
+ "github.com/kubernetes-incubator/cri-o/server"
+ "github.com/kubernetes-incubator/cri-o/version"
+ "github.com/opencontainers/selinux/go-selinux"
+ "github.com/sirupsen/logrus"
+ "github.com/soheilhy/cmux"
+ "github.com/urfave/cli"
+ "golang.org/x/sys/unix"
+ "google.golang.org/grpc"
+ "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
+)
+
+// gitCommit is the commit that the binary is being built from.
+// It will be populated by the Makefile.
+var gitCommit = ""
+
+func validateConfig(config *server.Config) error {
+ switch config.ImageVolumes {
+ case libkpod.ImageVolumesMkdir:
+ case libkpod.ImageVolumesIgnore:
+ case libkpod.ImageVolumesBind:
+ default:
+ return fmt.Errorf("Unrecognized image volume type specified")
+
+ }
+
+ // This needs to match the read buffer size in conmon
+ if config.LogSizeMax >= 0 && config.LogSizeMax < 8192 {
+ return fmt.Errorf("log size max should be negative or >= 8192")
+ }
+ return nil
+}
+
+func mergeConfig(config *server.Config, ctx *cli.Context) error {
+ // Don't parse the config if the user explicitly set it to "".
+ if path := ctx.GlobalString("config"); path != "" {
+ if err := config.UpdateFromFile(path); err != nil {
+ if ctx.GlobalIsSet("config") || !os.IsNotExist(err) {
+ return err
+ }
+
+ // We don't error out if --config wasn't explicitly set and the
+ // default doesn't exist. But we will log a warning about it, so
+ // the user doesn't miss it.
+ logrus.Warnf("default configuration file does not exist: %s", server.CrioConfigPath)
+ }
+ }
+
+ // Override options set with the CLI.
+ if ctx.GlobalIsSet("conmon") {
+ config.Conmon = ctx.GlobalString("conmon")
+ }
+ if ctx.GlobalIsSet("pause-command") {
+ config.PauseCommand = ctx.GlobalString("pause-command")
+ }
+ if ctx.GlobalIsSet("pause-image") {
+ config.PauseImage = ctx.GlobalString("pause-image")
+ }
+ if ctx.GlobalIsSet("signature-policy") {
+ config.SignaturePolicyPath = ctx.GlobalString("signature-policy")
+ }
+ if ctx.GlobalIsSet("root") {
+ config.Root = ctx.GlobalString("root")
+ }
+ if ctx.GlobalIsSet("runroot") {
+ config.RunRoot = ctx.GlobalString("runroot")
+ }
+ if ctx.GlobalIsSet("storage-driver") {
+ config.Storage = ctx.GlobalString("storage-driver")
+ }
+ if ctx.GlobalIsSet("storage-opt") {
+ config.StorageOptions = ctx.GlobalStringSlice("storage-opt")
+ }
+ if ctx.GlobalIsSet("file-locking") {
+ config.FileLocking = ctx.GlobalBool("file-locking")
+ }
+ if ctx.GlobalIsSet("insecure-registry") {
+ config.InsecureRegistries = ctx.GlobalStringSlice("insecure-registry")
+ }
+ if ctx.GlobalIsSet("registry") {
+ config.Registries = ctx.GlobalStringSlice("registry")
+ }
+ if ctx.GlobalIsSet("default-transport") {
+ config.DefaultTransport = ctx.GlobalString("default-transport")
+ }
+ if ctx.GlobalIsSet("listen") {
+ config.Listen = ctx.GlobalString("listen")
+ }
+ if ctx.GlobalIsSet("stream-address") {
+ config.StreamAddress = ctx.GlobalString("stream-address")
+ }
+ if ctx.GlobalIsSet("stream-port") {
+ config.StreamPort = ctx.GlobalString("stream-port")
+ }
+ if ctx.GlobalIsSet("runtime") {
+ config.Runtime = ctx.GlobalString("runtime")
+ }
+ if ctx.GlobalIsSet("selinux") {
+ config.SELinux = ctx.GlobalBool("selinux")
+ }
+ if ctx.GlobalIsSet("seccomp-profile") {
+ config.SeccompProfile = ctx.GlobalString("seccomp-profile")
+ }
+ if ctx.GlobalIsSet("apparmor-profile") {
+ config.ApparmorProfile = ctx.GlobalString("apparmor-profile")
+ }
+ if ctx.GlobalIsSet("cgroup-manager") {
+ config.CgroupManager = ctx.GlobalString("cgroup-manager")
+ }
+ if ctx.GlobalIsSet("hooks-dir-path") {
+ config.HooksDirPath = ctx.GlobalString("hooks-dir-path")
+ }
+ if ctx.GlobalIsSet("default-mounts") {
+ config.DefaultMounts = ctx.GlobalStringSlice("default-mounts")
+ }
+ if ctx.GlobalIsSet("pids-limit") {
+ config.PidsLimit = ctx.GlobalInt64("pids-limit")
+ }
+ if ctx.GlobalIsSet("log-size-max") {
+ config.LogSizeMax = ctx.GlobalInt64("log-size-max")
+ }
+ if ctx.GlobalIsSet("cni-config-dir") {
+ config.NetworkDir = ctx.GlobalString("cni-config-dir")
+ }
+ if ctx.GlobalIsSet("cni-plugin-dir") {
+ config.PluginDir = ctx.GlobalString("cni-plugin-dir")
+ }
+ if ctx.GlobalIsSet("image-volumes") {
+ config.ImageVolumes = libkpod.ImageVolumesType(ctx.GlobalString("image-volumes"))
+ }
+ return nil
+}
+
+func catchShutdown(gserver *grpc.Server, sserver *server.Server, hserver *http.Server, signalled *bool) {
+ sig := make(chan os.Signal, 10)
+ signal.Notify(sig, unix.SIGINT, unix.SIGTERM)
+ go func() {
+ for s := range sig {
+ switch s {
+ case unix.SIGINT:
+ logrus.Debugf("Caught SIGINT")
+ case unix.SIGTERM:
+ logrus.Debugf("Caught SIGTERM")
+ default:
+ continue
+ }
+ *signalled = true
+ gserver.GracefulStop()
+ hserver.Shutdown(context.Background())
+ // TODO(runcom): enable this after https://github.com/kubernetes/kubernetes/pull/51377
+ //sserver.StopStreamServer()
+ sserver.StopExitMonitor()
+ if err := sserver.Shutdown(); err != nil {
+ logrus.Warnf("error shutting down main service %v", err)
+ }
+ return
+ }
+ }()
+}
+
+func main() {
+ if reexec.Init() {
+ return
+ }
+ app := cli.NewApp()
+
+ var v []string
+ v = append(v, version.Version)
+ if gitCommit != "" {
+ v = append(v, fmt.Sprintf("commit: %s", gitCommit))
+ }
+ app.Name = "crio"
+ app.Usage = "crio server"
+ app.Version = strings.Join(v, "\n")
+ app.Metadata = map[string]interface{}{
+ "config": server.DefaultConfig(),
+ }
+
+ app.Flags = []cli.Flag{
+ cli.StringFlag{
+ Name: "config",
+ Value: server.CrioConfigPath,
+ Usage: "path to configuration file",
+ },
+ cli.StringFlag{
+ Name: "conmon",
+ Usage: "path to the conmon executable",
+ },
+ cli.StringFlag{
+ Name: "listen",
+ Usage: "path to crio socket",
+ },
+ cli.StringFlag{
+ Name: "stream-address",
+ Usage: "bind address for streaming socket",
+ },
+ cli.StringFlag{
+ Name: "stream-port",
+ Usage: "bind port for streaming socket (default: \"10010\")",
+ },
+ cli.StringFlag{
+ Name: "log",
+ Value: "",
+ Usage: "set the log file path where internal debug information is written",
+ },
+ cli.StringFlag{
+ Name: "log-format",
+ Value: "text",
+ Usage: "set the format used by logs ('text' (default), or 'json')",
+ },
+ cli.StringFlag{
+ Name: "log-level",
+ Usage: "log messages above specified level: debug, info (default), warn, error, fatal or panic",
+ },
+
+ cli.StringFlag{
+ Name: "pause-command",
+ Usage: "name of the pause command in the pause image",
+ },
+ cli.StringFlag{
+ Name: "pause-image",
+ Usage: "name of the pause image",
+ },
+ cli.StringFlag{
+ Name: "signature-policy",
+ Usage: "path to signature policy file",
+ },
+ cli.StringFlag{
+ Name: "root",
+ Usage: "crio root dir",
+ },
+ cli.StringFlag{
+ Name: "runroot",
+ Usage: "crio state dir",
+ },
+ cli.StringFlag{
+ Name: "storage-driver",
+ Usage: "storage driver",
+ },
+ cli.StringSliceFlag{
+ Name: "storage-opt",
+ Usage: "storage driver option",
+ },
+ cli.BoolFlag{
+ Name: "file-locking",
+ Usage: "enable or disable file-based locking",
+ },
+ cli.StringSliceFlag{
+ Name: "insecure-registry",
+ Usage: "whether to disable TLS verification for the given registry",
+ },
+ cli.StringSliceFlag{
+ Name: "registry",
+ Usage: "registry to be prepended when pulling unqualified images, can be specified multiple times",
+ },
+ cli.StringFlag{
+ Name: "default-transport",
+ Usage: "default transport",
+ },
+ cli.StringFlag{
+ Name: "runtime",
+ Usage: "OCI runtime path",
+ },
+ cli.StringFlag{
+ Name: "seccomp-profile",
+ Usage: "default seccomp profile path",
+ },
+ cli.StringFlag{
+ Name: "apparmor-profile",
+ Usage: "default apparmor profile name (default: \"crio-default\")",
+ },
+ cli.BoolFlag{
+ Name: "selinux",
+ Usage: "enable selinux support",
+ },
+ cli.StringFlag{
+ Name: "cgroup-manager",
+ Usage: "cgroup manager (cgroupfs or systemd)",
+ },
+ cli.Int64Flag{
+ Name: "pids-limit",
+ Value: libkpod.DefaultPidsLimit,
+ Usage: "maximum number of processes allowed in a container",
+ },
+ cli.Int64Flag{
+ Name: "log-size-max",
+ Value: libkpod.DefaultLogSizeMax,
+ Usage: "maximum log size in bytes for a container",
+ },
+ cli.StringFlag{
+ Name: "cni-config-dir",
+ Usage: "CNI configuration files directory",
+ },
+ cli.StringFlag{
+ Name: "cni-plugin-dir",
+ Usage: "CNI plugin binaries directory",
+ },
+ cli.StringFlag{
+ Name: "image-volumes",
+ Value: string(libkpod.ImageVolumesMkdir),
+ Usage: "image volume handling ('mkdir', 'bind', or 'ignore')",
+ },
+ cli.StringFlag{
+ Name: "hooks-dir-path",
+ Usage: "set the OCI hooks directory path",
+ Value: libkpod.DefaultHooksDirPath,
+ Hidden: true,
+ },
+ cli.StringSliceFlag{
+ Name: "default-mounts",
+ Usage: "add one or more default mount paths in the form host:container",
+ Hidden: true,
+ },
+ cli.BoolFlag{
+ Name: "profile",
+ Usage: "enable pprof remote profiler on localhost:6060",
+ },
+ cli.IntFlag{
+ Name: "profile-port",
+ Value: 6060,
+ Usage: "port for the pprof profiler",
+ },
+ cli.BoolFlag{
+ Name: "enable-metrics",
+ Usage: "enable metrics endpoint for the servier on localhost:9090",
+ },
+ cli.IntFlag{
+ Name: "metrics-port",
+ Value: 9090,
+ Usage: "port for the metrics endpoint",
+ },
+ }
+
+ sort.Sort(cli.FlagsByName(app.Flags))
+ sort.Sort(cli.FlagsByName(configCommand.Flags))
+
+ app.Commands = []cli.Command{
+ configCommand,
+ }
+
+ app.Before = func(c *cli.Context) error {
+ // Load the configuration file.
+ config := c.App.Metadata["config"].(*server.Config)
+ if err := mergeConfig(config, c); err != nil {
+ return err
+ }
+
+ if err := validateConfig(config); err != nil {
+ return err
+ }
+
+ cf := &logrus.TextFormatter{
+ TimestampFormat: "2006-01-02 15:04:05.000000000Z07:00",
+ FullTimestamp: true,
+ }
+
+ logrus.SetFormatter(cf)
+
+ if loglevel := c.GlobalString("log-level"); loglevel != "" {
+ level, err := logrus.ParseLevel(loglevel)
+ if err != nil {
+ return err
+ }
+
+ logrus.SetLevel(level)
+ }
+
+ if path := c.GlobalString("log"); path != "" {
+ f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0666)
+ if err != nil {
+ return err
+ }
+ logrus.SetOutput(f)
+ }
+
+ switch c.GlobalString("log-format") {
+ case "text":
+ // retain logrus's default.
+ case "json":
+ logrus.SetFormatter(new(logrus.JSONFormatter))
+ default:
+ return fmt.Errorf("unknown log-format %q", c.GlobalString("log-format"))
+ }
+
+ return nil
+ }
+
+ app.Action = func(c *cli.Context) error {
+ if c.GlobalBool("profile") {
+ profilePort := c.GlobalInt("profile-port")
+ profileEndpoint := fmt.Sprintf("localhost:%v", profilePort)
+ go func() {
+ http.ListenAndServe(profileEndpoint, nil)
+ }()
+ }
+
+ args := c.Args()
+ if len(args) > 0 {
+ for _, command := range app.Commands {
+ if args[0] == command.Name {
+ break
+ }
+ }
+ return fmt.Errorf("command %q not supported", args[0])
+ }
+
+ config := c.App.Metadata["config"].(*server.Config)
+
+ if !config.SELinux {
+ selinux.SetDisabled()
+ }
+
+ if _, err := os.Stat(config.Runtime); os.IsNotExist(err) {
+ // path to runtime does not exist
+ return fmt.Errorf("invalid --runtime value %q", err)
+ }
+
+ // Remove the socket if it already exists
+ if _, err := os.Stat(config.Listen); err == nil {
+ if err := os.Remove(config.Listen); err != nil {
+ logrus.Fatal(err)
+ }
+ }
+ lis, err := net.Listen("unix", config.Listen)
+ if err != nil {
+ logrus.Fatalf("failed to listen: %v", err)
+ }
+
+ s := grpc.NewServer()
+
+ service, err := server.New(config)
+ if err != nil {
+ logrus.Fatal(err)
+ }
+
+ if c.GlobalBool("enable-metrics") {
+ metricsPort := c.GlobalInt("metrics-port")
+ me, err := service.CreateMetricsEndpoint()
+ if err != nil {
+ logrus.Fatalf("Failed to create metrics endpoint: %v", err)
+ }
+ l, err := net.Listen("tcp", fmt.Sprintf(":%v", metricsPort))
+ if err != nil {
+ logrus.Fatalf("Failed to create listener for metrics: %v", err)
+ }
+ go func() {
+ if err := http.Serve(l, me); err != nil {
+ logrus.Fatalf("Failed to serve metrics endpoint: %v", err)
+ }
+ }()
+ }
+
+ runtime.RegisterRuntimeServiceServer(s, service)
+ runtime.RegisterImageServiceServer(s, service)
+
+ // after the daemon is done setting up we can notify systemd api
+ notifySystem()
+
+ go func() {
+ service.StartExitMonitor()
+ }()
+
+ m := cmux.New(lis)
+ grpcL := m.Match(cmux.HTTP2HeaderField("content-type", "application/grpc"))
+ httpL := m.Match(cmux.HTTP1Fast())
+
+ infoMux := service.GetInfoMux()
+ srv := &http.Server{
+ Handler: infoMux,
+ ReadTimeout: 5 * time.Second,
+ }
+
+ graceful := false
+ catchShutdown(s, service, srv, &graceful)
+
+ go s.Serve(grpcL)
+ go srv.Serve(httpL)
+
+ serverCloseCh := make(chan struct{})
+ go func() {
+ defer close(serverCloseCh)
+ if err := m.Serve(); err != nil {
+ if graceful && strings.Contains(strings.ToLower(err.Error()), "use of closed network connection") {
+ err = nil
+ } else {
+ logrus.Errorf("Failed to serve grpc grpc request: %v", err)
+ }
+ }
+ }()
+
+ // TODO(runcom): enable this after https://github.com/kubernetes/kubernetes/pull/51377
+ //streamServerCloseCh := service.StreamingServerCloseChan()
+ serverExitMonitorCh := service.ExitMonitorCloseChan()
+ select {
+ // TODO(runcom): enable this after https://github.com/kubernetes/kubernetes/pull/51377
+ //case <-streamServerCloseCh:
+ case <-serverExitMonitorCh:
+ case <-serverCloseCh:
+ }
+
+ service.Shutdown()
+
+ // TODO(runcom): enable this after https://github.com/kubernetes/kubernetes/pull/51377
+ //<-streamServerCloseCh
+ //logrus.Debug("closed stream server")
+ <-serverExitMonitorCh
+ logrus.Debug("closed exit monitor")
+ <-serverCloseCh
+ logrus.Debug("closed main server")
+
+ return nil
+ }
+
+ if err := app.Run(os.Args); err != nil {
+ logrus.Fatal(err)
+ }
+}
diff --git a/cmd/crioctl/container.go b/cmd/crioctl/container.go
new file mode 100644
index 000000000..e420e6c99
--- /dev/null
+++ b/cmd/crioctl/container.go
@@ -0,0 +1,653 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/url"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/kubernetes-incubator/cri-o/client"
+ "github.com/urfave/cli"
+ "golang.org/x/net/context"
+ remocommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
+ restclient "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/remotecommand"
+ pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
+)
+
+var containerCommand = cli.Command{
+ Name: "container",
+ Aliases: []string{"ctr"},
+ Subcommands: []cli.Command{
+ createContainerCommand,
+ inspectContainerCommand,
+ startContainerCommand,
+ stopContainerCommand,
+ removeContainerCommand,
+ containerStatusCommand,
+ listContainersCommand,
+ execSyncCommand,
+ execCommand,
+ },
+}
+
+type createOptions struct {
+ // configPath is path to the config for container
+ configPath string
+ // name sets the container name
+ name string
+ // podID of the container
+ podID string
+ // labels for the container
+ labels map[string]string
+}
+
+var createContainerCommand = cli.Command{
+ Name: "create",
+ Usage: "create a container",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "pod",
+ Usage: "the id of the pod sandbox to which the container belongs",
+ },
+ cli.StringFlag{
+ Name: "config",
+ Value: "config.json",
+ Usage: "the path of a container config file",
+ },
+ cli.StringFlag{
+ Name: "name",
+ Value: "",
+ Usage: "the name of the container",
+ },
+ cli.StringSliceFlag{
+ Name: "label",
+ Usage: "add key=value labels to the container",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ if !context.IsSet("pod") {
+ return fmt.Errorf("Please specify the id of the pod sandbox to which the container belongs via the --pod option")
+ }
+
+ opts := createOptions{
+ configPath: context.String("config"),
+ name: context.String("name"),
+ podID: context.String("pod"),
+ labels: make(map[string]string),
+ }
+
+ for _, l := range context.StringSlice("label") {
+ pair := strings.Split(l, "=")
+ if len(pair) != 2 {
+ return fmt.Errorf("incorrectly specified label: %v", l)
+ }
+ opts.labels[pair[0]] = pair[1]
+ }
+
+ // Test RuntimeServiceClient.CreateContainer
+ err = CreateContainer(client, opts)
+ if err != nil {
+ return fmt.Errorf("Creating container failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var startContainerCommand = cli.Command{
+ Name: "start",
+ Usage: "start a container",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the container",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ err = StartContainer(client, context.String("id"))
+ if err != nil {
+ return fmt.Errorf("Starting the container failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var stopContainerCommand = cli.Command{
+ Name: "stop",
+ Usage: "stop a container",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the container",
+ },
+ cli.Int64Flag{
+ Name: "timeout",
+ Value: 10,
+ Usage: "seconds to wait to kill the container after a graceful stop is requested",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ err = StopContainer(client, context.String("id"), context.Int64("timeout"))
+ if err != nil {
+ return fmt.Errorf("Stopping the container failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var removeContainerCommand = cli.Command{
+ Name: "remove",
+ Usage: "remove a container",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the container",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ err = RemoveContainer(client, context.String("id"))
+ if err != nil {
+ return fmt.Errorf("Removing the container failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var containerStatusCommand = cli.Command{
+ Name: "status",
+ Usage: "get the status of a container",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the container",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ err = ContainerStatus(client, context.String("id"))
+ if err != nil {
+ return fmt.Errorf("Getting the status of the container failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var execSyncCommand = cli.Command{
+ Name: "execsync",
+ Usage: "exec a command synchronously in a container",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the container",
+ },
+ cli.Int64Flag{
+ Name: "timeout",
+ Value: 0,
+ Usage: "timeout for the command",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ err = ExecSync(client, context.String("id"), context.Args(), context.Int64("timeout"))
+ if err != nil {
+ return fmt.Errorf("execing command in container failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var execCommand = cli.Command{
+ Name: "exec",
+ Usage: "prepare a streaming endpoint to execute a command in the container",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the container",
+ },
+ cli.BoolFlag{
+ Name: "tty",
+ Usage: "whether to use tty",
+ },
+ cli.BoolFlag{
+ Name: "stdin",
+ Usage: "whether to stream to stdin",
+ },
+ cli.BoolFlag{
+ Name: "url",
+ Usage: "do not exec command, just prepare streaming endpoint",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ err = Exec(client, context.String("id"), context.Bool("tty"), context.Bool("stdin"), context.Bool("url"), context.Args())
+ if err != nil {
+ return fmt.Errorf("execing command in container failed: %v", err)
+ }
+ return nil
+ },
+}
+
+type listOptions struct {
+ // id of the container
+ id string
+ // podID of the container
+ podID string
+ // state of the container
+ state string
+ // quiet is for listing just container IDs
+ quiet bool
+ // labels are selectors for the container
+ labels map[string]string
+}
+
+var listContainersCommand = cli.Command{
+ Name: "list",
+ Usage: "list containers",
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "quiet",
+ Usage: "list only container IDs",
+ },
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "filter by container id",
+ },
+ cli.StringFlag{
+ Name: "pod",
+ Value: "",
+ Usage: "filter by container pod id",
+ },
+ cli.StringFlag{
+ Name: "state",
+ Value: "",
+ Usage: "filter by container state",
+ },
+ cli.StringSliceFlag{
+ Name: "label",
+ Usage: "filter by key=value label",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+ opts := listOptions{
+ id: context.String("id"),
+ podID: context.String("pod"),
+ state: context.String("state"),
+ quiet: context.Bool("quiet"),
+ labels: make(map[string]string),
+ }
+
+ for _, l := range context.StringSlice("label") {
+ pair := strings.Split(l, "=")
+ if len(pair) != 2 {
+ return fmt.Errorf("incorrectly specified label: %v", l)
+ }
+ opts.labels[pair[0]] = pair[1]
+ }
+
+ err = ListContainers(client, opts)
+ if err != nil {
+ return fmt.Errorf("listing containers failed: %v", err)
+ }
+ return nil
+ },
+}
+
+// CreateContainer sends a CreateContainerRequest to the server, and parses
+// the returned CreateContainerResponse.
+func CreateContainer(client pb.RuntimeServiceClient, opts createOptions) error {
+ config, err := loadContainerConfig(opts.configPath)
+ if err != nil {
+ return err
+ }
+
+ // Override the name by the one specified through CLI
+ if opts.name != "" {
+ config.Metadata.Name = opts.name
+ }
+
+ for k, v := range opts.labels {
+ config.Labels[k] = v
+ }
+
+ r, err := client.CreateContainer(context.Background(), &pb.CreateContainerRequest{
+ PodSandboxId: opts.podID,
+ Config: config,
+ // TODO(runcom): this is missing PodSandboxConfig!!!
+ // we should/could find a way to retrieve it from the fs and set it here
+ })
+ if err != nil {
+ return err
+ }
+ fmt.Println(r.ContainerId)
+ return nil
+}
+
+// StartContainer sends a StartContainerRequest to the server, and parses
+// the returned StartContainerResponse.
+func StartContainer(client pb.RuntimeServiceClient, ID string) error {
+ if ID == "" {
+ return fmt.Errorf("ID cannot be empty")
+ }
+ _, err := client.StartContainer(context.Background(), &pb.StartContainerRequest{
+ ContainerId: ID,
+ })
+ if err != nil {
+ return err
+ }
+ fmt.Println(ID)
+ return nil
+}
+
+// StopContainer sends a StopContainerRequest to the server, and parses
+// the returned StopContainerResponse.
+func StopContainer(client pb.RuntimeServiceClient, ID string, timeout int64) error {
+ if ID == "" {
+ return fmt.Errorf("ID cannot be empty")
+ }
+ _, err := client.StopContainer(context.Background(), &pb.StopContainerRequest{
+ ContainerId: ID,
+ Timeout: timeout,
+ })
+ if err != nil {
+ return err
+ }
+ fmt.Println(ID)
+ return nil
+}
+
+// RemoveContainer sends a RemoveContainerRequest to the server, and parses
+// the returned RemoveContainerResponse.
+func RemoveContainer(client pb.RuntimeServiceClient, ID string) error {
+ if ID == "" {
+ return fmt.Errorf("ID cannot be empty")
+ }
+ _, err := client.RemoveContainer(context.Background(), &pb.RemoveContainerRequest{
+ ContainerId: ID,
+ })
+ if err != nil {
+ return err
+ }
+ fmt.Println(ID)
+ return nil
+}
+
+// ContainerStatus sends a ContainerStatusRequest to the server, and parses
+// the returned ContainerStatusResponse.
+func ContainerStatus(client pb.RuntimeServiceClient, ID string) error {
+ if ID == "" {
+ return fmt.Errorf("ID cannot be empty")
+ }
+ r, err := client.ContainerStatus(context.Background(), &pb.ContainerStatusRequest{
+ ContainerId: ID})
+ if err != nil {
+ return err
+ }
+ fmt.Printf("ID: %s\n", r.Status.Id)
+ if r.Status.Metadata != nil {
+ if r.Status.Metadata.Name != "" {
+ fmt.Printf("Name: %s\n", r.Status.Metadata.Name)
+ }
+ fmt.Printf("Attempt: %v\n", r.Status.Metadata.Attempt)
+ }
+ // TODO(mzylowski): print it prettier
+ fmt.Printf("Status: %s\n", r.Status.State)
+ ctm := time.Unix(0, r.Status.CreatedAt)
+ fmt.Printf("Created: %v\n", ctm)
+ stm := time.Unix(0, r.Status.StartedAt)
+ fmt.Printf("Started: %v\n", stm)
+ ftm := time.Unix(0, r.Status.FinishedAt)
+ fmt.Printf("Finished: %v\n", ftm)
+ fmt.Printf("Exit Code: %v\n", r.Status.ExitCode)
+ fmt.Printf("Reason: %v\n", r.Status.Reason)
+ if r.Status.Image != nil {
+ fmt.Printf("Image: %v\n", r.Status.Image.Image)
+ }
+ fmt.Printf("ImageRef: %v\n", r.Status.ImageRef)
+
+ return nil
+}
+
+// ExecSync sends an ExecSyncRequest to the server, and parses
+// the returned ExecSyncResponse.
+func ExecSync(client pb.RuntimeServiceClient, ID string, cmd []string, timeout int64) error {
+ if ID == "" {
+ return fmt.Errorf("ID cannot be empty")
+ }
+ r, err := client.ExecSync(context.Background(), &pb.ExecSyncRequest{
+ ContainerId: ID,
+ Cmd: cmd,
+ Timeout: timeout,
+ })
+ if err != nil {
+ return err
+ }
+ fmt.Println("Stdout:")
+ fmt.Println(string(r.Stdout))
+ fmt.Println("Stderr:")
+ fmt.Println(string(r.Stderr))
+ fmt.Printf("Exit code: %v\n", r.ExitCode)
+
+ return nil
+}
+
+// Exec sends an ExecRequest to the server, and parses
+// the returned ExecResponse.
+func Exec(client pb.RuntimeServiceClient, ID string, tty bool, stdin bool, urlOnly bool, cmd []string) error {
+ if ID == "" {
+ return fmt.Errorf("ID cannot be empty")
+ }
+ r, err := client.Exec(context.Background(), &pb.ExecRequest{
+ ContainerId: ID,
+ Cmd: cmd,
+ Tty: tty,
+ Stdin: stdin,
+ })
+ if err != nil {
+ return err
+ }
+
+ if urlOnly {
+ fmt.Println("URL:")
+ fmt.Println(r.Url)
+ return nil
+ }
+
+ execURL, err := url.Parse(r.Url)
+ if err != nil {
+ return err
+ }
+
+ streamExec, err := remotecommand.NewExecutor(&restclient.Config{}, "GET", execURL)
+ if err != nil {
+ return err
+ }
+
+ options := remotecommand.StreamOptions{
+ SupportedProtocols: remocommandconsts.SupportedStreamingProtocols,
+ Stdout: os.Stdout,
+ Stderr: os.Stderr,
+ Tty: tty,
+ }
+
+ if stdin {
+ options.Stdin = os.Stdin
+ }
+
+ return streamExec.Stream(options)
+}
+
+// ListContainers sends a ListContainerRequest to the server, and parses
+// the returned ListContainerResponse.
+func ListContainers(client pb.RuntimeServiceClient, opts listOptions) error {
+ filter := &pb.ContainerFilter{}
+ if opts.id != "" {
+ filter.Id = opts.id
+ }
+ if opts.podID != "" {
+ filter.PodSandboxId = opts.podID
+ }
+ if opts.state != "" {
+ st := &pb.ContainerStateValue{}
+ st.State = pb.ContainerState_CONTAINER_UNKNOWN
+ switch opts.state {
+ case "created":
+ st.State = pb.ContainerState_CONTAINER_CREATED
+ filter.State = st
+ case "running":
+ st.State = pb.ContainerState_CONTAINER_RUNNING
+ filter.State = st
+ case "stopped":
+ st.State = pb.ContainerState_CONTAINER_EXITED
+ filter.State = st
+ default:
+ log.Fatalf("--state should be one of created, running or stopped")
+ }
+ }
+ if opts.labels != nil {
+ filter.LabelSelector = opts.labels
+ }
+ r, err := client.ListContainers(context.Background(), &pb.ListContainersRequest{
+ Filter: filter,
+ })
+ if err != nil {
+ return err
+ }
+ for _, c := range r.GetContainers() {
+ if opts.quiet {
+ fmt.Println(c.Id)
+ continue
+ }
+ fmt.Printf("ID: %s\n", c.Id)
+ fmt.Printf("Pod: %s\n", c.PodSandboxId)
+ if c.Metadata != nil {
+ if c.Metadata.Name != "" {
+ fmt.Printf("Name: %s\n", c.Metadata.Name)
+ }
+ fmt.Printf("Attempt: %v\n", c.Metadata.Attempt)
+ }
+ fmt.Printf("Status: %s\n", c.State)
+ if c.Image != nil {
+ fmt.Printf("Image: %s\n", c.Image.Image)
+ }
+ ctm := time.Unix(0, c.CreatedAt)
+ fmt.Printf("Created: %v\n", ctm)
+ if c.Labels != nil {
+ fmt.Println("Labels:")
+ for _, k := range getSortedKeys(c.Labels) {
+ fmt.Printf("\t%s -> %s\n", k, c.Labels[k])
+ }
+ }
+ if c.Annotations != nil {
+ fmt.Println("Annotations:")
+ for _, k := range getSortedKeys(c.Annotations) {
+ fmt.Printf("\t%s -> %s\n", k, c.Annotations[k])
+ }
+ }
+ fmt.Println()
+ }
+ return nil
+}
+
+var inspectContainerCommand = cli.Command{
+ Name: "inspect",
+ Usage: "get container info from crio daemon",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the container",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ ID := context.String("id")
+ if ID == "" {
+ return fmt.Errorf("ID cannot be empty")
+ }
+ c, err := client.New(context.GlobalString("connect"))
+ if err != nil {
+ return err
+ }
+
+ cInfo, err := c.ContainerInfo(ID)
+ if err != nil {
+ return err
+ }
+
+ jsonBytes, err := json.MarshalIndent(cInfo, "", " ")
+ if err != nil {
+ return err
+ }
+ fmt.Println(string(jsonBytes))
+ return nil
+ },
+}
diff --git a/cmd/crioctl/image.go b/cmd/crioctl/image.go
new file mode 100644
index 000000000..426c67e9d
--- /dev/null
+++ b/cmd/crioctl/image.go
@@ -0,0 +1,173 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/urfave/cli"
+ "golang.org/x/net/context"
+ pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
+)
+
+var imageCommand = cli.Command{
+ Name: "image",
+ Subcommands: []cli.Command{
+ pullImageCommand,
+ listImageCommand,
+ imageStatusCommand,
+ removeImageCommand,
+ },
+}
+
+var pullImageCommand = cli.Command{
+ Name: "pull",
+ Usage: "pull an image",
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewImageServiceClient(conn)
+
+ _, err = PullImage(client, context.Args().Get(0))
+ if err != nil {
+ return fmt.Errorf("pulling image failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var listImageCommand = cli.Command{
+ Name: "list",
+ Usage: "list images",
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "quiet",
+ Usage: "list only image IDs",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewImageServiceClient(conn)
+
+ r, err := ListImages(client, context.Args().Get(0))
+ if err != nil {
+ return fmt.Errorf("listing images failed: %v", err)
+ }
+ quiet := context.Bool("quiet")
+ for _, image := range r.Images {
+ if quiet {
+ fmt.Printf("%s\n", image.Id)
+ continue
+ }
+ fmt.Printf("ID: %s\n", image.Id)
+ for _, tag := range image.RepoTags {
+ fmt.Printf("Tag: %s\n", tag)
+ }
+ for _, digest := range image.RepoDigests {
+ fmt.Printf("Digest: %s\n", digest)
+ }
+ if image.Size_ != 0 {
+ fmt.Printf("Size: %d\n", image.Size_)
+ }
+ }
+ return nil
+ },
+}
+
+var imageStatusCommand = cli.Command{
+ Name: "status",
+ Usage: "return the status of an image",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Usage: "id of the image",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewImageServiceClient(conn)
+
+ r, err := ImageStatus(client, context.String("id"))
+ if err != nil {
+ return fmt.Errorf("image status request failed: %v", err)
+ }
+ image := r.Image
+ if image == nil {
+ return fmt.Errorf("no such image present")
+ }
+ fmt.Printf("ID: %s\n", image.Id)
+ for _, tag := range image.RepoTags {
+ fmt.Printf("Tag: %s\n", tag)
+ }
+ for _, digest := range image.RepoDigests {
+ fmt.Printf("Digest: %s\n", digest)
+ }
+ fmt.Printf("Size: %d\n", image.Size_)
+ return nil
+ },
+}
+var removeImageCommand = cli.Command{
+ Name: "remove",
+ Usage: "remove an image",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the image",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewImageServiceClient(conn)
+
+ _, err = RemoveImage(client, context.String("id"))
+ if err != nil {
+ return fmt.Errorf("removing the image failed: %v", err)
+ }
+ return nil
+ },
+}
+
+// PullImage sends a PullImageRequest to the server, and parses
+// the returned PullImageResponse.
+func PullImage(client pb.ImageServiceClient, image string) (*pb.PullImageResponse, error) {
+ return client.PullImage(context.Background(), &pb.PullImageRequest{Image: &pb.ImageSpec{Image: image}})
+}
+
+// ListImages sends a ListImagesRequest to the server, and parses
+// the returned ListImagesResponse.
+func ListImages(client pb.ImageServiceClient, image string) (*pb.ListImagesResponse, error) {
+ return client.ListImages(context.Background(), &pb.ListImagesRequest{Filter: &pb.ImageFilter{Image: &pb.ImageSpec{Image: image}}})
+}
+
+// ImageStatus sends an ImageStatusRequest to the server, and parses
+// the returned ImageStatusResponse.
+func ImageStatus(client pb.ImageServiceClient, image string) (*pb.ImageStatusResponse, error) {
+ return client.ImageStatus(context.Background(), &pb.ImageStatusRequest{Image: &pb.ImageSpec{Image: image}})
+}
+
+// RemoveImage sends a RemoveImageRequest to the server, and parses
+// the returned RemoveImageResponse.
+func RemoveImage(client pb.ImageServiceClient, image string) (*pb.RemoveImageResponse, error) {
+ if image == "" {
+ return nil, fmt.Errorf("ID cannot be empty")
+ }
+ return client.RemoveImage(context.Background(), &pb.RemoveImageRequest{Image: &pb.ImageSpec{Image: image}})
+}
diff --git a/cmd/crioctl/info.go b/cmd/crioctl/info.go
new file mode 100644
index 000000000..1f06f594a
--- /dev/null
+++ b/cmd/crioctl/info.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/kubernetes-incubator/cri-o/client"
+ "github.com/urfave/cli"
+)
+
+var infoCommand = cli.Command{
+ Name: "info",
+ Usage: "get crio daemon info",
+ Action: func(context *cli.Context) error {
+ c, err := client.New(context.GlobalString("connect"))
+ if err != nil {
+ return err
+ }
+ di, err := c.DaemonInfo()
+ if err != nil {
+ return err
+ }
+
+ jsonBytes, err := json.MarshalIndent(di, "", " ")
+ if err != nil {
+ return err
+ }
+ fmt.Println(string(jsonBytes))
+ return nil
+ },
+}
diff --git a/cmd/crioctl/main.go b/cmd/crioctl/main.go
new file mode 100644
index 000000000..3d77867fe
--- /dev/null
+++ b/cmd/crioctl/main.go
@@ -0,0 +1,113 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+ "google.golang.org/grpc"
+ pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
+)
+
+// This is populated by the Makefile from the VERSION file
+// in the repository
+var version = ""
+
+// gitCommit is the commit that the binary is being built from.
+// It will be populated by the Makefile.
+var gitCommit = ""
+
+func getClientConnection(context *cli.Context) (*grpc.ClientConn, error) {
+ conn, err := grpc.Dial(context.GlobalString("connect"), grpc.WithInsecure(), grpc.WithTimeout(context.GlobalDuration("timeout")),
+ grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) {
+ return net.DialTimeout("unix", addr, timeout)
+ }))
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect: %v", err)
+ }
+ return conn, nil
+}
+
+func openFile(path string) (*os.File, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil, fmt.Errorf("config at %s not found", path)
+ }
+ return nil, err
+ }
+ return f, nil
+}
+
+func loadPodSandboxConfig(path string) (*pb.PodSandboxConfig, error) {
+ f, err := openFile(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ var config pb.PodSandboxConfig
+ if err := json.NewDecoder(f).Decode(&config); err != nil {
+ return nil, err
+ }
+ return &config, nil
+}
+
+func loadContainerConfig(path string) (*pb.ContainerConfig, error) {
+ f, err := openFile(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ var config pb.ContainerConfig
+ if err := json.NewDecoder(f).Decode(&config); err != nil {
+ return nil, err
+ }
+ return &config, nil
+}
+
+func main() {
+ app := cli.NewApp()
+ var v []string
+ if version != "" {
+ v = append(v, version)
+ }
+ if gitCommit != "" {
+ v = append(v, fmt.Sprintf("commit: %s", gitCommit))
+ }
+
+ app.Name = "crioctl"
+ app.Usage = "client for crio"
+ app.Version = strings.Join(v, "\n")
+
+ app.Commands = []cli.Command{
+ podSandboxCommand,
+ containerCommand,
+ runtimeVersionCommand,
+ imageCommand,
+ infoCommand,
+ }
+
+ app.Flags = []cli.Flag{
+ cli.StringFlag{
+ Name: "connect",
+ Value: "/var/run/crio.sock",
+ Usage: "Socket to connect to",
+ },
+ cli.DurationFlag{
+ Name: "timeout",
+ Value: 10 * time.Second,
+ Usage: "Timeout of connecting to server",
+ },
+ }
+
+ if err := app.Run(os.Args); err != nil {
+ logrus.Fatal(err)
+ }
+}
diff --git a/cmd/crioctl/sandbox.go b/cmd/crioctl/sandbox.go
new file mode 100644
index 000000000..e44183be3
--- /dev/null
+++ b/cmd/crioctl/sandbox.go
@@ -0,0 +1,386 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/urfave/cli"
+ "golang.org/x/net/context"
+ pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
+)
+
+var podSandboxCommand = cli.Command{
+ Name: "pod",
+ Subcommands: []cli.Command{
+ runPodSandboxCommand,
+ stopPodSandboxCommand,
+ removePodSandboxCommand,
+ podSandboxStatusCommand,
+ listPodSandboxCommand,
+ },
+}
+
+var runPodSandboxCommand = cli.Command{
+ Name: "run",
+ Usage: "run a pod",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "config",
+ Value: "",
+ Usage: "the path of a pod sandbox config file",
+ },
+ cli.StringFlag{
+ Name: "name",
+ Value: "",
+ Usage: "the name of the pod sandbox",
+ },
+ cli.StringSliceFlag{
+ Name: "label",
+ Usage: "add key=value labels to the container",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ opts := createOptions{
+ configPath: context.String("config"),
+ name: context.String("name"),
+ labels: make(map[string]string),
+ }
+
+ for _, l := range context.StringSlice("label") {
+ pair := strings.Split(l, "=")
+ if len(pair) != 2 {
+ return fmt.Errorf("incorrectly specified label: %v", l)
+ }
+ opts.labels[pair[0]] = pair[1]
+ }
+
+ // Test RuntimeServiceClient.RunPodSandbox
+ err = RunPodSandbox(client, opts)
+ if err != nil {
+ return fmt.Errorf("Creating the pod sandbox failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var stopPodSandboxCommand = cli.Command{
+ Name: "stop",
+ Usage: "stop a pod sandbox",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the pod sandbox",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ err = StopPodSandbox(client, context.String("id"))
+ if err != nil {
+ return fmt.Errorf("stopping the pod sandbox failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var removePodSandboxCommand = cli.Command{
+ Name: "remove",
+ Usage: "remove a pod sandbox",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the pod sandbox",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ err = RemovePodSandbox(client, context.String("id"))
+ if err != nil {
+ return fmt.Errorf("removing the pod sandbox failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var podSandboxStatusCommand = cli.Command{
+ Name: "status",
+ Usage: "return the status of a pod",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "id of the pod",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ err = PodSandboxStatus(client, context.String("id"))
+ if err != nil {
+ return fmt.Errorf("getting the pod sandbox status failed: %v", err)
+ }
+ return nil
+ },
+}
+
+var listPodSandboxCommand = cli.Command{
+ Name: "list",
+ Usage: "list pod sandboxes",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "id",
+ Value: "",
+ Usage: "filter by pod sandbox id",
+ },
+ cli.StringFlag{
+ Name: "state",
+ Value: "",
+ Usage: "filter by pod sandbox state",
+ },
+ cli.StringSliceFlag{
+ Name: "label",
+ Usage: "filter by key=value label",
+ },
+ cli.BoolFlag{
+ Name: "quiet",
+ Usage: "list only pod IDs",
+ },
+ },
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ opts := listOptions{
+ id: context.String("id"),
+ state: context.String("state"),
+ quiet: context.Bool("quiet"),
+ labels: make(map[string]string),
+ }
+
+ for _, l := range context.StringSlice("label") {
+ pair := strings.Split(l, "=")
+ if len(pair) != 2 {
+ return fmt.Errorf("incorrectly specified label: %v", l)
+ }
+ opts.labels[pair[0]] = pair[1]
+ }
+
+ err = ListPodSandboxes(client, opts)
+ if err != nil {
+ return fmt.Errorf("listing pod sandboxes failed: %v", err)
+ }
+ return nil
+ },
+}
+
+// RunPodSandbox sends a RunPodSandboxRequest to the server, and parses
+// the returned RunPodSandboxResponse.
+func RunPodSandbox(client pb.RuntimeServiceClient, opts createOptions) error {
+ config, err := loadPodSandboxConfig(opts.configPath)
+ if err != nil {
+ return err
+ }
+
+ // Override the name by the one specified through CLI
+ if opts.name != "" {
+ config.Metadata.Name = opts.name
+ }
+
+ for k, v := range opts.labels {
+ config.Labels[k] = v
+ }
+
+ r, err := client.RunPodSandbox(context.Background(), &pb.RunPodSandboxRequest{Config: config})
+ if err != nil {
+ return err
+ }
+ fmt.Println(r.PodSandboxId)
+ return nil
+}
+
+// StopPodSandbox sends a StopPodSandboxRequest to the server, and parses
+// the returned StopPodSandboxResponse.
+func StopPodSandbox(client pb.RuntimeServiceClient, ID string) error {
+ if ID == "" {
+ return fmt.Errorf("ID cannot be empty")
+ }
+ _, err := client.StopPodSandbox(context.Background(), &pb.StopPodSandboxRequest{PodSandboxId: ID})
+ if err != nil {
+ return err
+ }
+ fmt.Println(ID)
+ return nil
+}
+
+// RemovePodSandbox sends a RemovePodSandboxRequest to the server, and parses
+// the returned RemovePodSandboxResponse.
+func RemovePodSandbox(client pb.RuntimeServiceClient, ID string) error {
+ if ID == "" {
+ return fmt.Errorf("ID cannot be empty")
+ }
+ _, err := client.RemovePodSandbox(context.Background(), &pb.RemovePodSandboxRequest{PodSandboxId: ID})
+ if err != nil {
+ return err
+ }
+ fmt.Println(ID)
+ return nil
+}
+
+// PodSandboxStatus sends a PodSandboxStatusRequest to the server, and parses
+// the returned PodSandboxStatusResponse.
+func PodSandboxStatus(client pb.RuntimeServiceClient, ID string) error {
+ if ID == "" {
+ return fmt.Errorf("ID cannot be empty")
+ }
+ r, err := client.PodSandboxStatus(context.Background(), &pb.PodSandboxStatusRequest{PodSandboxId: ID})
+ if err != nil {
+ return err
+ }
+ fmt.Printf("ID: %s\n", r.Status.Id)
+ if r.Status.Metadata != nil {
+ if r.Status.Metadata.Name != "" {
+ fmt.Printf("Name: %s\n", r.Status.Metadata.Name)
+ }
+ if r.Status.Metadata.Uid != "" {
+ fmt.Printf("UID: %s\n", r.Status.Metadata.Uid)
+ }
+ if r.Status.Metadata.Namespace != "" {
+ fmt.Printf("Namespace: %s\n", r.Status.Metadata.Namespace)
+ }
+ fmt.Printf("Attempt: %v\n", r.Status.Metadata.Attempt)
+ }
+ fmt.Printf("Status: %s\n", r.Status.State)
+ ctm := time.Unix(0, r.Status.CreatedAt)
+ fmt.Printf("Created: %v\n", ctm)
+ if r.Status.Network != nil {
+ fmt.Printf("IP Address: %v\n", r.Status.Network.Ip)
+ }
+ if r.Status.Labels != nil {
+ fmt.Println("Labels:")
+ for _, k := range getSortedKeys(r.Status.Labels) {
+ fmt.Printf("\t%s -> %s\n", k, r.Status.Labels[k])
+ }
+ }
+ if r.Status.Annotations != nil {
+ fmt.Println("Annotations:")
+ for _, k := range getSortedKeys(r.Status.Annotations) {
+ fmt.Printf("\t%s -> %s\n", k, r.Status.Annotations[k])
+ }
+ }
+ return nil
+}
+
+// ListPodSandboxes sends a ListPodSandboxRequest to the server, and parses
+// the returned ListPodSandboxResponse.
+func ListPodSandboxes(client pb.RuntimeServiceClient, opts listOptions) error {
+ filter := &pb.PodSandboxFilter{}
+ if opts.id != "" {
+ filter.Id = opts.id
+ }
+ if opts.state != "" {
+ st := &pb.PodSandboxStateValue{}
+ st.State = pb.PodSandboxState_SANDBOX_NOTREADY
+ switch opts.state {
+ case "ready":
+ st.State = pb.PodSandboxState_SANDBOX_READY
+ filter.State = st
+ case "notready":
+ st.State = pb.PodSandboxState_SANDBOX_NOTREADY
+ filter.State = st
+ default:
+ log.Fatalf("--state should be ready or notready")
+ }
+ }
+ if opts.labels != nil {
+ filter.LabelSelector = opts.labels
+ }
+ r, err := client.ListPodSandbox(context.Background(), &pb.ListPodSandboxRequest{
+ Filter: filter,
+ })
+ if err != nil {
+ return err
+ }
+ for _, pod := range r.Items {
+ if opts.quiet {
+ fmt.Println(pod.Id)
+ continue
+ }
+ fmt.Printf("ID: %s\n", pod.Id)
+ if pod.Metadata != nil {
+ if pod.Metadata.Name != "" {
+ fmt.Printf("Name: %s\n", pod.Metadata.Name)
+ }
+ if pod.Metadata.Uid != "" {
+ fmt.Printf("UID: %s\n", pod.Metadata.Uid)
+ }
+ if pod.Metadata.Namespace != "" {
+ fmt.Printf("Namespace: %s\n", pod.Metadata.Namespace)
+ }
+ fmt.Printf("Attempt: %v\n", pod.Metadata.Attempt)
+ }
+ fmt.Printf("Status: %s\n", pod.State)
+ ctm := time.Unix(0, pod.CreatedAt)
+ fmt.Printf("Created: %v\n", ctm)
+ if pod.Labels != nil {
+ fmt.Println("Labels:")
+ for _, k := range getSortedKeys(pod.Labels) {
+ fmt.Printf("\t%s -> %s\n", k, pod.Labels[k])
+ }
+ }
+ if pod.Annotations != nil {
+ fmt.Println("Annotations:")
+ for _, k := range getSortedKeys(pod.Annotations) {
+ fmt.Printf("\t%s -> %s\n", k, pod.Annotations[k])
+ }
+ }
+ fmt.Println()
+ }
+ return nil
+}
+
+func getSortedKeys(m map[string]string) []string {
+ var keys []string
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ return keys
+}
diff --git a/cmd/crioctl/system.go b/cmd/crioctl/system.go
new file mode 100644
index 000000000..7e04161c2
--- /dev/null
+++ b/cmd/crioctl/system.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/urfave/cli"
+ "golang.org/x/net/context"
+ pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
+)
+
+var runtimeVersionCommand = cli.Command{
+ Name: "runtimeversion",
+ Usage: "get runtime version information",
+ Action: func(context *cli.Context) error {
+ // Set up a connection to the server.
+ conn, err := getClientConnection(context)
+ if err != nil {
+ return fmt.Errorf("failed to connect: %v", err)
+ }
+ defer conn.Close()
+ client := pb.NewRuntimeServiceClient(conn)
+
+ // Test RuntimeServiceClient.Version
+ version := "v1alpha1"
+ err = Version(client, version)
+ if err != nil {
+ return fmt.Errorf("Getting the runtime version failed: %v", err)
+ }
+ return nil
+ },
+}
+
+// Version sends a VersionRequest to the server, and parses the returned VersionResponse.
+func Version(client pb.RuntimeServiceClient, version string) error {
+ r, err := client.Version(context.Background(), &pb.VersionRequest{Version: version})
+ if err != nil {
+ return err
+ }
+ fmt.Printf("VersionResponse: Version: %s, RuntimeName: %s, RuntimeVersion: %s, RuntimeApiVersion: %s\n", r.Version, r.RuntimeName, r.RuntimeVersion, r.RuntimeApiVersion)
+ return nil
+}
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 = "<missing>"
+ }
+ 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 := "<none>"
+ 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 != "<none>" {
+ return true
+ }
+ if common.IsTrue(params.Dangling) && params.ImageName == "<none>" {
+ 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
+}