diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..654cfd2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.dockerignore +.editorconfig +README.md +LICENSE +node_modules diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7391c86 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ + +[*] +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,json,md}] +indent_size = 2 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..05c1883 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,80 @@ +############################ +# Final container +############################ + +# Because we are using `asdf` to manage our tools, we can simply use the bash +# image from the cto.ai registry, as it provides the `sdk-daemon` runtime that +# we need to connect to the CTO.ai platform, and we don't need to worry about +# the version of Node.js that is installed in the image by default. +FROM registry.cto.ai/official_images/bash:2-bullseye-slim + +# Download the Tailscale binaries and extract them to the `/usr/local/bin` +# directory, as well as create the `/var/run/tailscale` directory which the +# Tailscale daemon uses to store runtime information. +ARG TAILSCALE_VERSION +ENV TAILSCALE_VERSION=${TAILSCALE_VERSION:-1.74.1} +RUN curl -fsSL "https://pkgs.tailscale.com/stable/tailscale_${TAILSCALE_VERSION}_amd64.tgz" --max-time 300 --fail \ + | tar -xz -C /usr/local/bin --strip-components=1 --no-anchored tailscale tailscaled \ + && mkdir -p /var/run/tailscale \ + && chown -R ops:9999 /usr/local/bin/tailscale* /var/run/tailscale + +# Copy the `entrypoint.sh` script to the container and set the appropriate +# permissions to ensure that it can be executed by the `ops` user. We need to +# use an entrypoint script to ensure the Tailscale daemon is running before we +# run the code that defines our workflow. +COPY --chown=ops:9999 lib/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# The base directory for our image is `/ops`, which is where all of the code +# that defines our workflow will live. +WORKDIR /ops + +# Run the container as the `ops` user by default, and set the appropriate +# environment variables for the user. Because we're going to use `asdf` to +# manage our tools, we'll manually set the `ASDF_DIR` and `PATH` environment +# variables to point to the `/ops/.asdf` directory that will soon be installed. +ENV USER=ops HOME=/ops XDG_RUNTIME_DIR=/run/ops/9999 \ + PATH=/ops/.asdf/shims:/ops/.asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +# Set the `ASDF_VERSION_TAG` and `ASDF_DIR` environment variables manually to +# ensure that the correct version of the tool is installed in `/ops/.asdf`. +ENV ASDF_VERSION_TAG=v0.14.1 \ + ASDF_DIR=/ops/.asdf + +# Copy the contents of the `lib/` directory into the root of the image. This +# means, for example, that the `./lib/build/` directory will be at `/build/`. +COPY --chown=ops:9999 lib/build/ /build/ + +# Uncomment to install any additional packages needed to run the tools and code +# we will be using during the build process OR in our final container. +# RUN apt-get update \ +# && apt-get install -y \ +# build-essential \ +# && apt-get clean \ +# && rm -rf /var/lib/apt/lists/* + +# Run the script that will install the `asdf` tool, the plugins necessary to +# install the tools specified in the `.tool-versions` file, and then install +# the tools themselves. This is how a more recent version of Node.js will be +# installed and managed in our image. +RUN bash /build/install-asdf-tools.sh + +# Copy the `package.json` file to the container and run `npm install` to ensure +# that all of the dependencies for our Node.js code are installed. +COPY --chown=ops:9999 package.json . +RUN npm install + +# Copy the `index.js` file that defines the behavior of our workflow when the +# workflow is run using the `ops run` command or any other trigger. +COPY --chown=ops:9999 . /ops/ + +############################################################################## +# As a security best practice the container will always run as non-root user. +############################################################################## + +# Finally, set the `ops` user as the default user for the container and set the +# `entrypoint.sh` script as the default command that will be run when the +# workflow container is run. The `entrypoint.sh` script will be passed the `run` +# value from the `ops.yml` file that defines this workflow. +USER ops +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/LICENSE b/LICENSE index 2f62d69..4b28a9c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 workflows-sh +Copyright (c) 2024 CTO.ai Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..336c731 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# Example Workflow: Command with Tailscale Integration + +This repository contains an example workflow that demonstrates how to use the Tailscale CLI to connect the running [Command](https://cto.ai/docs/commands/overview/) to a Tailscale network. + +It also uses the [asdf](https://asdf-vm.com/) CLI—an all-in-one runtime version manager akin to nvm or pyenv—to manage the version of Node.js that is used to run the business logic of the Command. + +
+
+
TAILSCALE_AUTHKEY_.
+
+Thus, for the default value of `TS_HOSTNAME` in the `ops.yml` file, the Secret in the Secrets Store would be named `TAILSCALE_AUTHKEY_SAMPLE_COMMAND_TAILSCALED`. To run this Command as-is, you can add you Tailscale authentication key to a Secret with that name in the Secrets Store associated with your team on the CTO.ai platform.
+
+> [!NOTE]
+> If a Tailscale auth key is not added to the appropriate Secret name in the CTO.ai Secret Store associated with your team, you will be prompted to provide a value for that Secret the first time this Command is run.
+
+Alternatively, set a value for `AUTHKEY_SECRET_NAME` as a [static environment variable](https://cto.ai/docs/configs-and-secrets/managing-variables/#managing-workflow-behavior-with-environment-variables) in the `ops.yml` file, and the Command will look for the Tailscale authentication key in a Secret with the name specified by that value.
+
+## Creating Your Own Workflow
+
+Once you have this template initialized locally as a new Command workflow, you can modify the code in [index.js](./index.js) to define how the workflow should behave when it is run (see the [Workflow Architecture](#workflow-architecture) section below for more information).
+
+When you are ready to test your changes, you can [build and run the Command](https://cto.ai/docs/workflows/using-workflows/) locally using the `ops run` command with the `-b` flag:
+
+```bash
+ops run -b .
+```
+
+When you are ready to deploy your Command to the CTO.ai platform to make it available to your team via the `ops` CLI or our [Slack integration](https://cto.ai/docs/slackops/overview/), you can use the `ops publish` command:
+
+```bash
+ops publish .
+```
+
+## Workflow Architecture
+
+The five main components described below define this example Command workflow.
+
+### Runtime container definition: `Dockerfile`
+
+The [Dockerfile](./Dockerfile) defines the build stage for the container image that executes the workflow. This is where dependencies are installed, including the `tailscale` and `tailscaled` binaries, as well as the dependencies managed by `asdf`.
+
+### Build dependencies: `lib/build/`
+
+Contains the scripts that are executed by the Dockerfile to install the dependencies managed by `asdf`. Within this directory, the [`install-asdf-tools.sh`](./lib/build/install-asdf-tools.sh) script installs the asdf-managed dependency versions defined in the [`asdf-installs`](./lib/build/asdf-installs) file.
+
+### Container entrypoint: `lib/entrypoint.sh`
+
+The [`entrypoint.sh`](./lib/entrypoint.sh) script that is executed when the container starts. This script starts the `tailscaled` service, which will allow the client to connect to a Tailscale network when the Command is run. After the script starts the daemon, it uses the `exec` command to replace the current process (that is, the `entrypoint.sh` script) with the process specified in the `ops.yml` file.
+
+### Workflow definition(s): `ops.yml`
+
+The [`ops.yml`](./ops.yml) defines the configuration for this Command. The script to execute as the [business logic of the workflow](https://cto.ai/docs/usage-reference/ops-yml/) is passed as the value of the `run` key, which is passed to the entrypoint of the final container.
+
+### Workflow business logic: `index.js`
+
+The business logic of the workflow. The [`index.js`](./index.js) script is executed by the Command when it is run.
+
+There is where connection to a Tailscale network is initiated using the `tailscale up` command, which connects to the socket created by the `tailscaled` daemon started by the `entrypoint.sh` script.
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..913ca37
--- /dev/null
+++ b/index.js
@@ -0,0 +1,109 @@
+const { ux, sdk } = require('@cto.ai/sdk');
+
+/**
+ * Return the hostname depending on which environment variables are set
+ * @returns {string} hostname
+ */
+function getWorkflowHostname() {
+ return process.env.TS_HOSTNAME || process.env.OPS_OP_NAME
+}
+
+/**
+ * Determine the name of the environment variable that contains the Tailscale
+ * auth key for the current hostname.
+ * @returns {string} authkeySecretName
+ */
+function getAuthKeySecretName() {
+ // If the the `AUTHKEY_SECRET_NAME` static environment variable has been set
+ // in the `ops.yml` for the workflow, use the value of that variable as the
+ // name of the secret containing the Tailscale auth key.
+ if (process.env.AUTHKEY_SECRET_NAME) {
+ return process.env.AUTHKEY_SECRET_NAME
+ } else {
+ // Otherwise, generate the name of the secret based on the hostname
+ const hostkey = getWorkflowHostname().toUpperCase().replace(/-/g, '_').trim()
+ return `TAILSCALE_AUTHKEY_${hostkey}`
+ }
+}
+
+/**
+ * Retrieve the Tailscale auth key from the Secrets Store using the name of the
+ * secret that contains the key. The name of the secret to retrieve is determined
+ * by the string passed as the `authkeyName` parameter.
+ * @param {string} authkeyName
+ * @returns {Promise