Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
86c45e6
feat: only allow creation of one `UDSPackage` per namespace
noahpb Mar 21, 2025
8aab60b
lint fix: pepr format
noahpb Mar 21, 2025
77066cd
Merge branch 'main' into feat/single-pkg-namespace
noahpb Mar 26, 2025
72a7627
wip: package watch
noahpb Mar 31, 2025
d45ed74
feat: add watch for UDSPackages
noahpb Apr 1, 2025
d4f2bbf
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 1, 2025
8190126
adding `PackageStore.init`, adding `hasKey` and `getPkgName` function…
noahpb Apr 2, 2025
886bf71
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 2, 2025
28f1de6
`npx pepr format`
noahpb Apr 2, 2025
205d303
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 2, 2025
0a58f06
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 3, 2025
73f467e
always validate packages with `deletionTimestamp`
noahpb Apr 3, 2025
8e9c997
multi dimensional package map
noahpb Apr 3, 2025
43f5123
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 4, 2025
ce704d3
add license
noahpb Apr 4, 2025
02c2fda
update comments
noahpb Apr 4, 2025
b7cf5b8
update `app-curl` test to use single pkg per ns
noahpb Apr 4, 2025
f319098
pepr format
noahpb Apr 4, 2025
40d13cc
fix spelling mistakes
noahpb Apr 4, 2025
1d14f70
update
noahpb Apr 4, 2025
b7557f4
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 4, 2025
c392354
update docs and diagram
noahpb Apr 7, 2025
aceef72
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 7, 2025
a652074
update diagram
noahpb Apr 7, 2025
20c2c63
address feedback
noahpb Apr 7, 2025
c1e2cc7
addressing more feedback
noahpb Apr 7, 2025
e98d060
address more feedback
noahpb Apr 7, 2025
577c69c
`npx pepr format`
noahpb Apr 7, 2025
b395029
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 7, 2025
570d5f7
update diagram
noahpb Apr 8, 2025
06665f2
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 8, 2025
1951989
nit: fix arrow color
noahpb Apr 8, 2025
8b7dfdd
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 8, 2025
39f9c3b
only init `packageNamespaceMap` on `Package.Store.init()`; add error …
noahpb Apr 8, 2025
017f58d
add watch for modified packages, support updating existing package in…
noahpb Apr 9, 2025
c9d7234
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 9, 2025
16a858b
Merge branch 'main' into feat/single-pkg-namespace
noahpb Apr 9, 2025
4c93682
fix logic in pkg update
noahpb Apr 9, 2025
6140532
switch to map of map stucture for package store
noahpb Apr 9, 2025
40c41c9
consildate switch cases
noahpb Apr 9, 2025
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
2 changes: 1 addition & 1 deletion docs/.images/diagrams/uds-core-operator-uds-package.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
354 changes: 110 additions & 244 deletions docs/.images/diagrams/uds-core-pepr-operator-flow.drawio

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions docs/reference/configuration/UDS operator/package.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ title: UDS Package
![UDS Operator Package Flowchart](https://github.com/defenseunicorns/uds-core/blob/main/docs/.images/diagrams/uds-core-operator-uds-package.svg?raw=true)
Comment thread
mjnagel marked this conversation as resolved.

## Package
:::note
Only one UDS Package Custom Resource can exist in a namespace. This pattern was chosen by design to better enable workload isolation and to reduce complexity.
:::
Namespaces are a common boundary for isolating workloads in multi-tenant clusters. When defining a UDS Package resource, consider the implications for all workloads that exist in the target namespace. If the UDS Package that you are defining has configurations that conflict with each other or would be simplified by using a separate UDS Package definition, consider using a separate Kubernetes namespace. Read more about namespaces and mulitenancy [here](https://kubernetes.io/docs/concepts/security/multi-tenancy/).

The UDS Operator seamlessly enables the following enhancements and protections for your workloads:
- **Enabling Istio Sidecar Injection:**
- The operator facilitates the activation of Istio sidecar injection within namespaces where the CR is deployed.
- **Support for Istio Ambient Mode:**
Expand Down
4 changes: 3 additions & 1 deletion pepr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import cfg from "./package.json";
import { Component, setupLogger } from "./src/pepr/logger";
import { operator } from "./src/pepr/operator";
import { setupAuthserviceSecret } from "./src/pepr/operator/controllers/keycloak/authservice/config";
import { setupKeycloakClientSecret } from "./src/pepr/operator/controllers/keycloak/config";
import { startPackageWatch } from "./src/pepr/operator/controllers/packages/packages";
import { registerCRDs } from "./src/pepr/operator/crd/register";
import { patches } from "./src/pepr/patches";
import { policies, startExemptionWatch } from "./src/pepr/policies";
import { prometheus } from "./src/pepr/prometheus";
import { setupKeycloakClientSecret } from "./src/pepr/operator/controllers/keycloak/config";

const log = setupLogger(Component.STARTUP);

Expand All @@ -23,6 +24,7 @@ const log = setupLogger(Component.STARTUP);
await registerCRDs();
// KFC watch for exemptions and update in-memory map
await startExemptionWatch();
await startPackageWatch();
await setupAuthserviceSecret();
await setupKeycloakClientSecret();
new PeprModule(cfg, [
Expand Down
1 change: 1 addition & 0 deletions src/pepr/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export enum Component {
OPERATOR_AUTHSERVICE = "operator.authservice",
OPERATOR_MONITORING = "operator.monitoring",
OPERATOR_NETWORK = "operator.network",
OPERATOR_PACKAGES = "operator.packages",
OPERATOR_GENERATORS = "operator.generators",
OPERATOR_CRD = "operator.crd",
OPERATOR_RECONCILERS = "operator.reconcilers",
Expand Down
77 changes: 77 additions & 0 deletions src/pepr/operator/controllers/packages/package-store.spec.ts
Comment thread
chance-coleman marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Copyright 2025 Defense Unicorns
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial
*/

import { describe, expect, it } from "@jest/globals";
import { PeprValidateRequest } from "pepr";
import { UDSPackage } from "../../crd";
import { PackageStore } from "./package-store";
PackageStore.init();

const makeMockReq = (pkg: Partial<UDSPackage>) => {
const defaultPkg: UDSPackage = {
metadata: {
namespace: "application-system",
name: "application",
},
spec: {
network: {
expose: [],
allow: [],
},
sso: [],
monitor: [],
},
};
return {
Raw: { ...defaultPkg, ...pkg },
} as unknown as PeprValidateRequest<UDSPackage>;
};

describe("Package Store", () => {
it("Should add a package", async () => {
const mockReq = makeMockReq({});
const ns = mockReq.Raw.metadata?.namespace || "";
PackageStore.add(mockReq.Raw);
expect(PackageStore.hasKey(ns)).toEqual(true);
});

it("Should add multiple unique packages", async () => {
const mockReq = makeMockReq({});
const ns = mockReq.Raw.metadata?.namespace || "";
const mockReqNewPkg = makeMockReq({ metadata: { namespace: "test", name: "other-package" } });
const nsNewPkg = mockReqNewPkg.Raw.metadata?.namespace || "";
PackageStore.add(mockReqNewPkg.Raw);
let pkgsExist = false;
if (PackageStore.hasKey(ns) && PackageStore.hasKey(nsNewPkg)) {
pkgsExist = true;
}
expect(pkgsExist).toEqual(true);
});

it("Should return the first package in the namespace", async () => {
const mockReq = makeMockReq({});
const ns = mockReq.Raw.metadata?.namespace || "";
const pkgName = mockReq.Raw.metadata?.name || "";
expect(PackageStore.getPkgName(ns)).toEqual(pkgName);
});

it("Should update an existing package", async () => {
const mockReq = makeMockReq({
metadata: { namespace: "test", name: "other-package", labels: { test: "value" } },
});
const ns = mockReq.Raw.metadata?.namespace || "";
const pkgName = mockReq.Raw.metadata?.name || "";
PackageStore.add(mockReq.Raw);
const updatedPackage = PackageStore.getPkgName(ns);
expect(updatedPackage).toEqual(pkgName);
});

it("Should remove a package", async () => {
const mockReq = makeMockReq({});
const ns = mockReq.Raw.metadata?.namespace || "";
PackageStore.remove(mockReq.Raw);
expect(PackageStore.hasKey(ns)).toEqual(false);
});
});
143 changes: 143 additions & 0 deletions src/pepr/operator/controllers/packages/package-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Copyright 2025 Defense Unicorns
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial
*/

/**
* A collection of functions related to watching UDSPackages
* Manages an in-memory map of UDSPackage resources
* Used in Pepr Validating Webhook Pods when vetting UDS Package resources for admission
*/
import { Component, setupLogger } from "../../../logger";
import { UDSPackage } from "../../crd";
const log = setupLogger(Component.OPERATOR_PACKAGES);

// Map structure: namespace -> (package name -> package)
export type PackageNamespaceMap = Map<string, Map<string, UDSPackage>>;
let packageNamespaceMap: PackageNamespaceMap;

/**
* Initializes the package namespace map.
*
* This function creates a new `Map` object and assigns it to the `packageNamespaceMap` variable.
* The `packageNamespaceMap` is used to store packages, using their namespace as the key.
*/
function init(): void {
packageNamespaceMap = new Map();
Comment thread
noahpb marked this conversation as resolved.
}

/**
* Adds a package to the package namespace map.
*
* @param {UDSPackage} pkg - The package to be added. It should contain metadata with a namespace and name.
* @param {boolean} [logger=true] - Optional flag to enable logging. Defaults to true.
*
* This function retrieves the namespace and name from the package metadata and adds the package
* to the packageNamespaceMap. If the namespace doesn't exist, it creates a new map for that namespace.
* The function then adds or updates the package in the namespace map using the package name as the key.
*/
function add(pkg: UDSPackage, logger: boolean = true): void {
Comment thread
noahpb marked this conversation as resolved.
if (!pkg.metadata?.namespace || !pkg.metadata.name) {
throw new Error(`Invalid Package definition, missing namespace or name`);
}
const namespace = pkg.metadata.namespace;
const name = pkg.metadata.name;

// Get or create the namespace map
if (!packageNamespaceMap.has(namespace)) {
packageNamespaceMap.set(namespace, new Map());
}

const namespaceMap = packageNamespaceMap.get(namespace)!;
const isUpdate = namespaceMap.has(name);

// Set the package
namespaceMap.set(name, pkg);

if (logger) {
if (isUpdate) {
log.debug(`Updating PackageStore for package ${name} in namespace ${namespace}.`);
} else {
log.debug(`Added package: ${namespace}/${name} to package map`);
}
}
}

/**
* Removes a package from the package namespace map.
*
* @param {UDSPackage} pkg - The package to be removed. It should contain metadata with a namespace and name.
* @param {boolean} [logger=true] - Optional flag to enable logging. Defaults to true.
*
* This function retrieves the namespace and name from the package metadata and removes the package
* from the packageNamespaceMap. If the namespace map becomes empty after removal, the namespace
* is also removed from the packageNamespaceMap.
*/
function remove(pkg: UDSPackage, logger: boolean = true): void {
if (!pkg.metadata?.namespace || !pkg.metadata.name) {
throw new Error(`Invalid Package definition, missing namespace or name`);
}

const namespace = pkg.metadata.namespace;
const name = pkg.metadata.name;

const namespaceMap = packageNamespaceMap.get(namespace);
if (!namespaceMap) {
// Namespace doesn't exist, nothing to remove
return;
}

// Remove the package
namespaceMap.delete(name);

// If namespace map is empty, remove the namespace
if (namespaceMap.size === 0) {
packageNamespaceMap.delete(namespace);
}

if (logger) {
log.debug(`Removed package: ${namespace}/${name} from package map`);
}
}

/**
* Checks if a given namespace exists within the package namespace map.
*
* This function determines whether a namespace has been previously registered
* in the `packageNamespaceMap`. It provides a way to verify the existence
* of a package in a given namespace.
*
* @param namespace The namespace to check for existence.
* @returns `true` if the namespace exists in the map; otherwise, `false`.
*/
function hasKey(namespace: string): boolean {
return packageNamespaceMap.has(namespace);
}

/**
* Retrieves the package name associated with a given namespace.
*
* This function looks up the namespace in the `packageNamespaceMap` and, if found,
* returns the name of the first package in that namespace.
* If the namespace is not found or there are no packages, it returns null.
*
* @param namespace The namespace to look up in the `packageNamespaceMap`.
* @returns The package name associated with the namespace, or null if not found.
*/
function getPkgName(namespace: string): string | null {
const namespaceMap = packageNamespaceMap.get(namespace);
if (!namespaceMap || namespaceMap.size === 0) {
return null;
}

// Return the name of the first package in the namespace
return Array.from(namespaceMap.keys())[0];
}

export const PackageStore = {
init,
add,
hasKey,
getPkgName,
remove,
};
69 changes: 69 additions & 0 deletions src/pepr/operator/controllers/packages/packages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright 2025 Defense Unicorns
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial
*/

import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
import { PeprValidateRequest } from "pepr";
import { UDSPackage } from "../../crd";
import { PackageStore } from "./package-store";
import { processPackages } from "./packages";

PackageStore.init();

const makeMockReq = (pkg: Partial<UDSPackage>) => {
const defaultPkg: UDSPackage = {
metadata: {
namespace: "application-system",
name: "application",
},
spec: {
network: {
expose: [],
allow: [],
},
sso: [],
monitor: [],
},
};
return {
Raw: { ...defaultPkg, ...pkg },
} as unknown as PeprValidateRequest<UDSPackage>;
};

describe("Package Watch", () => {
it("Should add a package", async () => {
const mockReq = makeMockReq({});
const ns = mockReq.Raw.metadata?.namespace || "";
processPackages(mockReq.Raw, WatchPhase.Added);
const addedPackage = PackageStore.hasKey(ns);
expect(addedPackage).toBe(true);
});

it("Should add multiple unique packages", async () => {
const mockReq = makeMockReq({});
const ns = mockReq.Raw.metadata?.namespace || "";
const mockReqNewPkg = makeMockReq({ metadata: { namespace: "test", name: "other-package" } });
const nsNewPkg = mockReqNewPkg.Raw.metadata?.namespace || "";
processPackages(mockReqNewPkg.Raw, WatchPhase.Added);
let pkgsExist = false;
if (PackageStore.hasKey(ns) && PackageStore.hasKey(nsNewPkg)) {
pkgsExist = true;
}
expect(pkgsExist).toBe(true);
});

it("Should remove packages", async () => {
const mockReq = makeMockReq({});
const mockReqNewPkg = makeMockReq({ metadata: { namespace: "test", name: "other-package" } });
const ns = mockReq.Raw.metadata?.namespace || "";
const nsNewPkg = mockReqNewPkg.Raw.metadata?.namespace || "";
processPackages(mockReq.Raw, WatchPhase.Deleted);
processPackages(mockReqNewPkg.Raw, WatchPhase.Deleted);
let pkgsExist = false;
if (PackageStore.hasKey(ns) && PackageStore.hasKey(nsNewPkg)) {
pkgsExist = true;
}
expect(pkgsExist).toBe(false);
});
});
Loading