199 lines
5.8 KiB
Go
199 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"strconv"
|
|
"time"
|
|
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"mutating-webhook/internal/config"
|
|
"mutating-webhook/internal/operations"
|
|
|
|
admission "k8s.io/api/admission/v1"
|
|
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
)
|
|
|
|
const InvalidMethod string = "Invalid http method."
|
|
|
|
func httpAccessLog(req *http.Request) {
|
|
log.Printf("[TRACE] %s - %s - %s\n", req.Method, req.RemoteAddr, req.RequestURI)
|
|
}
|
|
|
|
func crossSiteOrigin(w http.ResponseWriter) {
|
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
|
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
|
w.Header().Add("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, X-API-Token")
|
|
}
|
|
|
|
func strictTransport(w http.ResponseWriter) {
|
|
w.Header().Add("Strict-Transport-Security", "max-age=63072000")
|
|
}
|
|
|
|
func httpServer(cfg *config.Config) {
|
|
serverCertificate, _ := tls.X509KeyPair(append([]byte(cfg.CertCert), []byte(cfg.CACert)...), []byte(cfg.CertPrivateKey))
|
|
|
|
path := http.NewServeMux()
|
|
|
|
connection := &http.Server{
|
|
Addr: cfg.WebServerIP + ":" + strconv.FormatInt(int64(cfg.WebServerPort), 10),
|
|
Handler: path,
|
|
ReadTimeout: time.Duration(cfg.WebServerReadTimeout) * time.Second,
|
|
WriteTimeout: time.Duration(cfg.WebServerWriteTimeout) * time.Second,
|
|
IdleTimeout: time.Duration(cfg.WebServerIdleTimeout) * time.Second,
|
|
TLSConfig: &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
CipherSuites: []uint16{
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
},
|
|
Certificates: []tls.Certificate{
|
|
serverCertificate,
|
|
},
|
|
},
|
|
}
|
|
|
|
ah := &admissionHandler{
|
|
decoder: serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer(),
|
|
config: cfg,
|
|
}
|
|
|
|
// pod admission
|
|
path.HandleFunc("/api/v1/admit/pod", ah.ahServe(operations.PodsValidation()))
|
|
// deployment admission
|
|
path.HandleFunc("/api/v1/admit/deployment", ah.ahServe(operations.DeploymentsValidation()))
|
|
// pod mutation
|
|
path.HandleFunc("/api/v1/mutate/pod", ah.ahServe(operations.PodsMutation()))
|
|
// web root
|
|
path.HandleFunc("/", webServe())
|
|
|
|
if err := connection.ListenAndServeTLS("", ""); err != nil {
|
|
log.Fatalf("[ERROR] %s\n", err)
|
|
}
|
|
}
|
|
|
|
func webServe() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
httpAccessLog(r)
|
|
crossSiteOrigin(w)
|
|
strictTransport(w)
|
|
|
|
switch {
|
|
case r.Method != http.MethodGet:
|
|
msg := fmt.Sprintf("incorrect method: got request type %s, expected request type %s", r.Method, http.MethodPost)
|
|
log.Printf("[DEBUG] %s", msg)
|
|
tmpltError(w, http.StatusMethodNotAllowed, msg)
|
|
case r.URL.Path == "/api/v1/admin":
|
|
tmpltAdminToggle(w, r.URL.Query())
|
|
case r.URL.Path == "/healthcheck":
|
|
tmpltHealthCheck(w)
|
|
case r.URL.Path == "/":
|
|
tmpltWebRoot(w)
|
|
default:
|
|
msg := fmt.Sprintf("Unable to locate requested path: '%s'", r.URL.Path)
|
|
log.Printf("[DEBUG] %s", msg)
|
|
tmpltError(w, http.StatusNotFound, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
type admissionHandler struct {
|
|
decoder runtime.Decoder
|
|
config *config.Config
|
|
}
|
|
|
|
func (h *admissionHandler) ahServe(hook operations.Hook) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
httpAccessLog(r)
|
|
crossSiteOrigin(w)
|
|
strictTransport(w)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if r.Method != http.MethodPost {
|
|
msg := fmt.Sprintf("incorrect method: got request type %s, expected request type %s", r.Method, http.MethodPost)
|
|
log.Printf("[DEBUG] %s", msg)
|
|
tmpltError(w, http.StatusMethodNotAllowed, msg)
|
|
return
|
|
}
|
|
|
|
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
|
|
msg := "only content type 'application/json' is supported"
|
|
log.Printf("[DEBUG] %s", msg)
|
|
tmpltError(w, http.StatusBadRequest, msg)
|
|
return
|
|
}
|
|
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("could not read request body: %v", err)
|
|
log.Printf("[DEBUG] %s", msg)
|
|
tmpltError(w, http.StatusBadRequest, msg)
|
|
return
|
|
}
|
|
|
|
var review admission.AdmissionReview
|
|
if _, _, err := h.decoder.Decode(body, nil, &review); err != nil {
|
|
msg := fmt.Sprintf("could not deserialize request: %v", err)
|
|
log.Printf("[DEBUG] %s", msg)
|
|
tmpltError(w, http.StatusBadRequest, msg)
|
|
return
|
|
}
|
|
|
|
if review.Request == nil {
|
|
msg := "malformed admission review: request is nil"
|
|
log.Printf("[DEBUG] %s", msg)
|
|
tmpltError(w, http.StatusBadRequest, msg)
|
|
return
|
|
}
|
|
|
|
result, err := hook.Execute(review.Request, &cfg)
|
|
if err != nil {
|
|
msg := err.Error()
|
|
log.Printf("[ERROR] Internal Server Error: %s", msg)
|
|
tmpltError(w, http.StatusInternalServerError, msg)
|
|
return
|
|
}
|
|
|
|
admissionResponse := admission.AdmissionReview{
|
|
Response: &admission.AdmissionResponse{
|
|
UID: review.Request.UID,
|
|
Allowed: result.Allowed,
|
|
Result: &meta.Status{Message: result.Msg},
|
|
},
|
|
}
|
|
|
|
// set the patch operations for mutating admission
|
|
if len(result.PatchOps) > 0 {
|
|
patchBytes, err := json.Marshal(result.PatchOps)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("could not marshal JSON patch: %v", err)
|
|
log.Printf("[ERROR] %s", msg)
|
|
tmpltError(w, http.StatusInternalServerError, msg)
|
|
}
|
|
admissionResponse.Response.Patch = patchBytes
|
|
}
|
|
|
|
res, err := json.Marshal(admissionResponse)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("could not marshal response: %v", err)
|
|
log.Printf("[ERROR] %s", msg)
|
|
tmpltError(w, http.StatusInternalServerError, msg)
|
|
return
|
|
}
|
|
|
|
log.Printf("[DEBUG] Webhook [%s] - Allowed: %t", review.Request.Operation, result.Allowed)
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(res)
|
|
}
|
|
}
|