Commit c80e65e2 authored by Geoff Simmons's avatar Geoff Simmons

REST API error response body for invalid /pems/ requests.

parent b6cd2f4f
...@@ -29,9 +29,12 @@ ...@@ -29,9 +29,12 @@
package rest package rest
import ( import (
"encoding/json"
"net/http" "net/http"
"regexp" "regexp"
"strconv"
"strings" "strings"
"sync/atomic"
"time" "time"
"code.uplex.de/k8s/k8s-crt-dnldr/pkg/crt" "code.uplex.de/k8s/k8s-crt-dnldr/pkg/crt"
...@@ -45,6 +48,14 @@ const ( ...@@ -45,6 +48,14 @@ const (
verParam = "version" verParam = "version"
) )
// ErrorDetails stores the fixed strings used in the bodies of error
// responses.
type ErrorDetails struct {
Type string
Title string
Detail string
}
var ( var (
pemsRegex = regexp.MustCompile("^" + pemsPfx + "([^/]+)/([^/]+)$") pemsRegex = regexp.MustCompile("^" + pemsPfx + "([^/]+)/([^/]+)$")
...@@ -62,8 +73,27 @@ var ( ...@@ -62,8 +73,27 @@ var (
http.MethodPut: struct{}{}, http.MethodPut: struct{}{},
} }
allowPems = "GET, HEAD, PUT, POST, DELETE" allowPems = "GET, HEAD, PUT, POST, DELETE"
errCtr = uint64(0)
problemContentType = "application/problem+json"
errPemsPattern = ErrorDetails{
Type: "/errors/pems/urlPattern",
Title: "Invalid /v1/pems/ URL path",
Detail: "/v1/pems/ URL path does not match " +
"/v1/pems/{namespace}/{name}",
}
) )
// Problem Details object per RFC7807
type Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail"`
Instance string `json:"instance"`
}
// Date format for Common Log Format // Date format for Common Log Format
const common = "02/Jan/2006:15:04:05 -0700" const common = "02/Jan/2006:15:04:05 -0700"
...@@ -87,6 +117,11 @@ func errLog(log *logrus.Logger, req *http.Request, err error) { ...@@ -87,6 +117,11 @@ func errLog(log *logrus.Logger, req *http.Request, err error) {
req.RequestURI, req.Proto, err) req.RequestURI, req.Proto, err)
} }
func getErrInstance() string {
n := atomic.AddUint64(&errCtr, 1)
return "/log/errors/" + strconv.FormatUint(n, 36)
}
type healthzHndlr struct { type healthzHndlr struct {
log *logrus.Logger log *logrus.Logger
version string version string
...@@ -119,6 +154,45 @@ type pemsHndlr struct { ...@@ -119,6 +154,45 @@ type pemsHndlr struct {
crtGetter *crt.Getter crtGetter *crt.Getter
} }
func (h *pemsHndlr) errorResponse(
resp http.ResponseWriter,
req *http.Request,
now time.Time,
status int,
details ErrorDetails,
err error,
) {
problem := Problem{
Type: details.Type,
Title: details.Title,
Detail: details.Detail,
Status: status,
Instance: getErrInstance(),
}
if err != nil {
problem.Detail = err.Error()
}
h.log.Errorf("%s %s: %+v", req.Method, req.RequestURI, problem)
body, err := json.Marshal(problem)
if err != nil {
resp.WriteHeader(status)
h.log.Errorf("%s %s: cannot marshal %+v: %v", req.Method,
req.RequestURI, problem, err)
reqLog(h.log, req, now, status, 0)
return
}
resp.Header().Set("Content-Type", problemContentType)
resp.Header().Set("Content-Length", strconv.Itoa(len(body)))
resp.WriteHeader(status)
n, err := resp.Write(body)
if err != nil {
h.log.Errorf("%s %s: cannot write body: %v", req.Method,
req.RequestURI, err)
}
reqLog(h.log, req, now, status, n)
}
func (h *pemsHndlr) allPems( func (h *pemsHndlr) allPems(
resp http.ResponseWriter, resp http.ResponseWriter,
req *http.Request, req *http.Request,
...@@ -143,9 +217,8 @@ func (h *pemsHndlr) ServeHTTP(resp http.ResponseWriter, req *http.Request) { ...@@ -143,9 +217,8 @@ func (h *pemsHndlr) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
matches := pemsRegex.FindStringSubmatch(req.URL.Path) matches := pemsRegex.FindStringSubmatch(req.URL.Path)
if matches == nil { if matches == nil {
status = http.StatusNotFound h.errorResponse(resp, req, now, http.StatusNotFound,
resp.WriteHeader(status) errPemsPattern, nil)
reqLog(h.log, req, now, status, bytes)
return return
} }
......
...@@ -31,10 +31,13 @@ package rest ...@@ -31,10 +31,13 @@ package rest
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"regexp"
"strconv"
"testing" "testing"
"code.uplex.de/k8s/k8s-crt-dnldr/pkg/crt" "code.uplex.de/k8s/k8s-crt-dnldr/pkg/crt"
...@@ -813,3 +816,67 @@ func TestAllPem(t *testing.T) { ...@@ -813,3 +816,67 @@ func TestAllPem(t *testing.T) {
} }
} }
} }
var errInstancePattern = regexp.MustCompile(`^/log/errors/\d+$`)
func TestErrors(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,
}
req := httptest.NewRequest(http.MethodGet, "/v1/pems/foo", nil)
rr := httptest.NewRecorder()
hndlr.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("GET /v1/pems/foo status: got %d want %d", rr.Code,
http.StatusNotFound)
}
if rr.Header().Get("Content-Type") != problemContentType {
t.Errorf("GET /v1/pems/foo Content-Type: got %s want %s",
rr.Header().Get("Content-Type"), problemContentType)
}
bodylen := len(rr.Body.String())
if rr.Header().Get("Content-Length") != strconv.Itoa(bodylen) {
t.Errorf("GET /v1/pems/foo Content-Length: got %s want %d",
rr.Header().Get("Content-Length"), bodylen)
}
problem := &Problem{}
if err = json.Unmarshal(rr.Body.Bytes(), problem); err != nil {
t.Fatalf("GET /v1/pems/foo body unmarshal: %v", err)
}
if problem.Type != errPemsPattern.Type {
t.Errorf("GET /v1/pems/foo problem type: got %s want %s",
problem.Type, errPemsPattern.Type)
}
if problem.Title != errPemsPattern.Title {
t.Errorf("GET /v1/pems/foo problem title: got %s want %s",
problem.Title, errPemsPattern.Title)
}
if problem.Detail != errPemsPattern.Detail {
t.Errorf("GET /v1/pems/foo problem title: got %s want %s",
problem.Detail, errPemsPattern.Title)
}
if problem.Status != http.StatusNotFound {
t.Errorf("GET /v1/pems/foo problem status: got %d want %d",
problem.Status, http.StatusNotFound)
}
if !errInstancePattern.Match([]byte(problem.Instance)) {
t.Errorf("GET /v1/pems/foo problem instance: "+
"got %s want /log/errors/N", problem.Instance)
}
}
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