Commit ae5ea8db authored by Geoff Simmons's avatar Geoff Simmons

Add pkg/rest.

parent 04ae0ce0
...@@ -35,7 +35,8 @@ build: ...@@ -35,7 +35,8 @@ build:
check: build check: build
golint ./pkg/crt/... golint ./pkg/crt/...
golint ./pkg/pem/... golint ./pkg/pem/...
go test -v ./pkg/crt/... ./pkg/pem/... golint ./pkg/rest/...
go test -v ./pkg/crt/... ./pkg/pem/... ./pkg/rest/...
test: check test: check
......
/*
* Copyright (c) 2020 UPLEX Nils Goroll Systemoptimierung
* All rights reserved
*
* Author: Geoffrey Simmons <geoffrey.simmons@uplex.de>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package rest
import (
"net/http"
"regexp"
"strings"
"time"
"code.uplex.de/k8s/k8s-crt-dnldr/pkg/crt"
"code.uplex.de/k8s/k8s-crt-dnldr/pkg/pem"
"github.com/sirupsen/logrus"
)
const (
uidParam = "uid"
verParam = "version"
)
var (
pemsRegex = regexp.MustCompile("^" + pemsPfx + "/([^/]+)/([^/]+)$")
allowedHealthz = map[string]struct{}{
http.MethodGet: struct{}{},
http.MethodHead: struct{}{},
}
allowedPems = map[string]struct{}{
http.MethodGet: struct{}{},
http.MethodHead: struct{}{},
http.MethodDelete: struct{}{},
http.MethodPost: struct{}{},
http.MethodPut: struct{}{},
}
)
// Date format for Common Log Format
const common = "02/Jan/2006:15:04:05 -0700"
func reqLog(
log *logrus.Logger,
req *http.Request,
now time.Time,
status, bytes int,
) {
clientIP := req.RemoteAddr
if colon := strings.LastIndex(clientIP, ":"); colon != -1 {
clientIP = clientIP[:colon]
}
log.Infof("%s - - [%s] \"%s %s %s\" %d %d\n", clientIP,
now.Format(common), req.Method, req.RequestURI, req.Proto,
status, bytes)
}
func errLog(log *logrus.Logger, req *http.Request, err error) {
log.Errorf("%s \"%s %s %s\": %v", req.RemoteAddr, req.Method,
req.RequestURI, req.Proto, err)
}
type healthzHndlr struct {
log *logrus.Logger
version string
}
func (h *healthzHndlr) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
now := time.Now()
resp.Header().Set("Content-Type", "text/plain")
resp.Header().Set("Content-Length", "0")
resp.Header().Set("Cache-Control", "no-store")
status := http.StatusTeapot
if req.URL.Path != healthzPath {
status = http.StatusNotFound
} else if _, ok := allowedHealthz[req.Method]; !ok {
status = http.StatusMethodNotAllowed
} else {
status = http.StatusNoContent
resp.Header().Del("Content-Length")
resp.Header().Set("Application-Version", h.version)
}
resp.WriteHeader(status)
reqLog(h.log, req, now, status, 0)
}
type pemsHndlr struct {
log *logrus.Logger
files *pem.Files
crtGetter *crt.Getter
}
func (h *pemsHndlr) allPems(
resp http.ResponseWriter,
req *http.Request,
now time.Time,
) {
resp.Header().Set("Content-Type", "text/plain")
resp.Header().Set("Content-Length", "0")
resp.WriteHeader(http.StatusNotImplemented)
reqLog(h.log, req, now, http.StatusNotImplemented, 0)
}
func (h *pemsHndlr) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
now := time.Now()
if req.URL.Path == pemsPfx {
h.allPems(resp, req, now)
return
}
resp.Header().Set("Content-Type", "text/plain")
resp.Header().Set("Content-Length", "0")
status := http.StatusTeapot
bytes := 0
defer reqLog(h.log, req, now, status, bytes)
matches := pemsRegex.FindStringSubmatch(req.URL.Path)
if matches == nil {
status = http.StatusNotFound
resp.WriteHeader(status)
return
}
if _, ok := allowedPems[req.Method]; !ok {
status = http.StatusMethodNotAllowed
resp.WriteHeader(status)
return
}
if req.Method == http.MethodGet || req.Method == http.MethodHead {
status = http.StatusNotImplemented
resp.WriteHeader(status)
return
}
ns, name := matches[1], matches[2]
if req.Method == http.MethodDelete {
if exist, err := h.files.Delete(ns, name); !exist {
status = http.StatusNotFound
} else if err != nil {
// XXX problem description in body
status = http.StatusInternalServerError
errLog(h.log, req, err)
} else {
status = http.StatusNoContent
resp.Header().Del("Content-Length")
}
resp.WriteHeader(status)
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)
return
} else if h.files.Check(ns, name, uid, version) {
status = http.StatusNoContent
resp.Header().Del("Content-Length")
resp.WriteHeader(status)
return
}
if found, valid, err := h.files.Write(ns, name, uid, version); !found {
status = http.StatusNotFound
resp.WriteHeader(status)
return
} else if !valid || err != nil {
// XXX problem description in body
status = http.StatusForbidden
errLog(h.log, req, err)
resp.WriteHeader(status)
return
}
if !have {
status = http.StatusCreated
} else {
status = http.StatusNoContent
resp.Header().Del("Content-Length")
}
// XXX Location: /v1/pems/ns/name?uid=uid&version=version
// XXX ETag: uid/version
// XXX LastModified: file mtime
resp.WriteHeader(status)
}
This diff is collapsed.
/*
* Copyright (c) 2020 UPLEX Nils Goroll Systemoptimierung
* All rights reserved
*
* Author: Geoffrey Simmons <geoffrey.simmons@uplex.de>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package rest
import (
"context"
"net"
"net/http"
"os"
"strings"
"code.uplex.de/k8s/k8s-crt-dnldr/pkg/crt"
"code.uplex.de/k8s/k8s-crt-dnldr/pkg/pem"
"github.com/sirupsen/logrus"
)
const (
healthzPath = "/v1/healthz"
pemsPfx = "/v1/pems"
)
// Server encapsulates the HTTP server for the REST API.
type Server struct {
server http.Server
addr string
gid int
mode int
version string
log *logrus.Logger
files *pem.Files
crtGetter *crt.Getter
}
// NewServer creates a Server instance for the REST API.
//
// addr is the address at which the server listens, suitable for use by
// net.Listen(). If addr begins with "unix@", then the remainder of the
// strings is a Unix domain socket at which the server listens.
//
// version is a version string for the application, whose value is to
// be returned in a response header for health checks.
//
// If addr designates a Unix domain socket, then if gid and/or mode
// are >= 0, they define permissions for the newly created socket
// file. Mode sets the file mode (permissions), and gid sets the
// socket file's group ID. gid and mode are ignored if they are
// negative, or if addr does not designate a Unix domain socket.
//
// log is a logger for request and error logging. Requests are logged
// in Common Log Format.
//
// files is initialized to mamage PEM files, see pkg/pem.Files. crtGetter
// retrieves and validates Secret data, see pkg/crt.Getter.
func NewServer(
addr, version string,
gid, mode int,
log *logrus.Logger,
files *pem.Files,
crtGetter *crt.Getter,
) *Server {
srv := &Server{
addr: addr,
gid: -1,
mode: -1,
version: version,
log: log,
files: files,
crtGetter: crtGetter,
}
if gid >= 0 {
srv.gid = gid
}
if mode >= 0 {
srv.mode = mode
}
return srv
}
// Start launches an HTTP server for the REST API, listening at the
// address given in NewServer().
//
// Start may return a non-nil error if there is an error opening the
// listenner address. It may also return an error setting permissions,
// if the listen address is a Unix domain sockert.
//
// Otherwise, the server is started in a separate goroutine, and Start
// returns immediately.
func (srv *Server) Start() error {
srv.log.Infof("Starting HTTP server for the REST API")
network := "tcp"
if strings.HasPrefix(srv.addr, "unix@") {
srv.addr = (srv.addr)[5:]
if _, err := os.Stat(srv.addr); err == nil {
srv.log.Info("deleting ", srv.addr)
if err = os.Remove(srv.addr); err != nil {
srv.log.Error("Cannot delete ", srv.addr,
": ", err)
return err
}
} else if !os.IsNotExist(err) {
return err
}
network = "unix"
}
lsnr, err := net.Listen(network, srv.addr)
if err != nil {
return err
}
if network == "unix" {
if srv.gid != -1 {
if err = os.Chown(srv.addr, -1, srv.gid); err != nil {
srv.log.Errorf("Cannot set gid %d for %s: %v",
srv.gid, srv.addr, err)
return err
}
}
if srv.mode != -1 {
if err = os.Chmod(
srv.addr, os.FileMode(srv.mode)); err != nil {
srv.log.Errorf("Cannot set mode %0o for %s: "+
"%v", srv.mode, srv.addr, err)
return err
}
}
}
srv.log.Infof("REST API HTTP server listening at %s", srv.addr)
mux := http.NewServeMux()
mux.Handle(healthzPath, &healthzHndlr{
log: srv.log,
version: srv.version,
})
mux.Handle(pemsPfx, &pemsHndlr{
log: srv.log,
files: srv.files,
crtGetter: srv.crtGetter,
})
srv.server = http.Server{Handler: mux}
go func() {
if err := srv.server.Serve(lsnr); err != nil {
if err != http.ErrServerClosed {
srv.log.Errorf("REST API HTTP server: %+v", err)
}
}
}()
return nil
}
// Stop shuts down the HTTP server running the REST API.
func (srv *Server) Stop() error {
srv.log.Info("Stopping HTTP server for the REST API")
ctx := context.Background()
if err := srv.server.Shutdown(ctx); err != nil {
return err
}
return nil
}
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