mirror of
https://github.com/kyverno/kyverno.git
synced 2024-12-14 11:57:48 +00:00
NK-22: Fixed build error with Selector pointer. Added comments. Changed tab to 4 spaces identation. Added unit tests for LabelSelector.
This commit is contained in:
parent
d0de77f9ce
commit
d593fe1a92
13 changed files with 669 additions and 571 deletions
|
@ -1,111 +1,111 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
clientset "github.com/nirmata/kube-policy/pkg/client/clientset/versioned"
|
||||
informers "github.com/nirmata/kube-policy/pkg/client/informers/externalversions"
|
||||
lister "github.com/nirmata/kube-policy/pkg/client/listers/policy/v1alpha1"
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
clientset "github.com/nirmata/kube-policy/pkg/client/clientset/versioned"
|
||||
informers "github.com/nirmata/kube-policy/pkg/client/informers/externalversions"
|
||||
lister "github.com/nirmata/kube-policy/pkg/client/listers/policy/v1alpha1"
|
||||
)
|
||||
|
||||
// PolicyController for CRD
|
||||
type PolicyController struct {
|
||||
policyInformerFactory informers.SharedInformerFactory
|
||||
policyLister lister.PolicyLister
|
||||
logger *log.Logger
|
||||
policyInformerFactory informers.SharedInformerFactory
|
||||
policyLister lister.PolicyLister
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewPolicyController from cmd args
|
||||
func NewPolicyController(masterURL, kubeconfigPath string, logger *log.Logger) (*PolicyController, error) {
|
||||
if logger == nil {
|
||||
logger = log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile)
|
||||
}
|
||||
if logger == nil {
|
||||
logger = log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile)
|
||||
}
|
||||
|
||||
cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfigPath)
|
||||
if err != nil {
|
||||
logger.Printf("Error building kubeconfig: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfigPath)
|
||||
if err != nil {
|
||||
logger.Printf("Error building kubeconfig: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policyClientset, err := clientset.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
logger.Printf("Error building policy clientset: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
policyClientset, err := clientset.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
logger.Printf("Error building policy clientset: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policyInformerFactory := informers.NewSharedInformerFactory(policyClientset, time.Second*30)
|
||||
policyInformer := policyInformerFactory.Nirmata().V1alpha1().Policies()
|
||||
policyInformerFactory := informers.NewSharedInformerFactory(policyClientset, time.Second*30)
|
||||
policyInformer := policyInformerFactory.Nirmata().V1alpha1().Policies()
|
||||
|
||||
controller := &PolicyController{
|
||||
policyInformerFactory: policyInformerFactory,
|
||||
policyLister: policyInformer.Lister(),
|
||||
logger: logger,
|
||||
}
|
||||
controller := &PolicyController{
|
||||
policyInformerFactory: policyInformerFactory,
|
||||
policyLister: policyInformer.Lister(),
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
policyInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: controller.createPolicyHandler,
|
||||
UpdateFunc: controller.updatePolicyHandler,
|
||||
DeleteFunc: controller.deletePolicyHandler,
|
||||
})
|
||||
policyInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: controller.createPolicyHandler,
|
||||
UpdateFunc: controller.updatePolicyHandler,
|
||||
DeleteFunc: controller.deletePolicyHandler,
|
||||
})
|
||||
|
||||
return controller, nil
|
||||
return controller, nil
|
||||
}
|
||||
|
||||
// Run is main controller thread
|
||||
func (c *PolicyController) Run(stopCh <-chan struct{}) {
|
||||
c.policyInformerFactory.Start(stopCh)
|
||||
c.policyInformerFactory.Start(stopCh)
|
||||
}
|
||||
|
||||
// GetPolicies retrieves all policy resources
|
||||
// from cache. Cache is refreshed by informer
|
||||
func (c *PolicyController) GetPolicies() []types.Policy {
|
||||
// Create nil Selector to grab all the policies
|
||||
selector := labels.NewSelector()
|
||||
cachedPolicies, err := c.policyLister.List(selector)
|
||||
// Create nil Selector to grab all the policies
|
||||
selector := labels.NewSelector()
|
||||
cachedPolicies, err := c.policyLister.List(selector)
|
||||
|
||||
if err != nil {
|
||||
c.logger.Printf("Error: %v", err)
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
c.logger.Printf("Error: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var policies []types.Policy
|
||||
for _, elem := range cachedPolicies {
|
||||
policies = append(policies, *elem.DeepCopy())
|
||||
}
|
||||
var policies []types.Policy
|
||||
for _, elem := range cachedPolicies {
|
||||
policies = append(policies, *elem.DeepCopy())
|
||||
}
|
||||
|
||||
return policies
|
||||
return policies
|
||||
}
|
||||
|
||||
func (c *PolicyController) createPolicyHandler(resource interface{}) {
|
||||
key := c.getResourceKey(resource)
|
||||
c.logger.Printf("Created policy: %s\n", key)
|
||||
key := c.getResourceKey(resource)
|
||||
c.logger.Printf("Created policy: %s\n", key)
|
||||
}
|
||||
|
||||
func (c *PolicyController) updatePolicyHandler(oldResource, newResource interface{}) {
|
||||
oldKey := c.getResourceKey(oldResource)
|
||||
newKey := c.getResourceKey(newResource)
|
||||
oldKey := c.getResourceKey(oldResource)
|
||||
newKey := c.getResourceKey(newResource)
|
||||
|
||||
c.logger.Printf("Updated policy from %s to %s\n", oldKey, newKey)
|
||||
c.logger.Printf("Updated policy from %s to %s\n", oldKey, newKey)
|
||||
}
|
||||
|
||||
func (c *PolicyController) deletePolicyHandler(resource interface{}) {
|
||||
key := c.getResourceKey(resource)
|
||||
c.logger.Printf("Deleted policy: %s\n", key)
|
||||
key := c.getResourceKey(resource)
|
||||
c.logger.Printf("Deleted policy: %s\n", key)
|
||||
}
|
||||
|
||||
func (c *PolicyController) getResourceKey(resource interface{}) string {
|
||||
if key, err := cache.MetaNamespaceKeyFunc(resource); err != nil {
|
||||
c.logger.Fatalf("Error retrieving policy key: %v\n", err)
|
||||
} else {
|
||||
return key
|
||||
}
|
||||
if key, err := cache.MetaNamespaceKeyFunc(resource); err != nil {
|
||||
c.logger.Fatalf("Error retrieving policy key: %v\n", err)
|
||||
} else {
|
||||
return key
|
||||
}
|
||||
|
||||
return ""
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
apiVersion: nirmata.io/v1alpha1
|
||||
apiVersion: policy.nirmata.io/v1alpha1
|
||||
kind: Policy
|
||||
metadata:
|
||||
name: hello-policy
|
||||
name: hello-policy
|
||||
spec:
|
||||
policySpec: 'hi'
|
||||
image: hello-policy-image
|
||||
policySpec: 'hi'
|
||||
image: hello-policy-image
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
apiVersion: nirmata.io/v1alpha1
|
||||
apiVersion: policy.nirmata.io/v1alpha1
|
||||
kind : Policy
|
||||
metadata:
|
||||
name: selector-policy
|
||||
|
|
74
main.go
74
main.go
|
@ -1,58 +1,58 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/nirmata/kube-policy/controller"
|
||||
"github.com/nirmata/kube-policy/server"
|
||||
"github.com/nirmata/kube-policy/controller"
|
||||
"github.com/nirmata/kube-policy/server"
|
||||
|
||||
"k8s.io/sample-controller/pkg/signals"
|
||||
"k8s.io/sample-controller/pkg/signals"
|
||||
)
|
||||
|
||||
var (
|
||||
masterURL string
|
||||
kubeconfig string
|
||||
cert string
|
||||
key string
|
||||
masterURL string
|
||||
kubeconfig string
|
||||
cert string
|
||||
key string
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
flag.Parse()
|
||||
|
||||
if cert == "" || key == "" {
|
||||
log.Fatal("TLS certificate or/and key is not set")
|
||||
}
|
||||
if cert == "" || key == "" {
|
||||
log.Fatal("TLS certificate or/and key is not set")
|
||||
}
|
||||
|
||||
crdcLogger := log.New(os.Stdout, "Policy Controller: ", log.LstdFlags|log.Lshortfile)
|
||||
controller, err := controller.NewPolicyController(masterURL, kubeconfig, crdcLogger)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating PolicyController! Error: %s\n", err)
|
||||
return
|
||||
}
|
||||
crdcLogger := log.New(os.Stdout, "Policy Controller: ", log.LstdFlags|log.Lshortfile)
|
||||
controller, err := controller.NewPolicyController(masterURL, kubeconfig, crdcLogger)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating PolicyController! Error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
httpLogger := log.New(os.Stdout, "HTTPS Server: ", log.LstdFlags|log.Lshortfile)
|
||||
server := server.NewWebhookServer(cert, key, controller, httpLogger)
|
||||
server.RunAsync()
|
||||
httpLogger := log.New(os.Stdout, "HTTPS Server: ", log.LstdFlags|log.Lshortfile)
|
||||
server := server.NewWebhookServer(cert, key, controller, httpLogger)
|
||||
server.RunAsync()
|
||||
|
||||
stopCh := signals.SetupSignalHandler()
|
||||
controller.Run(stopCh)
|
||||
stopCh := signals.SetupSignalHandler()
|
||||
controller.Run(stopCh)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error running PolicyController! Error: %s\n", err)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("Error running PolicyController! Error: %s\n", err)
|
||||
}
|
||||
|
||||
fmt.Println("Policy Controller has started")
|
||||
<-stopCh
|
||||
server.Stop()
|
||||
fmt.Println("Policy Controller has stopped")
|
||||
fmt.Println("Policy Controller has started")
|
||||
<-stopCh
|
||||
server.Stop()
|
||||
fmt.Println("Policy Controller has stopped")
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
|
||||
flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
|
||||
flag.StringVar(&cert, "cert", "", "TLS certificate used in connection with cluster.")
|
||||
flag.StringVar(&key, "key", "", "Key, used in TLS connection.")
|
||||
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
|
||||
flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
|
||||
flag.StringVar(&cert, "cert", "", "TLS certificate used in connection with cluster.")
|
||||
flag.StringVar(&key, "key", "", "Key, used in TLS connection.")
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
package policy
|
||||
|
||||
const (
|
||||
GroupName = "policy.nirmata.io"
|
||||
GroupName = "policy.nirmata.io"
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
|
||||
"github.com/nirmata/kube-policy/pkg/apis/policy"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +genclient
|
||||
|
@ -9,63 +9,63 @@ import (
|
|||
|
||||
// Policy is a specification for a Policy resource
|
||||
type Policy struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
Spec PolicySpec `json:"spec"`
|
||||
Status PolicyStatus `json:"status"`
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
Spec PolicySpec `json:"spec"`
|
||||
Status PolicyStatus `json:"status"`
|
||||
}
|
||||
|
||||
// PolicySpec is the spec for a Policy resource
|
||||
type PolicySpec struct {
|
||||
FailurePolicy *string `json:"failurePolicy"`
|
||||
Rules []PolicyRule `json:"rules"`
|
||||
FailurePolicy *string `json:"failurePolicy"`
|
||||
Rules []PolicyRule `json:"rules"`
|
||||
}
|
||||
|
||||
// PolicyRule is policy rule that will be applied to resource
|
||||
type PolicyRule struct {
|
||||
Resource PolicyResource `json:"resource"`
|
||||
Patches []PolicyPatch `json:"patch,omitempty"`
|
||||
ConfigMapGenerator *PolicyConfigGenerator `json:"configMapGenerator,omitempty"`
|
||||
SecretGenerator *PolicyConfigGenerator `json:"secretGenerator,omitempty"`
|
||||
Resource PolicyResource `json:"resource"`
|
||||
Patches []PolicyPatch `json:"patch,omitempty"`
|
||||
ConfigMapGenerator *PolicyConfigGenerator `json:"configMapGenerator,omitempty"`
|
||||
SecretGenerator *PolicyConfigGenerator `json:"secretGenerator,omitempty"`
|
||||
}
|
||||
|
||||
// PolicyResource describes the resource rule applied to
|
||||
type PolicyResource struct {
|
||||
Kind string `json:"kind"`
|
||||
Name *string `json:"name"`
|
||||
Selector metav1.LabelSelector `json:"selector,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
Name *string `json:"name"`
|
||||
Selector *metav1.LabelSelector `json:"selector,omitempty"`
|
||||
}
|
||||
|
||||
// PolicyPatch is TODO
|
||||
type PolicyPatch struct {
|
||||
Path string `json:"path"`
|
||||
Operation string `json:"op"`
|
||||
Value string `json:"value"`
|
||||
Path string `json:"path"`
|
||||
Operation string `json:"op"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// PolicyConfigGenerator is TODO
|
||||
type PolicyConfigGenerator struct {
|
||||
Name string `json:"name"`
|
||||
CopyFrom *PolicyCopyFrom `json:"copyFrom"`
|
||||
Data map[string]string `json:"data"`
|
||||
Name string `json:"name"`
|
||||
CopyFrom *PolicyCopyFrom `json:"copyFrom"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
// PolicyCopyFrom is TODO
|
||||
type PolicyCopyFrom struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// PolicyStatus is the status for a Policy resource
|
||||
type PolicyStatus struct {
|
||||
Logs []string `json:"log"`
|
||||
Logs []string `json:"log"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// PolicyList is a list of Policy resources
|
||||
type PolicyList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata"`
|
||||
Items []Policy `json:"items"`
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata"`
|
||||
Items []Policy `json:"items"`
|
||||
}
|
||||
|
|
230
server/server.go
230
server/server.go
|
@ -1,158 +1,158 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
controller "github.com/nirmata/kube-policy/controller"
|
||||
webhooks "github.com/nirmata/kube-policy/webhooks"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
controller "github.com/nirmata/kube-policy/controller"
|
||||
webhooks "github.com/nirmata/kube-policy/webhooks"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
// WebhookServer is a struct that describes
|
||||
// TLS server with mutation webhook
|
||||
type WebhookServer struct {
|
||||
server http.Server
|
||||
logger *log.Logger
|
||||
policyController *controller.PolicyController
|
||||
mutationWebhook *webhooks.MutationWebhook
|
||||
server http.Server
|
||||
logger *log.Logger
|
||||
policyController *controller.PolicyController
|
||||
mutationWebhook *webhooks.MutationWebhook
|
||||
}
|
||||
|
||||
// NewWebhookServer creates new instance of WebhookServer and configures it
|
||||
func NewWebhookServer(certFile string, keyFile string, controller *controller.PolicyController, logger *log.Logger) *WebhookServer {
|
||||
if logger == nil {
|
||||
logger = log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile)
|
||||
}
|
||||
if controller == nil {
|
||||
logger.Fatal("Controller is not specified for webhook server")
|
||||
}
|
||||
if logger == nil {
|
||||
logger = log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile)
|
||||
}
|
||||
if controller == nil {
|
||||
logger.Fatal("Controller is not specified for webhook server")
|
||||
}
|
||||
|
||||
var config tls.Config
|
||||
pair, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
logger.Fatal("Unable to load certificate and key: ", err)
|
||||
}
|
||||
config.Certificates = []tls.Certificate{pair}
|
||||
var config tls.Config
|
||||
pair, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
logger.Fatal("Unable to load certificate and key: ", err)
|
||||
}
|
||||
config.Certificates = []tls.Certificate{pair}
|
||||
|
||||
mw, err := webhooks.NewMutationWebhook(logger)
|
||||
if err != nil {
|
||||
logger.Fatal("Unable to create mutation webhook: ", err)
|
||||
}
|
||||
mw, err := webhooks.NewMutationWebhook(logger)
|
||||
if err != nil {
|
||||
logger.Fatal("Unable to create mutation webhook: ", err)
|
||||
}
|
||||
|
||||
ws := &WebhookServer{
|
||||
logger: logger,
|
||||
policyController: controller,
|
||||
mutationWebhook: mw,
|
||||
}
|
||||
ws := &WebhookServer{
|
||||
logger: logger,
|
||||
policyController: controller,
|
||||
mutationWebhook: mw,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/mutate", ws.serve)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/mutate", ws.serve)
|
||||
|
||||
ws.server = http.Server{
|
||||
Addr: ":443", // Listen on port for HTTPS requests
|
||||
TLSConfig: &config,
|
||||
Handler: mux,
|
||||
ErrorLog: logger,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
}
|
||||
ws.server = http.Server{
|
||||
Addr: ":443", // Listen on port for HTTPS requests
|
||||
TLSConfig: &config,
|
||||
Handler: mux,
|
||||
ErrorLog: logger,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
return ws
|
||||
return ws
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/mutate" {
|
||||
admissionReview := ws.parseAdmissionReview(r, w)
|
||||
if admissionReview == nil {
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/mutate" {
|
||||
admissionReview := ws.parseAdmissionReview(r, w)
|
||||
if admissionReview == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var admissionResponse *v1beta1.AdmissionResponse
|
||||
if webhooks.AdmissionIsRequired(admissionReview.Request) {
|
||||
admissionResponse = ws.mutationWebhook.Mutate(admissionReview.Request, ws.policyController.GetPolicies())
|
||||
}
|
||||
var admissionResponse *v1beta1.AdmissionResponse
|
||||
if webhooks.AdmissionIsRequired(admissionReview.Request) {
|
||||
admissionResponse = ws.mutationWebhook.Mutate(admissionReview.Request, ws.policyController.GetPolicies())
|
||||
}
|
||||
|
||||
if admissionResponse == nil {
|
||||
admissionResponse = &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
}
|
||||
}
|
||||
if admissionResponse == nil {
|
||||
admissionResponse = &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
}
|
||||
}
|
||||
|
||||
admissionReview.Response = admissionResponse
|
||||
admissionReview.Response.UID = admissionReview.Request.UID
|
||||
admissionReview.Response = admissionResponse
|
||||
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
|
||||
}
|
||||
responseJson, err := json.Marshal(admissionReview)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not encode response: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ws.logger.Printf("Response body\n:%v", string(responseJson))
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, fmt.Sprintf("Unexpected method path: %v", r.URL.Path), http.StatusNotFound)
|
||||
}
|
||||
ws.logger.Printf("Response body\n:%v", string(responseJson))
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, fmt.Sprintf("Unexpected method path: %v", r.URL.Path), http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
// Answers to the http.ResponseWriter if request is not valid
|
||||
func (ws *WebhookServer) parseAdmissionReview(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 {
|
||||
ws.logger.Print("Error: empty body")
|
||||
http.Error(writer, "empty body", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
var body []byte
|
||||
if request.Body != nil {
|
||||
if data, err := ioutil.ReadAll(request.Body); err == nil {
|
||||
body = data
|
||||
}
|
||||
}
|
||||
if len(body) == 0 {
|
||||
ws.logger.Print("Error: empty body")
|
||||
http.Error(writer, "empty body", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
contentType := request.Header.Get("Content-Type")
|
||||
if contentType != "application/json" {
|
||||
ws.logger.Printf("Error: invalid Content-Type: %v", contentType)
|
||||
http.Error(writer, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
|
||||
return nil
|
||||
}
|
||||
contentType := request.Header.Get("Content-Type")
|
||||
if contentType != "application/json" {
|
||||
ws.logger.Printf("Error: invalid Content-Type: %v", contentType)
|
||||
http.Error(writer, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
|
||||
return nil
|
||||
}
|
||||
|
||||
admissionReview := &v1beta1.AdmissionReview{}
|
||||
if err := json.Unmarshal(body, &admissionReview); err != nil {
|
||||
ws.logger.Printf("Error: Can't decode body as AdmissionReview: %v", err)
|
||||
http.Error(writer, "Can't decode body as AdmissionReview", http.StatusExpectationFailed)
|
||||
return nil
|
||||
} else {
|
||||
ws.logger.Printf("Request body:\n%v", string(body))
|
||||
return admissionReview
|
||||
}
|
||||
admissionReview := &v1beta1.AdmissionReview{}
|
||||
if err := json.Unmarshal(body, &admissionReview); err != nil {
|
||||
ws.logger.Printf("Error: Can't decode body as AdmissionReview: %v", err)
|
||||
http.Error(writer, "Can't decode body as AdmissionReview", http.StatusExpectationFailed)
|
||||
return nil
|
||||
} else {
|
||||
ws.logger.Printf("Request body:\n%v", string(body))
|
||||
return admissionReview
|
||||
}
|
||||
}
|
||||
|
||||
// RunAsync runs TLS server in separate
|
||||
// thread and returns control immediately
|
||||
func (ws *WebhookServer) RunAsync() {
|
||||
go func(ws *WebhookServer) {
|
||||
err := ws.server.ListenAndServeTLS("", "")
|
||||
if err != nil {
|
||||
ws.logger.Fatal(err)
|
||||
}
|
||||
}(ws)
|
||||
go func(ws *WebhookServer) {
|
||||
err := ws.server.ListenAndServeTLS("", "")
|
||||
if err != nil {
|
||||
ws.logger.Fatal(err)
|
||||
}
|
||||
}(ws)
|
||||
}
|
||||
|
||||
// Stop stops TLS server
|
||||
func (ws *WebhookServer) Stop() {
|
||||
err := ws.server.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
// Error from closing listeners, or context timeout:
|
||||
ws.logger.Printf("Server Shutdown error: %v", err)
|
||||
ws.server.Close()
|
||||
}
|
||||
err := ws.server.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
// Error from closing listeners, or context timeout:
|
||||
ws.logger.Printf("Server Shutdown error: %v", err)
|
||||
ws.server.Close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,109 +1,110 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
"encoding/json"
|
||||
"encoding/json"
|
||||
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
var supportedKinds = [...]string{
|
||||
"ConfigMap",
|
||||
"CronJob",
|
||||
"DaemonSet",
|
||||
"Deployment",
|
||||
"Endpoint",
|
||||
"HorizontalPodAutoscaler",
|
||||
"Ingress",
|
||||
"Job",
|
||||
"LimitRange",
|
||||
"Namespace",
|
||||
"NetworkPolicy",
|
||||
"PersistentVolumeClaim",
|
||||
"PodDisruptionBudget",
|
||||
"PodTemplate",
|
||||
"ResourceQuota",
|
||||
"Secret",
|
||||
"Service",
|
||||
"StatefulSet",
|
||||
"ConfigMap",
|
||||
"CronJob",
|
||||
"DaemonSet",
|
||||
"Deployment",
|
||||
"Endpoint",
|
||||
"HorizontalPodAutoscaler",
|
||||
"Ingress",
|
||||
"Job",
|
||||
"LimitRange",
|
||||
"Namespace",
|
||||
"NetworkPolicy",
|
||||
"PersistentVolumeClaim",
|
||||
"PodDisruptionBudget",
|
||||
"PodTemplate",
|
||||
"ResourceQuota",
|
||||
"Secret",
|
||||
"Service",
|
||||
"StatefulSet",
|
||||
}
|
||||
|
||||
func kindIsSupported(kind string) bool {
|
||||
for _, k := range supportedKinds {
|
||||
if k == kind {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
for _, k := range supportedKinds {
|
||||
if k == kind {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AdmissionIsRequired checks for admission if kind is supported
|
||||
func AdmissionIsRequired(request *v1beta1.AdmissionRequest) bool {
|
||||
// Here you can make additional hardcoded checks
|
||||
return kindIsSupported(request.Kind.Kind)
|
||||
// Here you can make additional hardcoded checks
|
||||
return kindIsSupported(request.Kind.Kind)
|
||||
}
|
||||
|
||||
// IsRuleApplicableToRequest checks requests kind, name and labels to fit the policy
|
||||
func IsRuleApplicableToRequest(policyResource types.PolicyResource, request *v1beta1.AdmissionRequest) bool {
|
||||
if policyResource.Selector == nil && policyResource.Name == nil {
|
||||
// TODO: selector or name MUST be specified
|
||||
return false
|
||||
}
|
||||
|
||||
if policyResource.Kind != request.Kind.Kind {
|
||||
return false
|
||||
}
|
||||
if policyResource.Selector == nil && policyResource.Name == nil {
|
||||
// TODO: selector or name MUST be specified
|
||||
return false
|
||||
}
|
||||
|
||||
if policyResource.Kind != request.Kind.Kind {
|
||||
return false
|
||||
}
|
||||
|
||||
if request.Object.Raw != nil {
|
||||
meta := parseMetadataFromObject(request.Object.Raw)
|
||||
name := parseNameFromMetadata(meta)
|
||||
meta := parseMetadataFromObject(request.Object.Raw)
|
||||
name := parseNameFromMetadata(meta)
|
||||
|
||||
if (policyResource.Name != nil && *policyResource.Name != name) {
|
||||
return false
|
||||
}
|
||||
|
||||
if policyResource.Selector != nil {
|
||||
selector, err := metav1.LabelSelectorAsSelector(policyResource.Selector)
|
||||
if policyResource.Name != nil && *policyResource.Name != name {
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// TODO: log that selector is invalid
|
||||
return false
|
||||
}
|
||||
|
||||
labelMap := parseLabelsFromMetadata(meta)
|
||||
|
||||
if !selector.Matches(labelMap) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if policyResource.Selector != nil {
|
||||
selector, err := metav1.LabelSelectorAsSelector(policyResource.Selector)
|
||||
|
||||
return true
|
||||
if err != nil {
|
||||
// TODO: log that selector is invalid
|
||||
return false
|
||||
}
|
||||
|
||||
labelMap := parseLabelsFromMetadata(meta)
|
||||
|
||||
if !selector.Matches(labelMap) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func parseMetadataFromObject(bytes []byte) map[string]interface{} {
|
||||
var objectJSON map[string]interface{}
|
||||
json.Unmarshal(bytes, &objectJSON)
|
||||
|
||||
return objectJSON["metadata"].(map[string]interface{})
|
||||
var objectJSON map[string]interface{}
|
||||
json.Unmarshal(bytes, &objectJSON)
|
||||
|
||||
return objectJSON["metadata"].(map[string]interface{})
|
||||
}
|
||||
|
||||
func parseLabelsFromMetadata(meta map[string]interface{}) labels.Set {
|
||||
if interfaceMap, ok := meta["labels"].(map[string]interface{}); ok {
|
||||
labelMap := make(labels.Set, len(interfaceMap))
|
||||
if interfaceMap, ok := meta["labels"].(map[string]interface{}); ok {
|
||||
labelMap := make(labels.Set, len(interfaceMap))
|
||||
|
||||
for key, value := range interfaceMap {
|
||||
labelMap[key] = value.(string)
|
||||
}
|
||||
return labelMap
|
||||
}
|
||||
return nil
|
||||
for key, value := range interfaceMap {
|
||||
labelMap[key] = value.(string)
|
||||
}
|
||||
return labelMap
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseNameFromMetadata(meta map[string]interface{}) string {
|
||||
if name, ok := meta["name"].(string); ok {
|
||||
return name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -1,133 +1,224 @@
|
|||
package webhooks_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing"
|
||||
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
"github.com/nirmata/kube-policy/webhooks"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
"github.com/nirmata/kube-policy/webhooks"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestAdmissionIsRequired(t *testing.T) {
|
||||
var request v1beta1.AdmissionRequest
|
||||
request.Kind.Kind = "ConfigMap"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "CronJob"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "DaemonSet"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Deployment"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Endpoint"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "HorizontalPodAutoscaler"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Ingress"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Job"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "LimitRange"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Namespace"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "NetworkPolicy"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "PersistentVolumeClaim"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "PodDisruptionBudget"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "PodTemplate"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "ResourceQuota"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Secret"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Service"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "StatefulSet"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
var request v1beta1.AdmissionRequest
|
||||
request.Kind.Kind = "ConfigMap"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "CronJob"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "DaemonSet"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Deployment"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Endpoint"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "HorizontalPodAutoscaler"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Ingress"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Job"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "LimitRange"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Namespace"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "NetworkPolicy"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "PersistentVolumeClaim"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "PodDisruptionBudget"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "PodTemplate"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "ResourceQuota"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Secret"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Service"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "StatefulSet"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
}
|
||||
|
||||
func TestIsRuleResourceFitsRequest_Kind(t *testing.T) {
|
||||
resourceName := "test-config-map"
|
||||
resource := types.PolicyResource {
|
||||
Kind: "ConfigMap",
|
||||
Name: &resourceName,
|
||||
}
|
||||
request := v1beta1.AdmissionRequest {
|
||||
Kind: metav1.GroupVersionKind{ Kind: "ConfigMap" },
|
||||
}
|
||||
resourceName := "test-config-map"
|
||||
resource := types.PolicyResource{
|
||||
Kind: "ConfigMap",
|
||||
Name: &resourceName,
|
||||
}
|
||||
request := v1beta1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{Kind: "ConfigMap"},
|
||||
}
|
||||
|
||||
objectByteArray := []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
objectByteArray := []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
resource.Kind = "Deployment"
|
||||
assertEq(t, false, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
resource.Kind = "Deployment"
|
||||
assertEq(t, false, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
}
|
||||
|
||||
func TestIsRuleResourceFitsRequest_Name(t *testing.T) {
|
||||
resourceName := "test-config-map"
|
||||
resource := types.PolicyResource {
|
||||
Kind: "ConfigMap",
|
||||
Name: &resourceName,
|
||||
}
|
||||
request := v1beta1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{Kind: "ConfigMap"},
|
||||
}
|
||||
resourceName := "test-config-map"
|
||||
resource := types.PolicyResource{
|
||||
Kind: "ConfigMap",
|
||||
Name: &resourceName,
|
||||
}
|
||||
request := v1beta1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{Kind: "ConfigMap"},
|
||||
}
|
||||
|
||||
objectByteArray := []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
resourceName = "test-config-map-new"
|
||||
assertEq(t, false, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
objectByteArray := []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
resourceName = "test-config-map-new"
|
||||
assertEq(t, false, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
|
||||
objectByteArray = []byte(`{"metadata":{"name":"test-config-map-new","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
objectByteArray = []byte(`{"metadata":{"name":"test-config-map-new","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
|
||||
objectByteArray = []byte(`{"metadata":{"name":"","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
assertEq(t, false, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
objectByteArray = []byte(`{"metadata":{"name":"","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
assertEq(t, false, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
}
|
||||
|
||||
func TestIsRuleResourceFitsRequest_Selector(t *testing.T) {
|
||||
resource := types.PolicyResource {
|
||||
Kind: "ConfigMap",
|
||||
Selector: &metav1.LabelSelector {
|
||||
MatchLabels: map[string]string {
|
||||
"label1" : "test1",
|
||||
"label2" : "test2",
|
||||
},
|
||||
MatchExpressions: nil,
|
||||
},
|
||||
}
|
||||
func TestIsRuleResourceFitsRequest_MatchExpressions(t *testing.T) {
|
||||
request := v1beta1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{Kind: "ConfigMap"},
|
||||
}
|
||||
|
||||
request := v1beta1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{Kind: "ConfigMap"},
|
||||
}
|
||||
resource := types.PolicyResource{
|
||||
Kind: "ConfigMap",
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: nil,
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
metav1.LabelSelectorRequirement{
|
||||
Key: "label2",
|
||||
Operator: "NotIn",
|
||||
Values: []string{
|
||||
"sometest1",
|
||||
},
|
||||
},
|
||||
metav1.LabelSelectorRequirement{
|
||||
Key: "label1",
|
||||
Operator: "In",
|
||||
Values: []string{
|
||||
"test1",
|
||||
"test8",
|
||||
"test201",
|
||||
},
|
||||
},
|
||||
metav1.LabelSelectorRequirement{
|
||||
Key: "label3",
|
||||
Operator: "DoesNotExist",
|
||||
Values: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
objectByteArray := []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
objectByteArray := []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
|
||||
objectByteArray = []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label3":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
assertEq(t, false, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
}
|
||||
|
||||
resource = types.PolicyResource {
|
||||
Kind: "ConfigMap",
|
||||
Selector: &metav1.LabelSelector {
|
||||
MatchLabels: map[string]string {
|
||||
"label3" : "test1",
|
||||
"label2" : "test2",
|
||||
},
|
||||
MatchExpressions: nil,
|
||||
},
|
||||
}
|
||||
func TestIsRuleResourceFitsRequest_MatchLabels(t *testing.T) {
|
||||
resource := types.PolicyResource{
|
||||
Kind: "ConfigMap",
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"label1": "test1",
|
||||
"label2": "test2",
|
||||
},
|
||||
MatchExpressions: nil,
|
||||
},
|
||||
}
|
||||
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
request := v1beta1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{Kind: "ConfigMap"},
|
||||
}
|
||||
|
||||
// TODO: MatchExpressions tests should be done
|
||||
}
|
||||
objectByteArray := []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
|
||||
objectByteArray = []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label3":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
assertEq(t, false, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
|
||||
resource = types.PolicyResource{
|
||||
Kind: "ConfigMap",
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"label3": "test1",
|
||||
"label2": "test2",
|
||||
},
|
||||
MatchExpressions: nil,
|
||||
},
|
||||
}
|
||||
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
}
|
||||
|
||||
func TestIsRuleResourceFitsRequest_MatchLabelsAndMatchExpressions(t *testing.T) {
|
||||
request := v1beta1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{Kind: "ConfigMap"},
|
||||
}
|
||||
|
||||
resource := types.PolicyResource{
|
||||
Kind: "ConfigMap",
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"label1": "test1",
|
||||
},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
metav1.LabelSelectorRequirement{
|
||||
Key: "label2",
|
||||
Operator: "In",
|
||||
Values: []string{
|
||||
"test2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
objectByteArray := []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
|
||||
resource = types.PolicyResource{
|
||||
Kind: "ConfigMap",
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"label1": "test1",
|
||||
},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
metav1.LabelSelectorRequirement{
|
||||
Key: "label2",
|
||||
Operator: "NotIn",
|
||||
Values: []string{
|
||||
"sometest1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
objectByteArray = []byte(`{"metadata":{"name":"test-config-map","namespace":"default","creationTimestamp":null,"labels":{"label1":"test1","label2":"test2"}}}`)
|
||||
request.Object.Raw = objectByteArray
|
||||
|
||||
assertEq(t, true, webhooks.IsRuleApplicableToRequest(resource, &request))
|
||||
}
|
||||
|
|
|
@ -1,126 +1,132 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// MutationWebhook is a data type that represents
|
||||
// buisness logic for resource mutation
|
||||
type MutationWebhook struct {
|
||||
logger *log.Logger
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewMutationWebhook is a method that returns new instance
|
||||
// of MutationWebhook struct
|
||||
func NewMutationWebhook(logger *log.Logger) (*MutationWebhook, error) {
|
||||
if logger == nil {
|
||||
return nil, errors.New("Logger must be set for the mutation webhook")
|
||||
}
|
||||
return &MutationWebhook{logger: logger}, nil
|
||||
if logger == nil {
|
||||
return nil, errors.New("Logger must be set for the mutation webhook")
|
||||
}
|
||||
return &MutationWebhook{logger: logger}, nil
|
||||
}
|
||||
|
||||
// Mutate applies admission to request
|
||||
func (mw *MutationWebhook) Mutate(request *v1beta1.AdmissionRequest, policies []types.Policy) *v1beta1.AdmissionResponse {
|
||||
mw.logger.Printf("AdmissionReview for Kind=%v, Namespace=%v Name=%v UID=%v patchOperation=%v UserInfo=%v",
|
||||
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation, request.UserInfo)
|
||||
mw.logger.Printf("AdmissionReview for Kind=%v, Namespace=%v Name=%v UID=%v patchOperation=%v UserInfo=%v",
|
||||
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation, request.UserInfo)
|
||||
|
||||
if len(policies) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(policies) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var allPatches []types.PolicyPatch
|
||||
for _, policy := range policies {
|
||||
var stopOnError bool = true
|
||||
if policy.Spec.FailurePolicy != nil && *policy.Spec.FailurePolicy == "continueOnError" {
|
||||
stopOnError = false
|
||||
}
|
||||
var allPatches []types.PolicyPatch
|
||||
for _, policy := range policies {
|
||||
stopOnError := true
|
||||
if policy.Spec.FailurePolicy != nil && *policy.Spec.FailurePolicy == "continueOnError" {
|
||||
stopOnError = false
|
||||
}
|
||||
|
||||
for ruleIdx, rule := range policy.Spec.Rules {
|
||||
if IsRuleApplicableToRequest(rule.Resource, request) {
|
||||
mw.logger.Printf("Applying policy %v, rule index = %v", policy.ObjectMeta.Name, ruleIdx)
|
||||
rulePatches, err := mw.applyPolicyRule(request, rule)
|
||||
/*
|
||||
* If at least one error is detected in the rule, the entire rule will not be applied.
|
||||
* This may be changed in the future by varying the policy.Spec.FailurePolicy values.
|
||||
*/
|
||||
if err != nil {
|
||||
mw.logger.Printf("Error occurred while applying the policy: %v", err)
|
||||
if stopOnError {
|
||||
mw.logger.Printf("/!\\ Denying the request according to FailurePolicy spec /!\\")
|
||||
return errorToResponse(err, false)
|
||||
}
|
||||
} else {
|
||||
mw.logger.Printf("Prepared %v patches", len(rulePatches))
|
||||
allPatches = append(allPatches, rulePatches...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for ruleIdx, rule := range policy.Spec.Rules {
|
||||
if IsRuleApplicableToRequest(rule.Resource, request) {
|
||||
mw.logger.Printf("Applying policy %v, rule index = %v", policy.ObjectMeta.Name, ruleIdx)
|
||||
rulePatches, err := mw.applyPolicyRule(request, rule)
|
||||
/*
|
||||
* If at least one error is detected in the rule, the entire rule will not be applied.
|
||||
* This may be changed in the future by varying the policy.Spec.FailurePolicy values.
|
||||
*/
|
||||
if err != nil {
|
||||
mw.logger.Printf("Error occurred while applying the policy: %v", err)
|
||||
if stopOnError {
|
||||
mw.logger.Printf("/!\\ Denying the request according to FailurePolicy spec /!\\")
|
||||
return errorToResponse(err, false)
|
||||
}
|
||||
} else {
|
||||
mw.logger.Printf("Prepared %v patches", len(rulePatches))
|
||||
allPatches = append(allPatches, rulePatches...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patchesBytes, err := SerializePatches(allPatches)
|
||||
if err != nil {
|
||||
mw.logger.Printf("Error occerred while serializing JSONPathch: %v", err)
|
||||
return errorToResponse(err, true)
|
||||
}
|
||||
patchesBytes, err := SerializePatches(allPatches)
|
||||
if err != nil {
|
||||
mw.logger.Printf("Error occerred while serializing JSONPathch: %v", err)
|
||||
return errorToResponse(err, true)
|
||||
}
|
||||
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
Patch: patchesBytes,
|
||||
PatchType: func() *v1beta1.PatchType {
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
return &pt
|
||||
}(),
|
||||
}
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
Patch: patchesBytes,
|
||||
PatchType: func() *v1beta1.PatchType {
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
return &pt
|
||||
}(),
|
||||
}
|
||||
}
|
||||
|
||||
// Applies all possible patches in a rule
|
||||
func (mw *MutationWebhook) applyPolicyRule(request *v1beta1.AdmissionRequest, rule types.PolicyRule) ([]types.PolicyPatch, error) {
|
||||
var allPatches []types.PolicyPatch
|
||||
if rule.Patches == nil && rule.ConfigMapGenerator == nil && rule.SecretGenerator == nil {
|
||||
return nil, errors.New("The rule is empty!")
|
||||
}
|
||||
var allPatches []types.PolicyPatch
|
||||
if rule.Patches == nil && rule.ConfigMapGenerator == nil && rule.SecretGenerator == nil {
|
||||
return nil, errors.New("The rule is empty")
|
||||
}
|
||||
|
||||
allPatches = append(allPatches, rule.Patches...)
|
||||
allPatches = append(allPatches, rule.Patches...)
|
||||
|
||||
if rule.ConfigMapGenerator != nil {
|
||||
// TODO: Make patches from configMapGenerator and add them to returned array
|
||||
}
|
||||
if rule.ConfigMapGenerator != nil {
|
||||
// TODO: Make patches from configMapGenerator and add them to returned array
|
||||
}
|
||||
|
||||
if rule.SecretGenerator != nil {
|
||||
// TODO: Make patches from secretGenerator and add them to returned array
|
||||
}
|
||||
if rule.SecretGenerator != nil {
|
||||
// TODO: Make patches from secretGenerator and add them to returned array
|
||||
}
|
||||
|
||||
return allPatches, nil
|
||||
return allPatches, nil
|
||||
}
|
||||
|
||||
// SerializePatches converts JSON patches to byte array
|
||||
func SerializePatches(patches []types.PolicyPatch) ([]byte, error) {
|
||||
var result []byte
|
||||
result = append(result, []byte("[\n")...)
|
||||
for index, patch := range patches {
|
||||
if patch.Operation == "" || patch.Path == "" {
|
||||
return nil, errors.New("JSONPatch doesn't contain mandatory fields 'path' or 'op'")
|
||||
}
|
||||
var result []byte
|
||||
result = append(result, []byte("[\n")...)
|
||||
for index, patch := range patches {
|
||||
if patch.Operation == "" || patch.Path == "" {
|
||||
return nil, errors.New("JSONPatch doesn't contain mandatory fields 'path' or 'op'")
|
||||
}
|
||||
|
||||
patchBytes, err := json.Marshal(patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patchBytes, err := json.Marshal(patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, patchBytes...)
|
||||
if index != (len(patches) - 1) {
|
||||
result = append(result, []byte(",\n")...)
|
||||
}
|
||||
}
|
||||
result = append(result, []byte("\n]")...)
|
||||
return result, nil
|
||||
result = append(result, patchBytes...)
|
||||
if index != (len(patches) - 1) {
|
||||
result = append(result, []byte(",\n")...)
|
||||
}
|
||||
}
|
||||
result = append(result, []byte("\n]")...)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func errorToResponse(err error, allowed bool) *v1beta1.AdmissionResponse {
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Result: &metav1.Status{
|
||||
Message: err.Error(),
|
||||
},
|
||||
Allowed: allowed,
|
||||
}
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Result: &metav1.Status{
|
||||
Message: err.Error(),
|
||||
},
|
||||
Allowed: allowed,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,48 @@
|
|||
package webhooks_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing"
|
||||
|
||||
"github.com/nirmata/kube-policy/webhooks"
|
||||
"github.com/nirmata/kube-policy/webhooks"
|
||||
|
||||
//v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
//metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
//v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
//metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
)
|
||||
|
||||
func TestSerializePatches_Empty(t *testing.T) {
|
||||
var patches []types.PolicyPatch
|
||||
bytes, err := webhooks.SerializePatches(patches)
|
||||
assertEq(t, nil, err)
|
||||
assertEqStringAndData(t, "[\n\n]", bytes)
|
||||
var patches []types.PolicyPatch
|
||||
bytes, err := webhooks.SerializePatches(patches)
|
||||
assertEq(t, nil, err)
|
||||
assertEqStringAndData(t, "[\n\n]", bytes)
|
||||
}
|
||||
|
||||
func TestSerializePatches_SingleValid(t *testing.T) {
|
||||
patch := types.PolicyPatch{
|
||||
Path: "/metadata/labels/is-mutated",
|
||||
Operation: "add",
|
||||
Value: "true",
|
||||
}
|
||||
patches := []types.PolicyPatch{patch}
|
||||
bytes, err := webhooks.SerializePatches(patches)
|
||||
assertEq(t, nil, err)
|
||||
assertEqStringAndData(t, `[
|
||||
patch := types.PolicyPatch{
|
||||
Path: "/metadata/labels/is-mutated",
|
||||
Operation: "add",
|
||||
Value: "true",
|
||||
}
|
||||
patches := []types.PolicyPatch{patch}
|
||||
bytes, err := webhooks.SerializePatches(patches)
|
||||
assertEq(t, nil, err)
|
||||
assertEqStringAndData(t, `[
|
||||
{"path":"/metadata/labels/is-mutated","op":"add","value":"true"}
|
||||
]`, bytes)
|
||||
}
|
||||
|
||||
func TestSerializePatches_SingleInvalid(t *testing.T) {
|
||||
patch := types.PolicyPatch{
|
||||
Path: "/metadata/labels/is-mutated",
|
||||
Value: "true",
|
||||
}
|
||||
patches := []types.PolicyPatch{patch}
|
||||
_, err := webhooks.SerializePatches(patches)
|
||||
assertNe(t, nil, err)
|
||||
patches[0].Path = ""
|
||||
patches[0].Operation = "delete"
|
||||
_, err = webhooks.SerializePatches(patches)
|
||||
assertNe(t, nil, err)
|
||||
patch := types.PolicyPatch{
|
||||
Path: "/metadata/labels/is-mutated",
|
||||
Value: "true",
|
||||
}
|
||||
patches := []types.PolicyPatch{patch}
|
||||
_, err := webhooks.SerializePatches(patches)
|
||||
assertNe(t, nil, err)
|
||||
patches[0].Path = ""
|
||||
patches[0].Operation = "delete"
|
||||
_, err = webhooks.SerializePatches(patches)
|
||||
assertNe(t, nil, err)
|
||||
}
|
||||
|
||||
// patch := `[ {"op":"add","path":"/metadata/labels","value":{"is-mutated":"true"}} ]`
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
package webhooks_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assertEq(t *testing.T, expected interface{}, actual interface{}) {
|
||||
if expected != actual {
|
||||
t.Errorf("%s != %s", expected, actual)
|
||||
}
|
||||
if expected != actual {
|
||||
t.Errorf("%s != %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func assertNe(t *testing.T, expected interface{}, actual interface{}) {
|
||||
if expected == actual {
|
||||
t.Errorf("%s != %s", expected, actual)
|
||||
}
|
||||
if expected == actual {
|
||||
t.Errorf("%s != %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqDataImpl(t *testing.T, expected, actual []byte, formatModifier string) {
|
||||
if len(expected) != len(actual) {
|
||||
t.Errorf("len(expected) != len(actual): %d != %d\n1:"+formatModifier+"\n2:"+formatModifier, len(expected), len(actual), expected, actual)
|
||||
return
|
||||
}
|
||||
if len(expected) != len(actual) {
|
||||
t.Errorf("len(expected) != len(actual): %d != %d\n1:"+formatModifier+"\n2:"+formatModifier, len(expected), len(actual), expected, actual)
|
||||
return
|
||||
}
|
||||
|
||||
for idx, val := range actual {
|
||||
if val != expected[idx] {
|
||||
t.Errorf("Slices not equal at index %d:\n1:"+formatModifier+"\n2:"+formatModifier, idx, expected, actual)
|
||||
}
|
||||
}
|
||||
for idx, val := range actual {
|
||||
if val != expected[idx] {
|
||||
t.Errorf("Slices not equal at index %d:\n1:"+formatModifier+"\n2:"+formatModifier, idx, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqData(t *testing.T, expected, actual []byte) {
|
||||
assertEqDataImpl(t, expected, actual, "%x")
|
||||
assertEqDataImpl(t, expected, actual, "%x")
|
||||
}
|
||||
|
||||
func assertEqStringAndData(t *testing.T, str string, data []byte) {
|
||||
assertEqDataImpl(t, []byte(str), data, "%s")
|
||||
assertEqDataImpl(t, []byte(str), data, "%s")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue