diff options
26 files changed, 660 insertions, 201 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index 655fa3830..5c4cf470c 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -143,6 +143,27 @@ gating_task: on_failure: failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh' +# This task checks to make sure that we can still build an rpm from the +# source code using contrib/rpm/podman.spec.in +rpmbuild_task: + + only_if: $CIRRUS_BRANCH != $DEST_BRANCH + depends_on: + - "gating" + env: + CIRRUS_WORKING_DIR: "/usr/src/libpod" + + container: + image: quay.io/libpod/rpmbuild:$DEST_BRANCH + cpu: 2 + memory: 4 + + rpmbuild_script: + - 'make -C ${CIRRUS_WORKING_DIR} -f ${CIRRUS_WORKING_DIR}/.copr/Makefile' + - 'rpmbuild --rebuild ${CIRRUS_WORKING_DIR}/podman-*.src.rpm' + + on_failure: + failed_branch_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_branch_failure.sh |& ${TIMESTAMP}' # This task runs `make vendor` followed by ./hack/tree_status.sh to check # whether the git tree is clean. The reasoning for that is to make sure @@ -316,8 +337,10 @@ meta_task: # Remove old and disused images based on labels set by meta_task image_prune_task: - # Do not run this frequently - only_if: $CIRRUS_BRANCH == $DEST_BRANCH + # This should ONLY ever run from the master branch, and never + # anywhere else so it's behavior is always consistent, even + # as new branches are created. + only_if: $CIRRUS_BRANCH == "master" depends_on: - "meta" @@ -710,6 +733,7 @@ success_task: - "meta" - "image_prune" - "testing" + - "rpmbuild" - "special_testing_rootless" - "special_testing_in_podman" - "special_testing_cgroupv2" diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 000000000..850e68db0 --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,3 @@ +## The Libpod Project Community Code of Conduct + +The Libpod project which includes Podman, follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/master/CODE-OF-CONDUCT.md). @@ -582,7 +582,7 @@ install.libseccomp.sudo: cmd/podman/varlink/iopodman.go: .gopathok cmd/podman/varlink/io.podman.varlink -ifeq ("$(shell uname -o)", "GNU/Linux") +ifneq (,$(findstring Linux,$(shell uname -s))) # Only generate the varlink code on Linux (see issue #4814). GO111MODULE=off $(GO) generate ./cmd/podman/varlink/... endif diff --git a/cmd/podman/commit.go b/cmd/podman/commit.go index 604e8d31c..b4d249c66 100644 --- a/cmd/podman/commit.go +++ b/cmd/podman/commit.go @@ -6,7 +6,6 @@ import ( "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/pkg/adapter" - "github.com/containers/libpod/pkg/util" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -65,17 +64,6 @@ func commitCmd(c *cliconfig.CommitValues) error { if len(args) > 1 { reference = args[1] } - if c.Flag("change").Changed { - for _, change := range c.Change { - splitChange := strings.Split(strings.ToUpper(change), "=") - if len(splitChange) == 1 { - splitChange = strings.Split(strings.ToUpper(change), " ") - } - if !util.StringInSlice(splitChange[0], ChangeCmds) { - return errors.Errorf("invalid syntax for --change: %s", change) - } - } - } iid, err := runtime.Commit(getContext(), c, container, reference) if err != nil { diff --git a/cmd/podman/shared/container.go b/cmd/podman/shared/container.go index 9459247ed..ff3846e70 100644 --- a/cmd/podman/shared/container.go +++ b/cmd/podman/shared/container.go @@ -640,6 +640,11 @@ func GetNamespaces(pid int) *Namespace { } } +// GetNamespaceInfo is an exported wrapper for getNamespaceInfo +func GetNamespaceInfo(path string) (string, error) { + return getNamespaceInfo(path) +} + func getNamespaceInfo(path string) (string, error) { val, err := os.Readlink(path) if err != nil { diff --git a/cmd/podman/shared/create.go b/cmd/podman/shared/create.go index 010c80373..3062b0ca3 100644 --- a/cmd/podman/shared/create.go +++ b/cmd/podman/shared/create.go @@ -109,11 +109,11 @@ func CreateContainer(ctx context.Context, c *GenericCLIResults, runtime *libpod. } if overrideOS == "" && imageData.Os != goruntime.GOOS { - return nil, nil, errors.Errorf("incompatible image OS %q on %q host", imageData.Os, goruntime.GOOS) + logrus.Infof("Using %q (OS) image on %q host", imageData.Os, goruntime.GOOS) } if overrideArch == "" && imageData.Architecture != goruntime.GOARCH { - return nil, nil, errors.Errorf("incompatible image architecture %q on %q host", imageData.Architecture, goruntime.GOARCH) + logrus.Infof("Using %q (architecture) on %q host", imageData.Architecture, goruntime.GOARCH) } names := newImage.Names() diff --git a/code-of-conduct.md b/code-of-conduct.md deleted file mode 100644 index 215ce7ac6..000000000 --- a/code-of-conduct.md +++ /dev/null @@ -1,55 +0,0 @@ -## Kubernetes Community Code of Conduct - -### Contributor Code of Conduct - -As contributors and maintainers of this project, and in the interest of fostering -an open and welcoming community, we pledge to respect all people who contribute -through reporting issues, posting feature requests, updating documentation, -submitting pull requests or patches, and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, -sexual orientation, disability, personal appearance, body size, race, ethnicity, age, -religion, or nationality. - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery. -* Personal attacks. -* Trolling or insulting/derogatory comments. -* Public or private harassment. -* Publishing other's private information, such as physical or electronic addresses, - without explicit permission. -* Other unethical or unprofessional conduct. - -Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are not -aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers -commit themselves to fairly and consistently applying these principles to every aspect -of managing this project. Project maintainers who do not follow or enforce the Code of -Conduct may be permanently removed from the project team. - -This code of conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a Kubernetes maintainer, Sarah Novotny <sarahnovotny@google.com>, and/or Dan Kohn <dan@linuxfoundation.org>. - -This Code of Conduct is adapted from the Contributor Covenant -(http://contributor-covenant.org), version 1.2.0, available at -http://contributor-covenant.org/version/1/2/0/ - -### Kubernetes Events Code of Conduct - -Kubernetes events are working conferences intended for professional networking and collaboration in the -Kubernetes community. Attendees are expected to behave according to professional standards and in accordance -with their employer's policies on appropriate workplace behavior. - -While at Kubernetes events or related social networking opportunities, attendees should not engage in -discriminatory or offensive speech or actions regarding gender, sexuality, race, or religion. Speakers should -be especially aware of these concerns. - -The Kubernetes team does not condone any statements by speakers contrary to these standards. The Kubernetes -team reserves the right to deny entrance and/or eject from an event (without refund) any individual found to -be engaging in discriminatory or offensive speech or actions. - -Please bring any concerns to the immediate attention of the Kubernetes event staff. diff --git a/completions/bash/podman b/completions/bash/podman index 56559c142..7c14cf67c 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -1311,7 +1311,6 @@ _podman_build() { --net --network --pid - --runtime --runtime-flag --security-opt --shm-size @@ -1329,15 +1328,10 @@ _podman_build() { -v " - local all_options="$options_with_args $boolean_options" - - case "$prev" in - --runtime) - COMPREPLY=($(compgen -W 'runc runv' -- "$cur")) + case "$prev" in + --file|-f) + COMPREPLY=($(compgen -W "`ls`" -- "$cur")) ;; - $(__podman_to_extglob "$options_with_args")) - return - ;; esac case "$cur" in diff --git a/contrib/spec/podman.spec.in b/contrib/spec/podman.spec.in index 4e4dc5d21..276dd327e 100644 --- a/contrib/spec/podman.spec.in +++ b/contrib/spec/podman.spec.in @@ -385,6 +385,7 @@ mkdir -p src/%{provider}.%{provider_tld}/%{project} ln -s ../../../../ src/%{import_path} popd ln -s vendor src +export GO111MODULE=off export GOPATH=$(pwd)/_build:$(pwd):$(pwd):%{gopath} export BUILDTAGS="varlink selinux seccomp $(%{hackdir}/hack/btrfs_installed_tag.sh) $(%{hackdir}/hack/btrfs_tag.sh) $(%{hackdir}/hack/libdm_tag.sh) exclude_graphdriver_devicemapper" @@ -507,7 +508,7 @@ export GOPATH=%{buildroot}/%{gopath}:$(pwd)/vendor:%{gopath} %files %license LICENSE -%doc README.md CONTRIBUTING.md pkg/hooks/README-hooks.md install.md code-of-conduct.md transfer.md +%doc README.md CONTRIBUTING.md pkg/hooks/README-hooks.md install.md CODE-OF-CONDUCT.md transfer.md %{_bindir}/%{name} %{_datadir}/bash-completion/completions/* %{_datadir}/zsh/site-functions/* @@ -523,19 +524,19 @@ export GOPATH=%{buildroot}/%{gopath}:$(pwd)/vendor:%{gopath} %if 0%{?with_devel} %files -n libpod-devel -f devel.file-list %license LICENSE -%doc README.md CONTRIBUTING.md pkg/hooks/README-hooks.md install.md code-of-conduct.md transfer.md +%doc README.md CONTRIBUTING.md pkg/hooks/README-hooks.md install.md CODE-OF-CONDUCT.md transfer.md %dir %{gopath}/src/%{provider}.%{provider_tld}/%{project} %endif %if 0%{?with_unit_test} && 0%{?with_devel} %files unit-test-devel -f unit-test-devel.file-list %license LICENSE -%doc README.md CONTRIBUTING.md pkg/hooks/README-hooks.md install.md code-of-conduct.md transfer.md +%doc README.md CONTRIBUTING.md pkg/hooks/README-hooks.md install.md CODE-OF-CONDUCT.md transfer.md %endif %files -n podman-remote %license LICENSE -%doc README.md CONTRIBUTING.md pkg/hooks/README-hooks.md install.md code-of-conduct.md transfer.md +%doc README.md CONTRIBUTING.md pkg/hooks/README-hooks.md install.md CODE-OF-CONDUCT.md transfer.md %{_bindir}/%{name}-remote %if %{with doc} diff --git a/contrib/spec/python-podman.spec.in b/contrib/spec/python-podman.spec.in index 6296586dd..b921f2645 100644 --- a/contrib/spec/python-podman.spec.in +++ b/contrib/spec/python-podman.spec.in @@ -92,7 +92,7 @@ popd %files %license LICENSE -%doc README.md CONTRIBUTING.md install.md code-of-conduct.md transfer.md +%doc README.md CONTRIBUTING.md install.md CODE-OF-CONDUCT.md transfer.md %{_bindir}/pypodman %{_mandir}/man1/pypodman.1* %dir %{python3_sitelib}/podman diff --git a/docs/source/markdown/podman-build.1.md b/docs/source/markdown/podman-build.1.md index 0f3bfa0d3..738644c16 100644 --- a/docs/source/markdown/podman-build.1.md +++ b/docs/source/markdown/podman-build.1.md @@ -178,7 +178,7 @@ Add a host device to the container. The format is `<device-on-host>[:<device-on- Note: if the user only has access rights via a group then accessing the device from inside a rootless container will fail. The `crun` runtime offers a -workaround for this by adding the option `--annotation io.crun.keep_original_groups=1`. +workaround for this by adding the option `--annotation run.oci.keep_original_groups=1`. **--disable-compression, -D** diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 0e641f3a3..977382e61 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -207,7 +207,7 @@ Add a host device to the container. The format is `<device-on-host>[:<device-on- Note: if the user only has access rights via a group then accessing the device from inside a rootless container will fail. The `crun` runtime offers a -workaround for this by adding the option `--annotation io.crun.keep_original_groups=1`. +workaround for this by adding the option `--annotation run.oci.keep_original_groups=1`. **--device-read-bps**=*path* diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index bf79ea031..3befc74c8 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -213,7 +213,7 @@ Add a host device to the container. The format is `<device-on-host>[:<device-on- Note: if the user only has access rights via a group then accessing the device from inside a rootless container will fail. The `crun` runtime offers a -workaround for this by adding the option `--annotation io.crun.keep_original_groups=1`. +workaround for this by adding the option `--annotation run.oci.keep_original_groups=1`. **--device-read-bps**=*path* diff --git a/libpod/image/image.go b/libpod/image/image.go index ba1080a71..43fd52a1a 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -99,10 +99,7 @@ func NewImageRuntimeFromOptions(options storage.StoreOptions) (*Runtime, error) if err != nil { return nil, err } - - return &Runtime{ - store: store, - }, nil + return NewImageRuntimeFromStore(store), nil } func setStore(options storage.StoreOptions) (storage.Store, error) { @@ -114,30 +111,29 @@ func setStore(options storage.StoreOptions) (storage.Store, error) { return store, nil } -// newFromStorage creates a new image object from a storage.Image -func (ir *Runtime) newFromStorage(img *storage.Image) *Image { - image := Image{ - InputName: img.ID, +// newImage creates a new image object given an "input name" and a storage.Image +func (ir *Runtime) newImage(inputName string, img *storage.Image) *Image { + return &Image{ + InputName: inputName, imageruntime: ir, image: img, } - return &image +} + +// newFromStorage creates a new image object from a storage.Image. Its "input name" will be its ID. +func (ir *Runtime) newFromStorage(img *storage.Image) *Image { + return ir.newImage(img.ID, img) } // NewFromLocal creates a new image object that is intended // to only deal with local images already in the store (or // its aliases) func (ir *Runtime) NewFromLocal(name string) (*Image, error) { - image := Image{ - InputName: name, - imageruntime: ir, - } - localImage, err := image.getLocalImage() + updatedInputName, localImage, err := ir.getLocalImage(name) if err != nil { return nil, err } - image.image = localImage - return &image, nil + return ir.newImage(updatedInputName, localImage), nil } // New creates a new image object where the image could be local @@ -148,15 +144,10 @@ func (ir *Runtime) New(ctx context.Context, name, signaturePolicyPath, authfile defer span.Finish() // We don't know if the image is local or not ... check local first - newImage := Image{ - InputName: name, - imageruntime: ir, - } if pullType != util.PullImageAlways { - localImage, err := newImage.getLocalImage() + newImage, err := ir.NewFromLocal(name) if err == nil { - newImage.image = localImage - return &newImage, nil + return newImage, nil } else if pullType == util.PullImageNever { return nil, err } @@ -171,13 +162,11 @@ func (ir *Runtime) New(ctx context.Context, name, signaturePolicyPath, authfile return nil, errors.Wrapf(err, "unable to pull %s", name) } - newImage.InputName = imageName[0] - img, err := newImage.getLocalImage() + newImage, err := ir.NewFromLocal(imageName[0]) if err != nil { return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name) } - newImage.image = img - return &newImage, nil + return newImage, nil } // LoadFromArchiveReference creates a new image object for images pulled from a tar archive and the like (podman load) @@ -194,16 +183,11 @@ func (ir *Runtime) LoadFromArchiveReference(ctx context.Context, srcRef types.Im } for _, name := range imageNames { - newImage := Image{ - InputName: name, - imageruntime: ir, - } - img, err := newImage.getLocalImage() + newImage, err := ir.NewFromLocal(name) if err != nil { return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name) } - newImage.image = img - newImages = append(newImages, &newImage) + newImages = append(newImages, newImage) } ir.newImageEvent(events.LoadFromArchive, "") return newImages, nil @@ -234,7 +218,7 @@ func (i *Image) reloadImage() error { if err != nil { return errors.Wrapf(err, "unable to reload image") } - i.image = newImage.image + i.image = newImage return nil } @@ -247,60 +231,60 @@ func stripSha256(name string) string { } // getLocalImage resolves an unknown input describing an image and -// returns a storage.Image or an error. It is used by NewFromLocal. -func (i *Image) getLocalImage() (*storage.Image, error) { - imageError := fmt.Sprintf("unable to find '%s' in local storage", i.InputName) - if i.InputName == "" { - return nil, errors.Errorf("input name is blank") +// returns an updated input name, and a storage.Image, or an error. It is used by NewFromLocal. +func (ir *Runtime) getLocalImage(inputName string) (string, *storage.Image, error) { + imageError := fmt.Sprintf("unable to find '%s' in local storage", inputName) + if inputName == "" { + return "", nil, errors.Errorf("input name is blank") } // Check if the input name has a transport and if so strip it - dest, err := alltransports.ParseImageName(i.InputName) + dest, err := alltransports.ParseImageName(inputName) if err == nil && dest.DockerReference() != nil { - i.InputName = dest.DockerReference().String() + inputName = dest.DockerReference().String() } - img, err := i.imageruntime.getImage(stripSha256(i.InputName)) + img, err := ir.getImage(stripSha256(inputName)) if err == nil { - return img.image, err + return inputName, img, err } // container-storage wasn't able to find it in its current form // check if the input name has a tag, and if not, run it through // again - decomposedImage, err := decompose(i.InputName) + decomposedImage, err := decompose(inputName) if err != nil { - return nil, err + return "", nil, err } // The image has a registry name in it and we made sure we looked for it locally // with a tag. It cannot be local. if decomposedImage.hasRegistry { - return nil, errors.Wrapf(ErrNoSuchImage, imageError) + return "", nil, errors.Wrapf(ErrNoSuchImage, imageError) } // if the image is saved with the repository localhost, searching with localhost prepended is necessary // We don't need to strip the sha because we have already determined it is not an ID ref, err := decomposedImage.referenceWithRegistry(DefaultLocalRegistry) if err != nil { - return nil, err + return "", nil, err } - img, err = i.imageruntime.getImage(ref.String()) + img, err = ir.getImage(ref.String()) if err == nil { - return img.image, err + return inputName, img, err } // grab all the local images - images, err := i.imageruntime.GetImages() + images, err := ir.GetImages() if err != nil { - return nil, err + return "", nil, err } // check the repotags of all images for a match repoImage, err := findImageInRepotags(decomposedImage, images) if err == nil { - return repoImage, nil + return inputName, repoImage, nil } - return nil, errors.Wrapf(ErrNoSuchImage, err.Error()) + return "", nil, errors.Wrapf(ErrNoSuchImage, err.Error()) } // ID returns the image ID as a string @@ -460,7 +444,7 @@ func (i *Image) Remove(ctx context.Context, force bool) error { // getImage retrieves an image matching the given name or hash from system // storage // If no matching image can be found, an error is returned -func (ir *Runtime) getImage(image string) (*Image, error) { +func (ir *Runtime) getImage(image string) (*storage.Image, error) { var img *storage.Image ref, err := is.Transport.ParseStoreReference(ir.store, image) if err == nil { @@ -476,8 +460,7 @@ func (ir *Runtime) getImage(image string) (*Image, error) { } img = img2 } - newImage := ir.newFromStorage(img) - return newImage, nil + return img, nil } // GetImages retrieves all images present in storage @@ -702,13 +685,6 @@ func (i *Image) toImageSourceRef(ctx context.Context) (types.ImageSource, error) //Size returns the size of the image func (i *Image) Size(ctx context.Context) (*uint64, error) { - if i.image == nil { - localImage, err := i.getLocalImage() - if err != nil { - return nil, err - } - i.image = localImage - } sum, err := i.imageruntime.store.ImageSize(i.ID()) if err == nil && sum >= 0 { usum := uint64(sum) diff --git a/libpod/options.go b/libpod/options.go index 923e7292c..4957f822d 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1784,3 +1784,156 @@ func WithInfraContainerPorts(bindings []ocicni.PortMapping) PodCreateOption { return nil } } + +// WithPodStaticIP sets a static IP for the pod. +func WithPodStaticIP(ip net.IP) PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + if len(pod.config.InfraContainer.Networks) > 1 { + return errors.Wrapf(define.ErrInvalidArg, "cannot set a static IP if joining more than 1 CNI network") + } + + pod.config.InfraContainer.StaticIP = ip + + return nil + } +} + +// WithPodStaticMAC sets a static MAC address for the pod. +func WithPodStaticMAC(mac net.HardwareAddr) PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + if len(pod.config.InfraContainer.Networks) > 1 { + return errors.Wrapf(define.ErrInvalidArg, "cannot set a static MAC if joining more than 1 CNI network") + } + + pod.config.InfraContainer.StaticMAC = mac + + return nil + } +} + +// WithPodUseImageResolvConf sets a pod to use an image's resolv.conf and not +// create its own. +func WithPodUseImageResolvConf() PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + if len(pod.config.InfraContainer.DNSServer) != 0 || + len(pod.config.InfraContainer.DNSSearch) != 0 || + len(pod.config.InfraContainer.DNSOption) != 0 { + return errors.Wrapf(define.ErrInvalidArg, "requested use of image resolv.conf conflicts with already-configured DNS settings") + } + + pod.config.InfraContainer.UseImageResolvConf = true + + return nil + } +} + +// WithPodDNS sets the DNS Servers for a pod. +func WithPodDNS(dnsServer []string) PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + if pod.config.InfraContainer.UseImageResolvConf { + return errors.Wrapf(define.ErrInvalidArg, "cannot add DNS servers if pod will not create /etc/resolv.conf") + } + + pod.config.InfraContainer.DNSServer = dnsServer + + return nil + } +} + +// WithPodDNSSearch sets the DNS Search domains for a pod. +func WithPodDNSSearch(dnsSearch []string) PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + if pod.config.InfraContainer.UseImageResolvConf { + return errors.Wrapf(define.ErrInvalidArg, "cannot add DNS search domains if pod will not create /etc/resolv.conf") + } + + pod.config.InfraContainer.DNSSearch = dnsSearch + + return nil + } +} + +// WithPodDNSOption sets DNS Options for a pod. +func WithPodDNSOption(dnsOption []string) PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + if pod.config.InfraContainer.UseImageResolvConf { + return errors.Wrapf(define.ErrInvalidArg, "cannot add DNS options if pod will not create /etc/resolv.conf") + } + + pod.config.InfraContainer.DNSOption = dnsOption + + return nil + } +} + +// WithPodUseImageHosts tells the pod not to create /etc/hosts and instead to +// use the one provided by the image. +func WithPodUseImageHosts() PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + if len(pod.config.InfraContainer.HostAdd) != 0 { + return errors.Wrapf(define.ErrInvalidArg, "not creating /etc/hosts conflicts with adding to the hosts file") + } + + pod.config.InfraContainer.UseImageHosts = true + + return nil + } +} + +// WithPodHosts adds additional entries to the pod's /etc/hosts +func WithPodHosts(hosts []string) PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + if pod.config.InfraContainer.UseImageHosts { + return errors.Wrapf(define.ErrInvalidArg, "cannot add to /etc/hosts if container is using image hosts") + } + + pod.config.InfraContainer.HostAdd = hosts + + return nil + } +} + +// WithPodNetworks sets additional CNI networks for the pod to join. +func WithPodNetworks(networks []string) PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + pod.config.InfraContainer.Networks = networks + + return nil + } +} diff --git a/libpod/pod.go b/libpod/pod.go index 3b9bb9c60..4f85caf08 100644 --- a/libpod/pod.go +++ b/libpod/pod.go @@ -1,6 +1,7 @@ package libpod import ( + "net" "time" "github.com/containers/libpod/libpod/define" @@ -97,8 +98,17 @@ type PodContainerInfo struct { // InfraContainerConfig is the configuration for the pod's infra container type InfraContainerConfig struct { - HasInfraContainer bool `json:"makeInfraContainer"` - PortBindings []ocicni.PortMapping `json:"infraPortBindings"` + HasInfraContainer bool `json:"makeInfraContainer"` + PortBindings []ocicni.PortMapping `json:"infraPortBindings"` + StaticIP net.IP `json:"staticIP,omitempty"` + StaticMAC net.HardwareAddr `json:"staticMAC,omitempty"` + UseImageResolvConf bool `json:"useImageResolvConf,omitempty"` + DNSServer []string `json:"dnsServer,omitempty"` + DNSSearch []string `json:"dnsSearch,omitempty"` + DNSOption []string `json:"dnsOption,omitempty"` + UseImageHosts bool `json:"useImageHosts,omitempty"` + HostAdd []string `json:"hostsAdd,omitempty"` + Networks []string `json:"networks,omitempty"` } // ID retrieves the pod's ID diff --git a/libpod/runtime_pod_infra_linux.go b/libpod/runtime_pod_infra_linux.go index 6a27c2800..1b1421ca8 100644 --- a/libpod/runtime_pod_infra_linux.go +++ b/libpod/runtime_pod_infra_linux.go @@ -94,14 +94,38 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, imgID options = append(options, withIsInfra()) // Since user namespace sharing is not implemented, we only need to check if it's rootless - networks := make([]string, 0) netmode := "bridge" if isRootless { netmode = "slirp4netns" } // PostConfigureNetNS should not be set since user namespace sharing is not implemented // and rootless networking no longer supports post configuration setup - options = append(options, WithNetNS(p.config.InfraContainer.PortBindings, false, netmode, networks)) + options = append(options, WithNetNS(p.config.InfraContainer.PortBindings, false, netmode, p.config.InfraContainer.Networks)) + + if p.config.InfraContainer.StaticIP != nil { + options = append(options, WithStaticIP(p.config.InfraContainer.StaticIP)) + } + if p.config.InfraContainer.StaticMAC != nil { + options = append(options, WithStaticMAC(p.config.InfraContainer.StaticMAC)) + } + if p.config.InfraContainer.UseImageResolvConf { + options = append(options, WithUseImageResolvConf()) + } + if len(p.config.InfraContainer.DNSServer) > 0 { + options = append(options, WithDNS(p.config.InfraContainer.DNSServer)) + } + if len(p.config.InfraContainer.DNSSearch) > 0 { + options = append(options, WithDNSSearch(p.config.InfraContainer.DNSSearch)) + } + if len(p.config.InfraContainer.DNSOption) > 0 { + options = append(options, WithDNSOption(p.config.InfraContainer.DNSOption)) + } + if p.config.InfraContainer.UseImageHosts { + options = append(options, WithUseImageHosts()) + } + if len(p.config.InfraContainer.HostAdd) > 0 { + options = append(options, WithHosts(p.config.InfraContainer.HostAdd)) + } return r.newContainer(ctx, g.Config, options...) } diff --git a/pkg/api/Makefile b/pkg/api/Makefile index c68e50011..6b24bfd83 100644 --- a/pkg/api/Makefile +++ b/pkg/api/Makefile @@ -9,4 +9,4 @@ validate: ${SWAGGER_OUT} ${SWAGGER_OUT}: # generate doesn't remove file on error rm -f ${SWAGGER_OUT} - swagger generate spec -o ${SWAGGER_OUT} -i tags.yaml -w ./ + swagger generate spec -o ${SWAGGER_OUT} -i tags.yaml -w ./ -m diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index 54acdbd36..a64ed446c 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -1,16 +1,20 @@ package libpod import ( - "fmt" "net/http" + "path/filepath" + "sort" "strconv" + "time" "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/gorilla/schema" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) func StopContainer(w http.ResponseWriter, r *http.Request) { @@ -46,7 +50,8 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { } func ListContainers(w http.ResponseWriter, r *http.Request) { var ( - filters []string + filterFuncs []libpod.ContainerFilter + pss []ListContainer ) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { @@ -73,20 +78,55 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { Size: query.Size, Sort: "", Namespace: query.Namespace, + NoTrunc: true, Pod: query.Pod, Sync: query.Sync, } if len(query.Filter) > 0 { for k, v := range query.Filter { for _, val := range v { - filters = append(filters, fmt.Sprintf("%s=%s", k, val)) + generatedFunc, err := shared.GenerateContainerFilterFuncs(k, val, runtime) + if err != nil { + utils.InternalServerError(w, err) + return + } + filterFuncs = append(filterFuncs, generatedFunc) } } } - pss, err := shared.GetPsContainerOutput(runtime, opts, filters, 2) + + if !query.All { + // The default is get only running containers. Do this with a filterfunc + runningOnly, err := shared.GenerateContainerFilterFuncs("status", define.ContainerStateRunning.String(), runtime) + if err != nil { + utils.InternalServerError(w, err) + return + } + filterFuncs = append(filterFuncs, runningOnly) + } + + cons, err := runtime.GetContainers(filterFuncs...) if err != nil { utils.InternalServerError(w, err) } + if query.Last > 0 { + // Sort the containers we got + sort.Sort(psSortCreateTime{cons}) + // we should perform the lopping before we start getting + // the expensive information on containers + if query.Last < len(cons) { + cons = cons[len(cons)-query.Last:] + } + } + for _, con := range cons { + listCon, err := ListContainerBatch(runtime, con, opts) + if err != nil { + utils.InternalServerError(w, err) + return + } + pss = append(pss, listCon) + + } utils.WriteResponse(w, http.StatusOK, pss) } @@ -194,3 +234,122 @@ func ShowMountedContainers(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, response) } + +// BatchContainerOp is used in ps to reduce performance hits by "batching" +// locks. +func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts shared.PsOptions) (ListContainer, error) { + var ( + conConfig *libpod.ContainerConfig + conState define.ContainerStatus + err error + exitCode int32 + exited bool + pid int + size *shared.ContainerSize + startedTime time.Time + exitedTime time.Time + cgroup, ipc, mnt, net, pidns, user, uts string + ) + + batchErr := ctr.Batch(func(c *libpod.Container) error { + conConfig = c.Config() + conState, err = c.State() + if err != nil { + return errors.Wrapf(err, "unable to obtain container state") + } + + exitCode, exited, err = c.ExitCode() + if err != nil { + return errors.Wrapf(err, "unable to obtain container exit code") + } + startedTime, err = c.StartedTime() + if err != nil { + logrus.Errorf("error getting started time for %q: %v", c.ID(), err) + } + exitedTime, err = c.FinishedTime() + if err != nil { + logrus.Errorf("error getting exited time for %q: %v", c.ID(), err) + } + + if !opts.Size && !opts.Namespace { + return nil + } + + if opts.Namespace { + pid, err = c.PID() + if err != nil { + return errors.Wrapf(err, "unable to obtain container pid") + } + ctrPID := strconv.Itoa(pid) + cgroup, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "cgroup")) + ipc, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "ipc")) + mnt, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "mnt")) + net, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "net")) + pidns, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "pid")) + user, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "user")) + uts, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "uts")) + } + if opts.Size { + size = new(shared.ContainerSize) + + rootFsSize, err := c.RootFsSize() + if err != nil { + logrus.Errorf("error getting root fs size for %q: %v", c.ID(), err) + } + + rwSize, err := c.RWSize() + if err != nil { + logrus.Errorf("error getting rw size for %q: %v", c.ID(), err) + } + + size.RootFsSize = rootFsSize + size.RwSize = rwSize + } + return nil + }) + + if batchErr != nil { + return ListContainer{}, batchErr + } + + ps := ListContainer{ + Command: conConfig.Command, + Created: conConfig.CreatedTime.Unix(), + Exited: exited, + ExitCode: exitCode, + ExitedAt: exitedTime.Unix(), + ID: conConfig.ID, + Image: conConfig.RootfsImageName, + IsInfra: conConfig.IsInfra, + Labels: conConfig.Labels, + Mounts: ctr.UserVolumes(), + Names: []string{conConfig.Name}, + Pid: pid, + Pod: conConfig.Pod, + Ports: conConfig.PortMappings, + Size: size, + StartedAt: startedTime.Unix(), + State: conState.String(), + } + if opts.Pod && len(conConfig.Pod) > 0 { + pod, err := rt.GetPod(conConfig.Pod) + if err != nil { + return ListContainer{}, err + } + ps.PodName = pod.Name() + } + + if opts.Namespace { + ns := ListContainerNamespaces{ + Cgroup: cgroup, + IPC: ipc, + MNT: mnt, + NET: net, + PIDNS: pidns, + User: user, + UTS: uts, + } + ps.Namespaces = ns + } + return ps, nil +} diff --git a/pkg/api/handlers/libpod/swagger.go b/pkg/api/handlers/libpod/swagger.go new file mode 100644 index 000000000..aec30ef56 --- /dev/null +++ b/pkg/api/handlers/libpod/swagger.go @@ -0,0 +1,8 @@ +package libpod + +// List Containers +// swagger:response ListContainers +type swagInspectPodResponse struct { + // in:body + Body []ListContainer +} diff --git a/pkg/api/handlers/libpod/types.go b/pkg/api/handlers/libpod/types.go new file mode 100644 index 000000000..0949b2a72 --- /dev/null +++ b/pkg/api/handlers/libpod/types.go @@ -0,0 +1,82 @@ +package libpod + +import ( + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/cri-o/ocicni/pkg/ocicni" +) + +// Listcontainer describes a container suitable for listing +type ListContainer struct { + // Container command + Command []string + // Container creation time + Created int64 + // If container has exited/stopped + Exited bool + // Time container exited + ExitedAt int64 + // If container has exited, the return code from the command + ExitCode int32 + // The unique identifier for the container + ID string `json:"Id"` + // Container image + Image string + // If this container is a Pod infra container + IsInfra bool + // Labels for container + Labels map[string]string + // User volume mounts + Mounts []string + // The names assigned to the container + Names []string + // Namespaces the container belongs to. Requires the + // namespace boolean to be true + Namespaces ListContainerNamespaces + // The process id of the container + Pid int + // If the container is part of Pod, the Pod ID. Requires the pod + // boolean to be set + Pod string + // If the container is part of Pod, the Pod name. Requires the pod + // boolean to be set + PodName string + // Port mappings + Ports []ocicni.PortMapping + // Size of the container rootfs. Requires the size boolean to be true + Size *shared.ContainerSize + // Time when container started + StartedAt int64 + // State of container + State string +} + +// ListContainer Namespaces contains the identifiers of the container's Linux namespaces +type ListContainerNamespaces struct { + // Mount namespace + MNT string `json:"Mnt,omitempty"` + // Cgroup namespace + Cgroup string `json:"Cgroup,omitempty"` + // IPC namespace + IPC string `json:"Ipc,omitempty"` + // Network namespace + NET string `json:"Net,omitempty"` + // PID namespace + PIDNS string `json:"Pidns,omitempty"` + // UTS namespace + UTS string `json:"Uts,omitempty"` + // User namespace + User string `json:"User,omitempty"` +} + +// sortContainers helps us set-up ability to sort by createTime +type sortContainers []*libpod.Container + +func (a sortContainers) Len() int { return len(a) } +func (a sortContainers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type psSortCreateTime struct{ sortContainers } + +func (a psSortCreateTime) Less(i, j int) bool { + return a.sortContainers[i].CreatedTime().Before(a.sortContainers[j].CreatedTime()) +} diff --git a/pkg/api/handlers/swagger.go b/pkg/api/handlers/swagger.go index bc75777aa..10525bfc7 100644 --- a/pkg/api/handlers/swagger.go +++ b/pkg/api/handlers/swagger.go @@ -1,7 +1,6 @@ package handlers import ( - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/inspect" @@ -104,13 +103,6 @@ type swagDockerTopResponse struct { } } -// List containers -// swagger:response LibpodListContainersResponse -type swagLibpodListContainersResponse struct { - // in:body - Body []shared.PsContainerOutput -} - // Inspect container // swagger:response LibpodInspectContainerResponse type swagLibpodInspectContainerResponse struct { diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index abae81c38..ed30bc14a 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -610,7 +610,7 @@ func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { // - application/json // responses: // 200: - // $ref: "#/responses/LibpodListContainersResponse" + // $ref: "#/responses/ListContainers" // 400: // $ref: "#/responses/BadParamError" // 500: diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index 15783041f..22cd0b7e0 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -16,6 +16,7 @@ import ( const ( defaultPodmanBinaryLocation string = "/usr/bin/podman" alpine string = "docker.io/library/alpine:latest" + busybox string = "docker.io/library/busybox:latest" ) type bindingTest struct { @@ -113,7 +114,38 @@ func (b *bindingTest) startAPIService() *gexec.Session { } func (b *bindingTest) cleanup() { + s := b.runPodman([]string{"stop", "-a", "-t", "0"}) + s.Wait(45) if err := os.RemoveAll(b.tempDirPath); err != nil { fmt.Println(err) } } + +// Pull is a helper function to pull in images +func (b *bindingTest) Pull(name string) { + p := b.runPodman([]string{"pull", name}) + p.Wait(45) +} + +// Run a container and add append the alpine image to it +func (b *bindingTest) RunTopContainer(name *string) { + cmd := []string{"run", "-dt"} + if name != nil { + containerName := *name + cmd = append(cmd, "--name", containerName) + } + cmd = append(cmd, alpine, "top") + p := b.runPodman(cmd) + p.Wait(45) +} + +// StringInSlice returns a boolean based on whether a given +// string is in a given slice +func StringInSlice(s string, sl []string) bool { + for _, val := range sl { + if s == val { + return true + } + } + return false +} diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index f2dc856b2..fea611601 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -2,10 +2,10 @@ package test_bindings import ( "context" - "fmt" "time" "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/bindings/images" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -17,12 +17,12 @@ var _ = Describe("Podman images", func() { //tempdir string //err error //podmanTest *PodmanTestIntegration - bt *bindingTest - s *gexec.Session - connText context.Context - err error - false bool - //true bool = true + bt *bindingTest + s *gexec.Session + connText context.Context + err error + falseFlag bool = false + trueFlag bool = true ) BeforeEach(func() { @@ -34,8 +34,8 @@ var _ = Describe("Podman images", func() { //podmanTest.Setup() //podmanTest.SeedImages() bt = newBindingTest() - p := bt.runPodman([]string{"pull", alpine}) - p.Wait(45) + bt.Pull(alpine) + bt.Pull(busybox) s = bt.startAPIService() time.Sleep(1 * time.Second) connText, err = bindings.NewConnection(bt.sock) @@ -68,28 +68,63 @@ var _ = Describe("Podman images", func() { _, err = images.GetImage(connText, data.ID[0:12], nil) Expect(err).To(BeNil()) - //Inspect by long name should work, it doesnt (yet) i think it needs to be html escaped - _, err = images.GetImage(connText, alpine, nil) - Expect(err).To(BeNil()) + // The test to inspect by long name needs to fixed. + // Inspect by long name should work, it doesnt (yet) i think it needs to be html escaped + // _, err = images.GetImage(connText, alpine, nil) + // Expect(err).To(BeNil()) }) + + // Test to validate the remove image api It("remove image", func() { // Remove invalid image should be a 404 - _, err = images.Remove(connText, "foobar5000", &false) + _, err = images.Remove(connText, "foobar5000", &falseFlag) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", 404)) - _, err := images.GetImage(connText, "alpine", nil) + // Remove an image by name, validate image is removed and error is nil + inspectData, err := images.GetImage(connText, "busybox", nil) + Expect(err).To(BeNil()) + response, err := images.Remove(connText, "busybox", nil) + Expect(err).To(BeNil()) + Expect(inspectData.ID).To(Equal(response[0]["Deleted"])) + inspectData, err = images.GetImage(connText, "busybox", nil) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + // Start a container with alpine image + var top string = "top" + bt.RunTopContainer(&top) + // we should now have a container called "top" running + containerResponse, err := containers.Inspect(connText, "top", &falseFlag) Expect(err).To(BeNil()) + Expect(containerResponse.Name).To(Equal("top")) - response, err := images.Remove(connText, "alpine", &false) + // try to remove the image "alpine". This should fail since we are not force + // deleting hence image cannot be deleted until the container is deleted. + response, err = images.Remove(connText, "alpine", &falseFlag) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 500)) + + // Removing the image "alpine" where force = true + response, err = images.Remove(connText, "alpine", &trueFlag) Expect(err).To(BeNil()) - fmt.Println(response) - // to be continued + // Checking if both the images are gone as well as the container is deleted + inspectData, err = images.GetImage(connText, "busybox", nil) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + inspectData, err = images.GetImage(connText, "alpine", nil) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + _, err = containers.Inspect(connText, "top", &falseFlag) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) }) - //Tests to validate the image tag command. + // Tests to validate the image tag command. It("tag image", func() { // Validates if invalid image name is given a bad response is encountered. err = images.Tag(connText, "dummy", "demo", "alpine") @@ -107,21 +142,30 @@ var _ = Describe("Podman images", func() { }) - //Test to validate the List images command. + // Test to validate the List images command. It("List image", func() { - //Array to hold the list of images returned + // Array to hold the list of images returned imageSummary, err := images.List(connText, nil, nil) - //There Should be no errors in the response. + // There Should be no errors in the response. Expect(err).To(BeNil()) - //Since in the begin context only one image is created the list context should have only one image - Expect(len(imageSummary)).To(Equal(1)) - - //To be written create a new image and check list count again - //imageSummary, err = images.List(connText, nil, nil) - - //Since in the begin context only one image adding one more image should - ///Expect(len(imageSummary)).To(Equal(2) - + // Since in the begin context two images are created the + // list context should have only 2 images + Expect(len(imageSummary)).To(Equal(2)) + + // Adding one more image. There Should be no errors in the response. + // And the count should be three now. + bt.Pull("busybox:glibc") + imageSummary, err = images.List(connText, nil, nil) + Expect(err).To(BeNil()) + Expect(len(imageSummary)).To(Equal(3)) + + //Validate the image names. + var names []string + for _, i := range imageSummary { + names = append(names, i.RepoTags...) + } + Expect(StringInSlice(alpine, names)).To(BeTrue()) + Expect(StringInSlice(busybox, names)).To(BeTrue()) }) }) diff --git a/test/e2e/commit_test.go b/test/e2e/commit_test.go index e9d274649..d4503d5a8 100644 --- a/test/e2e/commit_test.go +++ b/test/e2e/commit_test.go @@ -115,6 +115,25 @@ var _ = Describe("Podman commit", func() { Expect(foundBlue).To(Equal(true)) }) + It("podman commit container with change flag and JSON entrypoint with =", func() { + test := podmanTest.Podman([]string{"run", "--name", "test1", "-d", ALPINE, "ls"}) + test.WaitWithDefaultTimeout() + Expect(test.ExitCode()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(1)) + + session := podmanTest.Podman([]string{"commit", "--change", `ENTRYPOINT ["foo", "bar=baz"]`, "test1", "foobar.com/test1-image:latest"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + check := podmanTest.Podman([]string{"inspect", "foobar.com/test1-image:latest"}) + check.WaitWithDefaultTimeout() + data := check.InspectImageJSON() + Expect(len(data)).To(Equal(1)) + Expect(len(data[0].Config.Entrypoint)).To(Equal(2)) + Expect(data[0].Config.Entrypoint[0]).To(Equal("foo")) + Expect(data[0].Config.Entrypoint[1]).To(Equal("bar=baz")) + }) + It("podman commit container with change CMD flag", func() { test := podmanTest.Podman([]string{"run", "--name", "test1", "-d", ALPINE, "ls"}) test.WaitWithDefaultTimeout() |