diff --git a/README.md b/README.md index df1035e3c..644366c30 100644 --- a/README.md +++ b/README.md @@ -138,8 +138,9 @@ kubectl apply -f capabilities/hello-pepr.samples.yaml A module is the top-level collection of capabilities. It is a single, complete TypeScript project that includes an entry point to load all the configuration and capabilities, along with their actions. -During the Pepr build process, each module produces a unique Kubernetes MutatingWebhookConfiguration and ValidatingWebhookConfiguration, along with a secret containing the transpiled and compressed TypeScript code. -The webhooks and secret are deployed into the Kubernetes cluster with their own isolated controller. +During the Pepr build process, each module produces a secret containing the transpiled and compressed TypeScript code. +Modules with `Mutate()` or `Validate()` actions also produce the corresponding Kubernetes MutatingWebhookConfiguration or ValidatingWebhookConfiguration resources. +Modules without admission, watch, queue, finalize, or schedule actions still deploy their module code with an isolated admission controller, but do not produce webhook configuration resources unless admission actions are defined. See [Module](docs/user-guide/pepr-modules.md) for more details. @@ -147,7 +148,7 @@ See [Module](docs/user-guide/pepr-modules.md) for more details. A capability is set of related actions that work together to achieve a specific transformation or operation on Kubernetes resources. Capabilities are user-defined and can include one or more actions. -They are defined within a Pepr module and can be used in both MutatingWebhookConfigurations and ValidatingWebhookConfigurations. +They are defined within a Pepr module and can use MutatingWebhookConfigurations, ValidatingWebhookConfigurations, watchers, queues, or schedules depending on the actions they contain. A Capability can have a specific scope, such as mutating or validating, and can be reused in multiple Pepr modules. See [Capabilities](docs/user-guide/capabilities.md) for more details. diff --git a/docs/reference/best-practices.md b/docs/reference/best-practices.md index 7c6bd11ff..fc360fdb1 100644 --- a/docs/reference/best-practices.md +++ b/docs/reference/best-practices.md @@ -254,7 +254,7 @@ Pepr can monitor Mutations and Validations from Admission Controller the through ## Multiple Modules or Multiple Capabilities -Each module has it's own Mutating, Validating webhook configurations, Admission and Watch Controllers and Stores. This allows for each module to be deployed independently of each other. However, creating multiple modules creates overhead on the kube-apiserver, and the cluster. +Each module can have its own MutatingWebhookConfiguration, ValidatingWebhookConfiguration, Admission Controller, Watch Controller, and Store depending on the actions it contains. This allows for each module to be deployed independently of each other. However, creating multiple modules creates overhead on the kube-apiserver, and the cluster. Due to the overhead costs, it is recommended to deploy multiple capabilities that share the same resources (when possible). This will simplify analysis of which capabilities are responsible for changes on resources. diff --git a/docs/user-guide/capabilities.md b/docs/user-guide/capabilities.md index 6da2bfca3..40bd709bb 100644 --- a/docs/user-guide/capabilities.md +++ b/docs/user-guide/capabilities.md @@ -1,6 +1,6 @@ # Pepr Capabilities -A capability is set of related [actions](../actions/README.md) that work together to achieve a specific transformation or operation on Kubernetes resources. Capabilities are user-defined and can include one or more actions. They are defined within a Pepr module and can be used in both MutatingWebhookConfigurations and ValidatingWebhookConfigurations. A Capability can have a specific scope, such as mutating or validating, and can be reused in multiple Pepr modules. +A capability is set of related [actions](../actions/README.md) that work together to achieve a specific transformation or operation on Kubernetes resources. Capabilities are user-defined and can include one or more actions. They are defined within a Pepr module and can use MutatingWebhookConfigurations, ValidatingWebhookConfigurations, watchers, queues, or schedules depending on the actions they contain. A Capability can have a specific scope, such as mutating or validating, and can be reused in multiple Pepr modules. When you [`npx pepr init`](./pepr-cli.md#pepr-init), a `capabilities` directory is created for you. This directory is where you will define your capabilities. You can create as many capabilities as you need, and each capability can contain one or more actions. Pepr also automatically creates a `HelloPepr` capability with a number of example actions to help you get started. diff --git a/src/lib/assets/assets.test.ts b/src/lib/assets/assets.test.ts index 4ae421932..3f3f257fa 100644 --- a/src/lib/assets/assets.test.ts +++ b/src/lib/assets/assets.test.ts @@ -171,6 +171,10 @@ vi.mock("./index", () => ({ serviceAccountYaml: "/tmp/service-account.yaml", moduleSecretYaml: "/tmp/module-secret.yaml", valuesYaml: "/tmp/values.yaml", + admissionDeploymentYaml: "/tmp/admission-deployment.yaml", + admissionServiceMonitorYaml: "/tmp/admission-service-monitor.yaml", + mutationWebhookYaml: "/tmp/mutation-webhook.yaml", + validationWebhookYaml: "/tmp/validation-webhook.yaml", watcherDeploymentYaml: "/tmp/watcher-deployment.yaml", watcherServiceMonitorYaml: "/tmp/watcher-service-monitor.yaml", }, @@ -295,7 +299,7 @@ describe("Assets", () => { ); }); - it("should call writeWebhookFiles and write admissionController Deployment, ServiceMonitor, and WebhookConfigs", async () => { + it("should call writeWebhookFiles and write WebhookConfigs", async () => { const mockHelm = { files: { admissionDeploymentYaml: "/tmp/admission-deployment.yaml", @@ -307,9 +311,21 @@ describe("Assets", () => { const validateWebhook: V1ValidatingWebhookConfiguration = new kind.ValidatingWebhookConfiguration(); const mutateWebhook: V1MutatingWebhookConfiguration = new kind.MutatingWebhookConfiguration(); + (fs.writeFile as Mock).mockClear(); + await assets.writeWebhookFiles(validateWebhook, mutateWebhook, mockHelm); - expect(fs.writeFile).toHaveBeenCalledTimes(4); + expect(fs.writeFile).toHaveBeenCalledTimes(2); + expect(fs.writeFile).toHaveBeenCalledWith("/tmp/mutation-webhook.yaml", expect.any(String)); + expect(fs.writeFile).toHaveBeenCalledWith("/tmp/validation-webhook.yaml", expect.any(String)); + expect(fs.writeFile).not.toHaveBeenCalledWith( + "/tmp/admission-deployment.yaml", + expect.any(String), + ); + expect(fs.writeFile).not.toHaveBeenCalledWith( + "/tmp/admission-service-monitor.yaml", + expect.any(String), + ); }); it("should call generateHelmChart which should call createDirectoryIfNotExists twice for templates and charts", async () => { @@ -336,7 +352,90 @@ describe("Assets", () => { expect(createDirectoryIfNotExists).toHaveBeenCalledTimes(2); }); - it("should call generateHelmChart which should write file 40 times for built Kubernetes Manifests and helm chart generation", async () => { + it("should write admission controller files and WebhookConfigs for admission chart capabilities", async () => { + const webhookGeneratorFunction = createMockWebhookGenerator(); + const getWatcherFunction = vi.fn<() => kind.Deployment | null>().mockReturnValue(null); + const getModuleSecretFunction = createMockModuleSecret(); + assets.capabilities = [ + { + name: "capability-1", + description: "test", + namespaces: ["default"], + bindings: [{ isMutate: true }] as unknown as Binding[], + hasSchedule: false, + }, + ]; + (fs.writeFile as Mock).mockClear(); + + await assets.generateHelmChart( + webhookGeneratorFunction, + getWatcherFunction, + getModuleSecretFunction, + "/tmp", + ); + + const admissionAndWebhookFiles = [ + "/tmp/admission-deployment.yaml", + "/tmp/admission-service-monitor.yaml", + "/tmp/mutation-webhook.yaml", + "/tmp/validation-webhook.yaml", + ]; + const admissionAndWebhookWrites = (fs.writeFile as Mock).mock.calls.filter(([file]) => + admissionAndWebhookFiles.includes(file), + ); + + expect(admissionAndWebhookWrites.map(([file]) => file).sort()).toEqual( + admissionAndWebhookFiles.sort(), + ); + }); + + it("should write admission Deployment for charts when capabilities have no admission or watcher bindings", async () => { + const webhookGeneratorFunction = vi + .fn< + ( + assets: Assets, + mutateOrValidate: WebhookType, + timeoutSeconds: number | undefined, + ) => Promise + >() + .mockResolvedValue(null); + const getWatcherFunction = vi.fn<() => kind.Deployment | null>().mockReturnValue(null); + const getModuleSecretFunction = createMockModuleSecret(); + assets.capabilities = [ + { + name: "capability-1", + description: "test", + namespaces: ["default"], + bindings: [] as unknown as Binding[], + hasSchedule: false, + }, + ]; + + (fs.writeFile as Mock).mockClear(); + + await assets.generateHelmChart( + webhookGeneratorFunction, + getWatcherFunction, + getModuleSecretFunction, + "/tmp", + ); + + expect(fs.writeFile).toHaveBeenCalledWith( + "/tmp/admission-deployment.yaml", + expect.stringContaining("kind: Deployment"), + ); + expect(fs.writeFile).toHaveBeenCalledWith( + "/tmp/admission-service-monitor.yaml", + expect.stringContaining("kind: ServiceMonitor"), + ); + expect(fs.writeFile).not.toHaveBeenCalledWith("/tmp/mutation-webhook.yaml", expect.any(String)); + expect(fs.writeFile).not.toHaveBeenCalledWith( + "/tmp/validation-webhook.yaml", + expect.any(String), + ); + }); + + it("should call generateHelmChart which should write expected chart files", async () => { const webhookGeneratorFunction = createMockWebhookGenerator(); const getWatcherFunction = createMockWatcher(); const getModuleSecretFunction = createMockModuleSecret(); @@ -349,13 +448,15 @@ describe("Assets", () => { hasSchedule: false, }, ]; + (fs.writeFile as Mock).mockClear(); + await assets.generateHelmChart( webhookGeneratorFunction, getWatcherFunction, getModuleSecretFunction, "/tmp", ); - expect(fs.writeFile).toHaveBeenCalledTimes(40); + expect(fs.writeFile).toHaveBeenCalledTimes(16); }); it("should call generateHelmChart and get no error", async () => { diff --git a/src/lib/assets/assets.ts b/src/lib/assets/assets.ts index 92d357480..65defdbdb 100644 --- a/src/lib/assets/assets.ts +++ b/src/lib/assets/assets.ts @@ -182,24 +182,6 @@ export class Assets { mutateWebhook: V1MutatingWebhookConfiguration | V1ValidatingWebhookConfiguration | null, helm: Record>, ): Promise => { - if (validateWebhook || mutateWebhook) { - await fs.writeFile( - helm.files.admissionDeploymentYaml, - dedent(admissionDeployTemplate(this.buildTimestamp, "admission")), - ); - await fs.writeFile( - helm.files.admissionServiceMonitorYaml, - dedent( - serviceMonitorTemplate( - process.env.PEPR_CUSTOM_BUILD_NAME - ? `admission-${process.env.PEPR_CUSTOM_BUILD_NAME}` - : "admission", - `admission`, - ), - ), - ); - } - if (mutateWebhook) { await fs.writeFile( helm.files.mutationWebhookYaml, @@ -215,6 +197,26 @@ export class Assets { } }; + writeAdmissionControllerFiles = async ( + helm: Record>, + ): Promise => { + await fs.writeFile( + helm.files.admissionDeploymentYaml, + dedent(admissionDeployTemplate(this.buildTimestamp, "admission")), + ); + await fs.writeFile( + helm.files.admissionServiceMonitorYaml, + dedent( + serviceMonitorTemplate( + process.env.PEPR_CUSTOM_BUILD_NAME + ? `admission-${process.env.PEPR_CUSTOM_BUILD_NAME}` + : "admission", + `admission`, + ), + ), + ); + }; + generateHelmChart = async ( webhookGeneratorFunction: ( assets: Assets, @@ -299,6 +301,10 @@ export class Assets { ), }; + if (isAdmission(this.capabilities) || norWatchOrAdmission(this.capabilities)) { + await this.writeAdmissionControllerFiles(helm); + } + await this.writeWebhookFiles(webhooks.validate, webhooks.mutate, helm); const watchDeployment = getWatcherFunction(this, moduleHash, this.buildTimestamp);