more development

This commit is contained in:
2022-09-12 14:48:56 -05:00
parent 23f7f78e35
commit 4db753e161
8 changed files with 632 additions and 2 deletions

44
cmd/webhook/config.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"os"
"time"
"github.com/hashicorp/logutils"
)
type configStructure struct {
// time configuration
TimeFormat string
TimeZoneLocal *time.Location
TimeZoneUTC *time.Location
// logging
LogLevel int
Log *logutils.LevelFilter
// webserver
WebSrvPort int
WebSrvIP string
WebSrvReadTimeout int
WebSrvWriteTimeout int
WebSrvIdleTimeout int
}
type patchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value,omitempty"`
}
// Set Defaults
var config = configStructure{
TimeFormat: "2006-01-02 15:04:05",
Log: &logutils.LevelFilter{
Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARNING", "ERROR"},
Writer: os.Stderr,
},
WebSrvReadTimeout: 5,
WebSrvWriteTimeout: 10,
WebSrvIdleTimeout: 2,
}

72
cmd/webhook/httpServer.go Normal file
View File

@ -0,0 +1,72 @@
package main
import (
"log"
"net/http"
"strconv"
"strings"
"time"
)
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 httpServer(host string, port int) {
path := http.NewServeMux()
connection := &http.Server{
Addr: host + ":" + strconv.FormatInt(int64(port), 10),
Handler: path,
ReadTimeout: time.Duration(config.WebSrvReadTimeout) * time.Second,
WriteTimeout: time.Duration(config.WebSrvWriteTimeout) * time.Second,
IdleTimeout: time.Duration(config.WebSrvIdleTimeout) * time.Second,
}
// healthcheck
path.HandleFunc("/healthcheck", webHealthCheck)
// api-endpoint
path.HandleFunc("/api/v1/mutate", webMutatePod)
// web root
path.HandleFunc("/", webRoot)
if err := connection.ListenAndServe(); err != nil {
log.Fatalf("[ERROR] %s\n", err)
}
}
func webRoot(w http.ResponseWriter, r *http.Request) {
httpAccessLog(r)
crossSiteOrigin(w)
switch {
case strings.ToLower(r.Method) != "get":
log.Printf("[DEBUG] Request to '/' was made using the wrong method: expected %s, got %s", "GET", strings.ToUpper(r.Method))
tmpltError(w, http.StatusBadRequest, InvalidMethod)
case r.URL.Path != "/":
log.Printf("[DEBUG] Unable to locate requested path: '%s'", r.URL.Path)
tmpltError(w, http.StatusNotFound, "Requested path not found.")
default:
tmpltWebRoot(w)
}
}
func webHealthCheck(w http.ResponseWriter, r *http.Request) {
httpAccessLog(r)
crossSiteOrigin(w)
if strings.ToLower(r.Method) == "get" {
tmpltHealthCheck(w)
} else {
log.Printf("[DEBUG] Request to '/healthcheck' was made using the wrong method: expected %s, got %s", "GET", strings.ToUpper(r.Method))
tmpltError(w, http.StatusBadRequest, InvalidMethod)
}
}

View File

@ -0,0 +1,71 @@
package main
import (
"encoding/json"
"log"
"net/http"
)
const cT string = "Content-Type"
const cTjson string = "application/json"
const marshalErrorMsg string = "[TRACE] Unable to marshal error message: %v."
func tmpltError(w http.ResponseWriter, s int, m string) {
var (
output []byte
o = struct {
Error int `json:"error" yaml:"error"`
ErrorMsg string `json:"errorMessage" yaml:"errorMessage"`
}{
Error: s,
ErrorMsg: m,
}
err error
)
w.Header().Add(cT, cTjson)
output, err = json.MarshalIndent(o, "", " ")
if err != nil {
log.Printf(marshalErrorMsg, err)
}
w.WriteHeader(s)
w.Write(output) //nolint:errcheck
}
func tmpltHealthCheck(w http.ResponseWriter) {
o := struct {
WebServer bool `json:"webServerActive" yaml:"webServerActive"`
Status string `json:"status" yaml:"status"`
}{
WebServer: true,
Status: "healthy",
}
output, err := json.MarshalIndent(o, "", " ")
if err != nil {
log.Printf(marshalErrorMsg, err)
}
w.Header().Add(cT, cTjson)
w.Write(output) //nolint:errcheck
}
func tmpltWebRoot(w http.ResponseWriter) {
o := struct {
Application string `json:"application" yaml:"application"`
Description string `json:"description" yaml:"description"`
Version string `json:"version" yaml:"version"`
}{
Application: "Mutating-Webhook API",
Description: "Mutating Webhook for Simple Sidecar Injection",
Version: "v1.0.0",
}
w.Header().Add(cT, cTjson)
output, err := json.MarshalIndent(o, "", " ")
if err != nil {
log.Printf(marshalErrorMsg, err)
}
w.Write(output) //nolint:errcheck
}

