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  }