Skip to content

Commit

Permalink
feat: rbac overrides in package.json (#1331)
Browse files Browse the repository at this point in the history
## Description
We want to leverage package.json to defined RBAC settings that will work
with RBAC and create an excellent example test.

Describe the solution you'd like
Given we want to create a reproducible way to generate scoped RBAC
When we define the RBAC in package.json and run a build
Then we have our customized RBAC
...

End to End Test:  <!-- if applicable -->  
(See [Pepr Excellent
Examples](https://github.com/defenseunicorns/pepr-excellent-examples))

## Related Issue

Fixes #814 
<!-- or -->
Relates to #

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Other (security config, docs update, etc)

## Checklist before merging
- [ ] Unit,
[Journey](https://github.com/defenseunicorns/pepr/tree/main/journey),
[E2E Tests](https://github.com/defenseunicorns/pepr-excellent-examples),
[docs](https://github.com/defenseunicorns/pepr/tree/main/docs),
[adr](https://github.com/defenseunicorns/pepr/tree/main/adr) added or
updated as needed
- [ ] [Contributor Guide
Steps](https://docs.pepr.dev/main/contribute/#submitting-a-pull-request)
followed

---------

Co-authored-by: Case Wylie <cmwylie19@defenseunicorns.com>
  • Loading branch information
schaeferka and cmwylie19 authored Oct 24, 2024
1 parent c1c0de7 commit 2b0d353
Show file tree
Hide file tree
Showing 13 changed files with 921 additions and 70 deletions.
32 changes: 28 additions & 4 deletions docs/030_user-guide/120_customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ You can display warnings in the logs by setting the `PEPR_NODE_WARNINGS` environ

## Customizing Log Format

The log format can be customized by setting the `PINO_TIME_STAMP` environment variable in the `package.json` file or directly on the Watcher or Admission `Deployment`. The default value is a partial JSON timestamp string representation of the time. If set to `iso`, the timestamp is displayed in an ISO format.
The log format can be customized by setting the `PINO_TIME_STAMP` environment variable in the `package.json` file or directly on the Watcher or Admission `Deployment`. The default value is a partial JSON timestamp string representation of the time. If set to `iso`, the timestamp is displayed in an ISO format.

**Caution**: attempting to format time in-process will significantly impact logging performance.
**Caution**: attempting to format time in-process will significantly impact logging performance.

```json
{
Expand All @@ -46,13 +46,13 @@ With ISO:
{"level":30,"time":"2024-05-14T14:26:03.788Z","pid":16,"hostname":"pepr-static-test-7f4d54b6cc-9lxm6","method":"GET","url":"/healthz","status":200,"duration":"1 ms"}
```

Default (without):
Default (without):

```json
{"level":30,"time":"1715696764106","pid":16,"hostname":"pepr-static-test-watcher-559d94447f-xkq2h","method":"GET","url":"/healthz","status":200,"duration":"1 ms"}
```

## Customizing Watch Configuration
## Customizing Watch Configuration

The Watch configuration is a part of the Pepr module that allows you to watch for specific resources in the Kubernetes cluster. The Watch configuration can be customized by specific enviroment variables of the Watcher Deployment and can be set in the field in the `package.json` or in the helm `values.yaml` file.

Expand Down Expand Up @@ -132,5 +132,29 @@ Below are the available configurations through `package.json`.
| `alwaysIgnore` | Conditions to always ignore | `{namespaces: []}` |
| `includedFiles` | For working with WebAssembly | ["main.wasm", "wasm_exec.js"] |
| `env` | Environment variables for the container| `{LOG_LEVEL: "warn"}` |
| `rbac` | Custom RBAC rules | `{"rbac": [{"apiGroups": ["<apiGroups>"], "resources": ["<resources>"], "verbs": ["<verbs>"]}]}` |

These tables provide a comprehensive overview of the fields available for customization within the Helm overrides and the `package.json` file. Modify these according to your deployment requirements.

### Example Custom RBAC Rules

The following example demonstrates how to add custom RBAC rules to the Pepr module.

```json
{
"pepr": {
"rbac": [
{
"apiGroups": ["pepr.dev"],
"resources": ["customresources"],
"verbs": ["get", "list"]
},
{
"apiGroups": ["apps"],
"resources": ["deployments"],
"verbs": ["create", "delete"]
}
]
}
}
```
20 changes: 14 additions & 6 deletions journey/pepr-build-wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { cwd } from "./entrypoint.test";
// test npx pepr build -o dst
const outputDir = "dist/pepr-test-module/child/folder";
export function peprBuild() {

it("should build artifacts in the dst folder", async () => {
await fs.mkdir(outputDir, { recursive: true })
await fs.mkdir(outputDir, { recursive: true });
});

it("should successfully build the Pepr project with arguments", async () => {
execSync(`npx pepr build -r gchr.io/defenseunicorns --rbac-mode scoped -o ${outputDir}`, { cwd: cwd, stdio: "inherit" });
execSync(`npx pepr build -r gchr.io/defenseunicorns --rbac-mode scoped -o ${outputDir}`, {
cwd: cwd,
stdio: "inherit",
});
});

it("should generate produce the K8s yaml file", async () => {
Expand All @@ -37,18 +39,24 @@ export function peprBuild() {

async function validateClusterRoleYaml() {
// Read the generated yaml files
const k8sYaml = await fs.readFile(resolve(cwd, outputDir, "pepr-module-static-test.yaml"), "utf8");
const k8sYaml = await fs.readFile(
resolve(cwd, outputDir, "pepr-module-static-test.yaml"),
"utf8",
);
const cr = await fs.readFile(resolve("journey", "resources", "clusterrole.yaml"), "utf8");

expect(k8sYaml.includes(cr)).toEqual(true)
expect(k8sYaml.includes(cr)).toEqual(true);
}

async function validateZarfYaml() {
// Get the version of the pepr binary
const peprVer = execSync("npx pepr --version", { cwd }).toString().trim();

// Read the generated yaml files
const k8sYaml = await fs.readFile(resolve(cwd, outputDir, "pepr-module-static-test.yaml"), "utf8");
const k8sYaml = await fs.readFile(
resolve(cwd, outputDir, "pepr-module-static-test.yaml"),
"utf8",
);
const zarfYAML = await fs.readFile(resolve(cwd, outputDir, "zarf.yaml"), "utf8");

// The expected image name
Expand Down
83 changes: 57 additions & 26 deletions journey/pepr-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@ import { loadYaml } from "@kubernetes/client-node";
import { execSync } from "child_process";
import { promises as fs } from "fs";
import { resolve } from "path";
import { V1ObjectMeta, KubernetesObject } from '@kubernetes/client-node';
import { V1ObjectMeta, KubernetesObject } from "@kubernetes/client-node";
import yaml from "js-yaml";
import { cwd } from "./entrypoint.test";

const outputDir = "dist/pepr-test-module/child/folder";

export function peprBuild() {
it("should successfully build the Pepr project", async () => {
execSync("npx pepr build", { cwd: cwd, stdio: "inherit" });
validateHelmChart();
});

it("should successfully build the Pepr project with arguments", async () => {
execSync(`npx pepr build -r gchr.io/defenseunicorns --rbac-mode scoped -o ${outputDir}`, {
cwd: cwd,
stdio: "inherit",
});
});

it("should generate produce the K8s yaml file", async () => {
await fs.access(resolve(cwd, "dist", "pepr-module-static-test.yaml"));
});
Expand All @@ -25,6 +34,10 @@ export function peprBuild() {
await validateZarfYaml();
});

it("should generate a scoped ClusterRole", async () => {
await validateClusterRoleYaml();
});

it("should correctly merge in the package.json env vars into the values.yaml helm chart file", async () => {
interface ValuesJSON {
admission: {
Expand All @@ -37,57 +50,73 @@ export function peprBuild() {

const expectedWatcherEnv = [
{
"name": "PEPR_PRETTY_LOG",
"value": "false"
name: "PEPR_PRETTY_LOG",
value: "false",
},
{
"name": "LOG_LEVEL",
"value": "info"
name: "LOG_LEVEL",
value: "info",
},
{
"name": "MY_CUSTOM_VAR",
"value": "example-value"
name: "MY_CUSTOM_VAR",
value: "example-value",
},
{
"name": "ZARF_VAR",
"value": "###ZARF_VAR_THING###"
}
name: "ZARF_VAR",
value: "###ZARF_VAR_THING###",
},
];

const expectedAdmissionEnv = [
{
"name": "PEPR_PRETTY_LOG",
"value": "false"
name: "PEPR_PRETTY_LOG",
value: "false",
},
{
"name": "LOG_LEVEL",
"value": "info"
name: "LOG_LEVEL",
value: "info",
},
{
"name": "MY_CUSTOM_VAR",
"value": "example-value"
name: "MY_CUSTOM_VAR",
value: "example-value",
},
{
"name": "ZARF_VAR",
"value": "###ZARF_VAR_THING###"
}
]
name: "ZARF_VAR",
value: "###ZARF_VAR_THING###",
},
];

try {
const valuesYaml = await fs.readFile(resolve(cwd, "dist", "static-test-chart", "values.yaml"), "utf8");
const valuesYaml = await fs.readFile(
resolve(cwd, "dist", "static-test-chart", "values.yaml"),
"utf8",
);
const valuesJSON = yaml.load(valuesYaml) as ValuesJSON;
expect(valuesJSON.admission.env).toEqual(expectedAdmissionEnv);
expect(valuesJSON.watcher!.env).toEqual(expectedWatcherEnv);
} catch (error) {
expect(error).toBeUndefined();
}
})
});
}

async function validateHelmChart() {
async function validateClusterRoleYaml() {
// Read the generated yaml files
const k8sYaml = await fs.readFile(
resolve(cwd, outputDir, "pepr-module-static-test.yaml"),
"utf8",
);
const cr = await fs.readFile(resolve("journey", "resources", "clusterrole.yaml"), "utf8");


expect(k8sYaml.includes(cr)).toEqual(true);
}

async function validateHelmChart() {
const k8sYaml = await fs.readFile(resolve(cwd, "dist", "pepr-module-static-test.yaml"), "utf8");
const helmOutput = execSync('helm template .', { cwd: `${cwd}/dist/static-test-chart` }).toString();
const helmOutput = execSync("helm template .", {
cwd: `${cwd}/dist/static-test-chart`,
}).toString();

const helmParsed = parseYAMLToJSON(helmOutput);
const k8sParsed = parseYAMLToJSON(k8sYaml);
Expand Down Expand Up @@ -162,8 +191,10 @@ function parseYAMLToJSON(yamlContent: string): KubernetesObject[] | null {
function sortKubernetesObjects(objects: KubernetesObject[]): KubernetesObject[] {
return objects.sort((a, b) => {
if (a?.kind !== b?.kind) {
return (a?.kind ?? '').localeCompare(b?.kind ?? '');
return (a?.kind ?? "").localeCompare(b?.kind ?? "");
}
return ((a && a.metadata && (a.metadata as V1ObjectMeta)?.name) ?? '').localeCompare((b && b.metadata && (b.metadata as V1ObjectMeta)?.name) ?? '');
return ((a && a.metadata && (a.metadata as V1ObjectMeta)?.name) ?? "").localeCompare(
(b && b.metadata && (b.metadata as V1ObjectMeta)?.name) ?? "",
);
});
}
3 changes: 2 additions & 1 deletion src/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export default function (program: RootCmd) {
...cfg.pepr,
appVersion: cfg.version,
description: cfg.description,
rbacMode: opts.rbacMode,
},
path,
);
Expand Down Expand Up @@ -164,7 +165,7 @@ export default function (program: RootCmd) {
const yamlFile = `pepr-module-${uuid}.yaml`;
const chartPath = `${uuid}-chart`;
const yamlPath = resolve(outputDir, yamlFile);
const yaml = await assets.allYaml(opts.rbacMode, opts.withPullSecret);
const yaml = await assets.allYaml(opts.withPullSecret);

try {
// wait for capabilities to be loaded and test names
Expand Down
15 changes: 12 additions & 3 deletions src/lib/assets/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import crypto from "crypto";
import { promises as fs } from "fs";
import { K8s, kind } from "kubernetes-fluent-client";
import { V1PolicyRule as PolicyRule } from "@kubernetes/client-node";

import { Assets } from ".";
import Log from "../logger";
Expand Down Expand Up @@ -84,18 +85,25 @@ export async function deploy(assets: Assets, force: boolean, webhookTimeout?: nu
throw new Error("No code provided");
}

await setupRBAC(name, assets.capabilities, force);
await setupRBAC(name, assets.capabilities, force, assets.config);
await setupController(assets, code, hash, force);
await setupWatcher(assets, hash, force);
}

async function setupRBAC(name: string, capabilities: CapabilityExport[], force: boolean) {
async function setupRBAC(
name: string,
capabilities: CapabilityExport[],
force: boolean,
config: { rbacMode?: string; rbac?: PolicyRule[] },
) {
const { rbacMode, rbac } = config;

Log.info("Applying cluster role binding");
const crb = clusterRoleBinding(name);
await K8s(kind.ClusterRoleBinding).Apply(crb, { force });

Log.info("Applying cluster role");
const cr = clusterRole(name, capabilities);
const cr = clusterRole(name, capabilities, rbacMode, rbac);
await K8s(kind.ClusterRole).Apply(cr, { force });

Log.info("Applying service account");
Expand Down Expand Up @@ -135,6 +143,7 @@ async function setupController(assets: Assets, code: Buffer, hash: string, force
await K8s(kind.Deployment).Apply(dep, { force });
}

// Setup the watcher deployment and service
async function setupWatcher(assets: Assets, hash: string, force: boolean) {
// If the module has a watcher, deploy it
const watchDeployment = watcher(assets, hash, assets.buildTimestamp);
Expand Down
14 changes: 14 additions & 0 deletions src/lib/assets/helm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors

export function clusterRoleTemplate() {
return `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ .Values.uuid }}
namespace: pepr-system
rules:
{{- if .Values.rbac }}
{{- toYaml .Values.rbac | nindent 2 }}
{{- end }}
`;
}

export function nsTemplate() {
return `
apiVersion: v1
Expand Down
20 changes: 12 additions & 8 deletions src/lib/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@ import { allYaml, zarfYaml, overridesFile, zarfYamlChart } from "./yaml";
import { namespaceComplianceValidator, replaceString } from "../helpers";
import { createDirectoryIfNotExists, dedent } from "../helpers";
import { resolve } from "path";
import { chartYaml, nsTemplate, admissionDeployTemplate, watcherDeployTemplate, serviceMonitorTemplate } from "./helm";
import {
chartYaml,
nsTemplate,
admissionDeployTemplate,
watcherDeployTemplate,
clusterRoleTemplate,
serviceMonitorTemplate,
} from "./helm";
import { promises as fs } from "fs";
import { webhookConfig } from "./webhooks";
import { apiTokenSecret, service, tlsSecret, watcherService } from "./networking";
import { watcher, moduleSecret } from "./pods";

import { clusterRole, clusterRoleBinding, serviceAccount, storeRole, storeRoleBinding } from "./rbac";
import { clusterRoleBinding, serviceAccount, storeRole, storeRoleBinding } from "./rbac";
export class Assets {
readonly name: string;
readonly tls: TLSOut;
Expand Down Expand Up @@ -61,14 +68,14 @@ export class Assets {

zarfYamlChart = (path: string) => zarfYamlChart(this, path);

allYaml = async (rbacMode: string, imagePullSecret?: string) => {
allYaml = async (imagePullSecret?: string) => {
this.capabilities = await loadCapabilities(this.path);
// give error if namespaces are not respected
for (const capability of this.capabilities) {
namespaceComplianceValidator(capability, this.alwaysIgnore?.namespaces);
}

return allYaml(this, rbacMode, imagePullSecret);
return allYaml(this, imagePullSecret);
};

generateHelmChart = async (basePath: string) => {
Expand Down Expand Up @@ -123,10 +130,7 @@ export class Assets {
await fs.writeFile(moduleSecretPath, dumpYaml(moduleSecret(this.name, code, this.hash), { noRefs: true }));
await fs.writeFile(storeRolePath, dumpYaml(storeRole(this.name), { noRefs: true }));
await fs.writeFile(storeRoleBindingPath, dumpYaml(storeRoleBinding(this.name), { noRefs: true }));
await fs.writeFile(
clusterRolePath,
dumpYaml(clusterRole(this.name, this.capabilities, "rbac"), { noRefs: true }),
);
await fs.writeFile(clusterRolePath, dedent(clusterRoleTemplate()));
await fs.writeFile(clusterRoleBindingPath, dumpYaml(clusterRoleBinding(this.name), { noRefs: true }));
await fs.writeFile(serviceAccountPath, dumpYaml(serviceAccount(this.name), { noRefs: true }));

Expand Down
Loading

0 comments on commit 2b0d353

Please sign in to comment.