115
cmd/webhook/init.go Normal file
View File

@ -0,0 +1,115 @@
package main
import (
"flag"
"log"
"os"
"strconv"
"time"
"github.com/hashicorp/logutils"
)
// getEnvString returns string from environment variable
func getEnvString(env, def string) (val string) { //nolint:deadcode
val = os.Getenv(env)
if val == "" {
return def
}
return
}
// getEnvInt returns int from environment variable
func getEnvInt(env string, def int) (ret int) {
val := os.Getenv(env)
if val == "" {
return def
}
ret, err := strconv.Atoi(val)
if err != nil {
log.Fatalf("[ERROR] Environment variable is not numeric: %v\n", env)
}
return
}
func setLogLevel(l int) {
switch {
case l <= 20:
config.Log.SetMinLevel(logutils.LogLevel("ERROR"))
case l > 20 && l <= 40:
config.Log.SetMinLevel(logutils.LogLevel("WARNING"))
case l > 40 && l <= 60:
config.Log.SetMinLevel(logutils.LogLevel("INFO"))
case l > 60 && l <= 80:
config.Log.SetMinLevel(logutils.LogLevel("DEBUG"))
case l > 80:
config.Log.SetMinLevel(logutils.LogLevel("TRACE"))
}
}
func initialize() {
var (
tz string
err error
)
// log configuration
flag.IntVar(&config.LogLevel,
"log",
getEnvInt("LOG_LEVEL", 50),
"(LOG_LEVEL)\nlog level")
// local webserver configuration
flag.IntVar(&config.WebSrvPort,
"http-port",
getEnvInt("HTTP_PORT", 8080),
"(HTTP_PORT)\nlisten port for internal webserver")
flag.StringVar(&config.WebSrvIP,
"http-ip",
getEnvString("HTTP_IP", ""),
"(HTTP_IP)\nlisten ip for internal webserver")
flag.IntVar(&config.WebSrvReadTimeout,
"http-read-timeout",
getEnvInt("HTTP_READ_TIMEOUT", 5),
"(HTTP_READ_TIMEOUT)\ninternal http server read timeout in seconds")
flag.IntVar(&config.WebSrvWriteTimeout,
"http-write-timeout",
getEnvInt("HTTP_WRITE_TIMEOUT", 2),
"(HTTP_WRITE_TIMEOUT\ninternal http server write timeout in seconds")
flag.IntVar(&config.WebSrvIdleTimeout,
"http-idle-timeout",
getEnvInt("HTTP_IDLE_TIMEOUT", 2),
"(HTTP_IDLE_TIMEOUT)\ninternal http server idle timeout in seconds")
// timezone
flag.StringVar(&tz,
"timezone",
getEnvString("TZ", "America/Chicago"),
"(TZ)\ntimezone")
// read command line options
flag.Parse()
// logging level
setLogLevel(config.LogLevel)
log.SetOutput(config.Log)
// timezone configuration
config.TimeZoneUTC, _ = time.LoadLocation("UTC")
if config.TimeZoneLocal, err = time.LoadLocation(tz); err != nil {
log.Fatalf("[ERROR] Unable to parse timezone string. Please use one of the timezone database values listed here: %s", "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones")
}
// print current configuration
log.Printf("[DEBUG] configuration value set: LOG_LEVEL = %s\n", strconv.Itoa(config.LogLevel))
log.Printf("[DEBUG] configuration value set: HTTP_PORT = %s\n", strconv.Itoa(config.WebSrvPort))
log.Printf("[DEBUG] configuration value set: HTTP_IP = %s\n", config.WebSrvIP)
log.Printf("[DEBUG] configuration value set: HTTP_READ_TIMEOUT = %s\n", strconv.Itoa(config.WebSrvReadTimeout))
log.Printf("[DEBUG] configuration value set: HTTP_WRITE_TIMEOUT = %s\n", strconv.Itoa(config.WebSrvWriteTimeout))
log.Printf("[DEBUG] configuration value set: HTTP_IDLE_TIMEOUT = %s\n", strconv.Itoa(config.WebSrvIdleTimeout))
log.Printf("[DEBUG] configuration value set: TZ = %s\n", tz)
log.Println("[INFO] initialization complete")
}

View File

@ -1 +1,28 @@
package main
package main
import (
"log"
"os"
"os/signal"
"syscall"
)
func forever() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
sig := <-c
log.Printf("[INFO] shutting down, detected signal: %s", sig)
}
func main() {
defer func() {
log.Println("[DEBUG] shutdown sequence complete")
}()
initialize()
go httpServer(config.WebSrvIP, config.WebSrvPort)
forever()
}

188
cmd/webhook/mutate.go Normal file
View File

