diff options
101 files changed, 4085 insertions, 3115 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index 392d7b72d..6ab8beda3 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -135,6 +135,7 @@ gating_task: - '/usr/local/bin/entrypoint.sh clean podman-remote' - '/usr/local/bin/entrypoint.sh clean podman BUILDTAGS="exclude_graphdriver_devicemapper selinux seccomp"' - '/usr/local/bin/entrypoint.sh podman-remote-darwin' + - '/usr/local/bin/entrypoint.sh podman-remote-windows' on_failure: failed_master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh' @@ -120,6 +120,9 @@ podman-remote: .gopathok $(PODMAN_VARLINK_DEPENDENCIES) ## Build with podman on podman-remote-darwin: .gopathok $(PODMAN_VARLINK_DEPENDENCIES) ## Build with podman on remote OSX environment GOOS=darwin $(GO) build -ldflags '$(LDFLAGS_PODMAN)' -tags "remoteclient containers_image_openpgp exclude_graphdriver_devicemapper" -o bin/$@ $(PROJECT)/cmd/podman +podman-remote-windows: .gopathok $(PODMAN_VARLINK_DEPENDENCIES) ## Build with podman for a remote windows environment + GOOS=windows $(GO) build -ldflags '$(LDFLAGS_PODMAN)' -tags "remoteclient containers_image_openpgp exclude_graphdriver_devicemapper" -o bin/$@.exe $(PROJECT)/cmd/podman + local-cross: $(CROSS_BUILD_TARGETS) ## Cross local compilation bin/podman.cross.%: .gopathok @@ -185,6 +188,7 @@ localunit: test/goecho/goecho varlink_generate --tags "$(BUILDTAGS)" \ --succinct $(MAKE) -C contrib/cirrus/packer test + ./contrib/cirrus/lib.sh.t ginkgo: ginkgo -v -tags "$(BUILDTAGS)" $(GINKGOTIMEOUT) -cover -flakeAttempts 3 -progress -trace -noColor -nodes 3 test/e2e/. diff --git a/cmd/podman/build.go b/cmd/podman/build.go index 647ff1e86..24be9bb46 100644 --- a/cmd/podman/build.go +++ b/cmd/podman/build.go @@ -267,7 +267,7 @@ func buildCmd(c *cliconfig.BuildValues) error { MemorySwap: memorySwap, ShmSize: c.ShmSize, Ulimit: c.Ulimit, - Volumes: c.Volume, + Volumes: c.Volumes, } options := imagebuildah.BuildOptions{ diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index 77156f47a..b770aaca0 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -136,12 +136,18 @@ type ExportValues struct { PodmanCommand Output string } - type GenerateKubeValues struct { PodmanCommand Service bool } +type GenerateSystemdValues struct { + PodmanCommand + Name bool + RestartPolicy string + StopTimeout int +} + type HistoryValues struct { PodmanCommand Human bool @@ -177,6 +183,12 @@ type InfoValues struct { Format string } +type InitValues struct { + PodmanCommand + All bool + Latest bool +} + type InspectValues struct { PodmanCommand TypeObject string diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go index 4b0641d82..14451d944 100644 --- a/cmd/podman/commands.go +++ b/cmd/podman/commands.go @@ -17,7 +17,6 @@ func getMainCommands() []*cobra.Command { _loginCommand, _logoutCommand, _mountCommand, - _portCommand, _refreshCommand, _searchCommand, _statsCommand, @@ -45,7 +44,6 @@ func getContainerSubCommands() []*cobra.Command { _commitCommand, _execCommand, _mountCommand, - _portCommand, _refreshCommand, _restoreCommand, _runlabelCommand, diff --git a/cmd/podman/common.go b/cmd/podman/common.go index b02aa5990..8aca08248 100644 --- a/cmd/podman/common.go +++ b/cmd/podman/common.go @@ -315,7 +315,7 @@ func getCreateFlags(c *cliconfig.PodmanCommand) { ) createFlags.Bool( "http-proxy", true, - "Set proxy environment variables in container based on the host proxy vars", + "Set proxy environment variables in the container based on the host proxy vars", ) createFlags.String( "image-volume", cliconfig.DefaultImageVolume, diff --git a/cmd/podman/container.go b/cmd/podman/container.go index b3058bf12..bbf01d1f8 100644 --- a/cmd/podman/container.go +++ b/cmd/podman/container.go @@ -56,12 +56,14 @@ var ( _diffCommand, _exportCommand, _createCommand, + _initCommand, _killCommand, _listSubCommand, _logsCommand, _pauseCommand, - _restartCommand, + _portCommand, _pruneContainersCommand, + _restartCommand, _runCommand, _rmCommand, _startCommand, diff --git a/cmd/podman/errors_remote.go b/cmd/podman/errors_remote.go index ab255ea56..1e276be10 100644 --- a/cmd/podman/errors_remote.go +++ b/cmd/podman/errors_remote.go @@ -33,6 +33,8 @@ func outputError(err error) { ne = errors.New(e.Reason) case *iopodman.VolumeNotFound: ne = errors.New(e.Reason) + case *iopodman.InvalidState: + ne = errors.New(e.Reason) case *iopodman.ErrorOccurred: ne = errors.New(e.Reason) default: diff --git a/cmd/podman/generate.go b/cmd/podman/generate.go index a0637ecb2..98bfb00a1 100644 --- a/cmd/podman/generate.go +++ b/cmd/podman/generate.go @@ -18,6 +18,7 @@ var ( // Commands that are universally implemented generateCommands = []*cobra.Command{ _containerKubeCommand, + _containerSystemdCommand, } ) diff --git a/cmd/podman/generate_systemd.go b/cmd/podman/generate_systemd.go new file mode 100644 index 000000000..b4779e512 --- /dev/null +++ b/cmd/podman/generate_systemd.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/containers/libpod/pkg/adapter" + "github.com/containers/libpod/pkg/systemdgen" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + containerSystemdCommand cliconfig.GenerateSystemdValues + containerSystemdDescription = `Command generates a systemd unit file for a Podman container + ` + _containerSystemdCommand = &cobra.Command{ + Use: "systemd [flags] CONTAINER | POD", + Short: "Generate a systemd unit file for a Podman container", + Long: containerSystemdDescription, + RunE: func(cmd *cobra.Command, args []string) error { + containerSystemdCommand.InputArgs = args + containerSystemdCommand.GlobalFlags = MainGlobalOpts + containerSystemdCommand.Remote = remoteclient + return generateSystemdCmd(&containerSystemdCommand) + }, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) > 1 || len(args) < 1 { + return errors.New("provide only one container name or ID") + } + return nil + }, + Example: `podman generate kube ctrID +`, + } +) + +func init() { + containerSystemdCommand.Command = _containerSystemdCommand + containerSystemdCommand.SetHelpTemplate(HelpTemplate()) + containerSystemdCommand.SetUsageTemplate(UsageTemplate()) + flags := containerSystemdCommand.Flags() + flags.BoolVarP(&containerSystemdCommand.Name, "name", "n", false, "use the container name instead of ID") + flags.IntVarP(&containerSystemdCommand.StopTimeout, "timeout", "t", -1, "stop timeout override") + flags.StringVar(&containerSystemdCommand.RestartPolicy, "restart-policy", "on-failure", "applicable systemd restart-policy") +} + +func generateSystemdCmd(c *cliconfig.GenerateSystemdValues) error { + runtime, err := adapter.GetRuntime(getContext(), &c.PodmanCommand) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + // User input stop timeout must be 0 or greater + if c.Flag("timeout").Changed && c.StopTimeout < 0 { + return errors.New("timeout value must be 0 or greater") + } + // Make sure the input restart policy is valid + if err := systemdgen.ValidateRestartPolicy(c.RestartPolicy); err != nil { + return err + } + + unit, err := runtime.GenerateSystemd(c) + if err != nil { + return err + } + fmt.Println(unit) + return nil +} diff --git a/cmd/podman/init.go b/cmd/podman/init.go new file mode 100644 index 000000000..68c80631d --- /dev/null +++ b/cmd/podman/init.go @@ -0,0 +1,64 @@ +package main + +import ( + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/containers/libpod/pkg/adapter" + "github.com/opentracing/opentracing-go" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + initCommand cliconfig.InitValues + initDescription = `Initialize one or more containers, creating the OCI spec and mounts for inspection. Container names or IDs can be used.` + + _initCommand = &cobra.Command{ + Use: "init [flags] CONTAINER [CONTAINER...]", + Short: "Initialize one or more containers", + Long: initDescription, + RunE: func(cmd *cobra.Command, args []string) error { + initCommand.InputArgs = args + initCommand.GlobalFlags = MainGlobalOpts + initCommand.Remote = remoteclient + return initCmd(&initCommand) + }, + Args: func(cmd *cobra.Command, args []string) error { + return checkAllAndLatest(cmd, args, false) + }, + Example: `podman init --latest + podman init 3c45ef19d893 + podman init test1`, + } +) + +func init() { + initCommand.Command = _initCommand + initCommand.SetHelpTemplate(HelpTemplate()) + initCommand.SetUsageTemplate(UsageTemplate()) + flags := initCommand.Flags() + flags.BoolVarP(&initCommand.All, "all", "a", false, "Initialize all containers") + flags.BoolVarP(&initCommand.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + markFlagHiddenForRemoteClient("latest", flags) +} + +// initCmd initializes a container +func initCmd(c *cliconfig.InitValues) error { + if c.Bool("trace") { + span, _ := opentracing.StartSpanFromContext(Ctx, "initCmd") + defer span.Finish() + } + + ctx := getContext() + + runtime, err := adapter.GetRuntime(ctx, &c.PodmanCommand) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + ok, failures, err := runtime.InitContainers(ctx, c) + if err != nil { + return err + } + return printCmdResults(ok, failures) +} diff --git a/cmd/podman/libpodruntime/runtime.go b/cmd/podman/libpodruntime/runtime.go index b03846bbc..b533dc056 100644 --- a/cmd/podman/libpodruntime/runtime.go +++ b/cmd/podman/libpodruntime/runtime.go @@ -78,8 +78,6 @@ func getRuntime(ctx context.Context, c *cliconfig.PodmanCommand, renumber bool, options = append(options, libpod.WithRenumber()) } - options = append(options, libpod.WithContext(ctx)) - // Only set this if the user changes storage config on the command line if storageSet { options = append(options, libpod.WithStorageConfig(storageOpts)) @@ -146,7 +144,7 @@ func getRuntime(ctx context.Context, c *cliconfig.PodmanCommand, renumber bool, options = append(options, libpod.WithDefaultInfraCommand(infraCommand)) } if c.Flags().Changed("config") { - return libpod.NewRuntimeFromConfig(c.GlobalFlags.Config, options...) + return libpod.NewRuntimeFromConfig(ctx, c.GlobalFlags.Config, options...) } - return libpod.NewRuntime(options...) + return libpod.NewRuntime(ctx, options...) } diff --git a/cmd/podman/main.go b/cmd/podman/main.go index a0f1cf401..787dd55c0 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -4,7 +4,6 @@ import ( "context" "io" "os" - "syscall" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/libpod" @@ -13,7 +12,6 @@ import ( "github.com/containers/libpod/version" "github.com/containers/storage/pkg/reexec" "github.com/opentracing/opentracing-go" - "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -41,12 +39,14 @@ var mainCommands = []*cobra.Command{ &_imagesCommand, _importCommand, _infoCommand, + _initCommand, &_inspectCommand, _killCommand, _loadCommand, _logsCommand, _pauseCommand, podCommand.Command, + _portCommand, &_psCommand, _pullCommand, _pushCommand, @@ -118,25 +118,13 @@ func before(cmd *cobra.Command, args []string) error { } logrus.SetLevel(level) - rlimits := new(syscall.Rlimit) - rlimits.Cur = 1048576 - rlimits.Max = 1048576 - if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { - if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { - return errors.Wrapf(err, "error getting rlimits") - } - rlimits.Cur = rlimits.Max - if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { - return errors.Wrapf(err, "error setting new rlimits") - } + if err := setRLimits(); err != nil { + return err } - if rootless.IsRootless() { logrus.Info("running as rootless") } - - // Be sure we can create directories with 0755 mode. - syscall.Umask(0022) + setUMask() return profileOn(cmd) } diff --git a/cmd/podman/main_local.go b/cmd/podman/main_local.go index 5afd51e28..7452965a2 100644 --- a/cmd/podman/main_local.go +++ b/cmd/podman/main_local.go @@ -4,16 +4,17 @@ package main import ( "context" - "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/cmd/podman/libpodruntime" - "github.com/containers/libpod/pkg/rootless" "io/ioutil" "log/syslog" "os" "runtime/pprof" "strconv" "strings" + "syscall" + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/tracing" "github.com/opentracing/opentracing-go" "github.com/pkg/errors" @@ -154,3 +155,23 @@ func setupRootless(cmd *cobra.Command, args []string) error { } return nil } +func setRLimits() error { + rlimits := new(syscall.Rlimit) + rlimits.Cur = 1048576 + rlimits.Max = 1048576 + if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { + if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { + return errors.Wrapf(err, "error getting rlimits") + } + rlimits.Cur = rlimits.Max + if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { + return errors.Wrapf(err, "error setting new rlimits") + } + } + return nil +} + +func setUMask() { + // Be sure we can create directories with 0755 mode. + syscall.Umask(0022) +} diff --git a/cmd/podman/main_remote.go b/cmd/podman/main_remote.go index 2a7d184cd..a3335050a 100644 --- a/cmd/podman/main_remote.go +++ b/cmd/podman/main_remote.go @@ -41,3 +41,9 @@ func setupRootless(cmd *cobra.Command, args []string) error { } return nil } + +func setRLimits() error { + return nil +} + +func setUMask() {} diff --git a/cmd/podman/port.go b/cmd/podman/port.go index 7a9f01fe6..1bd2d623e 100644 --- a/cmd/podman/port.go +++ b/cmd/podman/port.go @@ -6,8 +6,7 @@ import ( "strings" "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/cmd/podman/libpodruntime" - "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/adapter" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -51,10 +50,7 @@ func portCmd(c *cliconfig.PortValues) error { var ( userProto, containerName string userPort int - container *libpod.Container - containers []*libpod.Container ) - args := c.InputArgs if c.Latest && c.All { @@ -66,9 +62,6 @@ func portCmd(c *cliconfig.PortValues) error { if len(args) == 0 && !c.Latest && !c.All { return errors.Errorf("you must supply a running container name or id") } - if !c.Latest && !c.All { - containerName = args[0] - } port := "" if len(args) > 1 && !c.Latest { @@ -98,36 +91,14 @@ func portCmd(c *cliconfig.PortValues) error { } } - runtime, err := libpodruntime.GetRuntime(getContext(), &c.PodmanCommand) + runtime, err := adapter.GetRuntime(getContext(), &c.PodmanCommand) if err != nil { return errors.Wrapf(err, "could not get runtime") } defer runtime.Shutdown(false) - if !c.Latest && !c.All { - container, err = runtime.LookupContainer(containerName) - if err != nil { - return errors.Wrapf(err, "unable to find container %s", containerName) - } - containers = append(containers, container) - } else if c.Latest { - container, err = runtime.GetLatestContainer() - if err != nil { - return errors.Wrapf(err, "unable to get last created container") - } - containers = append(containers, container) - } else { - containers, err = runtime.GetRunningContainers() - if err != nil { - return errors.Wrapf(err, "unable to get all containers") - } - } - + containers, err := runtime.Port(c) for _, con := range containers { - if state, _ := con.State(); state != libpod.ContainerStateRunning { - continue - } - portmappings, err := con.PortMappings() if err != nil { return err diff --git a/cmd/podman/search.go b/cmd/podman/search.go index 13948aef0..b236f3055 100644 --- a/cmd/podman/search.go +++ b/cmd/podman/search.go @@ -118,16 +118,3 @@ func searchToGeneric(params []image.SearchResult) (genericParams []interface{}) } return genericParams } - -func genSearchOutputMap() map[string]string { - io := image.SearchResult{} - v := reflect.Indirect(reflect.ValueOf(io)) - values := make(map[string]string) - - for i := 0; i < v.NumField(); i++ { - key := v.Type().Field(i).Name - value := key - values[key] = strings.ToUpper(splitCamelCase(value)) - } - return values -} diff --git a/cmd/podman/shared/create.go b/cmd/podman/shared/create.go index 1e185d21f..81566326b 100644 --- a/cmd/podman/shared/create.go +++ b/cmd/podman/shared/create.go @@ -711,11 +711,6 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod. return config, nil } -type namespace interface { - IsContainer() bool - Container() string -} - func CreateContainerFromCreateConfig(r *libpod.Runtime, createConfig *cc.CreateConfig, ctx context.Context, pod *libpod.Pod) (*libpod.Container, error) { runtimeSpec, options, err := createConfig.MakeContainerConfig(r, pod) if err != nil { diff --git a/cmd/podman/shared/workers.go b/cmd/podman/shared/workers.go index 112af89cc..b6e3f10e7 100644 --- a/cmd/podman/shared/workers.go +++ b/cmd/podman/shared/workers.go @@ -110,9 +110,14 @@ func (p *Pool) newWorker(slot int) { func DefaultPoolSize(name string) int { numCpus := runtime.NumCPU() switch name { + case "init": + fallthrough case "kill": + fallthrough case "pause": + fallthrough case "rm": + fallthrough case "unpause": if numCpus <= 3 { return numCpus * 3 diff --git a/cmd/podman/varlink/io.podman.varlink b/cmd/podman/varlink/io.podman.varlink index 309f9765a..ace81646c 100644 --- a/cmd/podman/varlink/io.podman.varlink +++ b/cmd/podman/varlink/io.podman.varlink @@ -641,6 +641,14 @@ method StartContainer(name: string) -> (container: string) # ~~~ method StopContainer(name: string, timeout: int) -> (container: string) +# InitContainer initializes the given container. It accepts a container name or +# ID, and will initialize the container matching that ID if possible, and error +# if not. Containers can only be initialized when they are in the Created or +# Exited states. Initialization prepares a container to be started, but does not +# start the container. It is intended to be used to debug a container's state +# prior to starting it. +method InitContainer(name: string) -> (container: string) + # RestartContainer will restart a running container given a container name or ID and timeout value. The timeout # value is the time before a forcible stop is used to stop the container. If the container cannot be found by # name or ID, a [ContainerNotFound](#ContainerNotFound) error will be returned; otherwise, the ID of the @@ -1210,6 +1218,8 @@ method GetLayersMapWithImageInfo() -> (layerMap: string) # BuildImageHierarchyMap is for the development of Podman and should not be used. method BuildImageHierarchyMap(name: string) -> (imageInfo: string) +method GenerateSystemd(name: string, restart: string, timeout: int, useName: bool) -> (unit: string) + # ImageNotFound means the image could not be found by the provided name or ID in local storage. error ImageNotFound (id: string, reason: string) @@ -1225,7 +1235,7 @@ error PodNotFound (name: string, reason: string) # VolumeNotFound means the volume could not be found by the name or ID in local storage. error VolumeNotFound (id: string, reason: string) -# PodContainerError means a container associated with a pod failed to preform an operation. It contains +# PodContainerError means a container associated with a pod failed to perform an operation. It contains # a container ID of the container that failed. error PodContainerError (podname: string, errors: []PodContainerErrorData) @@ -1233,6 +1243,9 @@ error PodContainerError (podname: string, errors: []PodContainerErrorData) # the pod ID. error NoContainersInPod (name: string) +# InvalidState indicates that a container or pod was in an improper state for the requested operation +error InvalidState (id: string, reason: string) + # ErrorOccurred is a generic error for an error that occurs during the execution. The actual error message # is includes as part of the error's text. error ErrorOccurred (reason: string) @@ -1241,4 +1254,4 @@ error ErrorOccurred (reason: string) error RuntimeError (reason: string) # The Podman endpoint requires that you use a streaming connection. -error WantsMoreRequired (reason: string) +error WantsMoreRequired (reason: string)
\ No newline at end of file diff --git a/commands.md b/commands.md index 1c05640f2..e6c211254 100644 --- a/commands.md +++ b/commands.md @@ -25,6 +25,8 @@ Command | Descr [podman-exec(1)](/docs/podman-exec.1.md) | Execute a command in a running container | [podman-export(1)](/docs/podman-export.1.md) | Export container's filesystem contents as a tar archive | [podman-generate(1)](/docs/podman-generate.1.md) | Generate structured output based on Podman containers and pods | +[podman-generate-kube(1)](/docs/podman-generate-kube.1.md) | Generate Kubernetes YAML based on a container or Pod | +[podman-generate-systemd(1)](/docs/podman-generate-systemd.1.md) | Generate a Systemd unit file for a container | [podman-history(1)](/docs/podman-history.1.md) | Shows the history of an image | [podman-image(1)](/docs/podman-image.1.md) | Manage Images | [podman-image-exists(1)](/docs/podman-image-exists.1.md) | Check if an image exists in local storage | @@ -34,6 +36,7 @@ Command | Descr [podman-images(1)](/docs/podman-images.1.md) | List images in local storage | [![...](/docs/play.png)](https://podman.io/asciinema/podman/images/) | [Here](https://github.com/containers/Demos/blob/master/podman_cli/podman_images.sh) [podman-import(1)](/docs/podman-import.1.md) | Import a tarball and save it as a filesystem image | [podman-info(1)](/docs/podman-info.1.md) | Display system information | +[podman-init(1)](/docs/podman-init.1.md) | Initialize a container | [podman-inspect(1)](/docs/podman-inspect.1.md) | Display the configuration of a container or image | [![...](/docs/play.png)](https://asciinema.org/a/133418) [podman-kill(1)](/docs/podman-kill.1.md) | Kill the main process in one or more running containers | [podman-load(1)](/docs/podman-load.1.md) | Load an image from a container image archive | diff --git a/completions/bash/podman b/completions/bash/podman index b5963f8b9..e3c0c1dbf 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -780,6 +780,10 @@ _podman_container_export() { _podman_export } +_podman_container_init() { + _podman_init +} + _podman_container_inspect() { _podman_inspect } @@ -915,6 +919,7 @@ _podman_generate() { " subcommands=" kube + systemd " __podman_subcommands "$subcommands $aliases" && return @@ -2223,6 +2228,27 @@ _podman_ps() { _complete_ "$options_with_args" "$boolean_options" } +_podman_init() { + local boolean_options=" + --all + -a + --help + -h + --latest + -l + " + local options_with_args=" + " + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + __podman_complete_containers_unpauseable + ;; + esac +} + _podman_start() { local options_with_args=" --detach-keys @@ -2435,6 +2461,32 @@ _podman_generate_kube() { esac } +_podman_generate_systemd() { + local options_with_args=" + --restart-policy + -t + --timeout" + + local boolean_options=" + -h + --help + -n + --name + " + + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + COMPREPLY=( $( compgen -W " + $(__podman_containers --all) + " -- "$cur" ) ) + __ltrim_colon_completions "$cur" + ;; + esac +} + _podman_play_kube() { local options_with_args=" --authfile diff --git a/contrib/cirrus/build_vm_images.sh b/contrib/cirrus/build_vm_images.sh index 6b86aa4d4..fc8cbb404 100755 --- a/contrib/cirrus/build_vm_images.sh +++ b/contrib/cirrus/build_vm_images.sh @@ -3,25 +3,7 @@ set -e source $(dirname $0)/lib.sh -req_env_var " -CNI_COMMIT $CNI_COMMIT -CRIO_COMMIT $CRIO_COMMIT -RUNC_COMMIT $RUNC_COMMIT -PACKER_BUILDS $PACKER_BUILDS -BUILT_IMAGE_SUFFIX $BUILT_IMAGE_SUFFIX -CENTOS_BASE_IMAGE $CENTOS_BASE_IMAGE -UBUNTU_BASE_IMAGE $UBUNTU_BASE_IMAGE -FEDORA_BASE_IMAGE $FEDORA_BASE_IMAGE -FAH_BASE_IMAGE $FAH_BASE_IMAGE -RHEL_BASE_IMAGE $RHEL_BASE_IMAGE -RHSM_COMMAND $RHSM_COMMAND -SERVICE_ACCOUNT $SERVICE_ACCOUNT -GCE_SSH_USERNAME $GCE_SSH_USERNAME -GCP_PROJECT_ID $GCP_PROJECT_ID -PACKER_VER $PACKER_VER -SCRIPT_BASE $SCRIPT_BASE -PACKER_BASE $PACKER_BASE -" +req_env_var CNI_COMMIT CRIO_COMMIT RUNC_COMMIT PACKER_BUILDS BUILT_IMAGE_SUFFIX CENTOS_BASE_IMAGE UBUNTU_BASE_IMAGE FEDORA_BASE_IMAGE FAH_BASE_IMAGE RHEL_BASE_IMAGE RHSM_COMMAND SERVICE_ACCOUNT GCE_SSH_USERNAME GCP_PROJECT_ID PACKER_VER SCRIPT_BASE PACKER_BASE record_timestamp "cache-image build start" diff --git a/contrib/cirrus/integration_test.sh b/contrib/cirrus/integration_test.sh index 95387ff49..71223803c 100755 --- a/contrib/cirrus/integration_test.sh +++ b/contrib/cirrus/integration_test.sh @@ -3,13 +3,7 @@ set -e source $(dirname $0)/lib.sh -req_env_var " -GOSRC $GOSRC -SCRIPT_BASE $SCRIPT_BASE -OS_RELEASE_ID $OS_RELEASE_ID -OS_RELEASE_VER $OS_RELEASE_VER -CONTAINER_RUNTIME $CONTAINER_RUNTIME -" +req_env_var GOSRC SCRIPT_BASE OS_RELEASE_ID OS_RELEASE_VER CONTAINER_RUNTIME exit_handler() { set +ex @@ -39,7 +33,7 @@ then exit $? elif [[ "$SPECIALMODE" == "rootless" ]] then - req_env_var "ROOTLESS_USER $ROOTLESS_USER" + req_env_var ROOTLESS_USER set -x ssh $ROOTLESS_USER@localhost \ -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o CheckHostIP=no \ diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh index d663616b2..1c459ebc7 100644 --- a/contrib/cirrus/lib.sh +++ b/contrib/cirrus/lib.sh @@ -33,15 +33,27 @@ then source "$HOME/$ENVLIB" fi -# Pass in a line delimited list of, space delimited name/value pairs -# exit non-zero with helpful error message if any value is empty +# Pass in a list of one or more envariable names; exit non-zero with +# helpful error message if any value is empty req_env_var() { - echo "$1" | while read NAME VALUE - do - if [[ -n "$NAME" ]] && [[ -z "$VALUE" ]] - then - echo "Required env. var. \$$NAME is not set" - exit 9 + # Provide context. If invoked from function use its name; else script name + local caller=${FUNCNAME[1]} + if [[ -n "$caller" ]]; then + # Indicate that it's a function name + caller="$caller()" + else + # Not called from a function: use script name + caller=$(basename $0) + fi + + # Usage check + [[ -n "$1" ]] || die 1 "FATAL: req_env_var: invoked without arguments" + + # Each input arg is an envariable name, e.g. HOME PATH etc. Expand each. + # If any is empty, bail out and explain why. + for i; do + if [[ -z "${!i}" ]]; then + die 9 "FATAL: $caller requires \$$i to be non-empty" fi done } @@ -97,20 +109,14 @@ PACKER_BUILDS $PACKER_BUILDS # Unset environment variables not needed for testing purposes clean_env() { - req_env_var " - UNSET_ENV_VARS $UNSET_ENV_VARS - " + req_env_var UNSET_ENV_VARS echo "Unsetting $(echo $UNSET_ENV_VARS | wc -w) environment variables" unset -v UNSET_ENV_VARS $UNSET_ENV_VARS || true # don't fail on read-only } die() { - req_env_var " - 1 $1 - 2 $2 - " - echo "$2" - exit $1 + echo "${2:-FATAL ERROR (but no message given!) in ${FUNCNAME[1]}()}" + exit ${1:-1} } # Return a GCE image-name compatible string representation of distribution name @@ -135,10 +141,8 @@ stub() { } ircmsg() { - req_env_var " - CIRRUS_TASK_ID $CIRRUS_TASK_ID - @ $@ - " + req_env_var CIRRUS_TASK_ID + [[ -n "$*" ]] || die 9 "ircmsg() invoked without args" # Sometimes setup_environment.sh didn't run SCRIPT="$(dirname $0)/podbot.py" NICK="podbot_$CIRRUS_TASK_ID" @@ -151,7 +155,7 @@ ircmsg() { record_timestamp() { set +x # sometimes it's turned on - req_env_var "TIMESTAMPS_FILEPATH $TIMESTAMPS_FILEPATH" + req_env_var TIMESTAMPS_FILEPATH echo "." # cirrus webui strips blank-lines STAMPMSG="The $1 time at the tone will be:" echo -e "$STAMPMSG\t$(date --iso-8601=seconds)" | \ @@ -160,11 +164,7 @@ record_timestamp() { } setup_rootless() { - req_env_var " - ROOTLESS_USER $ROOTLESS_USER - GOSRC $GOSRC - ENVLIB $ENVLIB - " + req_env_var ROOTLESS_USER GOSRC ENVLIB if passwd --status $ROOTLESS_USER then @@ -220,7 +220,7 @@ setup_rootless() { # Helper/wrapper script to only show stderr/stdout on non-zero exit install_ooe() { - req_env_var "SCRIPT_BASE $SCRIPT_BASE" + req_env_var SCRIPT_BASE echo "Installing script to mask stdout/stderr unless non-zero exit." sudo install -D -m 755 "/tmp/libpod/$SCRIPT_BASE/ooe.sh" /usr/local/bin/ooe.sh } @@ -241,10 +241,7 @@ EOF install_cni_plugins() { echo "Installing CNI Plugins from commit $CNI_COMMIT" - req_env_var " - GOPATH $GOPATH - CNI_COMMIT $CNI_COMMIT - " + req_env_var GOPATH CNI_COMMIT DEST="$GOPATH/src/github.com/containernetworking/plugins" rm -rf "$DEST" ooe.sh git clone "https://github.com/containernetworking/plugins.git" "$DEST" @@ -272,11 +269,7 @@ install_runc(){ OS_RELEASE_ID=$(os_release_id) echo "Installing RunC from commit $RUNC_COMMIT" echo "Platform is $OS_RELEASE_ID" - req_env_var " - GOPATH $GOPATH - RUNC_COMMIT $RUNC_COMMIT - OS_RELEASE_ID $OS_RELEASE_ID - " + req_env_var GOPATH RUNC_COMMIT OS_RELEASE_ID if [[ "$OS_RELEASE_ID" =~ "ubuntu" ]]; then echo "Running make install.libseccomp.sudo for ubuntu" if ! [[ -d "/tmp/libpod" ]] @@ -295,7 +288,7 @@ install_runc(){ install_buildah() { echo "Installing buildah from latest upstream master" - req_env_var "GOPATH $GOPATH" + req_env_var GOPATH DEST="$GOPATH/src/github.com/containers/buildah" rm -rf "$DEST" ooe.sh git clone https://github.com/containers/buildah "$DEST" @@ -307,10 +300,7 @@ install_buildah() { # Requires $GOPATH and $CRIO_COMMIT to be set install_conmon(){ echo "Installing conmon from commit $CRIO_COMMIT" - req_env_var " - GOPATH $GOPATH - CRIO_COMMIT $CRIO_COMMIT - " + req_env_var GOPATH CRIO_COMMIT DEST="$GOPATH/src/github.com/kubernetes-sigs/cri-o.git" rm -rf "$DEST" ooe.sh git clone https://github.com/kubernetes-sigs/cri-o.git "$DEST" @@ -327,9 +317,7 @@ install_criu(){ echo "Installing CRIU" echo "Installing CRIU from commit $CRIU_COMMIT" echo "Platform is $OS_RELEASE_ID" - req_env_var " - CRIU_COMMIT $CRIU_COMMIT - " + req_env_var CRIU_COMMIT if [[ "$OS_RELEASE_ID" =~ "ubuntu" ]]; then ooe.sh sudo -E add-apt-repository -y ppa:criu/ppa @@ -418,10 +406,7 @@ ubuntu_finalize(){ rhel_exit_handler() { set +ex - req_env_var " - GOPATH $GOPATH - RHSMCMD $RHSMCMD - " + req_env_var GOPATH RHSMCMD cd / sudo rm -rf "$RHSMCMD" sudo rm -rf "$GOPATH" @@ -431,9 +416,7 @@ rhel_exit_handler() { } rhsm_enable() { - req_env_var " - RHSM_COMMAND $RHSM_COMMAND - " + req_env_var RHSM_COMMAND export GOPATH="$(mktemp -d)" export RHSMCMD="$(mktemp)" trap "rhel_exit_handler" EXIT diff --git a/contrib/cirrus/lib.sh.t b/contrib/cirrus/lib.sh.t new file mode 100755 index 000000000..ce51f8ad2 --- /dev/null +++ b/contrib/cirrus/lib.sh.t @@ -0,0 +1,81 @@ +#!/bin/bash +# +# Unit tests for some functions in lib.sh +# +source $(dirname $0)/lib.sh + +# Iterator and return code; updated in test functions +testnum=0 +rc=0 + +function check_result { + testnum=$(expr $testnum + 1) + if [ "$1" = "$2" ]; then + echo "ok $testnum $3 = $1" + else + echo "not ok $testnum $3" + echo "# expected: $2" + echo "# actual: $1" + rc=1 + fi +} + +############################################################################### +# tests for die() + +function test_die() { + local input_status=$1 + local input_msg=$2 + local expected_status=$3 + local expected_msg=$4 + + local msg + msg=$(die $input_status "$input_msg") + local status=$? + + check_result "$msg" "$expected_msg" "die $input_status $input_msg" +} + +test_die 1 "a message" 1 "a message" +test_die 2 "" 2 "FATAL ERROR (but no message given!) in test_die()" +test_die '' '' 1 "FATAL ERROR (but no message given!) in test_die()" + +############################################################################### +# tests for req_env_var() + +function test_rev() { + local input_args=$1 + local expected_status=$2 + local expected_msg=$3 + + # bash gotcha: doing 'local msg=...' on one line loses exit status + local msg + msg=$(req_env_var $input_args) + local status=$? + + check_result "$msg" "$expected_msg" "req_env_var $input_args" + check_result "$status" "$expected_status" "req_env_var $input_args (rc)" +} + +# error if called with no args +test_rev '' 1 'FATAL: req_env_var: invoked without arguments' + +# error if desired envariable is unset +unset FOO BAR +test_rev FOO 9 'FATAL: test_rev() requires $FOO to be non-empty' +test_rev BAR 9 'FATAL: test_rev() requires $BAR to be non-empty' + +# OK if desired envariable is unset +FOO=1 +test_rev FOO 0 '' + +# ...but error if any single desired one is unset +test_rev "FOO BAR" 9 'FATAL: test_rev() requires $BAR to be non-empty' + +# ...and OK if all args are set +BAR=1 +test_rev "FOO BAR" 0 '' + +############################################################################### + +exit $rc diff --git a/contrib/cirrus/packer/centos_setup.sh b/contrib/cirrus/packer/centos_setup.sh index d947a1d7f..91b1963c2 100644 --- a/contrib/cirrus/packer/centos_setup.sh +++ b/contrib/cirrus/packer/centos_setup.sh @@ -8,12 +8,7 @@ set -e # Load in library (copied by packer, before this script was run) source /tmp/libpod/$SCRIPT_BASE/lib.sh -req_env_var " -SCRIPT_BASE $SCRIPT_BASE -CNI_COMMIT $CNI_COMMIT -CRIO_COMMIT $CRIO_COMMIT -CRIU_COMMIT $CRIU_COMMIT -" +req_env_var SCRIPT_BASE CNI_COMMIT CRIO_COMMIT CRIU_COMMIT install_ooe diff --git a/contrib/cirrus/packer/fah_setup.sh b/contrib/cirrus/packer/fah_setup.sh index 2e053b396..18c4db0af 100644 --- a/contrib/cirrus/packer/fah_setup.sh +++ b/contrib/cirrus/packer/fah_setup.sh @@ -8,9 +8,7 @@ set -e # Load in library (copied by packer, before this script was run) source /tmp/libpod/$SCRIPT_BASE/lib.sh -req_env_var " -SCRIPT_BASE $SCRIPT_BASE -" +req_env_var SCRIPT_BASE install_ooe diff --git a/contrib/cirrus/packer/fedora_setup.sh b/contrib/cirrus/packer/fedora_setup.sh index 84aee7667..36a65eb71 100644 --- a/contrib/cirrus/packer/fedora_setup.sh +++ b/contrib/cirrus/packer/fedora_setup.sh @@ -8,14 +8,7 @@ set -e # Load in library (copied by packer, before this script was run) source /tmp/libpod/$SCRIPT_BASE/lib.sh -req_env_var " -SCRIPT_BASE $SCRIPT_BASE -FEDORA_CNI_COMMIT $FEDORA_CNI_COMMIT -CNI_COMMIT $CNI_COMMIT -CRIO_COMMIT $CRIO_COMMIT -CRIU_COMMIT $CRIU_COMMIT -RUNC_COMMIT $RUNC_COMMIT -" +req_env_var SCRIPT_BASE FEDORA_CNI_COMMIT CNI_COMMIT CRIO_COMMIT CRIU_COMMIT RUNC_COMMIT install_ooe diff --git a/contrib/cirrus/packer/image-builder-image_base-setup.sh b/contrib/cirrus/packer/image-builder-image_base-setup.sh index 8cf9fd8ab..43cfa7180 100644 --- a/contrib/cirrus/packer/image-builder-image_base-setup.sh +++ b/contrib/cirrus/packer/image-builder-image_base-setup.sh @@ -11,12 +11,7 @@ set -e # Load in library (copied by packer, before this script was run) source $GOSRC/$SCRIPT_BASE/lib.sh -req_env_var " - TIMESTAMP $TIMESTAMP - GOSRC $GOSRC - SCRIPT_BASE $SCRIPT_BASE - PACKER_BASE $PACKER_BASE -" +req_env_var TIMESTAMP GOSRC SCRIPT_BASE PACKER_BASE install_ooe diff --git a/contrib/cirrus/packer/rhel_base-setup.sh b/contrib/cirrus/packer/rhel_base-setup.sh index fbf9f61af..8d5892d7d 100644 --- a/contrib/cirrus/packer/rhel_base-setup.sh +++ b/contrib/cirrus/packer/rhel_base-setup.sh @@ -10,9 +10,7 @@ set -e # Load in library (copied by packer, before this script was run) source $GOSRC/$SCRIPT_BASE/lib.sh -req_env_var " - RHSM_COMMAND $RHSM_COMMAND -" +req_env_var RHSM_COMMAND install_ooe diff --git a/contrib/cirrus/packer/rhel_setup.sh b/contrib/cirrus/packer/rhel_setup.sh index 20be97f9b..45f5c3e9b 100644 --- a/contrib/cirrus/packer/rhel_setup.sh +++ b/contrib/cirrus/packer/rhel_setup.sh @@ -8,13 +8,7 @@ set -e # Load in library (copied by packer, before this script was run) source /tmp/libpod/$SCRIPT_BASE/lib.sh -req_env_var " -SCRIPT_BASE $SCRIPT_BASE -CNI_COMMIT $CNI_COMMIT -CRIO_COMMIT $CRIO_COMMIT -CRIU_COMMIT $CRIU_COMMIT -RHSM_COMMAND $RHSM_COMMAND -" +req_env_var SCRIPT_BASE CNI_COMMIT CRIO_COMMIT CRIU_COMMIT RHSM_COMMAND install_ooe diff --git a/contrib/cirrus/packer/ubuntu_setup.sh b/contrib/cirrus/packer/ubuntu_setup.sh index e84566ce3..d3ac8bddb 100644 --- a/contrib/cirrus/packer/ubuntu_setup.sh +++ b/contrib/cirrus/packer/ubuntu_setup.sh @@ -8,13 +8,7 @@ set -e # Load in library (copied by packer, before this script was run) source /tmp/libpod/$SCRIPT_BASE/lib.sh -req_env_var " -SCRIPT_BASE $SCRIPT_BASE -CNI_COMMIT $CNI_COMMIT -CRIO_COMMIT $CRIO_COMMIT -CRIU_COMMIT $CRIU_COMMIT -RUNC_COMMIT $RUNC_COMMIT -" +req_env_var SCRIPT_BASE CNI_COMMIT CRIO_COMMIT CRIU_COMMIT RUNC_COMMIT install_ooe diff --git a/contrib/cirrus/rootless_test.sh b/contrib/cirrus/rootless_test.sh index 09c57f993..2803d4318 100755 --- a/contrib/cirrus/rootless_test.sh +++ b/contrib/cirrus/rootless_test.sh @@ -6,11 +6,7 @@ source $HOME/.bash_profile cd $GOSRC source $(dirname $0)/lib.sh -req_env_var " -GOSRC $GOSRC -OS_RELEASE_ID $OS_RELEASE_ID -OS_RELEASE_VER $OS_RELEASE_VER -" +req_env_var GOSRC OS_RELEASE_ID OS_RELEASE_VER if [[ "$UID" == "0" ]] then diff --git a/contrib/cirrus/setup_container_environment.sh b/contrib/cirrus/setup_container_environment.sh index 23df4fe8b..eda6f6167 100755 --- a/contrib/cirrus/setup_container_environment.sh +++ b/contrib/cirrus/setup_container_environment.sh @@ -3,11 +3,7 @@ set -e source $(dirname $0)/lib.sh -req_env_var " -GOSRC $GOSRC -OS_RELEASE_ID $OS_RELEASE_ID -CONTAINER_RUNTIME $CONTAINER_RUNTIME -" +req_env_var GOSRC OS_RELEASE_ID CONTAINER_RUNTIME DIST=$OS_RELEASE_ID IMAGE=${DIST}podmanbuild diff --git a/contrib/cirrus/setup_environment.sh b/contrib/cirrus/setup_environment.sh index 3818abbc7..3bc6c2290 100755 --- a/contrib/cirrus/setup_environment.sh +++ b/contrib/cirrus/setup_environment.sh @@ -6,12 +6,7 @@ source $(dirname $0)/lib.sh record_timestamp "env. setup start" -req_env_var " -USER $USER -HOME $HOME -ENVLIB $ENVLIB -SCRIPT_BASE $SCRIPT_BASE -CIRRUS_BUILD_ID $CIRRUS_BUILD_ID" +req_env_var USER HOME ENVLIB SCRIPT_BASE CIRRUS_BUILD_ID [[ "$SHELL" =~ "bash" ]] || chsh -s /bin/bash diff --git a/contrib/cirrus/success.sh b/contrib/cirrus/success.sh index 2b0cf4655..c4e150514 100755 --- a/contrib/cirrus/success.sh +++ b/contrib/cirrus/success.sh @@ -4,10 +4,7 @@ set -e source $(dirname $0)/lib.sh -req_env_var " - CIRRUS_BRANCH $CIRRUS_BRANCH - CIRRUS_BUILD_ID $CIRRUS_BUILD_ID -" +req_env_var CIRRUS_BRANCH CIRRUS_BUILD_ID REF=$(basename $CIRRUS_BRANCH) # PR number or branch named URL="https://cirrus-ci.com/build/$CIRRUS_BUILD_ID" diff --git a/contrib/cirrus/system_test.sh b/contrib/cirrus/system_test.sh index cb179407a..dd5ef511d 100755 --- a/contrib/cirrus/system_test.sh +++ b/contrib/cirrus/system_test.sh @@ -3,11 +3,7 @@ set -e source $(dirname $0)/lib.sh -req_env_var " -GOSRC $GOSRC -OS_RELEASE_ID $OS_RELEASE_ID -OS_RELEASE_VER $OS_RELEASE_VER -" +req_env_var GOSRC OS_RELEASE_ID OS_RELEASE_VER clean_env diff --git a/contrib/cirrus/unit_test.sh b/contrib/cirrus/unit_test.sh index 4ace19d10..a0964061f 100755 --- a/contrib/cirrus/unit_test.sh +++ b/contrib/cirrus/unit_test.sh @@ -3,11 +3,7 @@ set -e source $(dirname $0)/lib.sh -req_env_var " -GOSRC $GOSRC -OS_RELEASE_ID $OS_RELEASE_ID -OS_RELEASE_VER $OS_RELEASE_VER -" +req_env_var GOSRC OS_RELEASE_ID OS_RELEASE_VER record_timestamp "unit test start" diff --git a/docs/podman-container.1.md b/docs/podman-container.1.md index 1ba957480..564d791fa 100644 --- a/docs/podman-container.1.md +++ b/docs/podman-container.1.md @@ -22,6 +22,7 @@ The container command allows you to manage containers | exec | [podman-exec(1)](podman-exec.1.md) | Execute a command in a running container. | | exists | [podman-container-exists(1)](podman-container-exists.1.md) | Check if a container exists in local storage | | export | [podman-export(1)](podman-export.1.md) | Export a container's filesystem contents as a tar archive. | +| init | [podman-init(1)](podman-init.1.md) | Initialize a container | | inspect | [podman-inspect(1)](podman-inspect.1.md) | Display a container or image's configuration. | | kill | [podman-kill(1)](podman-kill.1.md) | Kill the main process in one or more containers. | | list | [podman-ps(1)](podman-ps.1.md) | List the containers on the system.(alias ls) | diff --git a/docs/podman-create.1.md b/docs/podman-create.1.md index 884a8adcc..6d7d983b6 100644 --- a/docs/podman-create.1.md +++ b/docs/podman-create.1.md @@ -250,7 +250,17 @@ By default proxy environment variables are passed into the container if set for the podman process. This can be disabled by setting the `--http-proxy` option to `false`. The environment variables passed in include `http_proxy`, `https_proxy`, `ftp_proxy`, `no_proxy`, and also the upper case versions of -those. +those. This option is only needed when the host system must use a proxy but +the container should not use any proxy. Proxy environment variables specified +for the container in any other way will override the values that would have +been passed thru from the host. (Other ways to specify the proxy for the +container include passing the values with the `--env` flag, or hardcoding the +proxy environment at container build time.) + +For example, to disable passing these environment variables from host to +container: + +`--http-proxy=false` Defaults to `true` @@ -269,7 +279,7 @@ The following example maps uids 0-2000 in the container to the uids 30000-31999 Add additional groups to run as -**--healthchech**="" +**--healthcheck**="" Set or alter a healthcheck for a container. The value must be of the format of: diff --git a/docs/podman-generate-systemd.1.md b/docs/podman-generate-systemd.1.md new file mode 100644 index 000000000..cc3f098a6 --- /dev/null +++ b/docs/podman-generate-systemd.1.md @@ -0,0 +1,69 @@ +% podman-generate Podman Man Pages +% Brent Baude +% April 2019 +# NAME +podman-generate-systemd- Generate Systemd Unit file + +# SYNOPSIS +**podman generate systemd** [*-n*|*--name*] [*-t*|*--timeout*] [*--restart-policy*] *container* + +# DESCRIPTION +**podman generate systemd** will create a Systemd unit file that can be used to control a container. The +command will dynamically create the unit file and output it to stdout where it can be piped by the user +to a file. The options can be used to influence the results of the output as well. + + +# OPTIONS: + +**--name** **-n** + +Use the name of the container for the start, stop, and description in the unit file + +**--timeout** **-t** + +Override the default stop timeout for the container with the given value. + +**--restart-policy** +Set the SystemD restart policy. The restart-policy must be one of: "no", "on-success", "on-failure", "on-abnormal", +"on-watchdog", "on-abort", or "always". The default policy is *on-failure*. + +## Examples ## + +Create a systemd unit file for a container running nginx: + +``` +$ sudo podman generate systemd nginx +[Unit] +Description=c21da63c4783be2ac2cd3487ef8d2ec15ee2a28f63dd8f145e3b05607f31cffc Podman Container +[Service] +Restart=on-failure +ExecStart=/usr/bin/podman start c21da63c4783be2ac2cd3487ef8d2ec15ee2a28f63dd8f145e3b05607f31cffc +ExecStop=/usr/bin/podman stop -t 10 c21da63c4783be2ac2cd3487ef8d2ec15ee2a28f63dd8f145e3b05607f31cffc +KillMode=none +Type=forking +PIDFile=/var/lib/containers/storage/overlay-containers/c21da63c4783be2ac2cd3487ef8d2ec15ee2a28f63dd8f145e3b05607f31cffc/userdata/c21da63c4783be2ac2cd3487ef8d2ec15ee2a28f63dd8f145e3b05607f31cffc.pid +[Install] +WantedBy=multi-user.target +``` + +Create a systemd unit file for a container running nginx with an *always* restart policy and 1-second timeout. +``` +$ sudo podman generate systemd --restart-policy=always -t 1 nginx +[Unit] +Description=c21da63c4783be2ac2cd3487ef8d2ec15ee2a28f63dd8f145e3b05607f31cffc Podman Container +[Service] +Restart=always +ExecStart=/usr/bin/podman start c21da63c4783be2ac2cd3487ef8d2ec15ee2a28f63dd8f145e3b05607f31cffc +ExecStop=/usr/bin/podman stop -t 1 c21da63c4783be2ac2cd3487ef8d2ec15ee2a28f63dd8f145e3b05607f31cffc +KillMode=none +Type=forking +PIDFile=/var/lib/containers/storage/overlay-containers/c21da63c4783be2ac2cd3487ef8d2ec15ee2a28f63dd8f145e3b05607f31cffc/userdata/c21da63c4783be2ac2cd3487ef8d2ec15ee2a28f63dd8f145e3b05607f31cffc.pid +[Install] +WantedBy=multi-user.target +``` + +## SEE ALSO +podman(1), podman-container(1) + +# HISTORY +April 2019, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/docs/podman-generate.1.md b/docs/podman-generate.1.md index d1736f38e..5a2386778 100644 --- a/docs/podman-generate.1.md +++ b/docs/podman-generate.1.md @@ -14,6 +14,7 @@ The generate command will create structured output (like YAML) based on a contai | Command | Man Page | Description | | ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- | | kube | [podman-generate-kube(1)](podman-generate-kube.1.md)| Generate Kubernetes YAML based on a pod or container. | +| systemd | [podman-generate-systemd(1)](podman-generate-systemd.1.md)| Generate a systemd unit file for a container. | ## SEE ALSO podman, podman-pod, podman-container diff --git a/docs/podman-init.1.md b/docs/podman-init.1.md new file mode 100644 index 000000000..f43757f62 --- /dev/null +++ b/docs/podman-init.1.md @@ -0,0 +1,41 @@ +% podman-init(1) + +## NAME +podman\-init - Initialize one or more containers + +## SYNOPSIS +**podman init** [*options*] *container* ... + +## DESCRIPTION +Initialize one or more containers. +You may use container IDs or names as input. +Initializing a container performs all tasks necessary for starting the container (mounting filesystems, creating an OCI spec, initializing the container network) but does not start the container. +If a container is not initialized, the `podman start` and `podman run` commands will do so automatically prior to starting it. +This command is intended to be used for inspecting or modifying the container's filesystem or OCI spec prior to starting it. +This can be used to inspect the container before it runs, or debug why a container is failing to run. + +## OPTIONS + +**--all, -a** + +Initialize all containers. Containers that have already initialized (including containers that have been started and are running) are ignored. + +**--latest, -l** +Instead of providing the container name or ID, use the last created container. If you use methods other than Podman +to run containers such as CRI-O, the last started container could be from either of those methods. + +The latest option is not supported on the remote client. + +## EXAMPLE + +podman init 35480fc9d568 + +podman init test1 + +podman init --latest + +## SEE ALSO +podman(1), podman-start(1) + +## HISTORY +April 2019, Originally compiled by Matthew Heon <mheon@redhat.com> diff --git a/docs/podman-run.1.md b/docs/podman-run.1.md index a0c17652a..9efb7f51c 100644 --- a/docs/podman-run.1.md +++ b/docs/podman-run.1.md @@ -257,7 +257,17 @@ By default proxy environment variables are passed into the container if set for the podman process. This can be disabled by setting the `--http-proxy` option to `false`. The environment variables passed in include `http_proxy`, `https_proxy`, `ftp_proxy`, `no_proxy`, and also the upper case versions of -those. +those. This option is only needed when the host system must use a proxy but +the container should not use any proxy. Proxy environment variables specified +for the container in any other way will override the values that would have +been passed thru from the host. (Other ways to specify the proxy for the +container include passing the values with the `--env` flag, or hardcoding the +proxy environment at container build time.) + +For example, to disable passing these environment variables from host to +container: + +`--http-proxy=false` Defaults to `true` @@ -277,7 +287,7 @@ The example maps gids 0-2000 in the container to the gids 30000-31999 on the hos Add additional groups to run as -**--healthchech**="" +**--healthcheck**="" Set or alter a healthcheck for a container. The value must be of the format of: @@ -583,7 +593,7 @@ Not implemented. Restart should be handled via a systemd unit files. Please add your podman commands to a unit file and allow systemd or your init system to handle the -restarting of the container processes. See example below. +restarting of the container processes. See *podman generate systemd*. **--rm**=*true*|*false* @@ -1141,21 +1151,6 @@ the uids and gids from the host. $ podman run --uidmap 0:30000:7000 --gidmap 0:30000:7000 fedora echo hello ``` -### Running a podman container to restart inside of a systemd unit file - - -``` -[Unit] -Description=My App -[Service] -Restart=always -ExecStart=/usr/bin/podman start -a my_app -ExecStop=/usr/bin/podman stop -t 10 my_app -KillMode=process -[Install] -WantedBy=multi-user.target -``` - ### Configuring Storage Options from the command line Podman allows for the configuration of storage by changing the values diff --git a/docs/podman.1.md b/docs/podman.1.md index 9c0ca8a7a..ef12cf1cc 100644 --- a/docs/podman.1.md +++ b/docs/podman.1.md @@ -147,6 +147,7 @@ the exit codes follow the `chroot` standard, see below: | [podman-images(1)](podman-images.1.md) | List images in local storage. | | [podman-import(1)](podman-import.1.md) | Import a tarball and save it as a filesystem image. | | [podman-info(1)](podman-info.1.md) | Displays Podman related system information. | +| [podman-init(1)](podman-init.1.md) | Initialize a container | | [podman-inspect(1)](podman-inspect.1.md) | Display a container or image's configuration. | | [podman-kill(1)](podman-kill.1.md) | Kill the main process in one or more containers. | | [podman-load(1)](podman-load.1.md) | Load an image from a container image archive into container storage. | diff --git a/install.md b/install.md index 548b38c1b..bd7f326c3 100644 --- a/install.md +++ b/install.md @@ -8,6 +8,8 @@ sudo pacman -S podman ``` +If you have problems when running podman in [rootless](README.md#rootless) mode follow [these instructions](https://wiki.archlinux.org/index.php/Linux_Containers#Enable_support_to_run_unprivileged_containers_(optional)) + #### [Fedora](https://www.fedoraproject.org), [CentOS](https://www.centos.org) ```bash diff --git a/libpod/container_api.go b/libpod/container_api.go index 465b23831..5bfd869b3 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -40,7 +40,7 @@ func (c *Container) Init(ctx context.Context) (err error) { if !(c.state.State == ContainerStateConfigured || c.state.State == ContainerStateStopped || c.state.State == ContainerStateExited) { - return errors.Wrapf(ErrCtrExists, "container %s has already been created in runtime", c.ID()) + return errors.Wrapf(ErrCtrStateInvalid, "container %s has already been created in runtime", c.ID()) } // don't recursively start diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 927b71b2b..a791df491 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -299,7 +299,7 @@ func (c *Container) setupStorage(ctx context.Context) error { return errors.Wrapf(err, "error creating container storage") } - if !rootless.IsRootless() && (len(c.config.IDMappings.UIDMap) != 0 || len(c.config.IDMappings.GIDMap) != 0) { + if len(c.config.IDMappings.UIDMap) != 0 || len(c.config.IDMappings.GIDMap) != 0 { if err := os.Chown(containerInfo.RunDir, c.RootUID(), c.RootGID()); err != nil { return err } @@ -811,8 +811,9 @@ func (c *Container) cleanupRuntime(ctx context.Context) error { span.SetTag("struct", "container") defer span.Finish() - // If the container is not ContainerStateStopped, do nothing - if c.state.State != ContainerStateStopped { + // If the container is not ContainerStateStopped or + // ContainerStateCreated, do nothing. + if c.state.State != ContainerStateStopped && c.state.State != ContainerStateCreated { return nil } @@ -825,9 +826,14 @@ func (c *Container) cleanupRuntime(ctx context.Context) error { return err } - // Our state is now Exited, as we've removed ourself from - // the runtime. - c.state.State = ContainerStateExited + // If we were Stopped, we are now Exited, as we've removed ourself + // from the runtime. + // If we were Created, we are now Configured. + if c.state.State == ContainerStateStopped { + c.state.State = ContainerStateExited + } else if c.state.State == ContainerStateCreated { + c.state.State = ContainerStateConfigured + } if c.valid { if err := c.save(); err != nil { diff --git a/libpod/healthcheck.go b/libpod/healthcheck.go index 3a6609740..5c48cc8ee 100644 --- a/libpod/healthcheck.go +++ b/libpod/healthcheck.go @@ -3,16 +3,13 @@ package libpod import ( "bufio" "bytes" - "fmt" "io/ioutil" "os" - "os/exec" "path/filepath" "strings" "time" "github.com/containers/libpod/pkg/inspect" - "github.com/coreos/go-systemd/dbus" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -241,62 +238,6 @@ func (c *Container) GetHealthCheckLog() (inspect.HealthCheckResults, error) { return healthCheck, nil } -// createTimer systemd timers for healthchecks of a container -func (c *Container) createTimer() error { - if c.disableHealthCheckSystemd() { - return nil - } - podman, err := os.Executable() - if err != nil { - return errors.Wrapf(err, "failed to get path for podman for a health check timer") - } - - var cmd = []string{"--unit", fmt.Sprintf("%s", c.ID()), fmt.Sprintf("--on-unit-inactive=%s", c.HealthCheckConfig().Interval.String()), "--timer-property=AccuracySec=1s", podman, "healthcheck", "run", c.ID()} - - conn, err := dbus.NewSystemdConnection() - if err != nil { - return errors.Wrapf(err, "unable to get systemd connection to add healthchecks") - } - conn.Close() - logrus.Debugf("creating systemd-transient files: %s %s", "systemd-run", cmd) - systemdRun := exec.Command("systemd-run", cmd...) - _, err = systemdRun.CombinedOutput() - if err != nil { - return err - } - return nil -} - -// startTimer starts a systemd timer for the healthchecks -func (c *Container) startTimer() error { - if c.disableHealthCheckSystemd() { - return nil - } - conn, err := dbus.NewSystemdConnection() - if err != nil { - return errors.Wrapf(err, "unable to get systemd connection to start healthchecks") - } - defer conn.Close() - _, err = conn.StartUnit(fmt.Sprintf("%s.service", c.ID()), "fail", nil) - return err -} - -// removeTimer removes the systemd timer and unit files -// for the container -func (c *Container) removeTimer() error { - if c.disableHealthCheckSystemd() { - return nil - } - conn, err := dbus.NewSystemdConnection() - if err != nil { - return errors.Wrapf(err, "unable to get systemd connection to remove healthchecks") - } - defer conn.Close() - serviceFile := fmt.Sprintf("%s.timer", c.ID()) - _, err = conn.StopUnit(serviceFile, "fail", nil) - return err -} - // HealthCheckStatus returns the current state of a container with a healthcheck func (c *Container) HealthCheckStatus() (string, error) { if !c.HasHealthCheck() { diff --git a/libpod/healthcheck_linux.go b/libpod/healthcheck_linux.go new file mode 100644 index 000000000..869605ea8 --- /dev/null +++ b/libpod/healthcheck_linux.go @@ -0,0 +1,67 @@ +package libpod + +import ( + "fmt" + "os" + "os/exec" + + "github.com/coreos/go-systemd/dbus" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// createTimer systemd timers for healthchecks of a container +func (c *Container) createTimer() error { + if c.disableHealthCheckSystemd() { + return nil + } + podman, err := os.Executable() + if err != nil { + return errors.Wrapf(err, "failed to get path for podman for a health check timer") + } + + var cmd = []string{"--unit", fmt.Sprintf("%s", c.ID()), fmt.Sprintf("--on-unit-inactive=%s", c.HealthCheckConfig().Interval.String()), "--timer-property=AccuracySec=1s", podman, "healthcheck", "run", c.ID()} + + conn, err := dbus.NewSystemdConnection() + if err != nil { + return errors.Wrapf(err, "unable to get systemd connection to add healthchecks") + } + conn.Close() + logrus.Debugf("creating systemd-transient files: %s %s", "systemd-run", cmd) + systemdRun := exec.Command("systemd-run", cmd...) + _, err = systemdRun.CombinedOutput() + if err != nil { + return err + } + return nil +} + +// startTimer starts a systemd timer for the healthchecks +func (c *Container) startTimer() error { + if c.disableHealthCheckSystemd() { + return nil + } + conn, err := dbus.NewSystemdConnection() + if err != nil { + return errors.Wrapf(err, "unable to get systemd connection to start healthchecks") + } + defer conn.Close() + _, err = conn.StartUnit(fmt.Sprintf("%s.service", c.ID()), "fail", nil) + return err +} + +// removeTimer removes the systemd timer and unit files +// for the container +func (c *Container) removeTimer() error { + if c.disableHealthCheckSystemd() { + return nil + } + conn, err := dbus.NewSystemdConnection() + if err != nil { + return errors.Wrapf(err, "unable to get systemd connection to remove healthchecks") + } + defer conn.Close() + serviceFile := fmt.Sprintf("%s.timer", c.ID()) + _, err = conn.StopUnit(serviceFile, "fail", nil) + return err +} diff --git a/libpod/healthcheck_unsupported.go b/libpod/healthcheck_unsupported.go new file mode 100644 index 000000000..d01d1ccd4 --- /dev/null +++ b/libpod/healthcheck_unsupported.go @@ -0,0 +1,19 @@ +// +build !linux + +package libpod + +// createTimer systemd timers for healthchecks of a container +func (c *Container) createTimer() error { + return ErrNotImplemented +} + +// startTimer starts a systemd timer for the healthchecks +func (c *Container) startTimer() error { + return ErrNotImplemented +} + +// removeTimer removes the systemd timer and unit files +// for the container +func (c *Container) removeTimer() error { + return ErrNotImplemented +} diff --git a/libpod/oci.go b/libpod/oci.go index 189359753..3dfde4f24 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -1,7 +1,6 @@ package libpod import ( - "bufio" "bytes" "fmt" "io/ioutil" @@ -9,21 +8,15 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strings" - "syscall" "time" - "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/util" - "github.com/coreos/go-systemd/activation" "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" - "github.com/opencontainers/selinux/go-selinux" "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" kwait "k8s.io/apimachinery/pkg/util/wait" // TODO import these functions into libpod and remove the import @@ -118,70 +111,6 @@ func createUnitName(prefix string, name string) string { return fmt.Sprintf("%s-%s.scope", prefix, name) } -// Wait for a container which has been sent a signal to stop -func waitContainerStop(ctr *Container, timeout time.Duration) error { - done := make(chan struct{}) - chControl := make(chan struct{}) - go func() { - for { - select { - case <-chControl: - return - default: - // Check if the process is still around - err := unix.Kill(ctr.state.PID, 0) - if err == unix.ESRCH { - close(done) - return - } - time.Sleep(100 * time.Millisecond) - } - } - }() - select { - case <-done: - return nil - case <-time.After(timeout): - close(chControl) - logrus.Debugf("container %s did not die within timeout %d", ctr.ID(), timeout) - return errors.Errorf("container %s did not die within timeout", ctr.ID()) - } -} - -// Wait for a set of given PIDs to stop -func waitPidsStop(pids []int, timeout time.Duration) error { - done := make(chan struct{}) - chControl := make(chan struct{}) - go func() { - for { - select { - case <-chControl: - return - default: - allClosed := true - for _, pid := range pids { - if err := unix.Kill(pid, 0); err != unix.ESRCH { - allClosed = false - break - } - } - if allClosed { - close(done) - return - } - time.Sleep(100 * time.Millisecond) - } - } - }() - select { - case <-done: - return nil - case <-time.After(timeout): - close(chControl) - return errors.Errorf("given PIDs did not die within timeout") - } -} - func bindPorts(ports []ocicni.PortMapping) ([]*os.File, error) { var files []*os.File notifySCTP := false @@ -234,241 +163,6 @@ func bindPorts(ports []ocicni.PortMapping) ([]*os.File, error) { return files, nil } -func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string, restoreOptions *ContainerCheckpointOptions) (err error) { - var stderrBuf bytes.Buffer - - runtimeDir, err := util.GetRootlessRuntimeDir() - if err != nil { - return err - } - - parentPipe, childPipe, err := newPipe() - if err != nil { - return errors.Wrapf(err, "error creating socket pair") - } - - childStartPipe, parentStartPipe, err := newPipe() - if err != nil { - return errors.Wrapf(err, "error creating socket pair for start pipe") - } - - defer parentPipe.Close() - defer parentStartPipe.Close() - - args := []string{} - if r.cgroupManager == SystemdCgroupsManager { - args = append(args, "-s") - } - args = append(args, "-c", ctr.ID()) - args = append(args, "-u", ctr.ID()) - args = append(args, "-r", r.path) - args = append(args, "-b", ctr.bundlePath()) - args = append(args, "-p", filepath.Join(ctr.state.RunDir, "pidfile")) - args = append(args, "-l", ctr.LogPath()) - args = append(args, "--exit-dir", r.exitsDir) - if ctr.config.ConmonPidFile != "" { - args = append(args, "--conmon-pidfile", ctr.config.ConmonPidFile) - } - if len(ctr.config.ExitCommand) > 0 { - args = append(args, "--exit-command", ctr.config.ExitCommand[0]) - for _, arg := range ctr.config.ExitCommand[1:] { - args = append(args, []string{"--exit-command-arg", arg}...) - } - } - args = append(args, "--socket-dir-path", r.socketsDir) - if ctr.config.Spec.Process.Terminal { - args = append(args, "-t") - } else if ctr.config.Stdin { - args = append(args, "-i") - } - if r.logSizeMax >= 0 { - args = append(args, "--log-size-max", fmt.Sprintf("%v", r.logSizeMax)) - } - if r.noPivot { - args = append(args, "--no-pivot") - } - - logLevel := logrus.GetLevel() - args = append(args, "--log-level", logLevel.String()) - - if logLevel == logrus.DebugLevel { - logrus.Debugf("%s messages will be logged to syslog", r.conmonPath) - args = append(args, "--syslog") - } - - if restoreOptions != nil { - args = append(args, "--restore", ctr.CheckpointPath()) - if restoreOptions.TCPEstablished { - args = append(args, "--restore-arg", "--tcp-established") - } - } - - logrus.WithFields(logrus.Fields{ - "args": args, - }).Debugf("running conmon: %s", r.conmonPath) - - cmd := exec.Command(r.conmonPath, args...) - cmd.Dir = ctr.bundlePath() - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } - // TODO this is probably a really bad idea for some uses - // Make this configurable - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if ctr.config.Spec.Process.Terminal { - cmd.Stderr = &stderrBuf - } - - cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe, childStartPipe) - // 0, 1 and 2 are stdin, stdout and stderr - cmd.Env = append(r.conmonEnv, fmt.Sprintf("_OCI_SYNCPIPE=%d", 3)) - cmd.Env = append(cmd.Env, fmt.Sprintf("_OCI_STARTPIPE=%d", 4)) - cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)) - cmd.Env = append(cmd.Env, fmt.Sprintf("_CONTAINERS_USERNS_CONFIGURED=%s", os.Getenv("_CONTAINERS_USERNS_CONFIGURED"))) - cmd.Env = append(cmd.Env, fmt.Sprintf("_CONTAINERS_ROOTLESS_UID=%s", os.Getenv("_CONTAINERS_ROOTLESS_UID"))) - cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", os.Getenv("HOME"))) - - if r.reservePorts && !ctr.config.NetMode.IsSlirp4netns() { - ports, err := bindPorts(ctr.config.PortMappings) - if err != nil { - return err - } - - // Leak the port we bound in the conmon process. These fd's won't be used - // by the container and conmon will keep the ports busy so that another - // process cannot use them. - cmd.ExtraFiles = append(cmd.ExtraFiles, ports...) - } - - if ctr.config.NetMode.IsSlirp4netns() { - ctr.rootlessSlirpSyncR, ctr.rootlessSlirpSyncW, err = os.Pipe() - if err != nil { - return errors.Wrapf(err, "failed to create rootless network sync pipe") - } - // Leak one end in conmon, the other one will be leaked into slirp4netns - cmd.ExtraFiles = append(cmd.ExtraFiles, ctr.rootlessSlirpSyncW) - } - - if notify, ok := os.LookupEnv("NOTIFY_SOCKET"); ok { - cmd.Env = append(cmd.Env, fmt.Sprintf("NOTIFY_SOCKET=%s", notify)) - } - if listenfds, ok := os.LookupEnv("LISTEN_FDS"); ok { - cmd.Env = append(cmd.Env, fmt.Sprintf("LISTEN_FDS=%s", listenfds), "LISTEN_PID=1") - fds := activation.Files(false) - cmd.ExtraFiles = append(cmd.ExtraFiles, fds...) - } - if selinux.GetEnabled() { - // Set the label of the conmon process to be level :s0 - // This will allow the container processes to talk to fifo-files - // passed into the container by conmon - var ( - plabel string - con selinux.Context - ) - plabel, err = selinux.CurrentLabel() - if err != nil { - childPipe.Close() - return errors.Wrapf(err, "Failed to get current SELinux label") - } - - con, err = selinux.NewContext(plabel) - if err != nil { - return errors.Wrapf(err, "Failed to get new context from SELinux label") - } - - runtime.LockOSThread() - if con["level"] != "s0" && con["level"] != "" { - con["level"] = "s0" - if err = label.SetProcessLabel(con.Get()); err != nil { - runtime.UnlockOSThread() - return err - } - } - err = cmd.Start() - // Ignore error returned from SetProcessLabel("") call, - // can't recover. - label.SetProcessLabel("") - runtime.UnlockOSThread() - } else { - err = cmd.Start() - } - if err != nil { - childPipe.Close() - return err - } - defer cmd.Wait() - - // We don't need childPipe on the parent side - childPipe.Close() - childStartPipe.Close() - - // Move conmon to specified cgroup - if err := r.moveConmonToCgroup(ctr, cgroupParent, cmd); err != nil { - return err - } - - /* We set the cgroup, now the child can start creating children */ - someData := []byte{0} - _, err = parentStartPipe.Write(someData) - if err != nil { - return err - } - - /* Wait for initial setup and fork, and reap child */ - err = cmd.Wait() - if err != nil { - return err - } - - defer func() { - if err != nil { - if err2 := r.deleteContainer(ctr); err2 != nil { - logrus.Errorf("Error removing container %s from runtime after creation failed", ctr.ID()) - } - } - }() - - // Wait to get container pid from conmon - type syncStruct struct { - si *syncInfo - err error - } - ch := make(chan syncStruct) - go func() { - var si *syncInfo - rdr := bufio.NewReader(parentPipe) - b, err := rdr.ReadBytes('\n') - if err != nil { - ch <- syncStruct{err: err} - } - if err := json.Unmarshal(b, &si); err != nil { - ch <- syncStruct{err: err} - return - } - ch <- syncStruct{si: si} - }() - - select { - case ss := <-ch: - if ss.err != nil { - return errors.Wrapf(ss.err, "error reading container (probably exited) json message") - } - logrus.Debugf("Received container pid: %d", ss.si.Pid) - if ss.si.Pid == -1 { - if ss.si.Message != "" { - return errors.Wrapf(ErrInternal, "container create failed: %s", ss.si.Message) - } - return errors.Wrapf(ErrInternal, "container create failed") - } - ctr.state.PID = ss.si.Pid - case <-time.After(ContainerCreateTimeout): - return errors.Wrapf(ErrInternal, "container creation timeout") - } - return nil -} - // updateContainerStatus retrieves the current status of the container from the // runtime. It updates the container's state but does not save it. // If useRunc is false, we will not directly hit runc to see the container's @@ -631,82 +325,6 @@ func (r *OCIRuntime) killContainer(ctr *Container, signal uint) error { return nil } -// stopContainer stops a container, first using its given stop signal (or -// SIGTERM if no signal was specified), then using SIGKILL -// Timeout is given in seconds. If timeout is 0, the container will be -// immediately kill with SIGKILL -// Does not set finished time for container, assumes you will run updateStatus -// after to pull the exit code -func (r *OCIRuntime) stopContainer(ctr *Container, timeout uint) error { - logrus.Debugf("Stopping container %s (PID %d)", ctr.ID(), ctr.state.PID) - - // Ping the container to see if it's alive - // If it's not, it's already stopped, return - err := unix.Kill(ctr.state.PID, 0) - if err == unix.ESRCH { - return nil - } - - stopSignal := ctr.config.StopSignal - if stopSignal == 0 { - stopSignal = uint(syscall.SIGTERM) - } - - if timeout > 0 { - if err := r.killContainer(ctr, stopSignal); err != nil { - // Is the container gone? - // If so, it probably died between the first check and - // our sending the signal - // The container is stopped, so exit cleanly - err := unix.Kill(ctr.state.PID, 0) - if err == unix.ESRCH { - return nil - } - - return err - } - - if err := waitContainerStop(ctr, time.Duration(timeout)*time.Second); err != nil { - logrus.Warnf("Timed out stopping container %s, resorting to SIGKILL", ctr.ID()) - } else { - // No error, the container is dead - return nil - } - } - - var args []string - if rootless.IsRootless() { - // we don't use --all for rootless containers as the OCI runtime might use - // the cgroups to determine the PIDs, but for rootless containers there is - // not any. - args = []string{"kill", ctr.ID(), "KILL"} - } else { - args = []string{"kill", "--all", ctr.ID(), "KILL"} - } - - runtimeDir, err := util.GetRootlessRuntimeDir() - if err != nil { - return err - } - env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, args...); err != nil { - // Again, check if the container is gone. If it is, exit cleanly. - err := unix.Kill(ctr.state.PID, 0) - if err == unix.ESRCH { - return nil - } - - return errors.Wrapf(err, "error sending SIGKILL to container %s", ctr.ID()) - } - - // Give runtime a few seconds to make it happen - if err := waitContainerStop(ctr, killContainerTimeout); err != nil { - return err - } - - return nil -} - // deleteContainer deletes a container from the OCI runtime func (r *OCIRuntime) deleteContainer(ctr *Container) error { runtimeDir, err := util.GetRootlessRuntimeDir() @@ -834,71 +452,6 @@ func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty return execCmd, nil } -// execStopContainer stops all active exec sessions in a container -// It will also stop all other processes in the container. It is only intended -// to be used to assist in cleanup when removing a container. -// SIGTERM is used by default to stop processes. If SIGTERM fails, SIGKILL will be used. -func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { - // Do we have active exec sessions? - if len(ctr.state.ExecSessions) == 0 { - return nil - } - - // Get a list of active exec sessions - execSessions := []int{} - for _, session := range ctr.state.ExecSessions { - pid := session.PID - // Ping the PID with signal 0 to see if it still exists - if err := unix.Kill(pid, 0); err == unix.ESRCH { - continue - } - - execSessions = append(execSessions, pid) - } - - // All the sessions may be dead - // If they are, just return - if len(execSessions) == 0 { - return nil - } - runtimeDir, err := util.GetRootlessRuntimeDir() - if err != nil { - return err - } - env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} - - // If timeout is 0, just use SIGKILL - if timeout > 0 { - // Stop using SIGTERM by default - // Use SIGSTOP after a timeout - logrus.Debugf("Killing all processes in container %s with SIGTERM", ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "kill", "--all", ctr.ID(), "TERM"); err != nil { - return errors.Wrapf(err, "error sending SIGTERM to container %s processes", ctr.ID()) - } - - // Wait for all processes to stop - if err := waitPidsStop(execSessions, time.Duration(timeout)*time.Second); err != nil { - logrus.Warnf("Timed out stopping container %s exec sessions", ctr.ID()) - } else { - // No error, all exec sessions are dead - return nil - } - } - - // Send SIGKILL - logrus.Debugf("Killing all processes in container %s with SIGKILL", ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "kill", "--all", ctr.ID(), "KILL"); err != nil { - return errors.Wrapf(err, "error sending SIGKILL to container %s processes", ctr.ID()) - } - - // Give the processes a few seconds to go down - if err := waitPidsStop(execSessions, killContainerTimeout); err != nil { - return errors.Wrapf(err, "failed to kill container %s exec sessions", ctr.ID()) - } - - return nil -} - // checkpointContainer checkpoints the given container func (r *OCIRuntime) checkpointContainer(ctr *Container, options ContainerCheckpointOptions) error { label.SetSocketLabel(ctr.ProcessLabel()) diff --git a/libpod/oci_linux.go b/libpod/oci_linux.go index 1f5411c1f..1c1e4a203 100644 --- a/libpod/oci_linux.go +++ b/libpod/oci_linux.go @@ -3,6 +3,8 @@ package libpod import ( + "bufio" + "bytes" "fmt" "os" "os/exec" @@ -10,12 +12,17 @@ import ( "runtime" "strings" "syscall" + "time" "github.com/containerd/cgroups" "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/util" "github.com/containers/libpod/utils" pmount "github.com/containers/storage/pkg/mount" + "github.com/coreos/go-systemd/activation" spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux" + "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" @@ -179,3 +186,443 @@ func (r *OCIRuntime) conmonPackage() string { } return dpkgVersion(r.conmonPath) } + +func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string, restoreOptions *ContainerCheckpointOptions) (err error) { + var stderrBuf bytes.Buffer + + runtimeDir, err := util.GetRootlessRuntimeDir() + if err != nil { + return err + } + + parentPipe, childPipe, err := newPipe() + if err != nil { + return errors.Wrapf(err, "error creating socket pair") + } + + childStartPipe, parentStartPipe, err := newPipe() + if err != nil { + return errors.Wrapf(err, "error creating socket pair for start pipe") + } + + defer parentPipe.Close() + defer parentStartPipe.Close() + + args := []string{} + if r.cgroupManager == SystemdCgroupsManager { + args = append(args, "-s") + } + args = append(args, "-c", ctr.ID()) + args = append(args, "-u", ctr.ID()) + args = append(args, "-r", r.path) + args = append(args, "-b", ctr.bundlePath()) + args = append(args, "-p", filepath.Join(ctr.state.RunDir, "pidfile")) + args = append(args, "-l", ctr.LogPath()) + args = append(args, "--exit-dir", r.exitsDir) + if ctr.config.ConmonPidFile != "" { + args = append(args, "--conmon-pidfile", ctr.config.ConmonPidFile) + } + if len(ctr.config.ExitCommand) > 0 { + args = append(args, "--exit-command", ctr.config.ExitCommand[0]) + for _, arg := range ctr.config.ExitCommand[1:] { + args = append(args, []string{"--exit-command-arg", arg}...) + } + } + args = append(args, "--socket-dir-path", r.socketsDir) + if ctr.config.Spec.Process.Terminal { + args = append(args, "-t") + } else if ctr.config.Stdin { + args = append(args, "-i") + } + if r.logSizeMax >= 0 { + args = append(args, "--log-size-max", fmt.Sprintf("%v", r.logSizeMax)) + } + if r.noPivot { + args = append(args, "--no-pivot") + } + + logLevel := logrus.GetLevel() + args = append(args, "--log-level", logLevel.String()) + + if logLevel == logrus.DebugLevel { + logrus.Debugf("%s messages will be logged to syslog", r.conmonPath) + args = append(args, "--syslog") + } + + if restoreOptions != nil { + args = append(args, "--restore", ctr.CheckpointPath()) + if restoreOptions.TCPEstablished { + args = append(args, "--restore-arg", "--tcp-established") + } + } + + logrus.WithFields(logrus.Fields{ + "args": args, + }).Debugf("running conmon: %s", r.conmonPath) + + cmd := exec.Command(r.conmonPath, args...) + cmd.Dir = ctr.bundlePath() + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + // TODO this is probably a really bad idea for some uses + // Make this configurable + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if ctr.config.Spec.Process.Terminal { + cmd.Stderr = &stderrBuf + } + + cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe, childStartPipe) + // 0, 1 and 2 are stdin, stdout and stderr + cmd.Env = append(r.conmonEnv, fmt.Sprintf("_OCI_SYNCPIPE=%d", 3)) + cmd.Env = append(cmd.Env, fmt.Sprintf("_OCI_STARTPIPE=%d", 4)) + cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)) + cmd.Env = append(cmd.Env, fmt.Sprintf("_CONTAINERS_USERNS_CONFIGURED=%s", os.Getenv("_CONTAINERS_USERNS_CONFIGURED"))) + cmd.Env = append(cmd.Env, fmt.Sprintf("_CONTAINERS_ROOTLESS_UID=%s", os.Getenv("_CONTAINERS_ROOTLESS_UID"))) + cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", os.Getenv("HOME"))) + + if r.reservePorts && !ctr.config.NetMode.IsSlirp4netns() { + ports, err := bindPorts(ctr.config.PortMappings) + if err != nil { + return err + } + + // Leak the port we bound in the conmon process. These fd's won't be used + // by the container and conmon will keep the ports busy so that another + // process cannot use them. + cmd.ExtraFiles = append(cmd.ExtraFiles, ports...) + } + + if ctr.config.NetMode.IsSlirp4netns() { + ctr.rootlessSlirpSyncR, ctr.rootlessSlirpSyncW, err = os.Pipe() + if err != nil { + return errors.Wrapf(err, "failed to create rootless network sync pipe") + } + // Leak one end in conmon, the other one will be leaked into slirp4netns + cmd.ExtraFiles = append(cmd.ExtraFiles, ctr.rootlessSlirpSyncW) + } + + if notify, ok := os.LookupEnv("NOTIFY_SOCKET"); ok { + cmd.Env = append(cmd.Env, fmt.Sprintf("NOTIFY_SOCKET=%s", notify)) + } + if listenfds, ok := os.LookupEnv("LISTEN_FDS"); ok { + cmd.Env = append(cmd.Env, fmt.Sprintf("LISTEN_FDS=%s", listenfds), "LISTEN_PID=1") + fds := activation.Files(false) + cmd.ExtraFiles = append(cmd.ExtraFiles, fds...) + } + if selinux.GetEnabled() { + // Set the label of the conmon process to be level :s0 + // This will allow the container processes to talk to fifo-files + // passed into the container by conmon + var ( + plabel string + con selinux.Context + ) + plabel, err = selinux.CurrentLabel() + if err != nil { + childPipe.Close() + return errors.Wrapf(err, "Failed to get current SELinux label") + } + + con, err = selinux.NewContext(plabel) + if err != nil { + return errors.Wrapf(err, "Failed to get new context from SELinux label") + } + + runtime.LockOSThread() + if con["level"] != "s0" && con["level"] != "" { + con["level"] = "s0" + if err = label.SetProcessLabel(con.Get()); err != nil { + runtime.UnlockOSThread() + return err + } + } + err = cmd.Start() + // Ignore error returned from SetProcessLabel("") call, + // can't recover. + label.SetProcessLabel("") + runtime.UnlockOSThread() + } else { + err = cmd.Start() + } + if err != nil { + childPipe.Close() + return err + } + defer cmd.Wait() + + // We don't need childPipe on the parent side + childPipe.Close() + childStartPipe.Close() + + // Move conmon to specified cgroup + if err := r.moveConmonToCgroup(ctr, cgroupParent, cmd); err != nil { + return err + } + + /* We set the cgroup, now the child can start creating children */ + someData := []byte{0} + _, err = parentStartPipe.Write(someData) + if err != nil { + return err + } + + /* Wait for initial setup and fork, and reap child */ + err = cmd.Wait() + if err != nil { + return err + } + + defer func() { + if err != nil { + if err2 := r.deleteContainer(ctr); err2 != nil { + logrus.Errorf("Error removing container %s from runtime after creation failed", ctr.ID()) + } + } + }() + + // Wait to get container pid from conmon + type syncStruct struct { + si *syncInfo + err error + } + ch := make(chan syncStruct) + go func() { + var si *syncInfo + rdr := bufio.NewReader(parentPipe) + b, err := rdr.ReadBytes('\n') + if err != nil { + ch <- syncStruct{err: err} + } + if err := json.Unmarshal(b, &si); err != nil { + ch <- syncStruct{err: err} + return + } + ch <- syncStruct{si: si} + }() + + select { + case ss := <-ch: + if ss.err != nil { + return errors.Wrapf(ss.err, "error reading container (probably exited) json message") + } + logrus.Debugf("Received container pid: %d", ss.si.Pid) + if ss.si.Pid == -1 { + if ss.si.Message != "" { + return errors.Wrapf(ErrInternal, "container create failed: %s", ss.si.Message) + } + return errors.Wrapf(ErrInternal, "container create failed") + } + ctr.state.PID = ss.si.Pid + case <-time.After(ContainerCreateTimeout): + return errors.Wrapf(ErrInternal, "container creation timeout") + } + return nil +} + +// Wait for a container which has been sent a signal to stop +func waitContainerStop(ctr *Container, timeout time.Duration) error { + done := make(chan struct{}) + chControl := make(chan struct{}) + go func() { + for { + select { + case <-chControl: + return + default: + // Check if the process is still around + err := unix.Kill(ctr.state.PID, 0) + if err == unix.ESRCH { + close(done) + return + } + time.Sleep(100 * time.Millisecond) + } + } + }() + select { + case <-done: + return nil + case <-time.After(timeout): + close(chControl) + logrus.Debugf("container %s did not die within timeout %d", ctr.ID(), timeout) + return errors.Errorf("container %s did not die within timeout", ctr.ID()) + } +} + +// Wait for a set of given PIDs to stop +func waitPidsStop(pids []int, timeout time.Duration) error { + done := make(chan struct{}) + chControl := make(chan struct{}) + go func() { + for { + select { + case <-chControl: + return + default: + allClosed := true + for _, pid := range pids { + if err := unix.Kill(pid, 0); err != unix.ESRCH { + allClosed = false + break + } + } + if allClosed { + close(done) + return + } + time.Sleep(100 * time.Millisecond) + } + } + }() + select { + case <-done: + return nil + case <-time.After(timeout): + close(chControl) + return errors.Errorf("given PIDs did not die within timeout") + } +} + +// stopContainer stops a container, first using its given stop signal (or +// SIGTERM if no signal was specified), then using SIGKILL +// Timeout is given in seconds. If timeout is 0, the container will be +// immediately kill with SIGKILL +// Does not set finished time for container, assumes you will run updateStatus +// after to pull the exit code +func (r *OCIRuntime) stopContainer(ctr *Container, timeout uint) error { + logrus.Debugf("Stopping container %s (PID %d)", ctr.ID(), ctr.state.PID) + + // Ping the container to see if it's alive + // If it's not, it's already stopped, return + err := unix.Kill(ctr.state.PID, 0) + if err == unix.ESRCH { + return nil + } + + stopSignal := ctr.config.StopSignal + if stopSignal == 0 { + stopSignal = uint(syscall.SIGTERM) + } + + if timeout > 0 { + if err := r.killContainer(ctr, stopSignal); err != nil { + // Is the container gone? + // If so, it probably died between the first check and + // our sending the signal + // The container is stopped, so exit cleanly + err := unix.Kill(ctr.state.PID, 0) + if err == unix.ESRCH { + return nil + } + + return err + } + + if err := waitContainerStop(ctr, time.Duration(timeout)*time.Second); err != nil { + logrus.Warnf("Timed out stopping container %s, resorting to SIGKILL", ctr.ID()) + } else { + // No error, the container is dead + return nil + } + } + + var args []string + if rootless.IsRootless() { + // we don't use --all for rootless containers as the OCI runtime might use + // the cgroups to determine the PIDs, but for rootless containers there is + // not any. + args = []string{"kill", ctr.ID(), "KILL"} + } else { + args = []string{"kill", "--all", ctr.ID(), "KILL"} + } + + runtimeDir, err := util.GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, args...); err != nil { + // Again, check if the container is gone. If it is, exit cleanly. + err := unix.Kill(ctr.state.PID, 0) + if err == unix.ESRCH { + return nil + } + + return errors.Wrapf(err, "error sending SIGKILL to container %s", ctr.ID()) + } + + // Give runtime a few seconds to make it happen + if err := waitContainerStop(ctr, killContainerTimeout); err != nil { + return err + } + + return nil +} + +// execStopContainer stops all active exec sessions in a container +// It will also stop all other processes in the container. It is only intended +// to be used to assist in cleanup when removing a container. +// SIGTERM is used by default to stop processes. If SIGTERM fails, SIGKILL will be used. +func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { + // Do we have active exec sessions? + if len(ctr.state.ExecSessions) == 0 { + return nil + } + + // Get a list of active exec sessions + execSessions := []int{} + for _, session := range ctr.state.ExecSessions { + pid := session.PID + // Ping the PID with signal 0 to see if it still exists + if err := unix.Kill(pid, 0); err == unix.ESRCH { + continue + } + + execSessions = append(execSessions, pid) + } + + // All the sessions may be dead + // If they are, just return + if len(execSessions) == 0 { + return nil + } + runtimeDir, err := util.GetRootlessRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + + // If timeout is 0, just use SIGKILL + if timeout > 0 { + // Stop using SIGTERM by default + // Use SIGSTOP after a timeout + logrus.Debugf("Killing all processes in container %s with SIGTERM", ctr.ID()) + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "kill", "--all", ctr.ID(), "TERM"); err != nil { + return errors.Wrapf(err, "error sending SIGTERM to container %s processes", ctr.ID()) + } + + // Wait for all processes to stop + if err := waitPidsStop(execSessions, time.Duration(timeout)*time.Second); err != nil { + logrus.Warnf("Timed out stopping container %s exec sessions", ctr.ID()) + } else { + // No error, all exec sessions are dead + return nil + } + } + + // Send SIGKILL + logrus.Debugf("Killing all processes in container %s with SIGKILL", ctr.ID()) + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "kill", "--all", ctr.ID(), "KILL"); err != nil { + return errors.Wrapf(err, "error sending SIGKILL to container %s processes", ctr.ID()) + } + + // Give the processes a few seconds to go down + if err := waitPidsStop(execSessions, killContainerTimeout); err != nil { + return errors.Wrapf(err, "failed to kill container %s exec sessions", ctr.ID()) + } + + return nil +} diff --git a/libpod/oci_unsupported.go b/libpod/oci_unsupported.go index 8c084d1e2..12183faf3 100644 --- a/libpod/oci_unsupported.go +++ b/libpod/oci_unsupported.go @@ -26,3 +26,15 @@ func (r *OCIRuntime) pathPackage() string { func (r *OCIRuntime) conmonPackage() string { return "" } + +func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string, restoreOptions *ContainerCheckpointOptions) (err error) { + return ErrOSNotSupported +} + +func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { + return ErrOSNotSupported +} + +func (r *OCIRuntime) stopContainer(ctr *Container, timeout uint) error { + return ErrOSNotSupported +} diff --git a/libpod/options.go b/libpod/options.go index dde9bc64c..86c04db09 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1,7 +1,6 @@ package libpod import ( - "context" "net" "os" "path/filepath" @@ -438,10 +437,9 @@ func WithRenumber() RuntimeOption { } } -// WithMigrate instructs libpod to perform a lock migrateing while -// initializing. This will handle migrations from early versions of libpod with -// file locks to newer versions with SHM locking, as well as changes in the -// number of configured locks. +// WithMigrate instructs libpod to migrate container configurations to account +// for changes between Libpod versions. All running containers will be stopped +// during a migration, then restarted after the migration is complete. func WithMigrate() RuntimeOption { return func(rt *Runtime) error { if rt.valid { @@ -468,19 +466,6 @@ func WithShmDir(dir string) CtrCreateOption { } } -// WithContext sets the context to use. -func WithContext(ctx context.Context) RuntimeOption { - return func(rt *Runtime) error { - if rt.valid { - return ErrRuntimeFinalized - } - - rt.ctx = ctx - - return nil - } -} - // WithSystemd turns on systemd mode in the container func WithSystemd() CtrCreateOption { return func(ctr *Container) error { diff --git a/libpod/runtime.go b/libpod/runtime.go index e85242028..6b8d97fd9 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -112,8 +112,6 @@ type Runtime struct { // mechanism to read and write even logs eventer events.Eventer - - ctx context.Context } // OCIRuntimePath contains information about an OCI runtime. @@ -353,8 +351,8 @@ func SetXdgRuntimeDir(val string) error { // NewRuntime creates a new container runtime // Options can be passed to override the default configuration for the runtime -func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) { - return newRuntimeFromConfig("", options...) +func NewRuntime(ctx context.Context, options ...RuntimeOption) (runtime *Runtime, err error) { + return newRuntimeFromConfig(ctx, "", options...) } // NewRuntimeFromConfig creates a new container runtime using the given @@ -362,14 +360,14 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) { // functions can be used to mutate this configuration further. // An error will be returned if the configuration file at the given path does // not exist or cannot be loaded -func NewRuntimeFromConfig(userConfigPath string, options ...RuntimeOption) (runtime *Runtime, err error) { +func NewRuntimeFromConfig(ctx context.Context, userConfigPath string, options ...RuntimeOption) (runtime *Runtime, err error) { if userConfigPath == "" { return nil, errors.New("invalid configuration file specified") } - return newRuntimeFromConfig(userConfigPath, options...) + return newRuntimeFromConfig(ctx, userConfigPath, options...) } -func newRuntimeFromConfig(userConfigPath string, options ...RuntimeOption) (runtime *Runtime, err error) { +func newRuntimeFromConfig(ctx context.Context, userConfigPath string, options ...RuntimeOption) (runtime *Runtime, err error) { runtime = new(Runtime) runtime.config = new(RuntimeConfig) runtime.configuredFrom = new(runtimeConfiguredFrom) @@ -563,7 +561,7 @@ func newRuntimeFromConfig(userConfigPath string, options ...RuntimeOption) (runt } } } - if err := makeRuntime(runtime); err != nil { + if err := makeRuntime(ctx, runtime); err != nil { return nil, err } return runtime, nil @@ -571,7 +569,7 @@ func newRuntimeFromConfig(userConfigPath string, options ...RuntimeOption) (runt // Make a new runtime based on the given configuration // Sets up containers/storage, state store, OCI runtime -func makeRuntime(runtime *Runtime) (err error) { +func makeRuntime(ctx context.Context, runtime *Runtime) (err error) { // Backward compatibility for `runtime_path` if runtime.config.RuntimePath != nil { // Don't print twice in rootless mode. @@ -980,7 +978,7 @@ func makeRuntime(runtime *Runtime) (err error) { os.Exit(ret) } } - if err := runtime.migrate(); err != nil { + if err := runtime.migrate(ctx); err != nil { return err } } diff --git a/libpod/runtime_migrate.go b/libpod/runtime_migrate.go index a084df289..0bb8e952f 100644 --- a/libpod/runtime_migrate.go +++ b/libpod/runtime_migrate.go @@ -1,13 +1,14 @@ package libpod import ( + "context" "path/filepath" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -func (r *Runtime) migrate() error { +func (r *Runtime) migrate(ctx context.Context) error { runningContainers, err := r.GetRunningContainers() if err != nil { return err @@ -38,7 +39,7 @@ func (r *Runtime) migrate() error { } for _, ctr := range runningContainers { - if err := ctr.Start(r.ctx, true); err != nil { + if err := ctr.Start(ctx, true); err != nil { logrus.Errorf("error restarting container %s", ctr.ID()) } } diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index 9ec897a60..d575bc9b0 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -18,6 +18,7 @@ import ( "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/adapter/shortcuts" + "github.com/containers/libpod/pkg/systemdgen" "github.com/containers/storage" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -133,6 +134,43 @@ func (r *LocalRuntime) KillContainers(ctx context.Context, cli *cliconfig.KillVa return pool.Run() } +// InitContainers initializes container(s) based on CLI inputs. +// Returns list of successful id(s), map of failed id(s) to errors, or a general +// error not from the container. +func (r *LocalRuntime) InitContainers(ctx context.Context, cli *cliconfig.InitValues) ([]string, map[string]error, error) { + maxWorkers := shared.DefaultPoolSize("init") + if cli.GlobalIsSet("max-workers") { + maxWorkers = cli.GlobalFlags.MaxWorks + } + logrus.Debugf("Setting maximum init workers to %d", maxWorkers) + + ctrs, err := shortcuts.GetContainersByContext(cli.All, cli.Latest, cli.InputArgs, r.Runtime) + if err != nil { + return nil, nil, err + } + + pool := shared.NewPool("init", maxWorkers, len(ctrs)) + for _, c := range ctrs { + ctr := c + + pool.Add(shared.Job{ + ctr.ID(), + func() error { + err := ctr.Init(ctx) + if err != nil { + // If we're initializing all containers, ignore invalid state errors + if cli.All && errors.Cause(err) == libpod.ErrCtrStateInvalid { + return nil + } + return err + } + return nil + }, + }) + } + return pool.Run() +} + // RemoveContainers removes container(s) based on CLI inputs. func (r *LocalRuntime) RemoveContainers(ctx context.Context, cli *cliconfig.RmValues) ([]string, map[string]error, error) { var ( @@ -876,3 +914,47 @@ func cleanupContainer(ctx context.Context, ctr *libpod.Container, runtime *Local } return nil } + +// Port displays port information about existing containers +func (r *LocalRuntime) Port(c *cliconfig.PortValues) ([]*Container, error) { + var ( + portContainers []*Container + containers []*libpod.Container + err error + ) + + if !c.All { + containers, err = shortcuts.GetContainersByContext(false, c.Latest, c.InputArgs, r.Runtime) + } else { + containers, err = r.Runtime.GetRunningContainers() + } + if err != nil { + return nil, err + } + + //Convert libpod containers to adapter Containers + for _, con := range containers { + if state, _ := con.State(); state != libpod.ContainerStateRunning { + continue + } + portContainers = append(portContainers, &Container{con}) + } + return portContainers, nil +} + +// GenerateSystemd creates a unit file for a container +func (r *LocalRuntime) GenerateSystemd(c *cliconfig.GenerateSystemdValues) (string, error) { + ctr, err := r.Runtime.LookupContainer(c.InputArgs[0]) + if err != nil { + return "", err + } + timeout := int(ctr.StopTimeout()) + if c.StopTimeout >= 0 { + timeout = int(c.StopTimeout) + } + name := ctr.ID() + if c.Name { + name = ctr.Name() + } + return systemdgen.CreateSystemdUnitAsString(name, ctr.ID(), c.RestartPolicy, ctr.Config().StaticDir, timeout) +} diff --git a/pkg/adapter/containers_remote.go b/pkg/adapter/containers_remote.go index a3a48a564..201249fc3 100644 --- a/pkg/adapter/containers_remote.go +++ b/pkg/adapter/containers_remote.go @@ -18,6 +18,7 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/inspect" "github.com/containers/libpod/pkg/varlinkapi/virtwriter" + "github.com/cri-o/ocicni/pkg/ocicni" "github.com/docker/docker/pkg/term" "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -63,6 +64,19 @@ func (c *Container) Unpause() error { return err } +func (c *Container) PortMappings() ([]ocicni.PortMapping, error) { + // First check if the container belongs to a network namespace (like a pod) + // Taken from libpod portmappings() + if len(c.config.NetNsCtr) > 0 { + netNsCtr, err := c.Runtime.LookupContainer(c.config.NetNsCtr) + if err != nil { + return nil, errors.Wrapf(err, "unable to lookup network namespace for container %s", c.ID()) + } + return netNsCtr.PortMappings() + } + return c.config.PortMappings, nil +} + // Config returns a container config func (r *LocalRuntime) Config(name string) *libpod.ContainerConfig { // TODO the Spec being returned is not populated. Matt and I could not figure out why. Will defer @@ -234,6 +248,40 @@ func (r *LocalRuntime) StopContainers(ctx context.Context, cli *cliconfig.StopVa return ok, failures, nil } +// InitContainers initializes container(s) based on Varlink. +// It returns a list of successful ID(s), a map of failed container ID to error, +// or an error if a more general error occurred. +func (r *LocalRuntime) InitContainers(ctx context.Context, cli *cliconfig.InitValues) ([]string, map[string]error, error) { + var ( + ok = []string{} + failures = map[string]error{} + ) + + ids, err := iopodman.GetContainersByContext().Call(r.Conn, cli.All, cli.Latest, cli.InputArgs) + if err != nil { + return nil, nil, err + } + + for _, id := range ids { + initialized, err := iopodman.InitContainer().Call(r.Conn, id) + if err != nil { + if cli.All { + switch err.(type) { + case *iopodman.InvalidState: + ok = append(ok, initialized) + default: + failures[id] = err + } + } else { + failures[id] = err + } + } else { + ok = append(ok, initialized) + } + } + return ok, failures, nil +} + // KillContainers sends signal to container(s) based on varlink. // Returns list of successful id(s), map of failed id(s) + error, or error not from container func (r *LocalRuntime) KillContainers(ctx context.Context, cli *cliconfig.KillValues, signal syscall.Signal) ([]string, map[string]error, error) { @@ -888,3 +936,28 @@ func (r *LocalRuntime) Prune(ctx context.Context, maxWorkers int, force bool) ([ func (r *LocalRuntime) CleanupContainers(ctx context.Context, cli *cliconfig.CleanupValues) ([]string, map[string]error, error) { return nil, nil, errors.New("container cleanup not supported for remote clients") } + +// Port displays port information about existing containers +func (r *LocalRuntime) Port(c *cliconfig.PortValues) ([]*Container, error) { + var ( + containers []*Container + err error + ) + // This one is a bit odd because when all is used, we only use running containers. + if !c.All { + containers, err = r.GetContainersByContext(false, c.Latest, c.InputArgs) + } else { + // we need to only use running containers if all + filters := []string{libpod.ContainerStateRunning.String()} + containers, err = r.LookupContainersWithStatus(filters) + } + if err != nil { + return nil, err + } + return containers, nil +} + +// GenerateSystemd creates a systemd until for a container +func (r *LocalRuntime) GenerateSystemd(c *cliconfig.GenerateSystemdValues) (string, error) { + return iopodman.GenerateSystemd().Call(r.Conn, c.InputArgs[0], c.RestartPolicy, int64(c.StopTimeout), c.Name) +} diff --git a/pkg/adapter/runtime_remote.go b/pkg/adapter/runtime_remote.go index 6102daccf..4986d16f7 100644 --- a/pkg/adapter/runtime_remote.go +++ b/pkg/adapter/runtime_remote.go @@ -889,3 +889,20 @@ func (r *LocalRuntime) GenerateKube(c *cliconfig.GenerateKubeValues) (*v1.Pod, * err = json.Unmarshal([]byte(reply.Service), &service) return &pod, &service, err } + +// GetContainersByContext looks up containers based on the cli input of all, latest, or a list +func (r *LocalRuntime) GetContainersByContext(all bool, latest bool, namesOrIDs []string) ([]*Container, error) { + var containers []*Container + cids, err := iopodman.GetContainersByContext().Call(r.Conn, all, latest, namesOrIDs) + if err != nil { + return nil, err + } + for _, cid := range cids { + ctr, err := r.LookupContainer(cid) + if err != nil { + return nil, err + } + containers = append(containers, ctr) + } + return containers, nil +} diff --git a/pkg/adapter/runtime_remote_supported.go b/pkg/adapter/runtime_remote_supported.go new file mode 100644 index 000000000..b8e8da308 --- /dev/null +++ b/pkg/adapter/runtime_remote_supported.go @@ -0,0 +1 @@ +package adapter diff --git a/pkg/adapter/sigproxy.go b/pkg/adapter/sigproxy_linux.go index af968cb89..af968cb89 100644 --- a/pkg/adapter/sigproxy.go +++ b/pkg/adapter/sigproxy_linux.go diff --git a/pkg/adapter/terminal.go b/pkg/adapter/terminal.go index 0b608decf..373c78322 100644 --- a/pkg/adapter/terminal.go +++ b/pkg/adapter/terminal.go @@ -2,16 +2,12 @@ package adapter import ( "context" - "fmt" "os" gosignal "os/signal" - "github.com/containers/libpod/libpod" "github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/term" - "github.com/pkg/errors" "github.com/sirupsen/logrus" - "golang.org/x/crypto/ssh/terminal" "k8s.io/client-go/tools/remotecommand" ) @@ -19,83 +15,6 @@ import ( type RawTtyFormatter struct { } -// StartAttachCtr starts and (if required) attaches to a container -func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr, stdin *os.File, detachKeys string, sigProxy bool, startContainer bool, recursive bool) error { - resize := make(chan remotecommand.TerminalSize) - - haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd())) - - // Check if we are attached to a terminal. If we are, generate resize - // events, and set the terminal to raw mode - if haveTerminal && ctr.Spec().Process.Terminal { - logrus.Debugf("Handling terminal attach") - - subCtx, cancel := context.WithCancel(ctx) - defer cancel() - - resizeTty(subCtx, resize) - - oldTermState, err := term.SaveState(os.Stdin.Fd()) - if err != nil { - return errors.Wrapf(err, "unable to save terminal state") - } - - logrus.SetFormatter(&RawTtyFormatter{}) - term.SetRawTerminal(os.Stdin.Fd()) - - defer restoreTerminal(oldTermState) - } - - streams := new(libpod.AttachStreams) - streams.OutputStream = stdout - streams.ErrorStream = stderr - streams.InputStream = stdin - streams.AttachOutput = true - streams.AttachError = true - streams.AttachInput = true - - if stdout == nil { - logrus.Debugf("Not attaching to stdout") - streams.AttachOutput = false - } - if stderr == nil { - logrus.Debugf("Not attaching to stderr") - streams.AttachError = false - } - if stdin == nil { - logrus.Debugf("Not attaching to stdin") - streams.AttachInput = false - } - - if !startContainer { - if sigProxy { - ProxySignals(ctr) - } - - return ctr.Attach(streams, detachKeys, resize) - } - - attachChan, err := ctr.StartAndAttach(ctx, streams, detachKeys, resize, recursive) - if err != nil { - return err - } - - if sigProxy { - ProxySignals(ctr) - } - - if stdout == nil && stderr == nil { - fmt.Printf("%s\n", ctr.ID()) - } - - err = <-attachChan - if err != nil { - return errors.Wrapf(err, "error attaching to container %s", ctr.ID()) - } - - return nil -} - // getResize returns a TerminalSize command matching stdin's current // size on success, and nil on errors. func getResize() *remotecommand.TerminalSize { diff --git a/pkg/adapter/terminal_linux.go b/pkg/adapter/terminal_linux.go new file mode 100644 index 000000000..3c4c3bd38 --- /dev/null +++ b/pkg/adapter/terminal_linux.go @@ -0,0 +1,91 @@ +package adapter + +import ( + "context" + "fmt" + "os" + + "github.com/containers/libpod/libpod" + "github.com/docker/docker/pkg/term" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh/terminal" + "k8s.io/client-go/tools/remotecommand" +) + +// StartAttachCtr starts and (if required) attaches to a container +func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr, stdin *os.File, detachKeys string, sigProxy bool, startContainer bool, recursive bool) error { + resize := make(chan remotecommand.TerminalSize) + + haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd())) + + // Check if we are attached to a terminal. If we are, generate resize + // events, and set the terminal to raw mode + if haveTerminal && ctr.Spec().Process.Terminal { + logrus.Debugf("Handling terminal attach") + + subCtx, cancel := context.WithCancel(ctx) + defer cancel() + + resizeTty(subCtx, resize) + + oldTermState, err := term.SaveState(os.Stdin.Fd()) + if err != nil { + return errors.Wrapf(err, "unable to save terminal state") + } + + logrus.SetFormatter(&RawTtyFormatter{}) + term.SetRawTerminal(os.Stdin.Fd()) + + defer restoreTerminal(oldTermState) + } + + streams := new(libpod.AttachStreams) + streams.OutputStream = stdout + streams.ErrorStream = stderr + streams.InputStream = stdin + streams.AttachOutput = true + streams.AttachError = true + streams.AttachInput = true + + if stdout == nil { + logrus.Debugf("Not attaching to stdout") + streams.AttachOutput = false + } + if stderr == nil { + logrus.Debugf("Not attaching to stderr") + streams.AttachError = false + } + if stdin == nil { + logrus.Debugf("Not attaching to stdin") + streams.AttachInput = false + } + + if !startContainer { + if sigProxy { + ProxySignals(ctr) + } + + return ctr.Attach(streams, detachKeys, resize) + } + + attachChan, err := ctr.StartAndAttach(ctx, streams, detachKeys, resize, recursive) + if err != nil { + return err + } + + if sigProxy { + ProxySignals(ctr) + } + + if stdout == nil && stderr == nil { + fmt.Printf("%s\n", ctr.ID()) + } + + err = <-attachChan + if err != nil { + return errors.Wrapf(err, "error attaching to container %s", ctr.ID()) + } + + return nil +} diff --git a/pkg/spec/config_linux.go b/pkg/spec/config_linux.go index a1873086e..eb2acf984 100644 --- a/pkg/spec/config_linux.go +++ b/pkg/spec/config_linux.go @@ -244,3 +244,9 @@ func makeThrottleArray(throttleInput []string, rateType int) ([]spec.LinuxThrott } return ltds, nil } + +func getStatFromPath(path string) (unix.Stat_t, error) { + s := unix.Stat_t{} + err := unix.Stat(path, &s) + return s, err +} diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 4d2d8304c..90e7accf3 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -17,7 +17,6 @@ import ( "github.com/opencontainers/runtime-tools/generate" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" ) // Type constants @@ -411,9 +410,3 @@ func NatToOCIPortBindings(ports nat.PortMap) ([]ocicni.PortMapping, error) { func (c *CreateConfig) AddPrivilegedDevices(g *generate.Generator) error { return c.addPrivilegedDevices(g) } - -func getStatFromPath(path string) (unix.Stat_t, error) { - s := unix.Stat_t{} - err := unix.Stat(path, &s) - return s, err -} diff --git a/pkg/systemdgen/systemdgen.go b/pkg/systemdgen/systemdgen.go new file mode 100644 index 000000000..3d1c31b5d --- /dev/null +++ b/pkg/systemdgen/systemdgen.go @@ -0,0 +1,43 @@ +package systemdgen + +import ( + "fmt" + "path/filepath" + + "github.com/pkg/errors" +) + +var template = `[Unit] +Description=%s Podman Container +[Service] +Restart=%s +ExecStart=/usr/bin/podman start %s +ExecStop=/usr/bin/podman stop -t %d %s +KillMode=none +Type=forking +PIDFile=%s +[Install] +WantedBy=multi-user.target` + +var restartPolicies = []string{"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always"} + +// ValidateRestartPolicy checks that the user-provided policy is valid +func ValidateRestartPolicy(restart string) error { + for _, i := range restartPolicies { + if i == restart { + return nil + } + } + return errors.Errorf("%s is not a valid restart policy", restart) +} + +// CreateSystemdUnitAsString takes variables to create a systemd unit file used to control +// a libpod container +func CreateSystemdUnitAsString(name, cid, restart, pidPath string, stopTimeout int) (string, error) { + if err := ValidateRestartPolicy(restart); err != nil { + return "", err + } + pidFile := filepath.Join(pidPath, fmt.Sprintf("%s.pid", cid)) + unit := fmt.Sprintf(template, name, restart, name, stopTimeout, name, pidFile) + return unit, nil +} diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 136f8fadd..14b0c2b55 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -6,12 +6,10 @@ import ( "path/filepath" "strings" "sync" - "syscall" "time" "github.com/BurntSushi/toml" "github.com/containers/image/types" - "github.com/containers/libpod/pkg/rootless" "github.com/containers/storage" "github.com/containers/storage/pkg/idtools" "github.com/opencontainers/image-spec/specs-go/v1" @@ -187,76 +185,6 @@ var ( rootlessRuntimeDir string ) -// GetRootlessRuntimeDir returns the runtime directory when running as non root -func GetRootlessRuntimeDir() (string, error) { - var rootlessRuntimeDirError error - - rootlessRuntimeDirOnce.Do(func() { - runtimeDir := os.Getenv("XDG_RUNTIME_DIR") - uid := fmt.Sprintf("%d", rootless.GetRootlessUID()) - if runtimeDir == "" { - tmpDir := filepath.Join("/run", "user", uid) - os.MkdirAll(tmpDir, 0700) - st, err := os.Stat(tmpDir) - if err == nil && int(st.Sys().(*syscall.Stat_t).Uid) == os.Geteuid() && st.Mode().Perm() == 0700 { - runtimeDir = tmpDir - } - } - if runtimeDir == "" { - tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("run-%s", uid)) - os.MkdirAll(tmpDir, 0700) - st, err := os.Stat(tmpDir) - if err == nil && int(st.Sys().(*syscall.Stat_t).Uid) == os.Geteuid() && st.Mode().Perm() == 0700 { - runtimeDir = tmpDir - } - } - if runtimeDir == "" { - home := os.Getenv("HOME") - if home == "" { - rootlessRuntimeDirError = fmt.Errorf("neither XDG_RUNTIME_DIR nor HOME was set non-empty") - return - } - resolvedHome, err := filepath.EvalSymlinks(home) - if err != nil { - rootlessRuntimeDirError = errors.Wrapf(err, "cannot resolve %s", home) - return - } - runtimeDir = filepath.Join(resolvedHome, "rundir") - } - rootlessRuntimeDir = runtimeDir - }) - - if rootlessRuntimeDirError != nil { - return "", rootlessRuntimeDirError - } - return rootlessRuntimeDir, nil -} - -// GetRootlessDirInfo returns the parent path of where the storage for containers and -// volumes will be in rootless mode -func GetRootlessDirInfo() (string, string, error) { - rootlessRuntime, err := GetRootlessRuntimeDir() - if err != nil { - return "", "", err - } - - dataDir := os.Getenv("XDG_DATA_HOME") - if dataDir == "" { - home := os.Getenv("HOME") - if home == "" { - return "", "", fmt.Errorf("neither XDG_DATA_HOME nor HOME was set non-empty") - } - // runc doesn't like symlinks in the rootfs path, and at least - // on CoreOS /home is a symlink to /var/home, so resolve any symlink. - resolvedHome, err := filepath.EvalSymlinks(home) - if err != nil { - return "", "", errors.Wrapf(err, "cannot resolve %s", home) - } - dataDir = filepath.Join(resolvedHome, ".local", "share") - } - return dataDir, rootlessRuntime, nil -} - type tomlOptionsConfig struct { MountProgram string `toml:"mount_program"` } diff --git a/pkg/util/utils_supported.go b/pkg/util/utils_supported.go new file mode 100644 index 000000000..af5e67fc1 --- /dev/null +++ b/pkg/util/utils_supported.go @@ -0,0 +1,60 @@ +// +build linux darwin + +package util + +// TODO once rootless function is consolidated under libpod, we +// should work to take darwin from this + +import ( + "fmt" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "os" + "path/filepath" + "syscall" +) + +// GetRootlessRuntimeDir returns the runtime directory when running as non root +func GetRootlessRuntimeDir() (string, error) { + var rootlessRuntimeDirError error + + rootlessRuntimeDirOnce.Do(func() { + runtimeDir := os.Getenv("XDG_RUNTIME_DIR") + uid := fmt.Sprintf("%d", rootless.GetRootlessUID()) + if runtimeDir == "" { + tmpDir := filepath.Join("/run", "user", uid) + os.MkdirAll(tmpDir, 0700) + st, err := os.Stat(tmpDir) + if err == nil && int(st.Sys().(*syscall.Stat_t).Uid) == os.Geteuid() && st.Mode().Perm() == 0700 { + runtimeDir = tmpDir + } + } + if runtimeDir == "" { + tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("run-%s", uid)) + os.MkdirAll(tmpDir, 0700) + st, err := os.Stat(tmpDir) + if err == nil && int(st.Sys().(*syscall.Stat_t).Uid) == os.Geteuid() && st.Mode().Perm() == 0700 { + runtimeDir = tmpDir + } + } + if runtimeDir == "" { + home := os.Getenv("HOME") + if home == "" { + rootlessRuntimeDirError = fmt.Errorf("neither XDG_RUNTIME_DIR nor HOME was set non-empty") + return + } + resolvedHome, err := filepath.EvalSymlinks(home) + if err != nil { + rootlessRuntimeDirError = errors.Wrapf(err, "cannot resolve %s", home) + return + } + runtimeDir = filepath.Join(resolvedHome, "rundir") + } + rootlessRuntimeDir = runtimeDir + }) + + if rootlessRuntimeDirError != nil { + return "", rootlessRuntimeDirError + } + return rootlessRuntimeDir, nil +} diff --git a/pkg/util/utils_windows.go b/pkg/util/utils_windows.go new file mode 100644 index 000000000..1e9ccea90 --- /dev/null +++ b/pkg/util/utils_windows.go @@ -0,0 +1,12 @@ +// +build windows + +package util + +import ( + "github.com/pkg/errors" +) + +// GetRootlessRuntimeDir returns the runtime directory when running as non root +func GetRootlessRuntimeDir() (string, error) { + return "", errors.New("this function is not implemented for windows") +} diff --git a/pkg/varlinkapi/containers.go b/pkg/varlinkapi/containers.go index 872c7bc26..c8be41636 100644 --- a/pkg/varlinkapi/containers.go +++ b/pkg/varlinkapi/containers.go @@ -365,6 +365,21 @@ func (i *LibpodAPI) StartContainer(call iopodman.VarlinkCall, name string) error return call.ReplyStartContainer(ctr.ID()) } +// InitContainer initializes the container given by Varlink. +func (i *LibpodAPI) InitContainer(call iopodman.VarlinkCall, name string) error { + ctr, err := i.Runtime.LookupContainer(name) + if err != nil { + return call.ReplyContainerNotFound(name, err.Error()) + } + if err := ctr.Init(getContext()); err != nil { + if errors.Cause(err) == libpod.ErrCtrStateInvalid { + return call.ReplyInvalidState(ctr.ID(), err.Error()) + } + return call.ReplyErrorOccurred(err.Error()) + } + return call.ReplyInitContainer(ctr.ID()) +} + // StopContainer ... func (i *LibpodAPI) StopContainer(call iopodman.VarlinkCall, name string, timeout int64) error { ctr, err := i.Runtime.LookupContainer(name) diff --git a/pkg/varlinkapi/generate.go b/pkg/varlinkapi/generate.go index bc600c397..9dc20d582 100644 --- a/pkg/varlinkapi/generate.go +++ b/pkg/varlinkapi/generate.go @@ -6,6 +6,7 @@ import ( "encoding/json" "github.com/containers/libpod/cmd/podman/shared" iopodman "github.com/containers/libpod/cmd/podman/varlink" + "github.com/containers/libpod/pkg/systemdgen" ) // GenerateKube ... @@ -28,3 +29,24 @@ func (i *LibpodAPI) GenerateKube(call iopodman.VarlinkCall, name string, service Service: string(servB), }) } + +// GenerateSystemd ... +func (i *LibpodAPI) GenerateSystemd(call iopodman.VarlinkCall, nameOrID, restart string, stopTimeout int64, useName bool) error { + ctr, err := i.Runtime.LookupContainer(nameOrID) + if err != nil { + return call.ReplyErrorOccurred(err.Error()) + } + timeout := int(ctr.StopTimeout()) + if stopTimeout >= 0 { + timeout = int(stopTimeout) + } + name := ctr.ID() + if useName { + name = ctr.Name() + } + unit, err := systemdgen.CreateSystemdUnitAsString(name, ctr.ID(), restart, ctr.Config().StaticDir, timeout) + if err != nil { + return call.ReplyErrorOccurred(err.Error()) + } + return call.ReplyGenerateSystemd(unit) +} diff --git a/test/e2e/generate_systemd_test.go b/test/e2e/generate_systemd_test.go new file mode 100644 index 000000000..940e894bc --- /dev/null +++ b/test/e2e/generate_systemd_test.go @@ -0,0 +1,74 @@ +// +build !remoteclient + +package integration + +import ( + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "os" +) + +var _ = Describe("Podman generate systemd", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.Setup() + podmanTest.RestoreAllArtifacts() + }) + + AfterEach(func() { + podmanTest.Cleanup() + f := CurrentGinkgoTestDescription() + processTestResult(f) + + }) + + It("podman generate systemd on bogus container", func() { + session := podmanTest.Podman([]string{"generate", "systemd", "foobar"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + + It("podman generate systemd bad restart policy", func() { + session := podmanTest.Podman([]string{"generate", "systemd", "--restart-policy", "never", "foobar"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + + It("podman generate systemd bad timeout value", func() { + session := podmanTest.Podman([]string{"generate", "systemd", "--timeout", "-1", "foobar"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + + It("podman generate systemd", func() { + n := podmanTest.Podman([]string{"run", "--name", "nginx", "-dt", nginx}) + n.WaitWithDefaultTimeout() + Expect(n.ExitCode()).To(Equal(0)) + + session := podmanTest.Podman([]string{"generate", "systemd", "nginx"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) + + It("podman generate systemd with timeout", func() { + n := podmanTest.Podman([]string{"run", "--name", "nginx", "-dt", nginx}) + n.WaitWithDefaultTimeout() + Expect(n.ExitCode()).To(Equal(0)) + + session := podmanTest.Podman([]string{"generate", "systemd", "--timeout", "5", "nginx"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) + +}) diff --git a/test/e2e/init_test.go b/test/e2e/init_test.go new file mode 100644 index 000000000..5865930a5 --- /dev/null +++ b/test/e2e/init_test.go @@ -0,0 +1,129 @@ +package integration + +import ( + "os" + + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman init", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.Setup() + podmanTest.RestoreAllArtifacts() + }) + + AfterEach(func() { + podmanTest.Cleanup() + f := CurrentGinkgoTestDescription() + processTestResult(f) + + }) + + It("podman init bogus container", func() { + session := podmanTest.Podman([]string{"start", "123456"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(125)) + }) + + It("podman init with no arguments", func() { + session := podmanTest.Podman([]string{"start"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(125)) + }) + + It("podman init single container by ID", func() { + session := podmanTest.Podman([]string{"create", "-d", ALPINE, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + cid := session.OutputToString() + init := podmanTest.Podman([]string{"init", cid}) + init.WaitWithDefaultTimeout() + Expect(init.ExitCode()).To(Equal(0)) + result := podmanTest.Podman([]string{"inspect", cid}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + conData := result.InspectContainerToJSON() + Expect(conData[0].State.Status).To(Equal("created")) + }) + + It("podman init single container by name", func() { + name := "test1" + session := podmanTest.Podman([]string{"create", "--name", name, "-d", ALPINE, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + init := podmanTest.Podman([]string{"init", name}) + init.WaitWithDefaultTimeout() + Expect(init.ExitCode()).To(Equal(0)) + result := podmanTest.Podman([]string{"inspect", name}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + conData := result.InspectContainerToJSON() + Expect(conData[0].State.Status).To(Equal("created")) + }) + + It("podman init latest container", func() { + session := podmanTest.Podman([]string{"create", "-d", ALPINE, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + init := podmanTest.Podman([]string{"init", "--latest"}) + init.WaitWithDefaultTimeout() + Expect(init.ExitCode()).To(Equal(0)) + result := podmanTest.Podman([]string{"inspect", "--latest"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + conData := result.InspectContainerToJSON() + Expect(conData[0].State.Status).To(Equal("created")) + }) + + It("podman init all three containers, one running", func() { + session := podmanTest.Podman([]string{"create", "--name", "test1", "-d", ALPINE, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session2 := podmanTest.Podman([]string{"create", "--name", "test2", "-d", ALPINE, "ls"}) + session2.WaitWithDefaultTimeout() + Expect(session2.ExitCode()).To(Equal(0)) + session3 := podmanTest.Podman([]string{"run", "--name", "test3", "-d", ALPINE, "top"}) + session3.WaitWithDefaultTimeout() + Expect(session3.ExitCode()).To(Equal(0)) + init := podmanTest.Podman([]string{"init", "--all"}) + init.WaitWithDefaultTimeout() + Expect(init.ExitCode()).To(Equal(0)) + result := podmanTest.Podman([]string{"inspect", "test1"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + conData := result.InspectContainerToJSON() + Expect(conData[0].State.Status).To(Equal("created")) + result2 := podmanTest.Podman([]string{"inspect", "test2"}) + result2.WaitWithDefaultTimeout() + Expect(result2.ExitCode()).To(Equal(0)) + conData2 := result2.InspectContainerToJSON() + Expect(conData2[0].State.Status).To(Equal("created")) + result3 := podmanTest.Podman([]string{"inspect", "test3"}) + result3.WaitWithDefaultTimeout() + Expect(result3.ExitCode()).To(Equal(0)) + conData3 := result3.InspectContainerToJSON() + Expect(conData3[0].State.Status).To(Equal("running")) + }) + + It("podman init running container errors", func() { + session := podmanTest.Podman([]string{"run", "-d", ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + init := podmanTest.Podman([]string{"init", "--latest"}) + init.WaitWithDefaultTimeout() + Expect(init.ExitCode()).To(Equal(125)) + }) +}) diff --git a/utils/utils.go b/utils/utils.go index c195daa5d..86adfb967 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -9,8 +9,6 @@ import ( "strings" "github.com/containers/storage/pkg/archive" - systemdDbus "github.com/coreos/go-systemd/dbus" - "github.com/godbus/dbus" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -55,37 +53,6 @@ func StatusToExitCode(status int) int { return ((status) & 0xff00) >> 8 } -// RunUnderSystemdScope adds the specified pid to a systemd scope -func RunUnderSystemdScope(pid int, slice string, unitName string) error { - var properties []systemdDbus.Property - conn, err := systemdDbus.New() - if err != nil { - return err - } - properties = append(properties, systemdDbus.PropSlice(slice)) - properties = append(properties, newProp("PIDs", []uint32{uint32(pid)})) - properties = append(properties, newProp("Delegate", true)) - properties = append(properties, newProp("DefaultDependencies", false)) - ch := make(chan string) - _, err = conn.StartTransientUnit(unitName, "replace", properties, ch) - if err != nil { - return err - } - defer conn.Close() - - // Block until job is started - <-ch - - return nil -} - -func newProp(name string, units interface{}) systemdDbus.Property { - return systemdDbus.Property{ - Name: name, - Value: dbus.MakeVariant(units), - } -} - // ErrDetach is an error indicating that the user manually detached from the // container. var ErrDetach = errors.New("detached from container") diff --git a/utils/utils_supported.go b/utils/utils_supported.go new file mode 100644 index 000000000..8b0ba4438 --- /dev/null +++ b/utils/utils_supported.go @@ -0,0 +1,39 @@ +// +build linux darwin + +package utils + +import ( + systemdDbus "github.com/coreos/go-systemd/dbus" + "github.com/godbus/dbus" +) + +// RunUnderSystemdScope adds the specified pid to a systemd scope +func RunUnderSystemdScope(pid int, slice string, unitName string) error { + var properties []systemdDbus.Property + conn, err := systemdDbus.New() + if err != nil { + return err + } + properties = append(properties, systemdDbus.PropSlice(slice)) + properties = append(properties, newProp("PIDs", []uint32{uint32(pid)})) + properties = append(properties, newProp("Delegate", true)) + properties = append(properties, newProp("DefaultDependencies", false)) + ch := make(chan string) + _, err = conn.StartTransientUnit(unitName, "replace", properties, ch) + if err != nil { + return err + } + defer conn.Close() + + // Block until job is started + <-ch + + return nil +} + +func newProp(name string, units interface{}) systemdDbus.Property { + return systemdDbus.Property{ + Name: name, + Value: dbus.MakeVariant(units), + } +} diff --git a/utils/utils_windows.go b/utils/utils_windows.go new file mode 100644 index 000000000..db27877d9 --- /dev/null +++ b/utils/utils_windows.go @@ -0,0 +1,9 @@ +// +build windows + +package utils + +import "github.com/pkg/errors" + +func RunUnderSystemdScope(pid int, slice string, unitName string) error { + return errors.New("not implemented for windows") +} diff --git a/vendor.conf b/vendor.conf index 02283beb9..c99b2c1d7 100644 --- a/vendor.conf +++ b/vendor.conf @@ -19,7 +19,7 @@ github.com/containers/image v1.5.1 github.com/vbauerster/mpb v3.3.4 github.com/mattn/go-isatty v0.0.4 github.com/VividCortex/ewma v1.1.1 -github.com/containers/storage v1.12.5 +github.com/containers/storage v1.12.6 github.com/containers/psgo v1.2.1 github.com/coreos/go-systemd v14 github.com/coreos/pkg v4 @@ -93,12 +93,12 @@ k8s.io/api kubernetes-1.10.13-beta.0 https://github.com/kubernetes/api k8s.io/apimachinery kubernetes-1.10.13-beta.0 https://github.com/kubernetes/apimachinery k8s.io/client-go kubernetes-1.10.13-beta.0 https://github.com/kubernetes/client-go github.com/mrunalp/fileutils 7d4729fb36185a7c1719923406c9d40e54fb93c7 -github.com/varlink/go 3ac79db6fd6aec70924193b090962f92985fe199 -github.com/containers/buildah v1.8.0 +github.com/varlink/go 64e07fabffa33e385817b41971cf2674f692f391 +github.com/containers/buildah v1.8.1 # TODO: Gotty has not been updated since 2012. Can we find replacement? github.com/Nvveen/Gotty cd527374f1e5bff4938207604a14f2e38a9cf512 github.com/fsouza/go-dockerclient v1.3.0 -github.com/openshift/imagebuilder 705fe9255c57f8505efb9723a9ac4082b67973bc +github.com/openshift/imagebuilder v1.1.0 github.com/ulikunitz/xz v0.5.5 github.com/coreos/go-iptables v0.4.0 github.com/google/shlex c34317bd91bf98fab745d77b03933cf8769299fe diff --git a/vendor/github.com/containers/buildah/buildah.go b/vendor/github.com/containers/buildah/buildah.go index b6e6545ec..13526057c 100644 --- a/vendor/github.com/containers/buildah/buildah.go +++ b/vendor/github.com/containers/buildah/buildah.go @@ -26,7 +26,7 @@ const ( Package = "buildah" // Version for the Package. Bump version in contrib/rpm/buildah.spec // too. - Version = "1.8.0" + Version = "1.8.1" // The value we use to identify what type of information, currently a // serialized Builder structure, we are using as per-container state. // This should only be changed when we make incompatible changes to diff --git a/vendor/github.com/containers/buildah/imagebuildah/build.go b/vendor/github.com/containers/buildah/imagebuildah/build.go index d9909cdc8..85848e297 100644 --- a/vendor/github.com/containers/buildah/imagebuildah/build.go +++ b/vendor/github.com/containers/buildah/imagebuildah/build.go @@ -1558,6 +1558,9 @@ func (b *Executor) Build(ctx context.Context, stages imagebuilder.Stages) (image // stages. for i := range cleanupImages { removeID := cleanupImages[len(cleanupImages)-i-1] + if removeID == imageID { + continue + } if _, err := b.store.DeleteImage(removeID, true); err != nil { logrus.Debugf("failed to remove intermediate image %q: %v", removeID, err) if b.forceRmIntermediateCtrs || errors.Cause(err) != storage.ErrImageUsedByContainer { @@ -1663,6 +1666,7 @@ func (b *Executor) Build(ctx context.Context, stages imagebuilder.Stages) (image if !b.layers { cleanupImages = append(cleanupImages, imageID) } + imageID = "" } } @@ -1812,9 +1816,10 @@ func (b *Executor) deleteSuccessfulIntermediateCtrs() error { } func (s *StageExecutor) EnsureContainerPath(path string) error { - _, err := os.Stat(filepath.Join(s.mountPoint, path)) + targetPath := filepath.Join(s.mountPoint, path) + _, err := os.Lstat(targetPath) if err != nil && os.IsNotExist(err) { - err = os.MkdirAll(filepath.Join(s.mountPoint, path), 0755) + err = os.MkdirAll(targetPath, 0755) } if err != nil { return errors.Wrapf(err, "error ensuring container path %q", path) diff --git a/vendor/github.com/containers/buildah/imagebuildah/chroot_symlink.go b/vendor/github.com/containers/buildah/imagebuildah/chroot_symlink_linux.go index 0789c2b3c..e9d745b67 100644 --- a/vendor/github.com/containers/buildah/imagebuildah/chroot_symlink.go +++ b/vendor/github.com/containers/buildah/imagebuildah/chroot_symlink_linux.go @@ -24,6 +24,22 @@ func init() { reexec.Register(symlinkModifiedTime, resolveSymlinkTimeModified) } +// resolveSymlink uses a child subprocess to resolve any symlinks in filename +// in the context of rootdir. +func resolveSymlink(rootdir, filename string) (string, error) { + // The child process expects a chroot and one path that + // will be consulted relative to the chroot directory and evaluated + // for any symbolic links present. + cmd := reexec.Command(symlinkChrootedCommand, rootdir, filename) + output, err := cmd.CombinedOutput() + if err != nil { + return "", errors.Wrapf(err, string(output)) + } + + // Hand back the resolved symlink, will be filename if a symlink is not found + return string(output), nil +} + // main() for resolveSymlink()'s subprocess. func resolveChrootedSymlinks() { status := 0 @@ -55,22 +71,6 @@ func resolveChrootedSymlinks() { os.Exit(status) } -// resolveSymlink uses a child subprocess to resolve any symlinks in filename -// in the context of rootdir. -func resolveSymlink(rootdir, filename string) (string, error) { - // The child process expects a chroot and one path that - // will be consulted relative to the chroot directory and evaluated - // for any symbolic links present. - cmd := reexec.Command(symlinkChrootedCommand, rootdir, filename) - output, err := cmd.CombinedOutput() - if err != nil { - return "", errors.Wrapf(err, string(output)) - } - - // Hand back the resolved symlink, will be filename if a symlink is not found - return string(output), nil -} - // main() for grandparent subprocess. Its main job is to shuttle stdio back // and forth, managing a pseudo-terminal if we want one, for our child, the // parent subprocess. diff --git a/vendor/github.com/containers/buildah/imagebuildah/chroot_symlink_unsupported.go b/vendor/github.com/containers/buildah/imagebuildah/chroot_symlink_unsupported.go new file mode 100644 index 000000000..2cec4fe21 --- /dev/null +++ b/vendor/github.com/containers/buildah/imagebuildah/chroot_symlink_unsupported.go @@ -0,0 +1,13 @@ +// +build !linux + +package imagebuildah + +import "github.com/pkg/errors" + +func resolveSymlink(rootdir, filename string) (string, error) { + return "", errors.New("function not supported on non-linux systems") +} + +func resolveModifiedTime(rootdir, filename, historyTime string) (bool, error) { + return false, errors.New("function not supported on non-linux systems") +} diff --git a/vendor/github.com/containers/buildah/pkg/cli/common.go b/vendor/github.com/containers/buildah/pkg/cli/common.go index 7fa0a7777..e7a571db6 100644 --- a/vendor/github.com/containers/buildah/pkg/cli/common.go +++ b/vendor/github.com/containers/buildah/pkg/cli/common.go @@ -96,7 +96,7 @@ type FromAndBudResults struct { SecurityOpt []string ShmSize string Ulimit []string - Volume []string + Volumes []string } // GetUserNSFlags returns the common flags for usernamespace @@ -190,7 +190,7 @@ func GetFromAndBudFlags(flags *FromAndBudResults, usernsResults *UserNSResults, fs.StringArrayVar(&flags.SecurityOpt, "security-opt", []string{}, "security options (default [])") fs.StringVar(&flags.ShmSize, "shm-size", "65536k", "size of '/dev/shm'. The format is `<number><unit>`.") fs.StringSliceVar(&flags.Ulimit, "ulimit", []string{}, "ulimit options (default [])") - fs.StringSliceVarP(&flags.Volume, "volume", "v", []string{}, "bind mount a volume into the container (default [])") + fs.StringSliceVarP(&flags.Volumes, "volume", "v", []string{}, "bind mount a volume into the container (default [])") // Add in the usernamespace and namespaceflags usernsFlags := GetUserNSFlags(usernsResults) diff --git a/vendor/github.com/containers/buildah/pkg/parse/parse.go b/vendor/github.com/containers/buildah/pkg/parse/parse.go index cc85136fd..e8517eafb 100644 --- a/vendor/github.com/containers/buildah/pkg/parse/parse.go +++ b/vendor/github.com/containers/buildah/pkg/parse/parse.go @@ -22,7 +22,6 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" - "golang.org/x/sys/unix" ) const ( @@ -39,14 +38,9 @@ func CommonBuildOptions(c *cobra.Command) (*buildah.CommonBuildOptions, error) { memorySwap int64 err error ) - rlim := unix.Rlimit{Cur: 1048576, Max: 1048576} - defaultLimits := []string{} - if err := unix.Setrlimit(unix.RLIMIT_NOFILE, &rlim); err == nil { - defaultLimits = append(defaultLimits, fmt.Sprintf("nofile=%d:%d", rlim.Cur, rlim.Max)) - } - if err := unix.Setrlimit(unix.RLIMIT_NPROC, &rlim); err == nil { - defaultLimits = append(defaultLimits, fmt.Sprintf("nproc=%d:%d", rlim.Cur, rlim.Max)) - } + + defaultLimits := getDefaultProcessLimits() + memVal, _ := c.Flags().GetString("memory") if memVal != "" { memoryLimit, err = units.RAMInBytes(memVal) @@ -155,27 +149,42 @@ func parseSecurityOpts(securityOpts []string, commonOpts *buildah.CommonBuildOpt return nil } +func ParseVolume(volume string) (specs.Mount, error) { + mount := specs.Mount{} + arr := strings.SplitN(volume, ":", 3) + if len(arr) < 2 { + return mount, errors.Errorf("incorrect volume format %q, should be host-dir:ctr-dir[:option]", volume) + } + if err := validateVolumeHostDir(arr[0]); err != nil { + return mount, err + } + if err := validateVolumeCtrDir(arr[1]); err != nil { + return mount, err + } + mountOptions := "" + if len(arr) > 2 { + mountOptions = arr[2] + if err := validateVolumeOpts(arr[2]); err != nil { + return mount, err + } + } + mountOpts := strings.Split(mountOptions, ",") + mount.Source = arr[0] + mount.Destination = arr[1] + mount.Type = "rbind" + mount.Options = mountOpts + return mount, nil +} + // ParseVolumes validates the host and container paths passed in to the --volume flag func ParseVolumes(volumes []string) error { if len(volumes) == 0 { return nil } for _, volume := range volumes { - arr := strings.SplitN(volume, ":", 3) - if len(arr) < 2 { - return errors.Errorf("incorrect volume format %q, should be host-dir:ctr-dir[:option]", volume) - } - if err := validateVolumeHostDir(arr[0]); err != nil { - return err - } - if err := validateVolumeCtrDir(arr[1]); err != nil { + if _, err := ParseVolume(volume); err != nil { return err } - if len(arr) > 2 { - if err := validateVolumeOpts(arr[2]); err != nil { - return err - } - } } return nil } diff --git a/vendor/github.com/containers/buildah/pkg/parse/parse_unix.go b/vendor/github.com/containers/buildah/pkg/parse/parse_unix.go new file mode 100644 index 000000000..d056c1bb3 --- /dev/null +++ b/vendor/github.com/containers/buildah/pkg/parse/parse_unix.go @@ -0,0 +1,20 @@ +// +build linux darwin + +package parse + +import ( + "fmt" + "golang.org/x/sys/unix" +) + +func getDefaultProcessLimits() []string { + rlim := unix.Rlimit{Cur: 1048576, Max: 1048576} + defaultLimits := []string{} + if err := unix.Setrlimit(unix.RLIMIT_NOFILE, &rlim); err == nil { + defaultLimits = append(defaultLimits, fmt.Sprintf("nofile=%d:%d", rlim.Cur, rlim.Max)) + } + if err := unix.Setrlimit(unix.RLIMIT_NPROC, &rlim); err == nil { + defaultLimits = append(defaultLimits, fmt.Sprintf("nproc=%d:%d", rlim.Cur, rlim.Max)) + } + return defaultLimits +} diff --git a/vendor/github.com/containers/buildah/pkg/parse/parse_unsupported.go b/vendor/github.com/containers/buildah/pkg/parse/parse_unsupported.go new file mode 100644 index 000000000..7e970624f --- /dev/null +++ b/vendor/github.com/containers/buildah/pkg/parse/parse_unsupported.go @@ -0,0 +1,7 @@ +// +build !linux,!darwin + +package parse + +func getDefaultProcessLimits() []string { + return []string{} +} diff --git a/vendor/github.com/containers/buildah/run.go b/vendor/github.com/containers/buildah/run.go index 00eac8e39..88900b6b7 100644 --- a/vendor/github.com/containers/buildah/run.go +++ b/vendor/github.com/containers/buildah/run.go @@ -1,44 +1,10 @@ package buildah import ( - "bytes" - "context" - "encoding/json" "fmt" "io" - "io/ioutil" - "net" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "sync" - "syscall" - "time" - "github.com/containernetworking/cni/libcni" - "github.com/containers/buildah/bind" - "github.com/containers/buildah/chroot" - "github.com/containers/buildah/pkg/secrets" - "github.com/containers/buildah/pkg/unshare" - "github.com/containers/buildah/util" - "github.com/containers/storage/pkg/idtools" - "github.com/containers/storage/pkg/ioutils" - "github.com/containers/storage/pkg/reexec" - "github.com/containers/storage/pkg/stringid" - units "github.com/docker/go-units" - "github.com/docker/libnetwork/resolvconf" - "github.com/docker/libnetwork/types" - digest "github.com/opencontainers/go-digest" - specs "github.com/opencontainers/runtime-spec/specs-go" - "github.com/opencontainers/runtime-tools/generate" - "github.com/opencontainers/selinux/go-selinux/label" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "golang.org/x/crypto/ssh/terminal" - "golang.org/x/sys/unix" + "github.com/opencontainers/runtime-spec/specs-go" ) const ( @@ -203,34 +169,6 @@ type RunOptions struct { DropCapabilities []string } -// DefaultNamespaceOptions returns the default namespace settings from the -// runtime-tools generator library. -func DefaultNamespaceOptions() (NamespaceOptions, error) { - options := NamespaceOptions{ - {Name: string(specs.CgroupNamespace), Host: true}, - {Name: string(specs.IPCNamespace), Host: true}, - {Name: string(specs.MountNamespace), Host: true}, - {Name: string(specs.NetworkNamespace), Host: true}, - {Name: string(specs.PIDNamespace), Host: true}, - {Name: string(specs.UserNamespace), Host: true}, - {Name: string(specs.UTSNamespace), Host: true}, - } - g, err := generate.New("linux") - if err != nil { - return options, errors.Wrapf(err, "error generating new 'linux' runtime spec") - } - spec := g.Config - if spec.Linux != nil { - for _, ns := range spec.Linux.Namespaces { - options.AddOrReplace(NamespaceOption{ - Name: string(ns.Type), - Path: ns.Path, - }) - } - } - return options, nil -} - // Find the configuration for the namespace of the given type. If there are // duplicates, find the _last_ one of the type, since we assume it was appended // more recently. @@ -258,1959 +196,3 @@ nextOption: *n = append(*n, option) } } - -func addRlimits(ulimit []string, g *generate.Generator) error { - var ( - ul *units.Ulimit - err error - ) - - for _, u := range ulimit { - if ul, err = units.ParseUlimit(u); err != nil { - return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u) - } - - g.AddProcessRlimits("RLIMIT_"+strings.ToUpper(ul.Name), uint64(ul.Hard), uint64(ul.Soft)) - } - return nil -} - -func addCommonOptsToSpec(commonOpts *CommonBuildOptions, g *generate.Generator) error { - // Resources - CPU - if commonOpts.CPUPeriod != 0 { - g.SetLinuxResourcesCPUPeriod(commonOpts.CPUPeriod) - } - if commonOpts.CPUQuota != 0 { - g.SetLinuxResourcesCPUQuota(commonOpts.CPUQuota) - } - if commonOpts.CPUShares != 0 { - g.SetLinuxResourcesCPUShares(commonOpts.CPUShares) - } - if commonOpts.CPUSetCPUs != "" { - g.SetLinuxResourcesCPUCpus(commonOpts.CPUSetCPUs) - } - if commonOpts.CPUSetMems != "" { - g.SetLinuxResourcesCPUMems(commonOpts.CPUSetMems) - } - - // Resources - Memory - if commonOpts.Memory != 0 { - g.SetLinuxResourcesMemoryLimit(commonOpts.Memory) - } - if commonOpts.MemorySwap != 0 { - g.SetLinuxResourcesMemorySwap(commonOpts.MemorySwap) - } - - // cgroup membership - if commonOpts.CgroupParent != "" { - g.SetLinuxCgroupsPath(commonOpts.CgroupParent) - } - - // Other process resource limits - if err := addRlimits(commonOpts.Ulimit, g); err != nil { - return err - } - - logrus.Debugf("Resources: %#v", commonOpts) - return nil -} - -func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath string, optionMounts []specs.Mount, bindFiles map[string]string, builtinVolumes, volumeMounts []string, shmSize string, namespaceOptions NamespaceOptions) error { - // Start building a new list of mounts. - var mounts []specs.Mount - haveMount := func(destination string) bool { - for _, mount := range mounts { - if mount.Destination == destination { - // Already have something to mount there. - return true - } - } - return false - } - - ipc := namespaceOptions.Find(string(specs.IPCNamespace)) - hostIPC := ipc == nil || ipc.Host - net := namespaceOptions.Find(string(specs.NetworkNamespace)) - hostNetwork := net == nil || net.Host - user := namespaceOptions.Find(string(specs.UserNamespace)) - hostUser := user == nil || user.Host - - // Copy mounts from the generated list. - mountCgroups := true - specMounts := []specs.Mount{} - for _, specMount := range spec.Mounts { - // Override some of the mounts from the generated list if we're doing different things with namespaces. - if specMount.Destination == "/dev/shm" { - specMount.Options = []string{"nosuid", "noexec", "nodev", "mode=1777", "size=" + shmSize} - if hostIPC && !hostUser { - if _, err := os.Stat("/dev/shm"); err != nil && os.IsNotExist(err) { - logrus.Debugf("/dev/shm is not present, not binding into container") - continue - } - specMount = specs.Mount{ - Source: "/dev/shm", - Type: "bind", - Destination: "/dev/shm", - Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev"}, - } - } - } - if specMount.Destination == "/dev/mqueue" { - if hostIPC && !hostUser { - if _, err := os.Stat("/dev/mqueue"); err != nil && os.IsNotExist(err) { - logrus.Debugf("/dev/mqueue is not present, not binding into container") - continue - } - specMount = specs.Mount{ - Source: "/dev/mqueue", - Type: "bind", - Destination: "/dev/mqueue", - Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev"}, - } - } - } - if specMount.Destination == "/sys" { - if hostNetwork && !hostUser { - mountCgroups = false - if _, err := os.Stat("/sys"); err != nil && os.IsNotExist(err) { - logrus.Debugf("/sys is not present, not binding into container") - continue - } - specMount = specs.Mount{ - Source: "/sys", - Type: "bind", - Destination: "/sys", - Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev", "ro"}, - } - } - } - specMounts = append(specMounts, specMount) - } - - // Add a mount for the cgroups filesystem, unless we're already - // recursively bind mounting all of /sys, in which case we shouldn't - // bother with it. - sysfsMount := []specs.Mount{} - if mountCgroups { - sysfsMount = []specs.Mount{{ - Destination: "/sys/fs/cgroup", - Type: "cgroup", - Source: "cgroup", - Options: []string{bind.NoBindOption, "nosuid", "noexec", "nodev", "relatime", "ro"}, - }} - } - - // Get the list of files we need to bind into the container. - bindFileMounts, err := runSetupBoundFiles(bundlePath, bindFiles) - if err != nil { - return err - } - - // After this point we need to know the per-container persistent storage directory. - cdir, err := b.store.ContainerDirectory(b.ContainerID) - if err != nil { - return errors.Wrapf(err, "error determining work directory for container %q", b.ContainerID) - } - - // Figure out which UID and GID to tell the secrets package to use - // for files that it creates. - rootUID, rootGID, err := util.GetHostRootIDs(spec) - if err != nil { - return err - } - - // Get the list of secrets mounts. - secretMounts := secrets.SecretMountsWithUIDGID(b.MountLabel, cdir, b.DefaultMountsFilePath, cdir, int(rootUID), int(rootGID), unshare.IsRootless()) - - // Add temporary copies of the contents of volume locations at the - // volume locations, unless we already have something there. - copyWithTar := b.copyWithTar(nil, nil) - builtins, err := runSetupBuiltinVolumes(b.MountLabel, mountPoint, cdir, copyWithTar, builtinVolumes, int(rootUID), int(rootGID)) - if err != nil { - return err - } - - // Get the list of explicitly-specified volume mounts. - volumes, err := runSetupVolumeMounts(spec.Linux.MountLabel, volumeMounts, optionMounts) - if err != nil { - return err - } - - // Add them all, in the preferred order, except where they conflict with something that was previously added. - for _, mount := range append(append(append(append(append(volumes, builtins...), secretMounts...), bindFileMounts...), specMounts...), sysfsMount...) { - if haveMount(mount.Destination) { - // Already mounting something there, no need to bother with this one. - continue - } - // Add the mount. - mounts = append(mounts, mount) - } - - // Set the list in the spec. - spec.Mounts = mounts - return nil -} - -func runSetupBoundFiles(bundlePath string, bindFiles map[string]string) (mounts []specs.Mount, err error) { - for dest, src := range bindFiles { - options := []string{"rbind"} - if strings.HasPrefix(src, bundlePath) { - options = append(options, bind.NoBindOption) - } - mounts = append(mounts, specs.Mount{ - Source: src, - Destination: dest, - Type: "bind", - Options: options, - }) - } - return mounts, nil -} - -func runSetupBuiltinVolumes(mountLabel, mountPoint, containerDir string, copyWithTar func(srcPath, dstPath string) error, builtinVolumes []string, rootUID, rootGID int) ([]specs.Mount, error) { - var mounts []specs.Mount - hostOwner := idtools.IDPair{UID: rootUID, GID: rootGID} - // Add temporary copies of the contents of volume locations at the - // volume locations, unless we already have something there. - for _, volume := range builtinVolumes { - subdir := digest.Canonical.FromString(volume).Hex() - volumePath := filepath.Join(containerDir, "buildah-volumes", subdir) - srcPath := filepath.Join(mountPoint, volume) - initializeVolume := false - // If we need to, initialize the volume path's initial contents. - if _, err := os.Stat(volumePath); err != nil { - if !os.IsNotExist(err) { - return nil, errors.Wrapf(err, "failed to stat %q for volume %q", volumePath, volume) - } - logrus.Debugf("setting up built-in volume at %q", volumePath) - if err = os.MkdirAll(volumePath, 0755); err != nil { - return nil, errors.Wrapf(err, "error creating directory %q for volume %q", volumePath, volume) - } - if err = label.Relabel(volumePath, mountLabel, false); err != nil { - return nil, errors.Wrapf(err, "error relabeling directory %q for volume %q", volumePath, volume) - } - initializeVolume = true - } - stat, err := os.Stat(srcPath) - if err != nil { - if !os.IsNotExist(err) { - return nil, errors.Wrapf(err, "failed to stat %q for volume %q", srcPath, volume) - } - if err = idtools.MkdirAllAndChownNew(srcPath, 0755, hostOwner); err != nil { - return nil, errors.Wrapf(err, "error creating directory %q for volume %q", srcPath, volume) - } - if stat, err = os.Stat(srcPath); err != nil { - return nil, errors.Wrapf(err, "failed to stat %q for volume %q", srcPath, volume) - } - } - if initializeVolume { - if err = os.Chmod(volumePath, stat.Mode().Perm()); err != nil { - return nil, errors.Wrapf(err, "failed to chmod %q for volume %q", volumePath, volume) - } - if err = os.Chown(volumePath, int(stat.Sys().(*syscall.Stat_t).Uid), int(stat.Sys().(*syscall.Stat_t).Gid)); err != nil { - return nil, errors.Wrapf(err, "error chowning directory %q for volume %q", volumePath, volume) - } - if err = copyWithTar(srcPath, volumePath); err != nil && !os.IsNotExist(errors.Cause(err)) { - return nil, errors.Wrapf(err, "error populating directory %q for volume %q using contents of %q", volumePath, volume, srcPath) - } - } - // Add the bind mount. - mounts = append(mounts, specs.Mount{ - Source: volumePath, - Destination: volume, - Type: "bind", - Options: []string{"bind"}, - }) - } - return mounts, nil -} - -func runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount) ([]specs.Mount, error) { - var mounts []specs.Mount - - parseMount := func(host, container string, options []string) (specs.Mount, error) { - var foundrw, foundro, foundz, foundZ bool - var rootProp string - for _, opt := range options { - switch opt { - case "rw": - foundrw = true - case "ro": - foundro = true - case "z": - foundz = true - case "Z": - foundZ = true - case "private", "rprivate", "slave", "rslave", "shared", "rshared": - rootProp = opt - } - } - if !foundrw && !foundro { - options = append(options, "rw") - } - if foundz { - if err := label.Relabel(host, mountLabel, true); err != nil { - return specs.Mount{}, errors.Wrapf(err, "relabeling %q failed", host) - } - } - if foundZ { - if err := label.Relabel(host, mountLabel, false); err != nil { - return specs.Mount{}, errors.Wrapf(err, "relabeling %q failed", host) - } - } - if rootProp == "" { - options = append(options, "private") - } - return specs.Mount{ - Destination: container, - Type: "bind", - Source: host, - Options: options, - }, nil - } - // Bind mount volumes specified for this particular Run() invocation - for _, i := range optionMounts { - logrus.Debugf("setting up mounted volume at %q", i.Destination) - mount, err := parseMount(i.Source, i.Destination, append(i.Options, "rbind")) - if err != nil { - return nil, err - } - mounts = append(mounts, mount) - } - // Bind mount volumes given by the user when the container was created - for _, i := range volumeMounts { - var options []string - spliti := strings.Split(i, ":") - if len(spliti) > 2 { - options = strings.Split(spliti[2], ",") - } - options = append(options, "rbind") - mount, err := parseMount(spliti[0], spliti[1], options) - if err != nil { - return nil, err - } - mounts = append(mounts, mount) - } - return mounts, nil -} - -// addNetworkConfig copies files from host and sets them up to bind mount into container -func (b *Builder) addNetworkConfig(rdir, hostPath string, chownOpts *idtools.IDPair, dnsServers, dnsSearch, dnsOptions []string) (string, error) { - stat, err := os.Stat(hostPath) - if err != nil { - return "", errors.Wrapf(err, "error statting %q for container %q", hostPath, b.ContainerID) - } - contents, err := ioutil.ReadFile(hostPath) - if err != nil { - return "", errors.Wrapf(err, "unable to read %s", hostPath) - } - - search := resolvconf.GetSearchDomains(contents) - nameservers := resolvconf.GetNameservers(contents, types.IP) - options := resolvconf.GetOptions(contents) - - if len(dnsSearch) > 0 { - search = dnsSearch - } - if len(dnsServers) != 0 { - dns, err := getDNSIP(dnsServers) - if err != nil { - return "", errors.Wrapf(err, "error getting dns servers") - } - nameservers = []string{} - for _, server := range dns { - nameservers = append(nameservers, server.String()) - } - } - - if len(dnsOptions) != 0 { - options = dnsOptions - } - - cfile := filepath.Join(rdir, filepath.Base(hostPath)) - if _, err = resolvconf.Build(cfile, nameservers, search, options); err != nil { - return "", errors.Wrapf(err, "error building resolv.conf for container %s", b.ContainerID) - } - - uid := int(stat.Sys().(*syscall.Stat_t).Uid) - gid := int(stat.Sys().(*syscall.Stat_t).Gid) - if chownOpts != nil { - uid = chownOpts.UID - gid = chownOpts.GID - } - if err = os.Chown(cfile, uid, gid); err != nil { - return "", errors.Wrapf(err, "error chowning file %q for container %q", cfile, b.ContainerID) - } - - if err := label.Relabel(cfile, b.MountLabel, false); err != nil { - return "", errors.Wrapf(err, "error relabeling %q in container %q", cfile, b.ContainerID) - } - - return cfile, nil -} - -func getDNSIP(dnsServers []string) (dns []net.IP, err error) { - for _, i := range dnsServers { - result := net.ParseIP(i) - if result == nil { - return dns, errors.Errorf("invalid IP address %s", i) - } - dns = append(dns, result) - } - return dns, nil -} - -// generateHosts creates a containers hosts file -func (b *Builder) generateHosts(rdir, hostname string, addHosts []string, chownOpts *idtools.IDPair) (string, error) { - hostPath := "/etc/hosts" - stat, err := os.Stat(hostPath) - if err != nil { - return "", errors.Wrapf(err, "error statting %q for container %q", hostPath, b.ContainerID) - } - - hosts := bytes.NewBufferString("# Generated by Buildah\n") - orig, err := ioutil.ReadFile(hostPath) - if err != nil { - return "", errors.Wrapf(err, "unable to read %s", hostPath) - } - hosts.Write(orig) - for _, host := range addHosts { - // verify the host format - values := strings.SplitN(host, ":", 2) - if len(values) != 2 { - return "", errors.Errorf("unable to parse host entry %q: incorrect format", host) - } - if values[0] == "" { - return "", errors.Errorf("hostname in host entry %q is empty", host) - } - if values[1] == "" { - return "", errors.Errorf("IP address in host entry %q is empty", host) - } - hosts.Write([]byte(fmt.Sprintf("%s\t%s\n", values[1], values[0]))) - } - - if hostname != "" { - hosts.Write([]byte(fmt.Sprintf("127.0.0.1 %s\n", hostname))) - hosts.Write([]byte(fmt.Sprintf("::1 %s\n", hostname))) - } - cfile := filepath.Join(rdir, filepath.Base(hostPath)) - if err = ioutils.AtomicWriteFile(cfile, hosts.Bytes(), stat.Mode().Perm()); err != nil { - return "", errors.Wrapf(err, "error writing /etc/hosts into the container") - } - uid := int(stat.Sys().(*syscall.Stat_t).Uid) - gid := int(stat.Sys().(*syscall.Stat_t).Gid) - if chownOpts != nil { - uid = chownOpts.UID - gid = chownOpts.GID - } - if err = os.Chown(cfile, uid, gid); err != nil { - return "", errors.Wrapf(err, "error chowning file %q for container %q", cfile, b.ContainerID) - } - if err := label.Relabel(cfile, b.MountLabel, false); err != nil { - return "", errors.Wrapf(err, "error relabeling %q in container %q", cfile, b.ContainerID) - } - - return cfile, nil -} - -func setupMaskedPaths(g *generate.Generator) { - for _, mp := range []string{ - "/proc/acpi", - "/proc/kcore", - "/proc/keys", - "/proc/latency_stats", - "/proc/timer_list", - "/proc/timer_stats", - "/proc/sched_debug", - "/proc/scsi", - "/sys/firmware", - } { - g.AddLinuxMaskedPaths(mp) - } -} - -func setupReadOnlyPaths(g *generate.Generator) { - for _, rp := range []string{ - "/proc/asound", - "/proc/bus", - "/proc/fs", - "/proc/irq", - "/proc/sys", - "/proc/sysrq-trigger", - } { - g.AddLinuxReadonlyPaths(rp) - } -} - -func setupCapAdd(g *generate.Generator, caps ...string) error { - for _, cap := range caps { - if err := g.AddProcessCapabilityBounding(cap); err != nil { - return errors.Wrapf(err, "error adding %q to the bounding capability set", cap) - } - if err := g.AddProcessCapabilityEffective(cap); err != nil { - return errors.Wrapf(err, "error adding %q to the effective capability set", cap) - } - if err := g.AddProcessCapabilityInheritable(cap); err != nil { - return errors.Wrapf(err, "error adding %q to the inheritable capability set", cap) - } - if err := g.AddProcessCapabilityPermitted(cap); err != nil { - return errors.Wrapf(err, "error adding %q to the permitted capability set", cap) - } - if err := g.AddProcessCapabilityAmbient(cap); err != nil { - return errors.Wrapf(err, "error adding %q to the ambient capability set", cap) - } - } - return nil -} - -func setupCapDrop(g *generate.Generator, caps ...string) error { - for _, cap := range caps { - if err := g.DropProcessCapabilityBounding(cap); err != nil { - return errors.Wrapf(err, "error removing %q from the bounding capability set", cap) - } - if err := g.DropProcessCapabilityEffective(cap); err != nil { - return errors.Wrapf(err, "error removing %q from the effective capability set", cap) - } - if err := g.DropProcessCapabilityInheritable(cap); err != nil { - return errors.Wrapf(err, "error removing %q from the inheritable capability set", cap) - } - if err := g.DropProcessCapabilityPermitted(cap); err != nil { - return errors.Wrapf(err, "error removing %q from the permitted capability set", cap) - } - if err := g.DropProcessCapabilityAmbient(cap); err != nil { - return errors.Wrapf(err, "error removing %q from the ambient capability set", cap) - } - } - return nil -} - -func setupCapabilities(g *generate.Generator, firstAdds, firstDrops, secondAdds, secondDrops []string) error { - g.ClearProcessCapabilities() - if err := setupCapAdd(g, util.DefaultCapabilities...); err != nil { - return err - } - if err := setupCapAdd(g, firstAdds...); err != nil { - return err - } - if err := setupCapDrop(g, firstDrops...); err != nil { - return err - } - if err := setupCapAdd(g, secondAdds...); err != nil { - return err - } - return setupCapDrop(g, secondDrops...) -} - -func setupTerminal(g *generate.Generator, terminalPolicy TerminalPolicy, terminalSize *specs.Box) { - switch terminalPolicy { - case DefaultTerminal: - onTerminal := terminal.IsTerminal(unix.Stdin) && terminal.IsTerminal(unix.Stdout) && terminal.IsTerminal(unix.Stderr) - if onTerminal { - logrus.Debugf("stdio is a terminal, defaulting to using a terminal") - } else { - logrus.Debugf("stdio is not a terminal, defaulting to not using a terminal") - } - g.SetProcessTerminal(onTerminal) - case WithTerminal: - g.SetProcessTerminal(true) - case WithoutTerminal: - g.SetProcessTerminal(false) - } - if terminalSize != nil { - g.SetProcessConsoleSize(terminalSize.Width, terminalSize.Height) - } -} - -func setupNamespaces(g *generate.Generator, namespaceOptions NamespaceOptions, idmapOptions IDMappingOptions, policy NetworkConfigurationPolicy) (configureNetwork bool, configureNetworks []string, configureUTS bool, err error) { - // Set namespace options in the container configuration. - configureUserns := false - specifiedNetwork := false - for _, namespaceOption := range namespaceOptions { - switch namespaceOption.Name { - case string(specs.UserNamespace): - configureUserns = false - if !namespaceOption.Host && namespaceOption.Path == "" { - configureUserns = true - } - case string(specs.NetworkNamespace): - specifiedNetwork = true - configureNetwork = false - if !namespaceOption.Host && (namespaceOption.Path == "" || !filepath.IsAbs(namespaceOption.Path)) { - if namespaceOption.Path != "" && !filepath.IsAbs(namespaceOption.Path) { - configureNetworks = strings.Split(namespaceOption.Path, ",") - namespaceOption.Path = "" - } - configureNetwork = (policy != NetworkDisabled) - } - case string(specs.UTSNamespace): - configureUTS = false - if !namespaceOption.Host && namespaceOption.Path == "" { - configureUTS = true - } - } - if namespaceOption.Host { - if err := g.RemoveLinuxNamespace(namespaceOption.Name); err != nil { - return false, nil, false, errors.Wrapf(err, "error removing %q namespace for run", namespaceOption.Name) - } - } else if err := g.AddOrReplaceLinuxNamespace(namespaceOption.Name, namespaceOption.Path); err != nil { - if namespaceOption.Path == "" { - return false, nil, false, errors.Wrapf(err, "error adding new %q namespace for run", namespaceOption.Name) - } - return false, nil, false, errors.Wrapf(err, "error adding %q namespace %q for run", namespaceOption.Name, namespaceOption.Path) - } - } - - // If we've got mappings, we're going to have to create a user namespace. - if len(idmapOptions.UIDMap) > 0 || len(idmapOptions.GIDMap) > 0 || configureUserns { - if err := g.AddOrReplaceLinuxNamespace(specs.UserNamespace, ""); err != nil { - return false, nil, false, errors.Wrapf(err, "error adding new %q namespace for run", string(specs.UserNamespace)) - } - hostUidmap, hostGidmap, err := unshare.GetHostIDMappings("") - if err != nil { - return false, nil, false, err - } - for _, m := range idmapOptions.UIDMap { - g.AddLinuxUIDMapping(m.HostID, m.ContainerID, m.Size) - } - if len(idmapOptions.UIDMap) == 0 { - for _, m := range hostUidmap { - g.AddLinuxUIDMapping(m.ContainerID, m.ContainerID, m.Size) - } - } - for _, m := range idmapOptions.GIDMap { - g.AddLinuxGIDMapping(m.HostID, m.ContainerID, m.Size) - } - if len(idmapOptions.GIDMap) == 0 { - for _, m := range hostGidmap { - g.AddLinuxGIDMapping(m.ContainerID, m.ContainerID, m.Size) - } - } - if !specifiedNetwork { - if err := g.AddOrReplaceLinuxNamespace(specs.NetworkNamespace, ""); err != nil { - return false, nil, false, errors.Wrapf(err, "error adding new %q namespace for run", string(specs.NetworkNamespace)) - } - configureNetwork = (policy != NetworkDisabled) - } - } else { - if err := g.RemoveLinuxNamespace(specs.UserNamespace); err != nil { - return false, nil, false, errors.Wrapf(err, "error removing %q namespace for run", string(specs.UserNamespace)) - } - if !specifiedNetwork { - if err := g.RemoveLinuxNamespace(specs.NetworkNamespace); err != nil { - return false, nil, false, errors.Wrapf(err, "error removing %q namespace for run", string(specs.NetworkNamespace)) - } - } - } - if configureNetwork { - for name, val := range util.DefaultNetworkSysctl { - g.AddLinuxSysctl(name, val) - } - } - return configureNetwork, configureNetworks, configureUTS, nil -} - -// Search for a command that isn't given as an absolute path using the $PATH -// under the rootfs. We can't resolve absolute symbolic links without -// chroot()ing, which we may not be able to do, so just accept a link as a -// valid resolution. -func runLookupPath(g *generate.Generator, command []string) []string { - // Look for the configured $PATH. - spec := g.Config - envPath := "" - for i := range spec.Process.Env { - if strings.HasPrefix(spec.Process.Env[i], "PATH=") { - envPath = spec.Process.Env[i] - } - } - // If there is no configured $PATH, supply one. - if envPath == "" { - defaultPath := "/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin" - envPath = "PATH=" + defaultPath - g.AddProcessEnv("PATH", defaultPath) - } - // No command, nothing to do. - if len(command) == 0 { - return command - } - // Command is already an absolute path, use it as-is. - if filepath.IsAbs(command[0]) { - return command - } - // For each element in the PATH, - for _, pathEntry := range filepath.SplitList(envPath[5:]) { - // if it's the empty string, it's ".", which is the Cwd, - if pathEntry == "" { - pathEntry = spec.Process.Cwd - } - // build the absolute path which it might be, - candidate := filepath.Join(pathEntry, command[0]) - // check if it's there, - if fi, err := os.Lstat(filepath.Join(spec.Root.Path, candidate)); fi != nil && err == nil { - // and if it's not a directory, and either a symlink or executable, - if !fi.IsDir() && ((fi.Mode()&os.ModeSymlink != 0) || (fi.Mode()&0111 != 0)) { - // use that. - return append([]string{candidate}, command[1:]...) - } - } - } - return command -} - -func (b *Builder) configureUIDGID(g *generate.Generator, mountPoint string, options RunOptions) error { - // Set the user UID/GID/supplemental group list/capabilities lists. - user, err := b.user(mountPoint, options.User) - if err != nil { - return err - } - if err := setupCapabilities(g, b.AddCapabilities, b.DropCapabilities, options.AddCapabilities, options.DropCapabilities); err != nil { - return err - } - g.SetProcessUID(user.UID) - g.SetProcessGID(user.GID) - for _, gid := range user.AdditionalGids { - g.AddProcessAdditionalGid(gid) - } - - // Remove capabilities if not running as root except Bounding set - if user.UID != 0 { - bounding := g.Config.Process.Capabilities.Bounding - g.ClearProcessCapabilities() - g.Config.Process.Capabilities.Bounding = bounding - } - - return nil -} - -func (b *Builder) configureEnvironment(g *generate.Generator, options RunOptions) { - g.ClearProcessEnv() - if b.CommonBuildOpts.HTTPProxy { - for _, envSpec := range []string{ - "http_proxy", - "HTTP_PROXY", - "https_proxy", - "HTTPS_PROXY", - "ftp_proxy", - "FTP_PROXY", - "no_proxy", - "NO_PROXY", - } { - envVal := os.Getenv(envSpec) - if envVal != "" { - g.AddProcessEnv(envSpec, envVal) - } - } - } - - for _, envSpec := range append(b.Env(), options.Env...) { - env := strings.SplitN(envSpec, "=", 2) - if len(env) > 1 { - g.AddProcessEnv(env[0], env[1]) - } - } - - for src, dest := range b.Args { - g.AddProcessEnv(src, dest) - } -} - -func (b *Builder) configureNamespaces(g *generate.Generator, options RunOptions) (bool, []string, error) { - defaultNamespaceOptions, err := DefaultNamespaceOptions() - if err != nil { - return false, nil, err - } - - namespaceOptions := defaultNamespaceOptions - namespaceOptions.AddOrReplace(b.NamespaceOptions...) - namespaceOptions.AddOrReplace(options.NamespaceOptions...) - - networkPolicy := options.ConfigureNetwork - if networkPolicy == NetworkDefault { - networkPolicy = b.ConfigureNetwork - } - - configureNetwork, configureNetworks, configureUTS, err := setupNamespaces(g, namespaceOptions, b.IDMappingOptions, networkPolicy) - if err != nil { - return false, nil, err - } - - if configureUTS { - if options.Hostname != "" { - g.SetHostname(options.Hostname) - } else if b.Hostname() != "" { - g.SetHostname(b.Hostname()) - } else { - g.SetHostname(stringid.TruncateID(b.ContainerID)) - } - } else { - g.SetHostname("") - } - - found := false - spec := g.Config - for i := range spec.Process.Env { - if strings.HasPrefix(spec.Process.Env[i], "HOSTNAME=") { - found = true - break - } - } - if !found { - spec.Process.Env = append(spec.Process.Env, fmt.Sprintf("HOSTNAME=%s", spec.Hostname)) - } - - return configureNetwork, configureNetworks, nil -} - -// Run runs the specified command in the container's root filesystem. -func (b *Builder) Run(command []string, options RunOptions) error { - p, err := ioutil.TempDir("", Package) - if err != nil { - return errors.Wrapf(err, "run: error creating temporary directory under %q", os.TempDir()) - } - // On some hosts like AH, /tmp is a symlink and we need an - // absolute path. - path, err := filepath.EvalSymlinks(p) - if err != nil { - return errors.Wrapf(err, "run: error evaluating %q for symbolic links", p) - } - logrus.Debugf("using %q to hold bundle data", path) - defer func() { - if err2 := os.RemoveAll(path); err2 != nil { - logrus.Errorf("error removing %q: %v", path, err2) - } - }() - - gp, err := generate.New("linux") - if err != nil { - return errors.Wrapf(err, "error generating new 'linux' runtime spec") - } - g := &gp - - isolation := options.Isolation - if isolation == IsolationDefault { - isolation = b.Isolation - if isolation == IsolationDefault { - isolation = IsolationOCI - } - } - if err := checkAndOverrideIsolationOptions(isolation, &options); err != nil { - return err - } - - b.configureEnvironment(g, options) - - if b.CommonBuildOpts == nil { - return errors.Errorf("Invalid format on container you must recreate the container") - } - - if err := addCommonOptsToSpec(b.CommonBuildOpts, g); err != nil { - return err - } - - if options.WorkingDir != "" { - g.SetProcessCwd(options.WorkingDir) - } else if b.WorkDir() != "" { - g.SetProcessCwd(b.WorkDir()) - } - setupSelinux(g, b.ProcessLabel, b.MountLabel) - mountPoint, err := b.Mount(b.MountLabel) - if err != nil { - return errors.Wrapf(err, "error mounting container %q", b.ContainerID) - } - defer func() { - if err := b.Unmount(); err != nil { - logrus.Errorf("error unmounting container: %v", err) - } - }() - g.SetRootPath(mountPoint) - if len(command) > 0 { - command = runLookupPath(g, command) - g.SetProcessArgs(command) - } else { - g.SetProcessArgs(nil) - } - - setupMaskedPaths(g) - setupReadOnlyPaths(g) - - setupTerminal(g, options.Terminal, options.TerminalSize) - - configureNetwork, configureNetworks, err := b.configureNamespaces(g, options) - if err != nil { - return err - } - - if err := b.configureUIDGID(g, mountPoint, options); err != nil { - return err - } - - g.SetProcessApparmorProfile(b.CommonBuildOpts.ApparmorProfile) - - // Now grab the spec from the generator. Set the generator to nil so that future contributors - // will quickly be able to tell that they're supposed to be modifying the spec directly from here. - spec := g.Config - g = nil - - logrus.Debugf("ensuring working directory %q exists", filepath.Join(mountPoint, spec.Process.Cwd)) - if err = os.MkdirAll(filepath.Join(mountPoint, spec.Process.Cwd), 0755); err != nil { - return errors.Wrapf(err, "error ensuring working directory %q exists", spec.Process.Cwd) - } - - // Set the seccomp configuration using the specified profile name. Some syscalls are - // allowed if certain capabilities are to be granted (example: CAP_SYS_CHROOT and chroot), - // so we sorted out the capabilities lists first. - if err = setupSeccomp(spec, b.CommonBuildOpts.SeccompProfilePath); err != nil { - return err - } - - // Figure out who owns files that will appear to be owned by UID/GID 0 in the container. - rootUID, rootGID, err := util.GetHostRootIDs(spec) - if err != nil { - return err - } - rootIDPair := &idtools.IDPair{UID: int(rootUID), GID: int(rootGID)} - - bindFiles := make(map[string]string) - namespaceOptions := append(b.NamespaceOptions, options.NamespaceOptions...) - volumes := b.Volumes() - - if !contains(volumes, "/etc/hosts") { - hostFile, err := b.generateHosts(path, spec.Hostname, b.CommonBuildOpts.AddHost, rootIDPair) - if err != nil { - return err - } - bindFiles["/etc/hosts"] = hostFile - } - - if !contains(volumes, "/etc/resolv.conf") { - resolvFile, err := b.addNetworkConfig(path, "/etc/resolv.conf", rootIDPair, b.CommonBuildOpts.DNSServers, b.CommonBuildOpts.DNSSearch, b.CommonBuildOpts.DNSOptions) - if err != nil { - return err - } - bindFiles["/etc/resolv.conf"] = resolvFile - } - - err = b.setupMounts(mountPoint, spec, path, options.Mounts, bindFiles, volumes, b.CommonBuildOpts.Volumes, b.CommonBuildOpts.ShmSize, namespaceOptions) - if err != nil { - return errors.Wrapf(err, "error resolving mountpoints for container %q", b.ContainerID) - } - - if options.CNIConfigDir == "" { - options.CNIConfigDir = b.CNIConfigDir - if b.CNIConfigDir == "" { - options.CNIConfigDir = util.DefaultCNIConfigDir - } - } - if options.CNIPluginPath == "" { - options.CNIPluginPath = b.CNIPluginPath - if b.CNIPluginPath == "" { - options.CNIPluginPath = util.DefaultCNIPluginPath - } - } - - switch isolation { - case IsolationOCI: - var moreCreateArgs []string - if options.NoPivot { - moreCreateArgs = []string{"--no-pivot"} - } else { - moreCreateArgs = nil - } - err = b.runUsingRuntimeSubproc(isolation, options, configureNetwork, configureNetworks, moreCreateArgs, spec, mountPoint, path, Package+"-"+filepath.Base(path)) - case IsolationChroot: - err = chroot.RunUsingChroot(spec, path, options.Stdin, options.Stdout, options.Stderr) - case IsolationOCIRootless: - moreCreateArgs := []string{"--no-new-keyring"} - if options.NoPivot { - moreCreateArgs = append(moreCreateArgs, "--no-pivot") - } - if err := setupRootlessSpecChanges(spec, path, rootUID, rootGID); err != nil { - return err - } - err = b.runUsingRuntimeSubproc(isolation, options, configureNetwork, configureNetworks, moreCreateArgs, spec, mountPoint, path, Package+"-"+filepath.Base(path)) - default: - err = errors.Errorf("don't know how to run this command") - } - return err -} - -func contains(volumes []string, v string) bool { - for _, i := range volumes { - if i == v { - return true - } - } - return false -} - -func checkAndOverrideIsolationOptions(isolation Isolation, options *RunOptions) error { - switch isolation { - case IsolationOCIRootless: - if ns := options.NamespaceOptions.Find(string(specs.IPCNamespace)); ns == nil || ns.Host { - logrus.Debugf("Forcing use of an IPC namespace.") - } - options.NamespaceOptions.AddOrReplace(NamespaceOption{Name: string(specs.IPCNamespace)}) - _, err := exec.LookPath("slirp4netns") - hostNetworking := err != nil - networkNamespacePath := "" - if ns := options.NamespaceOptions.Find(string(specs.NetworkNamespace)); ns != nil { - hostNetworking = ns.Host - networkNamespacePath = ns.Path - if !hostNetworking && networkNamespacePath != "" && !filepath.IsAbs(networkNamespacePath) { - logrus.Debugf("Disabling network namespace configuration.") - networkNamespacePath = "" - } - } - options.NamespaceOptions.AddOrReplace(NamespaceOption{ - Name: string(specs.NetworkNamespace), - Host: hostNetworking, - Path: networkNamespacePath, - }) - if ns := options.NamespaceOptions.Find(string(specs.PIDNamespace)); ns == nil || ns.Host { - logrus.Debugf("Forcing use of a PID namespace.") - } - options.NamespaceOptions.AddOrReplace(NamespaceOption{Name: string(specs.PIDNamespace), Host: false}) - if ns := options.NamespaceOptions.Find(string(specs.UserNamespace)); ns == nil || ns.Host { - logrus.Debugf("Forcing use of a user namespace.") - } - options.NamespaceOptions.AddOrReplace(NamespaceOption{Name: string(specs.UserNamespace)}) - if ns := options.NamespaceOptions.Find(string(specs.UTSNamespace)); ns != nil && !ns.Host { - logrus.Debugf("Disabling UTS namespace.") - } - options.NamespaceOptions.AddOrReplace(NamespaceOption{Name: string(specs.UTSNamespace), Host: true}) - case IsolationOCI: - pidns := options.NamespaceOptions.Find(string(specs.PIDNamespace)) - userns := options.NamespaceOptions.Find(string(specs.UserNamespace)) - if (pidns == nil || pidns.Host) && (userns != nil && !userns.Host) { - return fmt.Errorf("not allowed to mix host PID namespace with container user namespace") - } - } - return nil -} - -func setupRootlessSpecChanges(spec *specs.Spec, bundleDir string, rootUID, rootGID uint32) error { - spec.Hostname = "" - spec.Process.User.AdditionalGids = nil - spec.Linux.Resources = nil - - emptyDir := filepath.Join(bundleDir, "empty") - if err := os.Mkdir(emptyDir, 0); err != nil { - return errors.Wrapf(err, "error creating %q", emptyDir) - } - - // Replace /sys with a read-only bind mount. - mounts := []specs.Mount{ - { - Source: "/dev", - Destination: "/dev", - Type: "tmpfs", - Options: []string{"private", "strictatime", "noexec", "nosuid", "mode=755", "size=65536k"}, - }, - { - Source: "mqueue", - Destination: "/dev/mqueue", - Type: "mqueue", - Options: []string{"private", "nodev", "noexec", "nosuid"}, - }, - { - Source: "pts", - Destination: "/dev/pts", - Type: "devpts", - Options: []string{"private", "noexec", "nosuid", "newinstance", "ptmxmode=0666", "mode=0620"}, - }, - { - Source: "shm", - Destination: "/dev/shm", - Type: "tmpfs", - Options: []string{"private", "nodev", "noexec", "nosuid", "mode=1777", "size=65536k"}, - }, - { - Source: "/proc", - Destination: "/proc", - Type: "proc", - Options: []string{"private", "nodev", "noexec", "nosuid"}, - }, - { - Source: "/sys", - Destination: "/sys", - Type: "bind", - Options: []string{bind.NoBindOption, "rbind", "private", "nodev", "noexec", "nosuid", "ro"}, - }, - } - // Cover up /sys/fs/cgroup and /sys/fs/selinux, if they exist in our source for /sys. - if _, err := os.Stat("/sys/fs/cgroup"); err == nil { - spec.Linux.MaskedPaths = append(spec.Linux.MaskedPaths, "/sys/fs/cgroup") - } - if _, err := os.Stat("/sys/fs/selinux"); err == nil { - spec.Linux.MaskedPaths = append(spec.Linux.MaskedPaths, "/sys/fs/selinux") - } - // Keep anything that isn't under /dev, /proc, or /sys. - for i := range spec.Mounts { - if spec.Mounts[i].Destination == "/dev" || strings.HasPrefix(spec.Mounts[i].Destination, "/dev/") || - spec.Mounts[i].Destination == "/proc" || strings.HasPrefix(spec.Mounts[i].Destination, "/proc/") || - spec.Mounts[i].Destination == "/sys" || strings.HasPrefix(spec.Mounts[i].Destination, "/sys/") { - continue - } - mounts = append(mounts, spec.Mounts[i]) - } - spec.Mounts = mounts - return nil -} - -type runUsingRuntimeSubprocOptions struct { - Options RunOptions - Spec *specs.Spec - RootPath string - BundlePath string - ConfigureNetwork bool - ConfigureNetworks []string - MoreCreateArgs []string - ContainerName string - Isolation Isolation -} - -func (b *Builder) runUsingRuntimeSubproc(isolation Isolation, options RunOptions, configureNetwork bool, configureNetworks, moreCreateArgs []string, spec *specs.Spec, rootPath, bundlePath, containerName string) (err error) { - var confwg sync.WaitGroup - config, conferr := json.Marshal(runUsingRuntimeSubprocOptions{ - Options: options, - Spec: spec, - RootPath: rootPath, - BundlePath: bundlePath, - ConfigureNetwork: configureNetwork, - ConfigureNetworks: configureNetworks, - MoreCreateArgs: moreCreateArgs, - ContainerName: containerName, - Isolation: isolation, - }) - if conferr != nil { - return errors.Wrapf(conferr, "error encoding configuration for %q", runUsingRuntimeCommand) - } - cmd := reexec.Command(runUsingRuntimeCommand) - cmd.Dir = bundlePath - cmd.Stdin = options.Stdin - if cmd.Stdin == nil { - cmd.Stdin = os.Stdin - } - cmd.Stdout = options.Stdout - if cmd.Stdout == nil { - cmd.Stdout = os.Stdout - } - cmd.Stderr = options.Stderr - if cmd.Stderr == nil { - cmd.Stderr = os.Stderr - } - cmd.Env = append(os.Environ(), fmt.Sprintf("LOGLEVEL=%d", logrus.GetLevel())) - preader, pwriter, err := os.Pipe() - if err != nil { - return errors.Wrapf(err, "error creating configuration pipe") - } - confwg.Add(1) - go func() { - _, conferr = io.Copy(pwriter, bytes.NewReader(config)) - if conferr != nil { - conferr = errors.Wrapf(conferr, "error while copying configuration down pipe to child process") - } - confwg.Done() - }() - cmd.ExtraFiles = append([]*os.File{preader}, cmd.ExtraFiles...) - defer preader.Close() - defer pwriter.Close() - err = cmd.Run() - if err != nil { - err = errors.Wrapf(err, "error while running runtime") - } - confwg.Wait() - if err == nil { - return conferr - } - if conferr != nil { - logrus.Debugf("%v", conferr) - } - return err -} - -func init() { - reexec.Register(runUsingRuntimeCommand, runUsingRuntimeMain) -} - -func runUsingRuntimeMain() { - var options runUsingRuntimeSubprocOptions - // Set logging. - if level := os.Getenv("LOGLEVEL"); level != "" { - if ll, err := strconv.Atoi(level); err == nil { - logrus.SetLevel(logrus.Level(ll)) - } - } - // Unpack our configuration. - confPipe := os.NewFile(3, "confpipe") - if confPipe == nil { - fmt.Fprintf(os.Stderr, "error reading options pipe\n") - os.Exit(1) - } - defer confPipe.Close() - if err := json.NewDecoder(confPipe).Decode(&options); err != nil { - fmt.Fprintf(os.Stderr, "error decoding options: %v\n", err) - os.Exit(1) - } - // Set ourselves up to read the container's exit status. We're doing this in a child process - // so that we won't mess with the setting in a caller of the library. This stubs to OS specific - // calls - if err := setChildProcess(); err != nil { - os.Exit(1) - } - // Run the container, start to finish. - status, err := runUsingRuntime(options.Isolation, options.Options, options.ConfigureNetwork, options.ConfigureNetworks, options.MoreCreateArgs, options.Spec, options.RootPath, options.BundlePath, options.ContainerName) - if err != nil { - fmt.Fprintf(os.Stderr, "error running container: %v\n", err) - os.Exit(1) - } - // Pass the container's exit status back to the caller by exiting with the same status. - if status.Exited() { - os.Exit(status.ExitStatus()) - } else if status.Signaled() { - fmt.Fprintf(os.Stderr, "container exited on %s\n", status.Signal()) - os.Exit(1) - } - os.Exit(1) -} - -func runUsingRuntime(isolation Isolation, options RunOptions, configureNetwork bool, configureNetworks, moreCreateArgs []string, spec *specs.Spec, rootPath, bundlePath, containerName string) (wstatus unix.WaitStatus, err error) { - // Lock the caller to a single OS-level thread. - runtime.LockOSThread() - - // Set up bind mounts for things that a namespaced user might not be able to get to directly. - unmountAll, err := bind.SetupIntermediateMountNamespace(spec, bundlePath) - if unmountAll != nil { - defer func() { - if err := unmountAll(); err != nil { - logrus.Error(err) - } - }() - } - if err != nil { - return 1, err - } - - // Write the runtime configuration. - specbytes, err := json.Marshal(spec) - if err != nil { - return 1, errors.Wrapf(err, "error encoding configuration %#v as json", spec) - } - if err = ioutils.AtomicWriteFile(filepath.Join(bundlePath, "config.json"), specbytes, 0600); err != nil { - return 1, errors.Wrapf(err, "error storing runtime configuration in %q", filepath.Join(bundlePath, "config.json")) - } - - logrus.Debugf("config = %v", string(specbytes)) - - // Decide which runtime to use. - runtime := options.Runtime - if runtime == "" { - runtime = util.Runtime() - } - - // Default to just passing down our stdio. - getCreateStdio := func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { - return os.Stdin, os.Stdout, os.Stderr - } - - // Figure out how we're doing stdio handling, and create pipes and sockets. - var stdio sync.WaitGroup - var consoleListener *net.UnixListener - var errorFds, closeBeforeReadingErrorFds []int - stdioPipe := make([][]int, 3) - copyConsole := false - copyPipes := false - finishCopy := make([]int, 2) - if err = unix.Pipe(finishCopy); err != nil { - return 1, errors.Wrapf(err, "error creating pipe for notifying to stop stdio") - } - finishedCopy := make(chan struct{}) - if spec.Process != nil { - if spec.Process.Terminal { - copyConsole = true - // Create a listening socket for accepting the container's terminal's PTY master. - socketPath := filepath.Join(bundlePath, "console.sock") - consoleListener, err = net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"}) - if err != nil { - return 1, errors.Wrapf(err, "error creating socket %q to receive terminal descriptor", consoleListener.Addr()) - } - // Add console socket arguments. - moreCreateArgs = append(moreCreateArgs, "--console-socket", socketPath) - } else { - copyPipes = true - // Figure out who should own the pipes. - uid, gid, err := util.GetHostRootIDs(spec) - if err != nil { - return 1, err - } - // Create stdio pipes. - if stdioPipe, err = runMakeStdioPipe(int(uid), int(gid)); err != nil { - return 1, err - } - errorFds = []int{stdioPipe[unix.Stdout][0], stdioPipe[unix.Stderr][0]} - closeBeforeReadingErrorFds = []int{stdioPipe[unix.Stdout][1], stdioPipe[unix.Stderr][1]} - // Set stdio to our pipes. - getCreateStdio = func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { - stdin := os.NewFile(uintptr(stdioPipe[unix.Stdin][0]), "/dev/stdin") - stdout := os.NewFile(uintptr(stdioPipe[unix.Stdout][1]), "/dev/stdout") - stderr := os.NewFile(uintptr(stdioPipe[unix.Stderr][1]), "/dev/stderr") - return stdin, stdout, stderr - } - } - } else { - if options.Quiet { - // Discard stdout. - getCreateStdio = func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { - return os.Stdin, nil, os.Stderr - } - } - } - - // Build the commands that we'll execute. - pidFile := filepath.Join(bundlePath, "pid") - args := append(append(append(options.Args, "create", "--bundle", bundlePath, "--pid-file", pidFile), moreCreateArgs...), containerName) - create := exec.Command(runtime, args...) - create.Dir = bundlePath - stdin, stdout, stderr := getCreateStdio() - create.Stdin, create.Stdout, create.Stderr = stdin, stdout, stderr - if create.SysProcAttr == nil { - create.SysProcAttr = &syscall.SysProcAttr{} - } - - args = append(options.Args, "start", containerName) - start := exec.Command(runtime, args...) - start.Dir = bundlePath - start.Stderr = os.Stderr - - args = append(options.Args, "kill", containerName) - kill := exec.Command(runtime, args...) - kill.Dir = bundlePath - kill.Stderr = os.Stderr - - args = append(options.Args, "delete", containerName) - del := exec.Command(runtime, args...) - del.Dir = bundlePath - del.Stderr = os.Stderr - - // Actually create the container. - logrus.Debugf("Running %q", create.Args) - err = create.Run() - if err != nil { - return 1, errors.Wrapf(err, "error creating container for %v: %s", spec.Process.Args, runCollectOutput(errorFds, closeBeforeReadingErrorFds)) - } - defer func() { - err2 := del.Run() - if err2 != nil { - if err == nil { - err = errors.Wrapf(err2, "error deleting container") - } else { - logrus.Infof("error deleting container: %v", err2) - } - } - }() - - // Make sure we read the container's exit status when it exits. - pidValue, err := ioutil.ReadFile(pidFile) - if err != nil { - return 1, errors.Wrapf(err, "error reading pid from %q", pidFile) - } - pid, err := strconv.Atoi(strings.TrimSpace(string(pidValue))) - if err != nil { - return 1, errors.Wrapf(err, "error parsing pid %s as a number", string(pidValue)) - } - var reaping sync.WaitGroup - reaping.Add(1) - go func() { - defer reaping.Done() - var err error - _, err = unix.Wait4(pid, &wstatus, 0, nil) - if err != nil { - wstatus = 0 - logrus.Errorf("error waiting for container child process %d: %v\n", pid, err) - } - }() - - if configureNetwork { - teardown, err := runConfigureNetwork(isolation, options, configureNetworks, pid, containerName, spec.Process.Args) - if teardown != nil { - defer teardown() - } - if err != nil { - return 1, err - } - } - - if copyPipes { - // We don't need the ends of the pipes that belong to the container. - stdin.Close() - if stdout != nil { - stdout.Close() - } - stderr.Close() - } - - // Handle stdio for the container in the background. - stdio.Add(1) - go runCopyStdio(&stdio, copyPipes, stdioPipe, copyConsole, consoleListener, finishCopy, finishedCopy, spec) - - // Start the container. - logrus.Debugf("Running %q", start.Args) - err = start.Run() - if err != nil { - return 1, errors.Wrapf(err, "error starting container") - } - stopped := false - defer func() { - if !stopped { - err2 := kill.Run() - if err2 != nil { - if err == nil { - err = errors.Wrapf(err2, "error stopping container") - } else { - logrus.Infof("error stopping container: %v", err2) - } - } - } - }() - - // Wait for the container to exit. - for { - now := time.Now() - var state specs.State - args = append(options.Args, "state", containerName) - stat := exec.Command(runtime, args...) - stat.Dir = bundlePath - stat.Stderr = os.Stderr - stateOutput, stateErr := stat.Output() - if stateErr != nil { - return 1, errors.Wrapf(stateErr, "error reading container state") - } - if err = json.Unmarshal(stateOutput, &state); err != nil { - return 1, errors.Wrapf(stateErr, "error parsing container state %q", string(stateOutput)) - } - switch state.Status { - case "running": - case "stopped": - stopped = true - default: - return 1, errors.Errorf("container status unexpectedly changed to %q", state.Status) - } - if stopped { - break - } - select { - case <-finishedCopy: - stopped = true - case <-time.After(time.Until(now.Add(100 * time.Millisecond))): - continue - } - if stopped { - break - } - } - - // Close the writing end of the stop-handling-stdio notification pipe. - unix.Close(finishCopy[1]) - // Wait for the stdio copy goroutine to flush. - stdio.Wait() - // Wait until we finish reading the exit status. - reaping.Wait() - - return wstatus, nil -} - -func runCollectOutput(fds, closeBeforeReadingFds []int) string { - for _, fd := range closeBeforeReadingFds { - unix.Close(fd) - } - var b bytes.Buffer - buf := make([]byte, 8192) - for _, fd := range fds { - nread, err := unix.Read(fd, buf) - if err != nil { - if errno, isErrno := err.(syscall.Errno); isErrno { - switch errno { - default: - logrus.Errorf("error reading from pipe %d: %v", fd, err) - case syscall.EINTR, syscall.EAGAIN: - } - } else { - logrus.Errorf("unable to wait for data from pipe %d: %v", fd, err) - } - continue - } - for nread > 0 { - r := buf[:nread] - if nwritten, err := b.Write(r); err != nil || nwritten != len(r) { - if nwritten != len(r) { - logrus.Errorf("error buffering data from pipe %d: %v", fd, err) - break - } - } - nread, err = unix.Read(fd, buf) - if err != nil { - if errno, isErrno := err.(syscall.Errno); isErrno { - switch errno { - default: - logrus.Errorf("error reading from pipe %d: %v", fd, err) - case syscall.EINTR, syscall.EAGAIN: - } - } else { - logrus.Errorf("unable to wait for data from pipe %d: %v", fd, err) - } - break - } - } - } - return b.String() -} -func setupRootlessNetwork(pid int) (teardown func(), err error) { - slirp4netns, err := exec.LookPath("slirp4netns") - if err != nil { - return nil, errors.Wrapf(err, "cannot find slirp4netns") - } - - rootlessSlirpSyncR, rootlessSlirpSyncW, err := os.Pipe() - if err != nil { - return nil, errors.Wrapf(err, "cannot create slirp4netns sync pipe") - } - defer rootlessSlirpSyncR.Close() - - // Be sure there are no fds inherited to slirp4netns except the sync pipe - files, err := ioutil.ReadDir("/proc/self/fd") - if err != nil { - return nil, errors.Wrapf(err, "cannot list open fds") - } - for _, f := range files { - fd, err := strconv.Atoi(f.Name()) - if err != nil { - return nil, errors.Wrapf(err, "cannot parse fd") - } - if fd == int(rootlessSlirpSyncW.Fd()) { - continue - } - unix.CloseOnExec(fd) - } - - cmd := exec.Command(slirp4netns, "--mtu", "65520", "-r", "3", "-c", fmt.Sprintf("%d", pid), "tap0") - cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil - cmd.ExtraFiles = []*os.File{rootlessSlirpSyncW} - - err = cmd.Start() - rootlessSlirpSyncW.Close() - if err != nil { - return nil, errors.Wrapf(err, "cannot start slirp4netns") - } - - b := make([]byte, 1) - for { - if err := rootlessSlirpSyncR.SetDeadline(time.Now().Add(1 * time.Second)); err != nil { - return nil, errors.Wrapf(err, "error setting slirp4netns pipe timeout") - } - if _, err := rootlessSlirpSyncR.Read(b); err == nil { - break - } else { - if os.IsTimeout(err) { - // Check if the process is still running. - var status syscall.WaitStatus - _, err := syscall.Wait4(cmd.Process.Pid, &status, syscall.WNOHANG, nil) - if err != nil { - return nil, errors.Wrapf(err, "failed to read slirp4netns process status") - } - if status.Exited() || status.Signaled() { - return nil, errors.New("slirp4netns failed") - } - - continue - } - return nil, errors.Wrapf(err, "failed to read from slirp4netns sync pipe") - } - } - - return func() { - cmd.Process.Kill() - cmd.Wait() - }, nil -} - -func runConfigureNetwork(isolation Isolation, options RunOptions, configureNetworks []string, pid int, containerName string, command []string) (teardown func(), err error) { - var netconf, undo []*libcni.NetworkConfigList - - if isolation == IsolationOCIRootless { - if ns := options.NamespaceOptions.Find(string(specs.NetworkNamespace)); ns != nil && !ns.Host && ns.Path == "" { - return setupRootlessNetwork(pid) - } - } - // Scan for CNI configuration files. - confdir := options.CNIConfigDir - files, err := libcni.ConfFiles(confdir, []string{".conf"}) - if err != nil { - return nil, errors.Wrapf(err, "error finding CNI networking configuration files named *.conf in directory %q", confdir) - } - lists, err := libcni.ConfFiles(confdir, []string{".conflist"}) - if err != nil { - return nil, errors.Wrapf(err, "error finding CNI networking configuration list files named *.conflist in directory %q", confdir) - } - logrus.Debugf("CNI network configuration file list: %#v", append(files, lists...)) - // Read the CNI configuration files. - for _, file := range files { - nc, err := libcni.ConfFromFile(file) - if err != nil { - return nil, errors.Wrapf(err, "error loading networking configuration from file %q for %v", file, command) - } - if len(configureNetworks) > 0 && nc.Network != nil && (nc.Network.Name == "" || !util.StringInSlice(nc.Network.Name, configureNetworks)) { - if nc.Network.Name == "" { - logrus.Debugf("configuration in %q has no name, skipping it", file) - } else { - logrus.Debugf("configuration in %q has name %q, skipping it", file, nc.Network.Name) - } - continue - } - cl, err := libcni.ConfListFromConf(nc) - if err != nil { - return nil, errors.Wrapf(err, "error converting networking configuration from file %q for %v", file, command) - } - logrus.Debugf("using network configuration from %q", file) - netconf = append(netconf, cl) - } - for _, list := range lists { - cl, err := libcni.ConfListFromFile(list) - if err != nil { - return nil, errors.Wrapf(err, "error loading networking configuration list from file %q for %v", list, command) - } - if len(configureNetworks) > 0 && (cl.Name == "" || !util.StringInSlice(cl.Name, configureNetworks)) { - if cl.Name == "" { - logrus.Debugf("configuration list in %q has no name, skipping it", list) - } else { - logrus.Debugf("configuration list in %q has name %q, skipping it", list, cl.Name) - } - continue - } - logrus.Debugf("using network configuration list from %q", list) - netconf = append(netconf, cl) - } - // Make sure we can access the container's network namespace, - // even after it exits, to successfully tear down the - // interfaces. Ensure this by opening a handle to the network - // namespace, and using our copy to both configure and - // deconfigure it. - netns := fmt.Sprintf("/proc/%d/ns/net", pid) - netFD, err := unix.Open(netns, unix.O_RDONLY, 0) - if err != nil { - return nil, errors.Wrapf(err, "error opening network namespace for %v", command) - } - mynetns := fmt.Sprintf("/proc/%d/fd/%d", unix.Getpid(), netFD) - // Build our search path for the plugins. - pluginPaths := strings.Split(options.CNIPluginPath, string(os.PathListSeparator)) - cni := libcni.CNIConfig{Path: pluginPaths} - // Configure the interfaces. - rtconf := make(map[*libcni.NetworkConfigList]*libcni.RuntimeConf) - teardown = func() { - for _, nc := range undo { - if err = cni.DelNetworkList(context.Background(), nc, rtconf[nc]); err != nil { - logrus.Errorf("error cleaning up network %v for %v: %v", rtconf[nc].IfName, command, err) - } - } - unix.Close(netFD) - } - for i, nc := range netconf { - // Build the runtime config for use with this network configuration. - rtconf[nc] = &libcni.RuntimeConf{ - ContainerID: containerName, - NetNS: mynetns, - IfName: fmt.Sprintf("if%d", i), - Args: [][2]string{}, - CapabilityArgs: map[string]interface{}{}, - } - // Bring it up. - _, err := cni.AddNetworkList(context.Background(), nc, rtconf[nc]) - if err != nil { - return teardown, errors.Wrapf(err, "error configuring network list %v for %v", rtconf[nc].IfName, command) - } - // Add it to the list of networks to take down when the container process exits. - undo = append([]*libcni.NetworkConfigList{nc}, undo...) - } - return teardown, nil -} - -func runCopyStdio(stdio *sync.WaitGroup, copyPipes bool, stdioPipe [][]int, copyConsole bool, consoleListener *net.UnixListener, finishCopy []int, finishedCopy chan struct{}, spec *specs.Spec) { - defer func() { - unix.Close(finishCopy[0]) - if copyPipes { - unix.Close(stdioPipe[unix.Stdin][1]) - unix.Close(stdioPipe[unix.Stdout][0]) - unix.Close(stdioPipe[unix.Stderr][0]) - } - stdio.Done() - finishedCopy <- struct{}{} - }() - // Map describing where data on an incoming descriptor should go. - relayMap := make(map[int]int) - // Map describing incoming and outgoing descriptors. - readDesc := make(map[int]string) - writeDesc := make(map[int]string) - // Buffers. - relayBuffer := make(map[int]*bytes.Buffer) - // Set up the terminal descriptor or pipes for polling. - if copyConsole { - // Accept a connection over our listening socket. - fd, err := runAcceptTerminal(consoleListener, spec.Process.ConsoleSize) - if err != nil { - logrus.Errorf("%v", err) - return - } - terminalFD := fd - // Input from our stdin, output from the terminal descriptor. - relayMap[unix.Stdin] = terminalFD - readDesc[unix.Stdin] = "stdin" - relayBuffer[terminalFD] = new(bytes.Buffer) - writeDesc[terminalFD] = "container terminal input" - relayMap[terminalFD] = unix.Stdout - readDesc[terminalFD] = "container terminal output" - relayBuffer[unix.Stdout] = new(bytes.Buffer) - writeDesc[unix.Stdout] = "output" - // Set our terminal's mode to raw, to pass handling of special - // terminal input to the terminal in the container. - if terminal.IsTerminal(unix.Stdin) { - if state, err := terminal.MakeRaw(unix.Stdin); err != nil { - logrus.Warnf("error setting terminal state: %v", err) - } else { - defer func() { - if err = terminal.Restore(unix.Stdin, state); err != nil { - logrus.Errorf("unable to restore terminal state: %v", err) - } - }() - } - } - } - if copyPipes { - // Input from our stdin, output from the stdout and stderr pipes. - relayMap[unix.Stdin] = stdioPipe[unix.Stdin][1] - readDesc[unix.Stdin] = "stdin" - relayBuffer[stdioPipe[unix.Stdin][1]] = new(bytes.Buffer) - writeDesc[stdioPipe[unix.Stdin][1]] = "container stdin" - relayMap[stdioPipe[unix.Stdout][0]] = unix.Stdout - readDesc[stdioPipe[unix.Stdout][0]] = "container stdout" - relayBuffer[unix.Stdout] = new(bytes.Buffer) - writeDesc[unix.Stdout] = "stdout" - relayMap[stdioPipe[unix.Stderr][0]] = unix.Stderr - readDesc[stdioPipe[unix.Stderr][0]] = "container stderr" - relayBuffer[unix.Stderr] = new(bytes.Buffer) - writeDesc[unix.Stderr] = "stderr" - } - // Set our reading descriptors to non-blocking. - for rfd, wfd := range relayMap { - if err := unix.SetNonblock(rfd, true); err != nil { - logrus.Errorf("error setting %s to nonblocking: %v", readDesc[rfd], err) - return - } - if err := unix.SetNonblock(wfd, false); err != nil { - logrus.Errorf("error setting descriptor %d (%s) blocking: %v", wfd, writeDesc[wfd], err) - } - } - // Pass data back and forth. - pollTimeout := -1 - for len(relayMap) > 0 { - // Start building the list of descriptors to poll. - pollFds := make([]unix.PollFd, 0, len(relayMap)+1) - // Poll for a notification that we should stop handling stdio. - pollFds = append(pollFds, unix.PollFd{Fd: int32(finishCopy[0]), Events: unix.POLLIN | unix.POLLHUP}) - // Poll on our reading descriptors. - for rfd := range relayMap { - pollFds = append(pollFds, unix.PollFd{Fd: int32(rfd), Events: unix.POLLIN | unix.POLLHUP}) - } - buf := make([]byte, 8192) - // Wait for new data from any input descriptor, or a notification that we're done. - _, err := unix.Poll(pollFds, pollTimeout) - if !util.LogIfNotRetryable(err, fmt.Sprintf("error waiting for stdio/terminal data to relay: %v", err)) { - return - } - removes := make(map[int]struct{}) - for _, pollFd := range pollFds { - // If this descriptor's just been closed from the other end, mark it for - // removal from the set that we're checking for. - if pollFd.Revents&unix.POLLHUP == unix.POLLHUP { - removes[int(pollFd.Fd)] = struct{}{} - } - // If the descriptor was closed elsewhere, remove it from our list. - if pollFd.Revents&unix.POLLNVAL != 0 { - logrus.Debugf("error polling descriptor %s: closed?", readDesc[int(pollFd.Fd)]) - removes[int(pollFd.Fd)] = struct{}{} - } - // If the POLLIN flag isn't set, then there's no data to be read from this descriptor. - if pollFd.Revents&unix.POLLIN == 0 { - // If we're using pipes and it's our stdin and it's closed, close the writing - // end of the corresponding pipe. - if copyPipes && int(pollFd.Fd) == unix.Stdin && pollFd.Revents&unix.POLLHUP != 0 { - unix.Close(stdioPipe[unix.Stdin][1]) - stdioPipe[unix.Stdin][1] = -1 - } - continue - } - // Read whatever there is to be read. - readFD := int(pollFd.Fd) - writeFD, needToRelay := relayMap[readFD] - if needToRelay { - n, err := unix.Read(readFD, buf) - if !util.LogIfNotRetryable(err, fmt.Sprintf("unable to read %s data: %v", readDesc[readFD], err)) { - return - } - // If it's zero-length on our stdin and we're - // using pipes, it's an EOF, so close the stdin - // pipe's writing end. - if n == 0 && copyPipes && int(pollFd.Fd) == unix.Stdin { - unix.Close(stdioPipe[unix.Stdin][1]) - stdioPipe[unix.Stdin][1] = -1 - } - if n > 0 { - // Buffer the data in case we get blocked on where they need to go. - nwritten, err := relayBuffer[writeFD].Write(buf[:n]) - if err != nil { - logrus.Debugf("buffer: %v", err) - continue - } - if nwritten != n { - logrus.Debugf("buffer: expected to buffer %d bytes, wrote %d", n, nwritten) - continue - } - // If this is the last of the data we'll be able to read from this - // descriptor, read all that there is to read. - for pollFd.Revents&unix.POLLHUP == unix.POLLHUP { - nr, err := unix.Read(readFD, buf) - util.LogIfUnexpectedWhileDraining(err, fmt.Sprintf("read %s: %v", readDesc[readFD], err)) - if nr <= 0 { - break - } - nwritten, err := relayBuffer[writeFD].Write(buf[:nr]) - if err != nil { - logrus.Debugf("buffer: %v", err) - break - } - if nwritten != nr { - logrus.Debugf("buffer: expected to buffer %d bytes, wrote %d", nr, nwritten) - break - } - } - } - } - } - // Try to drain the output buffers. Set the default timeout - // for the next poll() to 100ms if we still have data to write. - pollTimeout = -1 - for writeFD := range relayBuffer { - if relayBuffer[writeFD].Len() > 0 { - n, err := unix.Write(writeFD, relayBuffer[writeFD].Bytes()) - if !util.LogIfNotRetryable(err, fmt.Sprintf("unable to write %s data: %v", writeDesc[writeFD], err)) { - return - } - if n > 0 { - relayBuffer[writeFD].Next(n) - } - } - if relayBuffer[writeFD].Len() > 0 { - pollTimeout = 100 - } - } - // Remove any descriptors which we don't need to poll any more from the poll descriptor list. - for remove := range removes { - delete(relayMap, remove) - } - // If the we-can-return pipe had anything for us, we're done. - for _, pollFd := range pollFds { - if int(pollFd.Fd) == finishCopy[0] && pollFd.Revents != 0 { - // The pipe is closed, indicating that we can stop now. - return - } - } - } -} - -func runAcceptTerminal(consoleListener *net.UnixListener, terminalSize *specs.Box) (int, error) { - defer consoleListener.Close() - c, err := consoleListener.AcceptUnix() - if err != nil { - return -1, errors.Wrapf(err, "error accepting socket descriptor connection") - } - defer c.Close() - // Expect a control message over our new connection. - b := make([]byte, 8192) - oob := make([]byte, 8192) - n, oobn, _, _, err := c.ReadMsgUnix(b, oob) - if err != nil { - return -1, errors.Wrapf(err, "error reading socket descriptor") - } - if n > 0 { - logrus.Debugf("socket descriptor is for %q", string(b[:n])) - } - if oobn > len(oob) { - return -1, errors.Errorf("too much out-of-bounds data (%d bytes)", oobn) - } - // Parse the control message. - scm, err := unix.ParseSocketControlMessage(oob[:oobn]) - if err != nil { - return -1, errors.Wrapf(err, "error parsing out-of-bound data as a socket control message") - } - logrus.Debugf("control messages: %v", scm) - // Expect to get a descriptor. - terminalFD := -1 - for i := range scm { - fds, err := unix.ParseUnixRights(&scm[i]) - if err != nil { - return -1, errors.Wrapf(err, "error parsing unix rights control message: %v", &scm[i]) - } - logrus.Debugf("fds: %v", fds) - if len(fds) == 0 { - continue - } - terminalFD = fds[0] - break - } - if terminalFD == -1 { - return -1, errors.Errorf("unable to read terminal descriptor") - } - // Set the pseudoterminal's size to the configured size, or our own. - winsize := &unix.Winsize{} - if terminalSize != nil { - // Use configured sizes. - winsize.Row = uint16(terminalSize.Height) - winsize.Col = uint16(terminalSize.Width) - } else { - if terminal.IsTerminal(unix.Stdin) { - // Use the size of our terminal. - if winsize, err = unix.IoctlGetWinsize(unix.Stdin, unix.TIOCGWINSZ); err != nil { - logrus.Warnf("error reading size of controlling terminal: %v", err) - winsize.Row = 0 - winsize.Col = 0 - } - } - } - if winsize.Row != 0 && winsize.Col != 0 { - if err = unix.IoctlSetWinsize(terminalFD, unix.TIOCSWINSZ, winsize); err != nil { - logrus.Warnf("error setting size of container pseudoterminal: %v", err) - } - // FIXME - if we're connected to a terminal, we should - // be passing the updated terminal size down when we - // receive a SIGWINCH. - } - return terminalFD, nil -} - -// Create pipes to use for relaying stdio. -func runMakeStdioPipe(uid, gid int) ([][]int, error) { - stdioPipe := make([][]int, 3) - for i := range stdioPipe { - stdioPipe[i] = make([]int, 2) - if err := unix.Pipe(stdioPipe[i]); err != nil { - return nil, errors.Wrapf(err, "error creating pipe for container FD %d", i) - } - } - if err := unix.Fchown(stdioPipe[unix.Stdin][0], uid, gid); err != nil { - return nil, errors.Wrapf(err, "error setting owner of stdin pipe descriptor") - } - if err := unix.Fchown(stdioPipe[unix.Stdout][1], uid, gid); err != nil { - return nil, errors.Wrapf(err, "error setting owner of stdout pipe descriptor") - } - if err := unix.Fchown(stdioPipe[unix.Stderr][1], uid, gid); err != nil { - return nil, errors.Wrapf(err, "error setting owner of stderr pipe descriptor") - } - return stdioPipe, nil -} diff --git a/vendor/github.com/containers/buildah/run_linux.go b/vendor/github.com/containers/buildah/run_linux.go index a7519a092..1acf655eb 100644 --- a/vendor/github.com/containers/buildah/run_linux.go +++ b/vendor/github.com/containers/buildah/run_linux.go @@ -3,9 +3,44 @@ package buildah import ( + "bytes" + "context" + "encoding/json" "fmt" - "golang.org/x/sys/unix" + "io" + "io/ioutil" + "net" "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/containernetworking/cni/libcni" + "github.com/containers/buildah/bind" + "github.com/containers/buildah/chroot" + "github.com/containers/buildah/pkg/secrets" + "github.com/containers/buildah/pkg/unshare" + "github.com/containers/buildah/util" + "github.com/containers/storage/pkg/idtools" + "github.com/containers/storage/pkg/ioutils" + "github.com/containers/storage/pkg/reexec" + "github.com/containers/storage/pkg/stringid" + "github.com/docker/go-units" + "github.com/docker/libnetwork/resolvconf" + "github.com/docker/libnetwork/types" + "github.com/opencontainers/go-digest" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/generate" + "github.com/opencontainers/selinux/go-selinux/label" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh/terminal" + "golang.org/x/sys/unix" ) func setChildProcess() error { @@ -15,3 +50,1988 @@ func setChildProcess() error { } return nil } + +// Run runs the specified command in the container's root filesystem. +func (b *Builder) Run(command []string, options RunOptions) error { + p, err := ioutil.TempDir("", Package) + if err != nil { + return errors.Wrapf(err, "run: error creating temporary directory under %q", os.TempDir()) + } + // On some hosts like AH, /tmp is a symlink and we need an + // absolute path. + path, err := filepath.EvalSymlinks(p) + if err != nil { + return errors.Wrapf(err, "run: error evaluating %q for symbolic links", p) + } + logrus.Debugf("using %q to hold bundle data", path) + defer func() { + if err2 := os.RemoveAll(path); err2 != nil { + logrus.Errorf("error removing %q: %v", path, err2) + } + }() + + gp, err := generate.New("linux") + if err != nil { + return errors.Wrapf(err, "error generating new 'linux' runtime spec") + } + g := &gp + + isolation := options.Isolation + if isolation == IsolationDefault { + isolation = b.Isolation + if isolation == IsolationDefault { + isolation = IsolationOCI + } + } + if err := checkAndOverrideIsolationOptions(isolation, &options); err != nil { + return err + } + + b.configureEnvironment(g, options) + + if b.CommonBuildOpts == nil { + return errors.Errorf("Invalid format on container you must recreate the container") + } + + if err := addCommonOptsToSpec(b.CommonBuildOpts, g); err != nil { + return err + } + + if options.WorkingDir != "" { + g.SetProcessCwd(options.WorkingDir) + } else if b.WorkDir() != "" { + g.SetProcessCwd(b.WorkDir()) + } + setupSelinux(g, b.ProcessLabel, b.MountLabel) + mountPoint, err := b.Mount(b.MountLabel) + if err != nil { + return errors.Wrapf(err, "error mounting container %q", b.ContainerID) + } + defer func() { + if err := b.Unmount(); err != nil { + logrus.Errorf("error unmounting container: %v", err) + } + }() + g.SetRootPath(mountPoint) + if len(command) > 0 { + command = runLookupPath(g, command) + g.SetProcessArgs(command) + } else { + g.SetProcessArgs(nil) + } + + setupMaskedPaths(g) + setupReadOnlyPaths(g) + + setupTerminal(g, options.Terminal, options.TerminalSize) + + configureNetwork, configureNetworks, err := b.configureNamespaces(g, options) + if err != nil { + return err + } + + if err := b.configureUIDGID(g, mountPoint, options); err != nil { + return err + } + + g.SetProcessApparmorProfile(b.CommonBuildOpts.ApparmorProfile) + + // Now grab the spec from the generator. Set the generator to nil so that future contributors + // will quickly be able to tell that they're supposed to be modifying the spec directly from here. + spec := g.Config + g = nil + + logrus.Debugf("ensuring working directory %q exists", filepath.Join(mountPoint, spec.Process.Cwd)) + if err = os.MkdirAll(filepath.Join(mountPoint, spec.Process.Cwd), 0755); err != nil && !os.IsExist(err) { + return errors.Wrapf(err, "error ensuring working directory %q exists", spec.Process.Cwd) + } + + // Set the seccomp configuration using the specified profile name. Some syscalls are + // allowed if certain capabilities are to be granted (example: CAP_SYS_CHROOT and chroot), + // so we sorted out the capabilities lists first. + if err = setupSeccomp(spec, b.CommonBuildOpts.SeccompProfilePath); err != nil { + return err + } + + // Figure out who owns files that will appear to be owned by UID/GID 0 in the container. + rootUID, rootGID, err := util.GetHostRootIDs(spec) + if err != nil { + return err + } + rootIDPair := &idtools.IDPair{UID: int(rootUID), GID: int(rootGID)} + + bindFiles := make(map[string]string) + namespaceOptions := append(b.NamespaceOptions, options.NamespaceOptions...) + volumes := b.Volumes() + + if !contains(volumes, "/etc/hosts") { + hostFile, err := b.generateHosts(path, spec.Hostname, b.CommonBuildOpts.AddHost, rootIDPair) + if err != nil { + return err + } + bindFiles["/etc/hosts"] = hostFile + } + + if !contains(volumes, "/etc/resolv.conf") { + resolvFile, err := b.addNetworkConfig(path, "/etc/resolv.conf", rootIDPair, b.CommonBuildOpts.DNSServers, b.CommonBuildOpts.DNSSearch, b.CommonBuildOpts.DNSOptions) + if err != nil { + return err + } + bindFiles["/etc/resolv.conf"] = resolvFile + } + + err = b.setupMounts(mountPoint, spec, path, options.Mounts, bindFiles, volumes, b.CommonBuildOpts.Volumes, b.CommonBuildOpts.ShmSize, namespaceOptions) + if err != nil { + return errors.Wrapf(err, "error resolving mountpoints for container %q", b.ContainerID) + } + + if options.CNIConfigDir == "" { + options.CNIConfigDir = b.CNIConfigDir + if b.CNIConfigDir == "" { + options.CNIConfigDir = util.DefaultCNIConfigDir + } + } + if options.CNIPluginPath == "" { + options.CNIPluginPath = b.CNIPluginPath + if b.CNIPluginPath == "" { + options.CNIPluginPath = util.DefaultCNIPluginPath + } + } + + switch isolation { + case IsolationOCI: + var moreCreateArgs []string + if options.NoPivot { + moreCreateArgs = []string{"--no-pivot"} + } else { + moreCreateArgs = nil + } + err = b.runUsingRuntimeSubproc(isolation, options, configureNetwork, configureNetworks, moreCreateArgs, spec, mountPoint, path, Package+"-"+filepath.Base(path)) + case IsolationChroot: + err = chroot.RunUsingChroot(spec, path, options.Stdin, options.Stdout, options.Stderr) + case IsolationOCIRootless: + moreCreateArgs := []string{"--no-new-keyring"} + if options.NoPivot { + moreCreateArgs = append(moreCreateArgs, "--no-pivot") + } + if err := setupRootlessSpecChanges(spec, path, rootUID, rootGID); err != nil { + return err + } + err = b.runUsingRuntimeSubproc(isolation, options, configureNetwork, configureNetworks, moreCreateArgs, spec, mountPoint, path, Package+"-"+filepath.Base(path)) + default: + err = errors.Errorf("don't know how to run this command") + } + return err +} + +func addCommonOptsToSpec(commonOpts *CommonBuildOptions, g *generate.Generator) error { + // Resources - CPU + if commonOpts.CPUPeriod != 0 { + g.SetLinuxResourcesCPUPeriod(commonOpts.CPUPeriod) + } + if commonOpts.CPUQuota != 0 { + g.SetLinuxResourcesCPUQuota(commonOpts.CPUQuota) + } + if commonOpts.CPUShares != 0 { + g.SetLinuxResourcesCPUShares(commonOpts.CPUShares) + } + if commonOpts.CPUSetCPUs != "" { + g.SetLinuxResourcesCPUCpus(commonOpts.CPUSetCPUs) + } + if commonOpts.CPUSetMems != "" { + g.SetLinuxResourcesCPUMems(commonOpts.CPUSetMems) + } + + // Resources - Memory + if commonOpts.Memory != 0 { + g.SetLinuxResourcesMemoryLimit(commonOpts.Memory) + } + if commonOpts.MemorySwap != 0 { + g.SetLinuxResourcesMemorySwap(commonOpts.MemorySwap) + } + + // cgroup membership + if commonOpts.CgroupParent != "" { + g.SetLinuxCgroupsPath(commonOpts.CgroupParent) + } + + // Other process resource limits + if err := addRlimits(commonOpts.Ulimit, g); err != nil { + return err + } + + logrus.Debugf("Resources: %#v", commonOpts) + return nil +} + +func runSetupBuiltinVolumes(mountLabel, mountPoint, containerDir string, copyWithTar func(srcPath, dstPath string) error, builtinVolumes []string, rootUID, rootGID int) ([]specs.Mount, error) { + var mounts []specs.Mount + hostOwner := idtools.IDPair{UID: rootUID, GID: rootGID} + // Add temporary copies of the contents of volume locations at the + // volume locations, unless we already have something there. + for _, volume := range builtinVolumes { + subdir := digest.Canonical.FromString(volume).Hex() + volumePath := filepath.Join(containerDir, "buildah-volumes", subdir) + srcPath := filepath.Join(mountPoint, volume) + initializeVolume := false + // If we need to, initialize the volume path's initial contents. + if _, err := os.Stat(volumePath); err != nil { + if !os.IsNotExist(err) { + return nil, errors.Wrapf(err, "failed to stat %q for volume %q", volumePath, volume) + } + logrus.Debugf("setting up built-in volume at %q", volumePath) + if err = os.MkdirAll(volumePath, 0755); err != nil { + return nil, errors.Wrapf(err, "error creating directory %q for volume %q", volumePath, volume) + } + if err = label.Relabel(volumePath, mountLabel, false); err != nil { + return nil, errors.Wrapf(err, "error relabeling directory %q for volume %q", volumePath, volume) + } + initializeVolume = true + } + stat, err := os.Stat(srcPath) + if err != nil { + if !os.IsNotExist(err) { + return nil, errors.Wrapf(err, "failed to stat %q for volume %q", srcPath, volume) + } + if err = idtools.MkdirAllAndChownNew(srcPath, 0755, hostOwner); err != nil { + return nil, errors.Wrapf(err, "error creating directory %q for volume %q", srcPath, volume) + } + if stat, err = os.Stat(srcPath); err != nil { + return nil, errors.Wrapf(err, "failed to stat %q for volume %q", srcPath, volume) + } + } + if initializeVolume { + if err = os.Chmod(volumePath, stat.Mode().Perm()); err != nil { + return nil, errors.Wrapf(err, "failed to chmod %q for volume %q", volumePath, volume) + } + if err = os.Chown(volumePath, int(stat.Sys().(*syscall.Stat_t).Uid), int(stat.Sys().(*syscall.Stat_t).Gid)); err != nil { + return nil, errors.Wrapf(err, "error chowning directory %q for volume %q", volumePath, volume) + } + if err = copyWithTar(srcPath, volumePath); err != nil && !os.IsNotExist(errors.Cause(err)) { + return nil, errors.Wrapf(err, "error populating directory %q for volume %q using contents of %q", volumePath, volume, srcPath) + } + } + // Add the bind mount. + mounts = append(mounts, specs.Mount{ + Source: volumePath, + Destination: volume, + Type: "bind", + Options: []string{"bind"}, + }) + } + return mounts, nil +} + +func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath string, optionMounts []specs.Mount, bindFiles map[string]string, builtinVolumes, volumeMounts []string, shmSize string, namespaceOptions NamespaceOptions) error { + // Start building a new list of mounts. + var mounts []specs.Mount + haveMount := func(destination string) bool { + for _, mount := range mounts { + if mount.Destination == destination { + // Already have something to mount there. + return true + } + } + return false + } + + ipc := namespaceOptions.Find(string(specs.IPCNamespace)) + hostIPC := ipc == nil || ipc.Host + net := namespaceOptions.Find(string(specs.NetworkNamespace)) + hostNetwork := net == nil || net.Host + user := namespaceOptions.Find(string(specs.UserNamespace)) + hostUser := user == nil || user.Host + + // Copy mounts from the generated list. + mountCgroups := true + specMounts := []specs.Mount{} + for _, specMount := range spec.Mounts { + // Override some of the mounts from the generated list if we're doing different things with namespaces. + if specMount.Destination == "/dev/shm" { + specMount.Options = []string{"nosuid", "noexec", "nodev", "mode=1777", "size=" + shmSize} + if hostIPC && !hostUser { + if _, err := os.Stat("/dev/shm"); err != nil && os.IsNotExist(err) { + logrus.Debugf("/dev/shm is not present, not binding into container") + continue + } + specMount = specs.Mount{ + Source: "/dev/shm", + Type: "bind", + Destination: "/dev/shm", + Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev"}, + } + } + } + if specMount.Destination == "/dev/mqueue" { + if hostIPC && !hostUser { + if _, err := os.Stat("/dev/mqueue"); err != nil && os.IsNotExist(err) { + logrus.Debugf("/dev/mqueue is not present, not binding into container") + continue + } + specMount = specs.Mount{ + Source: "/dev/mqueue", + Type: "bind", + Destination: "/dev/mqueue", + Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev"}, + } + } + } + if specMount.Destination == "/sys" { + if hostNetwork && !hostUser { + mountCgroups = false + if _, err := os.Stat("/sys"); err != nil && os.IsNotExist(err) { + logrus.Debugf("/sys is not present, not binding into container") + continue + } + specMount = specs.Mount{ + Source: "/sys", + Type: "bind", + Destination: "/sys", + Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev", "ro"}, + } + } + } + specMounts = append(specMounts, specMount) + } + + // Add a mount for the cgroups filesystem, unless we're already + // recursively bind mounting all of /sys, in which case we shouldn't + // bother with it. + sysfsMount := []specs.Mount{} + if mountCgroups { + sysfsMount = []specs.Mount{{ + Destination: "/sys/fs/cgroup", + Type: "cgroup", + Source: "cgroup", + Options: []string{bind.NoBindOption, "nosuid", "noexec", "nodev", "relatime", "ro"}, + }} + } + + // Get the list of files we need to bind into the container. + bindFileMounts, err := runSetupBoundFiles(bundlePath, bindFiles) + if err != nil { + return err + } + + // After this point we need to know the per-container persistent storage directory. + cdir, err := b.store.ContainerDirectory(b.ContainerID) + if err != nil { + return errors.Wrapf(err, "error determining work directory for container %q", b.ContainerID) + } + + // Figure out which UID and GID to tell the secrets package to use + // for files that it creates. + rootUID, rootGID, err := util.GetHostRootIDs(spec) + if err != nil { + return err + } + + // Get the list of secrets mounts. + secretMounts := secrets.SecretMountsWithUIDGID(b.MountLabel, cdir, b.DefaultMountsFilePath, cdir, int(rootUID), int(rootGID), unshare.IsRootless()) + + // Add temporary copies of the contents of volume locations at the + // volume locations, unless we already have something there. + copyWithTar := b.copyWithTar(nil, nil) + builtins, err := runSetupBuiltinVolumes(b.MountLabel, mountPoint, cdir, copyWithTar, builtinVolumes, int(rootUID), int(rootGID)) + if err != nil { + return err + } + + // Get the list of explicitly-specified volume mounts. + volumes, err := runSetupVolumeMounts(spec.Linux.MountLabel, volumeMounts, optionMounts) + if err != nil { + return err + } + + // Add them all, in the preferred order, except where they conflict with something that was previously added. + for _, mount := range append(append(append(append(append(volumes, builtins...), secretMounts...), bindFileMounts...), specMounts...), sysfsMount...) { + if haveMount(mount.Destination) { + // Already mounting something there, no need to bother with this one. + continue + } + // Add the mount. + mounts = append(mounts, mount) + } + + // Set the list in the spec. + spec.Mounts = mounts + return nil +} + +// addNetworkConfig copies files from host and sets them up to bind mount into container +func (b *Builder) addNetworkConfig(rdir, hostPath string, chownOpts *idtools.IDPair, dnsServers, dnsSearch, dnsOptions []string) (string, error) { + stat, err := os.Stat(hostPath) + if err != nil { + return "", errors.Wrapf(err, "error statting %q for container %q", hostPath, b.ContainerID) + } + contents, err := ioutil.ReadFile(hostPath) + if err != nil { + return "", errors.Wrapf(err, "unable to read %s", hostPath) + } + + search := resolvconf.GetSearchDomains(contents) + nameservers := resolvconf.GetNameservers(contents, types.IP) + options := resolvconf.GetOptions(contents) + + if len(dnsSearch) > 0 { + search = dnsSearch + } + if len(dnsServers) != 0 { + dns, err := getDNSIP(dnsServers) + if err != nil { + return "", errors.Wrapf(err, "error getting dns servers") + } + nameservers = []string{} + for _, server := range dns { + nameservers = append(nameservers, server.String()) + } + } + + if len(dnsOptions) != 0 { + options = dnsOptions + } + + cfile := filepath.Join(rdir, filepath.Base(hostPath)) + if _, err = resolvconf.Build(cfile, nameservers, search, options); err != nil { + return "", errors.Wrapf(err, "error building resolv.conf for container %s", b.ContainerID) + } + + uid := int(stat.Sys().(*syscall.Stat_t).Uid) + gid := int(stat.Sys().(*syscall.Stat_t).Gid) + if chownOpts != nil { + uid = chownOpts.UID + gid = chownOpts.GID + } + if err = os.Chown(cfile, uid, gid); err != nil { + return "", errors.Wrapf(err, "error chowning file %q for container %q", cfile, b.ContainerID) + } + + if err := label.Relabel(cfile, b.MountLabel, false); err != nil { + return "", errors.Wrapf(err, "error relabeling %q in container %q", cfile, b.ContainerID) + } + + return cfile, nil +} + +// generateHosts creates a containers hosts file +func (b *Builder) generateHosts(rdir, hostname string, addHosts []string, chownOpts *idtools.IDPair) (string, error) { + hostPath := "/etc/hosts" + stat, err := os.Stat(hostPath) + if err != nil { + return "", errors.Wrapf(err, "error statting %q for container %q", hostPath, b.ContainerID) + } + + hosts := bytes.NewBufferString("# Generated by Buildah\n") + orig, err := ioutil.ReadFile(hostPath) + if err != nil { + return "", errors.Wrapf(err, "unable to read %s", hostPath) + } + hosts.Write(orig) + for _, host := range addHosts { + // verify the host format + values := strings.SplitN(host, ":", 2) + if len(values) != 2 { + return "", errors.Errorf("unable to parse host entry %q: incorrect format", host) + } + if values[0] == "" { + return "", errors.Errorf("hostname in host entry %q is empty", host) + } + if values[1] == "" { + return "", errors.Errorf("IP address in host entry %q is empty", host) + } + hosts.Write([]byte(fmt.Sprintf("%s\t%s\n", values[1], values[0]))) + } + + if hostname != "" { + hosts.Write([]byte(fmt.Sprintf("127.0.0.1 %s\n", hostname))) + hosts.Write([]byte(fmt.Sprintf("::1 %s\n", hostname))) + } + cfile := filepath.Join(rdir, filepath.Base(hostPath)) + if err = ioutils.AtomicWriteFile(cfile, hosts.Bytes(), stat.Mode().Perm()); err != nil { + return "", errors.Wrapf(err, "error writing /etc/hosts into the container") + } + uid := int(stat.Sys().(*syscall.Stat_t).Uid) + gid := int(stat.Sys().(*syscall.Stat_t).Gid) + if chownOpts != nil { + uid = chownOpts.UID + gid = chownOpts.GID + } + if err = os.Chown(cfile, uid, gid); err != nil { + return "", errors.Wrapf(err, "error chowning file %q for container %q", cfile, b.ContainerID) + } + if err := label.Relabel(cfile, b.MountLabel, false); err != nil { + return "", errors.Wrapf(err, "error relabeling %q in container %q", cfile, b.ContainerID) + } + + return cfile, nil +} + +func setupTerminal(g *generate.Generator, terminalPolicy TerminalPolicy, terminalSize *specs.Box) { + switch terminalPolicy { + case DefaultTerminal: + onTerminal := terminal.IsTerminal(unix.Stdin) && terminal.IsTerminal(unix.Stdout) && terminal.IsTerminal(unix.Stderr) + if onTerminal { + logrus.Debugf("stdio is a terminal, defaulting to using a terminal") + } else { + logrus.Debugf("stdio is not a terminal, defaulting to not using a terminal") + } + g.SetProcessTerminal(onTerminal) + case WithTerminal: + g.SetProcessTerminal(true) + case WithoutTerminal: + g.SetProcessTerminal(false) + } + if terminalSize != nil { + g.SetProcessConsoleSize(terminalSize.Width, terminalSize.Height) + } +} + +func runUsingRuntime(isolation Isolation, options RunOptions, configureNetwork bool, configureNetworks, moreCreateArgs []string, spec *specs.Spec, rootPath, bundlePath, containerName string) (wstatus unix.WaitStatus, err error) { + // Lock the caller to a single OS-level thread. + runtime.LockOSThread() + + // Set up bind mounts for things that a namespaced user might not be able to get to directly. + unmountAll, err := bind.SetupIntermediateMountNamespace(spec, bundlePath) + if unmountAll != nil { + defer func() { + if err := unmountAll(); err != nil { + logrus.Error(err) + } + }() + } + if err != nil { + return 1, err + } + + // Write the runtime configuration. + specbytes, err := json.Marshal(spec) + if err != nil { + return 1, errors.Wrapf(err, "error encoding configuration %#v as json", spec) + } + if err = ioutils.AtomicWriteFile(filepath.Join(bundlePath, "config.json"), specbytes, 0600); err != nil { + return 1, errors.Wrapf(err, "error storing runtime configuration in %q", filepath.Join(bundlePath, "config.json")) + } + + logrus.Debugf("config = %v", string(specbytes)) + + // Decide which runtime to use. + runtime := options.Runtime + if runtime == "" { + runtime = util.Runtime() + } + + // Default to just passing down our stdio. + getCreateStdio := func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { + return os.Stdin, os.Stdout, os.Stderr + } + + // Figure out how we're doing stdio handling, and create pipes and sockets. + var stdio sync.WaitGroup + var consoleListener *net.UnixListener + var errorFds, closeBeforeReadingErrorFds []int + stdioPipe := make([][]int, 3) + copyConsole := false + copyPipes := false + finishCopy := make([]int, 2) + if err = unix.Pipe(finishCopy); err != nil { + return 1, errors.Wrapf(err, "error creating pipe for notifying to stop stdio") + } + finishedCopy := make(chan struct{}) + if spec.Process != nil { + if spec.Process.Terminal { + copyConsole = true + // Create a listening socket for accepting the container's terminal's PTY master. + socketPath := filepath.Join(bundlePath, "console.sock") + consoleListener, err = net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"}) + if err != nil { + return 1, errors.Wrapf(err, "error creating socket %q to receive terminal descriptor", consoleListener.Addr()) + } + // Add console socket arguments. + moreCreateArgs = append(moreCreateArgs, "--console-socket", socketPath) + } else { + copyPipes = true + // Figure out who should own the pipes. + uid, gid, err := util.GetHostRootIDs(spec) + if err != nil { + return 1, err + } + // Create stdio pipes. + if stdioPipe, err = runMakeStdioPipe(int(uid), int(gid)); err != nil { + return 1, err + } + errorFds = []int{stdioPipe[unix.Stdout][0], stdioPipe[unix.Stderr][0]} + closeBeforeReadingErrorFds = []int{stdioPipe[unix.Stdout][1], stdioPipe[unix.Stderr][1]} + // Set stdio to our pipes. + getCreateStdio = func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { + stdin := os.NewFile(uintptr(stdioPipe[unix.Stdin][0]), "/dev/stdin") + stdout := os.NewFile(uintptr(stdioPipe[unix.Stdout][1]), "/dev/stdout") + stderr := os.NewFile(uintptr(stdioPipe[unix.Stderr][1]), "/dev/stderr") + return stdin, stdout, stderr + } + } + } else { + if options.Quiet { + // Discard stdout. + getCreateStdio = func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { + return os.Stdin, nil, os.Stderr + } + } + } + + // Build the commands that we'll execute. + pidFile := filepath.Join(bundlePath, "pid") + args := append(append(append(options.Args, "create", "--bundle", bundlePath, "--pid-file", pidFile), moreCreateArgs...), containerName) + create := exec.Command(runtime, args...) + create.Dir = bundlePath + stdin, stdout, stderr := getCreateStdio() + create.Stdin, create.Stdout, create.Stderr = stdin, stdout, stderr + if create.SysProcAttr == nil { + create.SysProcAttr = &syscall.SysProcAttr{} + } + + args = append(options.Args, "start", containerName) + start := exec.Command(runtime, args...) + start.Dir = bundlePath + start.Stderr = os.Stderr + + args = append(options.Args, "kill", containerName) + kill := exec.Command(runtime, args...) + kill.Dir = bundlePath + kill.Stderr = os.Stderr + + args = append(options.Args, "delete", containerName) + del := exec.Command(runtime, args...) + del.Dir = bundlePath + del.Stderr = os.Stderr + + // Actually create the container. + logrus.Debugf("Running %q", create.Args) + err = create.Run() + if err != nil { + return 1, errors.Wrapf(err, "error creating container for %v: %s", spec.Process.Args, runCollectOutput(errorFds, closeBeforeReadingErrorFds)) + } + defer func() { + err2 := del.Run() + if err2 != nil { + if err == nil { + err = errors.Wrapf(err2, "error deleting container") + } else { + logrus.Infof("error deleting container: %v", err2) + } + } + }() + + // Make sure we read the container's exit status when it exits. + pidValue, err := ioutil.ReadFile(pidFile) + if err != nil { + return 1, errors.Wrapf(err, "error reading pid from %q", pidFile) + } + pid, err := strconv.Atoi(strings.TrimSpace(string(pidValue))) + if err != nil { + return 1, errors.Wrapf(err, "error parsing pid %s as a number", string(pidValue)) + } + var reaping sync.WaitGroup + reaping.Add(1) + go func() { + defer reaping.Done() + var err error + _, err = unix.Wait4(pid, &wstatus, 0, nil) + if err != nil { + wstatus = 0 + logrus.Errorf("error waiting for container child process %d: %v\n", pid, err) + } + }() + + if configureNetwork { + teardown, err := runConfigureNetwork(isolation, options, configureNetworks, pid, containerName, spec.Process.Args) + if teardown != nil { + defer teardown() + } + if err != nil { + return 1, err + } + } + + if copyPipes { + // We don't need the ends of the pipes that belong to the container. + stdin.Close() + if stdout != nil { + stdout.Close() + } + stderr.Close() + } + + // Handle stdio for the container in the background. + stdio.Add(1) + go runCopyStdio(&stdio, copyPipes, stdioPipe, copyConsole, consoleListener, finishCopy, finishedCopy, spec) + + // Start the container. + logrus.Debugf("Running %q", start.Args) + err = start.Run() + if err != nil { + return 1, errors.Wrapf(err, "error starting container") + } + stopped := false + defer func() { + if !stopped { + err2 := kill.Run() + if err2 != nil { + if err == nil { + err = errors.Wrapf(err2, "error stopping container") + } else { + logrus.Infof("error stopping container: %v", err2) + } + } + } + }() + + // Wait for the container to exit. + for { + now := time.Now() + var state specs.State + args = append(options.Args, "state", containerName) + stat := exec.Command(runtime, args...) + stat.Dir = bundlePath + stat.Stderr = os.Stderr + stateOutput, stateErr := stat.Output() + if stateErr != nil { + return 1, errors.Wrapf(stateErr, "error reading container state") + } + if err = json.Unmarshal(stateOutput, &state); err != nil { + return 1, errors.Wrapf(stateErr, "error parsing container state %q", string(stateOutput)) + } + switch state.Status { + case "running": + case "stopped": + stopped = true + default: + return 1, errors.Errorf("container status unexpectedly changed to %q", state.Status) + } + if stopped { + break + } + select { + case <-finishedCopy: + stopped = true + case <-time.After(time.Until(now.Add(100 * time.Millisecond))): + continue + } + if stopped { + break + } + } + + // Close the writing end of the stop-handling-stdio notification pipe. + unix.Close(finishCopy[1]) + // Wait for the stdio copy goroutine to flush. + stdio.Wait() + // Wait until we finish reading the exit status. + reaping.Wait() + + return wstatus, nil +} + +func runCollectOutput(fds, closeBeforeReadingFds []int) string { + for _, fd := range closeBeforeReadingFds { + unix.Close(fd) + } + var b bytes.Buffer + buf := make([]byte, 8192) + for _, fd := range fds { + nread, err := unix.Read(fd, buf) + if err != nil { + if errno, isErrno := err.(syscall.Errno); isErrno { + switch errno { + default: + logrus.Errorf("error reading from pipe %d: %v", fd, err) + case syscall.EINTR, syscall.EAGAIN: + } + } else { + logrus.Errorf("unable to wait for data from pipe %d: %v", fd, err) + } + continue + } + for nread > 0 { + r := buf[:nread] + if nwritten, err := b.Write(r); err != nil || nwritten != len(r) { + if nwritten != len(r) { + logrus.Errorf("error buffering data from pipe %d: %v", fd, err) + break + } + } + nread, err = unix.Read(fd, buf) + if err != nil { + if errno, isErrno := err.(syscall.Errno); isErrno { + switch errno { + default: + logrus.Errorf("error reading from pipe %d: %v", fd, err) + case syscall.EINTR, syscall.EAGAIN: + } + } else { + logrus.Errorf("unable to wait for data from pipe %d: %v", fd, err) + } + break + } + } + } + return b.String() +} + +func setupRootlessNetwork(pid int) (teardown func(), err error) { + slirp4netns, err := exec.LookPath("slirp4netns") + if err != nil { + return nil, errors.Wrapf(err, "cannot find slirp4netns") + } + + rootlessSlirpSyncR, rootlessSlirpSyncW, err := os.Pipe() + if err != nil { + return nil, errors.Wrapf(err, "cannot create slirp4netns sync pipe") + } + defer rootlessSlirpSyncR.Close() + + // Be sure there are no fds inherited to slirp4netns except the sync pipe + files, err := ioutil.ReadDir("/proc/self/fd") + if err != nil { + return nil, errors.Wrapf(err, "cannot list open fds") + } + for _, f := range files { + fd, err := strconv.Atoi(f.Name()) + if err != nil { + return nil, errors.Wrapf(err, "cannot parse fd") + } + if fd == int(rootlessSlirpSyncW.Fd()) { + continue + } + unix.CloseOnExec(fd) + } + + cmd := exec.Command(slirp4netns, "--mtu", "65520", "-r", "3", "-c", fmt.Sprintf("%d", pid), "tap0") + cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil + cmd.ExtraFiles = []*os.File{rootlessSlirpSyncW} + + err = cmd.Start() + rootlessSlirpSyncW.Close() + if err != nil { + return nil, errors.Wrapf(err, "cannot start slirp4netns") + } + + b := make([]byte, 1) + for { + if err := rootlessSlirpSyncR.SetDeadline(time.Now().Add(1 * time.Second)); err != nil { + return nil, errors.Wrapf(err, "error setting slirp4netns pipe timeout") + } + if _, err := rootlessSlirpSyncR.Read(b); err == nil { + break + } else { + if os.IsTimeout(err) { + // Check if the process is still running. + var status syscall.WaitStatus + _, err := syscall.Wait4(cmd.Process.Pid, &status, syscall.WNOHANG, nil) + if err != nil { + return nil, errors.Wrapf(err, "failed to read slirp4netns process status") + } + if status.Exited() || status.Signaled() { + return nil, errors.New("slirp4netns failed") + } + + continue + } + return nil, errors.Wrapf(err, "failed to read from slirp4netns sync pipe") + } + } + + return func() { + cmd.Process.Kill() + cmd.Wait() + }, nil +} + +func runConfigureNetwork(isolation Isolation, options RunOptions, configureNetworks []string, pid int, containerName string, command []string) (teardown func(), err error) { + var netconf, undo []*libcni.NetworkConfigList + + if isolation == IsolationOCIRootless { + if ns := options.NamespaceOptions.Find(string(specs.NetworkNamespace)); ns != nil && !ns.Host && ns.Path == "" { + return setupRootlessNetwork(pid) + } + } + // Scan for CNI configuration files. + confdir := options.CNIConfigDir + files, err := libcni.ConfFiles(confdir, []string{".conf"}) + if err != nil { + return nil, errors.Wrapf(err, "error finding CNI networking configuration files named *.conf in directory %q", confdir) + } + lists, err := libcni.ConfFiles(confdir, []string{".conflist"}) + if err != nil { + return nil, errors.Wrapf(err, "error finding CNI networking configuration list files named *.conflist in directory %q", confdir) + } + logrus.Debugf("CNI network configuration file list: %#v", append(files, lists...)) + // Read the CNI configuration files. + for _, file := range files { + nc, err := libcni.ConfFromFile(file) + if err != nil { + return nil, errors.Wrapf(err, "error loading networking configuration from file %q for %v", file, command) + } + if len(configureNetworks) > 0 && nc.Network != nil && (nc.Network.Name == "" || !util.StringInSlice(nc.Network.Name, configureNetworks)) { + if nc.Network.Name == "" { + logrus.Debugf("configuration in %q has no name, skipping it", file) + } else { + logrus.Debugf("configuration in %q has name %q, skipping it", file, nc.Network.Name) + } + continue + } + cl, err := libcni.ConfListFromConf(nc) + if err != nil { + return nil, errors.Wrapf(err, "error converting networking configuration from file %q for %v", file, command) + } + logrus.Debugf("using network configuration from %q", file) + netconf = append(netconf, cl) + } + for _, list := range lists { + cl, err := libcni.ConfListFromFile(list) + if err != nil { + return nil, errors.Wrapf(err, "error loading networking configuration list from file %q for %v", list, command) + } + if len(configureNetworks) > 0 && (cl.Name == "" || !util.StringInSlice(cl.Name, configureNetworks)) { + if cl.Name == "" { + logrus.Debugf("configuration list in %q has no name, skipping it", list) + } else { + logrus.Debugf("configuration list in %q has name %q, skipping it", list, cl.Name) + } + continue + } + logrus.Debugf("using network configuration list from %q", list) + netconf = append(netconf, cl) + } + // Make sure we can access the container's network namespace, + // even after it exits, to successfully tear down the + // interfaces. Ensure this by opening a handle to the network + // namespace, and using our copy to both configure and + // deconfigure it. + netns := fmt.Sprintf("/proc/%d/ns/net", pid) + netFD, err := unix.Open(netns, unix.O_RDONLY, 0) + if err != nil { + return nil, errors.Wrapf(err, "error opening network namespace for %v", command) + } + mynetns := fmt.Sprintf("/proc/%d/fd/%d", unix.Getpid(), netFD) + // Build our search path for the plugins. + pluginPaths := strings.Split(options.CNIPluginPath, string(os.PathListSeparator)) + cni := libcni.CNIConfig{Path: pluginPaths} + // Configure the interfaces. + rtconf := make(map[*libcni.NetworkConfigList]*libcni.RuntimeConf) + teardown = func() { + for _, nc := range undo { + if err = cni.DelNetworkList(context.Background(), nc, rtconf[nc]); err != nil { + logrus.Errorf("error cleaning up network %v for %v: %v", rtconf[nc].IfName, command, err) + } + } + unix.Close(netFD) + } + for i, nc := range netconf { + // Build the runtime config for use with this network configuration. + rtconf[nc] = &libcni.RuntimeConf{ + ContainerID: containerName, + NetNS: mynetns, + IfName: fmt.Sprintf("if%d", i), + Args: [][2]string{}, + CapabilityArgs: map[string]interface{}{}, + } + // Bring it up. + _, err := cni.AddNetworkList(context.Background(), nc, rtconf[nc]) + if err != nil { + return teardown, errors.Wrapf(err, "error configuring network list %v for %v", rtconf[nc].IfName, command) + } + // Add it to the list of networks to take down when the container process exits. + undo = append([]*libcni.NetworkConfigList{nc}, undo...) + } + return teardown, nil +} + +func runCopyStdio(stdio *sync.WaitGroup, copyPipes bool, stdioPipe [][]int, copyConsole bool, consoleListener *net.UnixListener, finishCopy []int, finishedCopy chan struct{}, spec *specs.Spec) { + defer func() { + unix.Close(finishCopy[0]) + if copyPipes { + unix.Close(stdioPipe[unix.Stdin][1]) + unix.Close(stdioPipe[unix.Stdout][0]) + unix.Close(stdioPipe[unix.Stderr][0]) + } + stdio.Done() + finishedCopy <- struct{}{} + }() + // Map describing where data on an incoming descriptor should go. + relayMap := make(map[int]int) + // Map describing incoming and outgoing descriptors. + readDesc := make(map[int]string) + writeDesc := make(map[int]string) + // Buffers. + relayBuffer := make(map[int]*bytes.Buffer) + // Set up the terminal descriptor or pipes for polling. + if copyConsole { + // Accept a connection over our listening socket. + fd, err := runAcceptTerminal(consoleListener, spec.Process.ConsoleSize) + if err != nil { + logrus.Errorf("%v", err) + return + } + terminalFD := fd + // Input from our stdin, output from the terminal descriptor. + relayMap[unix.Stdin] = terminalFD + readDesc[unix.Stdin] = "stdin" + relayBuffer[terminalFD] = new(bytes.Buffer) + writeDesc[terminalFD] = "container terminal input" + relayMap[terminalFD] = unix.Stdout + readDesc[terminalFD] = "container terminal output" + relayBuffer[unix.Stdout] = new(bytes.Buffer) + writeDesc[unix.Stdout] = "output" + // Set our terminal's mode to raw, to pass handling of special + // terminal input to the terminal in the container. + if terminal.IsTerminal(unix.Stdin) { + if state, err := terminal.MakeRaw(unix.Stdin); err != nil { + logrus.Warnf("error setting terminal state: %v", err) + } else { + defer func() { + if err = terminal.Restore(unix.Stdin, state); err != nil { + logrus.Errorf("unable to restore terminal state: %v", err) + } + }() + } + } + } + if copyPipes { + // Input from our stdin, output from the stdout and stderr pipes. + relayMap[unix.Stdin] = stdioPipe[unix.Stdin][1] + readDesc[unix.Stdin] = "stdin" + relayBuffer[stdioPipe[unix.Stdin][1]] = new(bytes.Buffer) + writeDesc[stdioPipe[unix.Stdin][1]] = "container stdin" + relayMap[stdioPipe[unix.Stdout][0]] = unix.Stdout + readDesc[stdioPipe[unix.Stdout][0]] = "container stdout" + relayBuffer[unix.Stdout] = new(bytes.Buffer) + writeDesc[unix.Stdout] = "stdout" + relayMap[stdioPipe[unix.Stderr][0]] = unix.Stderr + readDesc[stdioPipe[unix.Stderr][0]] = "container stderr" + relayBuffer[unix.Stderr] = new(bytes.Buffer) + writeDesc[unix.Stderr] = "stderr" + } + // Set our reading descriptors to non-blocking. + for rfd, wfd := range relayMap { + if err := unix.SetNonblock(rfd, true); err != nil { + logrus.Errorf("error setting %s to nonblocking: %v", readDesc[rfd], err) + return + } + if err := unix.SetNonblock(wfd, false); err != nil { + logrus.Errorf("error setting descriptor %d (%s) blocking: %v", wfd, writeDesc[wfd], err) + } + } + // Pass data back and forth. + pollTimeout := -1 + for len(relayMap) > 0 { + // Start building the list of descriptors to poll. + pollFds := make([]unix.PollFd, 0, len(relayMap)+1) + // Poll for a notification that we should stop handling stdio. + pollFds = append(pollFds, unix.PollFd{Fd: int32(finishCopy[0]), Events: unix.POLLIN | unix.POLLHUP}) + // Poll on our reading descriptors. + for rfd := range relayMap { + pollFds = append(pollFds, unix.PollFd{Fd: int32(rfd), Events: unix.POLLIN | unix.POLLHUP}) + } + buf := make([]byte, 8192) + // Wait for new data from any input descriptor, or a notification that we're done. + _, err := unix.Poll(pollFds, pollTimeout) + if !util.LogIfNotRetryable(err, fmt.Sprintf("error waiting for stdio/terminal data to relay: %v", err)) { + return + } + removes := make(map[int]struct{}) + for _, pollFd := range pollFds { + // If this descriptor's just been closed from the other end, mark it for + // removal from the set that we're checking for. + if pollFd.Revents&unix.POLLHUP == unix.POLLHUP { + removes[int(pollFd.Fd)] = struct{}{} + } + // If the descriptor was closed elsewhere, remove it from our list. + if pollFd.Revents&unix.POLLNVAL != 0 { + logrus.Debugf("error polling descriptor %s: closed?", readDesc[int(pollFd.Fd)]) + removes[int(pollFd.Fd)] = struct{}{} + } + // If the POLLIN flag isn't set, then there's no data to be read from this descriptor. + if pollFd.Revents&unix.POLLIN == 0 { + // If we're using pipes and it's our stdin and it's closed, close the writing + // end of the corresponding pipe. + if copyPipes && int(pollFd.Fd) == unix.Stdin && pollFd.Revents&unix.POLLHUP != 0 { + unix.Close(stdioPipe[unix.Stdin][1]) + stdioPipe[unix.Stdin][1] = -1 + } + continue + } + // Read whatever there is to be read. + readFD := int(pollFd.Fd) + writeFD, needToRelay := relayMap[readFD] + if needToRelay { + n, err := unix.Read(readFD, buf) + if !util.LogIfNotRetryable(err, fmt.Sprintf("unable to read %s data: %v", readDesc[readFD], err)) { + return + } + // If it's zero-length on our stdin and we're + // using pipes, it's an EOF, so close the stdin + // pipe's writing end. + if n == 0 && copyPipes && int(pollFd.Fd) == unix.Stdin { + unix.Close(stdioPipe[unix.Stdin][1]) + stdioPipe[unix.Stdin][1] = -1 + } + if n > 0 { + // Buffer the data in case we get blocked on where they need to go. + nwritten, err := relayBuffer[writeFD].Write(buf[:n]) + if err != nil { + logrus.Debugf("buffer: %v", err) + continue + } + if nwritten != n { + logrus.Debugf("buffer: expected to buffer %d bytes, wrote %d", n, nwritten) + continue + } + // If this is the last of the data we'll be able to read from this + // descriptor, read all that there is to read. + for pollFd.Revents&unix.POLLHUP == unix.POLLHUP { + nr, err := unix.Read(readFD, buf) + util.LogIfUnexpectedWhileDraining(err, fmt.Sprintf("read %s: %v", readDesc[readFD], err)) + if nr <= 0 { + break + } + nwritten, err := relayBuffer[writeFD].Write(buf[:nr]) + if err != nil { + logrus.Debugf("buffer: %v", err) + break + } + if nwritten != nr { + logrus.Debugf("buffer: expected to buffer %d bytes, wrote %d", nr, nwritten) + break + } + } + } + } + } + // Try to drain the output buffers. Set the default timeout + // for the next poll() to 100ms if we still have data to write. + pollTimeout = -1 + for writeFD := range relayBuffer { + if relayBuffer[writeFD].Len() > 0 { + n, err := unix.Write(writeFD, relayBuffer[writeFD].Bytes()) + if !util.LogIfNotRetryable(err, fmt.Sprintf("unable to write %s data: %v", writeDesc[writeFD], err)) { + return + } + if n > 0 { + relayBuffer[writeFD].Next(n) + } + } + if relayBuffer[writeFD].Len() > 0 { + pollTimeout = 100 + } + } + // Remove any descriptors which we don't need to poll any more from the poll descriptor list. + for remove := range removes { + delete(relayMap, remove) + } + // If the we-can-return pipe had anything for us, we're done. + for _, pollFd := range pollFds { + if int(pollFd.Fd) == finishCopy[0] && pollFd.Revents != 0 { + // The pipe is closed, indicating that we can stop now. + return + } + } + } +} + +func runAcceptTerminal(consoleListener *net.UnixListener, terminalSize *specs.Box) (int, error) { + defer consoleListener.Close() + c, err := consoleListener.AcceptUnix() + if err != nil { + return -1, errors.Wrapf(err, "error accepting socket descriptor connection") + } + defer c.Close() + // Expect a control message over our new connection. + b := make([]byte, 8192) + oob := make([]byte, 8192) + n, oobn, _, _, err := c.ReadMsgUnix(b, oob) + if err != nil { + return -1, errors.Wrapf(err, "error reading socket descriptor") + } + if n > 0 { + logrus.Debugf("socket descriptor is for %q", string(b[:n])) + } + if oobn > len(oob) { + return -1, errors.Errorf("too much out-of-bounds data (%d bytes)", oobn) + } + // Parse the control message. + scm, err := unix.ParseSocketControlMessage(oob[:oobn]) + if err != nil { + return -1, errors.Wrapf(err, "error parsing out-of-bound data as a socket control message") + } + logrus.Debugf("control messages: %v", scm) + // Expect to get a descriptor. + terminalFD := -1 + for i := range scm { + fds, err := unix.ParseUnixRights(&scm[i]) + if err != nil { + return -1, errors.Wrapf(err, "error parsing unix rights control message: %v", &scm[i]) + } + logrus.Debugf("fds: %v", fds) + if len(fds) == 0 { + continue + } + terminalFD = fds[0] + break + } + if terminalFD == -1 { + return -1, errors.Errorf("unable to read terminal descriptor") + } + // Set the pseudoterminal's size to the configured size, or our own. + winsize := &unix.Winsize{} + if terminalSize != nil { + // Use configured sizes. + winsize.Row = uint16(terminalSize.Height) + winsize.Col = uint16(terminalSize.Width) + } else { + if terminal.IsTerminal(unix.Stdin) { + // Use the size of our terminal. + if winsize, err = unix.IoctlGetWinsize(unix.Stdin, unix.TIOCGWINSZ); err != nil { + logrus.Warnf("error reading size of controlling terminal: %v", err) + winsize.Row = 0 + winsize.Col = 0 + } + } + } + if winsize.Row != 0 && winsize.Col != 0 { + if err = unix.IoctlSetWinsize(terminalFD, unix.TIOCSWINSZ, winsize); err != nil { + logrus.Warnf("error setting size of container pseudoterminal: %v", err) + } + // FIXME - if we're connected to a terminal, we should + // be passing the updated terminal size down when we + // receive a SIGWINCH. + } + return terminalFD, nil +} + +// Create pipes to use for relaying stdio. +func runMakeStdioPipe(uid, gid int) ([][]int, error) { + stdioPipe := make([][]int, 3) + for i := range stdioPipe { + stdioPipe[i] = make([]int, 2) + if err := unix.Pipe(stdioPipe[i]); err != nil { + return nil, errors.Wrapf(err, "error creating pipe for container FD %d", i) + } + } + if err := unix.Fchown(stdioPipe[unix.Stdin][0], uid, gid); err != nil { + return nil, errors.Wrapf(err, "error setting owner of stdin pipe descriptor") + } + if err := unix.Fchown(stdioPipe[unix.Stdout][1], uid, gid); err != nil { + return nil, errors.Wrapf(err, "error setting owner of stdout pipe descriptor") + } + if err := unix.Fchown(stdioPipe[unix.Stderr][1], uid, gid); err != nil { + return nil, errors.Wrapf(err, "error setting owner of stderr pipe descriptor") + } + return stdioPipe, nil +} + +func runUsingRuntimeMain() { + var options runUsingRuntimeSubprocOptions + // Set logging. + if level := os.Getenv("LOGLEVEL"); level != "" { + if ll, err := strconv.Atoi(level); err == nil { + logrus.SetLevel(logrus.Level(ll)) + } + } + // Unpack our configuration. + confPipe := os.NewFile(3, "confpipe") + if confPipe == nil { + fmt.Fprintf(os.Stderr, "error reading options pipe\n") + os.Exit(1) + } + defer confPipe.Close() + if err := json.NewDecoder(confPipe).Decode(&options); err != nil { + fmt.Fprintf(os.Stderr, "error decoding options: %v\n", err) + os.Exit(1) + } + // Set ourselves up to read the container's exit status. We're doing this in a child process + // so that we won't mess with the setting in a caller of the library. This stubs to OS specific + // calls + if err := setChildProcess(); err != nil { + os.Exit(1) + } + // Run the container, start to finish. + status, err := runUsingRuntime(options.Isolation, options.Options, options.ConfigureNetwork, options.ConfigureNetworks, options.MoreCreateArgs, options.Spec, options.RootPath, options.BundlePath, options.ContainerName) + if err != nil { + fmt.Fprintf(os.Stderr, "error running container: %v\n", err) + os.Exit(1) + } + // Pass the container's exit status back to the caller by exiting with the same status. + if status.Exited() { + os.Exit(status.ExitStatus()) + } else if status.Signaled() { + fmt.Fprintf(os.Stderr, "container exited on %s\n", status.Signal()) + os.Exit(1) + } + os.Exit(1) +} + +func setupNamespaces(g *generate.Generator, namespaceOptions NamespaceOptions, idmapOptions IDMappingOptions, policy NetworkConfigurationPolicy) (configureNetwork bool, configureNetworks []string, configureUTS bool, err error) { + // Set namespace options in the container configuration. + configureUserns := false + specifiedNetwork := false + for _, namespaceOption := range namespaceOptions { + switch namespaceOption.Name { + case string(specs.UserNamespace): + configureUserns = false + if !namespaceOption.Host && namespaceOption.Path == "" { + configureUserns = true + } + case string(specs.NetworkNamespace): + specifiedNetwork = true + configureNetwork = false + if !namespaceOption.Host && (namespaceOption.Path == "" || !filepath.IsAbs(namespaceOption.Path)) { + if namespaceOption.Path != "" && !filepath.IsAbs(namespaceOption.Path) { + configureNetworks = strings.Split(namespaceOption.Path, ",") + namespaceOption.Path = "" + } + configureNetwork = (policy != NetworkDisabled) + } + case string(specs.UTSNamespace): + configureUTS = false + if !namespaceOption.Host && namespaceOption.Path == "" { + configureUTS = true + } + } + if namespaceOption.Host { + if err := g.RemoveLinuxNamespace(namespaceOption.Name); err != nil { + return false, nil, false, errors.Wrapf(err, "error removing %q namespace for run", namespaceOption.Name) + } + } else if err := g.AddOrReplaceLinuxNamespace(namespaceOption.Name, namespaceOption.Path); err != nil { + if namespaceOption.Path == "" { + return false, nil, false, errors.Wrapf(err, "error adding new %q namespace for run", namespaceOption.Name) + } + return false, nil, false, errors.Wrapf(err, "error adding %q namespace %q for run", namespaceOption.Name, namespaceOption.Path) + } + } + + // If we've got mappings, we're going to have to create a user namespace. + if len(idmapOptions.UIDMap) > 0 || len(idmapOptions.GIDMap) > 0 || configureUserns { + if err := g.AddOrReplaceLinuxNamespace(specs.UserNamespace, ""); err != nil { + return false, nil, false, errors.Wrapf(err, "error adding new %q namespace for run", string(specs.UserNamespace)) + } + hostUidmap, hostGidmap, err := unshare.GetHostIDMappings("") + if err != nil { + return false, nil, false, err + } + for _, m := range idmapOptions.UIDMap { + g.AddLinuxUIDMapping(m.HostID, m.ContainerID, m.Size) + } + if len(idmapOptions.UIDMap) == 0 { + for _, m := range hostUidmap { + g.AddLinuxUIDMapping(m.ContainerID, m.ContainerID, m.Size) + } + } + for _, m := range idmapOptions.GIDMap { + g.AddLinuxGIDMapping(m.HostID, m.ContainerID, m.Size) + } + if len(idmapOptions.GIDMap) == 0 { + for _, m := range hostGidmap { + g.AddLinuxGIDMapping(m.ContainerID, m.ContainerID, m.Size) + } + } + if !specifiedNetwork { + if err := g.AddOrReplaceLinuxNamespace(specs.NetworkNamespace, ""); err != nil { + return false, nil, false, errors.Wrapf(err, "error adding new %q namespace for run", string(specs.NetworkNamespace)) + } + configureNetwork = (policy != NetworkDisabled) + } + } else { + if err := g.RemoveLinuxNamespace(specs.UserNamespace); err != nil { + return false, nil, false, errors.Wrapf(err, "error removing %q namespace for run", string(specs.UserNamespace)) + } + if !specifiedNetwork { + if err := g.RemoveLinuxNamespace(specs.NetworkNamespace); err != nil { + return false, nil, false, errors.Wrapf(err, "error removing %q namespace for run", string(specs.NetworkNamespace)) + } + } + } + if configureNetwork { + for name, val := range util.DefaultNetworkSysctl { + g.AddLinuxSysctl(name, val) + } + } + return configureNetwork, configureNetworks, configureUTS, nil +} + +func (b *Builder) configureNamespaces(g *generate.Generator, options RunOptions) (bool, []string, error) { + defaultNamespaceOptions, err := DefaultNamespaceOptions() + if err != nil { + return false, nil, err + } + + namespaceOptions := defaultNamespaceOptions + namespaceOptions.AddOrReplace(b.NamespaceOptions...) + namespaceOptions.AddOrReplace(options.NamespaceOptions...) + + networkPolicy := options.ConfigureNetwork + if networkPolicy == NetworkDefault { + networkPolicy = b.ConfigureNetwork + } + + configureNetwork, configureNetworks, configureUTS, err := setupNamespaces(g, namespaceOptions, b.IDMappingOptions, networkPolicy) + if err != nil { + return false, nil, err + } + + if configureUTS { + if options.Hostname != "" { + g.SetHostname(options.Hostname) + } else if b.Hostname() != "" { + g.SetHostname(b.Hostname()) + } else { + g.SetHostname(stringid.TruncateID(b.ContainerID)) + } + } else { + g.SetHostname("") + } + + found := false + spec := g.Config + for i := range spec.Process.Env { + if strings.HasPrefix(spec.Process.Env[i], "HOSTNAME=") { + found = true + break + } + } + if !found { + spec.Process.Env = append(spec.Process.Env, fmt.Sprintf("HOSTNAME=%s", spec.Hostname)) + } + + return configureNetwork, configureNetworks, nil +} + +func runSetupBoundFiles(bundlePath string, bindFiles map[string]string) (mounts []specs.Mount, err error) { + for dest, src := range bindFiles { + options := []string{"rbind"} + if strings.HasPrefix(src, bundlePath) { + options = append(options, bind.NoBindOption) + } + mounts = append(mounts, specs.Mount{ + Source: src, + Destination: dest, + Type: "bind", + Options: options, + }) + } + return mounts, nil +} + +func addRlimits(ulimit []string, g *generate.Generator) error { + var ( + ul *units.Ulimit + err error + ) + + for _, u := range ulimit { + if ul, err = units.ParseUlimit(u); err != nil { + return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u) + } + + g.AddProcessRlimits("RLIMIT_"+strings.ToUpper(ul.Name), uint64(ul.Hard), uint64(ul.Soft)) + } + return nil +} + +func runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount) ([]specs.Mount, error) { + var mounts []specs.Mount + + parseMount := func(host, container string, options []string) (specs.Mount, error) { + var foundrw, foundro, foundz, foundZ bool + var rootProp string + for _, opt := range options { + switch opt { + case "rw": + foundrw = true + case "ro": + foundro = true + case "z": + foundz = true + case "Z": + foundZ = true + case "private", "rprivate", "slave", "rslave", "shared", "rshared": + rootProp = opt + } + } + if !foundrw && !foundro { + options = append(options, "rw") + } + if foundz { + if err := label.Relabel(host, mountLabel, true); err != nil { + return specs.Mount{}, errors.Wrapf(err, "relabeling %q failed", host) + } + } + if foundZ { + if err := label.Relabel(host, mountLabel, false); err != nil { + return specs.Mount{}, errors.Wrapf(err, "relabeling %q failed", host) + } + } + if rootProp == "" { + options = append(options, "private") + } + return specs.Mount{ + Destination: container, + Type: "bind", + Source: host, + Options: options, + }, nil + } + // Bind mount volumes specified for this particular Run() invocation + for _, i := range optionMounts { + logrus.Debugf("setting up mounted volume at %q", i.Destination) + mount, err := parseMount(i.Source, i.Destination, append(i.Options, "rbind")) + if err != nil { + return nil, err + } + mounts = append(mounts, mount) + } + // Bind mount volumes given by the user when the container was created + for _, i := range volumeMounts { + var options []string + spliti := strings.Split(i, ":") + if len(spliti) > 2 { + options = strings.Split(spliti[2], ",") + } + options = append(options, "rbind") + mount, err := parseMount(spliti[0], spliti[1], options) + if err != nil { + return nil, err + } + mounts = append(mounts, mount) + } + return mounts, nil +} + +func setupMaskedPaths(g *generate.Generator) { + for _, mp := range []string{ + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + } { + g.AddLinuxMaskedPaths(mp) + } +} + +func setupReadOnlyPaths(g *generate.Generator) { + for _, rp := range []string{ + "/proc/asound", + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger", + } { + g.AddLinuxReadonlyPaths(rp) + } +} + +func setupCapAdd(g *generate.Generator, caps ...string) error { + for _, cap := range caps { + if err := g.AddProcessCapabilityBounding(cap); err != nil { + return errors.Wrapf(err, "error adding %q to the bounding capability set", cap) + } + if err := g.AddProcessCapabilityEffective(cap); err != nil { + return errors.Wrapf(err, "error adding %q to the effective capability set", cap) + } + if err := g.AddProcessCapabilityInheritable(cap); err != nil { + return errors.Wrapf(err, "error adding %q to the inheritable capability set", cap) + } + if err := g.AddProcessCapabilityPermitted(cap); err != nil { + return errors.Wrapf(err, "error adding %q to the permitted capability set", cap) + } + if err := g.AddProcessCapabilityAmbient(cap); err != nil { + return errors.Wrapf(err, "error adding %q to the ambient capability set", cap) + } + } + return nil +} + +func setupCapDrop(g *generate.Generator, caps ...string) error { + for _, cap := range caps { + if err := g.DropProcessCapabilityBounding(cap); err != nil { + return errors.Wrapf(err, "error removing %q from the bounding capability set", cap) + } + if err := g.DropProcessCapabilityEffective(cap); err != nil { + return errors.Wrapf(err, "error removing %q from the effective capability set", cap) + } + if err := g.DropProcessCapabilityInheritable(cap); err != nil { + return errors.Wrapf(err, "error removing %q from the inheritable capability set", cap) + } + if err := g.DropProcessCapabilityPermitted(cap); err != nil { + return errors.Wrapf(err, "error removing %q from the permitted capability set", cap) + } + if err := g.DropProcessCapabilityAmbient(cap); err != nil { + return errors.Wrapf(err, "error removing %q from the ambient capability set", cap) + } + } + return nil +} + +func setupCapabilities(g *generate.Generator, firstAdds, firstDrops, secondAdds, secondDrops []string) error { + g.ClearProcessCapabilities() + if err := setupCapAdd(g, util.DefaultCapabilities...); err != nil { + return err + } + if err := setupCapAdd(g, firstAdds...); err != nil { + return err + } + if err := setupCapDrop(g, firstDrops...); err != nil { + return err + } + if err := setupCapAdd(g, secondAdds...); err != nil { + return err + } + return setupCapDrop(g, secondDrops...) +} + +// Search for a command that isn't given as an absolute path using the $PATH +// under the rootfs. We can't resolve absolute symbolic links without +// chroot()ing, which we may not be able to do, so just accept a link as a +// valid resolution. +func runLookupPath(g *generate.Generator, command []string) []string { + // Look for the configured $PATH. + spec := g.Config + envPath := "" + for i := range spec.Process.Env { + if strings.HasPrefix(spec.Process.Env[i], "PATH=") { + envPath = spec.Process.Env[i] + } + } + // If there is no configured $PATH, supply one. + if envPath == "" { + defaultPath := "/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin" + envPath = "PATH=" + defaultPath + g.AddProcessEnv("PATH", defaultPath) + } + // No command, nothing to do. + if len(command) == 0 { + return command + } + // Command is already an absolute path, use it as-is. + if filepath.IsAbs(command[0]) { + return command + } + // For each element in the PATH, + for _, pathEntry := range filepath.SplitList(envPath[5:]) { + // if it's the empty string, it's ".", which is the Cwd, + if pathEntry == "" { + pathEntry = spec.Process.Cwd + } + // build the absolute path which it might be, + candidate := filepath.Join(pathEntry, command[0]) + // check if it's there, + if fi, err := os.Lstat(filepath.Join(spec.Root.Path, candidate)); fi != nil && err == nil { + // and if it's not a directory, and either a symlink or executable, + if !fi.IsDir() && ((fi.Mode()&os.ModeSymlink != 0) || (fi.Mode()&0111 != 0)) { + // use that. + return append([]string{candidate}, command[1:]...) + } + } + } + return command +} + +func getDNSIP(dnsServers []string) (dns []net.IP, err error) { + for _, i := range dnsServers { + result := net.ParseIP(i) + if result == nil { + return dns, errors.Errorf("invalid IP address %s", i) + } + dns = append(dns, result) + } + return dns, nil +} + +func (b *Builder) configureUIDGID(g *generate.Generator, mountPoint string, options RunOptions) error { + // Set the user UID/GID/supplemental group list/capabilities lists. + user, err := b.user(mountPoint, options.User) + if err != nil { + return err + } + if err := setupCapabilities(g, b.AddCapabilities, b.DropCapabilities, options.AddCapabilities, options.DropCapabilities); err != nil { + return err + } + g.SetProcessUID(user.UID) + g.SetProcessGID(user.GID) + for _, gid := range user.AdditionalGids { + g.AddProcessAdditionalGid(gid) + } + + // Remove capabilities if not running as root except Bounding set + if user.UID != 0 { + bounding := g.Config.Process.Capabilities.Bounding + g.ClearProcessCapabilities() + g.Config.Process.Capabilities.Bounding = bounding + } + + return nil +} + +func (b *Builder) configureEnvironment(g *generate.Generator, options RunOptions) { + g.ClearProcessEnv() + if b.CommonBuildOpts.HTTPProxy { + for _, envSpec := range []string{ + "http_proxy", + "HTTP_PROXY", + "https_proxy", + "HTTPS_PROXY", + "ftp_proxy", + "FTP_PROXY", + "no_proxy", + "NO_PROXY", + } { + envVal := os.Getenv(envSpec) + if envVal != "" { + g.AddProcessEnv(envSpec, envVal) + } + } + } + + for _, envSpec := range append(b.Env(), options.Env...) { + env := strings.SplitN(envSpec, "=", 2) + if len(env) > 1 { + g.AddProcessEnv(env[0], env[1]) + } + } + + for src, dest := range b.Args { + g.AddProcessEnv(src, dest) + } +} + +func setupRootlessSpecChanges(spec *specs.Spec, bundleDir string, rootUID, rootGID uint32) error { + spec.Hostname = "" + spec.Process.User.AdditionalGids = nil + spec.Linux.Resources = nil + + emptyDir := filepath.Join(bundleDir, "empty") + if err := os.Mkdir(emptyDir, 0); err != nil { + return errors.Wrapf(err, "error creating %q", emptyDir) + } + + // Replace /sys with a read-only bind mount. + mounts := []specs.Mount{ + { + Source: "/dev", + Destination: "/dev", + Type: "tmpfs", + Options: []string{"private", "strictatime", "noexec", "nosuid", "mode=755", "size=65536k"}, + }, + { + Source: "mqueue", + Destination: "/dev/mqueue", + Type: "mqueue", + Options: []string{"private", "nodev", "noexec", "nosuid"}, + }, + { + Source: "pts", + Destination: "/dev/pts", + Type: "devpts", + Options: []string{"private", "noexec", "nosuid", "newinstance", "ptmxmode=0666", "mode=0620"}, + }, + { + Source: "shm", + Destination: "/dev/shm", + Type: "tmpfs", + Options: []string{"private", "nodev", "noexec", "nosuid", "mode=1777", "size=65536k"}, + }, + { + Source: "/proc", + Destination: "/proc", + Type: "proc", + Options: []string{"private", "nodev", "noexec", "nosuid"}, + }, + { + Source: "/sys", + Destination: "/sys", + Type: "bind", + Options: []string{bind.NoBindOption, "rbind", "private", "nodev", "noexec", "nosuid", "ro"}, + }, + } + // Cover up /sys/fs/cgroup and /sys/fs/selinux, if they exist in our source for /sys. + if _, err := os.Stat("/sys/fs/cgroup"); err == nil { + spec.Linux.MaskedPaths = append(spec.Linux.MaskedPaths, "/sys/fs/cgroup") + } + if _, err := os.Stat("/sys/fs/selinux"); err == nil { + spec.Linux.MaskedPaths = append(spec.Linux.MaskedPaths, "/sys/fs/selinux") + } + // Keep anything that isn't under /dev, /proc, or /sys. + for i := range spec.Mounts { + if spec.Mounts[i].Destination == "/dev" || strings.HasPrefix(spec.Mounts[i].Destination, "/dev/") || + spec.Mounts[i].Destination == "/proc" || strings.HasPrefix(spec.Mounts[i].Destination, "/proc/") || + spec.Mounts[i].Destination == "/sys" || strings.HasPrefix(spec.Mounts[i].Destination, "/sys/") { + continue + } + mounts = append(mounts, spec.Mounts[i]) + } + spec.Mounts = mounts + return nil +} + +func (b *Builder) runUsingRuntimeSubproc(isolation Isolation, options RunOptions, configureNetwork bool, configureNetworks, moreCreateArgs []string, spec *specs.Spec, rootPath, bundlePath, containerName string) (err error) { + var confwg sync.WaitGroup + config, conferr := json.Marshal(runUsingRuntimeSubprocOptions{ + Options: options, + Spec: spec, + RootPath: rootPath, + BundlePath: bundlePath, + ConfigureNetwork: configureNetwork, + ConfigureNetworks: configureNetworks, + MoreCreateArgs: moreCreateArgs, + ContainerName: containerName, + Isolation: isolation, + }) + if conferr != nil { + return errors.Wrapf(conferr, "error encoding configuration for %q", runUsingRuntimeCommand) + } + cmd := reexec.Command(runUsingRuntimeCommand) + cmd.Dir = bundlePath + cmd.Stdin = options.Stdin + if cmd.Stdin == nil { + cmd.Stdin = os.Stdin + } + cmd.Stdout = options.Stdout + if cmd.Stdout == nil { + cmd.Stdout = os.Stdout + } + cmd.Stderr = options.Stderr + if cmd.Stderr == nil { + cmd.Stderr = os.Stderr + } + cmd.Env = append(os.Environ(), fmt.Sprintf("LOGLEVEL=%d", logrus.GetLevel())) + preader, pwriter, err := os.Pipe() + if err != nil { + return errors.Wrapf(err, "error creating configuration pipe") + } + confwg.Add(1) + go func() { + _, conferr = io.Copy(pwriter, bytes.NewReader(config)) + if conferr != nil { + conferr = errors.Wrapf(conferr, "error while copying configuration down pipe to child process") + } + confwg.Done() + }() + cmd.ExtraFiles = append([]*os.File{preader}, cmd.ExtraFiles...) + defer preader.Close() + defer pwriter.Close() + err = cmd.Run() + if err != nil { + err = errors.Wrapf(err, "error while running runtime") + } + confwg.Wait() + if err == nil { + return conferr + } + if conferr != nil { + logrus.Debugf("%v", conferr) + } + return err +} + +func checkAndOverrideIsolationOptions(isolation Isolation, options *RunOptions) error { + switch isolation { + case IsolationOCIRootless: + if ns := options.NamespaceOptions.Find(string(specs.IPCNamespace)); ns == nil || ns.Host { + logrus.Debugf("Forcing use of an IPC namespace.") + } + options.NamespaceOptions.AddOrReplace(NamespaceOption{Name: string(specs.IPCNamespace)}) + _, err := exec.LookPath("slirp4netns") + hostNetworking := err != nil + networkNamespacePath := "" + if ns := options.NamespaceOptions.Find(string(specs.NetworkNamespace)); ns != nil { + hostNetworking = ns.Host + networkNamespacePath = ns.Path + if !hostNetworking && networkNamespacePath != "" && !filepath.IsAbs(networkNamespacePath) { + logrus.Debugf("Disabling network namespace configuration.") + networkNamespacePath = "" + } + } + options.NamespaceOptions.AddOrReplace(NamespaceOption{ + Name: string(specs.NetworkNamespace), + Host: hostNetworking, + Path: networkNamespacePath, + }) + if ns := options.NamespaceOptions.Find(string(specs.PIDNamespace)); ns == nil || ns.Host { + logrus.Debugf("Forcing use of a PID namespace.") + } + options.NamespaceOptions.AddOrReplace(NamespaceOption{Name: string(specs.PIDNamespace), Host: false}) + if ns := options.NamespaceOptions.Find(string(specs.UserNamespace)); ns == nil || ns.Host { + logrus.Debugf("Forcing use of a user namespace.") + } + options.NamespaceOptions.AddOrReplace(NamespaceOption{Name: string(specs.UserNamespace)}) + if ns := options.NamespaceOptions.Find(string(specs.UTSNamespace)); ns != nil && !ns.Host { + logrus.Debugf("Disabling UTS namespace.") + } + options.NamespaceOptions.AddOrReplace(NamespaceOption{Name: string(specs.UTSNamespace), Host: true}) + case IsolationOCI: + pidns := options.NamespaceOptions.Find(string(specs.PIDNamespace)) + userns := options.NamespaceOptions.Find(string(specs.UserNamespace)) + if (pidns == nil || pidns.Host) && (userns != nil && !userns.Host) { + return fmt.Errorf("not allowed to mix host PID namespace with container user namespace") + } + } + return nil +} + +// DefaultNamespaceOptions returns the default namespace settings from the +// runtime-tools generator library. +func DefaultNamespaceOptions() (NamespaceOptions, error) { + options := NamespaceOptions{ + {Name: string(specs.CgroupNamespace), Host: true}, + {Name: string(specs.IPCNamespace), Host: true}, + {Name: string(specs.MountNamespace), Host: true}, + {Name: string(specs.NetworkNamespace), Host: true}, + {Name: string(specs.PIDNamespace), Host: true}, + {Name: string(specs.UserNamespace), Host: true}, + {Name: string(specs.UTSNamespace), Host: true}, + } + g, err := generate.New("linux") + if err != nil { + return options, errors.Wrapf(err, "error generating new 'linux' runtime spec") + } + spec := g.Config + if spec.Linux != nil { + for _, ns := range spec.Linux.Namespaces { + options.AddOrReplace(NamespaceOption{ + Name: string(ns.Type), + Path: ns.Path, + }) + } + } + return options, nil +} + +func contains(volumes []string, v string) bool { + for _, i := range volumes { + if i == v { + return true + } + } + return false +} + +type runUsingRuntimeSubprocOptions struct { + Options RunOptions + Spec *specs.Spec + RootPath string + BundlePath string + ConfigureNetwork bool + ConfigureNetworks []string + MoreCreateArgs []string + ContainerName string + Isolation Isolation +} + +func init() { + reexec.Register(runUsingRuntimeCommand, runUsingRuntimeMain) +} diff --git a/vendor/github.com/containers/buildah/run_unsupport.go b/vendor/github.com/containers/buildah/run_unsupport.go deleted file mode 100644 index 4824a0c4e..000000000 --- a/vendor/github.com/containers/buildah/run_unsupport.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build !linux - -package buildah - -import ( - "github.com/pkg/errors" -) - -func setChildProcess() error { - return errors.New("function not supported on non-linux systems") -} diff --git a/vendor/github.com/containers/buildah/run_unsupported.go b/vendor/github.com/containers/buildah/run_unsupported.go new file mode 100644 index 000000000..d8ff46cf6 --- /dev/null +++ b/vendor/github.com/containers/buildah/run_unsupported.go @@ -0,0 +1,20 @@ +// +build !linux + +package buildah + +import ( + "github.com/pkg/errors" +) + +func setChildProcess() error { + return errors.New("function not supported on non-linux systems") +} + +func runUsingRuntimeMain() {} + +func (b *Builder) Run(command []string, options RunOptions) error { + return errors.New("function not supported on non-linux systems") +} +func DefaultNamespaceOptions() (NamespaceOptions, error) { + return NamespaceOptions{}, errors.New("function not supported on non-linux systems") +} diff --git a/vendor/github.com/containers/buildah/util/util.go b/vendor/github.com/containers/buildah/util/util.go index 698d79a81..629d9748c 100644 --- a/vendor/github.com/containers/buildah/util/util.go +++ b/vendor/github.com/containers/buildah/util/util.go @@ -197,7 +197,7 @@ func FindImage(store storage.Store, firstRegistry string, systemContext *types.S break } if ref == nil || img == nil { - return nil, nil, errors.Wrapf(err, "error locating image with name %q", image) + return nil, nil, errors.Wrapf(err, "error locating image with name %q (%v)", image, names) } return ref, img, nil } diff --git a/vendor/github.com/containers/storage/drivers/driver.go b/vendor/github.com/containers/storage/drivers/driver.go index dda172574..e8f8bd5a7 100644 --- a/vendor/github.com/containers/storage/drivers/driver.go +++ b/vendor/github.com/containers/storage/drivers/driver.go @@ -40,6 +40,7 @@ var ( type CreateOpts struct { MountLabel string StorageOpt map[string]string + *idtools.IDMappings } // MountOpts contains optional arguments for LayerStope.Mount() methods. diff --git a/vendor/github.com/containers/storage/drivers/overlay/overlay.go b/vendor/github.com/containers/storage/drivers/overlay/overlay.go index 5d667d8c6..ef83b6c87 100644 --- a/vendor/github.com/containers/storage/drivers/overlay/overlay.go +++ b/vendor/github.com/containers/storage/drivers/overlay/overlay.go @@ -474,10 +474,22 @@ func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) (retErr func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts) (retErr error) { dir := d.dir(id) - rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) + uidMaps := d.uidMaps + gidMaps := d.gidMaps + + if opts != nil && opts.IDMappings != nil { + uidMaps = opts.IDMappings.UIDs() + gidMaps = opts.IDMappings.GIDs() + } + + rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps) if err != nil { return err } + // Make the link directory if it does not exist + if err := idtools.MkdirAllAs(path.Join(d.home, linkDir), 0700, rootUID, rootGID); err != nil && !os.IsExist(err) { + return err + } if err := idtools.MkdirAllAs(path.Dir(dir), 0700, rootUID, rootGID); err != nil { return err } @@ -690,9 +702,17 @@ func (d *Driver) recreateSymlinks() error { if err != nil { return fmt.Errorf("error reading driver home directory %q: %v", d.home, err) } + // This makes the link directory if it doesn't exist + rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) + if err != nil { + return err + } + if err := idtools.MkdirAllAs(path.Join(d.home, linkDir), 0700, rootUID, rootGID); err != nil && !os.IsExist(err) { + return err + } for _, dir := range dirs { - // Skip over the linkDir - if dir.Name() == linkDir || dir.Mode().IsRegular() { + // Skip over the linkDir and anything that is not a directory + if dir.Name() == linkDir || !dir.Mode().IsDir() { continue } // Read the "link" file under each layer to get the name of the symlink diff --git a/vendor/github.com/containers/storage/drivers/vfs/driver.go b/vendor/github.com/containers/storage/drivers/vfs/driver.go index 5941ccc17..9e256858c 100644 --- a/vendor/github.com/containers/storage/drivers/vfs/driver.go +++ b/vendor/github.com/containers/storage/drivers/vfs/driver.go @@ -123,8 +123,13 @@ func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts, ro bool return fmt.Errorf("--storage-opt is not supported for vfs") } + idMappings := d.idMappings + if opts != nil && opts.IDMappings != nil { + idMappings = opts.IDMappings + } + dir := d.dir(id) - rootIDs := d.idMappings.RootPair() + rootIDs := idMappings.RootPair() if err := idtools.MkdirAllAndChown(filepath.Dir(dir), 0700, rootIDs); err != nil { return err } diff --git a/vendor/github.com/containers/storage/layers.go b/vendor/github.com/containers/storage/layers.go index 110e737b2..7bec0aea6 100644 --- a/vendor/github.com/containers/storage/layers.go +++ b/vendor/github.com/containers/storage/layers.go @@ -614,6 +614,7 @@ func (r *layerStore) Put(id string, parentLayer *Layer, names []string, mountLab opts := drivers.CreateOpts{ MountLabel: mountLabel, StorageOpt: options, + IDMappings: idMappings, } if moreOptions.TemplateLayer != "" { if err = r.driver.CreateFromTemplate(id, moreOptions.TemplateLayer, templateIDMappings, parent, parentMappings, &opts, writeable); err != nil { diff --git a/vendor/github.com/openshift/imagebuilder/vendor.conf b/vendor/github.com/openshift/imagebuilder/vendor.conf index 39b216feb..e437b79c3 100644 --- a/vendor/github.com/openshift/imagebuilder/vendor.conf +++ b/vendor/github.com/openshift/imagebuilder/vendor.conf @@ -1,12 +1,11 @@ github.com/Azure/go-ansiterm d6e3b3328b783f23731bc4d058875b0371ff8109 -github.com/containerd/continuity 004b46473808b3e7a4a3049c20e4376c91eb966d +github.com/containers/storage v1.2 github.com/docker/docker b68221c37ee597950364788204546f9c9d0e46a1 github.com/docker/go-connections 97c2040d34dfae1d1b1275fa3a78dbdd2f41cf7e github.com/docker/go-units 2fb04c6466a548a03cb009c5569ee1ab1e35398e github.com/fsouza/go-dockerclient openshift-4.0 https://github.com/openshift/go-dockerclient.git github.com/gogo/protobuf c5a62797aee0054613cc578653a16c6237fef080 github.com/golang/glog 23def4e6c14b4da8ac2ed8007337bc5eb5007998 -github.com/golang/protobuf v1.3.0 github.com/konsorten/go-windows-terminal-sequences f55edac94c9bbba5d6182a4be46d86a2c9b5b50e github.com/Microsoft/go-winio 1a8911d1ed007260465c3bfbbc785ac6915a0bb8 github.com/Nvveen/Gotty cd527374f1e5bff4938207604a14f2e38a9cf512 @@ -14,8 +13,8 @@ github.com/opencontainers/go-digest ac19fd6e7483ff933754af248d80be865e543d22 github.com/opencontainers/image-spec 243ea084a44451d27322fed02b682d99e2af3ba9 github.com/opencontainers/runc 923a8f8a9a07aceada5fc48c4d37e905d9b019b5 github.com/pkg/errors 27936f6d90f9c8e1145f11ed52ffffbfdb9e0af7 +github.com/pquerna/ffjson d49c2bc1aa135aad0c6f4fc2056623ec78f5d5ac github.com/sirupsen/logrus d7b6bf5e4d26448fd977d07d745a2a66097ddecb golang.org/x/crypto ff983b9c42bc9fbf91556e191cc8efb585c16908 golang.org/x/net 45ffb0cd1ba084b73e26dee67e667e1be5acce83 -golang.org/x/sync 37e7f081c4d4c64e13b10787722085407fe5d15f golang.org/x/sys 7fbe1cd0fcc20051e1fcb87fbabec4a1bacaaeba diff --git a/vendor/github.com/varlink/go/varlink/bridge_windows.go b/vendor/github.com/varlink/go/varlink/bridge_windows.go index 220ae3156..751224ec8 100644 --- a/vendor/github.com/varlink/go/varlink/bridge_windows.go +++ b/vendor/github.com/varlink/go/varlink/bridge_windows.go @@ -44,8 +44,8 @@ func NewBridge(bridge string) (*Connection, error) { } c.conn = PipeCon{nil, cmd, &r, &w} c.address = "" - c.reader = bufio.NewReader(r) - c.writer = bufio.NewWriter(w) + c.Reader = bufio.NewReader(r) + c.Writer = bufio.NewWriter(w) err = cmd.Start() if err != nil { diff --git a/vendor/github.com/varlink/go/varlink/call.go b/vendor/github.com/varlink/go/varlink/call.go index d6e046f1d..0eaf24aca 100644 --- a/vendor/github.com/varlink/go/varlink/call.go +++ b/vendor/github.com/varlink/go/varlink/call.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "net" "strings" ) @@ -14,36 +15,38 @@ import ( type Call struct { *bufio.Reader *bufio.Writer - in *serviceCall + Conn *net.Conn + Request *[]byte + In *serviceCall Continues bool Upgrade bool } // WantsMore indicates if the calling client accepts more than one reply to this method call. func (c *Call) WantsMore() bool { - return c.in.More + return c.In.More } // WantsUpgrade indicates that the calling client wants the connection to be upgraded. func (c *Call) WantsUpgrade() bool { - return c.in.Upgrade + return c.In.Upgrade } // IsOneway indicate that the calling client does not expect a reply. func (c *Call) IsOneway() bool { - return c.in.Oneway + return c.In.Oneway } // GetParameters retrieves the method call parameters. func (c *Call) GetParameters(p interface{}) error { - if c.in.Parameters == nil { + if c.In.Parameters == nil { return fmt.Errorf("empty parameters") } - return json.Unmarshal(*c.in.Parameters, p) + return json.Unmarshal(*c.In.Parameters, p) } func (c *Call) sendMessage(r *serviceReply) error { - if c.in.Oneway { + if c.In.Oneway { return nil } @@ -75,7 +78,7 @@ func (c *Call) Reply(parameters interface{}) error { }) } - if !c.in.More { + if !c.In.More { return fmt.Errorf("call did not set more, it does not expect continues") } diff --git a/vendor/github.com/varlink/go/varlink/service.go b/vendor/github.com/varlink/go/varlink/service.go index abccffe6a..bf13aa1de 100644 --- a/vendor/github.com/varlink/go/varlink/service.go +++ b/vendor/github.com/varlink/go/varlink/service.go @@ -74,7 +74,7 @@ func (s *Service) getInterfaceDescription(c Call, name string) error { return c.replyGetInterfaceDescription(description) } -func (s *Service) handleMessage(reader *bufio.Reader, writer *bufio.Writer, request []byte) error { +func (s *Service) HandleMessage(conn *net.Conn, reader *bufio.Reader, writer *bufio.Writer, request []byte) error { var in serviceCall err := json.Unmarshal(request, &in) @@ -84,9 +84,11 @@ func (s *Service) handleMessage(reader *bufio.Reader, writer *bufio.Writer, requ } c := Call{ - Reader: reader, - Writer: writer, - in: &in, + Conn: conn, + Reader: reader, + Writer: writer, + In: &in, + Request: &request, } r := strings.LastIndex(in.Method, ".") @@ -131,7 +133,7 @@ func (s *Service) handleConnection(conn net.Conn, wg *sync.WaitGroup) { break } - err = s.handleMessage(reader, writer, request[:len(request)-1]) + err = s.HandleMessage(&conn, reader, writer, request[:len(request)-1]) if err != nil { // FIXME: report error //fmt.Fprintf(os.Stderr, "handleMessage: %v", err) @@ -179,25 +181,36 @@ func (s *Service) parseAddress(address string) error { return nil } -func getListener(protocol string, address string) (net.Listener, error) { +func (s *Service) GetListener() (*net.Listener, error) { + s.mutex.Lock() + l := s.listener + s.mutex.Unlock() + return &l, nil +} + +func (s *Service) setListener() error { l := activationListener() if l == nil { - if protocol == "unix" && address[0] != '@' { - os.Remove(address) + if s.protocol == "unix" && s.address[0] != '@' { + os.Remove(s.address) } var err error - l, err = net.Listen(protocol, address) + l, err = net.Listen(s.protocol, s.address) if err != nil { - return nil, err + return err } - if protocol == "unix" && address[0] != '@' { + if s.protocol == "unix" && s.address[0] != '@' { l.(*net.UnixListener).SetUnlinkOnClose(true) } } - return l, nil + s.mutex.Lock() + s.listener = l + s.mutex.Unlock() + + return nil } func (s *Service) refreshTimeout(timeout time.Duration) error { @@ -216,26 +229,84 @@ func (s *Service) refreshTimeout(timeout time.Duration) error { } // Listen starts a Service. -func (s *Service) Listen(address string, timeout time.Duration) error { - var wg sync.WaitGroup - defer func() { s.teardown(); wg.Wait() }() - +func (s *Service) Bind(address string) error { s.mutex.Lock() if s.running { s.mutex.Unlock() - return fmt.Errorf("Listen(): already running") + return fmt.Errorf("Init(): already running") } s.mutex.Unlock() s.parseAddress(address) - l, err := getListener(s.protocol, s.address) + err := s.setListener() + if err != nil { + return err + } + return nil +} + +// Listen starts a Service. +func (s *Service) Listen(address string, timeout time.Duration) error { + var wg sync.WaitGroup + defer func() { s.teardown(); wg.Wait() }() + + err := s.Bind(address) if err != nil { return err } s.mutex.Lock() - s.listener = l + s.running = true + l := s.listener + s.mutex.Unlock() + + for s.running { + if timeout != 0 { + if err := s.refreshTimeout(timeout); err != nil { + return err + } + } + conn, err := l.Accept() + if err != nil { + if err.(net.Error).Timeout() { + s.mutex.Lock() + if s.conncounter == 0 { + s.mutex.Unlock() + return ServiceTimeoutError{} + } + s.mutex.Unlock() + continue + } + if !s.running { + return nil + } + return err + } + s.mutex.Lock() + s.conncounter++ + s.mutex.Unlock() + wg.Add(1) + go s.handleConnection(conn, &wg) + } + + return nil +} + +// Listen starts a Service. +func (s *Service) DoListen(timeout time.Duration) error { + var wg sync.WaitGroup + defer func() { s.teardown(); wg.Wait() }() + + s.mutex.Lock() + l := s.listener + s.mutex.Unlock() + + if l == nil { + return fmt.Errorf("No listener set") + } + + s.mutex.Lock() s.running = true s.mutex.Unlock() diff --git a/vendor/github.com/varlink/go/varlink/varlink_test.go b/vendor/github.com/varlink/go/varlink/varlink_test.go index 9dd4ddc63..9e6d0a1f4 100644 --- a/vendor/github.com/varlink/go/varlink/varlink_test.go +++ b/vendor/github.com/varlink/go/varlink/varlink_test.go @@ -31,7 +31,7 @@ func TestService(t *testing.T) { r := bufio.NewReader(&br) var b bytes.Buffer w := bufio.NewWriter(&b) - if err := service.handleMessage(r, w, []byte{0}); err == nil { + if err := service.HandleMessage(nil, r, w, []byte{0}); err == nil { t.Fatal("HandleMessage returned non-error") } }) @@ -42,7 +42,7 @@ func TestService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"foo.GetInterfaceDescription" fdgdfg}`) - if err := service.handleMessage(r, w, msg); err == nil { + if err := service.HandleMessage(nil, r, w, msg); err == nil { t.Fatal("HandleMessage returned no error on invalid json") } }) @@ -53,7 +53,7 @@ func TestService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"foo.GetInterfaceDescription"}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatal("HandleMessage returned error on wrong interface") } expect(t, `{"parameters":{"interface":"foo"},"error":"org.varlink.service.InterfaceNotFound"}`+"\000", @@ -66,7 +66,7 @@ func TestService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"InvalidMethod"}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatal("HandleMessage returned error on invalid method") } expect(t, `{"parameters":{"parameter":"method"},"error":"org.varlink.service.InvalidParameter"}`+"\000", @@ -79,7 +79,7 @@ func TestService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"org.varlink.service.WrongMethod"}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatal("HandleMessage returned error on wrong method") } expect(t, `{"parameters":{"method":"WrongMethod"},"error":"org.varlink.service.MethodNotFound"}`+"\000", @@ -92,7 +92,7 @@ func TestService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"org.varlink.service.GetInterfaceDescription","parameters": null}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatalf("HandleMessage returned error: %v", err) } expect(t, `{"parameters":{"parameter":"parameters"},"error":"org.varlink.service.InvalidParameter"}`+"\000", @@ -105,7 +105,7 @@ func TestService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"org.varlink.service.GetInterfaceDescription","parameters":{}}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatalf("HandleMessage returned error: %v", err) } expect(t, `{"parameters":{"parameter":"interface"},"error":"org.varlink.service.InvalidParameter"}`+"\000", @@ -118,7 +118,7 @@ func TestService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"org.varlink.service.GetInterfaceDescription","parameters":{"interface":"foo"}}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatalf("HandleMessage returned error: %v", err) } expect(t, `{"parameters":{"parameter":"interface"},"error":"org.varlink.service.InvalidParameter"}`+"\000", @@ -131,7 +131,7 @@ func TestService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"org.varlink.service.GetInterfaceDescription","parameters":{"interface":"org.varlink.service"}}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatalf("HandleMessage returned error: %v", err) } expect(t, `{"parameters":{"description":"# The Varlink Service Interface is provided by every varlink service. It\n# describes the service and the interfaces it implements.\ninterface org.varlink.service\n\n# Get a list of all the interfaces a service provides and information\n# about the implementation.\nmethod GetInfo() -\u003e (\n vendor: string,\n product: string,\n version: string,\n url: string,\n interfaces: []string\n)\n\n# Get the description of an interface that is implemented by this service.\nmethod GetInterfaceDescription(interface: string) -\u003e (description: string)\n\n# The requested interface was not found.\nerror InterfaceNotFound (interface: string)\n\n# The requested method was not found\nerror MethodNotFound (method: string)\n\n# The interface defines the requested method, but the service does not\n# implement it.\nerror MethodNotImplemented (method: string)\n\n# One of the passed parameters is invalid.\nerror InvalidParameter (parameter: string)"}}`+"\000", @@ -144,7 +144,7 @@ func TestService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"org.varlink.service.GetInfo"}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatalf("HandleMessage returned error: %v", err) } expect(t, `{"parameters":{"vendor":"Varlink","product":"Varlink Test","version":"1","url":"https://github.com/varlink/go/varlink","interfaces":["org.varlink.service"]}}`+"\000", @@ -224,7 +224,7 @@ func TestMoreService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"org.example.test.Pingf"}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatalf("HandleMessage returned error: %v", err) } expect(t, `{"parameters":{"method":"Pingf"},"error":"org.varlink.service.MethodNotImplemented"}`+"\000", @@ -237,7 +237,7 @@ func TestMoreService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"org.example.test.PingError", "more" : true}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatalf("HandleMessage returned error: %v", err) } expect(t, `{"error":"org.example.test.PingError"}`+"\000", @@ -249,7 +249,7 @@ func TestMoreService(t *testing.T) { var b bytes.Buffer w := bufio.NewWriter(&b) msg := []byte(`{"method":"org.example.test.Ping", "more" : true}`) - if err := service.handleMessage(r, w, msg); err != nil { + if err := service.HandleMessage(nil, r, w, msg); err != nil { t.Fatalf("HandleMessage returned error: %v", err) } expect(t, `{"continues":true}`+"\000"+`{"continues":true}`+"\000"+`{}`+"\000", |