V2 verify JSON output is consistent and doesn't drift

$ cd test/apiv2
$ python -m unittest -v test_rest_v1_0_0.TestApi

Signed-off-by: Jhon Honce <jhonce@redhat.com>
This commit is contained in:
Jhon Honce
2020-05-27 15:51:24 -07:00
parent e8818ced80
commit 5626c2163b
12 changed files with 253 additions and 12 deletions

View File

@ -90,7 +90,7 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) {
// For Docker compatibility, we need to re-initialize containers in these states. // For Docker compatibility, we need to re-initialize containers in these states.
if state == define.ContainerStateConfigured || state == define.ContainerStateExited { if state == define.ContainerStateConfigured || state == define.ContainerStateExited {
if err := ctr.Init(r.Context()); err != nil { if err := ctr.Init(r.Context()); err != nil {
utils.InternalServerError(w, errors.Wrapf(err, "error preparing container %s for attach", ctr.ID())) utils.Error(w, "Container in wrong state", http.StatusConflict, errors.Wrapf(err, "error preparing container %s for attach", ctr.ID()))
return return
} }
} else if !(state == define.ContainerStateCreated || state == define.ContainerStateRunning) { } else if !(state == define.ContainerStateCreated || state == define.ContainerStateRunning) {

View File

@ -45,8 +45,8 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) {
utils.InternalServerError(w, err) utils.InternalServerError(w, err)
return return
} }
if state != define.ContainerStateRunning && !query.Stream { if state != define.ContainerStateRunning {
utils.InternalServerError(w, define.ErrCtrStateInvalid) utils.Error(w, "Container not running and streaming requested", http.StatusConflict, define.ErrCtrStateInvalid)
return return
} }

View File

@ -1,10 +1,12 @@
package compat package compat
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/api/handlers/utils"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -43,6 +45,14 @@ func ResizeTTY(w http.ResponseWriter, r *http.Request) {
utils.ContainerNotFound(w, name, err) utils.ContainerNotFound(w, name, err)
return return
} }
if state, err := ctnr.State(); err != nil {
utils.InternalServerError(w, errors.Wrapf(err, "cannot obtain container state"))
return
} else if state != define.ContainerStateRunning {
utils.Error(w, "Container not running", http.StatusConflict,
fmt.Errorf("container %q in wrong state %q", name, state.String()))
return
}
if err := ctnr.AttachResize(sz); err != nil { if err := ctnr.AttachResize(sz); err != nil {
utils.InternalServerError(w, errors.Wrapf(err, "cannot resize container")) utils.InternalServerError(w, errors.Wrapf(err, "cannot resize container"))
return return
@ -56,6 +66,14 @@ func ResizeTTY(w http.ResponseWriter, r *http.Request) {
utils.SessionNotFound(w, name, err) utils.SessionNotFound(w, name, err)
return return
} }
if state, err := ctnr.State(); err != nil {
utils.InternalServerError(w, errors.Wrapf(err, "cannot obtain session container state"))
return
} else if state != define.ContainerStateRunning {
utils.Error(w, "Container not running", http.StatusConflict,
fmt.Errorf("container %q in wrong state %q", name, state.String()))
return
}
if err := ctnr.ExecResize(name, sz); err != nil { if err := ctnr.ExecResize(name, sz); err != nil {
utils.InternalServerError(w, errors.Wrapf(err, "cannot resize session")) utils.InternalServerError(w, errors.Wrapf(err, "cannot resize session"))
return return

View File

@ -48,7 +48,7 @@ type StatsJSON struct {
Stats Stats
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
ID string `json:"id,omitempty"` ID string `json:"Id,omitempty"`
// Networks request version >=1.21 // Networks request version >=1.21
Networks map[string]docker.NetworkStats `json:"networks,omitempty"` Networks map[string]docker.NetworkStats `json:"networks,omitempty"`

View File

@ -66,6 +66,10 @@ func ListContainers(w http.ResponseWriter, r *http.Request) {
utils.InternalServerError(w, err) utils.InternalServerError(w, err)
return return
} }
if len(pss) == 0 {
utils.WriteResponse(w, http.StatusOK, "[]")
return
}
utils.WriteResponse(w, http.StatusOK, pss) utils.WriteResponse(w, http.StatusOK, pss)
} }

View File

@ -120,7 +120,7 @@ type CreateContainerConfig struct {
// swagger:model IDResponse // swagger:model IDResponse
type IDResponse struct { type IDResponse struct {
// ID // ID
ID string `json:"id"` ID string `json:"Id"`
} }
type ContainerTopOKBody struct { type ContainerTopOKBody struct {

View File

@ -50,7 +50,7 @@ func (i *Image) Id() string {
} }
type ImageSummary struct { type ImageSummary struct {
ID string ID string `json:"Id"`
ParentId string `json:",omitempty"` ParentId string `json:",omitempty"`
RepoTags []string `json:",omitempty"` RepoTags []string `json:",omitempty"`
Created time.Time `json:",omitempty"` Created time.Time `json:",omitempty"`

View File

@ -7,15 +7,15 @@
podman pull -q $IMAGE podman pull -q $IMAGE
t GET libpod/images/json 200 \ t GET libpod/images/json 200 \
.[0].ID~[0-9a-f]\\{64\\} .[0].Id~[0-9a-f]\\{64\\}
iid=$(jq -r '.[0].ID' <<<"$output") iid=$(jq -r '.[0].Id' <<<"$output")
t GET libpod/images/$iid/exists 204 t GET libpod/images/$iid/exists 204
t GET libpod/images/$PODMAN_TEST_IMAGE_NAME/exists 204 t GET libpod/images/$PODMAN_TEST_IMAGE_NAME/exists 204
# FIXME: compare to actual podman info # FIXME: compare to actual podman info
t GET libpod/images/json 200 \ t GET libpod/images/json 200 \
.[0].ID=${iid} .[0].Id=${iid}
t GET libpod/images/$iid/json 200 \ t GET libpod/images/$iid/json 200 \
.Id=$iid \ .Id=$iid \

View File

@ -5,8 +5,8 @@
t GET "libpod/pods/json (clean slate at start)" 200 null t GET "libpod/pods/json (clean slate at start)" 200 null
t POST libpod/pods/create name=foo 201 .id~[0-9a-f]\\{64\\} t POST libpod/pods/create name=foo 201 .Id~[0-9a-f]\\{64\\}
pod_id=$(jq -r .id <<<"$output") pod_id=$(jq -r .Id <<<"$output")
t GET libpod/pods/foo/exists 204 t GET libpod/pods/foo/exists 204
t GET libpod/pods/$pod_id/exists 204 t GET libpod/pods/$pod_id/exists 204
t GET libpod/pods/notfoo/exists 404 t GET libpod/pods/notfoo/exists 404

View File

View File

@ -0,0 +1,219 @@
import json
import os
import shlex
import signal
import string
import subprocess
import sys
import time
import unittest
from collections.abc import Iterable
from multiprocessing import Process
import requests
from dateutil.parser import parse
def _url(path):
return "http://localhost:8080/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"])
class TestApi(unittest.TestCase):
podman = None
def setUp(self):
super().setUp()
if TestApi.podman.poll() is not None:
sys.stderr.write("podman service returned {}",
TestApi.podman.returncode)
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("podman exited with error code {}\n".format(
TestApi.podman.returncode))
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)
self.validateObjectFields(r.text)
def test_inspect_container(self):
r = requests.get(_url(ctnr("/containers/{}/json")))
self.assertEqual(r.status_code, 200, r.text)
obj = self.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:
self.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")))
self.assertIn(r.status_code, (101, 409), 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)
self.validateObjectFields(r.text)
def test_images(self):
r = requests.get(_url("/images/json"))
self.assertEqual(r.status_code, 200, r.text)
self.validateObjectFields(r.content)
def test_inspect_image(self):
r = requests.get(_url("/images/alpine/json"))
self.assertEqual(r.status_code, 200, r.text)
obj = self.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=5)
self.assertEqual(r.status_code, 200, r.text)
json.loads(r.text)
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 validateObjectFields(self, buffer):
objs = json.loads(buffer)
if not isinstance(objs, dict):
for o in objs:
_ = o["Id"]
else:
_ = objs["Id"]
return objs
if __name__ == '__main__':
unittest.main()

View File

@ -28,7 +28,7 @@ load helpers
# 'created': podman includes fractional seconds, podman-remote does not # 'created': podman includes fractional seconds, podman-remote does not
tests=" tests="
Names[0] | $PODMAN_TEST_IMAGE_FQN Names[0] | $PODMAN_TEST_IMAGE_FQN
ID | [0-9a-f]\\\{64\\\} Id | [0-9a-f]\\\{64\\\}
Digest | sha256:[0-9a-f]\\\{64\\\} Digest | sha256:[0-9a-f]\\\{64\\\}
CreatedAt | [0-9-]\\\+T[0-9:.]\\\+Z CreatedAt | [0-9-]\\\+T[0-9:.]\\\+Z
Size | [0-9]\\\+ Size | [0-9]\\\+