prep for actual admission/hook efforts

This commit is contained in:
Hyatt 2023-03-18 11:43:52 -05:00
parent 9c1f349c97
commit 9dd4366e73
Signed by: nhyatt
GPG Key ID: C50D0BBB5BC40BEA
8 changed files with 212 additions and 148 deletions

View File

@ -1,14 +1,25 @@
package main
import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
"mutating-webhook/internal/certificate"
"mutating-webhook/internal/config"
"net/http"
"strconv"
"strings"
"time"
"crypto/tls"
"net/http"
"mutating-webhook/internal/certificate"
"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."
@ -62,9 +73,30 @@ func httpServer(cfg *config.Config) {
}
// healthcheck
path.HandleFunc("/healthcheck", webHealthCheck)
// api-endpoint
path.HandleFunc("/api/v1/mutate", webMutatePod)
path.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
webHealthCheck(w, r)
})
// pod admission
path.HandleFunc("/api/v1/admit/pod", func(w http.ResponseWriter, r *http.Request) {
ah := &admissionHandler{
decoder: serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer(),
}
ah.Serve(operations.PodsValidation())
})
// deployment admission
path.HandleFunc("/api/v1/admit/deployemnt", func(w http.ResponseWriter, r *http.Request) {
ah := &admissionHandler{
decoder: serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer(),
}
ah.Serve(operations.DeploymentsValidation())
})
// pod mutation
path.HandleFunc("/api/v1/mutate/pod", func(w http.ResponseWriter, r *http.Request) {
ah := &admissionHandler{
decoder: serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer(),
}
ah.Serve(operations.PodsMutation())
})
// web root
path.HandleFunc("/", webRoot)
@ -102,3 +134,93 @@ func webHealthCheck(w http.ResponseWriter, r *http.Request) {
tmpltError(w, http.StatusBadRequest, InvalidMethod)
}
}
type admissionHandler struct {
decoder runtime.Decoder
}
// Serve returns a http.HandlerFunc for an admission webhook
func (h *admissionHandler) Serve(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 := "malformed admission review: request is nil"
log.Printf("[TRACE] %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("[TRACE] %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("[TRACE] %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("[TRACE] %s", msg)
tmpltError(w, http.StatusBadRequest, msg)
return
}
if review.Request == nil {
msg := "malformed admission review: request is nil"
log.Printf("[TRACE] %s", msg)
tmpltError(w, http.StatusBadRequest, msg)
return
}
result, err := hook.Execute(review.Request)
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("[INFO] Webhook [%s - %s] - Allowed: %t", r.URL.Path, review.Request.Operation, result.Allowed)
w.WriteHeader(http.StatusOK)
w.Write(res)
}
}

View File

