2019-05-13 18:33:01 +00:00
|
|
|
package webhooks
|
2019-02-07 17:22:04 +00:00
|
|
|
|
|
|
|
import (
|
2019-03-04 18:40:02 +00:00
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
2019-05-30 19:28:56 +00:00
|
|
|
"github.com/golang/glog"
|
2019-05-22 17:29:10 +00:00
|
|
|
"github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
|
|
|
"github.com/nirmata/kyverno/pkg/config"
|
2019-05-29 21:12:09 +00:00
|
|
|
client "github.com/nirmata/kyverno/pkg/dclient"
|
2019-05-22 17:29:10 +00:00
|
|
|
engine "github.com/nirmata/kyverno/pkg/engine"
|
2019-06-05 10:43:07 +00:00
|
|
|
"github.com/nirmata/kyverno/pkg/result"
|
2019-05-22 17:29:10 +00:00
|
|
|
"github.com/nirmata/kyverno/pkg/sharedinformer"
|
|
|
|
tlsutils "github.com/nirmata/kyverno/pkg/tls"
|
2019-03-04 18:40:02 +00:00
|
|
|
v1beta1 "k8s.io/api/admission/v1beta1"
|
2019-05-20 12:41:23 +00:00
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
2019-05-13 18:27:47 +00:00
|
|
|
"k8s.io/apimachinery/pkg/labels"
|
2019-02-07 17:22:04 +00:00
|
|
|
)
|
|
|
|
|
2019-03-04 18:40:02 +00:00
|
|
|
// WebhookServer contains configured TLS server with MutationWebhook.
|
|
|
|
// MutationWebhook gets policies from policyController and takes control of the cluster with kubeclient.
|
2019-02-07 17:22:04 +00:00
|
|
|
type WebhookServer struct {
|
2019-05-13 18:27:47 +00:00
|
|
|
server http.Server
|
2019-05-20 17:56:12 +00:00
|
|
|
client *client.Client
|
2019-05-15 19:29:09 +00:00
|
|
|
policyLister v1alpha1.PolicyLister
|
2019-03-04 18:40:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewWebhookServer creates new instance of WebhookServer accordingly to given configuration
|
|
|
|
// Policy Controller and Kubernetes Client should be initialized in configuration
|
2019-05-13 18:27:47 +00:00
|
|
|
func NewWebhookServer(
|
2019-05-20 17:56:12 +00:00
|
|
|
client *client.Client,
|
2019-05-14 15:10:25 +00:00
|
|
|
tlsPair *tlsutils.TlsPemPair,
|
2019-05-30 19:28:56 +00:00
|
|
|
shareInformer sharedinformer.PolicyInformer) (*WebhookServer, error) {
|
2019-03-04 18:40:02 +00:00
|
|
|
|
2019-05-13 18:27:47 +00:00
|
|
|
if tlsPair == nil {
|
2019-03-22 20:11:55 +00:00
|
|
|
return nil, errors.New("NewWebhookServer is not initialized properly")
|
2019-03-04 18:40:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var tlsConfig tls.Config
|
2019-03-22 20:11:55 +00:00
|
|
|
pair, err := tls.X509KeyPair(tlsPair.Certificate, tlsPair.PrivateKey)
|
2019-03-04 18:40:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
tlsConfig.Certificates = []tls.Certificate{pair}
|
|
|
|
|
|
|
|
ws := &WebhookServer{
|
2019-05-20 17:56:12 +00:00
|
|
|
client: client,
|
2019-05-15 19:29:09 +00:00
|
|
|
policyLister: shareInformer.GetLister(),
|
2019-03-04 18:40:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
mux := http.NewServeMux()
|
2019-05-14 15:10:25 +00:00
|
|
|
mux.HandleFunc(config.MutatingWebhookServicePath, ws.serve)
|
|
|
|
mux.HandleFunc(config.ValidatingWebhookServicePath, ws.serve)
|
2019-03-04 18:40:02 +00:00
|
|
|
|
|
|
|
ws.server = http.Server{
|
|
|
|
Addr: ":443", // Listen on port for HTTPS requests
|
|
|
|
TLSConfig: &tlsConfig,
|
|
|
|
Handler: mux,
|
|
|
|
ReadTimeout: 15 * time.Second,
|
|
|
|
WriteTimeout: 15 * time.Second,
|
|
|
|
}
|
|
|
|
|
|
|
|
return ws, nil
|
2019-02-21 18:31:18 +00:00
|
|
|
}
|
|
|
|
|
2019-03-07 15:57:43 +00:00
|
|
|
// Main server endpoint for all requests
|
2019-02-07 17:22:04 +00:00
|
|
|
func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) {
|
2019-05-14 15:10:25 +00:00
|
|
|
admissionReview := ws.bodyToAdmissionReview(r, w)
|
|
|
|
if admissionReview == nil {
|
|
|
|
return
|
|
|
|
}
|
2019-03-04 18:40:02 +00:00
|
|
|
|
2019-05-14 15:10:25 +00:00
|
|
|
admissionReview.Response = &v1beta1.AdmissionResponse{
|
|
|
|
Allowed: true,
|
2019-03-04 18:40:02 +00:00
|
|
|
}
|
2019-05-22 07:16:22 +00:00
|
|
|
switch r.URL.Path {
|
|
|
|
case config.MutatingWebhookServicePath:
|
|
|
|
admissionReview.Response = ws.HandleMutation(admissionReview.Request)
|
|
|
|
case config.ValidatingWebhookServicePath:
|
|
|
|
admissionReview.Response = ws.HandleValidation(admissionReview.Request)
|
2019-03-04 18:40:02 +00:00
|
|
|
}
|
|
|
|
|
2019-05-14 15:10:25 +00:00
|
|
|
admissionReview.Response.UID = admissionReview.Request.UID
|
|
|
|
|
|
|
|
responseJson, err := json.Marshal(admissionReview)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, fmt.Sprintf("Could not encode response: %v", err), http.StatusInternalServerError)
|
|
|
|
return
|
2019-03-04 18:40:02 +00:00
|
|
|
}
|
|
|
|
|
2019-05-14 15:10:25 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
if _, err := w.Write(responseJson); err != nil {
|
|
|
|
http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
|
2019-03-04 18:40:02 +00:00
|
|
|
}
|
2019-02-15 18:00:49 +00:00
|
|
|
}
|
|
|
|
|
2019-05-14 15:10:25 +00:00
|
|
|
// RunAsync TLS server in separate thread and returns control immediately
|
2019-02-07 17:22:04 +00:00
|
|
|
func (ws *WebhookServer) RunAsync() {
|
2019-03-04 18:40:02 +00:00
|
|
|
go func(ws *WebhookServer) {
|
|
|
|
err := ws.server.ListenAndServeTLS("", "")
|
|
|
|
if err != nil {
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Fatal(err)
|
2019-03-04 18:40:02 +00:00
|
|
|
}
|
|
|
|
}(ws)
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Info("Started Webhook Server")
|
2019-02-07 17:22:04 +00:00
|
|
|
}
|
|
|
|
|
2019-05-14 15:10:25 +00:00
|
|
|
// Stop TLS server and returns control after the server is shut down
|
2019-02-07 17:22:04 +00:00
|
|
|
func (ws *WebhookServer) Stop() {
|
2019-03-04 18:40:02 +00:00
|
|
|
err := ws.server.Shutdown(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
// Error from closing listeners, or context timeout:
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Info("Server Shutdown error: %v", err)
|
2019-03-04 18:40:02 +00:00
|
|
|
ws.server.Close()
|
|
|
|
}
|
2019-02-07 17:22:04 +00:00
|
|
|
}
|
2019-05-13 18:27:47 +00:00
|
|
|
|
2019-05-14 15:10:25 +00:00
|
|
|
// HandleMutation handles mutating webhook admission request
|
|
|
|
func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
|
2019-05-14 15:10:25 +00:00
|
|
|
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation)
|
2019-05-13 18:27:47 +00:00
|
|
|
|
2019-05-14 15:10:25 +00:00
|
|
|
policies, err := ws.policyLister.List(labels.NewSelector())
|
2019-05-13 18:27:47 +00:00
|
|
|
if err != nil {
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Warning(err)
|
2019-05-13 18:27:47 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-06-05 10:43:07 +00:00
|
|
|
admissionResult := result.NewAdmissionResult(string(request.UID))
|
2019-05-21 15:27:56 +00:00
|
|
|
var allPatches []engine.PatchBytes
|
2019-05-13 18:27:47 +00:00
|
|
|
for _, policy := range policies {
|
2019-05-30 19:28:56 +00:00
|
|
|
|
|
|
|
glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules))
|
2019-05-13 18:27:47 +00:00
|
|
|
|
2019-06-05 10:43:07 +00:00
|
|
|
policyPatches, mutationResult := engine.Mutate(*policy, request.Object.Raw, request.Kind)
|
2019-05-13 18:27:47 +00:00
|
|
|
allPatches = append(allPatches, policyPatches...)
|
2019-06-05 10:43:07 +00:00
|
|
|
admissionResult = result.Append(admissionResult, mutationResult)
|
|
|
|
|
|
|
|
if mutationError := mutationResult.ToError(); mutationError != nil {
|
|
|
|
glog.Warningf(mutationError.Error())
|
|
|
|
}
|
2019-05-13 18:27:47 +00:00
|
|
|
|
|
|
|
if len(policyPatches) > 0 {
|
2019-05-15 11:25:32 +00:00
|
|
|
namespace := engine.ParseNamespaceFromObject(request.Object.Raw)
|
|
|
|
name := engine.ParseNameFromObject(request.Object.Raw)
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Infof("Mutation from policy %s has applied to %s %s/%s", policy.Name, request.Kind.Kind, namespace, name)
|
2019-05-13 18:27:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-05 12:02:58 +00:00
|
|
|
message := "\n" + admissionResult.String()
|
2019-06-05 10:43:07 +00:00
|
|
|
glog.Info(message)
|
|
|
|
|
|
|
|
if admissionResult.GetReason() == result.Success {
|
|
|
|
patchType := v1beta1.PatchTypeJSONPatch
|
|
|
|
return &v1beta1.AdmissionResponse{
|
|
|
|
Allowed: true,
|
|
|
|
Patch: engine.JoinPatches(allPatches),
|
|
|
|
PatchType: &patchType,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return &v1beta1.AdmissionResponse{
|
|
|
|
Allowed: false,
|
|
|
|
Result: &metav1.Status{
|
|
|
|
Message: message,
|
|
|
|
},
|
|
|
|
}
|
2019-05-13 18:27:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-14 15:10:25 +00:00
|
|
|
// HandleValidation handles validating webhook admission request
|
|
|
|
func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
|
2019-05-14 15:10:25 +00:00
|
|
|
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation)
|
|
|
|
|
|
|
|
policies, err := ws.policyLister.List(labels.NewSelector())
|
2019-05-13 18:27:47 +00:00
|
|
|
if err != nil {
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Warning(err)
|
2019-05-14 15:10:25 +00:00
|
|
|
return nil
|
2019-05-13 18:27:47 +00:00
|
|
|
}
|
|
|
|
|
2019-06-05 10:43:07 +00:00
|
|
|
admissionResult := result.NewAdmissionResult(string(request.UID))
|
2019-05-14 15:10:25 +00:00
|
|
|
for _, policy := range policies {
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Infof("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules))
|
2019-06-05 10:43:07 +00:00
|
|
|
validationResult := engine.Validate(*policy, request.Object.Raw, request.Kind)
|
|
|
|
admissionResult = result.Append(admissionResult, validationResult)
|
2019-05-14 15:10:25 +00:00
|
|
|
|
2019-06-05 10:43:07 +00:00
|
|
|
if validationError := validationResult.ToError(); validationError != nil {
|
|
|
|
glog.Warningf(validationError.Error())
|
|
|
|
}
|
|
|
|
}
|
2019-05-20 12:41:23 +00:00
|
|
|
|
2019-06-05 12:02:58 +00:00
|
|
|
message := "\n" + admissionResult.String()
|
2019-06-05 10:43:07 +00:00
|
|
|
glog.Info(message)
|
|
|
|
|
|
|
|
// Generation loop after all validation succeeded
|
|
|
|
var response *v1beta1.AdmissionResponse
|
|
|
|
|
|
|
|
if admissionResult.GetReason() == result.Success {
|
|
|
|
for _, policy := range policies {
|
|
|
|
engine.Generate(ws.client, *policy, request.Object.Raw, request.Kind)
|
2019-05-14 15:10:25 +00:00
|
|
|
}
|
2019-06-05 10:43:07 +00:00
|
|
|
glog.Info("Validation is successful")
|
2019-05-16 21:09:02 +00:00
|
|
|
|
2019-06-05 10:43:07 +00:00
|
|
|
response = &v1beta1.AdmissionResponse{
|
|
|
|
Allowed: true,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
response = &v1beta1.AdmissionResponse{
|
|
|
|
Allowed: false,
|
|
|
|
Result: &metav1.Status{
|
|
|
|
Message: message,
|
|
|
|
},
|
|
|
|
}
|
2019-05-13 18:27:47 +00:00
|
|
|
}
|
2019-05-20 12:41:23 +00:00
|
|
|
|
2019-06-05 10:43:07 +00:00
|
|
|
return response
|
2019-05-14 15:10:25 +00:00
|
|
|
}
|
2019-05-13 18:27:47 +00:00
|
|
|
|
2019-05-14 15:10:25 +00:00
|
|
|
// bodyToAdmissionReview creates AdmissionReview object from request body
|
|
|
|
// Answers to the http.ResponseWriter if request is not valid
|
|
|
|
func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer http.ResponseWriter) *v1beta1.AdmissionReview {
|
|
|
|
var body []byte
|
|
|
|
if request.Body != nil {
|
|
|
|
if data, err := ioutil.ReadAll(request.Body); err == nil {
|
|
|
|
body = data
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(body) == 0 {
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Error("Error: empty body")
|
2019-05-14 15:10:25 +00:00
|
|
|
http.Error(writer, "empty body", http.StatusBadRequest)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
contentType := request.Header.Get("Content-Type")
|
|
|
|
if contentType != "application/json" {
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Error("Error: invalid Content-Type: %v", contentType)
|
2019-05-14 15:10:25 +00:00
|
|
|
http.Error(writer, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
admissionReview := &v1beta1.AdmissionReview{}
|
|
|
|
if err := json.Unmarshal(body, &admissionReview); err != nil {
|
2019-05-30 19:28:56 +00:00
|
|
|
glog.Errorf("Error: Can't decode body as AdmissionReview: %v", err)
|
2019-05-14 15:10:25 +00:00
|
|
|
http.Error(writer, "Can't decode body as AdmissionReview", http.StatusExpectationFailed)
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return admissionReview
|
|
|
|
}
|
2019-05-13 18:27:47 +00:00
|
|
|
}
|