Skip to content
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

feat: sanitizing store keys - store path v2 #918

Merged
merged 45 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
e80b667
chore: base64 encode keys and add more testing
cmwylie19 Jun 27, 2024
ce22183
chore: update test
cmwylie19 Jun 27, 2024
744f0cd
chore: reformat
cmwylie19 Jun 27, 2024
6073bd3
chore: update empty string fuzzing
cmwylie19 Jun 27, 2024
d787ed4
chore: change build back
cmwylie19 Jun 27, 2024
b499a30
chore: fix journey tests
cmwylie19 Jun 27, 2024
3637462
Merge branch 'main' into 915
cmwylie19 Jun 28, 2024
59733e7
Merge branch 'main' into 915
cmwylie19 Jul 10, 2024
0870d2f
Merge branch 'main' into 915
cmwylie19 Jul 11, 2024
4747705
Merge branch 'main' into 915
cmwylie19 Jul 12, 2024
24a84b1
chore: store sanitization wip
cmwylie19 Jul 12, 2024
b94d620
chore: pepr-dev test
cmwylie19 Jul 12, 2024
8eadbb1
chore: change path
cmwylie19 Jul 12, 2024
1dc25ab
chore: journey test
cmwylie19 Jul 13, 2024
fa38b51
Merge branch 'main' into 915
cmwylie19 Jul 13, 2024
5552f1f
update entry point'
cmwylie19 Jul 13, 2024
ecf0a05
chore: remove unnecessary
cmwylie19 Jul 13, 2024
6d1c09b
chore: update journey test
cmwylie19 Jul 13, 2024
e040fb6
re-think test
cmwylie19 Jul 13, 2024
0cf107a
chore: update e2e test
cmwylie19 Jul 13, 2024
d918b98
chore: fixed the wait timeout problem with no wait null key assertion
cmwylie19 Jul 14, 2024
1dfe105
chore: assert undefined not null
cmwylie19 Jul 14, 2024
770f815
Update journey/entrypoint.test.ts
cmwylie19 Jul 15, 2024
4e55ed4
Update journey/pepr-deploy.ts
cmwylie19 Jul 15, 2024
896358c
Update journey/entrypoint.test.ts
cmwylie19 Jul 15, 2024
ca56892
Update journey/pepr-deploy.ts
cmwylie19 Jul 15, 2024
9717ee2
chore: update removeItemAndWait and setItemAndWait
cmwylie19 Jul 15, 2024
2e49d15
chore: prefix version without dash
cmwylie19 Jul 15, 2024
3591864
chore: e2e test
cmwylie19 Jul 16, 2024
2c8a1b3
chore: update migrate
cmwylie19 Jul 16, 2024
80e63ae
chore: restore migrate
cmwylie19 Jul 16, 2024
2f832ef
Merge branch 'main' into 915
cmwylie19 Jul 16, 2024
986d532
Update src/lib/storage.ts
cmwylie19 Jul 17, 2024
5043953
Update src/lib/storage.ts
cmwylie19 Jul 17, 2024
d656c51
Update src/lib/storage.ts
cmwylie19 Jul 17, 2024
00b5667
Update src/lib/storage.ts
cmwylie19 Jul 17, 2024
9ae3905
Update src/lib/storage.ts
cmwylie19 Jul 17, 2024
565b792
Update src/lib/storage.ts
cmwylie19 Jul 17, 2024
a5272df
Merge branch 'main' into 915
cmwylie19 Jul 17, 2024
9c964b6
Merge branch 'main' into 915
cmwylie19 Jul 17, 2024
a5bd544
chore: format yaml
cmwylie19 Jul 17, 2024
c4f7424
Merge branch 'main' into 915
cmwylie19 Jul 23, 2024
c4ee896
Merge branch 'main' into 915
cmwylie19 Jul 23, 2024
490060b
Update src/lib/storage.test.ts
cmwylie19 Jul 23, 2024
21811da
Update src/lib/storage.test.ts
cmwylie19 Jul 23, 2024
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
8 changes: 8 additions & 0 deletions journey/k8s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export async function waitForDeploymentReady(namespace: string, name: string) {
}
}

export async function noWaitPeprStoreKey(name: string, matchKey: string) {
const store = await K8s(PeprStore).InNamespace("pepr-system").Get(name);
if (store.data[matchKey]) {
return store.data[matchKey];
}
}


