diff options
author | Matthew Heon <matthew.heon@gmail.com> | 2017-11-01 11:24:59 -0400 |
---|---|---|
committer | Matthew Heon <matthew.heon@gmail.com> | 2017-11-01 11:24:59 -0400 |
commit | a031b83a09a8628435317a03f199cdc18b78262f (patch) | |
tree | bc017a96769ce6de33745b8b0b1304ccf38e9df0 /cmd | |
parent | 2b74391cd5281f6fdf391ff8ad50fd1490f6bf89 (diff) | |
download | podman-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')
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 +} |