diff --git a/.eslintrc.json b/.eslintrc.json index 038d2762a..f3831d9a6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,7 +15,7 @@ "complexity": ["warn", { "max": 10 }], "consistent-this": "warn", "eqeqeq": "error", - "max-depth": ["error", { "max": 3 }], + "max-depth": ["warn", { "max": 3 }], "max-nested-callbacks": ["warn", { "max": 4 }], "max-params": ["warn", { "max": 4 }], "max-statements": ["warn", { "max": 20 }, { "ignoreTopLevelFunctions": true }], diff --git a/src/cli/build.ts b/src/cli/build.ts index 27160977e..f0e5f09d2 100644 --- a/src/cli/build.ts +++ b/src/cli/build.ts @@ -81,119 +81,115 @@ export default function (program: RootCmd) { } // Build the module - const buildModuleResult = await buildModule(undefined, opts.entryPoint, opts.embed); - if (buildModuleResult?.cfg && buildModuleResult.path && buildModuleResult.uuid) { - const { cfg, path, uuid } = buildModuleResult; - // Files to include in controller image for WASM support - const { includedFiles } = cfg.pepr; - - let image: string = ""; - - // Build Kubernetes manifests with custom image - if (opts.customImage) { - if (opts.registry) { - console.error(`Custom Image and registry cannot be used together.`); - process.exit(1); - } - image = opts.customImage; - } - - // Check if there is a custom timeout defined - if (opts.timeout !== undefined) { - cfg.pepr.webhookTimeout = opts.timeout; - } + const { cfg, path, uuid } = await buildModule(undefined, opts.entryPoint, opts.embed); - if (opts.registryInfo !== undefined) { - console.info(`Including ${includedFiles.length} files in controller image.`); + // Files to include in controller image for WASM support + const { includedFiles } = cfg.pepr; - // for journey test to make sure the image is built - image = `${opts.registryInfo}/custom-pepr-controller:${cfg.pepr.peprVersion}`; + let image: string = ""; - // only actually build/push if there are files to include - if (includedFiles.length > 0) { - await createDockerfile(cfg.pepr.peprVersion, cfg.description, includedFiles); - execSync(`docker build --tag ${image} -f Dockerfile.controller .`, { - stdio: "inherit", - }); - execSync(`docker push ${image}`, { stdio: "inherit" }); - } + // Build Kubernetes manifests with custom image + if (opts.customImage) { + if (opts.registry) { + console.error(`Custom Image and registry cannot be used together.`); + process.exit(1); } + image = opts.customImage; + } - // If building without embedding, exit after building - if (!opts.embed) { - console.info(`✅ Module built successfully at ${path}`); - return; - } + // Check if there is a custom timeout defined + if (opts.timeout !== undefined) { + cfg.pepr.webhookTimeout = opts.timeout; + } - // set the image version if provided - if (opts.version) { - cfg.pepr.peprVersion = opts.version; - } + if (opts.registryInfo !== undefined) { + console.info(`Including ${includedFiles.length} files in controller image.`); - // Generate a secret for the module - const assets = new Assets( - { - ...cfg.pepr, - appVersion: cfg.version, - description: cfg.description, - // Can override the rbacMode with the CLI option - rbacMode: determineRbacMode(opts, cfg), - }, - path, - ); + // for journey test to make sure the image is built + image = `${opts.registryInfo}/custom-pepr-controller:${cfg.pepr.peprVersion}`; - // If registry is set to Iron Bank, use Iron Bank image - if (opts?.registry === "Iron Bank") { - console.info( - `\n\tThis command assumes the latest release. Pepr's Iron Bank image release cycle is dictated by renovate and is typically released a few days after the GitHub release.\n\tAs an alternative you may consider custom --custom-image to target a specific image and version.`, - ); - image = `registry1.dso.mil/ironbank/opensource/defenseunicorns/pepr/controller:v${cfg.pepr.peprVersion}`; + // only actually build/push if there are files to include + if (includedFiles.length > 0) { + await createDockerfile(cfg.pepr.peprVersion, cfg.description, includedFiles); + execSync(`docker build --tag ${image} -f Dockerfile.controller .`, { stdio: "inherit" }); + execSync(`docker push ${image}`, { stdio: "inherit" }); } + } - // if image is a custom image, use that instead of the default - if (image !== "") { - assets.image = image; - } + // If building without embedding, exit after building + if (!opts.embed) { + console.info(`✅ Module built successfully at ${path}`); + return; + } - // Ensure imagePullSecret is valid - if (opts.withPullSecret) { - if (sanitizeResourceName(opts.withPullSecret) !== opts.withPullSecret) { - // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names - console.error( - "Invalid imagePullSecret. Please provide a valid name as defined in RFC 1123.", - ); - process.exit(1); - } - } + // set the image version if provided + if (opts.version) { + cfg.pepr.peprVersion = opts.version; + } - const yamlFile = `pepr-module-${uuid}.yaml`; - const chartPath = `${uuid}-chart`; - const yamlPath = resolve(outputDir, yamlFile); - const yaml = await assets.allYaml(opts.withPullSecret); + // Generate a secret for the module + const assets = new Assets( + { + ...cfg.pepr, + appVersion: cfg.version, + description: cfg.description, + // Can override the rbacMode with the CLI option + rbacMode: determineRbacMode(opts, cfg), + }, + path, + ); - try { - // wait for capabilities to be loaded and test names - validateCapabilityNames(assets.capabilities); - } catch (e) { - console.error(`Error loading capability:`, e); - process.exit(1); - } + // If registry is set to Iron Bank, use Iron Bank image + if (opts?.registry === "Iron Bank") { + console.info( + `\n\tThis command assumes the latest release. Pepr's Iron Bank image release cycle is dictated by renovate and is typically released a few days after the GitHub release.\n\tAs an alternative you may consider custom --custom-image to target a specific image and version.`, + ); + image = `registry1.dso.mil/ironbank/opensource/defenseunicorns/pepr/controller:v${cfg.pepr.peprVersion}`; + } - const zarfPath = resolve(outputDir, "zarf.yaml"); + // if image is a custom image, use that instead of the default + if (image !== "") { + assets.image = image; + } - let zarf = ""; - if (opts.zarf === "chart") { - zarf = assets.zarfYamlChart(chartPath); - } else { - zarf = assets.zarfYaml(yamlFile); + // Ensure imagePullSecret is valid + if (opts.withPullSecret) { + if (sanitizeResourceName(opts.withPullSecret) !== opts.withPullSecret) { + // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names + console.error( + "Invalid imagePullSecret. Please provide a valid name as defined in RFC 1123.", + ); + process.exit(1); } - await fs.writeFile(yamlPath, yaml); - await fs.writeFile(zarfPath, zarf); + } - await assets.generateHelmChart(outputDir); + const yamlFile = `pepr-module-${uuid}.yaml`; + const chartPath = `${uuid}-chart`; + const yamlPath = resolve(outputDir, yamlFile); + const yaml = await assets.allYaml(opts.withPullSecret); + + try { + // wait for capabilities to be loaded and test names + validateCapabilityNames(assets.capabilities); + } catch (e) { + console.error(`Error loading capability:`, e); + process.exit(1); + } + + const zarfPath = resolve(outputDir, "zarf.yaml"); - console.info(`✅ K8s resource for the module saved to ${yamlPath}`); + let zarf = ""; + if (opts.zarf === "chart") { + zarf = assets.zarfYamlChart(chartPath); + } else { + zarf = assets.zarfYaml(yamlFile); } + await fs.writeFile(yamlPath, yaml); + await fs.writeFile(zarfPath, zarf); + + await assets.generateHelmChart(outputDir); + + console.info(`✅ K8s resource for the module saved to ${yamlPath}`); }); } @@ -336,38 +332,41 @@ export async function buildModule(reloader?: Reloader, entryPoint = peprTS, embe } catch (e) { console.error(`Error building module:`, e); - if (!e.stdout) process.exit(1); // Exit with a non-zero exit code on any other error + if (e.stdout) { + const out = e.stdout.toString() as string; + const err = e.stderr.toString(); - const out = e.stdout.toString() as string; - const err = e.stderr.toString(); + console.log(out); + console.error(err); - console.log(out); - console.error(err); + // Check for version conflicts + if (out.includes("Types have separate declarations of a private property '_name'.")) { + // Try to find the conflicting package + const pgkErrMatch = /error TS2322: .*? 'import\("\/.*?\/node_modules\/(.*?)\/node_modules/g; + out.matchAll(pgkErrMatch); - // Check for version conflicts - if (out.includes("Types have separate declarations of a private property '_name'.")) { - // Try to find the conflicting package - const pgkErrMatch = /error TS2322: .*? 'import\("\/.*?\/node_modules\/(.*?)\/node_modules/g; - out.matchAll(pgkErrMatch); + // Look for package conflict errors + const conflicts = [...out.matchAll(pgkErrMatch)]; - // Look for package conflict errors - const conflicts = [...out.matchAll(pgkErrMatch)]; + // If the regex didn't match, leave a generic error + if (conflicts.length < 1) { + console.info( + `\n\tOne or more imported Pepr Capabilities seem to be using an incompatible version of Pepr.\n\tTry updating your Pepr Capabilities to their latest versions.`, + "Version Conflict", + ); + } - // If the regex didn't match, leave a generic error - if (conflicts.length < 1) { - console.info( - `\n\tOne or more imported Pepr Capabilities seem to be using an incompatible version of Pepr.\n\tTry updating your Pepr Capabilities to their latest versions.`, - "Version Conflict", - ); + // Otherwise, loop through each conflicting package and print an error + conflicts.forEach(match => { + console.info( + `\n\tPackage '${match[1]}' seems to be incompatible with your current version of Pepr.\n\tTry updating to the latest version.`, + "Version Conflict", + ); + }); } - - // Otherwise, loop through each conflicting package and print an error - conflicts.forEach(match => { - console.info( - `\n\tPackage '${match[1]}' seems to be incompatible with your current version of Pepr.\n\tTry updating to the latest version.`, - "Version Conflict", - ); - }); } + + // On any other error, exit with a non-zero exit code + process.exit(1); } } diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index a49996c17..5184f0be4 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -72,37 +72,34 @@ export default function (program: RootCmd) { } // Build the module - const buildModuleResult = await buildModule(); - if (buildModuleResult?.cfg && buildModuleResult?.path) { - const { cfg, path } = buildModuleResult; + const { cfg, path } = await buildModule(); - // Generate a secret for the module - const webhook = new Assets( - { - ...cfg.pepr, - description: cfg.description, - }, - path, - ); + // Generate a secret for the module + const webhook = new Assets( + { + ...cfg.pepr, + description: cfg.description, + }, + path, + ); - if (opts.image) { - webhook.image = opts.image; - } + if (opts.image) { + webhook.image = opts.image; + } - // Identify conf'd webhookTimeout to give to deploy call - const timeout = cfg.pepr.webhookTimeout ? cfg.pepr.webhookTimeout : 10; + // Identify conf'd webhookTimeout to give to deploy call + const timeout = cfg.pepr.webhookTimeout ? cfg.pepr.webhookTimeout : 10; - try { - await webhook.deploy(opts.force, timeout); - // wait for capabilities to be loaded and test names - validateCapabilityNames(webhook.capabilities); - // Wait for the pepr-system resources to be fully up - await namespaceDeploymentsReady(); - console.info(`✅ Module deployed successfully`); - } catch (e) { - console.error(`Error deploying module:`, e); - process.exit(1); - } + try { + await webhook.deploy(opts.force, timeout); + // wait for capabilities to be loaded and test names + validateCapabilityNames(webhook.capabilities); + // Wait for the pepr-system resources to be fully up + await namespaceDeploymentsReady(); + console.info(`✅ Module deployed successfully`); + } catch (e) { + console.error(`Error deploying module:`, e); + process.exit(1); } }); } diff --git a/src/cli/format.ts b/src/cli/format.ts index 268921159..a3869432a 100644 --- a/src/cli/format.ts +++ b/src/cli/format.ts @@ -63,10 +63,13 @@ export async function peprFormat(validateOnly: boolean) { const formatted = await format(content, { filepath: filePath, ...cfg }); // If in validate-only mode, check if the file is formatted correctly - if (validateOnly && formatted !== content) { - hasFailure = true; - console.error(`File ${filePath} is not formatted correctly`); + if (validateOnly) { + if (formatted !== content) { + hasFailure = true; + console.error(`File ${filePath} is not formatted correctly`); + } } else { + // Otherwise, write the formatted file await fs.writeFile(filePath, formatted); } } diff --git a/src/cli/init/index.ts b/src/cli/init/index.ts index eadad7bd1..ee916a082 100644 --- a/src/cli/init/index.ts +++ b/src/cli/init/index.ts @@ -74,7 +74,25 @@ export default function (program: RootCmd) { await write(resolve(dirName, "capabilities", helloPepr.path), helloPepr.data); if (!opts.skipPostInit) { - doPostInitActions(dirName); + // run npm install from the new directory + process.chdir(dirName); + execSync("npm install", { + stdio: "inherit", + }); + + // setup git + execSync("git init --initial-branch=main", { + stdio: "inherit", + }); + + // try to open vscode + try { + execSync("code .", { + stdio: "inherit", + }); + } catch (e) { + // vscode not found, do nothing + } } console.log(`New Pepr module created at ${dirName}`); @@ -88,25 +106,3 @@ export default function (program: RootCmd) { } }); } - -const doPostInitActions = (dirName: string): void => { - // run npm install from the new directory - process.chdir(dirName); - execSync("npm install", { - stdio: "inherit", - }); - - // setup git - execSync("git init --initial-branch=main", { - stdio: "inherit", - }); - - // try to open vscode - try { - execSync("code .", { - stdio: "inherit", - }); - } catch (e) { - // vscode not found, do nothing - } -}; diff --git a/src/cli/monitor.ts b/src/cli/monitor.ts index 4b83811a9..ca1af80ce 100644 --- a/src/cli/monitor.ts +++ b/src/cli/monitor.ts @@ -49,41 +49,43 @@ export default function (program: RootCmd) { for (const line of lines) { // Check for `"msg":"Hello Pepr"` - if (!line.includes(respMsg)) continue; - try { - const payload = JSON.parse(line.trim()); - const isMutate = payload.res.patchType || payload.res.warnings; - - const name = `${payload.namespace}${payload.name}`; - const uid = payload.res.uid; - - if (isMutate) { - const plainPatch = - payload.res?.patch !== undefined && payload.res?.patch !== null - ? atob(payload.res.patch) - : ""; - - const patch = plainPatch !== "" && JSON.stringify(JSON.parse(plainPatch), null, 2); - const patchType = payload.res.patchType || payload.res.warnings || ""; - const allowOrDeny = payload.res.allowed ? "🔀" : "🚫"; - console.log(`\n${allowOrDeny} MUTATE ${name} (${uid})`); - patchType.length > 0 && console.log(`\n\u001b[1;34m${patch}\u001b[0m`); - } else { - const failures = Array.isArray(payload.res) ? payload.res : [payload.res]; - - const filteredFailures = failures - .filter((r: ResponseItem) => !r.allowed) - .map((r: ResponseItem) => r.status.message); - - console.log( - `\n${filteredFailures.length > 0 ? "❌" : "✅"} VALIDATE ${name} (${uid})`, - ); - console.log( - filteredFailures.length > 0 ? `\u001b[1;31m${filteredFailures}\u001b[0m` : "", - ); + if (line.includes(respMsg)) { + try { + const payload = JSON.parse(line.trim()); + const isMutate = payload.res.patchType || payload.res.warnings; + + const name = `${payload.namespace}${payload.name}`; + const uid = payload.res.uid; + + if (isMutate) { + const plainPatch = + payload.res?.patch !== undefined && payload.res?.patch !== null + ? atob(payload.res.patch) + : ""; + + const patch = plainPatch !== "" && JSON.stringify(JSON.parse(plainPatch), null, 2); + const patchType = payload.res.patchType || payload.res.warnings || ""; + const allowOrDeny = payload.res.allowed ? "🔀" : "🚫"; + console.log(`\n${allowOrDeny} MUTATE ${name} (${uid})`); + if (patchType.length > 0) { + console.log(`\n\u001b[1;34m${patch}\u001b[0m`); + } + } else { + const failures = Array.isArray(payload.res) ? payload.res : [payload.res]; + + const filteredFailures = failures + .filter((r: ResponseItem) => !r.allowed) + .map((r: ResponseItem) => r.status.message); + if (filteredFailures.length > 0) { + console.log(`\n❌ VALIDATE ${name} (${uid})`); + console.log(`\u001b[1;31m${filteredFailures}\u001b[0m`); + } else { + console.log(`\n✅ VALIDATE ${name} (${uid})`); + } + } + } catch { + // Do nothing } - } catch { - // Do nothing } } }); diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 769a96a9a..1bd580f6c 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -275,9 +275,12 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor ) { for (const regexNamespace of bindingRegexNamespaces) { let matches = false; - matches = - regexNamespace !== "" && - capabilityNamespaces.some(capabilityNamespace => matchesRegex(regexNamespace, capabilityNamespace)); + for (const capabilityNamespace of capabilityNamespaces) { + if (regexNamespace !== "" && matchesRegex(regexNamespace, capabilityNamespace)) { + matches = true; + break; + } + } if (!matches) { throw new Error( `Ignoring Watch Callback: Object namespace does not match any capability namespace with regex ${regexNamespace}.`, @@ -293,11 +296,12 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor ignoredNamespaces.length > 0 ) { for (const regexNamespace of bindingRegexNamespaces) { - const matchedNS = ignoredNamespaces.find(ignoredNS => matchesRegex(regexNamespace, ignoredNS)); - if (matchedNS) { - throw new Error( - `Ignoring Watch Callback: Regex namespace: ${regexNamespace}, is an ignored namespace: ${matchedNS}.`, - ); + for (const ignoredNS of ignoredNamespaces) { + if (matchesRegex(regexNamespace, ignoredNS)) { + throw new Error( + `Ignoring Watch Callback: Regex namespace: ${regexNamespace}, is an ignored namespace: ${ignoredNS}.`, + ); + } } } } @@ -385,7 +389,7 @@ export function dedent(file: string) { } export function replaceString(str: string, stringA: string, stringB: string) { - // eslint-disable-next-line no-useless-escape + //eslint-disable-next-line const escapedStringA = stringA.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); const regExp = new RegExp(escapedStringA, "g"); return str.replace(regExp, stringB); diff --git a/src/lib/mutate-processor.ts b/src/lib/mutate-processor.ts index 706f925a0..7f3b5b47f 100644 --- a/src/lib/mutate-processor.ts +++ b/src/lib/mutate-processor.ts @@ -89,14 +89,33 @@ export async function mutateProcessor( updateStatus("warning"); response.warnings = response.warnings || []; - const errorMessage = - e.message && e.message !== "[object Object]" ? e.message : "An error occurred in the mutate action."; + let errorMessage = ""; + + try { + if (e.message && e.message !== "[object Object]") { + errorMessage = e.message; + } else { + throw new Error("An error occurred in the mutate action."); + } + } catch (e) { + errorMessage = "An error occurred with the mutate action."; + } // Log on failure Log.error(actionMetadata, `Action failed: ${errorMessage}`); response.warnings.push(`Action failed: ${errorMessage}`); - handleMutationError(errorMessage, actionMetadata, response, config); + switch (config.onError) { + case Errors.reject: + Log.error(actionMetadata, `Action failed: ${errorMessage}`); + response.result = "Pepr module configured to reject on error"; + return response; + + case Errors.audit: + response.auditAnnotations = response.auditAnnotations || {}; + response.auditAnnotations[Date.now()] = `Action failed: ${errorMessage}`; + break; + } } } } @@ -142,22 +161,3 @@ export async function mutateProcessor( return response; } - -function handleMutationError( - errorMessage: string, - actionMetadata: Record, - response: MutateResponse, - config: ModuleConfig, -) { - switch (config.onError) { - case Errors.reject: - Log.error(actionMetadata, `Action failed: ${errorMessage}`); - response.result = "Pepr module configured to reject on error"; - break; - - case Errors.audit: - response.auditAnnotations = response.auditAnnotations || {}; - response.auditAnnotations[Date.now()] = `Action failed: ${errorMessage}`; - break; - } -} diff --git a/src/lib/validate-processor.ts b/src/lib/validate-processor.ts index 15d368bf0..43571a330 100644 --- a/src/lib/validate-processor.ts +++ b/src/lib/validate-processor.ts @@ -59,10 +59,12 @@ export async function validateProcessor( localResponse.allowed = resp.allowed; // If the validation callback returned a status code or message, set it in the Response - localResponse.status = { - code: resp.statusCode || 400, - message: resp.statusMessage || `Validation failed for ${name}`, - }; + if (resp.statusCode || resp.statusMessage) { + localResponse.status = { + code: resp.statusCode || 400, + message: resp.statusMessage || `Validation failed for ${name}`, + }; + } Log.info(actionMetadata, `Validation action complete (${label}): ${resp.allowed ? "allowed" : "denied"}`); } catch (e) { diff --git a/src/lib/watch-processor.ts b/src/lib/watch-processor.ts index 2386ad61d..7761127b1 100644 --- a/src/lib/watch-processor.ts +++ b/src/lib/watch-processor.ts @@ -96,20 +96,39 @@ async function runBinding(binding: Binding, capabilityNamespaces: string[], igno // The watch callback is run when an object is received or dequeued Log.debug({ watchCfg }, "Effective WatchConfig"); - const watchCallback = async (kubernetesObject: KubernetesObject, phase: WatchPhase) => { + const watchCallback = async (obj: KubernetesObject, phase: WatchPhase) => { // First, filter the object based on the phase if (phaseMatch.includes(phase)) { try { // Then, check if the object matches the filter - const filterMatch = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces, ignoredNamespaces); - if (filterMatch !== "") { - Log.debug(filterMatch); - return; - } - if (binding.isFinalize) { - await handleFinalizerRemoval(kubernetesObject); + const filterMatch = filterNoMatchReason(binding, obj, capabilityNamespaces, ignoredNamespaces); + if (filterMatch === "") { + if (binding.isFinalize) { + if (!obj.metadata?.deletionTimestamp) { + return; + } + + let shouldRemoveFinalizer: boolean | void | undefined = true; + try { + shouldRemoveFinalizer = await binding.finalizeCallback?.(obj); + + // if not opt'ed out of / if in error state, remove pepr finalizer + } finally { + const peprFinal = "pepr.dev/finalizer"; + const meta = obj.metadata!; + const resource = `${meta.namespace || "ClusterScoped"}/${meta.name}`; + + // [ true, void, undefined ] SHOULD remove finalizer + // [ false ] should NOT remove finalizer + shouldRemoveFinalizer === false + ? Log.debug({ obj }, `Skipping removal of finalizer '${peprFinal}' from '${resource}'`) + : await removeFinalizer(binding, obj); + } + } else { + await binding.watchCallback?.(obj, phase); + } } else { - await binding.watchCallback?.(kubernetesObject, phase); + Log.debug(filterMatch); } } catch (e) { // Errors in the watch callback should not crash the controller @@ -118,28 +137,6 @@ async function runBinding(binding: Binding, capabilityNamespaces: string[], igno } }; - const handleFinalizerRemoval = async (kubernetesObject: KubernetesObject) => { - if (!kubernetesObject.metadata?.deletionTimestamp) { - return; - } - let shouldRemoveFinalizer: boolean | void | undefined = true; - try { - shouldRemoveFinalizer = await binding.finalizeCallback?.(kubernetesObject); - - // if not opt'ed out of / if in error state, remove pepr finalizer - } finally { - const peprFinal = "pepr.dev/finalizer"; - const meta = kubernetesObject.metadata!; - const resource = `${meta.namespace || "ClusterScoped"}/${meta.name}`; - - // [ true, void, undefined ] SHOULD remove finalizer - // [ false ] should NOT remove finalizer - shouldRemoveFinalizer === false - ? Log.debug({ obj: kubernetesObject }, `Skipping removal of finalizer '${peprFinal}' from '${resource}'`) - : await removeFinalizer(binding, kubernetesObject); - } - }; - // Setup the resource watch const watcher = K8s(binding.model, binding.filters).Watch(async (obj, phase) => { Log.debug(obj, `Watch event ${phase} received`);