export async function waitForPeprStoreKey(name: string, matchKey: string) {
try {
const store = await K8s(PeprStore).InNamespace("pepr-system").Get(name);
Expand Down
63 changes: 55 additions & 8 deletions journey/pepr-deploy.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors

import { beforeAll, jest, afterAll, describe, expect, it } from "@jest/globals";
import { execSync, spawnSync, spawn, ChildProcess } from "child_process";
import { describe, expect, it } from "@jest/globals";
import { execSync, spawnSync, spawn } from "child_process";
import { K8s, kind } from "kubernetes-fluent-client";
import { resolve } from "path";

import { destroyModule } from "../src/lib/assets/destroy";
import { cwd } from "./entrypoint.test";
import {
deleteConfigMap,
noWaitPeprStoreKey,
waitForConfigMap,
waitForDeploymentReady,
waitForNamespace,
Expand All @@ -23,7 +23,18 @@ export function peprDeploy() {
// Purge the Pepr module from the cluster before running the tests
destroyModule("pepr-static-test");


it("should deploy the Pepr controller into the test cluster", async () => {
// Apply the store crd and pepr-system ns
await applyStoreCRD();

// Apply the store
await applyLegacyStoreResource();

/*
* when controller starts up, it will migrate the store
* and later on the keys will be tested to validate the migration
*/
execSync("npx pepr deploy -i pepr:dev --confirm", { cwd, stdio: "inherit" });

// Wait for the deployments to be ready
Expand All @@ -50,7 +61,7 @@ export function peprDeploy() {

it("npx pepr monitor should display validation results to console", async () => {
await testValidate();

const cmd = ['pepr', 'monitor', 'static-test']

const proc = spawn('npx', cmd, { shell: true })
Expand All @@ -68,7 +79,7 @@ export function peprDeploy() {
proc.stderr.destroy()
}
})

proc.on('exit', () => state.done = true);

await until(() => state.done)
Expand Down Expand Up @@ -164,6 +175,7 @@ function testIgnore() {
expect(cm.metadata?.labels?.["pepr"]).toBeUndefined();
});
}

cmwylie19 marked this conversation as resolved.
Show resolved Hide resolved
async function testValidate() {
// Apply the sample yaml for the HelloPepr capability
const applyOut = spawnSync("kubectl apply -f hello-pepr.samples.yaml", {
Expand Down Expand Up @@ -260,22 +272,57 @@ function testMutate() {
});
}


function testStore() {
it("should create the PeprStore", async () => {
const resp = await waitForPeprStoreKey("pepr-static-test-store", "__pepr_do_not_delete__");
expect(resp).toBe("k-thx-bye");
});

it("should write the correct data to the PeprStore", async () => {
const key1 = await waitForPeprStoreKey("pepr-static-test-store", "hello-pepr-example-1");
const key1 = await waitForPeprStoreKey("pepr-static-test-store", `hello-pepr-v2-example-1`);
expect(key1).toBe("was-here");

const key2 = await waitForPeprStoreKey("pepr-static-test-store", "hello-pepr-example-1-data");
// Should have been migrated and removed
const nullKey1 = await noWaitPeprStoreKey("pepr-static-test-store", `hello-pepr-example-1`);
expect(nullKey1).toBeUndefined();

const key2 = await waitForPeprStoreKey("pepr-static-test-store", `hello-pepr-v2-example-1-data`);
expect(key2).toBe(JSON.stringify({ key: "ex-1-val" }));

// Should have been migrated and removed
const nullKey2 = await noWaitPeprStoreKey("pepr-static-test-store", `hello-pepr-example-1-data`);
expect(nullKey1).toBeUndefined();
});

it("should write the correct data to the PeprStore from a Watch Action", async () => {
const key = await waitForPeprStoreKey("pepr-static-test-store", "hello-pepr-watch-data");
const key = await waitForPeprStoreKey("pepr-static-test-store", `hello-pepr-v2-watch-data`);
expect(key).toBe("This data was stored by a Watch Action.");
});
}



async function applyStoreCRD() {
// Apply the store crd
const appliedStoreCRD = spawnSync("kubectl apply -f journey/resources/pepr-store-crd.yaml", {
shell: true, // Run command in a shell
encoding: "utf-8", // Encode result as string
cwd: resolve(cwd, ".."),
});
const { stdout } = appliedStoreCRD;

expect(stdout).toContain("customresourcedefinition.apiextensions.k8s.io/peprstores.pepr.dev");
}

async function applyLegacyStoreResource() {
// Apply the store
const appliedStore = spawnSync("kubectl apply -f journey/resources/non-migrated-peprstore.yaml", {
shell: true, // Run command in a shell
encoding: "utf-8", // Encode result as string
cwd: resolve(cwd, ".."),
});
const { stdout } = appliedStore;

expect(stdout).toContain("peprstore.pepr.dev/pepr-static-test-store");
}
btlghrants marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 2 additions & 3 deletions journey/pepr-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { ChildProcessWithoutNullStreams, spawn } from "child_process";
import { Agent } from "https";
import { fetch } from "kubernetes-fluent-client";
import { RequestInit } from "node-fetch";

import { cwd } from "./entrypoint.test";
import { sleep } from "./k8s";

Expand All @@ -25,8 +24,8 @@ let expectedLines = [
"Validate Action configured for CREATE",
"Server listening on port 3000",
"Controller startup complete",
`"hello-pepr-example-1-data": "{\\"key\\":\\"ex-1-val\\"}"`,
`"hello-pepr-watch-data": "This data was stored by a Watch Action."`,
`"hello-pepr-v2-example-1-data": "{\\"key\\":\\"ex-1-val\\"}"`,
`"hello-pepr-v2-watch-data": "This data was stored by a Watch Action."`,
];

export function peprDev() {
Expand Down
10 changes: 10 additions & 0 deletions journey/resources/non-migrated-peprstore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: pepr.dev/v1
data:
__pepr_do_not_delete__: k-thx-bye
hello-pepr-example-1: was-here
hello-pepr-example-1-data: '{"key":"ex-1-val"}'
hello-pepr-watch-data: "This data was stored by a Watch Action."
kind: PeprStore
metadata:
name: pepr-static-test-store
namespace: pepr-system
34 changes: 34 additions & 0 deletions journey/resources/pepr-store-crd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: peprstores.pepr.dev
spec:
conversion:
strategy: None
group: pepr.dev
names:
kind: PeprStore
listKind: PeprStoreList
plural: peprstores
singular: peprstore
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
properties:
data:
additionalProperties:
type: string
type: object
type: object
served: true
storage: true
---
apiVersion: v1
kind: Namespace
metadata:
labels:
kubernetes.io/metadata.name: pepr-system
name: pepr-system
spec: {}
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@types/ramda": "0.30.1",
"express": "4.19.2",
"fast-json-patch": "3.1.1",
"json-pointer": "^0.6.2",
"kubernetes-fluent-client": "2.6.4",
"pino": "9.3.1",
"pino-pretty": "11.2.1",
Expand All @@ -48,11 +49,12 @@
"@jest/globals": "29.7.0",
"@types/eslint": "8.56.10",
"@types/express": "4.17.21",
"@types/json-pointer": "^1.0.34",
"@types/node": "18.x.x",
"@types/node-forge": "1.3.11",
"@types/prompts": "2.4.9",
"fast-check": "^3.19.0",
"@types/uuid": "10.0.0",
"fast-check": "^3.19.0",
"jest": "29.7.0",
"nock": "13.5.4",
"ts-jest": "29.2.2"
Expand Down
90 changes: 88 additions & 2 deletions src/lib/controller/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ export class PeprControllerStore {
K8s(PeprStore)
.InNamespace(namespace)
.Get(this.#name)
// If the get succeeds, setup the watch
.then(this.#setupWatch)
// If the get succeeds, migrate and setup the watch
.then(async (store: PeprStore) => await this.#migrateAndSetupWatch(store))
// Otherwise, create the resource
.catch(this.#createStoreResource),
Math.random() * 3000,
Expand All @@ -74,6 +74,91 @@ export class PeprControllerStore {
watcher.start().catch(e => Log.error(e, "Error starting Pepr store watch"));
};

#migrateAndSetupWatch = async (store: PeprStore) => {
Log.debug(store, "Pepr Store migration");
const data: DataStore = store.data || {};
const migrateCache: Record<string, Operation> = {};

// Send the cached updates to the cluster
const flushCache = async () => {
const indexes = Object.keys(migrateCache);
const payload = Object.values(migrateCache);

// Loop over each key in the cache and delete it to avoid collisions with other sender calls
for (const idx of indexes) {
delete migrateCache[idx];
}

try {
// Send the patch to the cluster
await K8s(PeprStore, { namespace, name: this.#name }).Patch(payload);
} catch (err) {
Log.error(err, "Pepr store update failure");

if (err.status === 422) {
Object.keys(migrateCache).forEach(key => delete migrateCache[key]);
} else {
// On failure to update, re-add the operations to the cache to be retried
for (const idx of indexes) {
migrateCache[idx] = payload[Number(idx)];
}
}
}
};

const fillCache = (name: string, op: DataOp, key: string[], val?: string) => {
if (op === "add") {
// adjust the path for the capability
const path = `/data/${name}-v2-${key}`;
const value = val || "";
const cacheIdx = [op, path, value].join(":");

// Add the operation to the cache
migrateCache[cacheIdx] = { op, path, value };

return;
}

if (op === "remove") {
if (key.length < 1) {
throw new Error(`Key is required for REMOVE operation`);
}

for (const k of key) {
const path = `/data/${name}-${k}`;
const cacheIdx = [op, path].join(":");

// Add the operation to the cache
migrateCache[cacheIdx] = { op, path };
}

return;
}

// If we get here, the operation is not supported
throw new Error(`Unsupported operation: ${op}`);
};

for (const name of Object.keys(this.#stores)) {
// Get the prefix offset for the keys
const offset = `${name}-`.length;

// Loop over each key in the store
for (const key of Object.keys(data)) {
// Match on the capability name as a prefix for non v2 keys
if (startsWith(name, key) && !startsWith(`${name}-v2`, key)) {
// populate migrate cache
fillCache(name, "remove", [key.slice(offset)], data[key]);
fillCache(name, "add", [key.slice(offset)], data[key]);
}
}

// await K8s(PeprStore, { namespace, name: this.#name }).Patch(payload);
}
await flushCache();
this.#setupWatch();
};

#receive = (store: PeprStore) => {
Log.debug(store, "Pepr Store update");

Expand Down Expand Up @@ -121,6 +206,7 @@ export class PeprControllerStore {
// Load the sendCache with patch operations
const fillCache = (op: DataOp, key: string[], val?: string) => {
if (op === "add") {
// adjust the path for the capability
const path = `/data/${capabilityName}-${key}`;
const value = val || "";
const cacheIdx = [op, path, value].join(":");
Expand Down
Loading
Loading