diff options
54 files changed, 1872 insertions, 551 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index e9e843be6..70c3cb3da 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -13,6 +13,8 @@ env: #### #### Global variables used for all tasks #### + # Name of the ultimate destination branch for this build + DEST_BRANCH: "master" # Overrides default location (/tmp/cirrus) for repo clone GOPATH: "/var/tmp/go" GOSRC: "/var/tmp/go/src/github.com/containers/libpod" @@ -29,9 +31,9 @@ env: #### #### Cache-image names to test with ### - FEDORA_CACHE_IMAGE_NAME: "fedora-30-libpod-5744029755506688" - PRIOR_FEDORA_CACHE_IMAGE_NAME: "fedora-29-libpod-5744029755506688" - UBUNTU_CACHE_IMAGE_NAME: "ubuntu-18-libpod-5744029755506688" + FEDORA_CACHE_IMAGE_NAME: "fedora-30-libpod-5925244995371008" + PRIOR_FEDORA_CACHE_IMAGE_NAME: "fedora-29-libpod-5925244995371008" + UBUNTU_CACHE_IMAGE_NAME: "ubuntu-18-libpod-5925244995371008" #### #### Variables for composing new cache-images (used in PR testing) from @@ -118,7 +120,7 @@ gating_task: pipefail_enabledscript: 'if /bin/false | /bin/true; then echo "pipefail fault" && exit 72; fi' on_failure: - failed_master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh' + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh' # This task runs `make vendor` followed by ./hack/tree_status.sh to check @@ -150,7 +152,7 @@ vendor_task: - 'cd ${GOSRC} && ./hack/tree_status.sh |& ${TIMESTAMP}' on_failure: - failed_master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh |& ${TIMESTAMP}' + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh |& ${TIMESTAMP}' # This task runs `make varlink_api_generate` followed by ./hack/tree_status.sh to check @@ -182,7 +184,7 @@ varlink_api_task: - 'cd ${GOSRC} && ./hack/tree_status.sh |& ${TIMESTAMP}' on_failure: - failed_master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh' + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh' build_each_commit_task: @@ -193,7 +195,7 @@ build_each_commit_task: - "varlink_api" # $CIRRUS_BASE_BRANCH is only set when testing a PR - only_if: $CIRRUS_BRANCH != 'master' && + only_if: $CIRRUS_BRANCH != $DEST_BRANCH && $CIRRUS_CHANGE_MESSAGE !=~ '.*\*\*\*\s*CIRRUS:\s*TEST\s*IMAGES\s*\*\*\*.*' gce_instance: @@ -213,7 +215,7 @@ build_each_commit_task: - 'make build-all-new-commits GIT_BASE_BRANCH=origin/$CIRRUS_BASE_BRANCH |& ${TIMESTAMP}' on_failure: - failed_master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh' + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh' build_without_cgo_task: @@ -224,7 +226,7 @@ build_without_cgo_task: - "varlink_api" # $CIRRUS_BASE_BRANCH is only set when testing a PR - only_if: $CIRRUS_BRANCH != 'master' && + only_if: $CIRRUS_BRANCH != $DEST_BRANCH && $CIRRUS_CHANGE_MESSAGE !=~ '.*\*\*\*\s*CIRRUS:\s*TEST\s*IMAGES\s*\*\*\*.*' gce_instance: @@ -243,7 +245,7 @@ build_without_cgo_task: - 'make build-no-cgo' on_failure: - failed_master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh' + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh' # Update metadata on VM images referenced by this repository state @@ -261,7 +263,7 @@ meta_task: cpu: 1 memory: 1 - env: + env: &meta_env_vars # Space-separated list of images used by this repository state IMGNAMES: >- ${FEDORA_CACHE_IMAGE_NAME} @@ -277,6 +279,31 @@ meta_task: timeout_in: 10m + # Cirrus-CI ignores entrypoint defined in image + script: '/usr/local/bin/entrypoint.sh |& ${TIMESTAMP}' + + +# Remove old and disused images based on labels set by meta_task +image_prune_task: + + # Do not run this frequently + only_if: $CIRRUS_BRANCH == 'master' + + depends_on: + - "meta" + + container: + image: "quay.io/libpod/imgprune:latest" # see contrib/imgprune + cpu: 1 + memory: 1 + + env: + <<: *meta_env_vars + GCPJSON: ENCRYPTED[4c11d8e09c904c30fc70eecb95c73dec0ddf19976f9b981a0f80f3f6599e8f990bcef93c253ac0277f200850d98528e7] + GCPNAME: ENCRYPTED[7f54557ba6e5a437f11283a53e71baec9ca546f48a9835538cc54d297f79968eb1337d4596a1025b14f9d1c5723fbd29] + + timeout_in: 10m + script: '/usr/local/bin/entrypoint.sh |& ${TIMESTAMP}' @@ -316,7 +343,7 @@ testing_task: $SCRIPT_BASE/cache_release_archive.sh |& ${TIMESTAMP} on_failure: - failed_master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh' + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh' always: &standardlogs ginkgo_node_logs_script: '$SCRIPT_BASE/logcollector.sh ginkgo' @@ -351,7 +378,7 @@ special_testing_rootless_task: system_test_script: '$SCRIPT_BASE/system_test.sh |& ${TIMESTAMP}' on_failure: - failed_master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh' + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh' always: <<: *standardlogs @@ -377,7 +404,7 @@ special_testing_in_podman_task: integration_test_script: '$SCRIPT_BASE/integration_test.sh |& ${TIMESTAMP}' on_failure: - failed_master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh' + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh' always: <<: *standardlogs @@ -403,14 +430,14 @@ special_testing_cross_task: cache_release_archive_script: '$SCRIPT_BASE/cache_release_archive.sh |& ${TIMESTAMP}' on_failure: - failed_master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh' + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh' # Test building of new cache-images for future PR testing, in this PR. test_build_cache_images_task: only_if: >- - $CIRRUS_BRANCH != 'master' && + $CIRRUS_BRANCH != $DEST_BRANCH && $CIRRUS_CHANGE_MESSAGE =~ '.*\*\*\*\s*CIRRUS:\s*TEST\s*IMAGES\s*\*\*\*.*' && $CIRRUS_CHANGE_MESSAGE !=~ '.*\*\*\*\s*CIRRUS:\s*SYSTEM\s*TEST\s*\*\*\*.*' @@ -445,7 +472,7 @@ test_build_cache_images_task: verify_test_built_images_task: only_if: >- - $CIRRUS_BRANCH != 'master' && + $CIRRUS_BRANCH != $DEST_BRANCH && $CIRRUS_CHANGE_MESSAGE =~ '.*\*\*\*\s*CIRRUS:\s*TEST\s*IMAGES\s*\*\*\*.*' && $CIRRUS_CHANGE_MESSAGE !=~ '.*\*\*\*\s*CIRRUS:\s*SYSTEM\s*TEST\s*\*\*\*.*' @@ -480,7 +507,9 @@ verify_test_built_images_task: # Post message to IRC if everything passed PR testing success_task: - only_if: $CIRRUS_BRANCH != 'master' + # This task is a required-pass in github settings, + # it blocks PRs from merging if a depends_on task fails + only_if: $CIRRUS_BRANCH != $DEST_BRANCH # ignores any dependent task conditions, include everything except 'release' depends_on: &alltasks @@ -530,3 +559,6 @@ release_task: GCPROJECT: ENCRYPTED[7c80e728e046b1c76147afd156a32c1c57d4a1ac1eab93b7e68e718c61ca8564fc61fef815952b8ae0a64e7034b8fe4f] uncache_release_archives_script: '$SCRIPT_BASE/uncache_release_archives.sh |& ${TIMESTAMP}' + + on_failure: + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh' @@ -20,6 +20,7 @@ SHAREDIR_CONTAINERS ?= ${PREFIX}/share/containers ETCDIR ?= /etc TMPFILESDIR ?= ${PREFIX}/lib/tmpfiles.d SYSTEMDDIR ?= ${PREFIX}/lib/systemd/system +BUILDFLAGS ?= BUILDTAGS ?= \ $(shell hack/apparmor_tag.sh) \ $(shell hack/btrfs_installed_tag.sh) \ @@ -126,8 +127,8 @@ help: .gopathok: ifeq ("$(wildcard $(GOPKGDIR))","") mkdir -p "$(GOPKGBASEDIR)" - ln -sf "$(CURDIR)" "$(GOPKGBASEDIR)" - ln -sf "$(CURDIR)/vendor/github.com/varlink" "$(FIRST_GOPATH)/src/github.com/varlink" + ln -sfnT "$(CURDIR)" "$(GOPKGDIR)" + ln -sfnT "$(CURDIR)/vendor/github.com/varlink" "$(FIRST_GOPATH)/src/github.com/varlink" endif touch $@ @@ -147,10 +148,10 @@ test/goecho/goecho: .gopathok $(wildcard test/goecho/*.go) $(GO) build -ldflags '$(LDFLAGS)' -o $@ $(PROJECT)/test/goecho podman: .gopathok $(PODMAN_VARLINK_DEPENDENCIES) ## Build with podman - $(GO) build -gcflags '$(GCFLAGS)' -asmflags '$(ASMFLAGS)' -ldflags '$(LDFLAGS_PODMAN)' -tags "$(BUILDTAGS)" -o bin/$@ $(PROJECT)/cmd/podman + $(GO) build $(BUILDFLAGS) -gcflags '$(GCFLAGS)' -asmflags '$(ASMFLAGS)' -ldflags '$(LDFLAGS_PODMAN)' -tags "$(BUILDTAGS)" -o bin/$@ $(PROJECT)/cmd/podman podman-remote: .gopathok $(PODMAN_VARLINK_DEPENDENCIES) ## Build with podman on remote environment - $(GO) build -gcflags '$(GCFLAGS)' -asmflags '$(ASMFLAGS)' -ldflags '$(LDFLAGS_PODMAN)' -tags "$(BUILDTAGS) remoteclient" -o bin/$@ $(PROJECT)/cmd/podman + $(GO) build $(BUILDFLAGS) -gcflags '$(GCFLAGS)' -asmflags '$(ASMFLAGS)' -ldflags '$(LDFLAGS_PODMAN)' -tags "$(BUILDTAGS) remoteclient" -o bin/$@ $(PROJECT)/cmd/podman podman-remote-darwin: .gopathok $(PODMAN_VARLINK_DEPENDENCIES) ## Build with podman on remote OSX environment CGO_ENABLED=0 GOOS=darwin $(GO) build -gcflags '$(GCFLAGS)' -asmflags '$(ASMFLAGS)' -ldflags '$(LDFLAGS_PODMAN)' -tags "remoteclient containers_image_openpgp exclude_graphdriver_devicemapper" -o bin/$@ $(PROJECT)/cmd/podman diff --git a/cmd/podman/common.go b/cmd/podman/common.go index 96a1c2244..15f753d55 100644 --- a/cmd/podman/common.go +++ b/cmd/podman/common.go @@ -244,23 +244,23 @@ func getCreateFlags(c *cliconfig.PodmanCommand) { "help", false, "", ) createFlags.String( - "healthcheck-command", "", + "health-cmd", "", "set a healthcheck command for the container ('none' disables the existing healthcheck)", ) createFlags.String( - "healthcheck-interval", cliconfig.DefaultHealthCheckInterval, + "health-interval", cliconfig.DefaultHealthCheckInterval, "set an interval for the healthchecks (a value of disable results in no automatic timer setup)", ) createFlags.Uint( - "healthcheck-retries", cliconfig.DefaultHealthCheckRetries, + "health-retries", cliconfig.DefaultHealthCheckRetries, "the number of retries allowed before a healthcheck is considered to be unhealthy", ) createFlags.String( - "healthcheck-start-period", cliconfig.DefaultHealthCheckStartPeriod, + "health-start-period", cliconfig.DefaultHealthCheckStartPeriod, "the initialization time needed for a container to bootstrap", ) createFlags.String( - "healthcheck-timeout", cliconfig.DefaultHealthCheckTimeout, + "health-timeout", cliconfig.DefaultHealthCheckTimeout, "the maximum time allowed to complete the healthcheck before an interval is considered failed", ) createFlags.StringP( diff --git a/cmd/podman/create.go b/cmd/podman/create.go index 93141a800..262cdffe4 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -40,7 +40,7 @@ func init() { getCreateFlags(&createCommand.PodmanCommand) flags := createCommand.Flags() flags.SetInterspersed(false) - + flags.SetNormalizeFunc(aliasFlags) } func createCmd(c *cliconfig.CreateValues) error { diff --git a/cmd/podman/inspect.go b/cmd/podman/inspect.go index df597c868..12d89764c 100644 --- a/cmd/podman/inspect.go +++ b/cmd/podman/inspect.go @@ -6,9 +6,7 @@ import ( "github.com/containers/buildah/pkg/formats" "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/pkg/adapter" - cc "github.com/containers/libpod/pkg/spec" "github.com/containers/libpod/pkg/util" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -148,19 +146,9 @@ func iterateInput(ctx context.Context, size bool, args []string, runtime *adapte inspectError = errors.Wrapf(err, "error looking up container %q", input) break } - libpodInspectData, err := ctr.Inspect(size) + data, err = ctr.Inspect(size) if err != nil { - inspectError = errors.Wrapf(err, "error getting libpod container inspect data %s", ctr.ID()) - break - } - artifact, err := getArtifact(ctr) - if inspectError != nil { - inspectError = err - break - } - data, err = shared.GetCtrInspectInfo(ctr.Config(), libpodInspectData, artifact) - if err != nil { - inspectError = errors.Wrapf(err, "error parsing container data %q", ctr.ID()) + inspectError = errors.Wrapf(err, "error inspecting container %s", ctr.ID()) break } case inspectTypeImage: @@ -188,19 +176,9 @@ func iterateInput(ctx context.Context, size bool, args []string, runtime *adapte break } } else { - libpodInspectData, err := ctr.Inspect(size) - if err != nil { - inspectError = errors.Wrapf(err, "error getting libpod container inspect data %s", ctr.ID()) - break - } - artifact, err := getArtifact(ctr) + data, err = ctr.Inspect(size) if err != nil { - inspectError = err - break - } - data, err = shared.GetCtrInspectInfo(ctr.Config(), libpodInspectData, artifact) - if err != nil { - inspectError = errors.Wrapf(err, "error parsing container data %s", ctr.ID()) + inspectError = errors.Wrapf(err, "error inspecting container %s", ctr.ID()) break } } @@ -211,15 +189,3 @@ func iterateInput(ctx context.Context, size bool, args []string, runtime *adapte } return inspectedItems, inspectError } - -func getArtifact(ctr *adapter.Container) (*cc.CreateConfig, error) { - var createArtifact cc.CreateConfig - artifact, err := ctr.GetArtifact("create-config") - if err != nil { - return nil, err - } - if err := json.Unmarshal(artifact, &createArtifact); err != nil { - return nil, err - } - return &createArtifact, nil -} diff --git a/cmd/podman/run.go b/cmd/podman/run.go index 76ab3d944..4836c99dc 100644 --- a/cmd/podman/run.go +++ b/cmd/podman/run.go @@ -34,6 +34,7 @@ func init() { runCommand.SetUsageTemplate(UsageTemplate()) flags := runCommand.Flags() flags.SetInterspersed(false) + flags.SetNormalizeFunc(aliasFlags) flags.Bool("sig-proxy", true, "Proxy received signals to the process") getCreateFlags(&runCommand.PodmanCommand) markFlagHiddenForRemoteClient("authfile", flags) diff --git a/cmd/podman/shared/container_inspect.go b/cmd/podman/shared/container_inspect.go deleted file mode 100644 index a8094466e..000000000 --- a/cmd/podman/shared/container_inspect.go +++ /dev/null @@ -1,211 +0,0 @@ -package shared - -import ( - "github.com/containers/libpod/libpod" - cc "github.com/containers/libpod/pkg/spec" - "github.com/docker/go-connections/nat" - "github.com/opencontainers/runtime-spec/specs-go" -) - -// InspectContainer holds all inspect data for a container. -// The format of individual components is fixed so the overall structure, when -// JSON encoded, matches the output of `docker inspect`. -// It combines Libpod-source inspect data with Podman-specific inspect data. -type InspectContainer struct { - *libpod.InspectContainerData - HostConfig *InspectContainerHostConfig `json:"HostConfig"` -} - -// InspectContainerHostConfig holds Container configuration that is not specific -// to Libpod. This information is (mostly) stored by Podman as an artifact. -// This struct is matched to the output of `docker inspect`. -type InspectContainerHostConfig struct { - ContainerIDFile string `json:"ContainerIDFile"` - LogConfig *InspectLogConfig `json:"LogConfig"` //TODO - NetworkMode string `json:"NetworkMode"` - PortBindings nat.PortMap `json:"PortBindings"` //TODO - AutoRemove bool `json:"AutoRemove"` - CapAdd []string `json:"CapAdd"` - CapDrop []string `json:"CapDrop"` - DNS []string `json:"DNS"` - DNSOptions []string `json:"DNSOptions"` - DNSSearch []string `json:"DNSSearch"` - ExtraHosts []string `json:"ExtraHosts"` - GroupAdd []uint32 `json:"GroupAdd"` - IpcMode string `json:"IpcMode"` - Cgroup string `json:"Cgroup"` - OomScoreAdj *int `json:"OomScoreAdj"` - PidMode string `json:"PidMode"` - Privileged bool `json:"Privileged"` - PublishAllPorts bool `json:"PublishAllPorts"` //TODO - ReadOnlyRootfs bool `json:"ReadonlyRootfs"` - ReadOnlyTmpfs bool `json:"ReadonlyTmpfs"` - SecurityOpt []string `json:"SecurityOpt"` - UTSMode string `json:"UTSMode"` - UsernsMode string `json:"UsernsMode"` - ShmSize int64 `json:"ShmSize"` - Runtime string `json:"Runtime"` - ConsoleSize *specs.Box `json:"ConsoleSize"` - CPUShares *uint64 `json:"CpuShares"` - Memory int64 `json:"Memory"` - NanoCPUs int `json:"NanoCpus"` - CgroupParent string `json:"CgroupParent"` - BlkioWeight *uint16 `json:"BlkioWeight"` - BlkioWeightDevice []specs.LinuxWeightDevice `json:"BlkioWeightDevice"` - BlkioDeviceReadBps []specs.LinuxThrottleDevice `json:"BlkioDeviceReadBps"` - BlkioDeviceWriteBps []specs.LinuxThrottleDevice `json:"BlkioDeviceWriteBps"` - BlkioDeviceReadIOps []specs.LinuxThrottleDevice `json:"BlkioDeviceReadIOps"` - BlkioDeviceWriteIOps []specs.LinuxThrottleDevice `json:"BlkioDeviceWriteIOps"` - CPUPeriod *uint64 `json:"CpuPeriod"` - CPUQuota *int64 `json:"CpuQuota"` - CPURealtimePeriod *uint64 `json:"CpuRealtimePeriod"` - CPURealtimeRuntime *int64 `json:"CpuRealtimeRuntime"` - CPUSetCPUs string `json:"CpuSetCpus"` - CPUSetMems string `json:"CpuSetMems"` - Devices []specs.LinuxDevice `json:"Devices"` - DiskQuota int `json:"DiskQuota"` //check type, TODO - KernelMemory *int64 `json:"KernelMemory"` - MemoryReservation *int64 `json:"MemoryReservation"` - MemorySwap *int64 `json:"MemorySwap"` - MemorySwappiness *uint64 `json:"MemorySwappiness"` - OomKillDisable *bool `json:"OomKillDisable"` - PidsLimit *int64 `json:"PidsLimit"` - Ulimits []string `json:"Ulimits"` - CPUCount int `json:"CpuCount"` - CPUPercent int `json:"CpuPercent"` - IOMaximumIOps int `json:"IOMaximumIOps"` //check type, TODO - IOMaximumBandwidth int `json:"IOMaximumBandwidth"` //check type, TODO - Tmpfs []string `json:"Tmpfs"` -} - -// InspectLogConfig holds information about a container's configured log driver -// and is presently unused. It is retained for Docker compatibility. -type InspectLogConfig struct { - Type string `json:"Type"` - Config map[string]string `json:"Config"` //idk type, TODO -} - -// GetCtrInspectInfo inspects a container, combining Libpod inspect information -// with other information not stored in Libpod and returning a struct that, when -// formatted for JSON output, is compatible with `docker inspect`. -func GetCtrInspectInfo(config *libpod.ContainerConfig, ctrInspectData *libpod.InspectContainerData, createArtifact *cc.CreateConfig) (*InspectContainer, error) { - spec := config.Spec - - cpus, mems, period, quota, realtimePeriod, realtimeRuntime, shares := getCPUInfo(spec) - blkioWeight, blkioWeightDevice, blkioReadBps, blkioWriteBps, blkioReadIOPS, blkioeWriteIOPS := getBLKIOInfo(spec) - memKernel, memReservation, memSwap, memSwappiness, memDisableOOMKiller := getMemoryInfo(spec) - pidsLimit := getPidsInfo(spec) - cgroup := getCgroup(spec) - logConfig := InspectLogConfig{ - config.LogDriver, - make(map[string]string), - } - - data := &InspectContainer{ - ctrInspectData, - &InspectContainerHostConfig{ - ConsoleSize: spec.Process.ConsoleSize, - OomScoreAdj: spec.Process.OOMScoreAdj, - CPUShares: shares, - BlkioWeight: blkioWeight, - BlkioWeightDevice: blkioWeightDevice, - BlkioDeviceReadBps: blkioReadBps, - BlkioDeviceWriteBps: blkioWriteBps, - BlkioDeviceReadIOps: blkioReadIOPS, - BlkioDeviceWriteIOps: blkioeWriteIOPS, - CPUPeriod: period, - CPUQuota: quota, - CPURealtimePeriod: realtimePeriod, - CPURealtimeRuntime: realtimeRuntime, - CPUSetCPUs: cpus, - CPUSetMems: mems, - Devices: spec.Linux.Devices, - KernelMemory: memKernel, - LogConfig: &logConfig, - MemoryReservation: memReservation, - MemorySwap: memSwap, - MemorySwappiness: memSwappiness, - OomKillDisable: memDisableOOMKiller, - PidsLimit: pidsLimit, - Privileged: config.Privileged, - ReadOnlyRootfs: spec.Root.Readonly, - ReadOnlyTmpfs: createArtifact.ReadOnlyTmpfs, - Runtime: config.OCIRuntime, - NetworkMode: string(createArtifact.NetMode), - IpcMode: string(createArtifact.IpcMode), - Cgroup: cgroup, - UTSMode: string(createArtifact.UtsMode), - UsernsMode: string(createArtifact.UsernsMode), - GroupAdd: spec.Process.User.AdditionalGids, - ContainerIDFile: createArtifact.CidFile, - AutoRemove: createArtifact.Rm, - CapAdd: createArtifact.CapAdd, - CapDrop: createArtifact.CapDrop, - DNS: createArtifact.DNSServers, - DNSOptions: createArtifact.DNSOpt, - DNSSearch: createArtifact.DNSSearch, - PidMode: string(createArtifact.PidMode), - CgroupParent: createArtifact.CgroupParent, - ShmSize: createArtifact.Resources.ShmSize, - Memory: createArtifact.Resources.Memory, - Ulimits: createArtifact.Resources.Ulimit, - SecurityOpt: createArtifact.SecurityOpts, - Tmpfs: createArtifact.Tmpfs, - }, - } - return data, nil -} - -func getCPUInfo(spec *specs.Spec) (string, string, *uint64, *int64, *uint64, *int64, *uint64) { - if spec.Linux.Resources == nil { - return "", "", nil, nil, nil, nil, nil - } - cpu := spec.Linux.Resources.CPU - if cpu == nil { - return "", "", nil, nil, nil, nil, nil - } - return cpu.Cpus, cpu.Mems, cpu.Period, cpu.Quota, cpu.RealtimePeriod, cpu.RealtimeRuntime, cpu.Shares -} - -func getBLKIOInfo(spec *specs.Spec) (*uint16, []specs.LinuxWeightDevice, []specs.LinuxThrottleDevice, []specs.LinuxThrottleDevice, []specs.LinuxThrottleDevice, []specs.LinuxThrottleDevice) { - if spec.Linux.Resources == nil { - return nil, nil, nil, nil, nil, nil - } - blkio := spec.Linux.Resources.BlockIO - if blkio == nil { - return nil, nil, nil, nil, nil, nil - } - return blkio.Weight, blkio.WeightDevice, blkio.ThrottleReadBpsDevice, blkio.ThrottleWriteBpsDevice, blkio.ThrottleReadIOPSDevice, blkio.ThrottleWriteIOPSDevice -} - -func getMemoryInfo(spec *specs.Spec) (*int64, *int64, *int64, *uint64, *bool) { - if spec.Linux.Resources == nil { - return nil, nil, nil, nil, nil - } - memory := spec.Linux.Resources.Memory - if memory == nil { - return nil, nil, nil, nil, nil - } - return memory.Kernel, memory.Reservation, memory.Swap, memory.Swappiness, memory.DisableOOMKiller -} - -func getPidsInfo(spec *specs.Spec) *int64 { - if spec.Linux.Resources == nil { - return nil - } - pids := spec.Linux.Resources.Pids - if pids == nil { - return nil - } - return &pids.Limit -} - -func getCgroup(spec *specs.Spec) string { - cgroup := "host" - for _, ns := range spec.Linux.Namespaces { - if ns.Type == specs.CgroupNamespace && ns.Path != "" { - cgroup = "container" - } - } - return cgroup -} diff --git a/cmd/podman/shared/create.go b/cmd/podman/shared/create.go index be1a731cc..5c37a1875 100644 --- a/cmd/podman/shared/create.go +++ b/cmd/podman/shared/create.go @@ -26,7 +26,6 @@ import ( "github.com/docker/docker/pkg/signal" "github.com/docker/go-connections/nat" "github.com/docker/go-units" - "github.com/google/shlex" "github.com/opencontainers/selinux/go-selinux/label" "github.com/opentracing/opentracing-go" "github.com/pkg/errors" @@ -116,6 +115,30 @@ func CreateContainer(ctx context.Context, c *GenericCLIResults, runtime *libpod. if err != nil { return nil, nil, errors.Wrapf(err, "unable to get healthcheck for %s", c.InputArgs[0]) } + + if healthCheck != nil { + hcCommand := healthCheck.Test + if len(hcCommand) < 1 || hcCommand[0] == "" || hcCommand[0] == "NONE" { + // disable health check + healthCheck = nil + } else { + // apply defaults if image doesn't override them + if healthCheck.Interval == 0 { + healthCheck.Interval = 30 * time.Second + } + if healthCheck.Timeout == 0 { + healthCheck.Timeout = 30 * time.Second + } + /* Docker default is 0s, so the following would be a no-op + if healthCheck.StartPeriod == 0 { + healthCheck.StartPeriod = 0 * time.Second + } + */ + if healthCheck.Retries == 0 { + healthCheck.Retries = 3 + } + } + } } } } @@ -628,6 +651,7 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod. ImageVolumeType: c.String("image-volume"), CapAdd: c.StringSlice("cap-add"), CapDrop: c.StringSlice("cap-drop"), + CidFile: c.String("cidfile"), CgroupParent: c.String("cgroup-parent"), Command: command, Detach: c.Bool("detach"), @@ -743,14 +767,6 @@ func CreateContainerFromCreateConfig(r *libpod.Runtime, createConfig *cc.CreateC if err != nil { return nil, err } - - createConfigJSON, err := json.Marshal(createConfig) - if err != nil { - return nil, err - } - if err := ctr.AddArtifact("create-config", createConfigJSON); err != nil { - return nil, err - } return ctr, nil } @@ -788,9 +804,12 @@ func makeHealthCheckFromCli(c *GenericCLIResults) (*manifest.Schema2HealthConfig return nil, errors.New("Must define a healthcheck command for all healthchecks") } - cmd, err := shlex.Split(inCommand) + // first try to parse option value as JSON array of strings... + cmd := []string{} + err := json.Unmarshal([]byte(inCommand), &cmd) if err != nil { - return nil, errors.Wrap(err, "failed to parse healthcheck command") + // ...otherwise pass it to "/bin/sh -c" inside the container + cmd = []string{"CMD-SHELL", inCommand} } hc := manifest.Schema2HealthConfig{ Test: cmd, diff --git a/cmd/podman/shared/intermediate.go b/cmd/podman/shared/intermediate.go index 855f84086..8337dc647 100644 --- a/cmd/podman/shared/intermediate.go +++ b/cmd/podman/shared/intermediate.go @@ -399,11 +399,11 @@ func NewIntermediateLayer(c *cliconfig.PodmanCommand, remote bool) GenericCLIRes m["gidmap"] = newCRStringSlice(c, "gidmap") m["group-add"] = newCRStringSlice(c, "group-add") m["help"] = newCRBool(c, "help") - m["healthcheck-command"] = newCRString(c, "healthcheck-command") - m["healthcheck-interval"] = newCRString(c, "healthcheck-interval") - m["healthcheck-retries"] = newCRUint(c, "healthcheck-retries") - m["healthcheck-start-period"] = newCRString(c, "healthcheck-start-period") - m["healthcheck-timeout"] = newCRString(c, "healthcheck-timeout") + m["healthcheck-command"] = newCRString(c, "health-cmd") + m["healthcheck-interval"] = newCRString(c, "health-interval") + m["healthcheck-retries"] = newCRUint(c, "health-retries") + m["healthcheck-start-period"] = newCRString(c, "health-start-period") + m["healthcheck-timeout"] = newCRString(c, "health-timeout") m["hostname"] = newCRString(c, "hostname") m["http-proxy"] = newCRBool(c, "http-proxy") m["image-volume"] = newCRString(c, "image-volume") diff --git a/cmd/podman/utils.go b/cmd/podman/utils.go index 0790f673a..c0ddaba4e 100644 --- a/cmd/podman/utils.go +++ b/cmd/podman/utils.go @@ -47,3 +47,19 @@ func markFlagHidden(flags *pflag.FlagSet, flag string) { logrus.Errorf("unable to mark flag '%s' as hidden: %q", flag, err) } } + +func aliasFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { + switch name { + case "healthcheck-command": + name = "health-cmd" + case "healthcheck-interval": + name = "health-interval" + case "healthcheck-retries": + name = "health-retries" + case "healthcheck-start-period": + name = "health-start-period" + case "healthcheck-timeout": + name = "health-timeout" + } + return pflag.NormalizedName(name) +} diff --git a/completions/bash/podman b/completions/bash/podman index 2b9254d47..0703029ea 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -1812,10 +1812,10 @@ _podman_container_run() { --health-interval --health-retries --health-timeout + --health-start-period " boolean_options="$boolean_options --detach -d - --no-healthcheck --rm --sig-proxy=false " diff --git a/contrib/cirrus/build_vm_images.sh b/contrib/cirrus/build_vm_images.sh index f5d53a92e..74b10158c 100755 --- a/contrib/cirrus/build_vm_images.sh +++ b/contrib/cirrus/build_vm_images.sh @@ -3,7 +3,8 @@ set -e source $(dirname $0)/lib.sh -ENV_VARS='PACKER_BUILDS BUILT_IMAGE_SUFFIX UBUNTU_BASE_IMAGE FEDORA_BASE_IMAGE PRIOR_FEDORA_BASE_IMAGE SERVICE_ACCOUNT GCE_SSH_USERNAME GCP_PROJECT_ID PACKER_VER SCRIPT_BASE PACKER_BASE' +BASE_IMAGE_VARS='FEDORA_BASE_IMAGE PRIOR_FEDORA_BASE_IMAGE UBUNTU_BASE_IMAGE' +ENV_VARS="PACKER_BUILDS BUILT_IMAGE_SUFFIX $BASE_IMAGE_VARS SERVICE_ACCOUNT GCE_SSH_USERNAME GCP_PROJECT_ID PACKER_VER SCRIPT_BASE PACKER_BASE CIRRUS_BUILD_ID CIRRUS_CHANGE_IN_REPO" req_env_var $ENV_VARS # Must also be made available through make, into packer process export $ENV_VARS @@ -24,6 +25,20 @@ then fi cd "$GOSRC/$PACKER_BASE" +# Add/update labels on base-images used in this build to prevent premature deletion +ARGS=" +" +for base_image_var in $BASE_IMAGE_VARS +do + # See entrypoint.sh in contrib/imgts and contrib/imgprune + # These updates can take a while, run them in the background, check later + gcloud compute images update "$image" \ + --update-labels=last-used=$(date +%s) \ + --update-labels=build-id=$CIRRUS_BUILD_ID \ + --update-labels=repo-ref=$CIRRUS_CHANGE_IN_REPO \ + --update-labels=project=$GCP_PROJECT_ID \ + ${!base_image_var} & +done make libpod_images \ PACKER_BUILDS=$PACKER_BUILDS \ @@ -33,9 +48,31 @@ make libpod_images \ PACKER_BASE=$PACKER_BASE \ BUILT_IMAGE_SUFFIX=$BUILT_IMAGE_SUFFIX +# Separate PR-produced images from those produced on master. +if [[ "${CIRRUS_BRANCH:-}" == "master" ]] +then + POST_MERGE_BUCKET_SUFFIX="-master" +else + POST_MERGE_BUCKET_SUFFIX="" +fi + # When successful, upload manifest of produced images using a filename unique # to this build. URI="gs://packer-import${POST_MERGE_BUCKET_SUFFIX}/manifest${BUILT_IMAGE_SUFFIX}.json" gsutil cp packer-manifest.json "$URI" +# Ensure any background 'gcloud compute images update' processes finish +set +e # need 'wait' exit code to avoid race +while [[ -n "$(jobs)" ]] +do + wait -n + RET=$? + if [[ "$RET" -eq "127" ]] || \ # Avoid TOCTOU race w/ jobs + wait + [[ "$RET" -eq "0" ]] + then + continue + fi + die $RET "Required base-image metadata update failed" +done + echo "Finished. A JSON manifest of produced images is available at $URI" diff --git a/contrib/cirrus/check_image.sh b/contrib/cirrus/check_image.sh index 67e807d61..690a38119 100755 --- a/contrib/cirrus/check_image.sh +++ b/contrib/cirrus/check_image.sh @@ -36,4 +36,10 @@ do "$(systemctl list-unit-files --no-legend $REQ_UNIT)" = "$REQ_UNIT enabled" || let "RET+=1" done +# Exits zero if any unit matching pattern is running +UNIT_STATUS=$(systemctl is-active $EVIL_UNITS; echo $?) +item_test "No interfering background units are active:" \ + "$UNIT_STATUS" -ne "0" || let "RET+=1" + +echo "Total failed tests: $RET" exit $RET diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh index b2fcaa749..a9da3f4ce 100644 --- a/contrib/cirrus/lib.sh +++ b/contrib/cirrus/lib.sh @@ -45,6 +45,7 @@ fi # Defaults when not running under CI export CI="${CI:-false}" CIRRUS_CI="${CIRRUS_CI:-false}" +DEST_BRANCH="${DEST_BRANCH:-master}" CONTINUOUS_INTEGRATION="${CONTINUOUS_INTEGRATION:-false}" CIRRUS_REPO_NAME=${CIRRUS_REPO_NAME:-libpod} CIRRUS_BASE_SHA=${CIRRUS_BASE_SHA:-unknown$(date +%s)} # difficult to reliably discover @@ -77,6 +78,9 @@ ROOTLESS_ENV_RE='(CIRRUS_.+)|(ROOTLESS_.+)|(.+_IMAGE.*)|(.+_BASE)|(.*DIRPATH)|(. # Unsafe env. vars for display SECRET_ENV_RE='(IRCID)|(ACCOUNT)|(^GC[EP]..+)|(SSH)' +# Names of systemd units which should never be running +EVIL_UNITS="cron crond atd apt-daily-upgrade apt-daily fstrim motd-news systemd-tmpfiles-clean" + SPECIALMODE="${SPECIALMODE:-none}" TEST_REMOTE_CLIENT="${TEST_REMOTE_CLIENT:-false}" export CONTAINER_RUNTIME=${CONTAINER_RUNTIME:-podman} @@ -349,6 +353,20 @@ remove_packaged_podman_files(){ done } +systemd_banish(){ + echo "Disabling periodic services that could destabilize testing:" + set +e # Not all of these exist on every platform + for unit in $EVIL_UNITS + do + ooe.sh sudo systemctl stop $unit + ooe.sh sudo systemctl disable $unit + ooe.sh sudo systemctl disable $unit.timer + ooe.sh sudo systemctl mask $unit + ooe.sh sudo systemctl mask $unit.timer + done + set -e +} + _finalize(){ set +e # Don't fail at the very end set +e # make errors non-fatal diff --git a/contrib/cirrus/notice_master_failure.sh b/contrib/cirrus/notice_branch_failure.sh index 1fc15cdf9..423231dfd 100755 --- a/contrib/cirrus/notice_master_failure.sh +++ b/contrib/cirrus/notice_branch_failure.sh @@ -9,10 +9,10 @@ ETX="$(echo -n -e '\x03')" RED="${ETX}4" NOR="$(echo -n -e '\x0f')" -if [[ "$CIRRUS_BRANCH" =~ "master" ]] +if [[ "$CIRRUS_BRANCH" = "$DEST_BRANCH" ]] then BURL="https://cirrus-ci.com/build/$CIRRUS_BUILD_ID" - ircmsg "${RED}[Action Recommended]: ${NOR}Post-merge testing ${RED}$CIRRUS_BRANCH failed${NOR} in $CIRRUS_TASK_NAME on $(OS_RELEASE_ID)-$(OS_RELEASE_VER): $BURL. Please investigate, and re-run if appropriate." + ircmsg "${RED}[Action Recommended]: ${NOR}Post-merge testing on ${RED}$CIRRUS_BRANCH failed${NOR} in $CIRRUS_TASK_NAME on $(OS_RELEASE_ID)-$(OS_RELEASE_VER): $BURL. Please investigate, and re-run if appropriate." fi # This script assumed to be executed on failure diff --git a/contrib/cirrus/packer/fedora_base-setup.sh b/contrib/cirrus/packer/fedora_base-setup.sh index a425b2b57..788a54c34 100644 --- a/contrib/cirrus/packer/fedora_base-setup.sh +++ b/contrib/cirrus/packer/fedora_base-setup.sh @@ -27,6 +27,9 @@ ooe.sh systemctl enable rngd echo "Setting cloud-init service to start after google-network-daemon.service" cp -v $GOSRC/$PACKER_BASE/cloud-init/fedora/cloud-init.service /etc/systemd/system/ +# Ensure there are no disruptive periodic services enabled by default in image +systemd_banish + rh_finalize echo "SUCCESS!" diff --git a/contrib/cirrus/packer/fedora_setup.sh b/contrib/cirrus/packer/fedora_setup.sh index eb95db907..1e25a1a3c 100644 --- a/contrib/cirrus/packer/fedora_setup.sh +++ b/contrib/cirrus/packer/fedora_setup.sh @@ -76,6 +76,9 @@ ooe.sh sudo dnf install -y \ xz \ zip +# Ensure there are no disruptive periodic services enabled by default in image +systemd_banish + sudo /tmp/libpod/hack/install_catatonit.sh rh_finalize diff --git a/contrib/cirrus/packer/ubuntu_setup.sh b/contrib/cirrus/packer/ubuntu_setup.sh index 6209f2f89..dba191ad2 100644 --- a/contrib/cirrus/packer/ubuntu_setup.sh +++ b/contrib/cirrus/packer/ubuntu_setup.sh @@ -100,6 +100,9 @@ ooe.sh sudo update-grub sudo /tmp/libpod/hack/install_catatonit.sh ooe.sh sudo make -C /tmp/libpod install.libseccomp.sudo +# Ensure there are no disruptive periodic services enabled by default in image +systemd_banish + ubuntu_finalize echo "SUCCESS!" diff --git a/contrib/imgprune/Dockerfile b/contrib/imgprune/Dockerfile new file mode 100644 index 000000000..26329e828 --- /dev/null +++ b/contrib/imgprune/Dockerfile @@ -0,0 +1,7 @@ +FROM libpod/imgts:latest + +RUN yum -y update && \ + yum clean all + +COPY /contrib/imgprune/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod 755 /usr/local/bin/entrypoint.sh diff --git a/contrib/imgprune/README.md b/contrib/imgprune/README.md new file mode 100644 index 000000000..48abc2028 --- /dev/null +++ b/contrib/imgprune/README.md @@ -0,0 +1,11 @@ +![PODMAN logo](../../logo/podman-logo-source.svg) + +A container image for maintaining the collection of +VM images used by CI/CD on this project and several others. +Acts upon metadata maintained by the imgts container. + +Example build (from repository root): + +```bash +sudo podman build -t $IMAGE_NAME -f contrib/imgprune/Dockerfile . +``` diff --git a/contrib/imgprune/entrypoint.sh b/contrib/imgprune/entrypoint.sh new file mode 100755 index 000000000..a4b77523b --- /dev/null +++ b/contrib/imgprune/entrypoint.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +set -e + +source /usr/local/bin/lib_entrypoint.sh + +req_env_var GCPJSON GCPNAME GCPPROJECT IMGNAMES + +gcloud_init + +# For safety's sake + limit nr background processes +PRUNE_LIMIT=10 +THEFUTURE=$(date --date='+1 hour' +%s) +TOO_OLD='90 days ago' +THRESHOLD=$(date --date="$TOO_OLD" +%s) +# Format Ref: https://cloud.google.com/sdk/gcloud/reference/topic/formats +FORMAT='value[quote](name,selfLink,creationTimestamp,labels)' +PROJRE="/v1/projects/$GCPPROJECT/global/" +BASE_IMAGE_RE='cloud-base' +RECENTLY=$(date --date='30 days ago' --iso-8601=date) +EXCLUDE="$IMGNAMES $IMAGE_BUILDER_CACHE_IMAGE_NAME" # whitespace separated values +# Filter Ref: https://cloud.google.com/sdk/gcloud/reference/topic/filters +FILTER="selfLink~$PROJRE AND creationTimestamp<$RECENTLY AND NOT name=($EXCLUDE)" +TODELETE=$(mktemp -p '' todelete.XXXXXX) + +echo "Searching images for pruning candidates older than $TOO_OLD ($THRESHOLD):" +$GCLOUD compute images list --format="$FORMAT" --filter="$FILTER" | \ + while read name selfLink creationTimestamp labels + do + created_ymd=$(date --date=$creationTimestamp --iso-8601=date) + last_used=$(egrep --only-matching --max-count=1 'last-used=[[:digit:]]+' <<< $labels || true) + markmsgpfx="Marking $name (created $created_ymd) for deletion" + if [[ -z "$last_used" ]] + then # image pre-dates addition of tracking labels + echo "$markmsgpfx: Missing 'last-used' metadata, labels: '$labels'" + echo "$name" >> $TODELETE + continue + fi + + last_used_timestamp=$(date --date=@$(cut -d= -f2 <<< $last_used || true) +%s || true) + last_used_ymd=$(date --date=@$last_used_timestamp --iso-8601=date) + if [[ -z "$last_used_timestamp" ]] || [[ "$last_used_timestamp" -ge "$THEFUTURE" ]] + then + echo "$markmsgpfx: Missing or invalid last-used timestamp: '$last_used_timestamp'" + echo "$name" >> $TODELETE + continue + fi + + if [[ "$last_used_timestamp" -le "$THRESHOLD" ]] + then + echo "$markmsgpfx: Used over $TOO_OLD on $last_used_ymd" + echo "$name" >> $TODELETE + continue + fi + + echo "NOT $markmsgpfx: last used on $last_used_ymd)" + done + +echo "Pruning up to $PRUNE_LIMIT images that were marked for deletion:" +for image_name in $(tail -$PRUNE_LIMIT $TODELETE | sort --random-sort) +do + # This can take quite some time (minutes), run in parallel disconnected from terminal + echo "TODO: Would have: $GCLOUD compute images delete $image_name &" + sleep "$[1+RANDOM/1000]s" & # Simlate background operation +done + +wait || true # Nothing to delete: No background jobs diff --git a/contrib/imgts/Dockerfile b/contrib/imgts/Dockerfile index 0746eca4c..deaadb899 100644 --- a/contrib/imgts/Dockerfile +++ b/contrib/imgts/Dockerfile @@ -7,14 +7,14 @@ RUN yum -y update && \ yum -y install google-cloud-sdk && \ yum clean all -COPY /contrib/imgts/entrypoint.sh /usr/local/bin/entrypoint.sh ENV GCPJSON="__unknown__" \ GCPNAME="__unknown__" \ GCPPROJECT="__unknown__" \ IMGNAMES="__unknown__" \ - TIMESTAMP="__unknown__" \ BUILDID="__unknown__" \ REPOREF="__unknown__" + +COPY ["/contrib/imgts/entrypoint.sh", "/contrib/imgts/lib_entrypoint.sh", "/usr/local/bin/"] RUN chmod 755 /usr/local/bin/entrypoint.sh ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/contrib/imgts/entrypoint.sh b/contrib/imgts/entrypoint.sh index 610e1f3b6..9c653eda0 100755 --- a/contrib/imgts/entrypoint.sh +++ b/contrib/imgts/entrypoint.sh @@ -2,45 +2,22 @@ set -e -RED="\e[1;36;41m" -YEL="\e[1;33;44m" -NOR="\e[0m" +source /usr/local/bin/lib_entrypoint.sh -die() { - echo -e "$2" >&2 - exit "$1" -} +req_env_var GCPJSON GCPNAME GCPPROJECT IMGNAMES BUILDID REPOREF -SENTINEL="__unknown__" # default set in dockerfile +gcloud_init -[[ "$GCPJSON" != "$SENTINEL" ]] || \ - die 1 "Must specify service account JSON in \$GCPJSON" -[[ "$GCPNAME" != "$SENTINEL" ]] || \ - die 2 "Must specify service account name in \$GCPNAME" -[[ "$GCPPROJECT" != "$SENTINEL" ]] || \ - die 4 "Must specify GCP Project ID in \$GCPPROJECT" -[[ -n "$GCPPROJECT" ]] || \ - die 5 "Must specify non-empty GCP Project ID in \$GCPPROJECT" -[[ "$IMGNAMES" != "$SENTINEL" ]] || \ - die 6 "Must specify space separated list of GCE image names in \$IMGNAMES" -[[ "$BUILDID" != "$SENTINEL" ]] || \ - die 7 "Must specify the number of current build in \$BUILDID" -[[ "$REPOREF" != "$SENTINEL" ]] || \ - die 8 "Must specify a PR number or Branch name in \$REPOREF" +ARGS=" + --update-labels=last-used=$(date +%s) + --update-labels=build-id=$BUILDID + --update-labels=repo-ref=$REPOREF + --update-labels=project=$GCPPROJECT +" -ARGS="--update-labels=last-used=$(date +%s)" -# optional -[[ -z "$BUILDID" ]] || ARGS="$ARGS --update-labels=build-id=$BUILDID" -[[ -z "$REPOREF" ]] || ARGS="$ARGS --update-labels=repo-ref=$REPOREF" -[[ -z "$GCPPROJECT" ]] || ARGS="$ARGS --update-labels=project=$GCPPROJECT" - -gcloud config set account "$GCPNAME" -gcloud config set project "$GCPPROJECT" -echo "$GCPJSON" > /tmp/gcp.json -gcloud auth activate-service-account --key-file=/tmp/gcp.json || rm /tmp/gcp.json for image in $IMGNAMES do - gcloud compute images update "$image" $ARGS & + $GCLOUD compute images update "$image" $ARGS & done -set +e # Actual update failures are only warnings -wait || die 0 "${RED}WARNING:$NOR ${YEL}Failed to update labels on one or more images:$NOR '$IMGNAMES'" + +wait || echo "Warning: No \$IMGNAMES were specified." diff --git a/contrib/imgts/lib_entrypoint.sh b/contrib/imgts/lib_entrypoint.sh new file mode 100644 index 000000000..7b76c823f --- /dev/null +++ b/contrib/imgts/lib_entrypoint.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -e + +RED="\e[1;36;41m" +YEL="\e[1;33;44m" +NOR="\e[0m" +SENTINEL="__unknown__" # default set in dockerfile +# Disable all input prompts +# https://cloud.google.com/sdk/docs/scripting-gcloud +GCLOUD="gcloud --quiet" + +die() { + EXIT=$1 + PFX=$2 + shift 2 + MSG="$@" + echo -e "${RED}${PFX}:${NOR} ${YEL}$MSG${NOR}" + [[ "$EXIT" -eq "0" ]] || exit "$EXIT" +} + +# 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() { + for i; do + if [[ -z "${!i}" ]] + then + die 1 FATAL entrypoint.sh requires \$$i to be non-empty. + elif [[ "${!i}" == "$SENTINEL" ]] + then + die 2 FATAL entrypoint.sh requires \$$i to be explicitly set. + fi + done +} + +gcloud_init() { + set +xe + TMPF=$(mktemp -p '' .$(uuidgen)XXXX) + trap "rm -f $TMPF" EXIT + echo "$GCPJSON" > $TMPF && \ + $GCLOUD auth activate-service-account --project "$GCPPROJECT" --key-file=$TMPF || \ + die 5 FATAL auth + rm -f $TMPF +} diff --git a/dependencies/analyses/README.md b/dependencies/analyses/README.md new file mode 100644 index 000000000..a440a0ebd --- /dev/null +++ b/dependencies/analyses/README.md @@ -0,0 +1,88 @@ +# A set of scripts and instructions that help to analyze and debloat go-lang dependencies + +Note that all scripts mentioned below follow the [KISS principle](https://en.wikipedia.org/wiki/KISS_principle) on purpose. +The scripts are meant to be used in combination to aid in understanding the packages' dependencies and how they contribute to the size of the compiled binary. + +## Size of packages + +To analyze the size of all go packages used during the build process, pass the `-work -a` build flags to `go build`. +The `-a` flag forces go to rebuild all packages even if they are already up-to-date (e.g., in the build cache), while the `-work` flag instructs go to print the temporary work directory used for compiling the packages. +The path to the temporary work directory of `go-build` must be passed to `go-archive-analysis.sh` by setting it as an environment variable. +The analysis script will then read and parse the build data and print a sorted table of the package size in bytes followed by the package name. + +Running such an analysis on libpod may look as follows: + +``` +# 1) Build the podman binary with `-work -a`. +[libpod]$ BUILDFLAGS="-work -a" make podman +[...] +WORK=/tmp/go-build794287815 + +# 2) Set the work directory as an environment variable and call the analysis script +[libpod]$ WORK=/tmp/go-build794287815 ./dependencies/analyses/go-archive-analysis.sh | head -n10 +17M github.com/containers/libpod/cmd/podman/cliconfig +13M github.com/containers/libpod/vendor/github.com/DataDog/zstd +10M github.com/containers/libpod/vendor/k8s.io/api/core/v1 +3.7M net/http +3.7M github.com/containers/libpod/libpod +3.2M runtime +2.7M github.com/containers/libpod/vendor/github.com/gogo/protobuf/proto +2.5M github.com/containers/libpod/vendor/k8s.io/apimachinery/pkg/apis/meta/v1 +2.3M github.com/containers/libpod/vendor/github.com/vishvananda/netlink +2.1M github.com/containers/libpod/cmd/podman/varlink +``` + +The output of the `go-archive-analysis.sh` script is a sorted table with the size in bytes followed by the package. +The size denotes the size of the compiled package (i.e., the `.a` file). + + +## Size of symbols in binary + +Once the binary is compiled, we can run another set of analyses on it. +The `nm-symbols-analysis.sh` is a wrapper around `go tool nm` and prints a table with the size in bytes followed by the symbol's name. +To avoid information overload, the scripts prints only symbols from the text/code segment. + +Running such an analysis on libpod may look as follows: + +``` +# 1) Compile the binary +[libpod]$ make podman +[...] + +# 2) Run the script with the binary as an argument +[libpod]$ ./dependencies/analyses/nm-symbols-analysis.sh ./bin/podman | grep "containers/libpod/libpod" | head -n10 +299 github.com/containers/libpod/libpod.(*BoltState).AddContainer +658 github.com/containers/libpod/libpod.(*BoltState).AddContainerToPod +2120 github.com/containers/libpod/libpod.(*BoltState).AddPod +3773 github.com/containers/libpod/libpod.(*BoltState).AddPod.func1 +965 github.com/containers/libpod/libpod.(*BoltState).AddVolume +1651 github.com/containers/libpod/libpod.(*BoltState).AddVolume.func1 +558 github.com/containers/libpod/libpod.(*BoltState).AllContainers +282 github.com/containers/libpod/libpod.(*BoltState).AllContainers.func1 +1121 github.com/containers/libpod/libpod.(*BoltState).AllContainers.func1.1 +558 github.com/containers/libpod/libpod.(*BoltState).AllPods +``` + +Running the script can help identify sources of bloat and reveal potential candidates (e.g., entire packages, types, or function) for refactoring. + + +## Dependency Tree + +Use the `dependency-tree.sh` script to figure out which package includes which packages. +The output of the script has the format `package: dependency_1, dependency_2, ...`. +Each line is followed by a blank line to make it easier to read. +The script generates two files: + + - `direct-tree.txt` - listing direct dependencies + - `transitive-tree.txt` - listing direct and transitive dependencies + +Running such a dependency-tree analysis may look as follows: + + +``` +[libpod]$ ./dependencies/analyses/dependency-tree.sh github.com/containers/libpod +[libpod]$ grep "^github.com/containers/libpod/pkg/registries" direct-tree.txt +github.com/containers/libpod/pkg/registries: github.com/containers/libpod/vendor/github.com/containers/image/pkg/sysregistriesv2, github.com/containers/libpod/vendor/github.com/containers/image/types, github.com/containers/libpod/pkg/rootless, github.com/containers/libpod/vendor/github.com/docker/distribution/reference, github.com/containers/libpod/vendor/github.com/pkg/errors, os, path/filepath, strings +``` + +As shown above, the script's output can then be used to query for specific packages (e.g, with `grep`). diff --git a/dependencies/analyses/dependency-tree.sh b/dependencies/analyses/dependency-tree.sh new file mode 100755 index 000000000..84085a50d --- /dev/null +++ b/dependencies/analyses/dependency-tree.sh @@ -0,0 +1,17 @@ +#!/usr/bin/bash + +if test "$#" -ne 1; then + echo "invalid arguments: usage: $0 path to package" + exit 1 +fi + +go list $1/... \ + | xargs -d '\n' go list -f '{{ .ImportPath }}: {{ join .Imports ", " }}' \ + | awk '{ printf "%s\n\n", $0 }' \ + > direct-tree.tmp.$$ && mv -f direct-tree.tmp.$$ direct-tree.txt + + +go list $1/... \ + | xargs -d '\n' go list -f '{{ .ImportPath }}: {{ join .Deps ", " }}' \ + | awk '{ printf "%s\n\n", $0 }' \ + > transitive-tree.tmp.$$ && mv -f transitive-tree.tmp.$$ transitive-tree.txt diff --git a/dependencies/analyses/go-archive-analysis.sh b/dependencies/analyses/go-archive-analysis.sh new file mode 100755 index 000000000..f10145dad --- /dev/null +++ b/dependencies/analyses/go-archive-analysis.sh @@ -0,0 +1,12 @@ +#!/usr/bin/bash + +if [ -z "$WORK" ] +then + echo "WORK environment variable must be set" + exit 1 +fi + +grep --no-filename packagefile $WORK/**/importcfg \ + | awk '{ split($2, data, "="); printf "%s ", data[1]; system("du -sh " data[2]) }' \ + | awk '{ printf "%s %s\n", $2, $1 }' \ + | sort -u | sort -rh diff --git a/dependencies/analyses/nm-symbols-analysis.sh b/dependencies/analyses/nm-symbols-analysis.sh new file mode 100755 index 000000000..361b746e4 --- /dev/null +++ b/dependencies/analyses/nm-symbols-analysis.sh @@ -0,0 +1,9 @@ +#!/usr/bin/bash + +if test "$#" -ne 1; then + echo "invalid arguments: usage: $0 path/to/binary" + exit 1 +fi + +go tool nm -size "$1" \ + | awk 'NF==4 && $3=="t" {printf "%s\t\t%s\n", $2, $4}' diff --git a/docs/podman-create.1.md b/docs/podman-create.1.md index 87e18dbb9..89f146670 100644 --- a/docs/podman-create.1.md +++ b/docs/podman-create.1.md @@ -272,26 +272,29 @@ The following example maps uids 0-2000 in the container to the uids 30000-31999 Add additional groups to run as -**--healthcheck-command**=*command* +**--health-cmd**=*"command"* | *'["command", "arg1", ...]'* Set or alter a healthcheck command for a container. The command is a command to be executed inside your container that determines your container health. The command is required for other healthcheck options to be applied. A value of `none` disables existing healthchecks. -**--healthcheck-interval**=*interval* +Multiple options can be passed in the form of a JSON array; otherwise, the command will be interpreted +as an argument to `/bin/sh -c`. + +**--health-interval**=*interval* Set an interval for the healthchecks (a value of `disable` results in no automatic timer setup) (default "30s") -**--healthcheck-retries**=*retries* +**--health-retries**=*retries* The number of retries allowed before a healthcheck is considered to be unhealthy. The default value is `3`. -**--healthcheck-start-period**=*period* +**--health-start-period**=*period* The initialization time needed for a container to bootstrap. The value can be expressed in time format like `2m3s`. The default value is `0s` -**--healthcheck-timeout**=*timeout* +**--health-timeout**=*timeout* The maximum time allowed to complete the healthcheck before an interval is considered failed. Like start-period, the value can be expressed in a time format such as `1m22s`. The default value is `30s`. diff --git a/docs/podman-run.1.md b/docs/podman-run.1.md index 95499edd6..ebf774b24 100644 --- a/docs/podman-run.1.md +++ b/docs/podman-run.1.md @@ -279,26 +279,29 @@ The example maps gids 0-2000 in the container to the gids 30000-31999 on the hos Add additional groups to run as -**--healthcheck-command**=*command* +**--health-cmd**=*"command"* | *'["command", "arg1", ...]'* Set or alter a healthcheck command for a container. The command is a command to be executed inside your container that determines your container health. The command is required for other healthcheck options to be applied. A value of `none` disables existing healthchecks. -**--healthcheck-interval**=*interval* +Multiple options can be passed in the form of a JSON array; otherwise, the command will be interpreted +as an argument to `/bin/sh -c`. + +**--health-interval**=*interval* Set an interval for the healthchecks (a value of `disable` results in no automatic timer setup) (default "30s") -**--healthcheck-retries**=*retries* +**--health-retries**=*retries* The number of retries allowed before a healthcheck is considered to be unhealthy. The default value is `3`. -**--healthcheck-start-period**=*period* +**--health-start-period**=*period* The initialization time needed for a container to bootstrap. The value can be expressed in time format like `2m3s`. The default value is `0s` -**--healthcheck-timeout**=*timeout* +**--health-timeout**=*timeout* The maximum time allowed to complete the healthcheck before an interval is considered failed. Like start-period, the value can be expressed in a time format such as `1m22s`. The default value is `30s`. @@ -101,7 +101,7 @@ require ( github.com/spf13/pflag v1.0.3 github.com/spf13/viper v1.4.0 // indirect github.com/stretchr/testify v1.3.0 - github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 // indirect + github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 github.com/tchap/go-patricia v2.3.0+incompatible // indirect github.com/uber/jaeger-client-go v2.16.0+incompatible github.com/uber/jaeger-lib v0.0.0-20190122222657-d036253de8f5 // indirect diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index de0027414..c4d2af66e 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -1,16 +1,88 @@ package libpod import ( + "fmt" "strings" "time" "github.com/containers/image/manifest" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/driver" + "github.com/containers/libpod/pkg/util" "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/generate" + "github.com/opencontainers/runtime-tools/validate" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/syndtr/gocapability/capability" +) + +const ( + // InspectAnnotationCIDFile is used by Inspect to determine if a + // container ID file was created for the container. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationCIDFile = "io.podman.annotations.cid-file" + // InspectAnnotationAutoremove is used by Inspect to determine if a + // container will be automatically removed on exit. + // If an annotation with this key is found in the OCI spec and is one of + // the two supported boolean values (InspectResponseTrue and + // InspectResponseFalse) it will be used in the output of Inspect(). + InspectAnnotationAutoremove = "io.podman.annotations.autoremove" + // InspectAnnotationVolumesFrom is used by Inspect to identify + // containers whose volumes are are being used by this container. + // It is expected to be a comma-separated list of container names and/or + // IDs. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationVolumesFrom = "io.podman.annotations.volumes-from" + // InspectAnnotationPrivileged is used by Inspect to identify containers + // which are privileged (IE, running with elevated privileges). + // It is expected to be a boolean, populated by one of + // InspectResponseTrue or InspectResponseFalse. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationPrivileged = "io.podman.annotations.privileged" + // InspectAnnotationPublishAll is used by Inspect to identify containers + // which have all the ports from their image published. + // It is expected to be a boolean, populated by one of + // InspectResponseTrue or InspectResponseFalse. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationPublishAll = "io.podman.annotations.publish-all" + // InspectAnnotationInit is used by Inspect to identify containers that + // mount an init binary in. + // It is expected to be a boolean, populated by one of + // InspectResponseTrue or InspectResponseFalse. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationInit = "io.podman.annotations.init" + // InspectAnnotationLabel is used by Inspect to identify containers with + // special SELinux-related settings. It is used to populate the output + // of the SecurityOpt setting. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationLabel = "io.podman.annotations.label" + // InspectAnnotationSeccomp is used by Inspect to identify containers + // with special Seccomp-related settings. It is used to populate the + // output of the SecurityOpt setting in Inspect. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationSeccomp = "io.podman.annotations.seccomp" + // InspectAnnotationApparmor is used by Inspect to identify containers + // with special Apparmor-related settings. It is used to populate the + // output of the SecurityOpt setting. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationApparmor = "io.podman.annotations.apparmor" + + // InspectResponseTrue is a boolean True response for an inspect + // annotation. + InspectResponseTrue = "TRUE" + // InspectResponseFalse is a boolean False response for an inspect + // annotation. + InspectResponseFalse = "FALSE" ) // InspectContainerData provides a detailed record of a container's configuration @@ -19,41 +91,42 @@ import ( // compatible with `docker inspect` JSON, but additional fields have been added // as required to share information not in the original output. type InspectContainerData struct { - ID string `json:"Id"` - Created time.Time `json:"Created"` - Path string `json:"Path"` - Args []string `json:"Args"` - State *InspectContainerState `json:"State"` - ImageID string `json:"Image"` - ImageName string `json:"ImageName"` - Rootfs string `json:"Rootfs"` - ResolvConfPath string `json:"ResolvConfPath"` - HostnamePath string `json:"HostnamePath"` - HostsPath string `json:"HostsPath"` - StaticDir string `json:"StaticDir"` - OCIConfigPath string `json:"OCIConfigPath,omitempty"` - OCIRuntime string `json:"OCIRuntime,omitempty"` - LogPath string `json:"LogPath"` - ConmonPidFile string `json:"ConmonPidFile"` - Name string `json:"Name"` - RestartCount int32 `json:"RestartCount"` - Driver string `json:"Driver"` - MountLabel string `json:"MountLabel"` - ProcessLabel string `json:"ProcessLabel"` - AppArmorProfile string `json:"AppArmorProfile"` - EffectiveCaps []string `json:"EffectiveCaps"` - BoundingCaps []string `json:"BoundingCaps"` - ExecIDs []string `json:"ExecIDs"` - GraphDriver *driver.Data `json:"GraphDriver"` - SizeRw int64 `json:"SizeRw,omitempty"` - SizeRootFs int64 `json:"SizeRootFs,omitempty"` - Mounts []InspectMount `json:"Mounts"` - Dependencies []string `json:"Dependencies"` - NetworkSettings *InspectNetworkSettings `json:"NetworkSettings"` //TODO - ExitCommand []string `json:"ExitCommand"` - Namespace string `json:"Namespace"` - IsInfra bool `json:"IsInfra"` - Config *InspectContainerConfig `json:"Config"` + ID string `json:"Id"` + Created time.Time `json:"Created"` + Path string `json:"Path"` + Args []string `json:"Args"` + State *InspectContainerState `json:"State"` + ImageID string `json:"Image"` + ImageName string `json:"ImageName"` + Rootfs string `json:"Rootfs"` + ResolvConfPath string `json:"ResolvConfPath"` + HostnamePath string `json:"HostnamePath"` + HostsPath string `json:"HostsPath"` + StaticDir string `json:"StaticDir"` + OCIConfigPath string `json:"OCIConfigPath,omitempty"` + OCIRuntime string `json:"OCIRuntime,omitempty"` + LogPath string `json:"LogPath"` + ConmonPidFile string `json:"ConmonPidFile"` + Name string `json:"Name"` + RestartCount int32 `json:"RestartCount"` + Driver string `json:"Driver"` + MountLabel string `json:"MountLabel"` + ProcessLabel string `json:"ProcessLabel"` + AppArmorProfile string `json:"AppArmorProfile"` + EffectiveCaps []string `json:"EffectiveCaps"` + BoundingCaps []string `json:"BoundingCaps"` + ExecIDs []string `json:"ExecIDs"` + GraphDriver *driver.Data `json:"GraphDriver"` + SizeRw int64 `json:"SizeRw,omitempty"` + SizeRootFs int64 `json:"SizeRootFs,omitempty"` + Mounts []InspectMount `json:"Mounts"` + Dependencies []string `json:"Dependencies"` + NetworkSettings *InspectNetworkSettings `json:"NetworkSettings"` //TODO + ExitCommand []string `json:"ExitCommand"` + Namespace string `json:"Namespace"` + IsInfra bool `json:"IsInfra"` + Config *InspectContainerConfig `json:"Config"` + HostConfig *InspectContainerHostConfig `json:"HostConfig"` } // InspectContainerConfig holds further data about how a container was initially @@ -102,6 +175,353 @@ type InspectContainerConfig struct { Healthcheck *manifest.Schema2HealthConfig `json:"Healthcheck,omitempty"` } +// InspectContainerHostConfig holds information used when the container was +// created. +// It's very much a Docker-specific struct, retained (mostly) as-is for +// compatibility. We fill individual fields as best as we can, inferring as much +// as possible from the spec and container config. +// Some things cannot be inferred. These will be populated by spec annotations +// (if available). +// Field names are fixed for compatibility and cannot be changed. +// As such, silence lint warnings about them. +//nolint +type InspectContainerHostConfig struct { + // Binds contains an array of user-added mounts. + // Both volume mounts and named volumes are included. + // Tmpfs mounts are NOT included. + // In 'docker inspect' this is separated into 'Binds' and 'Mounts' based + // on how a mount was added. We do not make this distinction and do not + // include a Mounts field in inspect. + // Format: <src>:<destination>[:<comma-separated options>] + Binds []string `json:"Binds"` + // ContainerIDFile is a file created during container creation to hold + // the ID of the created container. + // This is not handled within libpod and is stored in an annotation. + ContainerIDFile string `json:"ContainerIDFile"` + // LogConfig contains information on the container's logging backend + LogConfig *InspectLogConfig `json:"LogConfig"` + // NetworkMode is the configuration of the container's network + // namespace. + // Populated as follows: + // default - A network namespace is being created and configured via CNI + // none - A network namespace is being created, not configured via CNI + // host - No network namespace created + // container:<id> - Using another container's network namespace + // ns:<path> - A path to a network namespace has been specified + NetworkMode string `json:"NetworkMode"` + // PortBindings contains the container's port bindings. + // It is formatted as map[string][]InspectHostPort. + // The string key here is formatted as <integer port number>/<protocol> + // and represents the container port. A single container port may be + // bound to multiple host ports (on different IPs). + PortBindings map[string][]InspectHostPort `json:"PortBindings"` + // RestartPolicy contains the container's restart policy. + RestartPolicy *InspectRestartPolicy `json:"RestartPolicy"` + // AutoRemove is whether the container will be automatically removed on + // exiting. + // It is not handled directly within libpod and is stored in an + // annotation. + AutoRemove bool `json:"AutoRemove"` + // VolumeDriver is presently unused and is retained for Docker + // compatibility. + VolumeDriver string `json:"VolumeDriver"` + // VolumesFrom is a list of containers which this container uses volumes + // from. This is not handled directly within libpod and is stored in an + // annotation. + // It is formatted as an array of container names and IDs. + VolumesFrom []string `json:"VolumesFrom"` + // CapAdd is a list of capabilities added to the container. + // It is not directly stored by Libpod, and instead computed from the + // capabilities listed in the container's spec, compared against a set + // of default capabilities. + CapAdd []string `json:"CapAdd"` + // CapDrop is a list of capabilities removed from the container. + // It is not directly stored by libpod, and instead computed from the + // capabilities listed in the container's spec, compared against a set + // of default capabilities. + CapDrop []string `json:"CapDrop"` + // Dns is a list of DNS nameservers that will be added to the + // container's resolv.conf + Dns []string `json:"Dns"` + // DnsOptions is a list of DNS options that will be set in the + // container's resolv.conf + DnsOptions []string `json:"DnsOptions"` + // DnsSearch is a list of DNS search domains that will be set in the + // container's resolv.conf + DnsSearch []string `json:"DnsSearch"` + // ExtraHosts contains hosts that will be aded to the container's + // /etc/hosts. + ExtraHosts []string `json:"ExtraHosts"` + // GroupAdd contains groups that the user inside the container will be + // added to. + GroupAdd []string `json:"GroupAdd"` + // IpcMode represents the configuration of the container's IPC + // namespace. + // Populated as follows: + // "" (empty string) - Default, an IPC namespace will be created + // host - No IPC namespace created + // container:<id> - Using another container's IPC namespace + // ns:<path> - A path to an IPC namespace has been specified + IpcMode string `json:"IpcMode"` + // Cgroup contains the container's cgroup. It is presently not + // populated. + // TODO. + Cgroup string `json:"Cgroup"` + // Links is unused, and provided purely for Docker compatibility. + Links []string `json:"Links"` + // OOMScoreAdj is an adjustment that will be made to the container's OOM + // score. + OomScoreAdj int `json:"OomScoreAdj"` + // PidMode represents the configuration of the container's PID + // namespace. + // Populated as follows: + // "" (empty string) - Default, a PID namespace will be created + // host - No PID namespace created + // container:<id> - Using another container's PID namespace + // ns:<path> - A path to a PID namespace has been specified + PidMode string `json:"PidMode"` + // Privileged indicates whether the container is running with elevated + // privileges. + // This has a very specific meaning in the Docker sense, so it's very + // difficult to decode from the spec and config, and so is stored as an + // annotation. + Privileged bool `json:"Privileged"` + // PublishAllPorts indicates whether image ports are being published. + // This is not directly stored in libpod and is saved as an annotation. + PublishAllPorts bool `json:"PublishAllPorts"` + // ReadonlyRootfs is whether the container will be mounted read-only. + ReadonlyRootfs bool `json:"ReadonlyRootfs"` + // SecurityOpt is a list of security-related options that are set in the + // container. + SecurityOpt []string `json:"SecurityOpt"` + // Tmpfs is a list of tmpfs filesystems that will be mounted into the + // container. + // It is a map of destination path to options for the mount. + Tmpfs map[string]string `json:"Tmpfs"` + // UTSMode represents the configuration of the container's UID + // namespace. + // Populated as follows: + // "" (empty string) - Default, a UTS namespace will be created + // host - no UTS namespace created + // container:<id> - Using another container's UTS namespace + // ns:<path> - A path to a UTS namespace has been specified + UTSMode string `json:"UTSMode"` + // UsernsMode represents the configuration of the container's user + // namespace. + // When running rootless, a user namespace is created outside of libpod + // to allow some privileged operations. This will not be reflected here. + // Populated as follows: + // "" (empty string) - No user namespace will be created + // private - The container will be run in a user namespace + // container:<id> - Using another container's user namespace + // ns:<path> - A path to a user namespace has been specified + // TODO Rootless has an additional 'keep-id' option, presently not + // reflected here. + UsernsMode string `json:"UsernsMode"` + // ShmSize is the size of the container's SHM device. + ShmSize int64 `json:"ShmSize"` + // Runtime is provided purely for Docker compatibility. + // It is set unconditionally to "oci" as Podman does not presently + // support non-OCI runtimes. + Runtime string `json:"Runtime"` + // ConsoleSize is an array of 2 integers showing the size of the + // container's console. + // It is only set if the container is creating a terminal. + // TODO. + ConsoleSize []uint `json:"ConsoleSize"` + // Isolation is presently unused and provided solely for Docker + // compatibility. + Isolation string `json:"Isolation"` + // CpuShares indicates the CPU resources allocated to the container. + // It is a relative weight in the scheduler for assigning CPU time + // versus other CGroups. + CpuShares uint64 `json:"CpuShares"` + // Memory indicates the memory resources allocated to the container. + // This is the limit (in bytes) of RAM the container may use. + Memory int64 `json:"Memory"` + // NanoCpus indicates number of CPUs allocated to the container. + // It is an integer where one full CPU is indicated by 1000000000 (one + // billion). + // Thus, 2.5 CPUs (fractional portions of CPUs are allowed) would be + // 2500000000 (2.5 billion). + // In 'docker inspect' this is set exclusively of two further options in + // the output (CpuPeriod and CpuQuota) which are both used to implement + // this functionality. + // We can't distinguish here, so if CpuQuota is set to the default of + // 100000, we will set both CpuQuota, CpuPeriod, and NanoCpus. If + // CpuQuota is not the default, we will not set NanoCpus. + NanoCpus int64 `json:"NanoCpus"` + // CgroupParent is the CGroup parent of the container. + // Only set if not default. + CgroupParent string `json:"CgroupParent"` + // BlkioWeight indicates the I/O resources allocated to the container. + // It is a relative weight in the scheduler for assigning I/O time + // versus other CGroups. + BlkioWeight uint16 `json:"BlkioWeight"` + // BlkioWeightDevice is an array of I/O resource priorities for + // individual device nodes. + // Unfortunately, the spec only stores the device's Major/Minor numbers + // and not the path, which is used here. + // Fortunately, the kernel provides an interface for retrieving the path + // of a given node by major:minor at /sys/dev/. However, the exact path + // in use may not be what was used in the original CLI invocation - + // though it is guaranteed that the device node will be the same, and + // using the given path will be functionally identical. + BlkioWeightDevice []InspectBlkioWeightDevice `json:"BlkioWeightDevice"` + // BlkioDeviceReadBps is an array of I/O throttle parameters for + // individual device nodes. + // This specifically sets read rate cap in bytes per second for device + // nodes. + // As with BlkioWeightDevice, we pull the path from /sys/dev, and we + // don't guarantee the path will be identical to the original (though + // the node will be). + BlkioDeviceReadBps []InspectBlkioThrottleDevice `json:"BlkioDeviceReadBps"` + // BlkioDeviceWriteBps is an array of I/O throttle parameters for + // individual device nodes. + // this specifically sets write rate cap in bytes per second for device + // nodes. + // as with BlkioWeightDevice, we pull the path from /sys/dev, and we + // don't guarantee the path will be identical to the original (though + // the node will be). + BlkioDeviceWriteBps []InspectBlkioThrottleDevice `json:"BlkioDeviceWriteBps"` + // BlkioDeviceReadIOps is an array of I/O throttle parameters for + // individual device nodes. + // This specifically sets the read rate cap in iops per second for + // device nodes. + // As with BlkioWeightDevice, we pull the path from /sys/dev, and we + // don't guarantee the path will be identical to the original (though + // the node will be). + BlkioDeviceReadIOps []InspectBlkioThrottleDevice `json:"BlkioDeviceReadIOps"` + // BlkioDeviceWriteIOps is an array of I/O throttle parameters for + // individual device nodes. + // This specifically sets the write rate cap in iops per second for + // device nodes. + // As with BlkioWeightDevice, we pull the path from /sys/dev, and we + // don't guarantee the path will be identical to the original (though + // the node will be). + BlkioDeviceWriteIOps []InspectBlkioThrottleDevice `json:"BlkioDeviceWriteIOps"` + // CpuPeriod is the length of a CPU period in microseconds. + // It relates directly to CpuQuota. + CpuPeriod uint64 `json:"CpuPeriod"` + // CpuPeriod is the amount of time (in microseconds) that a container + // can use the CPU in every CpuPeriod. + CpuQuota int64 `json:"CpuQuota"` + // CpuRealtimePeriod is the length of time (in microseconds) of the CPU + // realtime period. If set to 0, no time will be allocated to realtime + // tasks. + CpuRealtimePeriod uint64 `json:"CpuRealtimePeriod"` + // CpuRealtimeRuntime is the length of time (in microseconds) allocated + // for realtime tasks within every CpuRealtimePeriod. + CpuRealtimeRuntime int64 `json:"CpuRealtimeRuntime"` + // CpusetCpus is the is the set of CPUs that the container will execute + // on. Formatted as `0-3` or `0,2`. Default (if unset) is all CPUs. + CpusetCpus string `json:"CpusetCpus"` + // CpusetMems is the set of memory nodes the container will use. + // Formatted as `0-3` or `0,2`. Default (if unset) is all memory nodes. + CpusetMems string `json:"CpusetMems"` + // Devices is a list of device nodes that will be added to the + // container. + // These are stored in the OCI spec only as type, major, minor while we + // display the host path. We convert this with /sys/dev, but we cannot + // guarantee that the host path will be identical - only that the actual + // device will be. + Devices []InspectDevice `json:"Devices"` + // DiskQuota is the maximum amount of disk space the container may use + // (in bytes). + // Presently not populated. + // TODO. + DiskQuota uint64 `json:"DiskQuota"` + // KernelMemory is the maximum amount of memory the kernel will devote + // to the container. + KernelMemory int64 `json:"KernelMemory"` + // MemoryReservation is the reservation (soft limit) of memory available + // to the container. Soft limits are warnings only and can be exceeded. + MemoryReservation int64 `json:"MemoryReservation"` + // MemorySwap is the total limit for all memory available to the + // container, including swap. 0 indicates that there is no limit to the + // amount of memory available. + MemorySwap int64 `json:"MemorySwap"` + // MemorySwappiness is the willingness of the kernel to page container + // memory to swap. It is an integer from 0 to 100, with low numbers + // being more likely to be put into swap. + // -1, the default, will not set swappiness and use the system defaults. + MemorySwappiness int64 `json:"MemorySwappiness"` + // OomKillDisable indicates whether the kernel OOM killer is disabled + // for the container. + OomKillDisable bool `json:"OomKillDisable"` + // Init indicates whether the container has an init mounted into it. + Init bool `json:"Init,omitempty"` + // PidsLimit is the maximum number of PIDs what may be created within + // the container. 0, the default, indicates no limit. + PidsLimit int64 `json:"PidsLimit"` + // Ulimits is a set of ulimits that will be set within the container. + Ulimits []InspectUlimit `json:"Ulimits"` + // CpuCount is Windows-only and not presently implemented. + CpuCount uint64 `json:"CpuCount"` + // CpuPercent is Windows-only and not presently implemented. + CpuPercent uint64 `json:"CpuPercent"` + // IOMaximumIOps is Windows-only and not presently implemented. + IOMaximumIOps uint64 `json:"IOMaximumIOps"` + // IOMaximumBandwidth is Windows-only and not presently implemented. + IOMaximumBandwidth uint64 `json:"IOMaximumBandwidth"` +} + +// InspectLogConfig holds information about a container's configured log driver +// and is presently unused. It is retained for Docker compatibility. +type InspectLogConfig struct { + Type string `json:"Type"` + Config map[string]string `json:"Config"` //idk type, TODO +} + +// InspectRestartPolicy holds information about the container's restart policy. +type InspectRestartPolicy struct { + // Name contains the container's restart policy. + // Allowable values are "no" or "" (take no action), + // "on-failure" (restart on non-zero exit code, with an optional max + // retry count), and "always" (always restart on container stop, unless + // explicitly requested by API). + // Note that this is NOT actually a name of any sort - the poor naming + // is for Docker compatibility. + Name string `json:"Name"` + // MaximumRetryCount is the maximum number of retries allowed if the + // "on-failure" restart policy is in use. Not used if "on-failure" is + // not set. + MaximumRetryCount uint `json:"MaximumRetryCount"` +} + +// InspectBlkioWeightDevice holds information about the relative weight +// of an individual device node. Weights are used in the I/O scheduler to give +// relative priority to some accesses. +type InspectBlkioWeightDevice struct { + // Path is the path to the device this applies to. + Path string `json:"Path"` + // Weight is the relative weight the scheduler will use when scheduling + // I/O. + Weight uint16 `json:"Weight"` +} + +// InspectBlkioThrottleDevice holds information about a speed cap for a device +// node. This cap applies to a specific operation (read, write, etc) on the given +// node. +type InspectBlkioThrottleDevice struct { + // Path is the path to the device this applies to. + Path string `json:"Path"` + // Rate is the maximum rate. It is in either bytes per second or iops + // per second, determined by where it is used - documentation will + // indicate which is appropriate. + Rate uint64 `json:"Rate"` +} + +// InspectUlimit is a ulimit that will be applied to the container. +type InspectUlimit struct { + // Name is the name (type) of the ulimit. + Name string `json:"Name"` + // Soft is the soft limit that will be applied. + Soft uint64 `json:"Soft"` + // Hard is the hard limit that will be applied. + Hard uint64 `json:"Hard"` +} + // InspectMount provides a record of a single mount in a container. It contains // fields for both named and normal volumes. Only user-specified volumes will be // included, and tmpfs volumes are not included even if the user specified them. @@ -131,6 +551,29 @@ type InspectMount struct { Propagation string `json:"Propagation"` } +// InspectDevice is a single device that will be mounted into the container. +type InspectDevice struct { + // PathOnHost is the path of the device on the host. + PathOnHost string `json:"PathOnHost"` + // PathInContainer is the path of the device within the container. + PathInContainer string `json:"PathInContainer"` + // CgroupPermissions is the permissions of the mounted device. + // Presently not populated. + // TODO. + CgroupPermissions string `json:"CgroupPermissions"` +} + +// InspectHostPort provides information on a port on the host that a container's +// port is bound to. +type InspectHostPort struct { + // IP on the host we are bound to. "" if not specified (binding to all + // IPs). + HostIP string `json:"HostIp"` + // Port on the host we are bound to. No special formatting - just an + // integer stuffed into a string. + HostPort string `json:"HostPort"` +} + // InspectContainerState provides a detailed record of a container's current // state. It is returned as part of InspectContainerData. // As with InspectContainerData, many portions of this struct are matched to @@ -206,7 +649,7 @@ func (c *Container) Inspect(size bool) (*InspectContainerData, error) { func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) (*InspectContainerData, error) { config := c.config runtimeInfo := c.state - stateSpec, err := c.specFromState() + ctrSpec, err := c.specFromState() if err != nil { return nil, err } @@ -244,7 +687,8 @@ func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) } } - mounts, err := c.getInspectMounts(stateSpec) + namedVolumes, mounts := c.sortUserVolumes(ctrSpec) + inspectMounts, err := c.getInspectMounts(ctrSpec, namedVolumes, mounts) if err != nil { return nil, err } @@ -255,7 +699,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) Path: path, Args: args, State: &InspectContainerState{ - OciVersion: stateSpec.Version, + OciVersion: ctrSpec.Version, Status: runtimeInfo.State.String(), Running: runtimeInfo.State == define.ContainerStateRunning, Paused: runtimeInfo.State == define.ContainerStatePaused, @@ -285,12 +729,12 @@ func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) Driver: driverData.Name, MountLabel: config.MountLabel, ProcessLabel: config.ProcessLabel, - EffectiveCaps: stateSpec.Process.Capabilities.Effective, - BoundingCaps: stateSpec.Process.Capabilities.Bounding, - AppArmorProfile: stateSpec.Process.ApparmorProfile, + EffectiveCaps: ctrSpec.Process.Capabilities.Effective, + BoundingCaps: ctrSpec.Process.Capabilities.Bounding, + AppArmorProfile: ctrSpec.Process.ApparmorProfile, ExecIDs: execIDs, GraphDriver: driverData, - Mounts: mounts, + Mounts: inspectMounts, Dependencies: c.Dependencies(), NetworkSettings: &InspectNetworkSettings{ Bridge: "", // TODO @@ -338,12 +782,18 @@ func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) // Get information on the container's network namespace (if present) data = c.getContainerNetworkInfo(data) - inspectConfig, err := c.generateInspectContainerConfig(stateSpec) + inspectConfig, err := c.generateInspectContainerConfig(ctrSpec) if err != nil { return nil, err } data.Config = inspectConfig + hostConfig, err := c.generateInspectContainerHostConfig(ctrSpec, namedVolumes, mounts) + if err != nil { + return nil, err + } + data.HostConfig = hostConfig + if size { rootFsSize, err := c.rootFsSize() if err != nil { @@ -362,7 +812,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) // Get inspect-formatted mounts list. // Only includes user-specified mounts. Only includes bind mounts and named // volumes, not tmpfs volumes. -func (c *Container) getInspectMounts(ctrSpec *spec.Spec) ([]InspectMount, error) { +func (c *Container) getInspectMounts(ctrSpec *spec.Spec, namedVolumes []*ContainerNamedVolume, mounts []spec.Mount) ([]InspectMount, error) { inspectMounts := []InspectMount{} // No mounts, return early @@ -370,7 +820,6 @@ func (c *Container) getInspectMounts(ctrSpec *spec.Spec) ([]InspectMount, error) return inspectMounts, nil } - namedVolumes, mounts := c.sortUserVolumes(ctrSpec) for _, volume := range namedVolumes { mountStruct := InspectMount{} mountStruct.Type = "volume" @@ -493,3 +942,505 @@ func (c *Container) generateInspectContainerConfig(spec *spec.Spec) (*InspectCon return ctrConfig, nil } + +// Generate the InspectContainerHostConfig struct for the HostConfig field of +// Inspect. +func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, namedVolumes []*ContainerNamedVolume, mounts []spec.Mount) (*InspectContainerHostConfig, error) { + hostConfig := new(InspectContainerHostConfig) + + logConfig := new(InspectLogConfig) + logConfig.Type = c.config.LogDriver + hostConfig.LogConfig = logConfig + + restartPolicy := new(InspectRestartPolicy) + restartPolicy.Name = c.config.RestartPolicy + restartPolicy.MaximumRetryCount = c.config.RestartRetries + hostConfig.RestartPolicy = restartPolicy + + hostConfig.Dns = make([]string, 0, len(c.config.DNSServer)) + for _, dns := range c.config.DNSServer { + hostConfig.Dns = append(hostConfig.Dns, dns.String()) + } + + hostConfig.DnsOptions = make([]string, 0, len(c.config.DNSOption)) + for _, opt := range c.config.DNSOption { + hostConfig.DnsOptions = append(hostConfig.DnsOptions, opt) + } + + hostConfig.DnsSearch = make([]string, 0, len(c.config.DNSSearch)) + for _, search := range c.config.DNSSearch { + hostConfig.DnsSearch = append(hostConfig.DnsSearch, search) + } + + hostConfig.ExtraHosts = make([]string, 0, len(c.config.HostAdd)) + for _, host := range c.config.HostAdd { + hostConfig.ExtraHosts = append(hostConfig.ExtraHosts, host) + } + + hostConfig.GroupAdd = make([]string, 0, len(c.config.Groups)) + for _, group := range c.config.Groups { + hostConfig.GroupAdd = append(hostConfig.GroupAdd, group) + } + + hostConfig.SecurityOpt = []string{} + if ctrSpec.Process != nil { + if ctrSpec.Process.OOMScoreAdj != nil { + hostConfig.OomScoreAdj = *ctrSpec.Process.OOMScoreAdj + } + if ctrSpec.Process.NoNewPrivileges { + hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, "no-new-privileges") + } + } + + hostConfig.ReadonlyRootfs = ctrSpec.Root.Readonly + hostConfig.ShmSize = c.config.ShmSize + hostConfig.Runtime = "oci" + + // This is very expensive to initialize. + // So we don't want to initialize it unless we absolutely have to - IE, + // there are things that require a major:minor to path translation. + var deviceNodes map[string]string + + // Annotations + if ctrSpec.Annotations != nil { + hostConfig.ContainerIDFile = ctrSpec.Annotations[InspectAnnotationCIDFile] + if ctrSpec.Annotations[InspectAnnotationAutoremove] == InspectResponseTrue { + hostConfig.AutoRemove = true + } + if ctrs, ok := ctrSpec.Annotations[InspectAnnotationVolumesFrom]; ok { + hostConfig.VolumesFrom = strings.Split(ctrs, ",") + } + if ctrSpec.Annotations[InspectAnnotationPrivileged] == InspectResponseTrue { + hostConfig.Privileged = true + } + if ctrSpec.Annotations[InspectAnnotationInit] == InspectResponseTrue { + hostConfig.Init = true + } + if label, ok := ctrSpec.Annotations[InspectAnnotationLabel]; ok { + hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("label=%s", label)) + } + if seccomp, ok := ctrSpec.Annotations[InspectAnnotationSeccomp]; ok { + hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("seccomp=%s", seccomp)) + } + if apparmor, ok := ctrSpec.Annotations[InspectAnnotationApparmor]; ok { + hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("apparmor=%s", apparmor)) + } + } + + // Resource limits + if ctrSpec.Linux != nil { + if ctrSpec.Linux.Resources != nil { + if ctrSpec.Linux.Resources.CPU != nil { + if ctrSpec.Linux.Resources.CPU.Shares != nil { + hostConfig.CpuShares = *ctrSpec.Linux.Resources.CPU.Shares + } + if ctrSpec.Linux.Resources.CPU.Period != nil { + hostConfig.CpuPeriod = *ctrSpec.Linux.Resources.CPU.Period + } + if ctrSpec.Linux.Resources.CPU.Quota != nil { + hostConfig.CpuQuota = *ctrSpec.Linux.Resources.CPU.Quota + } + if ctrSpec.Linux.Resources.CPU.RealtimePeriod != nil { + hostConfig.CpuRealtimePeriod = *ctrSpec.Linux.Resources.CPU.RealtimePeriod + } + if ctrSpec.Linux.Resources.CPU.RealtimeRuntime != nil { + hostConfig.CpuRealtimeRuntime = *ctrSpec.Linux.Resources.CPU.RealtimeRuntime + } + hostConfig.CpusetCpus = ctrSpec.Linux.Resources.CPU.Cpus + hostConfig.CpusetMems = ctrSpec.Linux.Resources.CPU.Mems + } + if ctrSpec.Linux.Resources.Memory != nil { + if ctrSpec.Linux.Resources.Memory.Limit != nil { + hostConfig.Memory = *ctrSpec.Linux.Resources.Memory.Limit + } + if ctrSpec.Linux.Resources.Memory.Kernel != nil { + hostConfig.KernelMemory = *ctrSpec.Linux.Resources.Memory.Kernel + } + if ctrSpec.Linux.Resources.Memory.Reservation != nil { + hostConfig.MemoryReservation = *ctrSpec.Linux.Resources.Memory.Reservation + } + if ctrSpec.Linux.Resources.Memory.Swap != nil { + hostConfig.MemorySwap = *ctrSpec.Linux.Resources.Memory.Swap + } + if ctrSpec.Linux.Resources.Memory.Swappiness != nil { + hostConfig.MemorySwappiness = int64(*ctrSpec.Linux.Resources.Memory.Swappiness) + } else { + // Swappiness has a default of -1 + hostConfig.MemorySwappiness = -1 + } + if ctrSpec.Linux.Resources.Memory.DisableOOMKiller != nil { + hostConfig.OomKillDisable = *ctrSpec.Linux.Resources.Memory.DisableOOMKiller + } + } + if ctrSpec.Linux.Resources.Pids != nil { + hostConfig.PidsLimit = ctrSpec.Linux.Resources.Pids.Limit + } + if ctrSpec.Linux.Resources.BlockIO != nil { + if ctrSpec.Linux.Resources.BlockIO.Weight != nil { + hostConfig.BlkioWeight = *ctrSpec.Linux.Resources.BlockIO.Weight + } + hostConfig.BlkioWeightDevice = []InspectBlkioWeightDevice{} + for _, dev := range ctrSpec.Linux.Resources.BlockIO.WeightDevice { + key := fmt.Sprintf("%d:%d", dev.Major, dev.Minor) + // TODO: how do we handle LeafWeight vs + // Weight? For now, ignore anything + // without Weight set. + if dev.Weight == nil { + logrus.Warnf("Ignoring weight device %s as it lacks a weight", key) + continue + } + if deviceNodes == nil { + nodes, err := util.FindDeviceNodes() + if err != nil { + return nil, err + } + deviceNodes = nodes + } + path, ok := deviceNodes[key] + if !ok { + logrus.Warnf("Could not locate weight device %s in system devices", key) + continue + } + weightDev := InspectBlkioWeightDevice{} + weightDev.Path = path + weightDev.Weight = *dev.Weight + hostConfig.BlkioWeightDevice = append(hostConfig.BlkioWeightDevice, weightDev) + } + + handleThrottleDevice := func(devs []spec.LinuxThrottleDevice) ([]InspectBlkioThrottleDevice, error) { + out := []InspectBlkioThrottleDevice{} + for _, dev := range devs { + key := fmt.Sprintf("%d:%d", dev.Major, dev.Minor) + if deviceNodes == nil { + nodes, err := util.FindDeviceNodes() + if err != nil { + return nil, err + } + deviceNodes = nodes + } + path, ok := deviceNodes[key] + if !ok { + logrus.Warnf("Could not locate throttle device %s in system devices", key) + continue + } + throttleDev := InspectBlkioThrottleDevice{} + throttleDev.Path = path + throttleDev.Rate = dev.Rate + out = append(out, throttleDev) + } + return out, nil + } + + readBps, err := handleThrottleDevice(ctrSpec.Linux.Resources.BlockIO.ThrottleReadBpsDevice) + if err != nil { + return nil, err + } + hostConfig.BlkioDeviceReadBps = readBps + + writeBps, err := handleThrottleDevice(ctrSpec.Linux.Resources.BlockIO.ThrottleWriteBpsDevice) + if err != nil { + return nil, err + } + hostConfig.BlkioDeviceWriteBps = writeBps + + readIops, err := handleThrottleDevice(ctrSpec.Linux.Resources.BlockIO.ThrottleReadIOPSDevice) + if err != nil { + return nil, err + } + hostConfig.BlkioDeviceReadIOps = readIops + + writeIops, err := handleThrottleDevice(ctrSpec.Linux.Resources.BlockIO.ThrottleWriteIOPSDevice) + if err != nil { + return nil, err + } + hostConfig.BlkioDeviceWriteIOps = writeIops + } + } + } + + // NanoCPUs. + // This is only calculated if CpuPeriod == 100000. + // It is given in nanoseconds, versus the microseconds used elsewhere - + // so multiply by 10000 (not sure why, but 1000 is off by 10). + if hostConfig.CpuPeriod == 100000 { + hostConfig.NanoCpus = 10000 * hostConfig.CpuQuota + } + + // Bind mounts, formatted as src:dst. + // We'll be appending some options that aren't necessarily in the + // original command line... but no helping that from inside libpod. + binds := []string{} + tmpfs := make(map[string]string) + for _, namedVol := range namedVolumes { + if len(namedVol.Options) > 0 { + binds = append(binds, fmt.Sprintf("%s:%s:%s", namedVol.Name, namedVol.Dest, strings.Join(namedVol.Options, ","))) + } else { + binds = append(binds, fmt.Sprintf("%s:%s", namedVol.Name, namedVol.Dest)) + } + } + for _, mount := range mounts { + if mount.Type == "tmpfs" { + tmpfs[mount.Destination] = strings.Join(mount.Options, ",") + } else { + // TODO - maybe we should parse for empty source/destination + // here. Would be confusing if we print just a bare colon. + if len(mount.Options) > 0 { + binds = append(binds, fmt.Sprintf("%s:%s:%s", mount.Source, mount.Destination, strings.Join(mount.Options, ","))) + } else { + binds = append(binds, fmt.Sprintf("%s:%s", mount.Source, mount.Destination)) + } + } + } + hostConfig.Binds = binds + hostConfig.Tmpfs = tmpfs + + // Network mode parsing. + networkMode := "" + if c.config.CreateNetNS { + networkMode = "default" + } else if c.config.NetNsCtr != "" { + networkMode = fmt.Sprintf("container:%s", c.config.NetNsCtr) + } else { + // Find the spec's network namespace. + // If there is none, it's host networking. + // If there is one and it has a path, it's "ns:". + foundNetNS := false + for _, ns := range ctrSpec.Linux.Namespaces { + if ns.Type == spec.NetworkNamespace { + foundNetNS = true + if ns.Path != "" { + networkMode = fmt.Sprintf("ns:%s", ns.Path) + } else { + networkMode = "none" + } + break + } + } + if !foundNetNS { + networkMode = "host" + } + } + hostConfig.NetworkMode = networkMode + + // Port bindings. + // Only populate if we're using CNI to configure the network. + portBindings := make(map[string][]InspectHostPort) + if c.config.CreateNetNS { + for _, port := range c.config.PortMappings { + key := fmt.Sprintf("%d/%s", port.ContainerPort, port.Protocol) + hostPorts := portBindings[key] + if hostPorts == nil { + hostPorts = []InspectHostPort{} + } + hostPorts = append(hostPorts, InspectHostPort{ + HostIP: port.HostIP, + HostPort: fmt.Sprintf("%d", port.HostPort), + }) + portBindings[key] = hostPorts + } + } + hostConfig.PortBindings = portBindings + + // Cap add and cap drop. + // We need a default set of capabilities to compare against. + // The OCI generate package has one, and is commonly used, so we'll + // use it. + // Problem: there are 5 sets of capabilities. + // Use the bounding set for this computation, it's the most encompassing + // (but still not perfect). + capAdd := []string{} + capDrop := []string{} + // No point in continuing if we got a spec without a Process block... + if ctrSpec.Process != nil { + // Max an O(1) lookup table for default bounding caps. + boundingCaps := make(map[string]bool) + g, err := generate.New("linux") + if err != nil { + return nil, err + } + if !hostConfig.Privileged { + for _, cap := range g.Config.Process.Capabilities.Bounding { + boundingCaps[cap] = true + } + } else { + // If we are privileged, use all caps. + for _, cap := range capability.List() { + if g.HostSpecific && cap > validate.LastCap() { + continue + } + boundingCaps[fmt.Sprintf("CAP_%s", strings.ToUpper(cap.String()))] = true + } + } + // Iterate through spec caps. + // If it's not in default bounding caps, it was added. + // If it is, delete from the default set. Whatever remains after + // we finish are the dropped caps. + for _, cap := range ctrSpec.Process.Capabilities.Bounding { + if _, ok := boundingCaps[cap]; ok { + delete(boundingCaps, cap) + } else { + capAdd = append(capAdd, cap) + } + } + for cap := range boundingCaps { + capDrop = append(capDrop, cap) + } + } + hostConfig.CapAdd = capAdd + hostConfig.CapDrop = capDrop + + // IPC Namespace mode + ipcMode := "" + if c.config.IPCNsCtr != "" { + ipcMode = fmt.Sprintf("container:%s", c.config.IPCNsCtr) + } else { + // Locate the spec's IPC namespace. + // If there is none, it's ipc=host. + // If there is one and it has a path, it's "ns:". + // If no path, it's default - the empty string. + foundIPCNS := false + for _, ns := range ctrSpec.Linux.Namespaces { + if ns.Type == spec.IPCNamespace { + foundIPCNS = true + if ns.Path != "" { + ipcMode = fmt.Sprintf("ns:%s", ns.Path) + } + break + } + } + if !foundIPCNS { + ipcMode = "host" + } + } + hostConfig.IpcMode = ipcMode + + // CGroup parent + // Need to check if it's the default, and not print if so. + defaultCgroupParent := "" + switch c.runtime.config.CgroupManager { + case CgroupfsCgroupsManager: + defaultCgroupParent = CgroupfsDefaultCgroupParent + case SystemdCgroupsManager: + defaultCgroupParent = SystemdDefaultCgroupParent + } + if c.config.CgroupParent != defaultCgroupParent { + hostConfig.CgroupParent = c.config.CgroupParent + } + + // PID namespace mode + pidMode := "" + if c.config.PIDNsCtr != "" { + pidMode = fmt.Sprintf("container:%s", c.config.PIDNsCtr) + } else { + // Locate the spec's PID namespace. + // If there is none, it's pid=host. + // If there is one and it has a path, it's "ns:". + // If there is no path, it's default - the empty string. + foundPIDNS := false + for _, ns := range ctrSpec.Linux.Namespaces { + if ns.Type == spec.PIDNamespace { + foundPIDNS = true + if ns.Path != "" { + pidMode = fmt.Sprintf("ns:%s", ns.Path) + } + break + } + } + if !foundPIDNS { + pidMode = "host" + } + } + hostConfig.PidMode = pidMode + + // UTS namespace mode + utsMode := "" + if c.config.UTSNsCtr != "" { + utsMode = fmt.Sprintf("container:%s", c.config.UTSNsCtr) + } else { + // Locate the spec's UTS namespace. + // If there is none, it's uts=host. + // If there is one and it has a path, it's "ns:". + // If there is no path, it's default - the empty string. + foundUTSNS := false + for _, ns := range ctrSpec.Linux.Namespaces { + if ns.Type == spec.UTSNamespace { + foundUTSNS = true + if ns.Path != "" { + utsMode = fmt.Sprintf("ns:%s", ns.Path) + } + break + } + } + if !foundUTSNS { + utsMode = "host" + } + } + hostConfig.UTSMode = utsMode + + // User namespace mode + usernsMode := "" + if c.config.UserNsCtr != "" { + usernsMode = fmt.Sprintf("container:%s", c.config.UserNsCtr) + } else { + // Locate the spec's user namespace. + // If there is none, it's default - the empty string. + // If there is one, it's "private" if no path, or "ns:" if + // there's a path. + for _, ns := range ctrSpec.Linux.Namespaces { + if ns.Type == spec.UserNamespace { + if ns.Path != "" { + usernsMode = fmt.Sprintf("ns:%s", ns.Path) + } else { + usernsMode = "private" + } + } + } + } + hostConfig.UsernsMode = usernsMode + + // Devices + // Do not include if privileged - assumed that all devices will be + // included. + hostConfig.Devices = []InspectDevice{} + if ctrSpec.Linux != nil && !hostConfig.Privileged { + for _, dev := range ctrSpec.Linux.Devices { + key := fmt.Sprintf("%d:%d", dev.Major, dev.Minor) + if deviceNodes == nil { + nodes, err := util.FindDeviceNodes() + if err != nil { + return nil, err + } + deviceNodes = nodes + } + path, ok := deviceNodes[key] + if !ok { + logrus.Warnf("Could not locate device %s on host", key) + continue + } + newDev := InspectDevice{} + newDev.PathOnHost = path + newDev.PathInContainer = dev.Path + hostConfig.Devices = append(hostConfig.Devices, newDev) + } + } + + // Ulimits + hostConfig.Ulimits = []InspectUlimit{} + if ctrSpec.Process != nil { + for _, limit := range ctrSpec.Process.Rlimits { + newLimit := InspectUlimit{} + newLimit.Name = limit.Type + newLimit.Soft = limit.Soft + newLimit.Hard = limit.Hard + hostConfig.Ulimits = append(hostConfig.Ulimits, newLimit) + } + } + + // Terminal size + // We can't actually get this for now... + // So default to something sane. + // TODO: Populate this. + hostConfig.ConsoleSize = []uint{0, 0} + + return hostConfig, nil +} diff --git a/libpod/healthcheck.go b/libpod/healthcheck.go index f4ea6c694..8ed2b12e1 100644 --- a/libpod/healthcheck.go +++ b/libpod/healthcheck.go @@ -107,16 +107,25 @@ func (c *Container) runHealthCheck() (HealthCheckStatus, error) { capture bytes.Buffer inStartPeriod bool ) - hcStatus, err := checkHealthCheckCanBeRun(c) - if err != nil { - return hcStatus, err - } hcCommand := c.HealthCheckConfig().Test - if len(hcCommand) > 0 && hcCommand[0] == "CMD-SHELL" { - newCommand = []string{"sh", "-c", strings.Join(hcCommand[1:], " ")} - } else { + if len(hcCommand) < 1 { + return HealthCheckNotDefined, errors.Errorf("container %s has no defined healthcheck", c.ID()) + } + switch hcCommand[0] { + case "", "NONE": + return HealthCheckNotDefined, errors.Errorf("container %s has no defined healthcheck", c.ID()) + case "CMD": + newCommand = hcCommand[1:] + case "CMD-SHELL": + // TODO: SHELL command from image not available in Container - use Docker default + newCommand = []string{"/bin/sh", "-c", strings.Join(hcCommand[1:], " ")} + default: + // command supplied on command line - pass as-is newCommand = hcCommand } + if len(newCommand) < 1 || newCommand[0] == "" { + return HealthCheckNotDefined, errors.Errorf("container %s has no defined healthcheck", c.ID()) + } captureBuffer := bufio.NewWriter(&capture) hcw := hcWriteCloser{ captureBuffer, diff --git a/libpod/image/pull.go b/libpod/image/pull.go index 581beb538..2f1d1e912 100644 --- a/libpod/image/pull.go +++ b/libpod/image/pull.go @@ -267,7 +267,7 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa copyOptions.SourceCtx.SystemRegistriesConfPath = systemRegistriesConfPath // FIXME: Set this more globally. Probably no reason not to have it in every types.SystemContext, and to compute the value just once in one place. // Print the following statement only when pulling from a docker or atomic registry if writer != nil && (imageInfo.srcRef.Transport().Name() == DockerTransport || imageInfo.srcRef.Transport().Name() == AtomicTransport) { - if _, err := io.WriteString(writer, fmt.Sprintf("Trying to pull %s...", imageInfo.image)); err != nil { + if _, err := io.WriteString(writer, fmt.Sprintf("Trying to pull %s...\n", imageInfo.image)); err != nil { return nil, err } } diff --git a/libpod/oci_linux.go b/libpod/oci_linux.go index 044373ec5..1182457f4 100644 --- a/libpod/oci_linux.go +++ b/libpod/oci_linux.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "runtime" + "strconv" "strings" "syscall" "time" @@ -461,8 +462,21 @@ func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string, res return errors.Wrapf(define.ErrInternal, "container create failed") } ctr.state.PID = ss.si.Pid - if cmd.Process != nil { - ctr.state.ConmonPID = cmd.Process.Pid + // Let's try reading the Conmon pid at the same time. + if ctr.config.ConmonPidFile != "" { + contents, err := ioutil.ReadFile(ctr.config.ConmonPidFile) + if err != nil { + logrus.Warnf("Error reading Conmon pidfile for container %s: %v", ctr.ID(), err) + } else { + // Convert it to an int + conmonPID, err := strconv.Atoi(string(contents)) + if err != nil { + logrus.Warnf("Error decoding Conmon PID %q for container %s: %v", string(contents), ctr.ID(), err) + } else { + ctr.state.ConmonPID = conmonPID + logrus.Infof("Got Conmon PID as %d", conmonPID) + } + } } case <-time.After(ContainerCreateTimeout): return errors.Wrapf(define.ErrInternal, "container creation timeout") diff --git a/libpod/runtime.go b/libpod/runtime.go index 9196547a2..08c6cb588 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -854,39 +854,20 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (err error) { } else if runtime.noStore { logrus.Debug("No store required. Not opening container store.") } else { - store, err = storage.GetStore(runtime.config.StorageConfig) - if err != nil { + if err := runtime.configureStore(); err != nil { return err } - err = nil - - defer func() { - if err != nil && store != nil { - // Don't forcibly shut down - // We could be opening a store in use by another libpod - _, err2 := store.Shutdown(false) - if err2 != nil { - logrus.Errorf("Error removing store for partially-created runtime: %s", err2) - } - } - }() } - - runtime.store = store - is.Transport.SetStore(store) - - // Set up image runtime and store in runtime - ir := image.NewImageRuntimeFromStore(runtime.store) - - runtime.imageRuntime = ir - - // Setting signaturepolicypath - ir.SignaturePolicyPath = runtime.config.SignaturePolicyPath - - // Set logfile path for events - ir.EventsLogFilePath = runtime.config.EventsLogFilePath - // Set logger type - ir.EventsLogger = runtime.config.EventsLogger + defer func() { + if err != nil && store != nil { + // Don't forcibly shut down + // We could be opening a store in use by another libpod + _, err2 := store.Shutdown(false) + if err2 != nil { + logrus.Errorf("Error removing store for partially-created runtime: %s", err2) + } + } + }() // Setup the eventer eventer, err := runtime.newEventer() @@ -894,7 +875,9 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (err error) { return err } runtime.eventer = eventer - ir.Eventer = eventer + if runtime.imageRuntime != nil { + runtime.imageRuntime.Eventer = eventer + } // Set up a storage service for creating container root filesystems from // images @@ -1125,6 +1108,13 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (err error) { // If we need to refresh the state, do it now - things are guaranteed to // be set up by now. if doRefresh { + // Ensure we have a store before refresh occurs + if runtime.store == nil { + if err := runtime.configureStore(); err != nil { + return err + } + } + if err2 := runtime.refresh(runtimeAliveFile); err2 != nil { return err2 } @@ -1330,7 +1320,29 @@ func (r *Runtime) generateName() (string, error) { // The code should never reach here. } -// ImageRuntime returns the imageruntime for image resolution +// Configure store and image runtime +func (r *Runtime) configureStore() error { + store, err := storage.GetStore(r.config.StorageConfig) + if err != nil { + return err + } + + r.store = store + is.Transport.SetStore(store) + + ir := image.NewImageRuntimeFromStore(r.store) + ir.SignaturePolicyPath = r.config.SignaturePolicyPath + ir.EventsLogFilePath = r.config.EventsLogFilePath + ir.EventsLogger = r.config.EventsLogger + + r.imageRuntime = ir + + return nil +} + +// ImageRuntime returns the imageruntime for image operations. +// If WithNoStore() was used, no image runtime will be available, and this +// function will return nil. func (r *Runtime) ImageRuntime() *image.Runtime { return r.imageRuntime } diff --git a/pkg/adapter/pods.go b/pkg/adapter/pods.go index b45b02d09..2ca4f228f 100644 --- a/pkg/adapter/pods.go +++ b/pkg/adapter/pods.go @@ -676,7 +676,7 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container if imageData != nil && imageData.Config != nil { containerConfig.Command = append(containerConfig.Command, imageData.Config.Entrypoint...) } - if len(containerConfig.Command) != 0 { + if len(containerYAML.Command) != 0 { containerConfig.Command = append(containerConfig.Command, containerYAML.Command...) } else if imageData != nil && imageData.Config != nil { containerConfig.Command = append(containerConfig.Command, imageData.Config.Cmd...) diff --git a/pkg/apparmor/apparmor_linux.go b/pkg/apparmor/apparmor_linux.go index 0d01f41e9..479600408 100644 --- a/pkg/apparmor/apparmor_linux.go +++ b/pkg/apparmor/apparmor_linux.go @@ -4,6 +4,7 @@ package apparmor import ( "bufio" + "bytes" "fmt" "io" "os" @@ -104,6 +105,18 @@ func InstallDefault(name string) error { return cmd.Wait() } +// DefaultContent returns the default profile content as byte slice. The +// profile is named as the provided `name`. The function errors if the profile +// generation fails. +func DefaultContent(name string) ([]byte, error) { + p := profileData{Name: name} + var bytes bytes.Buffer + if err := p.generateDefault(&bytes); err != nil { + return nil, err + } + return bytes.Bytes(), nil +} + // IsLoaded checks if a profile with the given name has been loaded into the // kernel. func IsLoaded(name string) (bool, error) { diff --git a/pkg/apparmor/apparmor_linux_test.go b/pkg/apparmor/apparmor_linux_test.go index ac3260723..e94293d87 100644 --- a/pkg/apparmor/apparmor_linux_test.go +++ b/pkg/apparmor/apparmor_linux_test.go @@ -78,10 +78,12 @@ Copyright 2009-2012 Canonical Ltd. } } -func TestInstallDefault(t *testing.T) { - profile := "libpod-default-testing" - aapath := "/sys/kernel/security/apparmor/" +const ( + aapath = "/sys/kernel/security/apparmor/" + profile = "libpod-default-testing" +) +func TestInstallDefault(t *testing.T) { if _, err := os.Stat(aapath); err != nil { t.Skip("AppArmor isn't available in this environment") } @@ -127,3 +129,12 @@ func TestInstallDefault(t *testing.T) { } checkLoaded(false) } + +func TestDefaultContent(t *testing.T) { + if _, err := os.Stat(aapath); err != nil { + t.Skip("AppArmor isn't available in this environment") + } + if err := DefaultContent(profile); err != nil { + t.Fatalf("Couldn't retrieve default AppArmor profile content '%s': %v", profile, err) + } +} diff --git a/pkg/apparmor/apparmor_unsupported.go b/pkg/apparmor/apparmor_unsupported.go index b2b4de5f5..13469f1b6 100644 --- a/pkg/apparmor/apparmor_unsupported.go +++ b/pkg/apparmor/apparmor_unsupported.go @@ -24,3 +24,8 @@ func CheckProfileAndLoadDefault(name string) (string, error) { } return "", ErrApparmorUnsupported } + +// DefaultContent dummy. +func DefaultContent(name string) ([]byte, error) { + return nil, nil +} diff --git a/pkg/cgroups/cgroups.go b/pkg/cgroups/cgroups.go index 081db772f..fda19bff8 100644 --- a/pkg/cgroups/cgroups.go +++ b/pkg/cgroups/cgroups.go @@ -187,8 +187,12 @@ func createCgroupv2Path(path string) (Err error) { }() } } - if err := ioutil.WriteFile(filepath.Join(current, "cgroup.subtree_control"), resByte, 0755); err != nil { - return errors.Wrapf(err, "write %s", filepath.Join(current, "cgroup.subtree_control")) + // We enable the controllers for all the path components except the last one. It is not allowed to add + // PIDs if there are already enabled controllers. + if i < len(elements[3:])-1 { + if err := ioutil.WriteFile(filepath.Join(current, "cgroup.subtree_control"), resByte, 0755); err != nil { + return errors.Wrapf(err, "write %s", filepath.Join(current, "cgroup.subtree_control")) + } } } return nil diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index d44beb3e4..41054633f 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -20,12 +20,6 @@ import ( const cpuPeriod = 100000 -type systemUlimit struct { - name string - max uint64 - cur uint64 -} - func getAvailableGids() (int64, error) { idMap, err := user.ParseIDMapFile("/proc/self/gid_map") if err != nil { @@ -86,23 +80,41 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM g.AddLinuxMaskedPaths("/sys/kernel") } } + gid5Available := true if isRootless { nGids, err := getAvailableGids() if err != nil { return nil, err } - if nGids < 5 { - // If we have no GID mappings, the gid=5 default option would fail, so drop it. - g.RemoveMount("/dev/pts") - devPts := spec.Mount{ - Destination: "/dev/pts", - Type: "devpts", - Source: "devpts", - Options: []string{"rprivate", "nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620"}, + gid5Available = nGids >= 5 + } + // When using a different user namespace, check that the GID 5 is mapped inside + // the container. + if gid5Available && len(config.IDMappings.GIDMap) > 0 { + mappingFound := false + for _, r := range config.IDMappings.GIDMap { + if r.ContainerID <= 5 && 5 < r.ContainerID+r.Size { + mappingFound = true + break } - g.AddMount(devPts) } + if !mappingFound { + gid5Available = false + } + + } + if !gid5Available { + // If we have no GID mappings, the gid=5 default option would fail, so drop it. + g.RemoveMount("/dev/pts") + devPts := spec.Mount{ + Destination: "/dev/pts", + Type: "devpts", + Source: "devpts", + Options: []string{"rprivate", "nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620"}, + } + g.AddMount(devPts) } + if inUserNS && config.IpcMode.IsHost() { g.RemoveMount("/dev/mqueue") devMqueue := spec.Mount{ @@ -406,6 +418,62 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM } } + // Add annotations + if configSpec.Annotations == nil { + configSpec.Annotations = make(map[string]string) + } + + if config.CidFile != "" { + configSpec.Annotations[libpod.InspectAnnotationCIDFile] = config.CidFile + } + + if config.Rm { + configSpec.Annotations[libpod.InspectAnnotationAutoremove] = libpod.InspectResponseTrue + } else { + configSpec.Annotations[libpod.InspectAnnotationAutoremove] = libpod.InspectResponseFalse + } + + if len(config.VolumesFrom) > 0 { + configSpec.Annotations[libpod.InspectAnnotationVolumesFrom] = strings.Join(config.VolumesFrom, ",") + } + + if config.Privileged { + configSpec.Annotations[libpod.InspectAnnotationPrivileged] = libpod.InspectResponseTrue + } else { + configSpec.Annotations[libpod.InspectAnnotationPrivileged] = libpod.InspectResponseFalse + } + + if config.PublishAll { + configSpec.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseTrue + } else { + configSpec.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseFalse + } + + if config.Init { + configSpec.Annotations[libpod.InspectAnnotationInit] = libpod.InspectResponseTrue + } else { + configSpec.Annotations[libpod.InspectAnnotationInit] = libpod.InspectResponseFalse + } + + for _, opt := range config.SecurityOpts { + // Split on both : and = + splitOpt := strings.Split(opt, "=") + if len(splitOpt) == 1 { + splitOpt = strings.Split(opt, ":") + } + if len(splitOpt) < 2 { + continue + } + switch splitOpt[0] { + case "label": + configSpec.Annotations[libpod.InspectAnnotationLabel] = splitOpt[1] + case "seccomp": + configSpec.Annotations[libpod.InspectAnnotationSeccomp] = splitOpt[1] + case "apparmor": + configSpec.Annotations[libpod.InspectAnnotationApparmor] = splitOpt[1] + } + } + return configSpec, nil } @@ -567,13 +635,7 @@ func addRlimits(config *CreateConfig, g *generate.Generator) error { if len(config.Resources.Ulimit) != 1 { return errors.New("ulimit can use host only once") } - hostLimits, err := getHostRlimits() - if err != nil { - return err - } - for _, i := range hostLimits { - g.AddProcessRlimits(i.name, i.max, i.cur) - } + g.Config.Process.Rlimits = nil break } diff --git a/pkg/spec/spec_linux.go b/pkg/spec/spec_linux.go deleted file mode 100644 index fcdfc5c4e..000000000 --- a/pkg/spec/spec_linux.go +++ /dev/null @@ -1,42 +0,0 @@ -//+build linux - -package createconfig - -import ( - "syscall" - - "github.com/pkg/errors" -) - -type systemRlimit struct { - name string - value int -} - -var systemLimits = []systemRlimit{ - {"RLIMIT_AS", syscall.RLIMIT_AS}, - {"RLIMIT_CORE", syscall.RLIMIT_CORE}, - {"RLIMIT_CPU", syscall.RLIMIT_CPU}, - {"RLIMIT_DATA", syscall.RLIMIT_DATA}, - {"RLIMIT_FSIZE", syscall.RLIMIT_FSIZE}, - {"RLIMIT_NOFILE", syscall.RLIMIT_NOFILE}, - {"RLIMIT_STACK", syscall.RLIMIT_STACK}, -} - -func getHostRlimits() ([]systemUlimit, error) { - ret := []systemUlimit{} - for _, i := range systemLimits { - var l syscall.Rlimit - if err := syscall.Getrlimit(i.value, &l); err != nil { - return nil, errors.Wrapf(err, "cannot read limits for %s", i.name) - } - s := systemUlimit{ - name: i.name, - max: l.Max, - cur: l.Cur, - } - ret = append(ret, s) - } - return ret, nil - -} diff --git a/pkg/spec/spec_unsupported.go b/pkg/spec/spec_unsupported.go deleted file mode 100644 index 0f6a9acdc..000000000 --- a/pkg/spec/spec_unsupported.go +++ /dev/null @@ -1,7 +0,0 @@ -//+build !linux - -package createconfig - -func getHostRlimits() ([]systemUlimit, error) { - return nil, nil -} diff --git a/pkg/spec/storage.go b/pkg/spec/storage.go index ed767f5ba..88f1f6dc1 100644 --- a/pkg/spec/storage.go +++ b/pkg/spec/storage.go @@ -211,6 +211,13 @@ func (config *CreateConfig) parseVolumes(runtime *libpod.Runtime) ([]spec.Mount, } mount.Options = opts } + if mount.Type == TypeBind { + absSrc, err := filepath.Abs(mount.Source) + if err != nil { + return nil, nil, errors.Wrapf(err, "error getting absolute path of %s", mount.Source) + } + mount.Source = absSrc + } finalMounts = append(finalMounts, mount) } finalVolumes := make([]*libpod.ContainerNamedVolume, 0, len(baseVolumes)) diff --git a/pkg/util/utils_linux.go b/pkg/util/utils_linux.go index 47fa1031f..318bd2b1b 100644 --- a/pkg/util/utils_linux.go +++ b/pkg/util/utils_linux.go @@ -1,7 +1,14 @@ package util import ( + "fmt" + "os" + "path/filepath" + "syscall" + "github.com/containers/psgo" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // GetContainerPidInformationDescriptors returns a string slice of all supported @@ -9,3 +16,39 @@ import ( func GetContainerPidInformationDescriptors() ([]string, error) { return psgo.ListDescriptors(), nil } + +// FindDeviceNodes parses /dev/ into a set of major:minor -> path, where +// [major:minor] is the device's major and minor numbers formatted as, for +// example, 2:0 and path is the path to the device node. +// Symlinks to nodes are ignored. +func FindDeviceNodes() (map[string]string, error) { + nodes := make(map[string]string) + err := filepath.Walk("/dev", func(path string, info os.FileInfo, err error) error { + if err != nil { + logrus.Warnf("Error descending into path %s: %v", path, err) + return filepath.SkipDir + } + + // If we aren't a device node, do nothing. + if info.Mode()&(os.ModeDevice|os.ModeCharDevice) == 0 { + return nil + } + + // We are a device node. Get major/minor. + sysstat, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return errors.Errorf("Could not convert stat output for use") + } + major := uint64(sysstat.Rdev / 256) + minor := uint64(sysstat.Rdev % 256) + + nodes[fmt.Sprintf("%d:%d", major, minor)] = path + + return nil + }) + if err != nil { + return nil, err + } + + return nodes, nil +} diff --git a/pkg/util/utils_unsupported.go b/pkg/util/utils_unsupported.go new file mode 100644 index 000000000..62805d7c8 --- /dev/null +++ b/pkg/util/utils_unsupported.go @@ -0,0 +1,12 @@ +// +build darwin windows + +package util + +import ( + "github.com/pkg/errors" +) + +// FindDeviceNodes is not implemented anywhere except Linux. +func FindDeviceNodes() (map[string]string, error) { + return nil, errors.Errorf("not supported on non-Linux OSes") +} diff --git a/pkg/varlinkapi/containers.go b/pkg/varlinkapi/containers.go index 6855a7231..700e02b0c 100644 --- a/pkg/varlinkapi/containers.go +++ b/pkg/varlinkapi/containers.go @@ -19,7 +19,6 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/logs" "github.com/containers/libpod/pkg/adapter/shortcuts" - cc "github.com/containers/libpod/pkg/spec" "github.com/containers/storage/pkg/archive" "github.com/pkg/errors" ) @@ -170,16 +169,7 @@ func (i *LibpodAPI) InspectContainer(call iopodman.VarlinkCall, name string) err if err != nil { return call.ReplyContainerNotFound(name, err.Error()) } - inspectInfo, err := ctr.Inspect(true) - if err != nil { - return call.ReplyErrorOccurred(err.Error()) - } - artifact, err := getArtifact(ctr) - if err != nil { - return call.ReplyErrorOccurred(err.Error()) - } - - data, err := shared.GetCtrInspectInfo(ctr.Config(), inspectInfo, artifact) + data, err := ctr.Inspect(true) if err != nil { return call.ReplyErrorOccurred(err.Error()) } @@ -587,18 +577,6 @@ func (i *LibpodAPI) ContainerRestore(call iopodman.VarlinkCall, name string, kee return call.ReplyContainerRestore(ctr.ID()) } -func getArtifact(ctr *libpod.Container) (*cc.CreateConfig, error) { - var createArtifact cc.CreateConfig - artifact, err := ctr.GetArtifact("create-config") - if err != nil { - return nil, err - } - if err := json.Unmarshal(artifact, &createArtifact); err != nil { - return nil, err - } - return &createArtifact, nil -} - // ContainerConfig returns just the container.config struct func (i *LibpodAPI) ContainerConfig(call iopodman.VarlinkCall, name string) error { ctr, err := i.Runtime.LookupContainer(name) diff --git a/pkg/varlinkapi/transfers.go b/pkg/varlinkapi/transfers.go index 24a91a86f..31d26c3aa 100644 --- a/pkg/varlinkapi/transfers.go +++ b/pkg/varlinkapi/transfers.go @@ -26,11 +26,6 @@ func (i *LibpodAPI) SendFile(call iopodman.VarlinkCall, ftype string, length int defer outputFile.Close() if err = call.ReplySendFile(outputFile.Name()); err != nil { - return call.ReplyErrorOccurred(err.Error()) - } - - // FIXME return parameter - if err = call.ReplySendFile("FIXME_file_handle"); err != nil { // If an error occurs while sending the reply, return the error return err } diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 21afc4b84..ef1c85518 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -12,7 +12,6 @@ import ( "testing" "time" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/inspect" "github.com/containers/libpod/pkg/rootless" @@ -322,7 +321,7 @@ func (s *PodmanSessionIntegration) InspectImageJSON() []inspect.ImageData { } // InspectContainer returns a container's inspect data in JSON format -func (p *PodmanTestIntegration) InspectContainer(name string) []shared.InspectContainer { +func (p *PodmanTestIntegration) InspectContainer(name string) []libpod.InspectContainerData { cmd := []string{"inspect", name} session := p.Podman(cmd) session.WaitWithDefaultTimeout() @@ -375,7 +374,7 @@ func (p *PodmanTestIntegration) RunNginxWithHealthCheck(name string) (*PodmanSes if name != "" { podmanArgs = append(podmanArgs, "--name", name) } - podmanArgs = append(podmanArgs, "-dt", "-P", "--healthcheck-command", "CMD-SHELL curl http://localhost/", nginx) + podmanArgs = append(podmanArgs, "-dt", "-P", "--health-cmd", "curl http://localhost/", nginx) session := p.Podman(podmanArgs) session.WaitWithDefaultTimeout() return session, session.OutputToString() @@ -481,8 +480,8 @@ func (p *PodmanTestIntegration) PullImage(image string) error { // InspectContainerToJSON takes the session output of an inspect // container and returns json -func (s *PodmanSessionIntegration) InspectContainerToJSON() []shared.InspectContainer { - var i []shared.InspectContainer +func (s *PodmanSessionIntegration) InspectContainerToJSON() []libpod.InspectContainerData { + var i []libpod.InspectContainerData err := json.Unmarshal(s.Out.Contents(), &i) Expect(err).To(BeNil()) return i diff --git a/test/e2e/healthcheck_run_test.go b/test/e2e/healthcheck_run_test.go index 125002bf9..dafc8a837 100644 --- a/test/e2e/healthcheck_run_test.go +++ b/test/e2e/healthcheck_run_test.go @@ -95,7 +95,7 @@ var _ = Describe("Podman healthcheck run", func() { }) It("podman healthcheck should be starting", func() { - session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "--healthcheck-retries", "2", "--healthcheck-command", "\"CMD-SHELL ls /foo || exit 1\"", ALPINE, "top"}) + session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "--health-retries", "2", "--health-cmd", "ls /foo || exit 1", ALPINE, "top"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) inspect := podmanTest.InspectContainer("hc") @@ -103,7 +103,7 @@ var _ = Describe("Podman healthcheck run", func() { }) It("podman healthcheck failed checks in start-period should not change status", func() { - session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "--healthcheck-start-period", "2m", "--healthcheck-retries", "2", "--healthcheck-command", "\"CMD-SHELL ls /foo || exit 1\"", ALPINE, "top"}) + session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "--health-start-period", "2m", "--health-retries", "2", "--health-cmd", "ls /foo || exit 1", ALPINE, "top"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) @@ -124,7 +124,7 @@ var _ = Describe("Podman healthcheck run", func() { }) It("podman healthcheck failed checks must reach retries before unhealthy ", func() { - session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "--healthcheck-retries", "2", "--healthcheck-command", "\"CMD-SHELL ls /foo || exit 1\"", ALPINE, "top"}) + session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "--health-retries", "2", "--health-cmd", "ls /foo || exit 1", ALPINE, "top"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) @@ -145,7 +145,7 @@ var _ = Describe("Podman healthcheck run", func() { }) It("podman healthcheck good check results in healthy even in start-period", func() { - session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "--healthcheck-start-period", "2m", "--healthcheck-retries", "2", "--healthcheck-command", "\"CMD-SHELL\" \"ls\" \"||\" \"exit\" \"1\"", ALPINE, "top"}) + session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "--health-start-period", "2m", "--health-retries", "2", "--health-cmd", "ls || exit 1", ALPINE, "top"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) @@ -158,7 +158,7 @@ var _ = Describe("Podman healthcheck run", func() { }) It("podman healthcheck single healthy result changes failed to healthy", func() { - session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "--healthcheck-retries", "2", "--healthcheck-command", "\"CMD-SHELL\" \"ls\" \"/foo\" \"||\" \"exit\" \"1\"", ALPINE, "top"}) + session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "--health-retries", "2", "--health-cmd", "ls /foo || exit 1", ALPINE, "top"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go new file mode 100644 index 000000000..a6f59a3da --- /dev/null +++ b/test/e2e/play_kube_test.go @@ -0,0 +1,123 @@ +// +build !remoteclient + +package integration + +import ( + "os" + "path/filepath" + "text/template" + + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var yamlTemplate = ` +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: "2019-07-17T14:44:08Z" + labels: + app: {{ .Name }} + name: {{ .Name }} +spec: + containers: +{{ with .Containers }} + {{ range . }} + - command: + - {{ .Cmd }} + env: + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - name: TERM + value: xterm + - name: HOSTNAME + - name: container + value: podman + image: {{ .Image }} + name: {{ .Name }} + resources: {} + securityContext: + allowPrivilegeEscalation: true + capabilities: {} + privileged: false + readOnlyRootFilesystem: false + workingDir: / + {{ end }} +{{ end }} +status: {} +` + +type Pod struct { + Name string + Containers []Container +} + +type Container struct { + Cmd string + Image string + Name string +} + +func generateKubeYaml(ctrs []Container, fileName string) error { + f, err := os.Create(fileName) + if err != nil { + return err + } + defer f.Close() + testPod := Pod{"test", ctrs} + + t, err := template.New("pod").Parse(yamlTemplate) + if err != nil { + return err + } + + if err := t.Execute(f, testPod); err != nil { + return err + } + + return nil +} + +var _ = Describe("Podman generate kube", 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.SeedImages() + }) + + AfterEach(func() { + podmanTest.Cleanup() + f := CurrentGinkgoTestDescription() + processTestResult(f) + }) + + It("podman play kube test correct command", func() { + ctrName := "testCtr" + ctrCmd := "top" + testContainer := Container{ctrCmd, ALPINE, ctrName} + tempFile := filepath.Join(podmanTest.TempDir, "kube.yaml") + + err := generateKubeYaml([]Container{testContainer}, tempFile) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", tempFile}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + inspect := podmanTest.Podman([]string{"inspect", ctrName}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + Expect(inspect.OutputToString()).To(ContainSubstring(ctrCmd)) + }) +}) diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index 8c7830204..e35c84f5b 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -750,21 +750,21 @@ USER mail` }) It("podman run with bad healthcheck retries", func() { - session := podmanTest.Podman([]string{"run", "-dt", "--healthcheck-command", "foo", "--healthcheck-retries", "0", ALPINE, "top"}) + session := podmanTest.Podman([]string{"run", "-dt", "--health-cmd", "[\"foo\"]", "--health-retries", "0", ALPINE, "top"}) session.Wait() Expect(session.ExitCode()).ToNot(Equal(0)) Expect(session.ErrorToString()).To(ContainSubstring("healthcheck-retries must be greater than 0")) }) It("podman run with bad healthcheck timeout", func() { - session := podmanTest.Podman([]string{"run", "-dt", "--healthcheck-command", "foo", "--healthcheck-timeout", "0s", ALPINE, "top"}) + session := podmanTest.Podman([]string{"run", "-dt", "--health-cmd", "[\"foo\"]", "--health-timeout", "0s", ALPINE, "top"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).ToNot(Equal(0)) Expect(session.ErrorToString()).To(ContainSubstring("healthcheck-timeout must be at least 1 second")) }) It("podman run with bad healthcheck start-period", func() { - session := podmanTest.Podman([]string{"run", "-dt", "--healthcheck-command", "foo", "--healthcheck-start-period", "-1s", ALPINE, "top"}) + session := podmanTest.Podman([]string{"run", "-dt", "--health-cmd", "[\"foo\"]", "--health-start-period", "-1s", ALPINE, "top"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).ToNot(Equal(0)) Expect(session.ErrorToString()).To(ContainSubstring("healthcheck-start-period must be 0 seconds or greater")) diff --git a/vendor/modules.txt b/vendor/modules.txt index ad2f69976..462ba1408 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -355,8 +355,8 @@ github.com/opencontainers/runc/libcontainer/system github.com/opencontainers/runtime-spec/specs-go # github.com/opencontainers/runtime-tools v0.9.0 github.com/opencontainers/runtime-tools/generate -github.com/opencontainers/runtime-tools/generate/seccomp github.com/opencontainers/runtime-tools/validate +github.com/opencontainers/runtime-tools/generate/seccomp github.com/opencontainers/runtime-tools/filepath github.com/opencontainers/runtime-tools/specerror github.com/opencontainers/runtime-tools/error |