diff options
Diffstat (limited to 'cmd')
23 files changed, 2632 insertions, 57 deletions
diff --git a/cmd/podman/shared/create.go b/cmd/podman/shared/create.go index 5fa8d6c0b..68a36d967 100644 --- a/cmd/podman/shared/create.go +++ b/cmd/podman/shared/create.go @@ -538,14 +538,16 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod. } } - // env overrides any previous variables - cmdlineEnv := c.StringSlice("env") - if len(cmdlineEnv) > 0 { - parsedEnv, err := envLib.ParseSlice(cmdlineEnv) - if err != nil { - return nil, err + if c.IsSet("env") { + // env overrides any previous variables + cmdlineEnv := c.StringSlice("env") + if len(cmdlineEnv) > 0 { + parsedEnv, err := envLib.ParseSlice(cmdlineEnv) + if err != nil { + return nil, err + } + env = envLib.Join(env, parsedEnv) } - env = envLib.Join(env, parsedEnv) } // LABEL VARIABLES @@ -636,10 +638,13 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod. if err != nil { return nil, errors.Wrapf(err, "unable to translate --shm-size") } - // Verify the additional hosts are in correct format - for _, host := range c.StringSlice("add-host") { - if _, err := parse.ValidateExtraHost(host); err != nil { - return nil, err + + if c.IsSet("add-host") { + // Verify the additional hosts are in correct format + for _, host := range c.StringSlice("add-host") { + if _, err := parse.ValidateExtraHost(host); err != nil { + return nil, err + } } } diff --git a/cmd/podmanV2/Makefile b/cmd/podmanV2/Makefile index 147a78d9c..b847a9385 100644 --- a/cmd/podmanV2/Makefile +++ b/cmd/podmanV2/Makefile @@ -1,2 +1,2 @@ all: - GO111MODULE=off go build -tags 'ABISupport' + CGO_ENABLED=1 GO111MODULE=off go build -tags 'ABISupport systemd seccomp' diff --git a/cmd/podmanV2/common/create.go b/cmd/podmanV2/common/create.go new file mode 100644 index 000000000..f81d021c8 --- /dev/null +++ b/cmd/podmanV2/common/create.go @@ -0,0 +1,534 @@ +package common + +import ( + "fmt" + "os" + + buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" +) + +const ( + sizeWithUnitFormat = "(format: `<number>[<unit>]`, where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes))" +) + +var ( + defaultContainerConfig = getDefaultContainerConfig() +) + +func getDefaultContainerConfig() *config.Config { + defaultContainerConfig, err := config.Default() + if err != nil { + logrus.Error(err) + os.Exit(1) + } + return defaultContainerConfig +} + +func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { + //createFlags := c.Flags() + createFlags := pflag.FlagSet{} + createFlags.StringSliceVar( + &cf.Annotation, + "annotation", []string{}, + "Add annotations to container (key:value)", + ) + createFlags.StringSliceVarP( + &cf.Attach, + "attach", "a", []string{}, + "Attach to STDIN, STDOUT or STDERR", + ) + createFlags.StringVar( + &cf.Authfile, + "authfile", buildahcli.GetDefaultAuthFile(), + "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override", + ) + createFlags.StringVar( + &cf.BlkIOWeight, + "blkio-weight", "", + "Block IO weight (relative weight) accepts a weight value between 10 and 1000.", + ) + createFlags.StringSliceVar( + &cf.BlkIOWeightDevice, + "blkio-weight-device", []string{}, + "Block IO weight (relative device weight, format: `DEVICE_NAME:WEIGHT`)", + ) + createFlags.StringSliceVar( + &cf.CapAdd, + "cap-add", []string{}, + "Add capabilities to the container", + ) + createFlags.StringSliceVar( + &cf.CapDrop, + "cap-drop", []string{}, + "Drop capabilities from the container", + ) + createFlags.StringVar( + &cf.CGroupsNS, + "cgroupns", getDefaultCgroupNS(), + "cgroup namespace to use", + ) + createFlags.StringVar( + &cf.CGroups, + "cgroups", "enabled", + `control container cgroup configuration ("enabled"|"disabled"|"no-conmon")`, + ) + createFlags.StringVar( + &cf.CGroupParent, + "cgroup-parent", "", + "Optional parent cgroup for the container", + ) + createFlags.StringVar( + &cf.CIDFile, + "cidfile", "", + "Write the container ID to the file", + ) + createFlags.StringVar( + &cf.ConmonPIDFile, + "conmon-pidfile", "", + "Path to the file that will receive the PID of conmon", + ) + createFlags.Uint64Var( + &cf.CPUPeriod, + "cpu-period", 0, + "Limit the CPU CFS (Completely Fair Scheduler) period", + ) + createFlags.Int64Var( + &cf.CPUQuota, + "cpu-quota", 0, + "Limit the CPU CFS (Completely Fair Scheduler) quota", + ) + createFlags.Uint64Var( + &cf.CPURTPeriod, + "cpu-rt-period", 0, + "Limit the CPU real-time period in microseconds", + ) + createFlags.Int64Var( + &cf.CPURTRuntime, + "cpu-rt-runtime", 0, + "Limit the CPU real-time runtime in microseconds", + ) + createFlags.Uint64Var( + &cf.CPUShares, + "cpu-shares", 0, + "CPU shares (relative weight)", + ) + createFlags.Float64Var( + &cf.CPUS, + "cpus", 0, + "Number of CPUs. The default is 0.000 which means no limit", + ) + createFlags.StringVar( + &cf.CPUSetCPUs, + "cpuset-cpus", "", + "CPUs in which to allow execution (0-3, 0,1)", + ) + createFlags.StringVar( + &cf.CPUSetMems, + "cpuset-mems", "", + "Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems.", + ) + createFlags.BoolVarP( + &cf.Detach, + "detach", "d", false, + "Run container in background and print container ID", + ) + createFlags.StringVar( + &cf.DetachKeys, + "detach-keys", GetDefaultDetachKeys(), + "Override the key sequence for detaching a container. Format is a single character `[a-Z]` or a comma separated sequence of `ctrl-<value>`, where `<value>` is one of: `a-cf`, `@`, `^`, `[`, `\\`, `]`, `^` or `_`", + ) + createFlags.StringSliceVar( + &cf.Device, + "device", getDefaultDevices(), + fmt.Sprintf("Add a host device to the container"), + ) + createFlags.StringSliceVar( + &cf.DeviceCGroupRule, + "device-cgroup-rule", []string{}, + "Add a rule to the cgroup allowed devices list", + ) + createFlags.StringSliceVar( + &cf.DeviceReadBPs, + "device-read-bps", []string{}, + "Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb)", + ) + createFlags.StringSliceVar( + &cf.DeviceReadIOPs, + "device-read-iops", []string{}, + "Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000)", + ) + createFlags.StringSliceVar( + &cf.DeviceWriteBPs, + "device-write-bps", []string{}, + "Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb)", + ) + createFlags.StringSliceVar( + &cf.DeviceWriteIOPs, + "device-write-iops", []string{}, + "Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000)", + ) + createFlags.StringVar( + &cf.Entrypoint, + "entrypoint", "", + "Overwrite the default ENTRYPOINT of the image", + ) + createFlags.StringArrayVarP( + &cf.env, + "env", "e", getDefaultEnv(), + "Set environment variables in container", + ) + createFlags.BoolVar( + &cf.EnvHost, + "env-host", false, "Use all current host environment variables in container", + ) + createFlags.StringSliceVar( + &cf.EnvFile, + "env-file", []string{}, + "Read in a file of environment variables", + ) + createFlags.StringSliceVar( + &cf.Expose, + "expose", []string{}, + "Expose a port or a range of ports", + ) + createFlags.StringSliceVar( + &cf.GIDMap, + "gidmap", []string{}, + "GID map to use for the user namespace", + ) + createFlags.StringSliceVar( + &cf.GroupAdd, + "group-add", []string{}, + "Add additional groups to join", + ) + createFlags.Bool( + "help", false, "", + ) + createFlags.StringVar( + &cf.HealthCmd, + "health-cmd", "", + "set a healthcheck command for the container ('none' disables the existing healthcheck)", + ) + createFlags.StringVar( + &cf.HealthInterval, + "health-interval", cliconfig.DefaultHealthCheckInterval, + "set an interval for the healthchecks (a value of disable results in no automatic timer setup)", + ) + createFlags.UintVar( + &cf.HealthRetries, + "health-retries", cliconfig.DefaultHealthCheckRetries, + "the number of retries allowed before a healthcheck is considered to be unhealthy", + ) + createFlags.StringVar( + &cf.HealthStartPeriod, + "health-start-period", cliconfig.DefaultHealthCheckStartPeriod, + "the initialization time needed for a container to bootstrap", + ) + createFlags.StringVar( + &cf.HealthTimeout, + "health-timeout", cliconfig.DefaultHealthCheckTimeout, + "the maximum time allowed to complete the healthcheck before an interval is considered failed", + ) + createFlags.StringVarP( + &cf.Hostname, + "hostname", "h", "", + "Set container hostname", + ) + createFlags.BoolVar( + &cf.HTTPProxy, + "http-proxy", true, + "Set proxy environment variables in the container based on the host proxy vars", + ) + createFlags.StringVar( + &cf.ImageVolume, + "image-volume", cliconfig.DefaultImageVolume, + `Tells podman how to handle the builtin image volumes ("bind"|"tmpfs"|"ignore")`, + ) + createFlags.BoolVar( + &cf.Init, + "init", false, + "Run an init binary inside the container that forwards signals and reaps processes", + ) + createFlags.StringVar( + &cf.InitPath, + "init-path", getDefaultInitPath(), + // Do not use the Value field for setting the default value to determine user input (i.e., non-empty string) + fmt.Sprintf("Path to the container-init binary"), + ) + createFlags.BoolVarP( + &cf.Interactive, + "interactive", "i", false, + "Keep STDIN open even if not attached", + ) + createFlags.StringVar( + &cf.IPC, + "ipc", getDefaultIPCNS(), + "IPC namespace to use", + ) + createFlags.StringVar( + &cf.KernelMemory, + "kernel-memory", "", + "Kernel memory limit "+sizeWithUnitFormat, + ) + createFlags.StringArrayVarP( + &cf.Label, + "label", "l", []string{}, + "Set metadata on container", + ) + createFlags.StringSliceVar( + &cf.LabelFile, + "label-file", []string{}, + "Read in a line delimited file of labels", + ) + createFlags.StringVar( + &cf.LogDriver, + "log-driver", "", + "Logging driver for the container", + ) + createFlags.StringSliceVar( + &cf.LogOptions, + "log-opt", []string{}, + "Logging driver options", + ) + createFlags.StringVarP( + &cf.Memory, + "memory", "m", "", + "Memory limit "+sizeWithUnitFormat, + ) + createFlags.StringVar( + &cf.MemoryReservation, + "memory-reservation", "", + "Memory soft limit "+sizeWithUnitFormat, + ) + createFlags.StringVar( + &cf.MemorySwap, + "memory-swap", "", + "Swap limit equal to memory plus swap: '-1' to enable unlimited swap", + ) + createFlags.Int64Var( + &cf.MemorySwappiness, + "memory-swappiness", -1, + "Tune container memory swappiness (0 to 100, or -1 for system default)", + ) + createFlags.StringVar( + &cf.Name, + "name", "", + "Assign a name to the container", + ) + createFlags.BoolVar( + &cf.NoHealthCheck, + "no-healthcheck", false, + "Disable healthchecks on container", + ) + createFlags.BoolVar( + &cf.OOMKillDisable, + "oom-kill-disable", false, + "Disable OOM Killer", + ) + createFlags.IntVar( + &cf.OOMScoreAdj, + "oom-score-adj", 0, + "Tune the host's OOM preferences (-1000 to 1000)", + ) + createFlags.StringVar( + &cf.OverrideArch, + "override-arch", "", + "use `ARCH` instead of the architecture of the machine for choosing images", + ) + //markFlagHidden(createFlags, "override-arch") + createFlags.StringVar( + &cf.OverrideOS, + "override-os", "", + "use `OS` instead of the running OS for choosing images", + ) + //markFlagHidden(createFlags, "override-os") + createFlags.StringVar( + &cf.PID, + "pid", getDefaultPidNS(), + "PID namespace to use", + ) + createFlags.Int64Var( + &cf.PIDsLimit, + "pids-limit", getDefaultPidsLimit(), + getDefaultPidsDescription(), + ) + createFlags.StringVar( + &cf.Pod, + "pod", "", + "Run container in an existing pod", + ) + createFlags.BoolVar( + &cf.Privileged, + "privileged", false, + "Give extended privileges to container", + ) + createFlags.BoolVarP( + &cf.PublishAll, + "publish-all", "P", false, + "Publish all exposed ports to random ports on the host interface", + ) + createFlags.StringVar( + &cf.Pull, + "pull", "missing", + `Pull image before creating ("always"|"missing"|"never")`, + ) + createFlags.BoolVarP( + &cf.Quiet, + "quiet", "q", false, + "Suppress output information when pulling images", + ) + createFlags.BoolVar( + &cf.ReadOnly, + "read-only", false, + "Make containers root filesystem read-only", + ) + createFlags.BoolVar( + &cf.ReadOnlyTmpFS, + "read-only-tmpfs", true, + "When running containers in read-only mode mount a read-write tmpfs on /run, /tmp and /var/tmp", + ) + createFlags.StringVar( + &cf.Restart, + "restart", "", + `Restart policy to apply when a container exits ("always"|"no"|"on-failure")`, + ) + createFlags.BoolVar( + &cf.Rm, + "rm", false, + "Remove container (and pod if created) after exit", + ) + createFlags.BoolVar( + &cf.RootFS, + "rootfs", false, + "The first argument is not an image but the rootfs to the exploded container", + ) + createFlags.StringArrayVar( + &cf.SecurityOpt, + "security-opt", getDefaultSecurityOptions(), + fmt.Sprintf("Security Options"), + ) + createFlags.StringVar( + &cf.ShmSize, + "shm-size", getDefaultShmSize(), + "Size of /dev/shm "+sizeWithUnitFormat, + ) + createFlags.StringVar( + &cf.StopSignal, + "stop-signal", "", + "Signal to stop a container. Default is SIGTERM", + ) + createFlags.UintVar( + &cf.StopTimeout, + "stop-timeout", defaultContainerConfig.Engine.StopTimeout, + "Timeout (in seconds) to stop a container. Default is 10", + ) + createFlags.StringSliceVar( + &cf.StoreageOpt, + "storage-opt", []string{}, + "Storage driver options per container", + ) + createFlags.StringVar( + &cf.SubUIDName, + "subgidname", "", + "Name of range listed in /etc/subgid for use in user namespace", + ) + createFlags.StringVar( + &cf.SubGIDName, + "subuidname", "", + "Name of range listed in /etc/subuid for use in user namespace", + ) + + createFlags.StringSliceVar( + &cf.Sysctl, + "sysctl", getDefaultSysctls(), + "Sysctl options", + ) + createFlags.StringVar( + &cf.SystemdD, + "systemd", "true", + `Run container in systemd mode ("true"|"false"|"always")`, + ) + createFlags.StringArrayVar( + &cf.TmpFS, + "tmpfs", []string{}, + "Mount a temporary filesystem (`tmpfs`) into a container", + ) + createFlags.BoolVarP( + &cf.TTY, + "tty", "t", false, + "Allocate a pseudo-TTY for container", + ) + createFlags.StringSliceVar( + &cf.UIDMap, + "uidmap", []string{}, + "UID map to use for the user namespace", + ) + createFlags.StringSliceVar( + &cf.Ulimit, + "ulimit", getDefaultUlimits(), + "Ulimit options", + ) + createFlags.StringVarP( + &cf.User, + "user", "u", "", + "Username or UID (format: <name|uid>[:<group|gid>])", + ) + createFlags.StringVar( + &cf.UserNS, + "userns", getDefaultUserNS(), + "User namespace to use", + ) + createFlags.StringVar( + &cf.UTS, + "uts", getDefaultUTSNS(), + "UTS namespace to use", + ) + createFlags.StringArrayVar( + &cf.Mount, + "mount", []string{}, + "Attach a filesystem mount to the container", + ) + createFlags.StringArrayVarP( + &cf.Volume, + "volume", "v", getDefaultVolumes(), + "Bind mount a volume into the container", + ) + createFlags.StringSliceVar( + &cf.VolumesFrom, + "volumes-from", []string{}, + "Mount volumes from the specified container(s)", + ) + createFlags.StringVarP( + &cf.Workdir, + "workdir", "w", "", + "Working directory inside the container", + ) + createFlags.StringVar( + &cf.SeccompPolicy, + "seccomp-policy", "default", + "Policy for selecting a seccomp profile (experimental)", + ) + return &createFlags +} + +func AliasFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { + switch name { + case "healthcheck-command": + name = "health-cmd" + case "healthcheck-interval": + name = "health-interval" + case "healthcheck-retries": + name = "health-retries" + case "healthcheck-start-period": + name = "health-start-period" + case "healthcheck-timeout": + name = "health-timeout" + case "net": + name = "network" + } + return pflag.NormalizedName(name) +} diff --git a/cmd/podmanV2/common/create_opts.go b/cmd/podmanV2/common/create_opts.go new file mode 100644 index 000000000..9d12e4b26 --- /dev/null +++ b/cmd/podmanV2/common/create_opts.go @@ -0,0 +1,103 @@ +package common + +import "github.com/containers/libpod/pkg/domain/entities" + +type ContainerCLIOpts struct { + Annotation []string + Attach []string + Authfile string + BlkIOWeight string + BlkIOWeightDevice []string + CapAdd []string + CapDrop []string + CGroupsNS string + CGroups string + CGroupParent string + CIDFile string + ConmonPIDFile string + CPUPeriod uint64 + CPUQuota int64 + CPURTPeriod uint64 + CPURTRuntime int64 + CPUShares uint64 + CPUS float64 + CPUSetCPUs string + CPUSetMems string + Detach bool + DetachKeys string + Device []string + DeviceCGroupRule []string + DeviceReadBPs []string + DeviceReadIOPs []string + DeviceWriteBPs []string + DeviceWriteIOPs []string + Entrypoint string + env []string + EnvHost bool + EnvFile []string + Expose []string + GIDMap []string + GroupAdd []string + HealthCmd string + HealthInterval string + HealthRetries uint + HealthStartPeriod string + HealthTimeout string + Hostname string + HTTPProxy bool + ImageVolume string + Init bool + InitPath string + Interactive bool + IPC string + KernelMemory string + Label []string + LabelFile []string + LogDriver string + LogOptions []string + Memory string + MemoryReservation string + MemorySwap string + MemorySwappiness int64 + Name string + NoHealthCheck bool + OOMKillDisable bool + OOMScoreAdj int + OverrideArch string + OverrideOS string + PID string + PIDsLimit int64 + Pod string + Privileged bool + PublishAll bool + Pull string + Quiet bool + ReadOnly bool + ReadOnlyTmpFS bool + Restart string + Rm bool + RootFS bool + SecurityOpt []string + ShmSize string + StopSignal string + StopTimeout uint + StoreageOpt []string + SubUIDName string + SubGIDName string + Sysctl []string + SystemdD string + TmpFS []string + TTY bool + UIDMap []string + Ulimit []string + User string + UserNS string + UTS string + Mount []string + Volume []string + VolumesFrom []string + Workdir string + SeccompPolicy string + + Net *entities.NetOptions +} diff --git a/cmd/podmanV2/common/createparse.go b/cmd/podmanV2/common/createparse.go new file mode 100644 index 000000000..89524a04b --- /dev/null +++ b/cmd/podmanV2/common/createparse.go @@ -0,0 +1,51 @@ +package common + +import ( + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" +) + +// validate determines if the flags and values given by the user are valid. things checked +// by validate must not need any state information on the flag (i.e. changed) +func (c *ContainerCLIOpts) validate() error { + var () + if c.Rm && c.Restart != "" && c.Restart != "no" { + return errors.Errorf("the --rm option conflicts with --restart") + } + + if _, err := util.ValidatePullType(c.Pull); err != nil { + return err + } + // Verify the additional hosts are in correct format + for _, host := range c.Net.AddHosts { + if _, err := parse.ValidateExtraHost(host); err != nil { + return err + } + } + + if dnsSearches := c.Net.DNSSearch; len(dnsSearches) > 0 { + // Validate domains are good + for _, dom := range dnsSearches { + if dom == "." { + if len(dnsSearches) > 1 { + return errors.Errorf("cannot pass additional search domains when also specifying '.'") + } + continue + } + if _, err := parse.ValidateDomain(dom); err != nil { + return err + } + } + } + var imageVolType = map[string]string{ + "bind": "", + "tmpfs": "", + "ignore": "", + } + if _, ok := imageVolType[c.ImageVolume]; !ok { + return errors.Errorf("invalid image-volume type %q. Pick one of bind, tmpfs, or ignore", c.ImageVolume) + } + return nil + +} diff --git a/cmd/podmanV2/common/default.go b/cmd/podmanV2/common/default.go new file mode 100644 index 000000000..b71fcb6f0 --- /dev/null +++ b/cmd/podmanV2/common/default.go @@ -0,0 +1,121 @@ +package common + +import ( + "fmt" + "os" + + "github.com/containers/buildah/pkg/parse" + "github.com/containers/libpod/pkg/apparmor" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/sysinfo" + "github.com/opencontainers/selinux/go-selinux" +) + +// TODO these options are directly embedded into many of the CLI cobra values, as such +// this approach will not work in a remote client. so we will need to likely do something like a +// supported and unsupported approach here and backload these options into the specgen +// once we are "on" the host system. +func getDefaultSecurityOptions() []string { + securityOpts := []string{} + if defaultContainerConfig.Containers.SeccompProfile != "" && defaultContainerConfig.Containers.SeccompProfile != parse.SeccompDefaultPath { + securityOpts = append(securityOpts, fmt.Sprintf("seccomp=%s", defaultContainerConfig.Containers.SeccompProfile)) + } + if apparmor.IsEnabled() && defaultContainerConfig.Containers.ApparmorProfile != "" { + securityOpts = append(securityOpts, fmt.Sprintf("apparmor=%s", defaultContainerConfig.Containers.ApparmorProfile)) + } + if selinux.GetEnabled() && !defaultContainerConfig.Containers.EnableLabeling { + securityOpts = append(securityOpts, fmt.Sprintf("label=%s", selinux.DisableSecOpt()[0])) + } + return securityOpts +} + +// getDefaultSysctls +func getDefaultSysctls() []string { + return defaultContainerConfig.Containers.DefaultSysctls +} + +func getDefaultVolumes() []string { + return defaultContainerConfig.Containers.Volumes +} + +func getDefaultDevices() []string { + return defaultContainerConfig.Containers.Devices +} + +func getDefaultDNSServers() []string { //nolint + return defaultContainerConfig.Containers.DNSServers +} + +func getDefaultDNSSearches() []string { //nolint + return defaultContainerConfig.Containers.DNSSearches +} + +func getDefaultDNSOptions() []string { //nolint + return defaultContainerConfig.Containers.DNSOptions +} + +func getDefaultEnv() []string { + return defaultContainerConfig.Containers.Env +} + +func getDefaultInitPath() string { + return defaultContainerConfig.Containers.InitPath +} + +func getDefaultIPCNS() string { + return defaultContainerConfig.Containers.IPCNS +} + +func getDefaultPidNS() string { + return defaultContainerConfig.Containers.PidNS +} + +func getDefaultNetNS() string { //nolint + if defaultContainerConfig.Containers.NetNS == "private" && rootless.IsRootless() { + return "slirp4netns" + } + return defaultContainerConfig.Containers.NetNS +} + +func getDefaultCgroupNS() string { + return defaultContainerConfig.Containers.CgroupNS +} + +func getDefaultUTSNS() string { + return defaultContainerConfig.Containers.UTSNS +} + +func getDefaultShmSize() string { + return defaultContainerConfig.Containers.ShmSize +} + +func getDefaultUlimits() []string { + return defaultContainerConfig.Containers.DefaultUlimits +} + +func getDefaultUserNS() string { + userns := os.Getenv("PODMAN_USERNS") + if userns != "" { + return userns + } + return defaultContainerConfig.Containers.UserNS +} + +func getDefaultPidsLimit() int64 { + if rootless.IsRootless() { + cgroup2, _ := cgroups.IsCgroup2UnifiedMode() + if cgroup2 { + return defaultContainerConfig.Containers.PidsLimit + } + } + return sysinfo.GetDefaultPidsLimit() +} + +func getDefaultPidsDescription() string { + return "Tune container pids limit (set 0 for unlimited)" +} + +func GetDefaultDetachKeys() string { + return defaultContainerConfig.Engine.DetachKeys +} diff --git a/cmd/podmanV2/common/ports.go b/cmd/podmanV2/common/ports.go new file mode 100644 index 000000000..7e2b1e79d --- /dev/null +++ b/cmd/podmanV2/common/ports.go @@ -0,0 +1,126 @@ +package common + +import ( + "fmt" + "net" + "strconv" + + "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// ExposedPorts parses user and image ports and returns binding information +func ExposedPorts(expose []string, publish []ocicni.PortMapping, publishAll bool, imageExposedPorts map[string]struct{}) ([]ocicni.PortMapping, error) { + containerPorts := make(map[string]string) + + // TODO this needs to be added into a something that + // has access to an imageengine + // add expose ports from the image itself + //for expose := range imageExposedPorts { + // _, port := nat.SplitProtoPort(expose) + // containerPorts[port] = "" + //} + + // add the expose ports from the user (--expose) + // can be single or a range + for _, expose := range expose { + //support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>] + _, port := nat.SplitProtoPort(expose) + //parse the start and end port and create a sequence of ports to expose + //if expose a port, the start and end port are the same + start, end, err := nat.ParsePortRange(port) + if err != nil { + return nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", expose, err) + } + for i := start; i <= end; i++ { + containerPorts[strconv.Itoa(int(i))] = "" + } + } + + // TODO/FIXME this is hell reencarnated + // parse user inputted port bindings + pbPorts, portBindings, err := nat.ParsePortSpecs([]string{}) + if err != nil { + return nil, err + } + + // delete exposed container ports if being used by -p + for i := range pbPorts { + delete(containerPorts, i.Port()) + } + + // iterate container ports and make port bindings from them + if publishAll { + for e := range containerPorts { + //support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>] + //proto, port := nat.SplitProtoPort(e) + p, err := nat.NewPort("tcp", e) + if err != nil { + return nil, err + } + rp, err := getRandomPort() + if err != nil { + return nil, err + } + logrus.Debug(fmt.Sprintf("Using random host port %d with container port %d", rp, p.Int())) + portBindings[p] = CreatePortBinding(rp, "") + } + } + + // We need to see if any host ports are not populated and if so, we need to assign a + // random port to them. + for k, pb := range portBindings { + if pb[0].HostPort == "" { + hostPort, err := getRandomPort() + if err != nil { + return nil, err + } + logrus.Debug(fmt.Sprintf("Using random host port %d with container port %s", hostPort, k.Port())) + pb[0].HostPort = strconv.Itoa(hostPort) + } + } + var pms []ocicni.PortMapping + for k, v := range portBindings { + for _, pb := range v { + hp, err := strconv.Atoi(pb.HostPort) + if err != nil { + return nil, err + } + pms = append(pms, ocicni.PortMapping{ + HostPort: int32(hp), + ContainerPort: int32(k.Int()), + //Protocol: "", + HostIP: pb.HostIP, + }) + } + } + return pms, nil +} + +func getRandomPort() (int, error) { + l, err := net.Listen("tcp", ":0") + if err != nil { + return 0, errors.Wrapf(err, "unable to get free port") + } + defer l.Close() + _, randomPort, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + return 0, errors.Wrapf(err, "unable to determine free port") + } + rp, err := strconv.Atoi(randomPort) + if err != nil { + return 0, errors.Wrapf(err, "unable to convert random port to int") + } + return rp, nil +} + +//CreatePortBinding takes port (int) and IP (string) and creates an array of portbinding structs +func CreatePortBinding(hostPort int, hostIP string) []nat.PortBinding { + pb := nat.PortBinding{ + HostPort: strconv.Itoa(hostPort), + } + pb.HostIP = hostIP + return []nat.PortBinding{pb} +} diff --git a/cmd/podmanV2/common/specgen.go b/cmd/podmanV2/common/specgen.go new file mode 100644 index 000000000..5245e206e --- /dev/null +++ b/cmd/podmanV2/common/specgen.go @@ -0,0 +1,647 @@ +package common + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/containers/image/v5/manifest" + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/libpod" + ann "github.com/containers/libpod/pkg/annotations" + envLib "github.com/containers/libpod/pkg/env" + ns "github.com/containers/libpod/pkg/namespaces" + "github.com/containers/libpod/pkg/specgen" + systemdGen "github.com/containers/libpod/pkg/systemd/generate" + "github.com/containers/libpod/pkg/util" + "github.com/docker/go-units" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string) error { + var ( + err error + //namespaces map[string]string + ) + + // validate flags as needed + if err := c.validate(); err != nil { + return nil + } + + inputCommand := args[1:] + if len(c.HealthCmd) > 0 { + s.HealthConfig, err = makeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod) + if err != nil { + return err + } + } + + s.IDMappings, err = util.ParseIDMapping(ns.UsernsMode(c.UserNS), c.UIDMap, c.GIDMap, c.SubUIDName, c.SubGIDName) + if err != nil { + return err + } + if m := c.Memory; len(m) > 0 { + ml, err := units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for memory") + } + s.ResourceLimits.Memory.Limit = &ml + } + if m := c.MemoryReservation; len(m) > 0 { + mr, err := units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for memory") + } + s.ResourceLimits.Memory.Reservation = &mr + } + if m := c.MemorySwap; len(m) > 0 { + var ms int64 + if m == "-1" { + ms = int64(-1) + s.ResourceLimits.Memory.Swap = &ms + } else { + ms, err = units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for memory") + } + } + s.ResourceLimits.Memory.Swap = &ms + } + if m := c.KernelMemory; len(m) > 0 { + mk, err := units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for kernel-memory") + } + s.ResourceLimits.Memory.Kernel = &mk + } + if b := c.BlkIOWeight; len(b) > 0 { + u, err := strconv.ParseUint(b, 10, 16) + if err != nil { + return errors.Wrapf(err, "invalid value for blkio-weight") + } + nu := uint16(u) + s.ResourceLimits.BlockIO.Weight = &nu + } + + s.Terminal = c.TTY + ep, err := ExposedPorts(c.Expose, c.Net.PublishPorts, c.PublishAll, nil) + if err != nil { + return err + } + s.PortMappings = ep + s.Pod = c.Pod + + //s.CgroupNS = specgen.Namespace{ + // NSMode: , + // Value: "", + //} + + //s.UserNS = specgen.Namespace{} + + // Kernel Namespaces + // TODO Fix handling of namespace from pod + // Instead of integrating here, should be done in libpod + // However, that also involves setting up security opts + // when the pod's namespace is integrated + //namespaces = map[string]string{ + // "cgroup": c.CGroupsNS, + // "pid": c.PID, + // //"net": c.Net.Network.Value, // TODO need help here + // "ipc": c.IPC, + // "user": c.User, + // "uts": c.UTS, + //} + // + //if len(c.PID) > 0 { + // split := strings.SplitN(c.PID, ":", 2) + // // need a way to do thsi + // specgen.Namespace{ + // NSMode: split[0], + // } + // //Value: split1 if len allows + //} + // TODO this is going to have be done after things like pod creation are done because + // pod creation changes these values. + //pidMode := ns.PidMode(namespaces["pid"]) + //usernsMode := ns.UsernsMode(namespaces["user"]) + //utsMode := ns.UTSMode(namespaces["uts"]) + //cgroupMode := ns.CgroupMode(namespaces["cgroup"]) + //ipcMode := ns.IpcMode(namespaces["ipc"]) + //// Make sure if network is set to container namespace, port binding is not also being asked for + //netMode := ns.NetworkMode(namespaces["net"]) + //if netMode.IsContainer() { + // if len(portBindings) > 0 { + // return nil, errors.Errorf("cannot set port bindings on an existing container network namespace") + // } + //} + + // TODO Remove when done with namespaces for realz + // Setting a default for IPC to get this working + s.IpcNS = specgen.Namespace{ + NSMode: specgen.Private, + Value: "", + } + + // TODO this is going to have to be done the libpod/server end of things + // USER + //user := c.String("user") + //if user == "" { + // switch { + // case usernsMode.IsKeepID(): + // user = fmt.Sprintf("%d:%d", rootless.GetRootlessUID(), rootless.GetRootlessGID()) + // case data == nil: + // user = "0" + // default: + // user = data.Config.User + // } + //} + + // STOP SIGNAL + signalString := "TERM" + if sig := c.StopSignal; len(sig) > 0 { + signalString = sig + } + stopSignal, err := util.ParseSignal(signalString) + if err != nil { + return err + } + s.StopSignal = &stopSignal + + // ENVIRONMENT VARIABLES + // + // Precedence order (higher index wins): + // 1) env-host, 2) image data, 3) env-file, 4) env + env := map[string]string{ + "container": "podman", + } + + // First transform the os env into a map. We need it for the labels later in + // any case. + osEnv, err := envLib.ParseSlice(os.Environ()) + if err != nil { + return errors.Wrap(err, "error parsing host environment variables") + } + + if c.EnvHost { + env = envLib.Join(env, osEnv) + } + // env-file overrides any previous variables + for _, f := range c.EnvFile { + fileEnv, err := envLib.ParseFile(f) + if err != nil { + return err + } + // File env is overridden by env. + env = envLib.Join(env, fileEnv) + } + + // env overrides any previous variables + if cmdLineEnv := c.env; len(cmdLineEnv) > 0 { + parsedEnv, err := envLib.ParseSlice(cmdLineEnv) + if err != nil { + return err + } + env = envLib.Join(env, parsedEnv) + } + s.Env = env + + // LABEL VARIABLES + labels, err := parse.GetAllLabels(c.LabelFile, c.Label) + if err != nil { + return errors.Wrapf(err, "unable to process labels") + } + + if systemdUnit, exists := osEnv[systemdGen.EnvVariable]; exists { + labels[systemdGen.EnvVariable] = systemdUnit + } + + s.Labels = labels + + // ANNOTATIONS + annotations := make(map[string]string) + + // First, add our default annotations + annotations[ann.TTY] = "false" + if c.TTY { + annotations[ann.TTY] = "true" + } + + // Last, add user annotations + for _, annotation := range c.Annotation { + splitAnnotation := strings.SplitN(annotation, "=", 2) + if len(splitAnnotation) < 2 { + return errors.Errorf("Annotations must be formatted KEY=VALUE") + } + annotations[splitAnnotation[0]] = splitAnnotation[1] + } + s.Annotations = annotations + + workDir := "/" + if wd := c.Workdir; len(wd) > 0 { + workDir = wd + } + s.WorkDir = workDir + entrypoint := []string{} + userCommand := []string{} + if ep := c.Entrypoint; len(ep) > 0 { + // Check if entrypoint specified is json + if err := json.Unmarshal([]byte(c.Entrypoint), &entrypoint); err != nil { + entrypoint = append(entrypoint, ep) + } + } + + var command []string + + // Build the command + // If we have an entry point, it goes first + if len(entrypoint) > 0 { + command = entrypoint + } + if len(inputCommand) > 0 { + // User command overrides data CMD + command = append(command, inputCommand...) + userCommand = append(userCommand, inputCommand...) + } + + if len(inputCommand) > 0 { + s.Command = userCommand + } else { + s.Command = command + } + + // SHM Size + shmSize, err := units.FromHumanSize(c.ShmSize) + if err != nil { + return errors.Wrapf(err, "unable to translate --shm-size") + } + s.ShmSize = &shmSize + s.HostAdd = c.Net.AddHosts + s.DNSServer = c.Net.DNSServers + s.DNSSearch = c.Net.DNSSearch + s.DNSOption = c.Net.DNSOptions + + // deferred, must be added on libpod side + //var ImageVolumes map[string]struct{} + //if data != nil && c.String("image-volume") != "ignore" { + // ImageVolumes = data.Config.Volumes + //} + + s.ImageVolumeMode = c.ImageVolume + systemd := c.SystemdD == "always" + if !systemd && command != nil { + x, err := strconv.ParseBool(c.SystemdD) + if err != nil { + return errors.Wrapf(err, "cannot parse bool %s", c.SystemdD) + } + if x && (command[0] == "/usr/sbin/init" || command[0] == "/sbin/init" || (filepath.Base(command[0]) == "systemd")) { + systemd = true + } + } + if systemd { + if s.StopSignal == nil { + stopSignal, err = util.ParseSignal("RTMIN+3") + if err != nil { + return errors.Wrapf(err, "error parsing systemd signal") + } + s.StopSignal = &stopSignal + } + } + swappiness := uint64(c.MemorySwappiness) + if s.ResourceLimits == nil { + s.ResourceLimits = &specs.LinuxResources{} + } + if s.ResourceLimits.Memory == nil { + s.ResourceLimits.Memory = &specs.LinuxMemory{} + } + s.ResourceLimits.Memory.Swappiness = &swappiness + + if s.LogConfiguration == nil { + s.LogConfiguration = &specgen.LogConfig{} + } + s.LogConfiguration.Driver = libpod.KubernetesLogging + if ld := c.LogDriver; len(ld) > 0 { + s.LogConfiguration.Driver = ld + } + if s.ResourceLimits.Pids == nil { + s.ResourceLimits.Pids = &specs.LinuxPids{} + } + s.ResourceLimits.Pids.Limit = c.PIDsLimit + if c.CGroups == "disabled" && c.PIDsLimit > 0 { + s.ResourceLimits.Pids.Limit = -1 + } + // TODO WTF + //cgroup := &cc.CgroupConfig{ + // Cgroups: c.String("cgroups"), + // Cgroupns: c.String("cgroupns"), + // CgroupParent: c.String("cgroup-parent"), + // CgroupMode: cgroupMode, + //} + // + //userns := &cc.UserConfig{ + // GroupAdd: c.StringSlice("group-add"), + // IDMappings: idmappings, + // UsernsMode: usernsMode, + // User: user, + //} + // + //uts := &cc.UtsConfig{ + // UtsMode: utsMode, + // NoHosts: c.Bool("no-hosts"), + // HostAdd: c.StringSlice("add-host"), + // Hostname: c.String("hostname"), + //} + + sysctl := map[string]string{} + if ctl := c.Sysctl; len(ctl) > 0 { + sysctl, err = util.ValidateSysctls(ctl) + if err != nil { + return err + } + } + s.Sysctl = sysctl + + s.CapAdd = c.CapAdd + s.CapDrop = c.CapDrop + s.Privileged = c.Privileged + s.ReadOnlyFilesystem = c.ReadOnly + + // TODO + // ouitside of specgen and oci though + // defaults to true, check spec/storage + //s.readon = c.ReadOnlyTmpFS + // TODO convert to map? + // check if key=value and convert + sysmap := make(map[string]string) + for _, ctl := range c.Sysctl { + splitCtl := strings.SplitN(ctl, "=", 2) + if len(splitCtl) < 2 { + return errors.Errorf("invalid sysctl value %q", ctl) + } + sysmap[splitCtl[0]] = splitCtl[1] + } + s.Sysctl = sysmap + + for _, opt := range c.SecurityOpt { + if opt == "no-new-privileges" { + s.ContainerSecurityConfig.NoNewPrivileges = true + } else { + con := strings.SplitN(opt, "=", 2) + if len(con) != 2 { + return fmt.Errorf("invalid --security-opt 1: %q", opt) + } + + switch con[0] { + case "label": + // TODO selinux opts and label opts are the same thing + s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, con[1]) + case "apparmor": + s.ContainerSecurityConfig.ApparmorProfile = con[1] + case "seccomp": + s.SeccompProfilePath = con[1] + default: + return fmt.Errorf("invalid --security-opt 2: %q", opt) + } + } + } + + // TODO any idea why this was done + // storage.go from spec/ + // grab it + //volumes := rtc.Containers.Volumes + // TODO conflict on populate? + //if v := c.Volume; len(v)> 0 { + // s.Volumes = append(volumes, c.StringSlice("volume")...) + //} + //s.volu + + //s.Mounts = c.Mount + s.VolumesFrom = c.VolumesFrom + + // TODO any idea why this was done + //devices := rtc.Containers.Devices + // TODO conflict on populate? + // + //if c.Changed("device") { + // devices = append(devices, c.StringSlice("device")...) + //} + + // TODO things i cannot find in spec + // we dont think these are in the spec + // init - initbinary + // initpath + s.Stdin = c.Interactive + // quiet + //DeviceCgroupRules: c.StringSlice("device-cgroup-rule"), + + if bps := c.DeviceReadBPs; len(bps) > 0 { + if s.ThrottleReadBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { + return err + } + } + + if bps := c.DeviceWriteBPs; len(bps) > 0 { + if s.ThrottleWriteBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { + return err + } + } + + if iops := c.DeviceReadIOPs; len(iops) > 0 { + if s.ThrottleReadIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { + return err + } + } + + if iops := c.DeviceWriteIOPs; len(iops) > 0 { + if s.ThrottleWriteIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { + return err + } + } + + s.ResourceLimits.Memory.DisableOOMKiller = &c.OOMKillDisable + + // Rlimits/Ulimits + for _, u := range c.Ulimit { + if u == "host" { + s.Rlimits = nil + break + } + ul, err := units.ParseUlimit(u) + if err != nil { + return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u) + } + rl := specs.POSIXRlimit{ + Type: ul.Name, + Hard: uint64(ul.Hard), + Soft: uint64(ul.Soft), + } + s.Rlimits = append(s.Rlimits, rl) + } + + //Tmpfs: c.StringArray("tmpfs"), + + // TODO how to handle this? + //Syslog: c.Bool("syslog"), + + logOpts := make(map[string]string) + for _, o := range c.LogOptions { + split := strings.SplitN(o, "=", 2) + if len(split) < 2 { + return errors.Errorf("invalid log option %q", o) + } + logOpts[split[0]] = split[1] + } + s.LogConfiguration.Options = logOpts + s.Name = c.Name + + if err := parseWeightDevices(c.BlkIOWeightDevice, s); err != nil { + return err + } + + if s.ResourceLimits.CPU == nil { + s.ResourceLimits.CPU = &specs.LinuxCPU{} + } + s.ResourceLimits.CPU.Shares = &c.CPUShares + s.ResourceLimits.CPU.Period = &c.CPUPeriod + + // TODO research these + //s.ResourceLimits.CPU.Cpus = c.CPUS + //s.ResourceLimits.CPU.Cpus = c.CPUSetCPUs + + //s.ResourceLimits.CPU. = c.CPUSetCPUs + s.ResourceLimits.CPU.Mems = c.CPUSetMems + s.ResourceLimits.CPU.Quota = &c.CPUQuota + s.ResourceLimits.CPU.RealtimePeriod = &c.CPURTPeriod + s.ResourceLimits.CPU.RealtimeRuntime = &c.CPURTRuntime + s.OOMScoreAdj = &c.OOMScoreAdj + s.RestartPolicy = c.Restart + s.Remove = c.Rm + s.StopTimeout = &c.StopTimeout + + // TODO where should we do this? + //func verifyContainerResources(config *cc.CreateConfig, update bool) ([]string, error) { + return nil +} + +func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string) (*manifest.Schema2HealthConfig, error) { + // Every healthcheck requires a command + if len(inCmd) == 0 { + return nil, errors.New("Must define a healthcheck command for all healthchecks") + } + + // first try to parse option value as JSON array of strings... + cmd := []string{} + err := json.Unmarshal([]byte(inCmd), &cmd) + if err != nil { + // ...otherwise pass it to "/bin/sh -c" inside the container + cmd = []string{"CMD-SHELL", inCmd} + } + hc := manifest.Schema2HealthConfig{ + Test: cmd, + } + + if interval == "disable" { + interval = "0" + } + intervalDuration, err := time.ParseDuration(interval) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-interval %s ", interval) + } + + hc.Interval = intervalDuration + + if retries < 1 { + return nil, errors.New("healthcheck-retries must be greater than 0.") + } + hc.Retries = int(retries) + timeoutDuration, err := time.ParseDuration(timeout) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-timeout %s", timeout) + } + if timeoutDuration < time.Duration(1) { + return nil, errors.New("healthcheck-timeout must be at least 1 second") + } + hc.Timeout = timeoutDuration + + startPeriodDuration, err := time.ParseDuration(startPeriod) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-start-period %s", startPeriod) + } + if startPeriodDuration < time.Duration(0) { + return nil, errors.New("healthcheck-start-period must be 0 seconds or greater") + } + hc.StartPeriod = startPeriodDuration + + return &hc, nil +} + +func parseWeightDevices(weightDevs []string, s *specgen.SpecGenerator) error { + for _, val := range weightDevs { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return fmt.Errorf("bad format for device path: %s", val) + } + weight, err := strconv.ParseUint(split[1], 10, 0) + if err != nil { + return fmt.Errorf("invalid weight for device: %s", val) + } + if weight > 0 && (weight < 10 || weight > 1000) { + return fmt.Errorf("invalid weight for device: %s", val) + } + w := uint16(weight) + s.WeightDevice[split[0]] = specs.LinuxWeightDevice{ + Weight: &w, + LeafWeight: nil, + } + } + return nil +} + +func parseThrottleBPSDevices(bpsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { + td := make(map[string]specs.LinuxThrottleDevice) + for _, val := range bpsDevices { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + rate, err := units.RAMInBytes(split[1]) + if err != nil { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>[<unit>]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) + } + if rate < 0 { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>[<unit>]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) + } + td[split[0]] = specs.LinuxThrottleDevice{Rate: uint64(rate)} + } + return td, nil +} + +func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { + td := make(map[string]specs.LinuxThrottleDevice) + for _, val := range iopsDevices { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + rate, err := strconv.ParseUint(split[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>. Number must be a positive integer", val) + } + td[split[0]] = specs.LinuxThrottleDevice{Rate: rate} + } + return td, nil +} diff --git a/cmd/podmanV2/containers/attach.go b/cmd/podmanV2/containers/attach.go new file mode 100644 index 000000000..d62dcff86 --- /dev/null +++ b/cmd/podmanV2/containers/attach.go @@ -0,0 +1,60 @@ +package containers + +import ( + "os" + + "github.com/containers/libpod/cmd/podmanV2/common" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + attachDescription = "The podman attach command allows you to attach to a running container using the container's ID or name, either to view its ongoing output or to control it interactively." + attachCommand = &cobra.Command{ + Use: "attach [flags] CONTAINER", + Short: "Attach to a running container", + Long: attachDescription, + RunE: attach, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) > 1 || (len(args) == 0 && !cmd.Flag("latest").Changed) { + return errors.Errorf("attach requires the name or id of one running container or the latest flag") + } + return nil + }, + PreRunE: preRunE, + Example: `podman attach ctrID + podman attach 1234 + podman attach --no-stdin foobar`, + } +) + +var ( + attachOpts entities.AttachOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: attachCommand, + }) + flags := attachCommand.Flags() + flags.StringVar(&attachOpts.DetachKeys, "detach-keys", common.GetDefaultDetachKeys(), "Select the key sequence for detaching a container. Format is a single character `[a-Z]` or a comma separated sequence of `ctrl-<value>`, where `<value>` is one of: `a-z`, `@`, `^`, `[`, `\\`, `]`, `^` or `_`") + flags.BoolVar(&attachOpts.NoStdin, "no-stdin", false, "Do not attach STDIN. The default is false") + flags.BoolVar(&attachOpts.SigProxy, "sig-proxy", true, "Proxy received signals to the process") + flags.BoolVarP(&attachOpts.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func attach(cmd *cobra.Command, args []string) error { + attachOpts.Stdin = os.Stdin + if attachOpts.NoStdin { + attachOpts.Stdin = nil + } + attachOpts.Stdout = os.Stdout + attachOpts.Stderr = os.Stderr + return registry.ContainerEngine().ContainerAttach(registry.GetContext(), args[0], attachOpts) +} diff --git a/cmd/podmanV2/containers/checkpoint.go b/cmd/podmanV2/containers/checkpoint.go new file mode 100644 index 000000000..7c3e551bc --- /dev/null +++ b/cmd/podmanV2/containers/checkpoint.go @@ -0,0 +1,79 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + checkpointDescription = ` + podman container checkpoint + + Checkpoints one or more running containers. The container name or ID can be used. +` + checkpointCommand = &cobra.Command{ + Use: "checkpoint [flags] CONTAINER [CONTAINER...]", + Short: "Checkpoints one or more containers", + Long: checkpointDescription, + RunE: checkpoint, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman container checkpoint --keep ctrID + podman container checkpoint --all + podman container checkpoint --leave-running --latest`, + } +) + +var ( + checkpointOptions entities.CheckpointOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: checkpointCommand, + Parent: containerCmd, + }) + flags := checkpointCommand.Flags() + flags.BoolVarP(&checkpointOptions.Keep, "keep", "k", false, "Keep all temporary checkpoint files") + flags.BoolVarP(&checkpointOptions.LeaveRuninng, "leave-running", "R", false, "Leave the container running after writing checkpoint to disk") + flags.BoolVar(&checkpointOptions.TCPEstablished, "tcp-established", false, "Checkpoint a container with established TCP connections") + flags.BoolVarP(&checkpointOptions.All, "all", "a", false, "Checkpoint all running containers") + flags.BoolVarP(&checkpointOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.StringVarP(&checkpointOptions.Export, "export", "e", "", "Export the checkpoint image to a tar.gz") + flags.BoolVar(&checkpointOptions.IgnoreRootFS, "ignore-rootfs", false, "Do not include root file-system changes when exporting") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func checkpoint(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + if rootless.IsRootless() { + return errors.New("checkpointing a container requires root") + } + if checkpointOptions.Export == "" && checkpointOptions.IgnoreRootFS { + return errors.Errorf("--ignore-rootfs can only be used with --export") + } + responses, err := registry.ContainerEngine().ContainerCheckpoint(context.Background(), args, checkpointOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/create.go b/cmd/podmanV2/containers/create.go new file mode 100644 index 000000000..fd5300966 --- /dev/null +++ b/cmd/podmanV2/containers/create.go @@ -0,0 +1,102 @@ +package containers + +import ( + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/common" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/specgen" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + createDescription = `Creates a new container from the given image or storage and prepares it for running the specified command. + + The container ID is then printed to stdout. You can then start it at any time with the podman start <container_id> command. The container will be created with the initial state 'created'.` + createCommand = &cobra.Command{ + Use: "create [flags] IMAGE [COMMAND [ARG...]]", + Short: "Create but do not start a container", + Long: createDescription, + RunE: create, + PersistentPreRunE: preRunE, + Args: cobra.MinimumNArgs(1), + Example: `podman create alpine ls + podman create --annotation HELLO=WORLD alpine ls + podman create -t -i --name myctr alpine ls`, + } +) + +var ( + cliVals common.ContainerCLIOpts +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: createCommand, + }) + //common.GetCreateFlags(createCommand) + flags := createCommand.Flags() + flags.AddFlagSet(common.GetCreateFlags(&cliVals)) + flags.AddFlagSet(common.GetNetFlags()) + flags.SetNormalizeFunc(common.AliasFlags) +} + +func create(cmd *cobra.Command, args []string) error { + var ( + err error + rawImageInput string + ) + cliVals.Net, err = common.NetFlagsToNetOptions(cmd) + if err != nil { + return err + } + if rfs := cliVals.RootFS; !rfs { + rawImageInput = args[0] + } + + if err := createInit(cmd); err != nil { + return err + } + //TODO rootfs still + s := specgen.NewSpecGenerator(rawImageInput) + if err := common.FillOutSpecGen(s, &cliVals, args); err != nil { + return err + } + + report, err := registry.ContainerEngine().ContainerCreate(registry.GetContext(), s) + if err != nil { + return err + } + fmt.Println(report.Id) + return nil +} + +func createInit(c *cobra.Command) error { + if c.Flag("privileged").Changed && c.Flag("security-opt").Changed { + logrus.Warn("setting security options with --privileged has no effect") + } + + if (c.Flag("dns").Changed || c.Flag("dns-opt").Changed || c.Flag("dns-search").Changed) && (cliVals.Net.Network.NSMode == specgen.NoNetwork || cliVals.Net.Network.IsContainer()) { + return errors.Errorf("conflicting options: dns and the network mode.") + } + + if c.Flag("cpu-period").Changed && c.Flag("cpus").Changed { + return errors.Errorf("--cpu-period and --cpus cannot be set together") + } + if c.Flag("cpu-quota").Changed && c.Flag("cpus").Changed { + return errors.Errorf("--cpu-quota and --cpus cannot be set together") + } + + if c.Flag("no-hosts").Changed && c.Flag("add-host").Changed { + return errors.Errorf("--no-hosts and --add-host cannot be set together") + } + + // Docker-compatibility: the "-h" flag for run/create is reserved for + // the hostname (see https://github.com/containers/libpod/issues/1367). + + return nil +} diff --git a/cmd/podmanV2/containers/exec.go b/cmd/podmanV2/containers/exec.go new file mode 100644 index 000000000..4bff57dbb --- /dev/null +++ b/cmd/podmanV2/containers/exec.go @@ -0,0 +1,93 @@ +package containers + +import ( + "bufio" + "os" + + "github.com/containers/libpod/cmd/podmanV2/common" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + envLib "github.com/containers/libpod/pkg/env" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + execDescription = `Execute the specified command inside a running container. +` + execCommand = &cobra.Command{ + Use: "exec [flags] CONTAINER [COMMAND [ARG...]]", + Short: "Run a process in a running container", + Long: execDescription, + PreRunE: preRunE, + RunE: exec, + Example: `podman exec -it ctrID ls + podman exec -it -w /tmp myCtr pwd + podman exec --user root ctrID ls`, + } +) + +var ( + envInput, envFile []string + execOpts entities.ExecOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: execCommand, + }) + flags := execCommand.Flags() + flags.SetInterspersed(false) + flags.StringVar(&execOpts.DetachKeys, "detach-keys", common.GetDefaultDetachKeys(), "Select the key sequence for detaching a container. Format is a single character [a-Z] or ctrl-<value> where <value> is one of: a-z, @, ^, [, , or _") + flags.StringArrayVarP(&envInput, "env", "e", []string{}, "Set environment variables") + flags.StringSliceVar(&envFile, "env-file", []string{}, "Read in a file of environment variables") + flags.BoolVarP(&execOpts.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached") + flags.BoolVarP(&execOpts.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVar(&execOpts.Privileged, "privileged", false, "Give the process extended Linux capabilities inside the container. The default is false") + flags.BoolVarP(&execOpts.Tty, "tty", "t", false, "Allocate a pseudo-TTY. The default is false") + flags.StringVarP(&execOpts.User, "user", "u", "", "Sets the username or UID used and optionally the groupname or GID for the specified command") + flags.UintVar(&execOpts.PreserveFDs, "preserve-fds", 0, "Pass N additional file descriptors to the container") + flags.StringVarP(&execOpts.WorkDir, "workdir", "w", "", "Working directory inside the container") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + _ = flags.MarkHidden("preserve-fds") + } + +} +func exec(cmd *cobra.Command, args []string) error { + var nameOrId string + execOpts.Cmd = args + if !execOpts.Latest { + execOpts.Cmd = args[1:] + nameOrId = args[0] + } + // Validate given environment variables + execOpts.Envs = make(map[string]string) + for _, f := range envFile { + fileEnv, err := envLib.ParseFile(f) + if err != nil { + return err + } + execOpts.Envs = envLib.Join(execOpts.Envs, fileEnv) + } + + cliEnv, err := envLib.ParseSlice(envInput) + if err != nil { + return errors.Wrap(err, "error parsing environment variables") + } + + execOpts.Envs = envLib.Join(execOpts.Envs, cliEnv) + execOpts.Streams.OutputStream = os.Stdout + execOpts.Streams.ErrorStream = os.Stderr + if execOpts.Interactive { + execOpts.Streams.InputStream = bufio.NewReader(os.Stdin) + execOpts.Streams.AttachInput = true + } + execOpts.Streams.AttachOutput = true + execOpts.Streams.AttachError = true + + exitCode, err := registry.ContainerEngine().ContainerExec(registry.GetContext(), nameOrId, execOpts) + registry.SetExitCode(exitCode) + return err +} diff --git a/cmd/podmanV2/containers/export.go b/cmd/podmanV2/containers/export.go new file mode 100644 index 000000000..b93b60878 --- /dev/null +++ b/cmd/podmanV2/containers/export.go @@ -0,0 +1,57 @@ +package containers + +import ( + "context" + "os" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" +) + +var ( + exportDescription = "Exports container's filesystem contents as a tar archive" + + " and saves it on the local machine." + + exportCommand = &cobra.Command{ + Use: "export [flags] CONTAINER", + Short: "Export container's filesystem contents as a tar archive", + Long: exportDescription, + PersistentPreRunE: preRunE, + RunE: export, + Args: cobra.ExactArgs(1), + Example: `podman export ctrID > myCtr.tar + podman export --output="myCtr.tar" ctrID`, + } +) + +var ( + exportOpts entities.ContainerExportOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: exportCommand, + }) + exportCommand.SetHelpTemplate(registry.HelpTemplate()) + exportCommand.SetUsageTemplate(registry.UsageTemplate()) + flags := exportCommand.Flags() + flags.StringVarP(&exportOpts.Output, "output", "o", "", "Write to a specified file (default: stdout, which must be redirected)") +} + +func export(cmd *cobra.Command, args []string) error { + if len(exportOpts.Output) == 0 { + file := os.Stdout + if terminal.IsTerminal(int(file.Fd())) { + return errors.Errorf("refusing to export to terminal. Use -o flag or redirect") + } + exportOpts.Output = "/dev/stdout" + } else if err := parse.ValidateFileName(exportOpts.Output); err != nil { + return err + } + return registry.ContainerEngine().ContainerExport(context.Background(), args[0], exportOpts) +} diff --git a/cmd/podmanV2/containers/restore.go b/cmd/podmanV2/containers/restore.go new file mode 100644 index 000000000..6cab6ab50 --- /dev/null +++ b/cmd/podmanV2/containers/restore.go @@ -0,0 +1,104 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + restoreDescription = ` + podman container restore + + Restores a container from a checkpoint. The container name or ID can be used. +` + restoreCommand = &cobra.Command{ + Use: "restore [flags] CONTAINER [CONTAINER...]", + Short: "Restores one or more containers from a checkpoint", + Long: restoreDescription, + RunE: restore, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, true, false) + }, + Example: `podman container restore ctrID + podman container restore --latest + podman container restore --all`, + } +) + +var ( + restoreOptions entities.RestoreOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: restoreCommand, + Parent: containerCmd, + }) + flags := restoreCommand.Flags() + flags.BoolVarP(&restoreOptions.All, "all", "a", false, "Restore all checkpointed containers") + flags.BoolVarP(&restoreOptions.Keep, "keep", "k", false, "Keep all temporary checkpoint files") + flags.BoolVarP(&restoreOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVar(&restoreOptions.TCPEstablished, "tcp-established", false, "Restore a container with established TCP connections") + flags.StringVarP(&restoreOptions.Import, "import", "i", "", "Restore from exported checkpoint archive (tar.gz)") + flags.StringVarP(&restoreOptions.Name, "name", "n", "", "Specify new name for container restored from exported checkpoint (only works with --import)") + flags.BoolVar(&restoreOptions.IgnoreRootFS, "ignore-rootfs", false, "Do not apply root file-system changes when importing from exported checkpoint") + flags.BoolVar(&restoreOptions.IgnoreStaticIP, "ignore-static-ip", false, "Ignore IP address set via --static-ip") + flags.BoolVar(&restoreOptions.IgnoreStaticMAC, "ignore-static-mac", false, "Ignore MAC address set via --mac-address") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func restore(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + if rootless.IsRootless() { + return errors.New("restoring a container requires root") + } + if restoreOptions.Import == "" && restoreOptions.IgnoreRootFS { + return errors.Errorf("--ignore-rootfs can only be used with --import") + } + if restoreOptions.Import == "" && restoreOptions.Name != "" { + return errors.Errorf("--name can only be used with --import") + } + if restoreOptions.Name != "" && restoreOptions.TCPEstablished { + return errors.Errorf("--tcp-established cannot be used with --name") + } + + argLen := len(args) + if restoreOptions.Import != "" { + if restoreOptions.All || restoreOptions.Latest { + return errors.Errorf("Cannot use --import with --all or --latest") + } + if argLen > 0 { + return errors.Errorf("Cannot use --import with positional arguments") + } + } + if (restoreOptions.All || restoreOptions.Latest) && argLen > 0 { + return errors.Errorf("no arguments are needed with --all or --latest") + } + if argLen < 1 && !restoreOptions.All && !restoreOptions.Latest && restoreOptions.Import == "" { + return errors.Errorf("you must provide at least one name or id") + } + responses, err := registry.ContainerEngine().ContainerRestore(context.Background(), args, restoreOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() + +} diff --git a/cmd/podmanV2/images/import.go b/cmd/podmanV2/images/import.go new file mode 100644 index 000000000..09a15585f --- /dev/null +++ b/cmd/podmanV2/images/import.go @@ -0,0 +1,87 @@ +package images + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + importDescription = `Create a container image from the contents of the specified tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz). + + Note remote tar balls can be specified, via web address. + Optionally tag the image. You can specify the instructions using the --change option.` + importCommand = &cobra.Command{ + Use: "import [flags] PATH [REFERENCE]", + Short: "Import a tarball to create a filesystem image", + Long: importDescription, + RunE: importCon, + PersistentPreRunE: preRunE, + Example: `podman import http://example.com/ctr.tar url-image + cat ctr.tar | podman -q import --message "importing the ctr.tar tarball" - image-imported + cat ctr.tar | podman import -`, + } +) + +var ( + importOpts entities.ImageImportOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: importCommand, + }) + + importCommand.SetHelpTemplate(registry.HelpTemplate()) + importCommand.SetUsageTemplate(registry.UsageTemplate()) + flags := importCommand.Flags() + flags.StringArrayVarP(&importOpts.Changes, "change", "c", []string{}, "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR") + flags.StringVarP(&importOpts.Message, "message", "m", "", "Set commit message for imported image") + flags.BoolVarP(&importOpts.Quiet, "quiet", "q", false, "Suppress output") +} + +func importCon(cmd *cobra.Command, args []string) error { + var ( + source string + reference string + ) + switch len(args) { + case 0: + return errors.Errorf("need to give the path to the tarball, or must specify a tarball of '-' for stdin") + case 1: + source = args[0] + case 2: + source = args[0] + // TODO when save is merged, we need to process reference + // like it is done in there or we end up with docker.io prepends + // instead of the localhost ones + reference = args[1] + default: + return errors.Errorf("too many arguments. Usage TARBALL [REFERENCE]") + } + errFileName := parse.ValidateFileName(source) + errURL := parse.ValidURL(source) + if errURL == nil { + importOpts.SourceIsURL = true + } + if errFileName != nil && errURL != nil { + return multierror.Append(errFileName, errURL) + } + + importOpts.Source = source + importOpts.Reference = reference + + response, err := registry.ImageEngine().Import(context.Background(), importOpts) + if err != nil { + return err + } + fmt.Println(response.Id) + return nil +} diff --git a/cmd/podmanV2/images/load.go b/cmd/podmanV2/images/load.go new file mode 100644 index 000000000..f60dc4908 --- /dev/null +++ b/cmd/podmanV2/images/load.go @@ -0,0 +1,61 @@ +package images + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + loadDescription = "Loads an image from a locally stored archive (tar file) into container storage." + loadCommand = &cobra.Command{ + Use: "load [flags] [NAME[:TAG]]", + Short: "Load an image from container archive", + Long: loadDescription, + RunE: load, + Args: cobra.MaximumNArgs(1), + PersistentPreRunE: preRunE, + } +) + +var ( + loadOpts entities.ImageLoadOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: loadCommand, + }) + + loadCommand.SetHelpTemplate(registry.HelpTemplate()) + loadCommand.SetUsageTemplate(registry.UsageTemplate()) + flags := loadCommand.Flags() + flags.StringVarP(&loadOpts.Input, "input", "i", "", "Read from specified archive file (default: stdin)") + flags.BoolVarP(&loadOpts.Quiet, "quiet", "q", false, "Suppress the output") + flags.StringVar(&loadOpts.SignaturePolicy, "signature-policy", "", "Pathname of signature policy file") + if registry.IsRemote() { + _ = flags.MarkHidden("signature-policy") + } + +} + +func load(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + repo, err := image.NormalizedTag(args[0]) + if err != nil { + return err + } + loadOpts.Name = repo.Name() + } + response, err := registry.ImageEngine().Load(context.Background(), loadOpts) + if err != nil { + return err + } + fmt.Println("Loaded image: " + response.Name) + return nil +} diff --git a/cmd/podmanV2/images/push.go b/cmd/podmanV2/images/push.go new file mode 100644 index 000000000..82cc0c486 --- /dev/null +++ b/cmd/podmanV2/images/push.go @@ -0,0 +1,127 @@ +package images + +import ( + buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// pushOptionsWrapper wraps entities.ImagepushOptions and prevents leaking +// CLI-only fields into the API types. +type pushOptionsWrapper struct { + entities.ImagePushOptions + TLSVerifyCLI bool // CLI only +} + +var ( + pushOptions = pushOptionsWrapper{} + pushDescription = `Pushes a source image to a specified destination. + + The Image "DESTINATION" uses a "transport":"details" format. See podman-push(1) section "DESTINATION" for the expected format.` + + // Command: podman push + pushCmd = &cobra.Command{ + Use: "push [flags] SOURCE DESTINATION", + Short: "Push an image to a specified destination", + Long: pushDescription, + PreRunE: preRunE, + RunE: imagePush, + Example: `podman push imageID docker://registry.example.com/repository:tag + podman push imageID oci-archive:/path/to/layout:image:tag`, + } + + // Command: podman image push + // It's basically a clone of `pushCmd` with the exception of being a + // child of the images command. + imagePushCmd = &cobra.Command{ + Use: pushCmd.Use, + Short: pushCmd.Short, + Long: pushCmd.Long, + PreRunE: pushCmd.PreRunE, + RunE: pushCmd.RunE, + Example: `podman image push imageID docker://registry.example.com/repository:tag + podman image push imageID oci-archive:/path/to/layout:image:tag`, + } +) + +func init() { + // push + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pushCmd, + }) + + pushCmd.SetHelpTemplate(registry.HelpTemplate()) + pushCmd.SetUsageTemplate(registry.UsageTemplate()) + + flags := pushCmd.Flags() + pushFlags(flags) + + // images push + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imagePushCmd, + Parent: imageCmd, + }) + + imagePushCmd.SetHelpTemplate(registry.HelpTemplate()) + imagePushCmd.SetUsageTemplate(registry.UsageTemplate()) + pushFlags(imagePushCmd.Flags()) +} + +// pushFlags set the flags for the push command. +func pushFlags(flags *pflag.FlagSet) { + flags.StringVar(&pushOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&pushOptions.CertDir, "cert-dir", "", "Path to a directory containing TLS certificates and keys") + flags.BoolVar(&pushOptions.Compress, "compress", false, "Compress tarball image layers when pushing to a directory using the 'dir' transport. (default is same compression type as source)") + flags.StringVar(&pushOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") + flags.StringVar(&pushOptions.DigestFile, "digestfile", "", "Write the digest of the pushed image to the specified file") + flags.StringVarP(&pushOptions.Format, "format", "f", "", "Manifest type (oci, v2s1, or v2s2) to use when pushing an image using the 'dir' transport (default is manifest type of source)") + flags.BoolVarP(&pushOptions.Quiet, "quiet", "q", false, "Suppress output information when pushing images") + flags.BoolVar(&pushOptions.RemoveSignatures, "remove-signatures", false, "Discard any pre-existing signatures in the image") + flags.StringVar(&pushOptions.SignaturePolicy, "signature-policy", "", "Path to a signature-policy file") + flags.StringVar(&pushOptions.SignBy, "sign-by", "", "Add a signature at the destination using the specified key") + flags.BoolVar(&pushOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + + if registry.IsRemote() { + _ = flags.MarkHidden("authfile") + _ = flags.MarkHidden("cert-dir") + _ = flags.MarkHidden("compress") + _ = flags.MarkHidden("quiet") + _ = flags.MarkHidden("signature-policy") + _ = flags.MarkHidden("tls-verify") + } +} + +// imagePush is implement the command for pushing images. +func imagePush(cmd *cobra.Command, args []string) error { + var source, destination string + switch len(args) { + case 1: + source = args[0] + case 2: + source = args[0] + destination = args[1] + case 0: + fallthrough + default: + return errors.New("push requires at least one image name, or optionally a second to specify a different destination") + } + + pushOptsAPI := pushOptions.ImagePushOptions + // TLS verification in c/image is controlled via a `types.OptionalBool` + // which allows for distinguishing among set-true, set-false, unspecified + // which is important to implement a sane way of dealing with defaults of + // boolean CLI flags. + if cmd.Flags().Changed("tls-verify") { + pushOptsAPI.TLSVerify = types.NewOptionalBool(pushOptions.TLSVerifyCLI) + } + + // Let's do all the remaining Yoga in the API to prevent us from scattering + // logic across (too) many parts of the code. + return registry.ImageEngine().Push(registry.GetContext(), source, destination, pushOptsAPI) +} diff --git a/cmd/podmanV2/images/save.go b/cmd/podmanV2/images/save.go new file mode 100644 index 000000000..ae39b7bce --- /dev/null +++ b/cmd/podmanV2/images/save.go @@ -0,0 +1,87 @@ +package images + +import ( + "context" + "os" + "strings" + + "github.com/containers/libpod/libpod/define" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" +) + +var validFormats = []string{define.OCIManifestDir, define.OCIArchive, define.V2s2ManifestDir, define.V2s2Archive} + +var ( + saveDescription = `Save an image to docker-archive or oci-archive on the local machine. Default is docker-archive.` + + saveCommand = &cobra.Command{ + Use: "save [flags] IMAGE", + Short: "Save image to an archive", + Long: saveDescription, + PersistentPreRunE: preRunE, + RunE: save, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.Errorf("need at least 1 argument") + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + if !util.StringInSlice(format, validFormats) { + return errors.Errorf("format value must be one of %s", strings.Join(validFormats, " ")) + } + return nil + }, + Example: `podman save --quiet -o myimage.tar imageID + podman save --format docker-dir -o ubuntu-dir ubuntu + podman save > alpine-all.tar alpine:latest`, + } +) + +var ( + saveOpts entities.ImageSaveOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: saveCommand, + }) + flags := saveCommand.Flags() + flags.BoolVar(&saveOpts.Compress, "compress", false, "Compress tarball image layers when saving to a directory using the 'dir' transport. (default is same compression type as source)") + flags.StringVar(&saveOpts.Format, "format", define.V2s2Archive, "Save image to oci-archive, oci-dir (directory with oci manifest type), docker-archive, docker-dir (directory with v2s2 manifest type)") + flags.StringVarP(&saveOpts.Output, "output", "o", "", "Write to a specified file (default: stdout, which must be redirected)") + flags.BoolVarP(&saveOpts.Quiet, "quiet", "q", false, "Suppress the output") + +} + +func save(cmd *cobra.Command, args []string) error { + var ( + tags []string + ) + if cmd.Flag("compress").Changed && (saveOpts.Format != define.OCIManifestDir && saveOpts.Format != define.V2s2ManifestDir && saveOpts.Format == "") { + return errors.Errorf("--compress can only be set when --format is either 'oci-dir' or 'docker-dir'") + } + if len(saveOpts.Output) == 0 { + fi := os.Stdout + if terminal.IsTerminal(int(fi.Fd())) { + return errors.Errorf("refusing to save to terminal. Use -o flag or redirect") + } + saveOpts.Output = "/dev/stdout" + } + if err := parse.ValidateFileName(saveOpts.Output); err != nil { + return err + } + if len(args) > 1 { + tags = args[1:] + } + return registry.ImageEngine().Save(context.Background(), args[0], tags, saveOpts) +} diff --git a/cmd/podmanV2/main.go b/cmd/podmanV2/main.go index e4daabb66..fe3cd9f16 100644 --- a/cmd/podmanV2/main.go +++ b/cmd/podmanV2/main.go @@ -12,9 +12,11 @@ import ( _ "github.com/containers/libpod/cmd/podmanV2/networks" _ "github.com/containers/libpod/cmd/podmanV2/pods" "github.com/containers/libpod/cmd/podmanV2/registry" + _ "github.com/containers/libpod/cmd/podmanV2/system" _ "github.com/containers/libpod/cmd/podmanV2/volumes" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/storage/pkg/reexec" "github.com/sirupsen/logrus" ) @@ -45,6 +47,11 @@ func init() { } func main() { + if reexec.Init() { + // We were invoked with a different argv[0] indicating that we + // had a specific job to do as a subprocess, and it's done. + return + } for _, c := range registry.Commands { if Contains(registry.EngineOptions.EngineMode, c.Mode) { parent := rootCmd diff --git a/cmd/podmanV2/parse/common.go b/cmd/podmanV2/parse/common.go new file mode 100644 index 000000000..a5e9b4fc2 --- /dev/null +++ b/cmd/podmanV2/parse/common.go @@ -0,0 +1,50 @@ +package parse + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// CheckAllLatestAndCIDFile checks that --all and --latest are used correctly. +// If cidfile is set, also check for the --cidfile flag. +func CheckAllLatestAndCIDFile(c *cobra.Command, args []string, ignoreArgLen bool, cidfile bool) error { + argLen := len(args) + if c.Flags().Lookup("all") == nil || c.Flags().Lookup("latest") == nil { + if !cidfile { + return errors.New("unable to lookup values for 'latest' or 'all'") + } else if c.Flags().Lookup("cidfile") == nil { + return errors.New("unable to lookup values for 'latest', 'all' or 'cidfile'") + } + } + + specifiedAll, _ := c.Flags().GetBool("all") + specifiedLatest, _ := c.Flags().GetBool("latest") + specifiedCIDFile := false + if cid, _ := c.Flags().GetStringArray("cidfile"); len(cid) > 0 { + specifiedCIDFile = true + } + + if specifiedCIDFile && (specifiedAll || specifiedLatest) { + return errors.Errorf("--all, --latest and --cidfile cannot be used together") + } else if specifiedAll && specifiedLatest { + return errors.Errorf("--all and --latest cannot be used together") + } + + if ignoreArgLen { + return nil + } + if (argLen > 0) && (specifiedAll || specifiedLatest) { + return errors.Errorf("no arguments are needed with --all or --latest") + } else if cidfile && (argLen > 0) && (specifiedAll || specifiedLatest || specifiedCIDFile) { + return errors.Errorf("no arguments are needed with --all, --latest or --cidfile") + } + + if specifiedCIDFile { + return nil + } + + if argLen < 1 && !specifiedAll && !specifiedLatest && !specifiedCIDFile { + return errors.Errorf("you must provide at least one name or id") + } + return nil +} diff --git a/cmd/podmanV2/parse/parse.go b/cmd/podmanV2/parse/net.go index 10d2146fa..03cda268c 100644 --- a/cmd/podmanV2/parse/parse.go +++ b/cmd/podmanV2/parse/net.go @@ -13,7 +13,6 @@ import ( "strings" "github.com/pkg/errors" - "github.com/spf13/cobra" ) const ( @@ -187,47 +186,3 @@ func ValidURL(urlStr string) error { } return nil } - -// checkAllLatestAndCIDFile checks that --all and --latest are used correctly. -// If cidfile is set, also check for the --cidfile flag. -func CheckAllLatestAndCIDFile(c *cobra.Command, args []string, ignoreArgLen bool, cidfile bool) error { - argLen := len(args) - if c.Flags().Lookup("all") == nil || c.Flags().Lookup("latest") == nil { - if !cidfile { - return errors.New("unable to lookup values for 'latest' or 'all'") - } else if c.Flags().Lookup("cidfile") == nil { - return errors.New("unable to lookup values for 'latest', 'all' or 'cidfile'") - } - } - - specifiedAll, _ := c.Flags().GetBool("all") - specifiedLatest, _ := c.Flags().GetBool("latest") - specifiedCIDFile := false - if cid, _ := c.Flags().GetStringArray("cidfile"); len(cid) > 0 { - specifiedCIDFile = true - } - - if specifiedCIDFile && (specifiedAll || specifiedLatest) { - return errors.Errorf("--all, --latest and --cidfile cannot be used together") - } else if specifiedAll && specifiedLatest { - return errors.Errorf("--all and --latest cannot be used together") - } - - if ignoreArgLen { - return nil - } - if (argLen > 0) && (specifiedAll || specifiedLatest) { - return errors.Errorf("no arguments are needed with --all or --latest") - } else if cidfile && (argLen > 0) && (specifiedAll || specifiedLatest || specifiedCIDFile) { - return errors.Errorf("no arguments are needed with --all, --latest or --cidfile") - } - - if specifiedCIDFile { - return nil - } - - if argLen < 1 && !specifiedAll && !specifiedLatest && !specifiedCIDFile { - return errors.Errorf("you must provide at least one name or id") - } - return nil -} diff --git a/cmd/podmanV2/parse/parse_test.go b/cmd/podmanV2/parse/net_test.go index a6ddc2be9..a6ddc2be9 100644 --- a/cmd/podmanV2/parse/parse_test.go +++ b/cmd/podmanV2/parse/net_test.go diff --git a/cmd/podmanV2/system/version.go b/cmd/podmanV2/system/version.go new file mode 100644 index 000000000..e8002056b --- /dev/null +++ b/cmd/podmanV2/system/version.go @@ -0,0 +1,119 @@ +package system + +import ( + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + "time" + + "github.com/containers/buildah/pkg/formats" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + versionCommand = &cobra.Command{ + Use: "version", + Args: cobra.NoArgs, + Short: "Display the Podman Version Information", + RunE: version, + PersistentPreRunE: preRunE, + } + format string +) + +type versionStruct struct { + Client define.Version + Server define.Version +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: versionCommand, + }) + flags := versionCommand.Flags() + flags.StringVarP(&format, "format", "f", "", "Change the output format to JSON or a Go template") +} + +func version(cmd *cobra.Command, args []string) error { + var ( + v versionStruct + err error + ) + v.Client, err = define.GetVersion() + if err != nil { + return errors.Wrapf(err, "unable to determine version") + } + // TODO we need to discuss how to implement + // this more. current endpoints dont have a + // version endpoint. maybe we use info? + //if remote { + // v.Server, err = getRemoteVersion(c) + // if err != nil { + // return err + // } + //} else { + v.Server = v.Client + //} + + versionOutputFormat := format + if versionOutputFormat != "" { + if strings.Join(strings.Fields(versionOutputFormat), "") == "{{json.}}" { + versionOutputFormat = formats.JSONString + } + var out formats.Writer + switch versionOutputFormat { + case formats.JSONString: + out = formats.JSONStruct{Output: v} + return out.Out() + default: + out = formats.StdoutTemplate{Output: v, Template: versionOutputFormat} + err := out.Out() + if err != nil { + // On Failure, assume user is using older version of podman version --format and check client + out = formats.StdoutTemplate{Output: v.Client, Template: versionOutputFormat} + if err1 := out.Out(); err1 != nil { + return err + } + } + } + return nil + } + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + defer w.Flush() + + if registry.IsRemote() { + if _, err := fmt.Fprintf(w, "Client:\n"); err != nil { + return err + } + formatVersion(w, v.Client) + if _, err := fmt.Fprintf(w, "\nServer:\n"); err != nil { + return err + } + formatVersion(w, v.Server) + } else { + formatVersion(w, v.Client) + } + return nil +} + +func formatVersion(writer io.Writer, version define.Version) { + fmt.Fprintf(writer, "Version:\t%s\n", version.Version) + fmt.Fprintf(writer, "RemoteAPI Version:\t%d\n", version.RemoteAPIVersion) + fmt.Fprintf(writer, "Go Version:\t%s\n", version.GoVersion) + if version.GitCommit != "" { + fmt.Fprintf(writer, "Git Commit:\t%s\n", version.GitCommit) + } + // Prints out the build time in readable format + if version.Built != 0 { + fmt.Fprintf(writer, "Built:\t%s\n", time.Unix(version.Built, 0).Format(time.ANSIC)) + } + + fmt.Fprintf(writer, "OS/Arch:\t%s\n", version.OsArch) +} |