Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,16 +138,17 @@ 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.

### Capability

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.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/capabilities.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
109 changes: 105 additions & 4 deletions src/lib/assets/assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down Expand Up @@ -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 () => {
Comment thread
AmberFryar marked this conversation as resolved.
const mockHelm = {
files: {
admissionDeploymentYaml: "/tmp/admission-deployment.yaml",
Expand All @@ -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 () => {
Expand All @@ -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<V1MutatingWebhookConfiguration | V1ValidatingWebhookConfiguration | null>
>()
.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();
Expand All @@ -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 () => {
Expand Down
42 changes: 24 additions & 18 deletions src/lib/assets/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,24 +182,6 @@ export class Assets {
mutateWebhook: V1MutatingWebhookConfiguration | V1ValidatingWebhookConfiguration | null,
helm: Record<string, Record<string, string>>,
): Promise<void> => {
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,
Expand All @@ -215,6 +197,26 @@ export class Assets {
}
};

writeAdmissionControllerFiles = async (
helm: Record<string, Record<string, string>>,
): Promise<void> => {
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,
Expand Down Expand Up @@ -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);
Expand Down
Loading