@ -0,0 +1,188 @@
package main
import (
"fmt"
"log"
"strings"
"encoding/json"
"io/ioutil"
"net/http"
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"
)
var (
codecs = serializer.NewCodecFactory(runtime.NewScheme())
)
func admissionReviewFromRequest(r *http.Request, deserializer runtime.Decoder) (*admission.AdmissionReview, error) {
// Validate that the incoming content type is correct.
if r.Header.Get("Content-Type") != "application/json" {
return nil, fmt.Errorf("expected application/json content-type")
}
// Get the body data, which will be the AdmissionReview
// content for the request.
var body []byte
if r.Body != nil {
requestData, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
body = requestData
}
// Decode the request body into
admissionReviewRequest := &admission.AdmissionReview{}
if _, _, err := deserializer.Decode(body, nil, admissionReviewRequest); err != nil {
return nil, err
}
return admissionReviewRequest, nil
}
func webMutatePod(w http.ResponseWriter, r *http.Request) {
httpAccessLog(r)
deserializer := codecs.UniversalDeserializer()
// Parse the AdmissionReview from the http request.
admissionReviewRequest, err := admissionReviewFromRequest(r, deserializer)
if err != nil {
msg := fmt.Sprintf("error getting admission review from request: %v", err)
log.Printf("[ERROR] %v", msg)
tmpltError(w, http.StatusBadRequest, msg)
return
}
// Do server-side validation that we are only dealing with a pod resource. This
// should also be part of the MutatingWebhookConfiguration in the cluster, but
// we should verify here before continuing.
podResource := meta.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
if admissionReviewRequest.Request.Resource != podResource {
msg := fmt.Sprintf("did not receive pod, got %s", admissionReviewRequest.Request.Resource.Resource)
log.Printf("[ERROR] %v", msg)
tmpltError(w, http.StatusBadRequest, msg)
return
}
// Decode the pod from the AdmissionReview.
rawRequest := admissionReviewRequest.Request.Object.Raw
pod := core.Pod{}
if _, _, err := deserializer.Decode(rawRequest, nil, &pod); err != nil {
msg := fmt.Sprintf("error decoding raw pod: %v", err)
log.Printf("[ERROR] %v", msg)
tmpltError(w, http.StatusBadRequest, msg)
return
}
// check to see if mutation is required by looking for a label
if !mutationRequired(&pod.ObjectMeta) {
mutationResp(w, admissionReviewRequest, &admission.AdmissionResponse{Allowed: true})
}
// Add sidecar
sidecarContainer := []core.Container{{
Image: "ca-cert-server:latest",
}}
patchBytes, _ := createPatch(&pod, sidecarContainer)
// respond with patch
mutationResp(w, admissionReviewRequest, &admission.AdmissionResponse{
Allowed: true,
Patch: patchBytes,
PatchType: func() *admission.PatchType {
pt := admission.PatchTypeJSONPatch
return &pt
}(),
})
}
// prepare response
func mutationResp(w http.ResponseWriter, aRRequest *admission.AdmissionReview, aResponse *admission.AdmissionResponse) {
var aRResponse admission.AdmissionReview
aRResponse.Response = aResponse
aRResponse.SetGroupVersionKind(aRRequest.GroupVersionKind())
aRResponse.Response.UID = aRRequest.Request.UID
resp, err := json.Marshal(aRResponse)
if err != nil {
msg := fmt.Sprintf("error marshalling response json: %v", err)
log.Printf("[ERROR] %v", msg)
tmpltError(w, http.StatusBadRequest, msg)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(resp)
}
// create mutation patch for resources
func createPatch(pod *core.Pod, containers []core.Container) ([]byte, error) {
var (
patch []patchOperation
first bool
value interface{}
)
if len(pod.Spec.Containers) == 0 {
first = true
}
for _, add := range containers {
value = add
path := "/spec/containers"
if first {
first = false
value = []core.Container{add}
} else {
path = path + "/-"
}
patch = append(patch, patchOperation{
Op: "add",
Path: path,
Value: value,
})
}
return json.Marshal(patch)
}
// Check whether the target resourse needs to be mutated
func mutationRequired(metadata *meta.ObjectMeta) bool {
var ignoredNamespaces = []string{
meta.NamespaceSystem,
meta.NamespacePublic,
}
// skip special kubernetes system namespaces
for _, namespace := range ignoredNamespaces {
if metadata.Namespace == namespace {
log.Printf("[TRACE] Skip mutation for %v for it's in special namespace:%v", metadata.Name, metadata.Namespace)
return false
}
}
annotations := metadata.GetLabels()
if annotations == nil {
annotations = map[string]string{}
}
// determine whether to perform mutation based on annotation for the target resource
var required bool
switch strings.ToLower(annotations["sidecar-injector-webhook/inject"]) {
case "yes", "y", "true", "t", "on":
required = true
default:
required = false
}
log.Printf("[TRACE] Mutation policy for %v/%v: required:%v", metadata.Namespace, metadata.Name, required)
return required
}