diff --git a/kubeclient/kubeclient.go b/kubeclient/kubeclient.go index 99518f59d6..bf84a61193 100644 --- a/kubeclient/kubeclient.go +++ b/kubeclient/kubeclient.go @@ -3,10 +3,12 @@ package kubeclient import ( "log" "os" + "time" types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) @@ -32,18 +34,113 @@ func NewKubeClient(config *rest.Config, logger *log.Logger) (*KubeClient, error) }, nil } -func (kc *KubeClient) CopySecret(from *types.PolicyCopyFrom, namespaceTo string) error { - // This is the test code, which works - var secret v1.Secret - secret.Namespace = namespaceTo - secret.ObjectMeta.SetName("test-secret") - secret.StringData = make(map[string]string) - secret.StringData["test-data"] = "test-value" - newSecret, err := kc.client.CoreV1().Secrets(namespaceTo).Create(&secret) - if err != nil { - kc.logger.Printf("Unable to create secret: %s", err) - } else { - kc.logger.Printf("Secret created: %s", newSecret.Name) +func (kc *KubeClient) GenerateConfigMap(generator types.PolicyConfigGenerator, namespace string) error { + kc.logger.Printf("Preparing to create configmap %s/%s", namespace, generator.Name) + configMap := &v1.ConfigMap{} + + var err error + if generator.CopyFrom != nil { + kc.logger.Printf("Copying data from configmap %s/%s", generator.CopyFrom.Namespace, generator.CopyFrom.Name) + configMap, err = kc.client.CoreV1().ConfigMaps(generator.CopyFrom.Namespace).Get(generator.CopyFrom.Name, defaultGetOptions()) + if err != nil { + return err + } + } + + configMap.ObjectMeta = metav1.ObjectMeta{ + Name: generator.Name, + Namespace: namespace, + } + + // Copy data from generator to the new configmap + if generator.Data != nil { + if configMap.Data == nil { + configMap.Data = make(map[string]string) + } + + for k, v := range generator.Data { + configMap.Data[k] = v + } + } + + go kc.createConfigMapAfterNamespaceIsCreated(*configMap, namespace) + return nil +} + +func (kc *KubeClient) GenerateSecret(generator types.PolicyConfigGenerator, namespace string) error { + kc.logger.Printf("Preparing to create secret %s/%s", namespace, generator.Name) + secret := &v1.Secret{} + + var err error + if generator.CopyFrom != nil { + kc.logger.Printf("Copying data from secret %s/%s", generator.CopyFrom.Namespace, generator.CopyFrom.Name) + secret, err = kc.client.CoreV1().Secrets(generator.CopyFrom.Namespace).Get(generator.CopyFrom.Name, defaultGetOptions()) + if err != nil { + return err + } + } + + secret.ObjectMeta = metav1.ObjectMeta{ + Name: generator.Name, + Namespace: namespace, + } + + // Copy data from generator to the new secret + if generator.Data != nil { + if secret.Data == nil { + secret.Data = make(map[string][]byte) + } + + for k, v := range generator.Data { + secret.Data[k] = []byte(v) + } + } + + go kc.createSecretAfterNamespaceIsCreated(*secret, namespace) + return nil +} + +func defaultGetOptions() metav1.GetOptions { + return metav1.GetOptions{ + ResourceVersion: "1", + IncludeUninitialized: true, + } +} + +const namespaceCreationMaxWaitTime time.Duration = 30 * time.Second +const namespaceCreationWaitInterval time.Duration = 100 * time.Millisecond + +// Waits until namespace is created with maximum duration maxWaitTimeForNamespaceCreation +func (kc *KubeClient) waitUntilNamespaceIsCreated(name string) error { + timeStart := time.Now() + + var lastError error = nil + for time.Now().Sub(timeStart) < namespaceCreationMaxWaitTime { + _, lastError = kc.client.CoreV1().Namespaces().Get(name, defaultGetOptions()) + if lastError == nil { + break + } + time.Sleep(namespaceCreationWaitInterval) + } + return lastError +} + +func (kc *KubeClient) createConfigMapAfterNamespaceIsCreated(configMap v1.ConfigMap, namespace string) { + err := kc.waitUntilNamespaceIsCreated(namespace) + if err == nil { + _, err = kc.client.CoreV1().ConfigMaps(namespace).Create(&configMap) + } + if err != nil { + kc.logger.Printf("Can't create a configmap: %s", err) + } +} + +func (kc *KubeClient) createSecretAfterNamespaceIsCreated(secret v1.Secret, namespace string) { + err := kc.waitUntilNamespaceIsCreated(namespace) + if err == nil { + _, err = kc.client.CoreV1().Secrets(namespace).Create(&secret) + } + if err != nil { + kc.logger.Printf("Can't create a secret: %s", err) } - return err } diff --git a/server/server.go b/server/server.go index 67a46e91e0..31a23cc404 100644 --- a/server/server.go +++ b/server/server.go @@ -23,7 +23,6 @@ import ( type WebhookServer struct { server http.Server policyController *controller.PolicyController - kubeclient *kubeclient.KubeClient mutationWebhook *webhooks.MutationWebhook logger *log.Logger } @@ -55,7 +54,7 @@ func NewWebhookServer(config WebhookServerConfig, logger *log.Logger) (*WebhookS } tlsConfig.Certificates = []tls.Certificate{pair} - mw, err := webhooks.NewMutationWebhook(logger) + mw, err := webhooks.NewMutationWebhook(logger, config.Kubeclient) if err != nil { return nil, err } @@ -63,7 +62,6 @@ func NewWebhookServer(config WebhookServerConfig, logger *log.Logger) (*WebhookS ws := &WebhookServer{ logger: logger, policyController: config.Controller, - kubeclient: config.Kubeclient, mutationWebhook: mw, } diff --git a/webhooks/admission.go b/webhooks/admission.go index 35996cc9db..db394f8309 100644 --- a/webhooks/admission.go +++ b/webhooks/admission.go @@ -1,12 +1,9 @@ package webhooks import ( - "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{ @@ -76,29 +73,3 @@ func IsRuleApplicableToRequest(policyResource types.PolicyResource, request *v1b return true } - -func parseMetadataFromObject(bytes []byte) 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)) - - 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 "" -} diff --git a/webhooks/mutation.go b/webhooks/mutation.go index fb5c61dee6..3d3cd2eeb4 100644 --- a/webhooks/mutation.go +++ b/webhooks/mutation.go @@ -6,6 +6,7 @@ import ( "fmt" "log" + kubeclient "github.com/nirmata/kube-policy/kubeclient" types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" v1beta1 "k8s.io/api/admission/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -14,16 +15,17 @@ import ( // MutationWebhook is a data type that represents // buisness logic for resource mutation type MutationWebhook struct { - logger *log.Logger + logger *log.Logger + kubeclient *kubeclient.KubeClient } // NewMutationWebhook is a method that returns new instance // of MutationWebhook struct -func NewMutationWebhook(logger *log.Logger) (*MutationWebhook, error) { +func NewMutationWebhook(logger *log.Logger, kubeclient *kubeclient.KubeClient) (*MutationWebhook, error) { if logger == nil { - return nil, errors.New("Logger must be set for the mutation webhook") + return nil, errors.New("Logger and/or KubeClient is not set") } - return &MutationWebhook{logger: logger}, nil + return &MutationWebhook{logger: logger, kubeclient: kubeclient}, nil } // Mutate applies admission to request @@ -50,20 +52,15 @@ func (mw *MutationWebhook) Mutate(request *v1beta1.AdmissionRequest, policies [] } if IsRuleApplicableToRequest(rule.Resource, request) { - mw.logger.Printf("Applying policy %s, rule index = %s", 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)) + mw.logger.Printf("Applying policy %s, rule index = %d", policy.ObjectMeta.Name, ruleIdx) + rulePatches, err := mw.applyRule(request, rule, stopOnError) + // If at least one error is detected in the rule, the entire rule will not be applied: + // it can be changed in the future by varying the policy.Spec.FailurePolicy values. + if err != nil && stopOnError { + mw.logger.Printf("/!\\ Denying the request according to FailurePolicy spec /!\\") + return errorToResponse(err, false) + } + if rulePatches != nil { allPatches = append(allPatches, rulePatches...) } } @@ -86,42 +83,68 @@ func (mw *MutationWebhook) Mutate(request *v1beta1.AdmissionRequest, policies [] } } -// Applies all possible patches in a rule -func (mw *MutationWebhook) applyPolicyRule(request *v1beta1.AdmissionRequest, rule types.PolicyRule) ([]types.PolicyPatch, error) { - var allPatches []types.PolicyPatch - allPatches = append(allPatches, rule.Patches...) - - // configMapGenerator and secretGenerator can be applied only to namespaces - if request.Kind.Kind == "Namespace" { - if rule.ConfigMapGenerator != nil { - err := mw.applyConfigGenerator(request, *rule.ConfigMapGenerator, "ConfigMap") - if err != nil { - mw.logger.Printf("Unable to apply configMapGenerator: %s", err) - } - } - - if rule.SecretGenerator != nil { - err := mw.applyConfigGenerator(request, *rule.SecretGenerator, "Secret") - if err != nil { - mw.logger.Printf("Unable to apply secretGenerator: %s", err) - } - } +// Applies all rule to the created object and returns list of JSON patches. +// May return nil patches if it is not necessary to create patches for requested object. +func (mw *MutationWebhook) applyRule(request *v1beta1.AdmissionRequest, rule types.PolicyRule, stopOnError bool) ([]types.PolicyPatch, error) { + rulePatches, err := mw.applyRulePatches(request, rule) + if err != nil { + mw.logger.Printf("Error occurred while applying patches according to the policy: %v", err) + } else { + mw.logger.Printf("Prepared %v patches", len(rulePatches)) } - return allPatches, nil + if err == nil || !stopOnError { + err = mw.applyRuleGenerators(request, rule) + } + + return rulePatches, err +} + +func (mw *MutationWebhook) applyRulePatches(request *v1beta1.AdmissionRequest, rule types.PolicyRule) ([]types.PolicyPatch, error) { + var patches []types.PolicyPatch + patches = append(patches, rule.Patches...) + return patches, nil +} + +func (mw *MutationWebhook) applyRuleGenerators(request *v1beta1.AdmissionRequest, rule types.PolicyRule) error { + // configMapGenerator and secretGenerator can be applied only to namespaces + if request.Kind.Kind == "Namespace" { + meta := parseMetadataFromObject(request.Object.Raw) + namespaceName := parseNameFromMetadata(meta) + + err := mw.applyConfigGenerator(rule.ConfigMapGenerator, namespaceName, "ConfigMap") + if err == nil { + err = mw.applyConfigGenerator(rule.SecretGenerator, namespaceName, "Secret") + } + return err + } + return nil } // Creates resourceKind (ConfigMap or Secret) with parameters specified in generator in cluster specified in request -func (mw *MutationWebhook) applyConfigGenerator(request *v1beta1.AdmissionRequest, generator types.PolicyConfigGenerator, resourceKind string) error { - err := generator.Validate() - if err != nil { - return errors.New(fmt.Sprintf("Generator for %s is invalid: %s", resourceKind, err)) +func (mw *MutationWebhook) applyConfigGenerator(generator *types.PolicyConfigGenerator, namespace string, configKind string) error { + if generator == nil { + return nil } - if generator.CopyFrom != nil { - // TODO: Implement copying of the object to another namespace + err := generator.Validate() + if err != nil { + return errors.New(fmt.Sprintf("Generator for '%s' is invalid: %s", configKind, err)) } - // TODO: Implement filling of existing object with data from generator + + switch configKind { + case "ConfigMap": + err = mw.kubeclient.GenerateConfigMap(*generator, namespace) + case "Secret": + err = mw.kubeclient.GenerateSecret(*generator, namespace) + default: + err = errors.New(fmt.Sprintf("Unsupported config Kind '%s'", configKind)) + } + + if err != nil { + return errors.New(fmt.Sprintf("Unable to apply generator for %s '%s/%s' : %s", configKind, namespace, generator.Name, err)) + } + return nil } diff --git a/webhooks/utils.go b/webhooks/utils.go new file mode 100644 index 0000000000..029975b952 --- /dev/null +++ b/webhooks/utils.go @@ -0,0 +1,33 @@ +package webhooks + +import ( + "encoding/json" + + "k8s.io/apimachinery/pkg/labels" +) + +func parseMetadataFromObject(bytes []byte) 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)) + + 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 "" +}