Commit 49b55fbb authored by Geoff Simmons's avatar Geoff Simmons

REST API: implement GET/HEAD /v1/pems/{namespace}/{name}

parent 43ecc180
......@@ -87,9 +87,9 @@ var (
Detail: "/v1/pems/ URL path does not match " +
"/v1/pems/{namespace}/{name}",
}
errPemDeleteNotFound = ErrorDetails{
Type: "/errors/pems/delete/notFound",
Title: "PEM file to be deleted not found",
errPemNotFound = ErrorDetails{
Type: "/errors/pems/notFound",
Title: "PEM file not found",
Detail: "",
}
errPemDeleteError = ErrorDetails{
......@@ -279,6 +279,47 @@ func (h *pemsHndlr) allPems(
reqLog(h.log, req, now, http.StatusOK, n)
}
func (h *pemsHndlr) pemInfo(
ns, name, uid, version string,
resp http.ResponseWriter,
req *http.Request,
now time.Time,
) {
// XXX check Accept
// XXX I-M-S and I-N-M
info, found, err := h.files.GetFileInfo(ns, name, uid, version)
if !found {
h.errorResponse(resp, req, now, http.StatusNotFound,
errPemNotFound, err)
return
}
if err != nil {
h.errorResponse(resp, req, now, http.StatusInternalServerError,
errPemsReadErr, err)
return
}
jsonBytes, err := json.Marshal(info)
if err != nil {
h.errorResponse(resp, req, now, http.StatusInternalServerError,
errPemsJSONMarshal, err)
return
}
// XXX LastModified and ETag
resp.Header().Set("Content-Type", jsonContentType)
resp.Header().Set("Content-Length", strconv.Itoa(len(jsonBytes)))
n := 0
if req.Method == http.MethodGet {
n, err = resp.Write(jsonBytes)
if err != nil {
h.log.Errorf("%s %s: cannot write body: %v", req.Method,
req.RequestURI, err)
}
} else {
resp.WriteHeader(http.StatusOK)
}
reqLog(h.log, req, now, http.StatusOK, n)
}
func (h *pemsHndlr) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
now := time.Now()
......@@ -289,7 +330,6 @@ func (h *pemsHndlr) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Content-Length", "0")
status := http.StatusTeapot
bytes := 0
matches := pemsRegex.FindStringSubmatch(req.URL.Path)
if matches == nil {
......@@ -302,22 +342,23 @@ func (h *pemsHndlr) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
status = http.StatusMethodNotAllowed
resp.Header().Set("Allow", allowPems)
resp.WriteHeader(status)
reqLog(h.log, req, now, status, bytes)
reqLog(h.log, req, now, status, 0)
return
}
ns, name := matches[1], matches[2]
uid := req.URL.Query().Get(uidParam)
version := req.URL.Query().Get(verParam)
if req.Method == http.MethodGet || req.Method == http.MethodHead {
status = http.StatusNotImplemented
resp.WriteHeader(status)
reqLog(h.log, req, now, status, bytes)
h.pemInfo(ns, name, uid, version, resp, req, now)
return
}
ns, name := matches[1], matches[2]
if req.Method == http.MethodDelete {
if exist, err := h.files.Delete(ns, name); !exist ||
(err != nil && os.IsNotExist(err)) {
h.errorResponse(resp, req, now, http.StatusNotFound,
errPemDeleteNotFound, err)
errPemNotFound, err)
return
} else if err != nil {
h.errorResponse(resp, req, now,
......@@ -328,26 +369,23 @@ func (h *pemsHndlr) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
status = http.StatusNoContent
resp.Header().Del("Content-Length")
resp.WriteHeader(status)
reqLog(h.log, req, now, status, bytes)
reqLog(h.log, req, now, status, 0)
return
}
uid := req.URL.Query().Get(uidParam)
version := req.URL.Query().Get(verParam)
// XXX If-Match: uid/version
// XXX If-Unmodified-Since: compare file mtime
have := h.files.Have(ns, name, uid, version)
if have && req.Method == http.MethodPost {
status = http.StatusConflict
resp.WriteHeader(status)
reqLog(h.log, req, now, status, bytes)
reqLog(h.log, req, now, status, 0)
return
} else if h.files.Check(ns, name, uid, version) {
status = http.StatusNoContent
resp.Header().Del("Content-Length")
resp.WriteHeader(status)
reqLog(h.log, req, now, status, bytes)
reqLog(h.log, req, now, status, 0)
return
}
if found, valid, err := h.files.Write(ns, name, uid, version); !found {
......@@ -392,5 +430,5 @@ func (h *pemsHndlr) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
// XXX ETag: uid/version
// XXX LastModified: file mtime
resp.WriteHeader(status)
reqLog(h.log, req, now, status, bytes)
reqLog(h.log, req, now, status, 0)
}
......@@ -893,15 +893,15 @@ func TestDeletePem(t *testing.T) {
t.Fatalf("2nd DELETE /v1/pems/ns1/secret1 body unmarshal: %v",
err)
}
if problem.Type != errPemDeleteNotFound.Type {
if problem.Type != errPemNotFound.Type {
t.Errorf("2nd DELETE /v1/pems/ns1/secret1 problem type: "+
"got %s want %s",
problem.Type, errPemDeleteNotFound.Type)
problem.Type, errPemNotFound.Type)
}
if problem.Title != errPemDeleteNotFound.Title {
if problem.Title != errPemNotFound.Title {
t.Errorf("2nd DELETE /v1/pems/ns1/secret1 problem title: "+
"got %s want %s",
problem.Title, errPemDeleteNotFound.Title)
problem.Title, errPemNotFound.Title)
}
if problem.Status != http.StatusNotFound {
t.Errorf("2nd DELETE /v1/pems/ns1/secret1 problem status: "+
......@@ -944,42 +944,6 @@ func TestDeletePem(t *testing.T) {
}
}
func TestGetPem(t *testing.T) {
client := fake.NewSimpleClientset()
lister := setupSecretLister(client)
getter := crt.NewGetter(lister)
base, err := getTempDir()
if err != nil {
t.Fatalf("ioutil.TempDir(): %+v", err)
}
defer os.RemoveAll(base)
files, err := pem.NewFiles(base, -1, getter)
if err != nil {
t.Fatalf("NewFiles(): %v", err)
}
hndlr := &pemsHndlr{
log: &logrus.Logger{Out: ioutil.Discard},
files: files,
crtGetter: getter,
}
for _, method := range []string{http.MethodGet, http.MethodHead} {
req := httptest.NewRequest(method, "/v1/pems/ns/name", nil)
rr := httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusNotImplemented {
t.Errorf("%s /v1/pems/ns/name status: got %d want %d",
method, rr.Code, http.StatusNotImplemented)
}
if rr.Result().Header.Get("Content-Length") != "0" {
t.Errorf("%s /v1/pems/ns/name Content-Length: "+
"got %s want 0", method,
rr.Result().Header.Get("Content-Length"))
}
}
}
func TestPem405(t *testing.T) {
client := fake.NewSimpleClientset()
lister := setupSecretLister(client)
......
......@@ -39,6 +39,7 @@ import (
"reflect"
"runtime"
"strconv"
"strings"
"testing"
"time"
......@@ -83,6 +84,15 @@ var (
ModTime: time.Date(2020, 7, 27, 9, 51, 0, 0, time.UTC),
}
enoentFile = &pem.File{
Namespace: "invalid",
Name: "enoent",
UID: "fee13b58-a4a1-41fa-848b-3771292e165d",
ResourceVersion: "666666",
Size: 666,
ModTime: time.Date(2020, 6, 6, 6, 6, 6, 6, time.UTC),
}
invalidFile = &pem.File{
Namespace: "invalid",
Name: "crt",
......@@ -417,3 +427,333 @@ func TestAllPems405(t *testing.T) {
}
}
}
func TestGetPem(t *testing.T) {
client := fake.NewSimpleClientset()
lister := setupSecretLister(client)
getter := crt.NewGetter(lister)
files, err := pem.NewFiles(basedir, -1, getter)
if err != nil {
t.Fatalf("NewFiles(): %v", err)
}
files.Files["ns1/cafe"] = cafeFile
files.Files["ns2/bar"] = barFile
files.Files["invalid/enoent"] = enoentFile
files.Files["invalid/crt"] = invalidFile
hndlr := &pemsHndlr{
log: &logrus.Logger{Out: ioutil.Discard},
files: files,
crtGetter: getter,
}
req := httptest.NewRequest(http.MethodGet, "/v1/pems/ns1/cafe", nil)
rr := httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("GET /v1/pems/ns1/cafe status: got %d want %d",
rr.Code, http.StatusOK)
}
var info pem.FileInfo
if err = json.Unmarshal(rr.Body.Bytes(), &info); err != nil {
t.Errorf("GET /v1/pems/ns1/cafe unmarshal: %v", err)
}
if !reflect.DeepEqual(info, expCafeInfo) {
t.Errorf("GET /v1/pems/ns1/cafe got %#v want %#v", info,
expCafeInfo)
}
bodyLen := strconv.Itoa(rr.Body.Len())
if rr.Header().Get("Content-Length") != bodyLen {
t.Errorf("GET /v1/pems/ns1/cafe Content-Length: got %s want %s",
rr.Header().Get("Content-Length"), bodyLen)
}
if rr.Header().Get("Content-Type") != jsonContentType {
t.Errorf("GET /v1/pems/ns1/cafe Content-Type: got %s want %s",
rr.Header().Get("Content-Type"), jsonContentType)
}
req = httptest.NewRequest(http.MethodHead, "/v1/pems/ns1/cafe", nil)
rr = httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("HEAD /v1/pems/ns1/cafe status: got %d want %d",
rr.Code, http.StatusOK)
}
if rr.Body.Len() != 0 {
t.Errorf("HEAD /v1/pems/ns1/cafe body len got %d want 0",
rr.Body.Len())
}
if rr.Header().Get("Content-Length") != bodyLen {
t.Errorf("HEAD /v1/pems/ns1/cafe Content-Length: got %s want %s",
rr.Header().Get("Content-Length"), bodyLen)
}
if rr.Header().Get("Content-Type") != jsonContentType {
t.Errorf("HEAD /v1/pems/ns1/cafe Content-Type: got %s want %s",
rr.Header().Get("Content-Type"), jsonContentType)
}
req = httptest.NewRequest(http.MethodGet, "/v1/pems/ns2/bar", nil)
rr = httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("GET /v1/pems/ns2/bar status: got %d want %d",
rr.Code, http.StatusOK)
}
if err = json.Unmarshal(rr.Body.Bytes(), &info); err != nil {
t.Errorf("GET /v1/pems/ns2/bar unmarshal: %v", err)
}
if !reflect.DeepEqual(info, expBarInfo) {
t.Errorf("GET /v1/pems/ns2/bar got %#v want %#v", info,
expCafeInfo)
}
bodyLen = strconv.Itoa(rr.Body.Len())
if rr.Header().Get("Content-Length") != bodyLen {
t.Errorf("GET /v1/pems/ns2/bar Content-Length: got %s want %s",
rr.Header().Get("Content-Length"), bodyLen)
}
if rr.Header().Get("Content-Type") != jsonContentType {
t.Errorf("GET /v1/pems/ns2/bar Content-Type: got %s want %s",
rr.Header().Get("Content-Type"), jsonContentType)
}
req = httptest.NewRequest(http.MethodGet,
"/v1/pems/ns1/cafe?uid=975d4e4f-9ea9-49d9-a81d-1b4bca92a743",
nil)
rr = httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("GET /v1/pems/ns1/cafe?uid status: got %d want %d",
rr.Code, http.StatusOK)
}
info = pem.FileInfo{}
if err = json.Unmarshal(rr.Body.Bytes(), &info); err != nil {
t.Errorf("GET /v1/pems/ns1/cafe?uid unmarshal: %v", err)
}
if !reflect.DeepEqual(info, expCafeInfo) {
t.Errorf("GET /v1/pems/ns1/cafe?uid got %#v want %#v", info,
expCafeInfo)
}
req = httptest.NewRequest(http.MethodGet,
"/v1/pems/ns1/cafe?version=4711", nil)
rr = httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("GET /v1/pems/ns1/cafe?version status: got %d want %d",
rr.Code, http.StatusOK)
}
info = pem.FileInfo{}
if err = json.Unmarshal(rr.Body.Bytes(), &info); err != nil {
t.Errorf("GET /v1/pems/ns1/cafe?version unmarshal: %v", err)
}
if !reflect.DeepEqual(info, expCafeInfo) {
t.Errorf("GET /v1/pems/ns1/cafe?version got %#v want %#v", info,
expCafeInfo)
}
req = httptest.NewRequest(http.MethodGet,
"/v1/pems/ns1/cafe?uid=975d4e4f-9ea9-49d9-a81d-1b4bca92a743&version=4711",
nil)
rr = httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("GET /v1/pems/ns1/cafe?uid&version status: "+
"got %d want %d", rr.Code, http.StatusOK)
}
info = pem.FileInfo{}
if err = json.Unmarshal(rr.Body.Bytes(), &info); err != nil {
t.Errorf("GET /v1/pems/ns1/cafe?uid&version unmarshal: %v", err)
}
if !reflect.DeepEqual(info, expCafeInfo) {
t.Errorf("GET /v1/pems/ns1/cafe?uid&version got %#v want %#v",
info, expCafeInfo)
}
req = httptest.NewRequest(http.MethodGet,
"/v1/pems/ns1/cafe?uid=d18974c5-94d7-4e04-b2af-6e9274ad46d8",
nil)
rr = httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("GET /v1/pems/ns1/cafe?uid status: got %d want %d",
rr.Code, http.StatusNotFound)
}
if rr.Header().Get("Content-Type") != problemContentType {
t.Errorf("GET /v1/pems/ns1/cafe?uid Content-Type: "+
"got %s want %s",
rr.Header().Get("Content-Type"), problemContentType)
}
bodylen := strconv.Itoa(rr.Body.Len())
if rr.Header().Get("Content-Length") != bodylen {
t.Errorf("GET /v1/pems/ns1/cafe?uid Content-Length: "+
"got %s want %s", rr.Header().Get("Content-Length"),
bodylen)
}
problem := &Problem{}
if err = json.Unmarshal(rr.Body.Bytes(), problem); err != nil {
t.Fatalf("GET /v1/pems/ns1/cafe?uid body unmarshal: %v",
err)
}
if problem.Type != errPemNotFound.Type {
t.Errorf("GET /v1/pems/ns1/cafe?uid problem type: "+
"got %s want %s", problem.Type, errPemNotFound.Type)
}
if problem.Title != errPemNotFound.Title {
t.Errorf("GET /v1/pems/ns1/cafe?uid problem title: "+
"got %s want %s", problem.Title, errPemNotFound.Title)
}
if !strings.Contains(problem.Detail,
"uid=d18974c5-94d7-4e04-b2af-6e9274ad46d8") {
t.Errorf("GET /v1/pems/ns1/cafe?uid problem detail got %s "+
"want contains "+
"uid=d18974c5-94d7-4e04-b2af-6e9274ad46d8",
problem.Detail)
}
if problem.Status != http.StatusNotFound {
t.Errorf("GET /v1/pems/ns1/cafe?uid problem status: "+
"got %d want %d", problem.Status, http.StatusNotFound)
}
if !errInstancePattern.Match([]byte(problem.Instance)) {
t.Errorf("GET /v1/pems/ns1/cafe?uid problem instance: "+
"got %s want /log/errors/N", problem.Instance)
}
req = httptest.NewRequest(http.MethodGet,
"/v1/pems/ns1/cafe?version=0815", nil)
rr = httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("GET /v1/pems/ns1/cafe?version status: got %d want %d",
rr.Code, http.StatusNotFound)
}
if rr.Header().Get("Content-Type") != problemContentType {
t.Errorf("GET /v1/pems/ns1/cafe?version Content-Type: "+
"got %s want %s",
rr.Header().Get("Content-Type"), problemContentType)
}
bodylen = strconv.Itoa(rr.Body.Len())
if rr.Header().Get("Content-Length") != bodylen {
t.Errorf("GET /v1/pems/ns1/cafe?version Content-Length: "+
"got %s want %s", rr.Header().Get("Content-Length"),
bodylen)
}
if err = json.Unmarshal(rr.Body.Bytes(), problem); err != nil {
t.Fatalf("GET /v1/pems/ns1/cafe?version body unmarshal: %v",
err)
}
if problem.Type != errPemNotFound.Type {
t.Errorf("GET /v1/pems/ns1/cafe?version problem type: "+
"got %s want %s", problem.Type, errPemNotFound.Type)
}
if problem.Title != errPemNotFound.Title {
t.Errorf("GET /v1/pems/ns1/cafe?version problem title: "+
"got %s want %s", problem.Title, errPemNotFound.Title)
}
if !strings.Contains(problem.Detail, "version=0815") {
t.Error("GET /v1/pems/ns1/cafe?version problem detail got %s "+
"want contains version=0815", problem.Detail)
}
if problem.Status != http.StatusNotFound {
t.Errorf("GET /v1/pems/ns1/cafe?version problem status: "+
"got %d want %d", problem.Status, http.StatusNotFound)
}
if !errInstancePattern.Match([]byte(problem.Instance)) {
t.Errorf("GET /v1/pems/ns1/cafe?version problem instance: "+
"got %s want /log/errors/N", problem.Instance)
}
req = httptest.NewRequest(http.MethodGet, "/v1/pems/foo/bar", nil)
rr = httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("GET /v1/pems/foo/bar status: got %d want %d",
rr.Code, http.StatusNotFound)
}
if rr.Header().Get("Content-Type") != problemContentType {
t.Errorf("GET /v1/pems/foo/bar Content-Type: got %s want %s",
rr.Header().Get("Content-Type"), problemContentType)
}
bodylen = strconv.Itoa(rr.Body.Len())
if rr.Header().Get("Content-Length") != bodylen {
t.Errorf("GET /v1/pems/foo/bar Content-Length: "+
"got %s want %s", rr.Header().Get("Content-Length"),
bodylen)
}
if err = json.Unmarshal(rr.Body.Bytes(), problem); err != nil {
t.Fatalf("GET /v1/pems/foo/bar body unmarshal: %v",
err)
}
if problem.Type != errPemNotFound.Type {
t.Errorf("GET /v1/pems/foo/bar problem type: "+
"got %s want %s", problem.Type, errPemNotFound.Type)
}
req = httptest.NewRequest(http.MethodGet, "/v1/pems/invalid/enoent",
nil)
rr = httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("GET /v1/pems/invalid/enoent status: got %d want %d",
rr.Code, http.StatusNotFound)
}
if rr.Header().Get("Content-Type") != problemContentType {
t.Errorf("GET /v1/pems/invalid/enoent Content-Type: "+
"got %s want %s",
rr.Header().Get("Content-Type"), problemContentType)
}
bodylen = strconv.Itoa(rr.Body.Len())
if rr.Header().Get("Content-Length") != bodylen {
t.Errorf("GET /v1/pems/invalid/enoent Content-Length: "+
"got %s want %s", rr.Header().Get("Content-Length"),
bodylen)
}
if err = json.Unmarshal(rr.Body.Bytes(), problem); err != nil {
t.Fatalf("GET /v1/pems/invalid/enoent body unmarshal: %v",
err)
}
if problem.Type != errPemNotFound.Type {
t.Errorf("GET /v1/pems/invalid/enoent problem type: "+
"got %s want %s", problem.Type, errPemNotFound.Type)
}
req = httptest.NewRequest(http.MethodGet, "/v1/pems/invalid/crt",
nil)
rr = httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusInternalServerError {
t.Errorf("GET /v1/pems/invalid/crt status: got %d want %d",
rr.Code, http.StatusInternalServerError)
}
if rr.Header().Get("Content-Type") != problemContentType {
t.Errorf("GET /v1/pems/invalid/crt Content-Type: "+
"got %s want %s",
rr.Header().Get("Content-Type"), problemContentType)
}
bodylen = strconv.Itoa(rr.Body.Len())
if rr.Header().Get("Content-Length") != bodylen {
t.Errorf("GET /v1/pems/invalid/crt Content-Length: "+
"got %s want %s", rr.Header().Get("Content-Length"),
bodylen)
}
if err = json.Unmarshal(rr.Body.Bytes(), problem); err != nil {
t.Fatalf("GET /v1/pems/invalid/crt body unmarshal: %v",
err)
}
if problem.Type != errPemsReadErr.Type {
t.Errorf("GET /v1/pems/invalid/crt problem type: "+
"got %s want %s", problem.Type, errPemsReadErr.Type)
}
if problem.Title != errPemsReadErr.Title {
t.Errorf("GET /v1/pems/invalid/crt problem type: "+
"got %s want %s", problem.Title, errPemsReadErr.Title)
}
if problem.Status != http.StatusInternalServerError {
t.Errorf("GET /v1/pems/invalid/crt problem status: "+
"got %d want %d", problem.Status,
http.StatusInternalServerError)
}
if !errInstancePattern.Match([]byte(problem.Instance)) {
t.Errorf("GET /v1/pems/invalid/crt problem instance: "+
"got %s want /log/errors/N", problem.Instance)
}
}
-----BEGIN CERTIFICATE-----
DEADBEEF
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
FOOBAR
-----END RSA PRIVATE KEY-----
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment