diff --git a/docs/030_user-guide/120_customization.md b/docs/030_user-guide/120_customization.md index e8576f98..94126c67 100644 --- a/docs/030_user-guide/120_customization.md +++ b/docs/030_user-guide/120_customization.md @@ -86,6 +86,7 @@ Below are the available Helm override configurations after you have built your P | Parameter | Description | Example Values | |---------------------------------|-------------------------------------------|------------------------------------------------| +| `additionalIgnoredNamespaces` | Namespaces to ignore in addition to alwaysIgnore.namespaces from Pepr config in `package.json`. | `- pepr-playground` | | `secrets.apiToken` | Kube API-Server Token. | `Buffer.from(apiToken).toString("base64")` | | `hash` | Unique hash for deployment. Do not change.| `` | | `namespace.annotations` | Namespace annotations | `{}` | diff --git a/src/lib/assets/helm.ts b/src/lib/assets/helm.ts index 2b65f368..e73574f9 100644 --- a/src/lib/assets/helm.ts +++ b/src/lib/assets/helm.ts @@ -115,6 +115,10 @@ export function watcherDeployTemplate(buildTimestamp: string): string { {{- toYaml .Values.watcher.env | nindent 12 }} - name: PEPR_WATCH_MODE value: "true" + {{- if .Values.additionalIgnoredNamespaces }} + - name: PEPR_ADDITIONAL_IGNORED_NAMESPACES + value: "{{ join ", " .Values.additionalIgnoredNamespaces }}" + {{- end }} envFrom: {{- toYaml .Values.watcher.envFrom | nindent 12 }} securityContext: @@ -195,6 +199,10 @@ export function admissionDeployTemplate(buildTimestamp: string): string { {{- toYaml .Values.admission.env | nindent 12 }} - name: PEPR_WATCH_MODE value: "false" + {{- if .Values.additionalIgnoredNamespaces }} + - name: PEPR_ADDITIONAL_IGNORED_NAMESPACES + value: "{{ join ", " .Values.additionalIgnoredNamespaces }}" + {{- end }} envFrom: {{- toYaml .Values.admission.envFrom | nindent 12 }} securityContext: diff --git a/src/lib/assets/index.test.ts b/src/lib/assets/index.test.ts new file mode 100644 index 00000000..0d21faf5 --- /dev/null +++ b/src/lib/assets/index.test.ts @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The Pepr Authors +import { it, describe, expect } from "@jest/globals"; +import { createWebhookYaml } from "./index"; +import { kind } from "kubernetes-fluent-client"; + +describe("createWebhookYaml", () => { + const webhookConfiguration = new kind.MutatingWebhookConfiguration(); + webhookConfiguration.apiVersion = "admissionregistration.k8s.io/v1"; + webhookConfiguration.kind = "MutatingWebhookConfiguration"; + webhookConfiguration.metadata = { name: "pepr-static-test" }; + webhookConfiguration.webhooks = [ + { + name: "pepr-static-test.pepr.dev", + admissionReviewVersions: ["v1", "v1beta1"], + clientConfig: { + caBundle: "", + service: { + name: "pepr-static-test", + namespace: "pepr-system", + path: "", + }, + }, + failurePolicy: "Fail", + matchPolicy: "Equivalent", + timeoutSeconds: 15, + namespaceSelector: { + matchExpressions: [ + { + key: "kubernetes.io/metadata.name", + operator: "NotIn", + values: ["kube-system", "pepr-system", "something"], + }, + ], + }, + sideEffects: "None", + }, + ]; + + const moduleConfig = { + onError: "reject", + webhookTimeout: 15, + uuid: "some-uuid", + alwaysIgnore: { + namespaces: ["kube-system", "pepr-system"], + }, + }; + + it("replaces placeholders in the YAML correctly", () => { + const result = createWebhookYaml("pepr-static-test", moduleConfig, webhookConfiguration); + console.log(result); + expect(result).toContain("{{ .Values.uuid }}"); + expect(result).toContain("{{ .Values.admission.failurePolicy }}"); + expect(result).toContain("{{ .Values.admission.webhookTimeout }}"); + expect(result).toContain("- pepr-system"); + expect(result).toContain("- kube-system"); + expect(result).toContain("{{- range .Values.additionalIgnoredNamespaces }}"); + expect(result).toContain("{{ . }}"); + expect(result).toContain("{{- end }}"); + }); +}); diff --git a/src/lib/assets/index.ts b/src/lib/assets/index.ts index 235de0e8..c91a9843 100644 --- a/src/lib/assets/index.ts +++ b/src/lib/assets/index.ts @@ -12,21 +12,45 @@ export function toYaml(obj: any): string { return dumpYaml(obj, { noRefs: true }); } +// Unit Test Me!! export function createWebhookYaml( name: string, config: ModuleConfig, webhookConfiguration: kind.MutatingWebhookConfiguration | kind.ValidatingWebhookConfiguration, ): string { const yaml = toYaml(webhookConfiguration); - return replaceString( - replaceString( - replaceString(yaml, name, "{{ .Values.uuid }}"), - config.onError === "reject" ? "Fail" : "Ignore", - "{{ .Values.admission.failurePolicy }}", - ), - `${config.webhookTimeout}` || "10", - "{{ .Values.admission.webhookTimeout }}", - ); + const replacements = [ + { search: name, replace: "{{ .Values.uuid }}" }, + { + search: config.onError === "reject" ? "Fail" : "Ignore", + replace: "{{ .Values.admission.failurePolicy }}", + }, + { + search: `${config.webhookTimeout}` || "10", + replace: "{{ .Values.admission.webhookTimeout }}", + }, + { + search: ` + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - pepr-system +`, + replace: ` + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - pepr-system + {{- range .Values.additionalIgnoredNamespaces }} + - {{ . }} + {{- end }} +`, + }, + ]; + + return replacements.reduce((updatedYaml, { search, replace }) => replaceString(updatedYaml, search, replace), yaml); } export function helmLayout(basePath: string, unique: string): Record> { diff --git a/src/lib/assets/webhooks.test.ts b/src/lib/assets/webhooks.test.ts new file mode 100644 index 00000000..5872d98b --- /dev/null +++ b/src/lib/assets/webhooks.test.ts @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The Pepr Authors +import { it, describe, expect } from "@jest/globals"; +import { resolveIgnoreNamespaces, peprIgnoreNamespaces } from "./webhooks"; + +describe("peprIgnoreNamespaces", () => { + it("should have order of kube-system, then pepr-system for the helm templating", () => { + expect(peprIgnoreNamespaces).toEqual(["kube-system", "pepr-system"]); + expect(peprIgnoreNamespaces[0]).toEqual("kube-system"); + expect(peprIgnoreNamespaces[1]).toEqual("pepr-system"); + }); +}); + +describe("resolveIgnoreNamespaces", () => { + it("should default to empty array ig config is empty", () => { + const result = resolveIgnoreNamespaces(); + expect(result).toEqual([]); + }); + + it("should return the config ignore namespaces if not provided PEPR_ADDITIONAL_IGNORED_NAMESPACES is not provided", () => { + const result = resolveIgnoreNamespaces(["payments", "istio-system"]); + expect(result).toEqual(["payments", "istio-system"]); + }); + + it("should include additionalIgnoredNamespaces when PEPR_ADDITIONAL_IGNORED_NAMESPACES is provided", () => { + process.env.PEPR_ADDITIONAL_IGNORED_NAMESPACES = "uds, project-fox"; + const result = resolveIgnoreNamespaces(["zarf", "lula"]); + expect(result).toEqual(["uds", "project-fox", "zarf", "lula"]); + }); +}); diff --git a/src/lib/assets/webhooks.ts b/src/lib/assets/webhooks.ts index a96ede64..1443293d 100644 --- a/src/lib/assets/webhooks.ts +++ b/src/lib/assets/webhooks.ts @@ -13,7 +13,7 @@ import { Assets } from "./assets"; import { Event } from "../enums"; import { Binding } from "../types"; -const peprIgnoreNamespaces: string[] = ["kube-system", "pepr-system"]; +export const peprIgnoreNamespaces: string[] = ["kube-system", "pepr-system"]; const validateRule = (binding: Binding, isMutateWebhook: boolean): V1RuleWithOperations | undefined => { const { event, kind, isMutate, isValidate } = binding; @@ -39,6 +39,21 @@ const validateRule = (binding: Binding, isMutateWebhook: boolean): V1RuleWithOpe return ruleObject; }; +export function resolveIgnoreNamespaces(ignoredNSConfig: string[] = []): string[] { + const ignoredNSEnv = process.env.PEPR_ADDITIONAL_IGNORED_NAMESPACES; + if (!ignoredNSEnv) { + return ignoredNSConfig; + } + + const namespaces = ignoredNSEnv.split(",").map(ns => ns.trim()); + + // add alwaysIgnore.namespaces to the list + if (ignoredNSConfig) { + namespaces.push(...ignoredNSConfig); + } + return namespaces.filter(ns => ns.length > 0); +} + export async function generateWebhookRules(assets: Assets, isMutateWebhook: boolean): Promise { const { config, capabilities } = assets; @@ -61,7 +76,7 @@ export async function webhookConfig( const ignore: V1LabelSelectorRequirement[] = []; const { name, tls, config, apiToken, host } = assets; - const ignoreNS = concat(peprIgnoreNamespaces, config?.alwaysIgnore?.namespaces || []); + const ignoreNS = concat(peprIgnoreNamespaces, resolveIgnoreNamespaces(config?.alwaysIgnore?.namespaces)); // Add any namespaces to ignore if (ignoreNS) { diff --git a/src/lib/assets/yaml.ts b/src/lib/assets/yaml.ts index ffb9a852..67db4e74 100644 --- a/src/lib/assets/yaml.ts +++ b/src/lib/assets/yaml.ts @@ -41,6 +41,7 @@ export async function overridesFile( const rbacOverrides = clusterRole(name, capabilities, config.rbacMode, config.rbac).rules; const overrides = { + additionalIgnoredNamespaces: [], rbac: rbacOverrides, secrets: { apiToken: Buffer.from(apiToken).toString("base64"), diff --git a/src/lib/core/module.ts b/src/lib/core/module.ts index 427261a5..dde53af8 100644 --- a/src/lib/core/module.ts +++ b/src/lib/core/module.ts @@ -9,6 +9,7 @@ import { CapabilityExport, AdmissionRequest } from "../types"; import { setupWatch } from "../processors/watch-processor"; import { Log } from "../../lib"; import { V1PolicyRule as PolicyRule } from "@kubernetes/client-node"; +import { resolveIgnoreNamespaces } from "../assets/webhooks"; /** Custom Labels Type for package.json */ export interface CustomLabels { @@ -113,7 +114,7 @@ export class PeprModule { // Wait for the controller to be ready before setting up watches if (isWatchMode() || isDevMode()) { try { - setupWatch(capabilities, pepr?.alwaysIgnore?.namespaces); + setupWatch(capabilities, resolveIgnoreNamespaces(pepr?.alwaysIgnore?.namespaces)); } catch (e) { Log.error(e, "Error setting up watch"); process.exit(1); diff --git a/src/lib/processors/mutate-processor.ts b/src/lib/processors/mutate-processor.ts index bf1ad044..5cc9f52a 100644 --- a/src/lib/processors/mutate-processor.ts +++ b/src/lib/processors/mutate-processor.ts @@ -14,6 +14,7 @@ import { ModuleConfig } from "../core/module"; import { PeprMutateRequest } from "../mutate-request"; import { base64Encode, convertFromBase64Map, convertToBase64Map } from "../utils"; import { OnError } from "../../cli/init/enums"; +import { resolveIgnoreNamespaces } from "../assets/webhooks"; export interface Bindable { req: AdmissionRequest; @@ -169,7 +170,7 @@ export async function mutateProcessor( bind.binding, bind.req, bind.namespaces, - bind.config?.alwaysIgnore?.namespaces, + resolveIgnoreNamespaces(bind.config?.alwaysIgnore?.namespaces), ); if (shouldSkip !== "") { Log.debug(shouldSkip); diff --git a/src/lib/processors/validate-processor.ts b/src/lib/processors/validate-processor.ts index ebb98efb..1fbd7372 100644 --- a/src/lib/processors/validate-processor.ts +++ b/src/lib/processors/validate-processor.ts @@ -10,6 +10,7 @@ import Log from "../telemetry/logger"; import { convertFromBase64Map } from "../utils"; import { PeprValidateRequest } from "../validate-request"; import { ModuleConfig } from "../core/module"; +import { resolveIgnoreNamespaces } from "../assets/webhooks"; export async function processRequest( binding: Binding, @@ -78,7 +79,12 @@ export async function validateProcessor( } // Continue to the next action without doing anything if this one should be skipped - const shouldSkip = shouldSkipRequest(binding, req, namespaces, config?.alwaysIgnore?.namespaces); + const shouldSkip = shouldSkipRequest( + binding, + req, + namespaces, + resolveIgnoreNamespaces(config?.alwaysIgnore?.namespaces), + ); if (shouldSkip !== "") { Log.debug(shouldSkip); continue;