From cfcab46b4f2d8c3f741775f25f4e1aaed010bfba Mon Sep 17 00:00:00 2001 From: Barrett LaFrance Date: Fri, 23 Aug 2024 17:42:14 -0500 Subject: [PATCH] refactor: collapse examples / async-ify callbacks --- README.md | 20 +++-- .../capabilities/reconcile.config.yaml | 28 ------- .../capabilities/reconcile.e2e.test.ts | 74 +++++++++---------- .../capabilities/reconcile.ts | 57 ++++++-------- .../capabilities/scenario.resources.yaml | 20 +++++ hello-pepr-reconcile/capabilities/script.sh | 13 ---- 6 files changed, 96 insertions(+), 116 deletions(-) delete mode 100644 hello-pepr-reconcile/capabilities/reconcile.config.yaml create mode 100644 hello-pepr-reconcile/capabilities/scenario.resources.yaml delete mode 100644 hello-pepr-reconcile/capabilities/script.sh diff --git a/README.md b/README.md index edccf8a1..f8af3461 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,27 @@ Here, you'll find practical implementations that demonstrate the use of Pepr in Most examples are designed around a test suite that demonstrates / verifies how they work. -To run all example suites in a single command: +To run all example suites: ```sh npm run test:e2e ``` -To run only a single example suite instead, specify which using `-w` flag: +To run all example suites against a custom version of Pepr, specify which using the `--image` flag: + +```sh +npm run test:e2e -- --image pepr:dev +``` + +To run a single example suite, specify which to run using `-w` flag: ```sh # npm run test:e2e -w -npm run test:e2e -w hello-pepr-validate -- --image pepr:dev +npm run test:e2e -w hello-pepr-validate ``` -To get even more targeted, you can select spec tests to run using some `-- --passthru` magic: +To run a subset of tests, give Jest the specifics via the `--passthru` flag: ```sh # npm run test:e2e -w -- --passthru="" @@ -30,8 +36,12 @@ To get even more targeted, you can select spec tests to run using some `-- --pas npm run test:e2e -w hello-pepr-validate -- --passthru="--testNamePattern='validate creates'" ``` -## Using k9s +## Troubleshooting + +### Viewing the Example Cluster (via K9s) ```sh +# KUBECONFIG=$(k3d kubeconfig write pexex--e2e) k9s + KUBECONFIG=$(k3d kubeconfig write pexex-hello-pepr-reconcile-e2e) k9s ``` diff --git a/hello-pepr-reconcile/capabilities/reconcile.config.yaml b/hello-pepr-reconcile/capabilities/reconcile.config.yaml deleted file mode 100644 index 79e64af6..00000000 --- a/hello-pepr-reconcile/capabilities/reconcile.config.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: pepr-demo ---- -apiVersion: v1 -data: - one: one -kind: ConfigMap -metadata: - name: cm-one - namespace: pepr-demo ---- -apiVersion: v1 -data: - two: two -kind: ConfigMap -metadata: - name: cm-two - namespace: pepr-demo ---- -apiVersion: v1 -data: - three: three -kind: ConfigMap -metadata: - name: cm-three - namespace: pepr-demo diff --git a/hello-pepr-reconcile/capabilities/reconcile.e2e.test.ts b/hello-pepr-reconcile/capabilities/reconcile.e2e.test.ts index 297a3cf4..df0c18bb 100644 --- a/hello-pepr-reconcile/capabilities/reconcile.e2e.test.ts +++ b/hello-pepr-reconcile/capabilities/reconcile.e2e.test.ts @@ -1,11 +1,10 @@ import { beforeAll, afterAll, describe, it, jest, expect } from "@jest/globals"; import { TestRunCfg } from "helpers/src/TestRunCfg"; import { mins, secs, timed } from "helpers/src/time"; -import { kind } from "kubernetes-fluent-client"; import { fullCreate } from "helpers/src/general"; import { moduleUp, moduleDown, untilLogged, logs } from "helpers/src/pepr"; -import { clean } from "helpers/src/cluster" -import { execSync } from "child_process"; +import { clean } from "helpers/src/cluster"; +import { K8s, kind } from "pepr"; const trc = new TestRunCfg(__filename); @@ -19,47 +18,48 @@ describe("reconcile.ts", () => { describe("tests reconcile module", () => { let logz: string[] - beforeAll(async () => { - const file = `${trc.root()}/capabilities/reconcile.config.yaml`; + beforeAll(async () => { + const file = `${trc.root()}/capabilities/scenario.resources.yaml`; await timed(`load: ${file}`, async () => { - const resources = await trc.load(file); - await fullCreate(resources, kind); - execSync(`sh ${trc.root()}/capabilities/script.sh`); - await untilLogged("Callback: Reconciling cm-three"); + let [ ns, slow, fast ] = await trc.load(file) + + await fullCreate([ns, slow, fast]) // slow = A, fast = X + + await K8s(kind[slow.kind]).Apply({...slow, data: { note: "B"}}) + await K8s(kind[slow.kind]).Apply({...slow, data: { note: "C"}}) + + await K8s(kind[fast.kind]).Apply({...fast, data: { note: "Y"}}) + await K8s(kind[fast.kind]).Apply({...fast, data: { note: "Z"}}) + + await untilLogged("Callback: Reconciling cm-slow C-") + await untilLogged("Callback: Reconciling cm-fast Z-") logz = await logs(); }); }, mins(2)); - it("maintains callback order even when execution times vary", () => { + it("maintains callback order within a queue, paralellizes across queues", () => { + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // cm-slow |+ A -|+ B -|+ C - | + // cm-fast |+ X -|+ Y -|+ Z -| const results = logz.filter(l => l.includes("Callback: Reconciling")) - expect(results[0]).toContain("cm-one") - expect(results[1]).toContain("cm-two") - expect(results[2]).toContain("cm-three") - }, secs(10)); - - it("assert that each Queue still runs in order and that they run independent", () => { - const allResults = logz.filter(l => l.includes("Pod with name")) - const aStack: string[] = ["Pod with name a has color red", "Pod with name a has color green", "Pod with name a has color blue","Pod with name a has color yellow"] - const bStack: string[] = ["Pod with name b has color red", "Pod with name b has color green", "Pod with name b has color blue"] - - /* - * Background - Tests that each stack run independently. - * all aStack results should be before bStack results - * even though they were created at the same time. - * - * aStack and bStack independently should run in the order they were created in the queue. - */ - - // Combine results in the order in quick they should be processed - const abStack = [...aStack, ...bStack] - allResults.forEach((result) => { - if(result.includes(abStack[0])) { - abStack.shift() - } + let wants = [ + "cm-slow A+", + "cm-fast X+", + "cm-fast X-", + "cm-fast Y+", + "cm-slow A-", + "cm-slow B+", + "cm-fast Y-", + "cm-fast Z+", + "cm-fast Z-", + "cm-slow B-", + "cm-slow C+", + "cm-slow C-" + ] + wants.forEach((wanted, atIndex) => { + expect(results[atIndex]).toContain(wanted) }) - expect(abStack.length).toEqual(0) - - }, mins(3)); + }, secs(10)); }); }); diff --git a/hello-pepr-reconcile/capabilities/reconcile.ts b/hello-pepr-reconcile/capabilities/reconcile.ts index 2a0dd4e2..4742dc08 100644 --- a/hello-pepr-reconcile/capabilities/reconcile.ts +++ b/hello-pepr-reconcile/capabilities/reconcile.ts @@ -3,45 +3,36 @@ import { Capability, Log, a } from "pepr"; export const HelloPeprReconcile = new Capability({ name: "hello-pepr-reconcile", description: "A Kubernetes Operator that manages WebApps", - namespaces: ["pepr-demo"], + namespaces: ["hello-pepr-reconcile"], }); const { When } = HelloPeprReconcile; -let i = 0; +const log = (name, note, tag) => { + Log.info(`Callback: Reconciling ${name} ${note}${tag}`); +} + +const task = (cm, durationMs) => new Promise(resolve => { + const name = cm.metadata.name + const note = cm.data.note + log(name, note, "+") + setTimeout(() => { log(name, note, "-") ; resolve() }, durationMs) +}); +const fast = (cm) => task(cm, 300) +const slow = (cm) => task(cm, 500) +const oops = (cm) => { throw `oops: ${cm}` } + When(a.ConfigMap) .IsCreatedOrUpdated() - .InNamespace("pepr-demo") - .Reconcile(async instance => { - if (instance.metadata?.name !== "kube-root-ca.crt") { - return new Promise(resolve => { - const timeOut = i++ % 2 == 0 ? 20000 : 5000; - setTimeout(() => { - Log.info( - `Callback: Reconciling ${instance.metadata.name} after ${ - timeOut / 1000 - }s`, - ); - resolve(); - }, timeOut); - }); - } - }); + .InNamespace("hello-pepr-reconcile") + .Reconcile(function keepsOrder(cm) { + const { name } = cm.metadata; -const logPod = (name: string, color: string) => { - Log.info(`Pod with name ${name} has color ${color}.`); -}; -When(a.Pod) - .IsCreatedOrUpdated() - .Reconcile(po => { - const { labels, name } = po.metadata; + if (name === "kube-root-ca.crt") { return } - switch (name) { - case "a": - setTimeout(() => logPod(name, labels.color), 1000); - break; - case "b": - setTimeout(() => logPod(name, labels.color), 10000); - break; - } + return ( + name === "cm-slow" ? slow(cm) : + name === "cm-fast" ? fast(cm) : + oops(cm) + ) }); diff --git a/hello-pepr-reconcile/capabilities/scenario.resources.yaml b/hello-pepr-reconcile/capabilities/scenario.resources.yaml new file mode 100644 index 00000000..d6f35227 --- /dev/null +++ b/hello-pepr-reconcile/capabilities/scenario.resources.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: hello-pepr-reconcile +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm-slow + namespace: hello-pepr-reconcile +data: + note: A +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm-fast + namespace: hello-pepr-reconcile +data: + note: X diff --git a/hello-pepr-reconcile/capabilities/script.sh b/hello-pepr-reconcile/capabilities/script.sh deleted file mode 100644 index e3c2d571..00000000 --- a/hello-pepr-reconcile/capabilities/script.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -kubectl run a -n pepr-demo --image=nginx --restart=Never -l color=red -kubectl run b -n pepr-demo --image=nginx --restart=Never -l color=red -sleep 1 -kubectl label po a color=green -n pepr-demo --overwrite -kubectl label po b color=green -n pepr-demo --overwrite -sleep 1 -kubectl label po a color=blue -n pepr-demo --overwrite -kubectl label po b color=blue -n pepr-demo --overwrite -sleep 1 -kubectl label po a color=yellow -n pepr-demo --overwrite -