diff --git a/Makefile b/Makefile index 0d52896..e04fcd6 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,8 @@ vet: # Generate code .PHONY: generate -generate: controller-gen wire +generate: controller-gen wire mockgen + $(MOCKGEN) -destination ./mocks/k8s_client_mock.go -package mocks sigs.k8s.io/controller-runtime/pkg/client Client $(CONTROLLER_GEN) object:headerFile="misc/boilerplate.go.txt" paths="./..." $(WIRE) ./... @@ -138,3 +139,19 @@ else WIRE=$(shell which wire) endif +# find or download mockgen +.PHONY: mockgen +mockgen: +ifeq (, $(shell which mockgen)) + @{ \ + set -e ;\ + MOCKGEN_TMP_DIR=$$(mktemp -d) ;\ + cd $$MOCKGEN_TMP_DIR ;\ + go mod init tmp ;\ + go install github.com/golang/mock/mockgen@v1.6.0 ;\ + rm -rf $$MOCKGEN_TMP_DIR ;\ + } +MOCKGEN=$(GOBIN)/mockgen +else +MOCKGEN=$(shell which mockgen) +endif diff --git a/README.md b/README.md index 534eb33..df9630c 100644 --- a/README.md +++ b/README.md @@ -858,13 +858,17 @@ By default, the following namespaces are tagged with the above label: ### Synthetic references -KubeMod 0.17.0 introduces `syntheticRefs` - a map of external resource manifests injected at the root of every Kubernetes resource processed by KubeMod. +KubeMod 0.17.0 introduced `syntheticRefs` - a map of external resource manifests injected at the root of every Kubernetes resource processed by KubeMod. -Currently the only external manifest injected in `syntheticRefs` is the manifest of the `namespace` of namespaced objects. -This unlocks use cases where a ModRule can be matched against objects not only based on their own manifest, but also the manifests of their namespaces. +Synthetic references unlock use cases where a ModRule can be matched against objects not only based on their own manifest, but also the manifests of their namespaces. In addition, since `syntheticRefs` exists in the body of the target resource, it can be used when constructing `patch` values. +Currently KubeMod injects the following manifests in `syntheticRefs`: + +- `namespace`: The manifest of the namespace of the target, if the target is a namespaced object. +- `node`: The manifest of the node of a pod (See [Node synthetic references](#node-synthetic-references) below for more information). + Here's an example ModRule which matches all pods created in namespaces labeled with `color` equal to `blue`. The ModRule mutates those pods by tagging them with a label `flavor`, whose value is inherited from the `flavor` label of the pod's namespace. @@ -918,6 +922,57 @@ The `syntheticRefs` map exists in the object's manifest only for the purpose of It is not actually inserted in the resulting resource manifest ultimately sent to the cluster. +#### Node synthetic references + +KubeMod 0.19.0 introduced `node` synthetic reference for ModRules targeting pod manifests. + +In order to capture the node which a pod has been scheduled on, KubeMod listens to pod scheduling events. + +If KubeMod intercepts a pod scheduling event for a pod which has annotation `ref.kubemod.io/inject-node-ref` set to `"true"`, KubeMod updates the pod by injecting annotation `ref.kubemod.io/node` whose value is set to the name of the node. + +This triggers an `UPDATE` operation, which is again captured by KubeMod. When KubeMod intercepts pod operations for pods with annotation `ref.kubemod.io/node`, it injects the node manifest into the pod's synthetic references, thus making them available for ModRule matching and patching. + +This enables a wide array of use cases not natively supported by Kubernetes (see https://github.com/kubernetes/kubernetes/issues/40610). + +For example, the following cluster-wide ModRule will inject a pod with it's node's availability region and zone, as soon as the pod gets scheduled to a node: + +```yaml +apiVersion: api.kubemod.io/v1beta1 +kind: ModRule +metadata: + name: inject-node-annotations + namespace: kubemod-system +spec: + type: Patch + targetNamespaceRegex: ".*" + admissionOperations: + - UPDATE + + match: + # Match pods... + - select: '$.kind' + matchValue: 'Pod' + # ...which have access to the node's manifest through the synthetic ref injected by KubeMod. + - select: '$.syntheticRefs.node.metadata.labels' + + patch: + # Grab the node's region and zone and put them in the pod's corresponding labels. + - op: add + path: /metadata/labels/topology.kubernetes.io~1region + value: '"{{ index .Target.syntheticRefs.node.metadata.labels "topology.kubernetes.io/region"}}"' + - op: add + path: /metadata/labels/topology.kubernetes.io~1zone + value: '"{{ index .Target.syntheticRefs.node.metadata.labels "topology.kubernetes.io/zone"}}"' +``` + +The above ModRule will apply to any pod created in any namespace as long as it has the following annotation: + +```yaml +ref.kubemod.io/inject-node-ref: "true" +``` + +If you want to have this rule apply to pods which don't have this annotation, you can create another ModRule which injects `ref.kubemod.io/inject-node-ref` into any pod that matches a given criteria, or to all pods that are created in the cluster. + ### Target resources By default, KubeMod targets the following list of resources: diff --git a/app/operatorapp.go b/app/operatorapp.go index 21f6ba0..2862d9d 100644 --- a/app/operatorapp.go +++ b/app/operatorapp.go @@ -36,6 +36,7 @@ func NewKubeModOperatorApp( manager manager.Manager, modRuleReconciler *controllers.ModRuleReconciler, coreDragnetWebhookHandler *core.DragnetWebhookHandler, + corePodBindingWebhookHandler *core.PodBindingWebhookHandler, log logr.Logger, ) (*KubeModOperatorApp, error) { @@ -66,6 +67,13 @@ func NewKubeModOperatorApp( }, ) + hookServer.Register( + "/podbinding-webhook", + &webhook.Admission{ + Handler: corePodBindingWebhookHandler, + }, + ) + // Start the manager. setupLog.Info("starting manager") if err := manager.Start(ctrl.SetupSignalHandler()); err != nil { diff --git a/app/wire.go b/app/wire.go index baf80cd..2c51440 100644 --- a/app/wire.go +++ b/app/wire.go @@ -43,6 +43,7 @@ func InitializeKubeModOperatorApp( core.NewModRuleStoreItemFactory, core.NewModRuleStore, core.NewDragnetWebhookHandler, + core.NewPodBindingWebhookHandler, controllers.NewModRuleReconciler, NewControllerManager, NewKubeModOperatorApp, diff --git a/app/wire_gen.go b/app/wire_gen.go index 6152661..5c82576 100644 --- a/app/wire_gen.go +++ b/app/wire_gen.go @@ -29,7 +29,8 @@ func InitializeKubeModOperatorApp(scheme *runtime.Scheme, metricsAddr OperatorMe return nil, err } dragnetWebhookHandler := core.NewDragnetWebhookHandler(manager, modRuleStore, log) - kubeModOperatorApp, err := NewKubeModOperatorApp(scheme, manager, modRuleReconciler, dragnetWebhookHandler, log) + podBindingWebhookHandler := core.NewPodBindingWebhookHandler(manager, log) + kubeModOperatorApp, err := NewKubeModOperatorApp(scheme, manager, modRuleReconciler, dragnetWebhookHandler, podBindingWebhookHandler, log) if err != nil { return nil, err } diff --git a/config/default/cluster_role_patch.yaml b/config/default/cluster_role_patch.yaml index 5d7cace..044205a 100644 --- a/config/default/cluster_role_patch.yaml +++ b/config/default/cluster_role_patch.yaml @@ -5,7 +5,20 @@ - "" resources: - namespaces + - nodes verbs: - list - get - watch +- op: add + path: /rules/-1 + value: + apiGroups: + - "" + resources: + - pods + verbs: + - list + - get + - watch + - update diff --git a/config/kubemod-crt/kustomization.yaml b/config/kubemod-crt/kustomization.yaml index a4cfc22..42811d1 100644 --- a/config/kubemod-crt/kustomization.yaml +++ b/config/kubemod-crt/kustomization.yaml @@ -4,4 +4,4 @@ resources: images: - name: kubemod-crt-image newName: kubemod/kubemod-crt - newTag: v1.2.1 + newTag: v1.3.0 diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 8979471..82b0a00 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: kubemod/kubemod - newTag: v0.18.1 + newTag: latest diff --git a/config/webhook/webhooks.yaml b/config/webhook/webhooks.yaml index 9d14e7a..28f637e 100644 --- a/config/webhook/webhooks.yaml +++ b/config/webhook/webhooks.yaml @@ -77,7 +77,33 @@ webhooks: - UPDATE - DELETE scope: '*' - +- name: podbinding.kubemod.io + clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /podbinding-webhook + failurePolicy: Fail + sideEffects: None + timeoutSeconds: 10 + admissionReviewVersions: ["v1beta1"] + namespaceSelector: + matchExpressions: + - key: admission.kubemod.io/ignore + operator: NotIn + values: ["true"] + rules: + - apiGroups: + - "" + apiVersions: + - v1 + resources: + - pods/binding + operations: + - CREATE + - UPDATE + scope: '*' --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration diff --git a/core/dragnetwebhook.go b/core/dragnetwebhook.go index 68b5044..ca26473 100644 --- a/core/dragnetwebhook.go +++ b/core/dragnetwebhook.go @@ -18,9 +18,10 @@ import ( "context" "encoding/json" "fmt" - "github.com/kubemod/kubemod/api/v1beta1" "strings" + "github.com/kubemod/kubemod/api/v1beta1" + "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -41,7 +42,7 @@ type DragnetWebhookHandler struct { func NewDragnetWebhookHandler(manager manager.Manager, modRuleStore *ModRuleStore, log logr.Logger) *DragnetWebhookHandler { return &DragnetWebhookHandler{ client: manager.GetClient(), - log: log.WithName("modrule-webhook"), + log: log.WithName("dragnet-webhook"), modRuleStore: modRuleStore, } } @@ -72,7 +73,7 @@ func (h *DragnetWebhookHandler) Handle(ctx context.Context, req admission.Reques if err != nil { log.Error(err, "Failed to inject syntheticRefs into object manifest") - return admission.Allowed("failed to obtain namespace") + return admission.Allowed("failed to inject syntheticRefs into object manifest") } // First run patch operations. @@ -107,38 +108,36 @@ func (h *DragnetWebhookHandler) Handle(ctx context.Context, req admission.Reques func (h *DragnetWebhookHandler) injectSyntheticRefs(ctx context.Context, originalJSON []byte, namespace string) ([]byte, error) { obj := &unstructured.Unstructured{} syntheticRefs := make(map[string]interface{}) + var err error // If the target is a namespaced object, grab the namespace from the manager's client. if namespace != "" { - ns := &unstructured.Unstructured{} - ns.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "", - Kind: "Namespace", - Version: "v1", - }) - - err := h.client.Get(ctx, client.ObjectKey{Name: namespace}, ns) - + err = h.injectNamespaceSyntheticRef(ctx, namespace, syntheticRefs) if err != nil { - return nil, fmt.Errorf("failed to retrieve namespace '%s' : %v", namespace, err) + return nil, fmt.Errorf("failed to inject namespace synthetic ref: %v", err) } - - // Remove managedFields - we don't need this and it only litters the namespace manifest. - ns.SetManagedFields(nil) - // Remove annotation kubectl.kubernetes.io/last-applied-configuration as it simply duplicates the namespace manifest. - annotations := ns.GetAnnotations() - delete(annotations, "kubectl.kubernetes.io/last-applied-configuration") - ns.SetAnnotations(annotations) - - syntheticRefs["namespace"] = ns } - err := json.Unmarshal(originalJSON, obj) + err = json.Unmarshal(originalJSON, obj) if err != nil { return nil, fmt.Errorf("failed to decode webhook request object's manifest into JSON: %v", err) } + // If the target object is a pod, check if the pod has a node name set by KubeMod in annotation ref.kubemod.io/nodename + // and if yes, inject the pod's node manifest into the synthetic refs. + if isPod(obj.UnstructuredContent()) { + nodeName, ok, err := unstructured.NestedString(obj.UnstructuredContent(), "metadata", "annotations", "ref.kubemod.io/nodename") + + if ok && err == nil && nodeName != "" { + err = h.injectPodNodeSyntheticRef(ctx, nodeName, syntheticRefs) + + if err != nil { + return nil, fmt.Errorf("failed to inject node synthetic ref: %v", err) + } + } + } + // Set KubeMod syntheticRefs field. obj.UnstructuredContent()["syntheticRefs"] = syntheticRefs @@ -148,3 +147,60 @@ func (h *DragnetWebhookHandler) injectSyntheticRefs(ctx context.Context, origina return json.Marshal(obj) } + +func isPod(obj map[string]interface{}) bool { + kind, ok, err := unstructured.NestedString(obj, "kind") + + if ok && err == nil && kind == "Pod" { + return true + } + + return false +} + +func (h *DragnetWebhookHandler) injectNamespaceSyntheticRef(ctx context.Context, namespace string, syntheticRefs map[string]interface{}) error { + ns := &unstructured.Unstructured{} + ns.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Namespace", + Version: "v1", + }) + + err := h.client.Get(ctx, client.ObjectKey{Name: namespace}, ns) + + if err != nil { + return fmt.Errorf("failed to retrieve namespace '%s' : %v", namespace, err) + } + + // Remove managedFields - we don't need this and it only litters the namespace manifest. + ns.SetManagedFields(nil) + // Remove annotation kubectl.kubernetes.io/last-applied-configuration as it simply duplicates the namespace manifest. + annotations := ns.GetAnnotations() + delete(annotations, "kubectl.kubernetes.io/last-applied-configuration") + ns.SetAnnotations(annotations) + + syntheticRefs["namespace"] = ns + return nil +} + +func (h *DragnetWebhookHandler) injectPodNodeSyntheticRef(ctx context.Context, nodeName string, syntheticRefs map[string]interface{}) error { + node := &unstructured.Unstructured{} + + node.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Node", + Version: "v1", + }) + + err := h.client.Get(ctx, client.ObjectKey{Name: nodeName}, node) + + if err != nil { + return fmt.Errorf("failed to retrieve node '%s': %v", nodeName, err) + } + + // Remove managedFields - we don't need this and it only litters the manifest. + node.SetManagedFields(nil) + + syntheticRefs["node"] = node + return nil +} diff --git a/core/dragnetwebhook_test.go b/core/dragnetwebhook_test.go new file mode 100644 index 0000000..67d0c44 --- /dev/null +++ b/core/dragnetwebhook_test.go @@ -0,0 +1,126 @@ +/* +Licensed under the BSD 3-Clause License (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://opensource.org/licenses/BSD-3-Clause + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package core + +import ( + "context" + "io/ioutil" + "path" + "sort" + + "github.com/golang/mock/gomock" + "github.com/kubemod/kubemod/api/v1beta1" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8sruntime "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sigs.k8s.io/yaml" +) + +var _ = Describe("DragnetWebhookHandler", func() { + var ( + testBed *DragnetWebhookHandlerTestBed + handler *DragnetWebhookHandler + ) + + BeforeEach(func() { + testBed = InitializeDragnetWebhookHandlerTestBed("kubemod-system", GinkgoT()) + + handler = &DragnetWebhookHandler{ + client: testBed.mockK8sClient, + log: testBed.log, + modRuleStore: testBed.modRuleStore, + } + }) + + AfterEach(func() { + testBed.mockCtrl.Finish() + }) + + // Load a modrule into the modrule store. + loadModRule := func(modRuleFile string, namespace string) { + modRuleYAML, err := ioutil.ReadFile(path.Join("testdata/modrules/", modRuleFile)) + Expect(err).NotTo(HaveOccurred()) + + modRule := v1beta1.ModRule{} + err = yaml.Unmarshal(modRuleYAML, &modRule) + Expect(err).NotTo(HaveOccurred()) + + // Fill out default values for missing properties. + modRule.Default() + + if modRule.Namespace == "" { + modRule.Namespace = namespace + } + + err = testBed.modRuleStore.Put(&modRule) + Expect(err).NotTo(HaveOccurred()) + } + + It("should be able to insert data derived from a pod's node synthetic reference", func() { + // Load resource JSON - a pod which has a ref.kubemod.io/nodename annotation (presumably injected by KubeMod's podbinding webhook). + resourceJSON, err := ioutil.ReadFile(path.Join("testdata/resources/", "pod-7.json")) + Expect(err).NotTo(HaveOccurred()) + + // Load modrule which injects annotations derived from node metadata. + loadModRule("patch/patch-33.yaml", "my-namespace") + + // Prepare the K8s client mock for a call to get the default namespace manifest. + testBed.mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{Name: "my-namespace"}, gomock.Any()).Return(nil) + + // Prepare the K8s client mock for a call to get the node manifest which the pod is scheduled to live on. + testBed.mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{Name: "my-node-1234"}, gomock.Any()). + DoAndReturn(func(ctx context.Context, key client.ObjectKey, node *unstructured.Unstructured) error { + // Stamp out the node's region and zone. + node.SetLabels(map[string]string{ + "topology.kubernetes.io/region": "us-west-2", + "topology.kubernetes.io/zone": "us-west-2b", + }) + return nil + }) + + request := admission.Request{ + AdmissionRequest: admissionv1beta1.AdmissionRequest{ + Namespace: "my-namespace", + Operation: "UPDATE", + Object: k8sruntime.RawExtension{ + Raw: resourceJSON, + }, + }, + } + + response := handler.Handle(context.Background(), request) + Expect(response).ToNot(BeNil()) + + Expect(len(response.Patches)).To(Equal(2)) + + // Sort the patch because the order returned by CalculatePatch is unstable. + sort.SliceStable(response.Patches, func(i, j int) bool { + return (response.Patches[i].Operation + response.Patches[i].Path) < (response.Patches[j].Operation + response.Patches[j].Path) + }) + + Expect(response.Patches[0].Operation).To(Equal("add")) + Expect(response.Patches[0].Path).To(Equal("/metadata/labels/topology.kubernetes.io~1region")) + Expect(response.Patches[0].Value).To(Equal("us-west-2")) + + Expect(response.Patches[1].Operation).To(Equal("add")) + Expect(response.Patches[1].Path).To(Equal("/metadata/labels/topology.kubernetes.io~1zone")) + Expect(response.Patches[1].Value).To(Equal("us-west-2b")) + }) + +}) diff --git a/core/podbindingwebhook.go b/core/podbindingwebhook.go new file mode 100644 index 0000000..e62472c --- /dev/null +++ b/core/podbindingwebhook.go @@ -0,0 +1,140 @@ +/* +Licensed under the BSD 3-Clause License (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://opensource.org/licenses/BSD-3-Clause + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package core + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// PodBindingWebhookHandler is the main entrypoint to KubeMod's pod binding interception admission webhook. +type PodBindingWebhookHandler struct { + client client.Client + decoder *admission.Decoder + log logr.Logger +} + +// NewPodBindingWebhookHandler constructs a new core webhook handler. +func NewPodBindingWebhookHandler(manager manager.Manager, log logr.Logger) *PodBindingWebhookHandler { + return &PodBindingWebhookHandler{ + client: manager.GetClient(), + log: log.WithName("podbinding-webhook"), + } +} + +// Handle triggers the main mutating logic. +func (h *PodBindingWebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response { + if req.Resource.Resource == "pods" && req.SubResource == "binding" { + log := h.log.WithValues("request uid", req.UID, "namespace", req.Namespace, "resource", fmt.Sprintf("%v/%v", req.Resource.Resource, req.Name), "operation", req.Operation) + + binding := &unstructured.Unstructured{} + + err := json.Unmarshal(req.Object.Raw, binding) + + if err != nil { + log.Error(err, "failed to decode webhook pods/binding request object's manifest into JSON") + return admission.Allowed("failed to decode webhook request object's manifest into JSON") + } + + podNamespace, podName, nodeName, err := getBindingTargets(binding.UnstructuredContent()) + + if err != nil { + log.Error(err, "failed to obtain target data from pod binding") + return admission.Allowed("failed to obtain target data from pod binding") + } + + // Grab the name of the node and update the pod by attaching an ref.kubemod.io/nodename annotation. + pod := &unstructured.Unstructured{} + pod.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Pod", + Version: "v1", + }) + + err = h.client.Get(ctx, client.ObjectKey{Namespace: podNamespace, Name: podName}, pod) + + if err != nil { + log.Error(err, "failed to obtain pod", "podNamespace", podNamespace, "podName", podName) + return admission.Allowed("failed to obtain pod") + } + + annotations := pod.GetAnnotations() + + if annotations == nil { + annotations = map[string]string{} + } + + if injectNodeSyntheticRef, ok := annotations["ref.kubemod.io/inject-node-ref"]; ok && injectNodeSyntheticRef == "true" { + annotations["ref.kubemod.io/nodename"] = nodeName + + pod.SetAnnotations(annotations) + + err = h.client.Update(ctx, pod) + + if err != nil { + log.Error(err, "failed to inject annotation ref.kubemod.io/nodename into pod", "podNamespace", podNamespace, "podName", podName) + return admission.Allowed("failed to obtain pod") + } + } + } + + return admission.Allowed("ok") +} + +func getBindingTargets(binding map[string]interface{}) (string, string, string, error) { + var ok bool + var podNamespace, podName, nodeName string + var err error + + podNamespace, ok, err = unstructured.NestedString(binding, "metadata", "namespace") + + if !ok { + return "", "", "", fmt.Errorf("unable to find metadata.namespace in pod binding manifest") + } + + if err != nil { + return "", "", "", fmt.Errorf("failed to obtain metadata.namespace from pod binding manifest: %v", err) + } + + podName, ok, err = unstructured.NestedString(binding, "metadata", "name") + + if !ok { + return "", "", "", fmt.Errorf("unable to find metadata.name in pod binding manifest") + } + + if err != nil { + return "", "", "", fmt.Errorf("failed to obtain metadata.name from pod binding manifest: %v", err) + } + + nodeName, ok, err = unstructured.NestedString(binding, "target", "name") + + if !ok { + return "", "", "", fmt.Errorf("unable to find target.name in pod binding manifest") + } + + if err != nil { + return "", "", "", fmt.Errorf("failed to obtain target.name from pod binding manifest: %v", err) + } + + return podNamespace, podName, nodeName, nil +} diff --git a/core/testdata/modrules/patch/patch-33.yaml b/core/testdata/modrules/patch/patch-33.yaml new file mode 100644 index 0000000..f9297a6 --- /dev/null +++ b/core/testdata/modrules/patch/patch-33.yaml @@ -0,0 +1,23 @@ +apiVersion: api.kubemod.io/v1beta1 +kind: ModRule +metadata: + name: modrule-33 +spec: + type: Patch + + admissionOperations: + - UPDATE + + match: + - select: '$.kind' + matchValue: 'Pod' + + - select: '$.syntheticRefs.node.metadata.labels' + + patch: + - op: add + path: /metadata/labels/topology.kubernetes.io~1region + value: '"{{ index .Target.syntheticRefs.node.metadata.labels "topology.kubernetes.io/region"}}"' + - op: add + path: /metadata/labels/topology.kubernetes.io~1zone + value: '"{{ index .Target.syntheticRefs.node.metadata.labels "topology.kubernetes.io/zone"}}"' diff --git a/core/testdata/resources/pod-7.json b/core/testdata/resources/pod-7.json new file mode 100644 index 0000000..93722a4 --- /dev/null +++ b/core/testdata/resources/pod-7.json @@ -0,0 +1,94 @@ +{ + "kind": "Pod", + "apiVersion": "v1", + "metadata": { + "generateName": "nginx-8598fccb59-", + "creationTimestamp": null, + "labels": { + "app": "nginx", + "color": "red", + "pod-template-hash": "8598fccb59" + }, + "annotations": { + "ref.kubemod.io/nodename": "my-node-1234" + }, + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "kind": "ReplicaSet", + "name": "nginx-8598fccb59", + "uid": "28e2f3b4-24e4-4624-a0ec-0fa51904b2a7", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "volumes": [ + { + "name": "default-token-xb267", + "secret": { + "secretName": "default-token-xb267" + } + } + ], + "containers": [ + { + "name": "nginx", + "image": "nginx:1.14.2", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "500m", + "memory": "1Gi" + }, + "requests": { + "cpu": "500m", + "memory": "1Gi" + } + }, + "volumeMounts": [ + { + "name": "default-token-xb267", + "readOnly": true, + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" + } + ], + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "IfNotPresent" + } + ], + "restartPolicy": "Always", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "securityContext": { + "runAsNonRoot": true + }, + "schedulerName": "default-scheduler", + "tolerations": [ + { + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }, + { + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + } + ], + "priority": 0, + "enableServiceLinks": true + }, + "status": {} +} diff --git a/core/wire.go b/core/wire.go index e308b1e..b0223a0 100644 --- a/core/wire.go +++ b/core/wire.go @@ -18,7 +18,9 @@ limitations under the License. package core import ( + "github.com/golang/mock/gomock" "github.com/kubemod/kubemod/expressions" + "github.com/kubemod/kubemod/mocks" "github.com/kubemod/kubemod/util" "github.com/go-logr/logr" @@ -35,11 +37,34 @@ type ModRuleStoreItemTestBed struct { itemFactory *ModRuleStoreItemFactory } +// DragnetWebhookHandlerTestBed is used in DragnetWebhookHandler test. +type DragnetWebhookHandlerTestBed struct { + mockCtrl *gomock.Controller + mockK8sClient *mocks.MockClient + modRuleStore *ModRuleStore + log logr.Logger +} + // NewLogger instantiates a new testing logger. func NewTestLogger(tLogger util.TLogger) logr.Logger { return util.TestLogger{TLogger: tLogger} } +// NewMockTestReporter converts a TLogger to gomock.TestReporter. +func NewMockTestReporter(tLogger util.TLogger) gomock.TestReporter { + return tLogger +} + +// NewGoMockController instantiates a new mock controller. +func NewGoMockController(testReporter gomock.TestReporter) *gomock.Controller { + return gomock.NewController(testReporter) +} + +// NewK8sMockClient instantiates a new K8S client mock. +func NewK8sMockClient(mockCtrl *gomock.Controller) *mocks.MockClient { + return mocks.NewMockClient(mockCtrl) +} + // NewModRuleStoreTestBed instantiates a new test bed. func NewModRuleStoreTestBed(modRuleStore *ModRuleStore) *ModRuleStoreTestBed { return &ModRuleStoreTestBed{ @@ -54,6 +79,16 @@ func NewModRuleStoreItemTestBed(itemFactory *ModRuleStoreItemFactory) *ModRuleSt } } +// NewDragnetWebhookHandlerTestBed instantiates a new test bed. +func NewDragnetWebhookHandlerTestBed(modRuleStore *ModRuleStore, mockCtrl *gomock.Controller, mockK8sClient *mocks.MockClient, log logr.Logger) *DragnetWebhookHandlerTestBed { + return &DragnetWebhookHandlerTestBed{ + mockCtrl: mockCtrl, + mockK8sClient: mockK8sClient, + modRuleStore: modRuleStore, + log: log, + } +} + // InitializeModRuleStoreTestBed instructs wire how to construct a new test bed. func InitializeModRuleStoreTestBed(clusterModRulesNamespace ClusterModRulesNamespace, tLogger util.TLogger) *ModRuleStoreTestBed { wire.Build( @@ -67,6 +102,22 @@ func InitializeModRuleStoreTestBed(clusterModRulesNamespace ClusterModRulesNames return nil } +// InitializeDragnetWebhookHandlerTestBed instructs wire how to construct a new test bed. +func InitializeDragnetWebhookHandlerTestBed(clusterModRulesNamespace ClusterModRulesNamespace, tLogger util.TLogger) *DragnetWebhookHandlerTestBed { + wire.Build( + NewDragnetWebhookHandlerTestBed, + NewMockTestReporter, + NewGoMockController, + NewK8sMockClient, + NewTestLogger, + expressions.NewKubeModJSONPathLanguage, + NewModRuleStoreItemFactory, + NewModRuleStore, + ) + + return nil +} + // InitializeModRuleStoreItemTestBed instructs wire how to construct a new test bed. func InitializeModRuleStoreItemTestBed(tLogger util.TLogger) *ModRuleStoreItemTestBed { wire.Build( diff --git a/core/wire_gen.go b/core/wire_gen.go index 7d71f0d..9cf7580 100644 --- a/core/wire_gen.go +++ b/core/wire_gen.go @@ -8,7 +8,9 @@ package core import ( "github.com/go-logr/logr" + "github.com/golang/mock/gomock" "github.com/kubemod/kubemod/expressions" + "github.com/kubemod/kubemod/mocks" "github.com/kubemod/kubemod/util" ) @@ -24,6 +26,19 @@ func InitializeModRuleStoreTestBed(clusterModRulesNamespace ClusterModRulesNames return modRuleStoreTestBed } +// InitializeDragnetWebhookHandlerTestBed instructs wire how to construct a new test bed. +func InitializeDragnetWebhookHandlerTestBed(clusterModRulesNamespace ClusterModRulesNamespace, tLogger util.TLogger) *DragnetWebhookHandlerTestBed { + language := expressions.NewKubeModJSONPathLanguage() + logger := NewTestLogger(tLogger) + modRuleStoreItemFactory := NewModRuleStoreItemFactory(language, logger) + modRuleStore := NewModRuleStore(modRuleStoreItemFactory, clusterModRulesNamespace, logger) + testReporter := NewMockTestReporter(tLogger) + controller := NewGoMockController(testReporter) + mockClient := NewK8sMockClient(controller) + dragnetWebhookHandlerTestBed := NewDragnetWebhookHandlerTestBed(modRuleStore, controller, mockClient, logger) + return dragnetWebhookHandlerTestBed +} + // InitializeModRuleStoreItemTestBed instructs wire how to construct a new test bed. func InitializeModRuleStoreItemTestBed(tLogger util.TLogger) *ModRuleStoreItemTestBed { language := expressions.NewKubeModJSONPathLanguage() @@ -45,11 +60,34 @@ type ModRuleStoreItemTestBed struct { itemFactory *ModRuleStoreItemFactory } +// DragnetWebhookHandlerTestBed is used in DragnetWebhookHandler test. +type DragnetWebhookHandlerTestBed struct { + mockCtrl *gomock.Controller + mockK8sClient *mocks.MockClient + modRuleStore *ModRuleStore + log logr.Logger +} + // NewLogger instantiates a new testing logger. func NewTestLogger(tLogger util.TLogger) logr.Logger { return util.TestLogger{TLogger: tLogger} } +// NewMockTestReporter converts a TLogger to gomock.TestReporter. +func NewMockTestReporter(tLogger util.TLogger) gomock.TestReporter { + return tLogger +} + +// NewGoMockController instantiates a new mock controller. +func NewGoMockController(testReporter gomock.TestReporter) *gomock.Controller { + return gomock.NewController(testReporter) +} + +// NewK8sMockClient instantiates a new K8S client mock. +func NewK8sMockClient(mockCtrl *gomock.Controller) *mocks.MockClient { + return mocks.NewMockClient(mockCtrl) +} + // NewModRuleStoreTestBed instantiates a new test bed. func NewModRuleStoreTestBed(modRuleStore *ModRuleStore) *ModRuleStoreTestBed { return &ModRuleStoreTestBed{ @@ -63,3 +101,13 @@ func NewModRuleStoreItemTestBed(itemFactory *ModRuleStoreItemFactory) *ModRuleSt itemFactory: itemFactory, } } + +// NewDragnetWebhookHandlerTestBed instantiates a new test bed. +func NewDragnetWebhookHandlerTestBed(modRuleStore *ModRuleStore, mockCtrl *gomock.Controller, mockK8sClient *mocks.MockClient, log logr.Logger) *DragnetWebhookHandlerTestBed { + return &DragnetWebhookHandlerTestBed{ + mockCtrl: mockCtrl, + mockK8sClient: mockK8sClient, + modRuleStore: modRuleStore, + log: log, + } +} diff --git a/go.mod b/go.mod index 3bc7b1e..9b7ad74 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,14 @@ require ( github.com/evanphx/json-patch/v5 v5.1.0 github.com/gin-gonic/gin v1.6.3 github.com/go-logr/logr v0.1.0 + github.com/golang/mock v1.2.0 github.com/google/wire v0.5.0 github.com/hexops/gotextdiff v1.0.3 github.com/onsi/ginkgo v1.14.1 github.com/onsi/gomega v1.10.2 github.com/segmentio/ksuid v1.0.3 go.uber.org/zap v1.10.0 + golang.org/x/sys v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.0.1 k8s.io/apimachinery v0.18.6 k8s.io/client-go v0.18.6 @@ -71,7 +73,6 @@ require ( golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 // indirect golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 // indirect golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect - golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.3.3 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect diff --git a/go.sum b/go.sum index 3c2d4c8..2b9ed8e 100644 --- a/go.sum +++ b/go.sum @@ -159,6 +159,7 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -438,8 +439,8 @@ golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/mocks/k8s_client_mock.go b/mocks/k8s_client_mock.go new file mode 100644 index 0000000..1417191 --- /dev/null +++ b/mocks/k8s_client_mock.go @@ -0,0 +1,180 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sigs.k8s.io/controller-runtime/pkg/client (interfaces: Client) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + runtime "k8s.io/apimachinery/pkg/runtime" + types "k8s.io/apimachinery/pkg/types" + client "sigs.k8s.io/controller-runtime/pkg/client" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockClient) Create(arg0 context.Context, arg1 runtime.Object, arg2 ...client.CreateOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockClientMockRecorder) Create(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClient)(nil).Create), varargs...) +} + +// Delete mocks base method. +func (m *MockClient) Delete(arg0 context.Context, arg1 runtime.Object, arg2 ...client.DeleteOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Delete", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockClientMockRecorder) Delete(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), varargs...) +} + +// DeleteAllOf mocks base method. +func (m *MockClient) DeleteAllOf(arg0 context.Context, arg1 runtime.Object, arg2 ...client.DeleteAllOfOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteAllOf", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAllOf indicates an expected call of DeleteAllOf. +func (mr *MockClientMockRecorder) DeleteAllOf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllOf", reflect.TypeOf((*MockClient)(nil).DeleteAllOf), varargs...) +} + +// Get mocks base method. +func (m *MockClient) Get(arg0 context.Context, arg1 types.NamespacedName, arg2 runtime.Object) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Get indicates an expected call of Get. +func (mr *MockClientMockRecorder) Get(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), arg0, arg1, arg2) +} + +// List mocks base method. +func (m *MockClient) List(arg0 context.Context, arg1 runtime.Object, arg2 ...client.ListOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// List indicates an expected call of List. +func (mr *MockClientMockRecorder) List(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClient)(nil).List), varargs...) +} + +// Patch mocks base method. +func (m *MockClient) Patch(arg0 context.Context, arg1 runtime.Object, arg2 client.Patch, arg3 ...client.PatchOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Patch", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Patch indicates an expected call of Patch. +func (mr *MockClientMockRecorder) Patch(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockClient)(nil).Patch), varargs...) +} + +// Status mocks base method. +func (m *MockClient) Status() client.StatusWriter { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Status") + ret0, _ := ret[0].(client.StatusWriter) + return ret0 +} + +// Status indicates an expected call of Status. +func (mr *MockClientMockRecorder) Status() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockClient)(nil).Status)) +} + +// Update mocks base method. +func (m *MockClient) Update(arg0 context.Context, arg1 runtime.Object, arg2 ...client.UpdateOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockClientMockRecorder) Update(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockClient)(nil).Update), varargs...) +} diff --git a/mocks/logr_mock.go b/mocks/logr_mock.go new file mode 100644 index 0000000..5acd912 --- /dev/null +++ b/mocks/logr_mock.go @@ -0,0 +1,129 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/go-logr/logr (interfaces: Logger) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + logr "github.com/go-logr/logr" + gomock "github.com/golang/mock/gomock" +) + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +// Enabled mocks base method. +func (m *MockLogger) Enabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Enabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Enabled indicates an expected call of Enabled. +func (mr *MockLoggerMockRecorder) Enabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enabled", reflect.TypeOf((*MockLogger)(nil).Enabled)) +} + +// Error mocks base method. +func (m *MockLogger) Error(arg0 error, arg1 string, arg2 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) +} + +// Error indicates an expected call of Error. +func (mr *MockLoggerMockRecorder) Error(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...) +} + +// Info mocks base method. +func (m *MockLogger) Info(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Info", varargs...) +} + +// Info indicates an expected call of Info. +func (mr *MockLoggerMockRecorder) Info(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...) +} + +// V mocks base method. +func (m *MockLogger) V(arg0 int) logr.InfoLogger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "V", arg0) + ret0, _ := ret[0].(logr.InfoLogger) + return ret0 +} + +// V indicates an expected call of V. +func (mr *MockLoggerMockRecorder) V(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "V", reflect.TypeOf((*MockLogger)(nil).V), arg0) +} + +// WithName mocks base method. +func (m *MockLogger) WithName(arg0 string) logr.Logger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithName", arg0) + ret0, _ := ret[0].(logr.Logger) + return ret0 +} + +// WithName indicates an expected call of WithName. +func (mr *MockLoggerMockRecorder) WithName(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithName", reflect.TypeOf((*MockLogger)(nil).WithName), arg0) +} + +// WithValues mocks base method. +func (m *MockLogger) WithValues(arg0 ...interface{}) logr.Logger { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "WithValues", varargs...) + ret0, _ := ret[0].(logr.Logger) + return ret0 +} + +// WithValues indicates an expected call of WithValues. +func (mr *MockLoggerMockRecorder) WithValues(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithValues", reflect.TypeOf((*MockLogger)(nil).WithValues), arg0...) +} diff --git a/samples/modrules/modrule-8.yaml b/samples/modrules/modrule-8.yaml new file mode 100644 index 0000000..e7986db --- /dev/null +++ b/samples/modrules/modrule-8.yaml @@ -0,0 +1,19 @@ +apiVersion: api.kubemod.io/v1beta1 +kind: ModRule +metadata: + name: patch-pod-with-node-annotations + namespace: kubemod-system +spec: + type: Patch + targetNamespaceRegex: ".*" + + match: + - select: '$.syntheticRefs.node.metadata.annotations["node.alpha.kubernetes.io/ttl"]' + + - select: '$.kind' + matchValue: 'Pod' + + patch: + - op: add + path: /metadata/annotations/node.alpha.kubernetes.io~1ttl + value: '"{{ index .Target.syntheticRefs.node.metadata.annotations "node.alpha.kubernetes.io/ttl"}}"' diff --git a/samples/stack/nginx-deployment.yaml b/samples/stack/nginx-deployment.yaml index 9b33bae..464f0ab 100644 --- a/samples/stack/nginx-deployment.yaml +++ b/samples/stack/nginx-deployment.yaml @@ -13,6 +13,11 @@ spec: metadata: labels: app: nginx + annotations: + # Turning on node synthetic ref will make KubeMod monitor for pod scheduling events. + # When the pod gets scheduled, KubeMod injects a synthetic ref to the pod's node + # and triggers all UPDATE modrules that match this pod. + ref.kubemod.io/inject-node-ref: "true" spec: containers: - name: nginx @@ -24,3 +29,9 @@ spec: limits: cpu: "500m" memory: "1Gi" + env: + - name: INJECTED_NODE_TTL + valueFrom: + fieldRef: + # The following annotation will be populated by modrule-7. + fieldPath: metadata.annotations['node.alpha.kubernetes.io/ttl'] diff --git a/util/testlogger.go b/util/testlogger.go index bf5cf67..d8f7c4e 100644 --- a/util/testlogger.go +++ b/util/testlogger.go @@ -18,9 +18,11 @@ import ( "github.com/go-logr/logr" ) -// TLogger is an interface which implements Logf. +// TLogger is an interface which implements Logf, Errorf, and Fatalf. type TLogger interface { Logf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) } // TestLogger is a logr.Logger that prints through a testing.T object.