diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 43d17f6..820fc4b 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -22,7 +22,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.17 - name: Install dependencies run: go get -u golang.org/x/lint/golint diff --git a/README.md b/README.md index 70d184d..899af24 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Namespace Node Affinity -Namespace Node Affinity is a Kubernetes mutating webhook which provides the ability to define node affinity for pods on a namespace level. +Namespace Node Affinity is a Kubernetes mutating webhook which provides the ability to define node affinity and/or tolerations for pods on a namespace level. It is a replacement for the [PodNodeSelector](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podnodeselector) admission controller and it is useful when using a managed k8s control plane such as [GKE](https://cloud.google.com/kubernetes-engine) or [EKS](https://aws.amazon.com/eks) where you do not have the ability to enable additional admission controller plugins and the [PodNodeSelector](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podnodeselector) might not be available. The only admission controller plugin required to run the namespace-node-affinity mutating webhook is the `MutatingAdmissionWebhook` which is already enabled on most managed Kubernetes services such as [EKS](https://docs.aws.amazon.com/eks/latest/userguide/platform-versions.html). @@ -18,8 +18,10 @@ kubectl apply -k deployments/base This will create the following: * namespace-node-affinity ServiceAccount + * namespace-node-affinity Role + * namespace-node-affinity RoleBinding * namespace-node-affinity ClusterRole - * namespace-node-affinity-rolebinding ClusterRoleBinding + * namespace-node-affinity ClusterRoleBinding * namespace-node-affinity Service * namespace-node-affinity Deployment @@ -31,11 +33,11 @@ Docker images for the webhook are available for multiple platforms [here](https: # Required Permissions -The namespace-node-affinity webhook requires `get` permissions for `configmaps` in all namespaces, so it can read the configuration for each namespace it's enabled for. +The namespace-node-affinity webhook requires `get` permissions for `configmaps` in the namespace where the centralised config is deployed. The init container (if used) requires `get`, `create` and `update` for `mutatingwebhookconfigurations` in the `admissionregistration.k8s.io` api group to create or update the MutatingWebhookConfiguration. -The `ClusterRole` included in [deployments](/deployments/) already includes all of the required permissions. +The `Role` and `ClusterRole` included in [deployments](/deployments/) already include all of the required permissions and the supplied `RoleBinding` and `ClusterRoleBinding` binds the `Role` and `ClusterRole` to the `ServiceAccount` used by the webhook. # Configuration @@ -44,28 +46,37 @@ To enable the namespace-node-affinity mutating webhook on a namespace you simply kubectl label ns my-namespace namespace-node-affinity=enabled ``` -In order to add `nodeAffinity` to pods in that namespace you will have to create a `ConfigMap` named `namespace-node-affinity` that contains a `nodeSelectorTerms` key with the node selector terms in either JSON or YAML format. The `nodeSelectorTerms` from the config map will be added as `requiredDuringSchedulingIgnoredDuringExecution` node affinity type to each pod that is created in the labeled namespace. An example config map can be found in [examples/sample_configmap.yaml](/examples/sample_configmap.yaml). More information on how node affinity works can be found [here](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity). +Each namespace with the `namespace-node-affinity=enabled` label will also need an entry in the `ConfigMap` where the configuration for the webhook is stored. The config for each namespace can be in either JSON or YAML format and must have at least one of `nodeSelectorTerms` or `tolerations`. The `nodeSelectorTerms` from the config will be added as `requiredDuringSchedulingIgnoredDuringExecution` node affinity type to each pod that is created in the labeled namespace. An example configuration can be found in [examples/sample_configmap.yaml](/examples/sample_configmap.yaml). + +More information on how node affinity works can be found [here](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity). +More information on how taints and tolerations work can be found [here](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/). # Failure Modes When using the provided init container to create the mutating webhook configuration, the namespace-node-affinity mutating webhook will fail silently so pods can still be created on the cluster if the webhook has been misconfigured. The affected namespace can be seen in the `AdmissionReview.Namespace`. - * Missing `namespace-node-affinity` `ConfigMap` in a namespace labeled with `namespace-node-affinity=enabled` + * Missing `namespace-node-affinity` `ConfigMap` ``` time="2021-04-10T09:35:06Z" level=info msg="Received AdmissionReview: {...} time="2021-04-10T09:35:06Z" level=error msg="missing configuration: configmaps \"namespace-node-affinity\" not found" ``` - * Missing `nodeSelectorTerms` from the `namespace-node-affinity` `ConfigMap` + * Missing entry for the namespace in the `ConfigMap` +``` +time="2021-09-03T17:32:16Z" level=info msg="Received AdmissionReview: {...} +time="2021-09-03T17:32:16Z" level=error msg="missing configuration: for testing-ns-e" +``` + + * Both `nodeSelectorTerms` and `tolerations` are missing from the entry for the namespace in the `ConfigMap` ``` -time="2021-04-10T09:37:57Z" level=info msg="Received AdmissionReview: {...} -time="2021-04-10T09:37:57Z" level=error msg="missing nodeSelectorTerms from config: nodeSelectorTerms is missing from the config map" +time="2021-09-03T17:38:46Z" level=info msg="Received AdmissionReview: {...} +time="2021-09-03T17:38:46Z" level=error msg="invalid configuration: at least one of nodeSelectorTerms or tolerations needs to be specified for testing-ns-d" ``` - * Invalid `nodeSelectorTerms` in the `namespace-node-affinity` `ConfigMap` + * Invalid `nodeSelectorTerms` or `tolerations` in the `namespace-node-affinity` `ConfigMap` ``` time="2021-04-10T09:40:59Z" level=info msg="Received AdmissionReview: {...} -time="2021-04-10T09:40:59Z" level=error msg="invalid configuration: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type []v1.NodeSelectorTerm" +time="2021-04-10T09:40:59Z" level=error msg="invalid configuration: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go struct field NamespaceConfig.nodeSelectorTerms of type []v1.NodeSelectorTerm" ``` # Contributing diff --git a/cmd/createcerts/main.go b/cmd/createcerts/main.go index 2581ee8..bc67593 100644 --- a/cmd/createcerts/main.go +++ b/cmd/createcerts/main.go @@ -8,13 +8,14 @@ import ( "crypto/x509/pkix" "encoding/pem" "fmt" - log "github.com/sirupsen/logrus" "math/big" "os" "path/filepath" "time" - "github.com/idgenchev/namespace-node-affinity/pkg/webhookconfig" + log "github.com/sirupsen/logrus" + + "github.com/idgenchev/namespace-node-affinity/webhookconfig" "github.com/jessevdk/go-flags" k8sclient "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" diff --git a/cmd/nsnodeaffinity/main.go b/cmd/nsnodeaffinity/main.go index da25851..28cd563 100644 --- a/cmd/nsnodeaffinity/main.go +++ b/cmd/nsnodeaffinity/main.go @@ -6,7 +6,8 @@ import ( "net/http" "time" - "github.com/idgenchev/namespace-node-affinity/pkg/affinityinjector" + "github.com/idgenchev/namespace-node-affinity/injector" + "github.com/jessevdk/go-flags" "github.com/prometheus/client_golang/prometheus/promhttp" log "github.com/sirupsen/logrus" @@ -20,7 +21,8 @@ var opts struct { WriteTimeout time.Duration `long:"write-timeout" default:"10s" description:"Write timeout"` CertFile string `lond:"cert" short:"c" env:"CERT" default:"/etc/webhook/certs/tls.crt" description:"Path to the cert file"` KeyFile string `lond:"key" short:"k" env:"KEY" default:"/etc/webhook/certs/tls.key" description:"Path to the key file"` - ConfigMapName string `long:"config-map-name" short:"m" env:"CONFIG_MAP_NAME" default:"namespace-node-affinity" description:"Name of the configm map containing the node selector terms to be applied to every pod on creation. This config map should be present in every namespace where this webhook is enabled"` + Namespace string `long:"namespace" short:"n" env:"NAMESPACE" description:"The namespace where the configmap is deployed"` + ConfigMapName string `long:"config-map-name" short:"m" env:"CONFIG_MAP_NAME" default:"namespace-node-affinity" description:"Name of the configm map containing the node selector terms to be applied to every pod on creation."` } type injectorInterface interface { @@ -68,7 +70,7 @@ func main() { } h := handler{ - affinityinjector.NewAffinityInjector(clientset, opts.ConfigMapName), + injector.NewInjector(clientset, opts.Namespace, opts.ConfigMapName), } mux.HandleFunc("/mutate", h.mutate) diff --git a/deployments/base/clusterrole.yaml b/deployments/base/clusterrole.yaml new file mode 100644 index 0000000..52d455c --- /dev/null +++ b/deployments/base/clusterrole.yaml @@ -0,0 +1,9 @@ +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: namespace-node-affinity +rules: +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["mutatingwebhookconfigurations"] + verbs: ["get", "create", "update"] diff --git a/deployments/base/clusterrolebinding.yaml b/deployments/base/clusterrolebinding.yaml new file mode 100644 index 0000000..f5e3352 --- /dev/null +++ b/deployments/base/clusterrolebinding.yaml @@ -0,0 +1,14 @@ +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: namespace-node-affinity +subjects: +- kind: ServiceAccount + name: namespace-node-affinity + namespace: default + apiGroup: "" +roleRef: + kind: ClusterRole + name: namespace-node-affinity + apiGroup: rbac.authorization.k8s.io diff --git a/deployments/base/deployment.yaml b/deployments/base/deployment.yaml index c329efc..4b56a6a 100644 --- a/deployments/base/deployment.yaml +++ b/deployments/base/deployment.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -26,9 +27,13 @@ spec: memory: 64Mi env: - name: CERT - value: &cert /etc/webhook/certs/tls.crt + value: /etc/webhook/certs/tls.crt - name: KEY - value: &key /etc/webhook/certs/tls.key + value: /etc/webhook/certs/tls.key + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace initContainers: - name: init-webhook image: idgenchev/namespace-node-affinity-init-container @@ -43,9 +48,9 @@ spec: - name: SERVICE_NAME value: namespace-node-affinity - name: CERT - value: *cert + value: /etc/webhook/certs/tls.crt - name: KEY - value: *key + value: /etc/webhook/certs/tls.key volumes: - name: webhook-certs emptyDir: {} diff --git a/deployments/base/kustomization.yaml b/deployments/base/kustomization.yaml index 50f10f9..5b76930 100644 --- a/deployments/base/kustomization.yaml +++ b/deployments/base/kustomization.yaml @@ -1,3 +1,4 @@ +--- apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization @@ -8,5 +9,7 @@ resources: - deployment.yaml - role.yaml - rolebinding.yaml +- clusterrole.yaml +- clusterrolebinding.yaml - sa.yaml - service.yaml diff --git a/deployments/base/role.yaml b/deployments/base/role.yaml index f407f76..628e2f3 100644 --- a/deployments/base/role.yaml +++ b/deployments/base/role.yaml @@ -1,11 +1,10 @@ -kind: ClusterRole +--- +kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: namespace-node-affinity + namespace: default rules: -- apiGroups: ["admissionregistration.k8s.io"] - resources: ["mutatingwebhookconfigurations"] - verbs: ["get", "create", "update"] - apiGroups: [""] resources: ["configmaps"] verbs: ["get"] diff --git a/deployments/base/rolebinding.yaml b/deployments/base/rolebinding.yaml index ce80716..12eb136 100644 --- a/deployments/base/rolebinding.yaml +++ b/deployments/base/rolebinding.yaml @@ -1,13 +1,15 @@ -kind: ClusterRoleBinding +--- +kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: namespace-node-affinity + namespace: default subjects: - kind: ServiceAccount name: namespace-node-affinity namespace: default apiGroup: "" roleRef: - kind: ClusterRole + kind: Role name: namespace-node-affinity apiGroup: rbac.authorization.k8s.io diff --git a/deployments/base/sa.yaml b/deployments/base/sa.yaml index 504b79b..5eea04b 100644 --- a/deployments/base/sa.yaml +++ b/deployments/base/sa.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: ServiceAccount metadata: diff --git a/deployments/base/service.yaml b/deployments/base/service.yaml index 8fcb443..954ae0a 100644 --- a/deployments/base/service.yaml +++ b/deployments/base/service.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Service metadata: diff --git a/deployments/overlays/local/deployment.yaml b/deployments/overlays/local/deployment.yaml index 3b76840..4f6b18d 100644 --- a/deployments/overlays/local/deployment.yaml +++ b/deployments/overlays/local/deployment.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apps/v1 kind: Deployment metadata: diff --git a/deployments/overlays/local/kustomization.yaml b/deployments/overlays/local/kustomization.yaml index 54d3e49..0a19545 100644 --- a/deployments/overlays/local/kustomization.yaml +++ b/deployments/overlays/local/kustomization.yaml @@ -1,3 +1,4 @@ +--- apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization diff --git a/examples/example_namespace.yaml b/examples/example_namespace.yaml deleted file mode 100644 index 684f3b1..0000000 --- a/examples/example_namespace.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: testing-ns - labels: - namespace-node-affinity: enabled diff --git a/examples/example_namespaces.yaml b/examples/example_namespaces.yaml new file mode 100644 index 0000000..95e2de1 --- /dev/null +++ b/examples/example_namespaces.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: testing-ns + labels: + namespace-node-affinity: enabled +--- +apiVersion: v1 +kind: Namespace +metadata: + name: testing-ns-b + labels: + namespace-node-affinity: enabled +--- +apiVersion: v1 +kind: Namespace +metadata: + name: testing-ns-c + labels: + namespace-node-affinity: enabled +--- +apiVersion: v1 +kind: Namespace +metadata: + name: testing-ns-d + labels: + namespace-node-affinity: enabled +--- +apiVersion: v1 +kind: Namespace +metadata: + name: testing-ns-e + labels: + namespace-node-affinity: enabled diff --git a/examples/sample_configmap.yaml b/examples/sample_configmap.yaml index c48bc4b..7cf1f1f 100644 --- a/examples/sample_configmap.yaml +++ b/examples/sample_configmap.yaml @@ -1,12 +1,35 @@ +--- apiVersion: v1 kind: ConfigMap metadata: name: namespace-node-affinity - namespace: testing-ns + namespace: default data: - nodeSelectorTerms: | - - matchExpressions: - - key: the-testing-key - operator: In - values: - - the-testing-val1 + testing-ns: | + nodeSelectorTerms: + - matchExpressions: + - key: the-testing-key + operator: In + values: + - the-testing-val1 + tolerations: + - key: "example-key" + operator: "Exists" + effect: "NoSchedule" + testing-ns-b: | + nodeSelectorTerms: + - matchExpressions: + - key: the-testing-key + operator: In + values: + - the-testing-val1 + testing-ns-c: | + tolerations: + - key: "example-key" + operator: "Exists" + effect: "NoSchedule" + testing-ns-d: | + invalid: + - key: "example-key" + operator: "Exists" + effect: "NoSchedule" diff --git a/examples/sample_pod.yaml b/examples/sample_pod.yaml new file mode 100644 index 0000000..0ff9207 --- /dev/null +++ b/examples/sample_pod.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: null + labels: + run: nginx-test + name: nginx-test + namespace: testing-ns +spec: + containers: + - image: nginx + name: nginx-test + resources: {} + dnsPolicy: ClusterFirst + restartPolicy: Always +status: {} diff --git a/pkg/affinityinjector/affinityinjector.go b/injector/injector.go similarity index 54% rename from pkg/affinityinjector/affinityinjector.go rename to injector/injector.go index e477041..3aab885 100644 --- a/pkg/affinityinjector/affinityinjector.go +++ b/injector/injector.go @@ -1,5 +1,5 @@ -// Package affinityinjector deals with AdmissionReview requests and responses -package affinityinjector +// Package injector deals with AdmissionReview requests and responses +package injector import ( "context" @@ -22,25 +22,29 @@ var ( ErrFailedToCreatePatch = errors.New("failed to create patch") ErrFailedToReadNodeSelectorTerms = errors.New("failed to load node selector terms") ErrMissingConfiguration = errors.New("missing configuration") - ErrMissingNodeSelectorTerms = errors.New("missing nodeSelectorTerms from config") ErrInvalidConfiguration = errors.New("invalid configuration") ) -// AffinityPath is the path for the JSON patch -type AffinityPath string +// PatchPath is the path for the JSON patch +type PatchPath string -// AffinityPath values +// PatchPath values const ( + // affinity CreateAffinity = "/spec/affinity" CreateNodeAffinity = "/spec/affinity/nodeAffinity" AddRequiredDuringScheduling = "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution" AddNodeSelectorTerms = "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution/nodeSelectorTerms/-" + // tolerations + CreateTolerations = "/spec/tolerations" + AddTolerations = "/spec/tolerations/-" ) const ( - cmKey = "nodeSelectorTerms" - successStatus = "Success" - annotationKey = "namespace-node-affinity.idgenchev.github.com/applied-patch" + nodeSelectorKey = "nodeSelectorTerms" + tolerationsKey = "tolerations" + successStatus = "Success" + annotationKey = "namespace-node-affinity.idgenchev.github.com/applied-patch" ) var ( @@ -52,27 +56,34 @@ var ( // JSONPatch is the JSON patch (http://jsonpatch.com) for patching k8s // object type JSONPatch struct { - Op string `json:"op"` - Path AffinityPath `json:"path"` - Value interface{} `json:"value"` + Op string `json:"op"` + Path PatchPath `json:"path"` + Value interface{} `json:"value"` } -// AffinityInjector handles AdmissionReview objects -type AffinityInjector struct { +// NamespaceConfig is the per-namespace configuration +type NamespaceConfig struct { + NodeSelectorTerms []corev1.NodeSelectorTerm `json:"nodeSelectorTerms"` + Tolerations []corev1.Toleration `json:"tolerations"` +} + +// Injector handles AdmissionReview objects +type Injector struct { clientset k8sclient.Interface + namespace string configMapName string } -// NewAffinityInjector returns *AffinityInjector with k8sclient and configMapName -func NewAffinityInjector(k8sclient k8sclient.Interface, configMapName string) *AffinityInjector { - return &AffinityInjector{k8sclient, configMapName} +// NewInjector returns *Injector with k8sclient and configMapName +func NewInjector(k8sclient k8sclient.Interface, namespace string, configMapName string) *Injector { + return &Injector{k8sclient, namespace, configMapName} } -// Mutate unmarshalls the AdmissionReview (body) and creates or -// updates the nodeAffinity of the k8s object in the admission review -// request, sets the AdmissionReview response and returns the -// marshalled AdmissionReview or an error -func (m *AffinityInjector) Mutate(body []byte) ([]byte, error) { +// Mutate unmarshalls the AdmissionReview (body) and creates or updates the +// nodeAffinity and/or the tolerations of the k8s object in the admission +// review request, sets the AdmissionReview response and returns the marshalled +// AdmissionReview or an error +func (m *Injector) Mutate(body []byte) ([]byte, error) { log.Infof("Received AdmissionReview: %s\n", string(body)) // unmarshal request into AdmissionReview struct @@ -101,17 +112,17 @@ func (m *AffinityInjector) Mutate(body []byte) ([]byte, error) { jsonPatch := v1beta1.PatchTypeJSONPatch resp.PatchType = &jsonPatch - namespace := req.Namespace - if namespace == "" { - namespace = "default" + podNamespace := req.Namespace + if podNamespace == "" { + podNamespace = "default" } - nodeSelectorTerms, err := m.nodeSelectorTerms(namespace) + + config, err := m.configForNamespace(podNamespace) if err != nil { return nil, err } - patchPath := buildPath(pod.Spec) - patch, err := buildPatch(patchPath, nodeSelectorTerms) + patch, err := buildPatch(config, pod.Spec) if err != nil { return nil, err } @@ -138,31 +149,33 @@ func (m *AffinityInjector) Mutate(body []byte) ([]byte, error) { return responseBody, nil } -func (m *AffinityInjector) nodeSelectorTerms(namespace string) ([]corev1.NodeSelectorTerm, error) { - // Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.ConfigMap, error) +func (m *Injector) configForNamespace(namespace string) (*NamespaceConfig, error) { configMap, err := m.clientset.CoreV1(). - ConfigMaps(namespace). + ConfigMaps(m.namespace). Get(context.Background(), m.configMapName, metav1.GetOptions{}) + if err != nil { return nil, fmt.Errorf("%w: %s", ErrMissingConfiguration, err) } - nodeSelectorTermsString, exists := configMap.Data[cmKey] + namespaceConfigString, exists := configMap.Data[namespace] if !exists { - return nil, fmt.Errorf("%w: nodeSelectorTerms is missing from the config map", ErrMissingNodeSelectorTerms) + return nil, fmt.Errorf("%w: for %s", ErrMissingConfiguration, namespace) } - var nodeSelectorTerms []corev1.NodeSelectorTerm - err = yamlUnmarshal([]byte(nodeSelectorTermsString), &nodeSelectorTerms) + config := &NamespaceConfig{} + err = yamlUnmarshal([]byte(namespaceConfigString), config) if err != nil { return nil, fmt.Errorf("%w: %s", ErrInvalidConfiguration, err) + } else if config.NodeSelectorTerms == nil && config.Tolerations == nil { + return nil, fmt.Errorf("%w: at least one of nodeSelectorTerms or tolerations needs to be specified for %s", ErrInvalidConfiguration, namespace) } - return nodeSelectorTerms, nil + return config, nil } -func buildPath(podSpec corev1.PodSpec) AffinityPath { - var path AffinityPath +func buildNodeSelectorTermsPath(podSpec corev1.PodSpec) PatchPath { + var path PatchPath if podSpec.Affinity == nil { path = CreateAffinity @@ -177,7 +190,14 @@ func buildPath(podSpec corev1.PodSpec) AffinityPath { return path } -func buildPatch(path AffinityPath, nodeSelectorTerms []corev1.NodeSelectorTerm) ([]byte, error) { +func buildTolerationsPath(podSpec corev1.PodSpec) PatchPath { + if podSpec.Tolerations == nil { + return CreateTolerations + } + return AddTolerations +} + +func buildNodeSelectorTermsPatch(path PatchPath, nodeSelectorTerms []corev1.NodeSelectorTerm) (JSONPatch, error) { patch := JSONPatch{ Op: "add", Path: path, @@ -201,13 +221,42 @@ func buildPatch(path AffinityPath, nodeSelectorTerms []corev1.NodeSelectorTerm) case CreateAffinity: patch.Value = patchAffinity default: - return nil, fmt.Errorf("%w: invalid patch path", ErrFailedToCreatePatch) + return JSONPatch{}, fmt.Errorf("%w: invalid patch path", ErrFailedToCreatePatch) + } + + return patch, nil +} + +func buildPatch(config *NamespaceConfig, podSpec corev1.PodSpec) ([]byte, error) { + patches := []JSONPatch{} + + if config.NodeSelectorTerms != nil { + nodeSelectorTermsPatchPath := buildNodeSelectorTermsPath(podSpec) + nodeSelectorTermsPatch, err := buildNodeSelectorTermsPatch(nodeSelectorTermsPatchPath, config.NodeSelectorTerms) + if err != nil { + return nil, err + } + + patches = append(patches, nodeSelectorTermsPatch) + } + + if config.Tolerations != nil { + tolerationsPatchPath := buildTolerationsPath(podSpec) + for _, toleration := range config.Tolerations { + tolerationsPatch := JSONPatch{ + Op: "add", + Path: tolerationsPatchPath, + Value: toleration, + } + + patches = append(patches, tolerationsPatch) + } } - patchString, err := jsonMarshal([]JSONPatch{patch}) + patch, err := jsonMarshal(patches) if err != nil { return nil, fmt.Errorf("%w: %s", ErrFailedToCreatePatch, err) } - return patchString, nil + return patch, nil } diff --git a/injector/injector_test.go b/injector/injector_test.go new file mode 100644 index 0000000..9d93ab0 --- /dev/null +++ b/injector/injector_test.go @@ -0,0 +1,475 @@ +package injector + +import ( + "encoding/json" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + v1beta1 "k8s.io/api/admission/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" +) + +func nodeSelectorTerms() []corev1.NodeSelectorTerm { + return []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "key", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"val"}, + }, + }, + }, + } +} + +func tolerations() []corev1.Toleration { + return []corev1.Toleration{ + { + Key: "example-key", + Operator: corev1.TolerationOpExists, + Value: "example-value", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "example-key-b", + Operator: corev1.TolerationOpExists, + Value: "example-value-b", + Effect: corev1.TaintEffectPreferNoSchedule, + }, + } +} + +func TestBuildNodeSelectorTermPath(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + podSpec corev1.PodSpec + expectedPath PatchPath + }{ + { + name: "WithNoAffinity", + podSpec: corev1.PodSpec{}, + expectedPath: CreateAffinity, + }, + { + name: "WithNoNodeAffinity", + podSpec: corev1.PodSpec{ + Affinity: &corev1.Affinity{PodAffinity: &corev1.PodAffinity{}}, + }, + expectedPath: CreateNodeAffinity, + }, + { + name: "WithPreferredDuringSchedulingIgnoredDuringExecution", + podSpec: corev1.PodSpec{ + Affinity: &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.PreferredSchedulingTerm{}, + }, + }, + }, + expectedPath: AddRequiredDuringScheduling, + }, + { + name: "WithExistingAffinity", + podSpec: corev1.PodSpec{ + Affinity: &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "key", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"val"}, + }, + }, + }, + }, + }, + }, + }, + }, + expectedPath: AddNodeSelectorTerms, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + path := buildNodeSelectorTermsPath(tc.podSpec) + assert.Equal(t, tc.expectedPath, path) + }) + } +} + +func TestBuildTolerationsPath(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + podSpec corev1.PodSpec + expectedPath PatchPath + }{ + { + name: "WithTolerations", + podSpec: corev1.PodSpec{ + Tolerations: tolerations(), + }, + expectedPath: AddTolerations, + }, + { + name: "WithoutTolerations", + podSpec: corev1.PodSpec{}, + expectedPath: CreateTolerations, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + path := buildTolerationsPath(tc.podSpec) + assert.Equal(t, tc.expectedPath, path) + }) + } +} + +func TestBuildNodeSelectorTermsPatch(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + path PatchPath + expectedPatch JSONPatch + }{ + { + name: "ForCreatePatchPath", + path: CreateAffinity, + expectedPatch: JSONPatch{ + Op: "add", + Path: CreateAffinity, + Value: &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: nodeSelectorTerms(), + }, + }, + }, + }, + }, + { + name: "ForCreateNodeAffinity", + path: CreateNodeAffinity, + expectedPatch: JSONPatch{ + Op: "add", + Path: CreateNodeAffinity, + Value: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: nodeSelectorTerms(), + }, + }, + }, + }, + { + name: "ForAddRequiredDuringSchedulingPath", + path: AddRequiredDuringScheduling, + expectedPatch: JSONPatch{ + Op: "add", + Path: AddRequiredDuringScheduling, + Value: &corev1.NodeSelector{ + NodeSelectorTerms: nodeSelectorTerms(), + }, + }, + }, + { + name: "ForAddNodeSelectorTerms", + path: AddNodeSelectorTerms, + expectedPatch: JSONPatch{ + Op: "add", + Path: AddNodeSelectorTerms, + Value: nodeSelectorTerms(), + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + patch, err := buildNodeSelectorTermsPatch(tc.path, nodeSelectorTerms()) + + assert.NoError(t, err) + assert.Equal(t, tc.expectedPatch, patch) + }) + } +} + +func TestBuildNodeSelectorTermsPatchWithInvalidPath(t *testing.T) { + t.Parallel() + + patch, err := buildNodeSelectorTermsPatch("invalid", nodeSelectorTerms()) + assert.Equal(t, JSONPatch{}, patch) + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrFailedToCreatePatch)) +} + +func TestMutateWithInvalidBody(t *testing.T) { + t.Parallel() + + clientset := fake.NewSimpleClientset() + m := Injector{clientset, "default", "cm"} + + body, err := m.Mutate([]byte("invalid")) + + assert.Nil(t, body) + assert.True(t, errors.Is(err, ErrInvalidAdmissionReview)) +} + +func TestMutateWithNoRequest(t *testing.T) { + t.Parallel() + + clientset := fake.NewSimpleClientset() + m := Injector{clientset, "default", "cm"} + + admissionReview := []byte("{}") + + body, err := m.Mutate(admissionReview) + + assert.Nil(t, body) + assert.NoError(t, err) +} + +func TestMutateWithMissingConfigMap(t *testing.T) { + t.Parallel() + + clientset := fake.NewSimpleClientset() + m := Injector{clientset, "default", "test-cm"} + + admissionReview := v1beta1.AdmissionReview{ + Request: &v1beta1.AdmissionRequest{ + Object: runtime.RawExtension{ + Object: &corev1.Pod{}, + }, + }, + } + j, err := json.Marshal(admissionReview) + assert.NoError(t, err) + + body, err := m.Mutate(j) + assert.Nil(t, body) + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrMissingConfiguration)) +} + +func TestMutateWithMissingConfigurationForTheNamespace(t *testing.T) { + t.Parallel() + + deploymentNamespace := "ns-node-affinity" + podNamespace := "test-ns" + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: deploymentNamespace, + }, + Data: map[string]string{"someconfig": "somevalue"}, + } + clientset := fake.NewSimpleClientset(cm) + m := Injector{clientset, deploymentNamespace, "test-cm"} + + admissionReview := v1beta1.AdmissionReview{ + Request: &v1beta1.AdmissionRequest{ + Namespace: podNamespace, + Object: runtime.RawExtension{ + Object: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: podNamespace, + }, + }, + }, + }, + } + j, err := json.Marshal(admissionReview) + assert.NoError(t, err) + + body, err := m.Mutate(j) + assert.Nil(t, body) + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrMissingConfiguration)) +} + +func TestMutateWithInvalidConfigForNamespace(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + }{ + { + name: "nodeSelectorTerms", + }, + { + name: "tolerations", + }, + { + name: "noneoftheexpected", + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + deploymentNamespace := "ns-node-affinity" + podNamespace := "test-ns" + + namespaceConfig := fmt.Sprintf("%s: \"invalid\"", tc.name) + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: deploymentNamespace, + }, + Data: map[string]string{podNamespace: namespaceConfig}, + } + clientset := fake.NewSimpleClientset(cm) + m := Injector{clientset, deploymentNamespace, "test-cm"} + + admissionReview := v1beta1.AdmissionReview{ + Request: &v1beta1.AdmissionRequest{ + Namespace: podNamespace, + Object: runtime.RawExtension{ + Object: &corev1.Pod{}, + }, + }, + } + j, err := json.Marshal(admissionReview) + assert.NoError(t, err) + + body, err := m.Mutate(j) + assert.Nil(t, body) + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrInvalidConfiguration)) + }) + } +} + +func TestMutateWithBuildPatchError(t *testing.T) { + deploymentNamespace := "ns-node-affinity" + podNamespace := "default" + nodeSelectorTermsJSON, _ := json.Marshal(nodeSelectorTerms()) + namespaceConfig := fmt.Sprintf("%s: %s", nodeSelectorKey, nodeSelectorTermsJSON) + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: deploymentNamespace, + }, + Data: map[string]string{podNamespace: namespaceConfig}, + } + clientset := fake.NewSimpleClientset(cm) + m := Injector{clientset, deploymentNamespace, "test-cm"} + + admissionReview := v1beta1.AdmissionReview{ + Request: &v1beta1.AdmissionRequest{ + Object: runtime.RawExtension{ + Object: &corev1.Pod{}, + }, + }, + } + j, err := json.Marshal(admissionReview) + assert.NoError(t, err) + + origMarshal := jsonMarshal + jsonMarshal = func(v interface{}) ([]byte, error) { + return nil, errors.New("some error") + } + defer func() { jsonMarshal = origMarshal }() + + body, err := m.Mutate(j) + assert.Nil(t, body) + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrFailedToCreatePatch)) +} + +func TestMutate(t *testing.T) { + t.Parallel() + + deploymentNamespace := "ns-node-affinity" + podNamespace := "testing-ns" + + nsConfig := NamespaceConfig{ + NodeSelectorTerms: nodeSelectorTerms(), + Tolerations: tolerations(), + } + nsConfigJSON, _ := json.Marshal(nsConfig) + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: deploymentNamespace, + }, + Data: map[string]string{podNamespace: string(nsConfigJSON)}, + } + clientset := fake.NewSimpleClientset(cm) + m := Injector{clientset, deploymentNamespace, "test-cm"} + + admissionReview := v1beta1.AdmissionReview{ + Request: &v1beta1.AdmissionRequest{ + Namespace: podNamespace, + Object: runtime.RawExtension{ + Object: &corev1.Pod{}, + }, + }, + } + j, err := json.Marshal(admissionReview) + assert.NoError(t, err) + + body, err := m.Mutate(j) + assert.NoError(t, err) + + nodeSelectorPatch, _ := buildNodeSelectorTermsPatch(CreateAffinity, nodeSelectorTerms()) + patches := []JSONPatch{ + nodeSelectorPatch, + { + Op: "add", + Path: CreateTolerations, + Value: tolerations()[0], + }, + { + Op: "add", + Path: CreateTolerations, + Value: tolerations()[1], + }, + } + expectedPatch, _ := json.Marshal(patches) + + jsonPatch := v1beta1.PatchTypeJSONPatch + expectedResp := v1beta1.AdmissionResponse{ + PatchType: &jsonPatch, + Allowed: true, + Patch: expectedPatch, + AuditAnnotations: map[string]string{annotationKey: string(expectedPatch)}, + Result: &metav1.Status{Status: successStatus}, + } + + expectedAdmissionReview := admissionReview + expectedAdmissionReview.Response = &expectedResp + + expectedBody, err := json.Marshal(expectedAdmissionReview) + assert.NoError(t, err) + assert.Equal(t, expectedBody, body) +} diff --git a/pkg/affinityinjector/affinityinjector_test.go b/pkg/affinityinjector/affinityinjector_test.go deleted file mode 100644 index db5b072..0000000 --- a/pkg/affinityinjector/affinityinjector_test.go +++ /dev/null @@ -1,357 +0,0 @@ -package affinityinjector - -import ( - "encoding/json" - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - v1beta1 "k8s.io/api/admission/v1beta1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - fake "k8s.io/client-go/kubernetes/fake" -) - -func nodeSelectorTerms() []corev1.NodeSelectorTerm { - return []corev1.NodeSelectorTerm{ - { - MatchExpressions: []corev1.NodeSelectorRequirement{ - { - Key: "key", - Operator: corev1.NodeSelectorOpIn, - Values: []string{"val"}, - }, - }, - }, - } -} - -func nodeSelectorTermsJSON() string { - terms, _ := json.Marshal(nodeSelectorTerms()) - return string(terms) -} - -func TestBuildPath(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - podSpec corev1.PodSpec - expectedPath AffinityPath - }{ - { - name: "WithNoAffinity", - podSpec: corev1.PodSpec{}, - expectedPath: CreateAffinity, - }, - { - name: "WithNoNodeAffinity", - podSpec: corev1.PodSpec{ - Affinity: &corev1.Affinity{PodAffinity: &corev1.PodAffinity{}}, - }, - expectedPath: CreateNodeAffinity, - }, - { - name: "WithPreferredDuringSchedulingIgnoredDuringExecution", - podSpec: corev1.PodSpec{ - Affinity: &corev1.Affinity{ - NodeAffinity: &corev1.NodeAffinity{ - PreferredDuringSchedulingIgnoredDuringExecution: []corev1.PreferredSchedulingTerm{}, - }, - }, - }, - expectedPath: AddRequiredDuringScheduling, - }, - { - name: "WithExistingAffinity", - podSpec: corev1.PodSpec{ - Affinity: &corev1.Affinity{ - NodeAffinity: &corev1.NodeAffinity{ - RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ - NodeSelectorTerms: []corev1.NodeSelectorTerm{ - { - MatchExpressions: []corev1.NodeSelectorRequirement{ - { - Key: "key", - Operator: corev1.NodeSelectorOpIn, - Values: []string{"val"}, - }, - }, - }, - }, - }, - }, - }, - }, - expectedPath: AddNodeSelectorTerms, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - path := buildPath(tc.podSpec) - assert.Equal(t, tc.expectedPath, path) - }) - } -} - -func TestBuildPatch(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - path AffinityPath - value string - }{ - { - name: "ForCreateAffinityPath", - path: CreateAffinity, - value: fmt.Sprintf( - "{\"nodeAffinity\":{\"requiredDuringSchedulingIgnoredDuringExecution\":{\"nodeSelectorTerms\":%s}}}", - nodeSelectorTermsJSON(), - ), - }, - { - name: "ForCreateNodeAffinityPath", - path: CreateNodeAffinity, - value: fmt.Sprintf( - "{\"requiredDuringSchedulingIgnoredDuringExecution\":{\"nodeSelectorTerms\":%s}}", - nodeSelectorTermsJSON(), - ), - }, - { - name: "ForAddRequiredDuringSchedulingPath", - path: AddRequiredDuringScheduling, - value: fmt.Sprintf( - "{\"nodeSelectorTerms\":%s}", - nodeSelectorTermsJSON(), - ), - }, - { - name: "ForAddNodeSelectorTermsPath", - path: AddNodeSelectorTerms, - value: nodeSelectorTermsJSON(), - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - patch, err := buildPatch(tc.path, nodeSelectorTerms()) - - expectedPatch := []byte( - fmt.Sprintf("[{\"op\":\"add\",\"path\":\"%s\",\"value\":%s}]", - tc.path, tc.value, - ), - ) - - assert.NoError(t, err) - assert.Equal(t, expectedPatch, patch) - }) - } -} - -func TestBuildPatchWithInvalidPath(t *testing.T) { - t.Parallel() - - patch, err := buildPatch("invalid", nodeSelectorTerms()) - assert.Nil(t, patch) - assert.Error(t, err) - assert.True(t, errors.Is(err, ErrFailedToCreatePatch)) -} - -func TestMutateWithInvalidBody(t *testing.T) { - t.Parallel() - - clientset := fake.NewSimpleClientset() - m := AffinityInjector{clientset, "cm"} - - body, err := m.Mutate([]byte("invalid")) - - assert.Nil(t, body) - assert.True(t, errors.Is(err, ErrInvalidAdmissionReview)) -} - -func TestMutateWithNoRequest(t *testing.T) { - t.Parallel() - - clientset := fake.NewSimpleClientset() - m := AffinityInjector{clientset, "cm"} - - admissionReview := []byte("{}") - - body, err := m.Mutate(admissionReview) - - assert.Nil(t, body) - assert.NoError(t, err) -} - -func TestMutateWithMissingConfigMap(t *testing.T) { - t.Parallel() - - clientset := fake.NewSimpleClientset() - m := AffinityInjector{clientset, "test-cm"} - - admissionReview := v1beta1.AdmissionReview{ - Request: &v1beta1.AdmissionRequest{ - Object: runtime.RawExtension{ - Object: &corev1.Pod{}, - }, - }, - } - j, err := json.Marshal(admissionReview) - assert.NoError(t, err) - - body, err := m.Mutate(j) - assert.Nil(t, body) - assert.Error(t, err) - assert.True(t, errors.Is(err, ErrMissingConfiguration)) -} - -func TestMutateWithMissingNodeSelectorTerms(t *testing.T) { - t.Parallel() - - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cm", - Namespace: "test-ns", - }, - } - clientset := fake.NewSimpleClientset(cm) - m := AffinityInjector{clientset, "test-cm"} - - admissionReview := v1beta1.AdmissionReview{ - Request: &v1beta1.AdmissionRequest{ - Namespace: "test-ns", - Object: runtime.RawExtension{ - Object: &corev1.Pod{}, - }, - }, - } - j, err := json.Marshal(admissionReview) - assert.NoError(t, err) - - body, err := m.Mutate(j) - assert.Nil(t, body) - assert.Error(t, err) - assert.True(t, errors.Is(err, ErrMissingNodeSelectorTerms)) -} - -func TestMutateWithInvalidNodeSelectorTerms(t *testing.T) { - t.Parallel() - - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cm", - Namespace: "default", - }, - Data: map[string]string{cmKey: "invalid"}, - } - clientset := fake.NewSimpleClientset(cm) - m := AffinityInjector{clientset, "test-cm"} - - admissionReview := v1beta1.AdmissionReview{ - Request: &v1beta1.AdmissionRequest{ - Object: runtime.RawExtension{ - Object: &corev1.Pod{}, - }, - }, - } - j, err := json.Marshal(admissionReview) - assert.NoError(t, err) - - body, err := m.Mutate(j) - assert.Nil(t, body) - assert.Error(t, err) - assert.True(t, errors.Is(err, ErrInvalidConfiguration)) -} - -func TestMutateWithBuildPatchError(t *testing.T) { - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cm", - Namespace: "default", - }, - Data: map[string]string{cmKey: nodeSelectorTermsJSON()}, - } - clientset := fake.NewSimpleClientset(cm) - m := AffinityInjector{clientset, "test-cm"} - - admissionReview := v1beta1.AdmissionReview{ - Request: &v1beta1.AdmissionRequest{ - Object: runtime.RawExtension{ - Object: &corev1.Pod{}, - }, - }, - } - j, err := json.Marshal(admissionReview) - assert.NoError(t, err) - - origMarshal := jsonMarshal - jsonMarshal = func(v interface{}) ([]byte, error) { - fmt.Printf("\n\nthe obj: %#v\n\n", v) - return nil, errors.New("some error") - } - defer func() { jsonMarshal = origMarshal }() - - body, err := m.Mutate(j) - assert.Nil(t, body) - assert.Error(t, err) - assert.True(t, errors.Is(err, ErrFailedToCreatePatch)) -} - -func TestMutate(t *testing.T) { - t.Parallel() - - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cm", - Namespace: "test-ns", - }, - Data: map[string]string{cmKey: nodeSelectorTermsJSON()}, - } - clientset := fake.NewSimpleClientset(cm) - m := AffinityInjector{clientset, "test-cm"} - - admissionReview := v1beta1.AdmissionReview{ - Request: &v1beta1.AdmissionRequest{ - Namespace: "test-ns", - Object: runtime.RawExtension{ - Object: &corev1.Pod{}, - }, - }, - } - j, err := json.Marshal(admissionReview) - assert.NoError(t, err) - - body, err := m.Mutate(j) - assert.NoError(t, err) - - affinity := fmt.Sprintf( - "{\"nodeAffinity\":{\"requiredDuringSchedulingIgnoredDuringExecution\":{\"nodeSelectorTerms\":%s}}}", - nodeSelectorTermsJSON(), - ) - expectedPatch := fmt.Sprintf("[{\"op\":\"add\",\"path\":\"%s\",\"value\":%s}]", CreateAffinity, affinity) - - jsonPatch := v1beta1.PatchTypeJSONPatch - expectedResp := v1beta1.AdmissionResponse{ - PatchType: &jsonPatch, - Allowed: true, - Patch: []byte(expectedPatch), - AuditAnnotations: map[string]string{annotationKey: expectedPatch}, - Result: &metav1.Status{Status: successStatus}, - } - - expectedAdmissionReview := admissionReview - expectedAdmissionReview.Response = &expectedResp - - expectedBody, err := json.Marshal(expectedAdmissionReview) - assert.NoError(t, err) - assert.Equal(t, expectedBody, body) -} diff --git a/pkg/webhookconfig/webhookconfig.go b/webhookconfig/webhookconfig.go similarity index 100% rename from pkg/webhookconfig/webhookconfig.go rename to webhookconfig/webhookconfig.go diff --git a/pkg/webhookconfig/webhookconfig_test.go b/webhookconfig/webhookconfig_test.go similarity index 100% rename from pkg/webhookconfig/webhookconfig_test.go rename to webhookconfig/webhookconfig_test.go