summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md12
-rw-r--r--contrib/rootless-cni-infra/Containerfile2
-rwxr-xr-xcontrib/rootless-cni-infra/rootless-cni-infra18
-rw-r--r--libpod/rootless_cni_linux.go2
-rw-r--r--pkg/api/handlers/libpod/images_pull.go9
-rw-r--r--pkg/api/handlers/types.go2
-rw-r--r--pkg/bindings/images/pull.go1
-rw-r--r--pkg/bindings/test/images_test.go8
-rw-r--r--pkg/domain/entities/images.go2
-rw-r--r--test/apiv2/rest_api/test_rest_v2_0_0.py246
10 files changed, 295 insertions, 7 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dc48b389e..ba321921c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,6 +7,7 @@ that we follow.
## Topics
* [Reporting Issues](#reporting-issues)
+* [Working On Issues](#working-on-issues)
* [Contributing to Podman](#contributing-to-podman)
* [Continuous Integration](#continuous-integration) [![Build Status](https://api.cirrus-ci.com/github/containers/podman.svg)](https://cirrus-ci.com/github/containers/podman/master)
* [Submitting Pull Requests](#submitting-pull-requests)
@@ -28,6 +29,17 @@ The easier it is for us to reproduce it, the faster it'll be fixed!
Please don't include any private/sensitive information in your issue!
+## Working On Issues
+
+Once you have decided to contribute to Podman by working on an issue, check our
+backlog of [open issues](https://github.com/containers/podman/issues) looking
+for any that do not have an "In Progress" label attached to it. Often issues
+will be assigned to someone, to be worked on at a later time. If you have the
+time to work on the issue now add yourself as an assignee, and set the
+"In Progress" label if you’re a member of the “Containers” GitHub organization.
+If you can not set the label, just add a quick comment in the issue asking that
+the “In Progress” label be set and a member will do so for you.
+
## Contributing to Podman
This section describes how to start a contribution to Podman.
diff --git a/contrib/rootless-cni-infra/Containerfile b/contrib/rootless-cni-infra/Containerfile
index 5be30ccc9..6bf70d644 100644
--- a/contrib/rootless-cni-infra/Containerfile
+++ b/contrib/rootless-cni-infra/Containerfile
@@ -34,4 +34,4 @@ COPY rootless-cni-infra /usr/local/bin
ENV CNI_PATH=/opt/cni/bin
CMD ["sleep", "infinity"]
-ENV ROOTLESS_CNI_INFRA_VERSION=1
+ENV ROOTLESS_CNI_INFRA_VERSION=2
diff --git a/contrib/rootless-cni-infra/rootless-cni-infra b/contrib/rootless-cni-infra/rootless-cni-infra
index f6622b23c..5cb43621d 100755
--- a/contrib/rootless-cni-infra/rootless-cni-infra
+++ b/contrib/rootless-cni-infra/rootless-cni-infra
@@ -4,6 +4,23 @@ set -eu
ARG0="$0"
BASE="/run/rootless-cni-infra"
+wait_unshare_net() {
+ pid="$1"
+ # NOTE: busybox shell doesn't support the `for ((i=0; i < $MAX; i++)); do foo; done` statement
+ i=0
+ while :; do
+ if [ "$(readlink /proc/self/ns/net)" != "$(readlink /proc/${pid}/ns/net)" ]; then
+ break
+ fi
+ sleep 0.1
+ if [ $i -ge 10 ]; then
+ echo >&2 "/proc/${pid}/ns/net cannot be unshared"
+ exit 1
+ fi
+ i=$((i + 1))
+ done
+}
+
# CLI subcommand: "alloc $CONTAINER_ID $NETWORK_NAME $POD_NAME"
cmd_entrypoint_alloc() {
if [ "$#" -ne 3 ]; then
@@ -24,6 +41,7 @@ cmd_entrypoint_alloc() {
else
unshare -n sleep infinity &
pid="$!"
+ wait_unshare_net "${pid}"
echo "${pid}" >"${dir}/pid"
nsenter -t "${pid}" -n ip link set lo up
fi
diff --git a/libpod/rootless_cni_linux.go b/libpod/rootless_cni_linux.go
index 7feec6b44..2877191e5 100644
--- a/libpod/rootless_cni_linux.go
+++ b/libpod/rootless_cni_linux.go
@@ -25,7 +25,7 @@ import (
// Built from ../contrib/rootless-cni-infra.
var rootlessCNIInfraImage = map[string]string{
- "amd64": "quay.io/libpod/rootless-cni-infra@sha256:8aa681c4c08dee3ec5d46ff592fddd0259a35626717006d6b77ee786b1d02967", // 1-amd64
+ "amd64": "quay.io/libpod/rootless-cni-infra@sha256:e92c3a6367f8e554121b96d39af1f19f0f9ac5a32922b290112e13bc661d3a29", // 2-amd64
}
const (
diff --git a/pkg/api/handlers/libpod/images_pull.go b/pkg/api/handlers/libpod/images_pull.go
index 8a2f4f4cf..ad8d1f38e 100644
--- a/pkg/api/handlers/libpod/images_pull.go
+++ b/pkg/api/handlers/libpod/images_pull.go
@@ -178,10 +178,19 @@ loop: // break out of for/select infinite loop
flush()
case <-runCtx.Done():
if !failed {
+ // Send all image id's pulled in 'images' stanza
report.Images = images
if err := enc.Encode(report); err != nil {
logrus.Warnf("Failed to json encode error %q", err.Error())
}
+
+ report.Images = nil
+ // Pull last ID from list and publish in 'id' stanza. This maintains previous API contract
+ report.ID = images[len(images)-1]
+ if err := enc.Encode(report); err != nil {
+ logrus.Warnf("Failed to json encode error %q", err.Error())
+ }
+
flush()
}
break loop // break out of for/select infinite loop
diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go
index 0ccaa95bb..9e503dbb0 100644
--- a/pkg/api/handlers/types.go
+++ b/pkg/api/handlers/types.go
@@ -33,7 +33,7 @@ type LibpodImagesLoadReport struct {
}
type LibpodImagesPullReport struct {
- ID string `json:"id"`
+ entities.ImagePullReport
}
// LibpodImagesRemoveReport is the return type for image removal via the rest
diff --git a/pkg/bindings/images/pull.go b/pkg/bindings/images/pull.go
index 261a481a2..2bfbbb2ac 100644
--- a/pkg/bindings/images/pull.go
+++ b/pkg/bindings/images/pull.go
@@ -89,6 +89,7 @@ func Pull(ctx context.Context, rawImage string, options entities.ImagePullOption
mErr = multierror.Append(mErr, errors.New(report.Error))
case len(report.Images) > 0:
images = report.Images
+ case report.ID != "":
default:
return images, errors.New("failed to parse pull results stream, unexpected input")
}
diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go
index e0dd28d7a..681855293 100644
--- a/pkg/bindings/test/images_test.go
+++ b/pkg/bindings/test/images_test.go
@@ -360,19 +360,19 @@ var _ = Describe("Podman images", func() {
rawImage := "docker.io/library/busybox:latest"
pulledImages, err := images.Pull(bt.conn, rawImage, entities.ImagePullOptions{})
- Expect(err).To(BeNil())
+ Expect(err).NotTo(HaveOccurred())
Expect(len(pulledImages)).To(Equal(1))
exists, err := images.Exists(bt.conn, rawImage)
- Expect(err).To(BeNil())
+ Expect(err).NotTo(HaveOccurred())
Expect(exists).To(BeTrue())
// Make sure the normalization AND the full-transport reference works.
_, err = images.Pull(bt.conn, "docker://"+rawImage, entities.ImagePullOptions{})
- Expect(err).To(BeNil())
+ Expect(err).NotTo(HaveOccurred())
// The v2 endpoint only supports the docker transport. Let's see if that's really true.
_, err = images.Pull(bt.conn, "bogus-transport:bogus.com/image:reference", entities.ImagePullOptions{})
- Expect(err).To(Not(BeNil()))
+ Expect(err).To(HaveOccurred())
})
})
diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go
index d0b738934..cad6693fa 100644
--- a/pkg/domain/entities/images.go
+++ b/pkg/domain/entities/images.go
@@ -156,6 +156,8 @@ type ImagePullReport struct {
Error string `json:"error,omitempty"`
// Images contains the ID's of the images pulled
Images []string `json:"images,omitempty"`
+ // ID contains image id (retained for backwards compatibility)
+ ID string `json:"id,omitempty"`
}
// ImagePushOptions are the arguments for pushing images.
diff --git a/test/apiv2/rest_api/test_rest_v2_0_0.py b/test/apiv2/rest_api/test_rest_v2_0_0.py
new file mode 100644
index 000000000..3376f8402
--- /dev/null
+++ b/test/apiv2/rest_api/test_rest_v2_0_0.py
@@ -0,0 +1,246 @@
+import json
+import os
+import subprocess
+import sys
+import time
+import unittest
+from multiprocessing import Process
+
+import requests
+from dateutil.parser import parse
+
+PODMAN_URL = "http://localhost:8080"
+
+
+def _url(path):
+ return PODMAN_URL + "/v1.0.0/libpod" + path
+
+
+def podman():
+ binary = os.getenv("PODMAN_BINARY")
+ if binary is None:
+ binary = "bin/podman"
+ return binary
+
+
+def ctnr(path):
+ r = requests.get(_url("/containers/json?all=true"))
+ try:
+ ctnrs = json.loads(r.text)
+ except Exception as e:
+ sys.stderr.write("Bad container response: {}/{}".format(r.text, e))
+ raise e
+ return path.format(ctnrs[0]["Id"])
+
+
+def validateObjectFields(buffer):
+ objs = json.loads(buffer)
+ if not isinstance(objs, dict):
+ for o in objs:
+ _ = o["Id"]
+ else:
+ _ = objs["Id"]
+ return objs
+
+
+class TestApi(unittest.TestCase):
+ podman = None
+
+ def setUp(self):
+ super().setUp()
+ if TestApi.podman.poll() is not None:
+ sys.stderr.write(f"podman service returned {TestApi.podman.returncode}\n")
+ sys.exit(2)
+ requests.get(
+ _url("/images/create?fromSrc=docker.io%2Falpine%3Alatest"))
+ # calling out to podman is easier than the API for running a container
+ subprocess.run([podman(), "run", "alpine", "/bin/ls"],
+ check=True,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ TestApi.podman = subprocess.Popen(
+ [
+ podman(), "system", "service", "tcp:localhost:8080",
+ "--log-level=debug", "--time=0"
+ ],
+ shell=False,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+ time.sleep(2)
+
+ @classmethod
+ def tearDownClass(cls):
+ TestApi.podman.terminate()
+ stdout, stderr = TestApi.podman.communicate(timeout=0.5)
+ if stdout:
+ print("\nService Stdout:\n" + stdout.decode('utf-8'))
+ if stderr:
+ print("\nService Stderr:\n" + stderr.decode('utf-8'))
+
+ if TestApi.podman.returncode > 0:
+ sys.stderr.write(f"podman exited with error code {TestApi.podman.returncode}\n")
+ sys.exit(2)
+
+ return super().tearDownClass()
+
+ def test_info(self):
+ r = requests.get(_url("/info"))
+ self.assertEqual(r.status_code, 200)
+ self.assertIsNotNone(r.content)
+ _ = json.loads(r.text)
+
+ def test_events(self):
+ r = requests.get(_url("/events?stream=false"))
+ self.assertEqual(r.status_code, 200, r.text)
+ self.assertIsNotNone(r.content)
+ for line in r.text.splitlines():
+ obj = json.loads(line)
+ # Actor.ID is uppercase for compatibility
+ _ = obj["Actor"]["ID"]
+
+ def test_containers(self):
+ r = requests.get(_url("/containers/json"), timeout=5)
+ self.assertEqual(r.status_code, 200, r.text)
+ obj = json.loads(r.text)
+ self.assertEqual(len(obj), 0)
+
+ def test_containers_all(self):
+ r = requests.get(_url("/containers/json?all=true"))
+ self.assertEqual(r.status_code, 200, r.text)
+ validateObjectFields(r.text)
+
+ def test_inspect_container(self):
+ r = requests.get(_url(ctnr("/containers/{}/json")))
+ self.assertEqual(r.status_code, 200, r.text)
+ obj = validateObjectFields(r.content)
+ _ = parse(obj["Created"])
+
+ def test_stats(self):
+ r = requests.get(_url(ctnr("/containers/{}/stats?stream=false")))
+ self.assertIn(r.status_code, (200, 409), r.text)
+ if r.status_code == 200:
+ validateObjectFields(r.text)
+
+ def test_delete_containers(self):
+ r = requests.delete(_url(ctnr("/containers/{}")))
+ self.assertEqual(r.status_code, 204, r.text)
+
+ def test_stop_containers(self):
+ r = requests.post(_url(ctnr("/containers/{}/start")))
+ self.assertIn(r.status_code, (204, 304), r.text)
+
+ r = requests.post(_url(ctnr("/containers/{}/stop")))
+ self.assertIn(r.status_code, (204, 304), r.text)
+
+ def test_start_containers(self):
+ r = requests.post(_url(ctnr("/containers/{}/stop")))
+ self.assertIn(r.status_code, (204, 304), r.text)
+
+ r = requests.post(_url(ctnr("/containers/{}/start")))
+ self.assertIn(r.status_code, (204, 304), r.text)
+
+ def test_restart_containers(self):
+ r = requests.post(_url(ctnr("/containers/{}/start")))
+ self.assertIn(r.status_code, (204, 304), r.text)
+
+ r = requests.post(_url(ctnr("/containers/{}/restart")), timeout=5)
+ self.assertEqual(r.status_code, 204, r.text)
+
+ def test_resize(self):
+ r = requests.post(_url(ctnr("/containers/{}/resize?h=43&w=80")))
+ self.assertIn(r.status_code, (200, 409), r.text)
+ if r.status_code == 200:
+ self.assertIsNone(r.text)
+
+ def test_attach_containers(self):
+ r = requests.post(_url(ctnr("/containers/{}/attach")), timeout=5)
+ self.assertIn(r.status_code, (101, 500), r.text)
+
+ def test_logs_containers(self):
+ r = requests.get(_url(ctnr("/containers/{}/logs?stdout=true")))
+ self.assertEqual(r.status_code, 200, r.text)
+
+ def test_post_create(self):
+ self.skipTest("TODO: create request body")
+ r = requests.post(_url("/containers/create?args=True"))
+ self.assertEqual(r.status_code, 200, r.text)
+ json.loads(r.text)
+
+ def test_commit(self):
+ r = requests.post(_url(ctnr("/commit?container={}")))
+ self.assertEqual(r.status_code, 200, r.text)
+ validateObjectFields(r.text)
+
+ def test_images(self):
+ r = requests.get(_url("/images/json"))
+ self.assertEqual(r.status_code, 200, r.text)
+ validateObjectFields(r.content)
+
+ def test_inspect_image(self):
+ r = requests.get(_url("/images/alpine/json"))
+ self.assertEqual(r.status_code, 200, r.text)
+ obj = validateObjectFields(r.content)
+ _ = parse(obj["Created"])
+
+ def test_delete_image(self):
+ r = requests.delete(_url("/images/alpine?force=true"))
+ self.assertEqual(r.status_code, 200, r.text)
+ json.loads(r.text)
+
+ def test_pull(self):
+ r = requests.post(_url("/images/pull?reference=alpine"), timeout=15)
+ self.assertEqual(r.status_code, 200, r.status_code)
+ text = r.text
+ keys = {
+ "error": False,
+ "id": False,
+ "images": False,
+ "stream": False,
+ }
+ # Read and record stanza's from pull
+ for line in str.splitlines(text):
+ obj = json.loads(line)
+ key_list = list(obj.keys())
+ for k in key_list:
+ keys[k] = True
+
+ self.assertFalse(keys["error"], "Expected no errors")
+ self.assertTrue(keys["id"], "Expected to find id stanza")
+ self.assertTrue(keys["images"], "Expected to find images stanza")
+ self.assertTrue(keys["stream"], "Expected to find stream progress stanza's")
+
+ def test_search(self):
+ # Had issues with this test hanging when repositories not happy
+ def do_search():
+ r = requests.get(_url("/images/search?term=alpine"), timeout=5)
+ self.assertEqual(r.status_code, 200, r.text)
+ json.loads(r.text)
+
+ search = Process(target=do_search)
+ search.start()
+ search.join(timeout=10)
+ self.assertFalse(search.is_alive(), "/images/search took too long")
+
+ def test_ping(self):
+ r = requests.get(PODMAN_URL + "/_ping")
+ self.assertEqual(r.status_code, 200, r.text)
+
+ r = requests.head(PODMAN_URL + "/_ping")
+ self.assertEqual(r.status_code, 200, r.text)
+
+ r = requests.get(_url("/_ping"))
+ self.assertEqual(r.status_code, 200, r.text)
+
+ r = requests.get(_url("/_ping"))
+ self.assertEqual(r.status_code, 200, r.text)
+
+
+if __name__ == '__main__':
+ unittest.main()