github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/examples/component-webhooks/capabilities/hook.ts (about) 1 import { Capability, a, Log, K8s, kind } from "pepr"; 2 import { DeployedPackage } from "./jackal-types"; 3 4 /** 5 * The Webhook Capability is an example capability to demonstrate using webhooks to interact with Jackal package deployments. 6 * To test this capability you run `pepr dev`and then deploy a jackal package! 7 */ 8 export const Webhook = new Capability({ 9 name: "example-webhook", 10 description: 11 "A simple example capability to show how webhooks work with Jackal package deployments.", 12 namespaces: ["jackal"], 13 }); 14 15 const webhookName = "test-webhook"; 16 17 const { When } = Webhook; 18 19 When(a.Secret) 20 .IsCreatedOrUpdated() 21 .InNamespace("jackal") 22 .WithLabel("package-deploy-info") 23 .Mutate(request => { 24 const secret = request.Raw; 25 let secretData: DeployedPackage; 26 let secretString: string; 27 let manuallyDecoded = false; 28 29 // Pepr does not decode/encode non-ASCII characters in secret data: https://github.com/defenseunicorns/pepr/issues/219 30 try { 31 secretString = atob(secret.data.data); 32 manuallyDecoded = true; 33 } catch (err) { 34 secretString = secret.data.data; 35 } 36 37 // Parse the secret object 38 try { 39 secretData = JSON.parse(secretString); 40 } catch (err) { 41 throw new Error(`Failed to parse the secret.data.data: ${err}`); 42 } 43 44 for (const deployedComponent of secretData?.deployedComponents ?? []) { 45 if (deployedComponent.status === "Deploying") { 46 Log.info( 47 `The component ${deployedComponent.name} is currently deploying`, 48 ); 49 50 const componentWebhook = 51 secretData.componentWebhooks?.[deployedComponent?.name]?.[ 52 webhookName 53 ]; 54 55 // Check if the component has a webhook running for the current package generation 56 if (componentWebhook?.observedGeneration === secretData.generation) { 57 Log.debug( 58 `The component ${deployedComponent.name} has already had a webhook executed for it. Not executing another.`, 59 ); 60 } else { 61 // Seed the componentWebhooks map/object 62 if (!secretData.componentWebhooks) { 63 secretData.componentWebhooks = {}; 64 } 65 66 // Update the secret noting that the webhook is running for this component 67 secretData.componentWebhooks[deployedComponent.name] = { 68 "test-webhook": { 69 name: webhookName, 70 status: "Running", 71 observedGeneration: secretData.generation, 72 }, 73 }; 74 75 // Call an async function that simulates background processing and then updates the secret with the new status when it's complete 76 sleepAndChangeStatus(secret.metadata.name, deployedComponent.name); 77 } 78 } 79 } 80 81 if (manuallyDecoded === true) { 82 secret.data.data = btoa(JSON.stringify(secretData)); 83 } else { 84 secret.data.data = JSON.stringify(secretData); 85 } 86 }); 87 88 // sleepAndChangeStatus sleeps for the specified duration and changes the status of the 'test-webhook' to 'Succeeded'. 89 async function sleepAndChangeStatus(secretName: string, componentName: string) { 90 await sleep(10); 91 92 const ns = "jackal"; 93 94 let secret: a.Secret; 95 96 // Fetch the package secret 97 try { 98 secret = await K8s(kind.Secret).InNamespace(ns).Get(secretName); 99 } catch (err) { 100 Log.error( 101 `Error: Failed to get package secret '${secretName}' in namespace '${ns}': ${JSON.stringify( 102 err, 103 )}`, 104 ); 105 } 106 107 const secretString = atob(secret.data.data); 108 const secretData: DeployedPackage = JSON.parse(secretString); 109 110 // Update the webhook status if the observedGeneration matches 111 const componentWebhook = 112 secretData.componentWebhooks[componentName]?.[webhookName]; 113 114 if (componentWebhook?.observedGeneration === secretData.generation) { 115 componentWebhook.status = "Succeeded"; 116 117 secretData.componentWebhooks[componentName][webhookName] = componentWebhook; 118 } 119 120 secret.data.data = btoa(JSON.stringify(secretData)); 121 122 // Update the status in the package secret 123 // Use Server-Side force apply to forcefully take ownership of the package secret data.data field 124 // Doing a Server-Side apply without the force option will result in a FieldManagerConflict error due to Jackal owning the object. 125 try { 126 await K8s(kind.Secret).Apply( 127 { 128 metadata: { 129 name: secretName, 130 namespace: ns, 131 }, 132 data: { 133 data: secret.data.data, 134 }, 135 }, 136 { force: true }, 137 ); 138 } catch (err) { 139 Log.error( 140 `Error: Failed to update package secret '${secretName}' in namespace '${ns}': ${JSON.stringify( 141 err, 142 )}`, 143 ); 144 } 145 } 146 147 function sleep(seconds: number) { 148 return new Promise(resolve => setTimeout(resolve, seconds * 1000)); 149 }