@ -11,10 +11,10 @@ import (
func forever() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
sig := <-c
log.Printf("[INFO] shutting down, detected signal: %s", sig)
log.Printf("[INFO] Received %s signal, shutting down...", sig)
}
func main() {

View File

@ -1,126 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
admission "k8s.io/api/admission/v1"
core "k8s.io/api/core/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
type result struct {
Allowed bool
Msg string
PatchOps []patchOperation
}
type admitFunc func(request *admission.AdmissionRequest) (*result, error)
type hook struct {
Create admitFunc
Delete admitFunc
Update admitFunc
Connect admitFunc
}
func webMutatePod(w http.ResponseWriter, r *http.Request) {
//https://github.com/douglasmakey/admissioncontroller
podsValidation := hook{
Create: validateCreate(),
}
admissionHandler := &struct {
decoder runtime.Decoder
}{
decoder: serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer(),
}
// read request body
body, err := io.ReadAll(r.Body)
if err != nil {
tmpltError(w, http.StatusBadRequest, "No data in request body.")
return
}
// see if request body can be decoded
var review admission.AdmissionReview
if _, _, err := admissionHandler.decoder.Decode(body, nil, &review); err != nil {
tmpltError(w, http.StatusBadRequest, "Unable to decode request body.")
return
}
var o *result
switch review.Request.Operation {
case admission.Create:
if podsValidation.Create == nil {
tmpltError(w, http.StatusBadRequest, fmt.Sprintf("operation %s is not registered", review.Request.Operation))
return
}
o, _ = podsValidation.Create(review.Request)
case admission.Update:
if podsValidation.Update == nil {
tmpltError(w, http.StatusBadRequest, fmt.Sprintf("operation %s is not registered", review.Request.Operation))
return
}
o, _ = podsValidation.Update(review.Request)
case admission.Delete:
if podsValidation.Delete == nil {
tmpltError(w, http.StatusBadRequest, fmt.Sprintf("operation %s is not registered", review.Request.Operation))
return
}
o, _ = podsValidation.Delete(review.Request)
case admission.Connect:
if podsValidation.Connect == nil {
tmpltError(w, http.StatusBadRequest, fmt.Sprintf("operation %s is not registered", review.Request.Operation))
return
}
o, _ = podsValidation.Connect(review.Request)
}
admissionResult := admission.AdmissionReview{
Response: &admission.AdmissionResponse{
UID: review.Request.UID,
Allowed: o.Allowed,
Result: &meta.Status{
Message: o.Msg,
},
},
}
resp, _ := json.Marshal(admissionResult)
w.WriteHeader(http.StatusOK)
w.Write(resp)
}
func validateCreate() admitFunc {
return func(r *admission.AdmissionRequest) (*result, error) {
pod, err := parsePod(r.Object.Raw)
if err != nil {
return &result{Msg: err.Error()}, nil
}
for _, c := range pod.Spec.Containers {
if strings.HasSuffix(c.Image, ":latest") {
return &result{Msg: "You cannot use the tag 'latest' in a container."}, nil
}
}
return &result{Allowed: true}, nil
}
}
func parsePod(object []byte) (*core.Pod, error) {
var pod core.Pod
if err := json.Unmarshal(object, &pod); err != nil {
return nil, err
}
return &pod, nil
}

View File

@ -0,0 +1,5 @@
package operations
func DeploymentsValidation() Hook {
return Hook{}
}

View File

@ -0,0 +1,51 @@
package operations
//https://github.com/douglasmakey/admissioncontroller
import (
"fmt"
admission "k8s.io/api/admission/v1"
)
// Result contains the result of an admission request
type Result struct {
Allowed bool
Msg string
PatchOps []PatchOperation
}
// AdmitFunc defines how to process an admission request
type AdmitFunc func(request *admission.AdmissionRequest) (*Result, error)
// Hook represents the set of functions for each operation in an admission webhook.
type Hook struct {
Create AdmitFunc
Delete AdmitFunc
Update AdmitFunc
Connect AdmitFunc
}
// Execute evaluates the request and try to execute the function for operation specified in the request.
func (h *Hook) Execute(r *admission.AdmissionRequest) (*Result, error) {
switch r.Operation {
case admission.Create:
return wrapperExecution(h.Create, r)
case admission.Update:
return wrapperExecution(h.Update, r)
case admission.Delete:
return wrapperExecution(h.Delete, r)
case admission.Connect:
return wrapperExecution(h.Connect, r)
}
return &Result{Msg: fmt.Sprintf("Invalid operation: %s", r.Operation)}, nil
}
func wrapperExecution(fn AdmitFunc, r *admission.AdmissionRequest) (*Result, error) {
if fn == nil {
return nil, fmt.Errorf("operation %s is not registered", r.Operation)
}
return fn(r)
}

View File

@ -1,4 +1,4 @@
package main
package operations
const (
addOperation = "add"
@ -8,15 +8,17 @@ const (
moveOperation = "move"
)
type patchOperation struct {
// PatchOperation is an operation of a JSON patch https://tools.ietf.org/html/rfc6902.
type PatchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
From string `json:"from"`
Value interface{} `json:"value,omitempty"`
}
func addPatchOperation(path string, value interface{}) patchOperation {
return patchOperation{
// AddPatchOperation returns an add JSON patch operation.
func AddPatchOperation(path string, value interface{}) PatchOperation {
return PatchOperation{
Op: addOperation,
Path: path,
Value: value,
@ -24,16 +26,16 @@ func addPatchOperation(path string, value interface{}) patchOperation {
}
// RemovePatchOperation returns a remove JSON patch operation.
func removePatchOperation(path string) patchOperation {
return patchOperation{
func RemovePatchOperation(path string) PatchOperation {
return PatchOperation{
Op: removeOperation,
Path: path,
}
}
// ReplacePatchOperation returns a replace JSON patch operation.
func replacePatchOperation(path string, value interface{}) patchOperation {
return patchOperation{
func ReplacePatchOperation(path string, value interface{}) PatchOperation {
return PatchOperation{
Op: replaceOperation,
Path: path,
Value: value,
@ -41,8 +43,8 @@ func replacePatchOperation(path string, value interface{}) patchOperation {
}
// CopyPatchOperation returns a copy JSON patch operation.
func copyPatchOperation(from, path string) patchOperation {
return patchOperation{
func CopyPatchOperation(from, path string) PatchOperation {
return PatchOperation{
Op: copyOperation,
Path: path,
From: from,
@ -50,10 +52,10 @@ func copyPatchOperation(from, path string) patchOperation {
}
// MovePatchOperation returns a move JSON patch operation.
func movePatchOperation(from, path string) patchOperation {
return patchOperation{
func MovePatchOperation(from, path string) PatchOperation {
return PatchOperation{
Op: moveOperation,
Path: path,
From: from,
}
}
}

View File

@ -0,0 +1,5 @@
package operations
func PodsMutation() Hook {
return Hook{}
}

View File

@ -0,0 +1,5 @@
package operations
func PodsValidation() Hook {
return Hook{}
}