diff options
35 files changed, 720 insertions, 165 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index fc528d70f..f2381f7e3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,54 @@ # Release Notes +## 1.8.1 +### Features +- Many networking-related flags have been added to `podman pod create` to enable customization of pod networks, including `--add-host`, `--dns`, `--dns-opt`, `--dns-search`, `--ip`, `--mac-address`, `--network`, and `--no-hosts` +- The `podman ps --format=json` command now includes the ID of the image containers were created with +- The `podman create` and `podman run` commands now support the `--device-cgroup-rule` flag ([#4876](https://github.com/containers/libpod/issues/4876)) +- While the HTTP API remains in alpha, many fixes and additions have landed. These are documented in a separate subsection below + +### Bugfixes +- Fixed CVE-2020-1726, a security issue where volumes manually populated before first being mounted into a container could have those contents overwritten on first being mounted into a container +- Fixed a bug where Podman containers with user namespaces in CNI networks with the DNS plugin enabled would not have the DNS plugin's nameserver added to their `resolv.conf` ([#5256](https://github.com/containers/libpod/issues/5256)) +- Fixed a bug where trailing `/` characters in image volume definitions could cause them to not be overridden by a user-specified mount at the same location ([#5219](https://github.com/containers/libpod/issues/5219)) +- Fixed a bug where the `label` option in `libpod.conf`, used to disable SELinux by default, was not being respected ([#5087](https://github.com/containers/libpod/issues/5087)) +- Fixed a bug where the `podman login` and `podman logout` commands required the registry to log into be specified ([#5146](https://github.com/containers/libpod/issues/5146)) +- Fixed a bug where detached rootless Podman containers could not forward ports ([#5167](https://github.com/containers/libpod/issues/5167)) +- Fixed a bug where rootless Podman could fail to run if the pause process had died +- Fixed a bug where Podman ignored labels that were specified with only a key and no value ([#3854](https://github.com/containers/libpod/issues/3854)) +- Fixed a bug where Podman would fail to create named volumes when the backing filesystem did not support SELinux labelling ([#5200](https://github.com/containers/libpod/issues/5200)) +- Fixed a bug where `--detach-keys=""` would not disable detaching from a container ([#5166](https://github.com/containers/libpod/issues/5166)) +- Fixed a bug where the `podman ps` command was too aggressive when filtering containers and would force `--all` on in too many situations +- Fixed a bug where the `podman play kube` command was ignoring image configuration, including volumes, working directory, labels, and stop signal ([#5174](https://github.com/containers/libpod/issues/5174)) +- Fixed a bug where the `Created` and `CreatedTime` fields in `podman images --format=json` were misnamed, which also broke Go template output for those fields ([#5110](https://github.com/containers/libpod/issues/5110)) +- Fixed a bug where rootless Podman containers with ports forwarded could hang when started ([#5182](https://github.com/containers/libpod/issues/5182)) +- Fixed a bug where `podman pull` could fail to parse registry names including port numbers +- Fixed a bug where Podman would incorrectly attempt to validate image OS and architecture when starting containers +- Fixed a bug where Bash completion for `podman build -f` would not list available files that could be built ([#3878](https://github.com/containers/libpod/issues/3878)) +- Fixed a bug where `podman commit --change` would perform incorrect validation, resulting in valid changes being rejected ([#5148](https://github.com/containers/libpod/issues/5148)) +- Fixed a bug where `podman logs --tail` could take large amounts of memory when the log file for a container was large ([#5131](https://github.com/containers/libpod/issues/5131)) +- Fixed a bug where Podman would sometimes incorrectly generate firewall rules on systems using `firewalld` + +### HTTP API +- Initial support for secure connections to servers via SSH tunneling has been added +- Initial support for the libpod `create` and `logs` endpoints for containers has been added +- Added a `/swagger/` endpoint to serve API documentation +- The `json` endpoint for containers has received many fixes +- Filtering images and containers has been greatly improved, with many bugs fixed and documentation improved +- Image creation endpoints (commit, pull, etc) have seen many fixes +- Server timeout has been fixed so that long operations will no longer trigger the timeout and shut the server down +- The `stats` endpoint for containers has seen major fixes and now provides accurate output +- Handling the HTTP 304 status code has been fixed for all endpoints +- Many fixes have been made to API documentation to ensure it matches the code + +### Misc +- Updated vendored Buildah to v1.14.1 +- Updated vendored containers/storage to v1.16.0 +- The `Created` field to `podman images --format=json` has been renamed to `CreatedSince` as part of the fix for ([#5110](https://github.com/containers/libpod/issues/5110)). Go templates using the old name should still work +- The `CreatedTime` field to `podman images --format=json` has been renamed to `CreatedAt` as part of the fix for ([#5110](https://github.com/containers/libpod/issues/5110)). Go templates using the old name should still work +- The `before` filter to `podman images` has been renamed to `since` for Docker compatibility. Using `before` will still work, but documentation has been changed to use the new `since` filter +- Using the `--password` flag to `podman login` now warns that passwords are being passed in plaintext + ## 1.8.0 ### Features - The `podman system service` command has been added, providing a preview of Podman's new Docker-compatible API. This API is still very new, and not yet ready for production use, but is available for early testing diff --git a/changelog.txt b/changelog.txt index 320526596..0dac716d0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,121 @@ +- Changelog for v1.8.1-rc1 (2020-02-21) + * Update release notes for v1.8.1 + * disable generation of cni firewall plugin + * search endpoint failure correction + * Remove ImageVolumes from database + * Upgrade make package-install for fedora31 + * Flake fix: race condition in same-IP test + * Add support for ssh:// and unix:// podman clients + * search test on fedora registry: retry 5 times + * Swagger: yet more fixes + * Login test: use --password-stdin + * implement reverse reader for log reads + * podman images: add --filter=since=XX + * populate resolv.conf with dnsname responses when in usernamespace + * Beautify podman bridge CNI config + * build(deps): bump github.com/spf13/cobra from 0.0.5 to 0.0.6 + * Warn user about --password cli option in login + * build(deps): bump github.com/stretchr/testify from 1.5.0 to 1.5.1 + * Swagger: fix one incorrect comment + * apiv2 container create using specgen + * Add test to validate the pod bindings api + * Update to the latest version of buildah + * New login and push tests + * Add network options to podman pod create + * Fixed syscall.Signal not convertable by decoder + * Fixed typo in KillContainer + * build(deps): bump github.com/containers/storage from 1.15.8 to 1.16.0 + * build(deps): bump github.com/stretchr/testify from 1.4.0 to 1.5.0 + * libpod.conf: clarify `label` description + * set process labels in pkg/spec + * libpod/config: use built-in TOML instead of manually merging + * Fixed CreateImageFromImage not respecting supplied Tag parameter + * Add installation of pre-commit to Makefile + * fix mandatory parameter in login/logout + * adds missing query struct tags and exports the fields + * Swagger: fix inconsistencies (try #2) + * Update mux rules to allow slashes in image names + * rootless: fix a regression when using -d + * Misc typo fixes + * Use cleaned destination path for indexing image volumes + * Add ability for pods to use the host network + * stats: Expose CPU usage in API + * rootless: check if the conmon process is valid + * apiv2: Fixup /containers/json filters documentation + * apiv2: Enable filtering images by ID + * Fix handler and systemd activation errors + * podman-ps: support image IDs + * Refactor image tree for API usage + * Update documentation of commit command to show image reference is optional + * Rework label parsing + * add caching for binding tests + * apiv2 libpod container logs + * add pkg/signal + * add pkg/capabilities + * build(deps): bump github.com/rootless-containers/rootlesskit + * Fix SELinux labels of volumes + * podman(1): fixes + * fix bug "" disable detach keys + * Fixed a bug about bash automatically complete + * Enhance fuse-overlayfs instructions. + * README: fix docs links + * Fix up play kube to use image data + * build(deps): bump k8s.io/api from 0.17.2 to 0.17.3 + * Only set --all when a status filter is given to ps + * use quay.io/libpod/fedora-minimal for reliability + * filtering behavior correction + * support device-cgroup-rule + * rootlessport: drop Pdeathsig in favor of Kill + * rootlessport: fix potential hang + * add pkg/seccomp + * Do not copy up when volume is not empty + * api: pull: fix reference parsing + * cmd/podman/pull: refactor code + * stats: add SystemUsage + * build(deps): bump k8s.io/apimachinery from 0.17.2 to 0.17.3 + * build(deps): bump github.com/gorilla/mux from 1.7.3 to 1.7.4 + * HTTP 304 (NotModified) is not an error! + * API v2 tests: catch up to moving target + * api: fix the CPU stats reported + * apiv2 stream events + * Fix container filters + * API v2: pods: fix two incorrect return codes + * Rewire ListContainers for APIv2 libpod + * podman build -f completions + * swagger: fix /libpod/images/{import,load,pull} + * Make: s/uname -o/uname -s/ + * container create: relax os/arch checks + * replace prow images test + * Remove incorrect validation of --change for commit + * [CI:DOCS] Update Code of Conduct to Containers variant + * Add test cases to validate remove and list images api. + * images --format compatible with docker + * bash-completions: Add missing subcommands in 'podman system' + * doc: Fix examples for 'podman system service' + * v2 api: /libpod/images/import + * v2 api: /libpod/images/load + * v2 api: /libpod/images/pull + * docs: add workaround for --device with rootless containers (II) + * Fix varlink code generation target. + * Modify Runtime.getImage to return a storage.Image + * Document an aspect of newFromStorage behavior + * Introduce a Runtime.newImage constructor + * Move Image.getLocalImage to Runtime.getLocalImage + * Remove the getLocalImage() call from Image.Size + * Use Runtime.NewFromLocal instead of open-coded copies + * Trivial simplification + * Create two separate newImage instances in Runtime.New + * Call NewImageRuntimeFromStore from NewImageRuntimeFromOptions + * Update readme to 1.8.0 release + * Refactor runtime functions to pass options structure + * build(deps): bump github.com/containers/image/v5 from 5.2.0 to 5.2.1 + * LibpodAPI.BuildImage: don't require a name for the new image + * Bump to v1.8.1-dev + * Cirrus: Never run prune on other branches + * Add dockerfile to mirror fedora-minimal + * Add /swagger/ endpoint to serve swagger yaml to clients + * Add backend code for pod network options + - Changelog for v1.8.0 (2020-02-06) * [CI:DOCS]update contrib systemd user * [CI:DOCS]fix systemd files for apiv2 diff --git a/cni/87-podman-bridge.conflist b/cni/87-podman-bridge.conflist index 39e79b13c..cd01b97ce 100644 --- a/cni/87-podman-bridge.conflist +++ b/cni/87-podman-bridge.conflist @@ -27,10 +27,6 @@ } }, { - "type": "firewall", - "backend": "iptables" - }, - { "type": "tuning" } ] diff --git a/contrib/build_rpm.sh b/contrib/build_rpm.sh index b162a9c88..e6acbdb15 100755 --- a/contrib/build_rpm.sh +++ b/contrib/build_rpm.sh @@ -48,7 +48,7 @@ fi # btrfs-progs-devel is not available in CentOS/RHEL-8 if ! (grep -i 'Red Hat\|CentOS' /etc/redhat-release | grep " 8" ); then - PKGS+=(golang-github-cpuguy83-go-md2man \ + PKGS+=(golang-github-cpuguy83-md2man \ btrfs-progs-devel \ ) fi diff --git a/hack/get_ci_vm.sh b/hack/get_ci_vm.sh index 22f902e2d..768137213 100755 --- a/hack/get_ci_vm.sh +++ b/hack/get_ci_vm.sh @@ -97,7 +97,7 @@ keys=[k for k in env if "ENCRYPTED" not in str(env[k])] for k,v in env.items(): v=str(v) if "ENCRYPTED" not in v: - print "{0}=\"{1}\"".format(k, v), + print("{0}=\"{1}\"".format(k, v)), ' } diff --git a/libpod/common_test.go b/libpod/common_test.go index 83b162c8a..63ea4f41b 100644 --- a/libpod/common_test.go +++ b/libpod/common_test.go @@ -23,7 +23,6 @@ func getTestContainer(id, name string, manager lock.Manager) (*Container, error) Name: name, RootfsImageID: id, RootfsImageName: "testimg", - ImageVolumes: true, StaticDir: "/does/not/exist/", LogPath: "/does/not/exist/", Stdin: true, diff --git a/libpod/container.go b/libpod/container.go index 5e5c8ab26..dbd15e55f 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -249,8 +249,6 @@ type ContainerConfig struct { RootfsImageName string `json:"rootfsImageName,omitempty"` // Rootfs to use for the container, this conflicts with RootfsImageID Rootfs string `json:"rootfs,omitempty"` - // Whether to mount volumes specified in the image. - ImageVolumes bool `json:"imageVolumes"` // Src path to be mounted on /dev/shm in container. ShmDir string `json:"ShmDir,omitempty"` // Size of the container's SHM. @@ -510,12 +508,6 @@ func (c *Container) Image() (string, string) { return c.config.RootfsImageID, c.config.RootfsImageName } -// ImageVolumes returns whether the container is configured to create -// persistent volumes requested by the image -func (c *Container) ImageVolumes() bool { - return c.config.ImageVolumes -} - // ShmDir returns the sources path to be mounted on /dev/shm in container func (c *Container) ShmDir() string { return c.config.ShmDir diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 216bbe669..11f9721dc 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -914,6 +914,7 @@ func (c *Container) checkDependenciesRunning() ([]string, error) { } func (c *Container) completeNetworkSetup() error { + var outResolvConf []string netDisabled, err := c.NetworkDisabled() if err != nil { return err @@ -927,7 +928,37 @@ func (c *Container) completeNetworkSetup() error { if c.config.NetMode == "slirp4netns" { return c.runtime.setupRootlessNetNS(c) } - return c.runtime.setupNetNS(c) + if err := c.runtime.setupNetNS(c); err != nil { + return err + } + state := c.state + // collect any dns servers that cni tells us to use (dnsname) + for _, cni := range state.NetworkStatus { + if cni.DNS.Nameservers != nil { + for _, server := range cni.DNS.Nameservers { + outResolvConf = append(outResolvConf, fmt.Sprintf("nameserver %s", server)) + } + } + } + // check if we have a bindmount for resolv.conf + resolvBindMount := state.BindMounts["/etc/resolv.conf"] + if len(outResolvConf) < 1 || resolvBindMount == "" || len(c.config.NetNsCtr) > 0 { + return nil + } + // read the existing resolv.conf + b, err := ioutil.ReadFile(resolvBindMount) + if err != nil { + return err + } + for _, line := range strings.Split(string(b), "\n") { + // only keep things that dont start with nameserver from the old + // resolv.conf file + if !strings.HasPrefix(line, "nameserver") { + outResolvConf = append([]string{line}, outResolvConf...) + } + } + // write and return + return ioutil.WriteFile(resolvBindMount, []byte(strings.Join(outResolvConf, "\n")), 0644) } // Initialize a container, creating it in the runtime diff --git a/libpod/image/filters.go b/libpod/image/filters.go index 7c7394930..c54ca6333 100644 --- a/libpod/image/filters.go +++ b/libpod/image/filters.go @@ -102,6 +102,13 @@ func ReferenceFilter(ctx context.Context, referenceFilter string) ResultFilter { } } +// IdFilter allows you to filter by image Id +func IdFilter(idFilter string) ResultFilter { + return func(i *Image) bool { + return i.ID() == idFilter + } +} + // OutputImageFilter allows you to filter by an a specific image name func OutputImageFilter(userImage *Image) ResultFilter { return func(i *Image) bool { @@ -165,6 +172,8 @@ func (ir *Runtime) createFilterFuncs(filters []string, img *Image) ([]ResultFilt case "reference": referenceFilter := strings.Join(splitFilter[1:], "=") filterFuncs = append(filterFuncs, ReferenceFilter(ctx, referenceFilter)) + case "id": + filterFuncs = append(filterFuncs, IdFilter(splitFilter[1])) default: return nil, errors.Errorf("invalid filter %s ", splitFilter[0]) } diff --git a/libpod/logs/log.go b/libpod/logs/log.go index 9a7bcb5be..bd918abae 100644 --- a/libpod/logs/log.go +++ b/libpod/logs/log.go @@ -2,13 +2,16 @@ package logs import ( "fmt" - "io/ioutil" + "io" + "os" "strings" "sync" "time" + "github.com/containers/libpod/libpod/logs/reversereader" "github.com/hpcloud/tail" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) const ( @@ -74,43 +77,84 @@ func GetLogFile(path string, options *LogOptions) (*tail.Tail, []*LogLine, error func getTailLog(path string, tail int) ([]*LogLine, error) { var ( - tailLog []*LogLine - nlls []*LogLine - tailCounter int - partial string + nlls []*LogLine + nllCounter int + leftover string + partial string + tailLog []*LogLine ) - content, err := ioutil.ReadFile(path) + f, err := os.Open(path) if err != nil { return nil, err } - splitContent := strings.Split(string(content), "\n") - // We read the content in reverse and add each nll until we have the same - // number of F type messages as the desired tail - for i := len(splitContent) - 1; i >= 0; i-- { - if len(splitContent[i]) == 0 { - continue - } - nll, err := NewLogLine(splitContent[i]) - if err != nil { - return nil, err + rr, err := reversereader.NewReverseReader(f) + if err != nil { + return nil, err + } + + inputs := make(chan []string) + go func() { + for { + s, err := rr.Read() + if err != nil { + if errors.Cause(err) == io.EOF { + inputs <- []string{leftover} + close(inputs) + break + } + logrus.Error(err) + close(inputs) + } + line := strings.Split(s+leftover, "\n") + if len(line) > 1 { + inputs <- line[1:] + } + leftover = line[0] } - nlls = append(nlls, nll) - if !nll.Partial() { - tailCounter++ + }() + + for i := range inputs { + // the incoming array is FIFO; we want FIFO so + // reverse the slice read order + for j := len(i) - 1; j >= 0; j-- { + // lines that are "" are junk + if len(i[j]) < 1 { + continue + } + // read the content in reverse and add each nll until we have the same + // number of F type messages as the desired tail + nll, err := NewLogLine(i[j]) + if err != nil { + return nil, err + } + nlls = append(nlls, nll) + if !nll.Partial() { + nllCounter++ + } } - if tailCounter == tail { + // if we have enough loglines, we can hangup + if nllCounter >= tail { + if err := f.Close(); err != nil { + logrus.Error(err) + } break } } - // Now we iterate the results and assemble partial messages to become full messages + + // re-assemble the log lines and trim (if needed) to the + // tail length for _, nll := range nlls { if nll.Partial() { partial += nll.Msg } else { nll.Msg += partial - tailLog = append(tailLog, nll) + // prepend because we need to reverse the order again to FIFO + tailLog = append([]*LogLine{nll}, tailLog...) partial = "" } + if len(tailLog) == tail { + break + } } return tailLog, nil } diff --git a/libpod/logs/reversereader/reversereader.go b/libpod/logs/reversereader/reversereader.go new file mode 100644 index 000000000..72d9ad975 --- /dev/null +++ b/libpod/logs/reversereader/reversereader.go @@ -0,0 +1,66 @@ +package reversereader + +import ( + "io" + "os" + + "github.com/pkg/errors" +) + +// ReverseReader structure for reading a file backwards +type ReverseReader struct { + reader *os.File + offset int64 + readSize int64 +} + +// NewReverseReader returns a reader that reads from the end of a file +// rather than the beginning. It sets the readsize to pagesize and determines +// the first offset using using modulus. +func NewReverseReader(reader *os.File) (*ReverseReader, error) { + // pagesize should be safe for memory use and file reads should be on page + // boundaries as well + pageSize := int64(os.Getpagesize()) + stat, err := reader.Stat() + if err != nil { + return nil, err + } + // figure out the last page boundary + remainder := stat.Size() % pageSize + end, err := reader.Seek(0, 2) + if err != nil { + return nil, err + } + // set offset (starting position) to the last page boundary or + // zero if fits in one page + startOffset := end - remainder + if startOffset < 0 { + startOffset = 0 + } + rr := ReverseReader{ + reader: reader, + offset: startOffset, + readSize: pageSize, + } + return &rr, nil +} + +// ReverseReader reads from a given offset to the previous offset and +// then sets the newoff set one pagesize less than the previous read. +func (r *ReverseReader) Read() (string, error) { + if r.offset < 0 { + return "", errors.Wrap(io.EOF, "at beginning of file") + } + // Read from given offset + b := make([]byte, r.readSize) + n, err := r.reader.ReadAt(b, r.offset) + if err != nil && errors.Cause(err) != io.EOF { + return "", err + } + if int64(n) < r.readSize { + b = b[0:n] + } + // Set to the next page boundary + r.offset = -r.readSize + return string(b), nil +} diff --git a/libpod/options.go b/libpod/options.go index 1fd588867..d01e8a85f 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -593,7 +593,7 @@ func WithUser(user string) CtrCreateOption { // other configuration from the image will be added to the config. // TODO: Replace image name and ID with a libpod.Image struct when that is // finished. -func WithRootFSFromImage(imageID string, imageName string, useImageVolumes bool) CtrCreateOption { +func WithRootFSFromImage(imageID string, imageName string) CtrCreateOption { return func(ctr *Container) error { if ctr.valid { return define.ErrCtrFinalized @@ -608,7 +608,6 @@ func WithRootFSFromImage(imageID string, imageName string, useImageVolumes bool) ctr.config.RootfsImageID = imageID ctr.config.RootfsImageName = imageName - ctr.config.ImageVolumes = useImageVolumes return nil } diff --git a/libpod/runtime_pod_infra_linux.go b/libpod/runtime_pod_infra_linux.go index a6cac2b72..da46f03e8 100644 --- a/libpod/runtime_pod_infra_linux.go +++ b/libpod/runtime_pod_infra_linux.go @@ -127,7 +127,7 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, imgID containerName := p.ID()[:IDTruncLength] + "-infra" options = append(options, r.WithPod(p)) - options = append(options, WithRootFSFromImage(imgID, imgName, false)) + options = append(options, WithRootFSFromImage(imgID, imgName)) options = append(options, WithName(containerName)) options = append(options, withIsInfra()) diff --git a/libpod/util_test.go b/libpod/util_test.go index 70e989e1a..227686c2b 100644 --- a/libpod/util_test.go +++ b/libpod/util_test.go @@ -1,8 +1,9 @@ package libpod import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestRemoveScientificNotationFromFloat(t *testing.T) { diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index 170b2e24e..ab4255f89 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -338,7 +338,11 @@ func (r *LocalRuntime) Log(c *cliconfig.LogsValues, options *logs.LogOptions) er if tailLen < 0 { tailLen = 0 } - logChannel := make(chan *logs.LogLine, tailLen*len(c.InputArgs)+1) + numContainers := len(c.InputArgs) + if numContainers == 0 { + numContainers = 1 + } + logChannel := make(chan *logs.LogLine, tailLen*numContainers+1) containers, err := shortcuts.GetContainersByContext(false, c.Latest, c.InputArgs, r.Runtime) if err != nil { return err diff --git a/pkg/adapter/network.go b/pkg/adapter/network.go index b25f54a13..c5bd91534 100644 --- a/pkg/adapter/network.go +++ b/pkg/adapter/network.go @@ -209,7 +209,6 @@ func (r *LocalRuntime) NetworkCreateBridge(cli *cliconfig.NetworkCreateValues) ( bridge := network.NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, ipamConfig) plugins = append(plugins, bridge) plugins = append(plugins, network.NewPortMapPlugin()) - plugins = append(plugins, network.NewFirewallPlugin()) // if we find the dnsname plugin, we add configuration for it if network.HasDNSNamePlugin(runtimeConfig.CNIPluginDir) && !cli.DisableDNS { // Note: in the future we might like to allow for dynamic domain names diff --git a/pkg/api/handlers/decoder.go b/pkg/api/handlers/decoder.go index 890d77ecc..03b86275d 100644 --- a/pkg/api/handlers/decoder.go +++ b/pkg/api/handlers/decoder.go @@ -3,8 +3,10 @@ package handlers import ( "encoding/json" "reflect" + "syscall" "time" + "github.com/containers/libpod/pkg/util" "github.com/gorilla/schema" "github.com/sirupsen/logrus" ) @@ -17,6 +19,9 @@ func NewAPIDecoder() *schema.Decoder { d.IgnoreUnknownKeys(true) d.RegisterConverter(map[string][]string{}, convertUrlValuesString) d.RegisterConverter(time.Time{}, convertTimeString) + + var Signal syscall.Signal + d.RegisterConverter(Signal, convertSignal) return d } @@ -89,3 +94,11 @@ func convertTimeString(query string) reflect.Value { func ParseDateTime(query string) time.Time { return convertTimeString(query).Interface().(time.Time) } + +func convertSignal(query string) reflect.Value { + signal, err := util.ParseSignal(query) + if err != nil { + logrus.Infof("convertSignal: Failed to parse %s: %s", query, err.Error()) + } + return reflect.ValueOf(signal) +} diff --git a/pkg/api/handlers/generic/containers_stats.go b/pkg/api/handlers/generic/containers_stats.go index 19e2cc882..977979741 100644 --- a/pkg/api/handlers/generic/containers_stats.go +++ b/pkg/api/handlers/generic/containers_stats.go @@ -7,7 +7,6 @@ import ( "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/containers/libpod/pkg/cgroups" docker "github.com/docker/docker/api/types" @@ -58,17 +57,18 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { } var preRead time.Time - var preCPUStats docker.CPUStats + var preCPUStats CPUStats if query.Stream { preRead = time.Now() systemUsage, _ := cgroups.GetSystemCPUUsage() - preCPUStats = docker.CPUStats{ + preCPUStats = CPUStats{ CPUUsage: docker.CPUUsage{ TotalUsage: stats.CPUNano, PercpuUsage: stats.PerCPU, UsageInKernelmode: stats.CPUSystemNano, UsageInUsermode: stats.CPUNano - stats.CPUSystemNano, }, + CPU: stats.CPU, SystemUsage: systemUsage, OnlineCPUs: 0, ThrottlingData: docker.ThrottlingData{}, @@ -124,9 +124,8 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { } systemUsage, _ := cgroups.GetSystemCPUUsage() - - s := handlers.Stats{StatsJSON: docker.StatsJSON{ - Stats: docker.Stats{ + s := StatsJSON{ + Stats: Stats{ Read: time.Now(), PreRead: preRead, PidsStats: docker.PidsStats{ @@ -143,13 +142,14 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { IoTimeRecursive: nil, SectorsRecursive: nil, }, - CPUStats: docker.CPUStats{ + CPUStats: CPUStats{ CPUUsage: docker.CPUUsage{ TotalUsage: cgroupStat.CPU.Usage.Total, PercpuUsage: cgroupStat.CPU.Usage.PerCPU, UsageInKernelmode: cgroupStat.CPU.Usage.Kernel, UsageInUsermode: cgroupStat.CPU.Usage.Total - cgroupStat.CPU.Usage.Kernel, }, + CPU: stats.CPU, SystemUsage: systemUsage, OnlineCPUs: uint32(len(cgroupStat.CPU.Usage.PerCPU)), ThrottlingData: docker.ThrottlingData{ @@ -173,7 +173,7 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { Name: stats.Name, ID: stats.ContainerID, Networks: net, - }} + } utils.WriteJSON(w, http.StatusOK, s) if flusher, ok := w.(http.Flusher); ok { diff --git a/pkg/api/handlers/generic/types.go b/pkg/api/handlers/generic/types.go new file mode 100644 index 000000000..f068ac011 --- /dev/null +++ b/pkg/api/handlers/generic/types.go @@ -0,0 +1,55 @@ +package generic + +import ( + "time" + + docker "github.com/docker/docker/api/types" +) + +// CPUStats aggregates and wraps all CPU related info of container +type CPUStats struct { + // CPU Usage. Linux and Windows. + CPUUsage docker.CPUUsage `json:"cpu_usage"` + + // System Usage. Linux only. + SystemUsage uint64 `json:"system_cpu_usage,omitempty"` + + // Online CPUs. Linux only. + OnlineCPUs uint32 `json:"online_cpus,omitempty"` + + // Usage of CPU in %. Linux only. + CPU float64 `json:"cpu"` + + // Throttling Data. Linux only. + ThrottlingData docker.ThrottlingData `json:"throttling_data,omitempty"` +} + +// Stats is Ultimate struct aggregating all types of stats of one container +type Stats struct { + // Common stats + Read time.Time `json:"read"` + PreRead time.Time `json:"preread"` + + // Linux specific stats, not populated on Windows. + PidsStats docker.PidsStats `json:"pids_stats,omitempty"` + BlkioStats docker.BlkioStats `json:"blkio_stats,omitempty"` + + // Windows specific stats, not populated on Linux. + NumProcs uint32 `json:"num_procs"` + StorageStats docker.StorageStats `json:"storage_stats,omitempty"` + + // Shared stats + CPUStats CPUStats `json:"cpu_stats,omitempty"` + PreCPUStats CPUStats `json:"precpu_stats,omitempty"` // "Pre"="Previous" + MemoryStats docker.MemoryStats `json:"memory_stats,omitempty"` +} + +type StatsJSON struct { + Stats + + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + + // Networks request version >=1.21 + Networks map[string]docker.NetworkStats `json:"networks,omitempty"` +} diff --git a/pkg/api/handlers/images.go b/pkg/api/handlers/images.go index e4e394d68..96bcbdc96 100644 --- a/pkg/api/handlers/images.go +++ b/pkg/api/handlers/images.go @@ -156,6 +156,7 @@ func SearchImages(w http.ResponseWriter, r *http.Request) { results, err := image.SearchImages(query.Term, options) if err != nil { utils.BadRequest(w, "term", query.Term, err) + return } utils.WriteResponse(w, http.StatusOK, results) } diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index eac0e4dad..71603e6cc 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "strconv" + "strings" "github.com/containers/image/v5/docker" "github.com/containers/image/v5/docker/reference" @@ -133,11 +134,16 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { var libpodFilters = []string{} if _, found := r.URL.Query()["filters"]; found { - all, err = strconv.ParseBool(query.Filters["all"][0]) - if err != nil { - utils.InternalServerError(w, err) - return + dangling := query.Filters["all"] + if len(dangling) > 0 { + all, err = strconv.ParseBool(query.Filters["all"][0]) + if err != nil { + utils.InternalServerError(w, err) + return + } } + // dangling is special and not implemented in the libpod side of things + delete(query.Filters, "dangling") for k, v := range query.Filters { libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) } @@ -157,7 +163,7 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { Compress bool `schema:"compress"` Format string `schema:"format"` }{ - // override any golang type defaults + Format: "docker-archive", } if err := decoder.Decode(&query, r.URL.Query()); err != nil { @@ -166,11 +172,6 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { return } - if len(query.Format) < 1 { - utils.InternalServerError(w, errors.New("format parameter cannot be empty.")) - return - } - tmpfile, err := ioutil.TempFile("", "api.tar") if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) @@ -186,6 +187,7 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { utils.ImageNotFound(w, name, err) return } + if err := newImage.Save(r.Context(), name, query.Format, tmpfile.Name(), []string{}, false, query.Compress); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err) return @@ -234,8 +236,20 @@ func ImagesLoad(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to load image")) return } - - utils.WriteResponse(w, http.StatusOK, []handlers.LibpodImagesLoadReport{{ID: loadedImage}}) + split := strings.Split(loadedImage, ",") + newImage, err := runtime.ImageRuntime().NewFromLocal(split[0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + // TODO this should go into libpod proper at some point. + if len(query.Reference) > 0 { + if err := newImage.TagImage(query.Reference); err != nil { + utils.InternalServerError(w, err) + return + } + } + utils.WriteResponse(w, http.StatusOK, handlers.LibpodImagesLoadReport{ID: loadedImage}) } func ImagesImport(w http.ResponseWriter, r *http.Request) { @@ -275,7 +289,6 @@ func ImagesImport(w http.ResponseWriter, r *http.Request) { tmpfile.Close() source = tmpfile.Name() } - importedImage, err := runtime.Import(context.Background(), source, query.Reference, query.Changes, query.Message, true) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to import image")) diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 6268028f5..c72b0f817 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -78,10 +78,6 @@ type Container struct { docker.ContainerCreateConfig } -type ContainerStats struct { - docker.ContainerStats -} - type Version struct { docker.Version } @@ -143,10 +139,6 @@ type IDResponse struct { ID string `json:"id"` } -type Stats struct { - docker.StatsJSON -} - type ContainerTopOKBody struct { dockerContainer.ContainerTopOKBody } diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 6007a2d00..6a2ba4b1e 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -550,7 +550,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { libpod endpoints */ - // swagger:operation POST /containers/create libpod libpodContainerCreate + // swagger:operation POST /libpod/containers/create libpod libpodCreateContainer // --- // summary: Create a container // tags: @@ -615,21 +615,21 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // name: filters // type: string // description: | - // Returns a list of containers. - // - ancestor=(<image-name>[:<tag>], <image id>, or <image@digest>) - // - before=(<container id> or <container name>) - // - expose=(<port>[/<proto>]|<startport-endport>/[<proto>]) - // - exited=<int> containers with exit code of <int> - // - health=(starting|healthy|unhealthy|none) - // - id=<ID> a container's ID - // - is-task=(true|false) - // - label=key or label="key=value" of a container label - // - name=<name> a container's name - // - network=(<network id> or <network name>) - // - publish=(<port>[/<proto>]|<startport-endport>/[<proto>]) - // - since=(<container id> or <container name>) - // - status=(created|restarting|running|removing|paused|exited|dead) - // - volume=(<volume name> or <mount point destination>) + // A JSON encoded value of the filters (a `map[string][]string`) to process on the containers list. Available filters: + // - `ancestor`=(`<image-name>[:<tag>]`, `<image id>`, or `<image@digest>`) + // - `before`=(`<container id>` or `<container name>`) + // - `expose`=(`<port>[/<proto>]` or `<startport-endport>/[<proto>]`) + // - `exited=<int>` containers with exit code of `<int>` + // - `health`=(`starting`, `healthy`, `unhealthy` or `none`) + // - `id=<ID>` a container's ID + // - `is-task`=(`true` or `false`) + // - `label`=(`key` or `"key=value"`) of an container label + // - `name=<name>` a container's name + // - `network`=(`<network id>` or `<network name>`) + // - `publish`=(`<port>[/<proto>]` or `<startport-endport>/[<proto>]`) + // - `since`=(`<container id>` or `<container name>`) + // - `status`=(`created`, `restarting`, `running`, `removing`, `paused`, `exited` or `dead`) + // - `volume`=(`<volume name>` or `<mount point destination>`) // produces: // - application/json // responses: @@ -662,7 +662,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/prune"), s.APIHandler(handlers.PruneContainers)).Methods(http.MethodPost) - // swagger:operation GET /libpod/containers/showmounted libpod showMountedContainers + // swagger:operation GET /libpod/containers/showmounted libpod libpodShowMountedContainers // --- // tags: // - containers @@ -769,7 +769,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/kill"), s.APIHandler(libpod.KillContainer)).Methods(http.MethodPost) - // swagger:operation POST /libpod/containers/{name}/mount libpod mountContainer + // swagger:operation POST /libpod/containers/{name}/mount libpod libpodMountContainer // --- // tags: // - containers @@ -1047,7 +1047,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/wait"), s.APIHandler(libpod.WaitContainer)).Methods(http.MethodPost) - // swagger:operation GET /libpod/containers/{name}/exists libpod containerExists + // swagger:operation GET /libpod/containers/{name}/exists libpod libpodContainerExists // --- // tags: // - containers @@ -1096,7 +1096,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/stop"), s.APIHandler(handlers.StopContainer)).Methods(http.MethodPost) - // swagger:operation POST /libpod/containers/{name}/attach libpod libpodAttach + // swagger:operation POST /libpod/containers/{name}/attach libpod libpodAttachContainer // --- // tags: // - containers @@ -1151,7 +1151,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/attach"), s.APIHandler(handlers.AttachContainer)).Methods(http.MethodPost) - // swagger:operation POST /libpod/containers/{name}/resize libpod libpodResize + // swagger:operation POST /libpod/containers/{name}/resize libpod libpodResizeContainer // --- // tags: // - containers diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 4c8f05385..db04ecdc9 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -648,6 +648,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - `dangling=true` // - `label=key` or `label="key=value"` of an image label // - `reference`=(`<image-name>[:<tag>]`) + // - `id`=(`<image-id>`) // - `since`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`) // type: string // produces: diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index 75f1fc6a5..ba5f9c3aa 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -206,7 +206,7 @@ func unixClient(_url *url.URL) (*http.Client, error) { } // DoRequest assembles the http request and returns the response -func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, queryParams map[string]string, pathValues ...string) (*APIResponse, error) { +func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, queryParams url.Values, pathValues ...string) (*APIResponse, error) { var ( err error response *http.Response @@ -225,12 +225,7 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, return nil, err } if len(queryParams) > 0 { - // if more desirable we could use url to form the encoded endpoint with params - r := req.URL.Query() - for k, v := range queryParams { - r.Add(k, v) - } - req.URL.RawQuery = r.Encode() + req.URL.RawQuery = queryParams.Encode() } // Give the Do three chances in the case of a comm/service hiccup for i := 0; i < 3; i++ { diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index a437e9a9b..2985787a6 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -3,6 +3,7 @@ package containers import ( "context" "net/http" + "net/url" "strconv" "github.com/containers/libpod/libpod" @@ -21,28 +22,28 @@ func List(ctx context.Context, filters map[string][]string, all *bool, last *int return nil, err } var containers []lpapiv2.ListContainer - params := make(map[string]string) + params := url.Values{} if all != nil { - params["all"] = strconv.FormatBool(*all) + params.Set("all", strconv.FormatBool(*all)) } if last != nil { - params["last"] = strconv.Itoa(*last) + params.Set("last", strconv.Itoa(*last)) } if pod != nil { - params["pod"] = strconv.FormatBool(*pod) + params.Set("pod", strconv.FormatBool(*pod)) } if size != nil { - params["size"] = strconv.FormatBool(*size) + params.Set("size", strconv.FormatBool(*size)) } if sync != nil { - params["sync"] = strconv.FormatBool(*sync) + params.Set("sync", strconv.FormatBool(*sync)) } if filters != nil { filterString, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = filterString + params.Set("filters", filterString) } response, err := conn.DoRequest(nil, http.MethodGet, "/containers/json", params) if err != nil { @@ -63,13 +64,13 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if filters != nil { filterString, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = filterString + params.Set("filters", filterString) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/prune", params) if err != nil { @@ -86,12 +87,12 @@ func Remove(ctx context.Context, nameOrID string, force, volumes *bool) error { if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if force != nil { - params["force"] = strconv.FormatBool(*force) + params.Set("force", strconv.FormatBool(*force)) } if volumes != nil { - params["vols"] = strconv.FormatBool(*volumes) + params.Set("vols", strconv.FormatBool(*volumes)) } response, err := conn.DoRequest(nil, http.MethodDelete, "/containers/%s", params, nameOrID) if err != nil { @@ -109,9 +110,9 @@ func Inspect(ctx context.Context, nameOrID string, size *bool) (*libpod.InspectC if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if size != nil { - params["size"] = strconv.FormatBool(*size) + params.Set("size", strconv.FormatBool(*size)) } response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/json", params, nameOrID) if err != nil { @@ -129,8 +130,8 @@ func Kill(ctx context.Context, nameOrID string, signal string) error { if err != nil { return err } - params := make(map[string]string) - params["signal"] = signal + params := url.Values{} + params.Set("signal", signal) response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/kill", params, nameOrID) if err != nil { return err @@ -162,9 +163,9 @@ func Restart(ctx context.Context, nameOrID string, timeout *int) error { if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if timeout != nil { - params["t"] = strconv.Itoa(*timeout) + params.Set("t", strconv.Itoa(*timeout)) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/restart", params, nameOrID) if err != nil { @@ -181,9 +182,9 @@ func Start(ctx context.Context, nameOrID string, detachKeys *string) error { if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if detachKeys != nil { - params["detachKeys"] = *detachKeys + params.Set("detachKeys", *detachKeys) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/start", params, nameOrID) if err != nil { @@ -242,13 +243,13 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { // Stop stops a running container. The timeout is optional. The nameOrID can be a container name // or a partial/full ID func Stop(ctx context.Context, nameOrID string, timeout *int) error { - params := make(map[string]string) + params := url.Values{} conn, err := bindings.GetClient(ctx) if err != nil { return err } if timeout != nil { - params["t"] = strconv.Itoa(*timeout) + params.Set("t", strconv.Itoa(*timeout)) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/stop", params, nameOrID) if err != nil { diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index 271d58952..c84aa4601 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -2,8 +2,10 @@ package images import ( "context" + "errors" "io" "net/http" + "net/url" "strconv" "github.com/containers/libpod/pkg/api/handlers" @@ -33,16 +35,16 @@ func List(ctx context.Context, all *bool, filters map[string][]string) ([]*handl if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if all != nil { - params["all"] = strconv.FormatBool(*all) + params.Set("all", strconv.FormatBool(*all)) } if filters != nil { strFilters, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = strFilters + params.Set("filters", strFilters) } response, err := conn.DoRequest(nil, http.MethodGet, "/images/json", params) if err != nil { @@ -58,9 +60,9 @@ func GetImage(ctx context.Context, nameOrID string, size *bool) (*inspect.ImageD if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if size != nil { - params["size"] = strconv.FormatBool(*size) + params.Set("size", strconv.FormatBool(*size)) } inspectedData := inspect.ImageData{} response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/json", params, nameOrID) @@ -88,15 +90,21 @@ func History(ctx context.Context, nameOrID string) ([]*handlers.HistoryResponse, return history, response.Process(&history) } -func Load(ctx context.Context, r io.Reader) error { +func Load(ctx context.Context, r io.Reader, name *string) (string, error) { + var id handlers.IDResponse conn, err := bindings.GetClient(ctx) if err != nil { - return err + return "", err } - // TODO this still needs error handling added - //_, err := http.Post(c.makeEndpoint("/images/loads"), "application/json", r) //nolint - _ = conn - return bindings.ErrNotImplemented + params := url.Values{} + if name != nil { + params.Set("reference", *name) + } + response, err := conn.DoRequest(r, http.MethodPost, "/images/load", params) + if err != nil { + return "", err + } + return id.ID, response.Process(&id) } // Remove deletes an image from local storage. The optional force parameter will forcibly remove @@ -107,9 +115,9 @@ func Remove(ctx context.Context, nameOrID string, force *bool) ([]map[string]str if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if force != nil { - params["force"] = strconv.FormatBool(*force) + params.Set("force", strconv.FormatBool(*force)) } response, err := conn.DoRequest(nil, http.MethodDelete, "/images/%s", params, nameOrID) if err != nil { @@ -125,12 +133,12 @@ func Export(ctx context.Context, nameOrID string, w io.Writer, format *string, c if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if format != nil { - params["format"] = *format + params.Set("format", *format) } if compress != nil { - params["compress"] = strconv.FormatBool(*compress) + params.Set("compress", strconv.FormatBool(*compress)) } response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/get", params, nameOrID) if err != nil { @@ -153,13 +161,13 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if filters != nil { stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = stringFilter + params.Set("filters", stringFilter) } response, err := conn.DoRequest(nil, http.MethodPost, "/images/prune", params) if err != nil { @@ -174,9 +182,9 @@ func Tag(ctx context.Context, nameOrID, tag, repo string) error { if err != nil { return err } - params := make(map[string]string) - params["tag"] = tag - params["repo"] = repo + params := url.Values{} + params.Set("tag", tag) + params.Set("repo", repo) response, err := conn.DoRequest(nil, http.MethodPost, "/images/%s/tag", params, nameOrID) if err != nil { return err @@ -185,3 +193,35 @@ func Tag(ctx context.Context, nameOrID, tag, repo string) error { } func Build(nameOrId string) {} + +// Imports adds the given image to the local image store. This can be done by file and the given reader +// or via the url parameter. Additional metadata can be associated with the image by using the changes and +// message parameters. The image can also be tagged given a reference. One of url OR r must be provided. +func Import(ctx context.Context, changes []string, message, reference, u *string, r io.Reader) (string, error) { + var id handlers.IDResponse + if r != nil && u != nil { + return "", errors.New("url and r parameters cannot be used together") + } + conn, err := bindings.GetClient(ctx) + if err != nil { + return "", err + } + params := url.Values{} + for _, change := range changes { + params.Add("changes", change) + } + if message != nil { + params.Set("message", *message) + } + if reference != nil { + params.Set("reference", *reference) + } + if u != nil { + params.Set("url", *u) + } + response, err := conn.DoRequest(r, http.MethodPost, "/images/import", params) + if err != nil { + return "", err + } + return id.ID, response.Process(&id) +} diff --git a/pkg/bindings/images/search.go b/pkg/bindings/images/search.go index dca1b0e63..183ff3d77 100644 --- a/pkg/bindings/images/search.go +++ b/pkg/bindings/images/search.go @@ -3,6 +3,7 @@ package images import ( "context" "net/http" + "net/url" "strconv" "github.com/containers/libpod/libpod/image" @@ -20,17 +21,17 @@ func Search(ctx context.Context, term string, limit *int, filters map[string][]s if err != nil { return nil, err } - params := make(map[string]string) - params["term"] = term + params := url.Values{} + params.Set("term", term) if limit != nil { - params["limit"] = strconv.Itoa(*limit) + params.Set("limit", strconv.Itoa(*limit)) } if filters != nil { stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = stringFilter + params.Set("filters", stringFilter) } response, err := conn.DoRequest(nil, http.MethodGet, "/images/search", params) if err != nil { diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index 838b22e43..1a8c31be1 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -3,6 +3,7 @@ package pods import ( "context" "net/http" + "net/url" "strconv" "github.com/containers/libpod/libpod" @@ -48,9 +49,9 @@ func Kill(ctx context.Context, nameOrID string, signal *string) error { if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if signal != nil { - params["signal"] = *signal + params.Set("signal", *signal) } response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/kill", params, nameOrID) if err != nil { @@ -95,13 +96,13 @@ func List(ctx context.Context, filters map[string][]string) ([]*libpod.PodInspec if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if filters != nil { stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = stringFilter + params.Set("filters", stringFilter) } response, err := conn.DoRequest(nil, http.MethodGet, "/pods/json", params) if err != nil { @@ -130,9 +131,9 @@ func Remove(ctx context.Context, nameOrID string, force *bool) error { if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if force != nil { - params["force"] = strconv.FormatBool(*force) + params.Set("force", strconv.FormatBool(*force)) } response, err := conn.DoRequest(nil, http.MethodDelete, "/pods/%s", params, nameOrID) if err != nil { @@ -166,9 +167,9 @@ func Stop(ctx context.Context, nameOrID string, timeout *int) error { if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if timeout != nil { - params["t"] = strconv.Itoa(*timeout) + params.Set("t", strconv.Itoa(*timeout)) } response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/stop", params, nameOrID) if err != nil { diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index 98d64bbaa..38f5014ca 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -20,9 +20,18 @@ type testImage struct { } const ( + devPodmanBinaryLocation string = "../../../bin/podman" defaultPodmanBinaryLocation string = "/usr/bin/podman" ) +func getPodmanBinary() string { + _, err := os.Stat(devPodmanBinaryLocation) + if os.IsNotExist(err) { + return defaultPodmanBinaryLocation + } + return devPodmanBinaryLocation +} + var ( ImageCacheDir = "/tmp/podman/imagecachedir" LockTmpDir string @@ -50,7 +59,7 @@ type bindingTest struct { func (b *bindingTest) runPodman(command []string) *gexec.Session { var cmd []string - podmanBinary := defaultPodmanBinaryLocation + podmanBinary := getPodmanBinary() val, ok := os.LookupEnv("PODMAN_BINARY") if ok { podmanBinary = val @@ -166,7 +175,7 @@ func (b *bindingTest) restoreImageFromCache(i testImage) { // and add or append the alpine image to it func (b *bindingTest) RunTopContainer(containerName *string, insidePod *bool, podName *string) { cmd := []string{"run", "-dt"} - if *insidePod && podName != nil { + if insidePod != nil && podName != nil { pName := *podName cmd = append(cmd, "--pod", pName) } else if containerName != nil { diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index 0b51c8c9e..c51ce4a32 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -3,6 +3,8 @@ package test_bindings import ( "context" "net/http" + "os" + "path/filepath" "time" "github.com/containers/libpod/pkg/bindings" @@ -71,6 +73,14 @@ var _ = Describe("Podman images", func() { // Inspect by long name _, err = images.GetImage(connText, alpine.name, nil) Expect(err).To(BeNil()) + // TODO it looks like the images API alwaays returns size regardless + // of bool or not. What should we do ? + //Expect(data.Size).To(BeZero()) + + // Enabling the size parameter should result in size being populated + data, err = images.GetImage(connText, alpine.name, &trueFlag) + Expect(err).To(BeNil()) + Expect(data.Size).To(BeNumerically(">", 0)) }) // Test to validate the remove image api @@ -181,4 +191,106 @@ var _ = Describe("Podman images", func() { Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) }) + It("Image Exists", func() { + // exists on bogus image should be false, with no error + exists, err := images.Exists(connText, "foobar") + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + + // exists with shortname should be true + exists, err = images.Exists(connText, alpine.shortName) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + + // exists with fqname should be true + exists, err = images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + }) + + It("Load|Import Image", func() { + // load an image + _, err := images.Remove(connText, alpine.name, nil) + Expect(err).To(BeNil()) + exists, err := images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + f, err := os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) + defer f.Close() + Expect(err).To(BeNil()) + names, err := images.Load(connText, f, nil) + Expect(err).To(BeNil()) + Expect(names).To(Equal(alpine.name)) + exists, err = images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + + // load with a repo name + f, err = os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) + Expect(err).To(BeNil()) + _, err = images.Remove(connText, alpine.name, nil) + Expect(err).To(BeNil()) + exists, err = images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + newName := "quay.io/newname:fizzle" + names, err = images.Load(connText, f, &newName) + Expect(err).To(BeNil()) + Expect(names).To(Equal(alpine.name)) + exists, err = images.Exists(connText, newName) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + + // load with a bad repo name should trigger a 500 + f, err = os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) + Expect(err).To(BeNil()) + _, err = images.Remove(connText, alpine.name, nil) + Expect(err).To(BeNil()) + exists, err = images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + badName := "quay.io/newName:fizzle" + _, err = images.Load(connText, f, &badName) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("Export Image", func() { + // Export an image + exportPath := filepath.Join(bt.tempDirPath, alpine.tarballName) + w, err := os.Create(filepath.Join(bt.tempDirPath, alpine.tarballName)) + defer w.Close() + Expect(err).To(BeNil()) + err = images.Export(connText, alpine.name, w, nil, nil) + Expect(err).To(BeNil()) + _, err = os.Stat(exportPath) + Expect(err).To(BeNil()) + + // TODO how do we verify that a format change worked? + }) + + It("Import Image", func() { + // load an image + _, err = images.Remove(connText, alpine.name, nil) + Expect(err).To(BeNil()) + exists, err := images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + f, err := os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) + defer f.Close() + Expect(err).To(BeNil()) + changes := []string{"CMD /bin/foobar"} + testMessage := "test_import" + _, err = images.Import(connText, changes, &testMessage, &alpine.name, nil, f) + Expect(err).To(BeNil()) + exists, err = images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + data, err := images.GetImage(connText, alpine.name, nil) + Expect(err).To(BeNil()) + Expect(data.Comment).To(Equal(testMessage)) + + }) + }) diff --git a/pkg/bindings/volumes/volumes.go b/pkg/bindings/volumes/volumes.go index 8313a7460..7f6a9cc9b 100644 --- a/pkg/bindings/volumes/volumes.go +++ b/pkg/bindings/volumes/volumes.go @@ -3,6 +3,7 @@ package volumes import ( "context" "net/http" + "net/url" "strconv" "github.com/containers/libpod/libpod" @@ -73,9 +74,9 @@ func Remove(ctx context.Context, nameOrID string, force *bool) error { if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if force != nil { - params["force"] = strconv.FormatBool(*force) + params.Set("force", strconv.FormatBool(*force)) } response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/%s/prune", params, nameOrID) if err != nil { diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 5011df496..02678a687 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -341,9 +341,8 @@ func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *l } options = append(options, nsOpts...) - useImageVolumes := c.ImageVolumeType == TypeBind // Gather up the options for NewContainer which consist of With... funcs - options = append(options, libpod.WithRootFSFromImage(c.ImageID, c.Image, useImageVolumes)) + options = append(options, libpod.WithRootFSFromImage(c.ImageID, c.Image)) options = append(options, libpod.WithConmonPidFile(c.ConmonPidFile)) options = append(options, libpod.WithLabels(c.Labels)) options = append(options, libpod.WithShmSize(c.Resources.ShmSize)) diff --git a/pkg/specgen/create.go b/pkg/specgen/create.go index c8fee5f05..34f9ffac2 100644 --- a/pkg/specgen/create.go +++ b/pkg/specgen/create.go @@ -36,9 +36,7 @@ func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, er return nil, err } - // TODO mheon wants to talk with Dan about this - useImageVolumes := s.ImageVolumeMode == "bind" - options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image, useImageVolumes)) + options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image)) runtimeSpec, err := s.toOCISpec(rt, newImage) if err != nil { diff --git a/test/e2e/create_staticip_test.go b/test/e2e/create_staticip_test.go index 72a0638f9..693795637 100644 --- a/test/e2e/create_staticip_test.go +++ b/test/e2e/create_staticip_test.go @@ -4,6 +4,7 @@ package integration import ( "os" + "time" . "github.com/containers/libpod/test/utils" . "github.com/onsi/ginkgo" @@ -86,8 +87,23 @@ var _ = Describe("Podman create with --ip flag", func() { result = podmanTest.Podman([]string{"start", "test1"}) result.WaitWithDefaultTimeout() Expect(result.ExitCode()).To(Equal(0)) + + // race prevention: wait until IP address is assigned + for i := 0; i < 5; i++ { + result = podmanTest.Podman([]string{"inspect", "--format", "{{.NetworkSettings.IPAddress}}", "test1"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + if result.OutputToString() != "" { + break + } + time.Sleep(1 * time.Second) + } + Expect(result.OutputToString()).To(Equal(ip)) + + // test1 container is running with the given IP. result = podmanTest.Podman([]string{"start", "test2"}) result.WaitWithDefaultTimeout() Expect(result).To(ExitWithError()) + Expect(result.ErrorToString()).To(ContainSubstring("requested IP address " + ip + " is not available")) }) }) |