more development
This commit is contained in:
44
cmd/webhook/config.go
Normal file
44
cmd/webhook/config.go
Normal 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
72
cmd/webhook/httpServer.go
Normal 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)
|
||||
}
|
||||
}
|
71
cmd/webhook/httpServerTemplates.go
Normal file
71
cmd/webhook/httpServerTemplates.go
Normal 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
115
cmd/webhook/init.go
Normal 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")
|
||||
}
|
@ -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
188
cmd/webhook/mutate.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user