-
Notifications
You must be signed in to change notification settings - Fork 38
feat!: only allow creation of one UDSPackage per namespace
#1372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 8aab60b
lint fix: pepr format
noahpb 77066cd
Merge branch 'main' into feat/single-pkg-namespace
noahpb 72a7627
wip: package watch
noahpb d45ed74
feat: add watch for UDSPackages
noahpb d4f2bbf
Merge branch 'main' into feat/single-pkg-namespace
noahpb 8190126
adding `PackageStore.init`, adding `hasKey` and `getPkgName` function…
noahpb 886bf71
Merge branch 'main' into feat/single-pkg-namespace
noahpb 28f1de6
`npx pepr format`
noahpb 205d303
Merge branch 'main' into feat/single-pkg-namespace
noahpb 0a58f06
Merge branch 'main' into feat/single-pkg-namespace
noahpb 73f467e
always validate packages with `deletionTimestamp`
noahpb 8e9c997
multi dimensional package map
noahpb 43f5123
Merge branch 'main' into feat/single-pkg-namespace
noahpb ce704d3
add license
noahpb 02c2fda
update comments
noahpb b7cf5b8
update `app-curl` test to use single pkg per ns
noahpb f319098
pepr format
noahpb 40d13cc
fix spelling mistakes
noahpb 1d14f70
update
noahpb b7557f4
Merge branch 'main' into feat/single-pkg-namespace
noahpb c392354
update docs and diagram
noahpb aceef72
Merge branch 'main' into feat/single-pkg-namespace
noahpb a652074
update diagram
noahpb 20c2c63
address feedback
noahpb c1e2cc7
addressing more feedback
noahpb e98d060
address more feedback
noahpb 577c69c
`npx pepr format`
noahpb b395029
Merge branch 'main' into feat/single-pkg-namespace
noahpb 570d5f7
update diagram
noahpb 06665f2
Merge branch 'main' into feat/single-pkg-namespace
noahpb 1951989
nit: fix arrow color
noahpb 8b7dfdd
Merge branch 'main' into feat/single-pkg-namespace
noahpb 39f9c3b
only init `packageNamespaceMap` on `Package.Store.init()`; add error …
noahpb 017f58d
add watch for modified packages, support updating existing package in…
noahpb c9d7234
Merge branch 'main' into feat/single-pkg-namespace
noahpb 16a858b
Merge branch 'main' into feat/single-pkg-namespace
noahpb 4c93682
fix logic in pkg update
noahpb 6140532
switch to map of map stucture for package store
noahpb 40c41c9
consildate switch cases
noahpb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
354
docs/.images/diagrams/uds-core-pepr-operator-flow.drawio
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
77 changes: 77 additions & 0 deletions
77
src/pepr/operator/controllers/packages/package-store.spec.ts
|
chance-coleman marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
143
src/pepr/operator/controllers/packages/package-store.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
|
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 { | ||
|
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, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.