diff --git a/.icons/tailscale.svg b/.icons/tailscale.svg new file mode 100644 index 000000000..6cc6f2d09 --- /dev/null +++ b/.icons/tailscale.svg @@ -0,0 +1 @@ + diff --git a/registry/dy-ma/.images/avatar.png b/registry/dy-ma/.images/avatar.png new file mode 100644 index 000000000..4a10d0241 Binary files /dev/null and b/registry/dy-ma/.images/avatar.png differ diff --git a/registry/dy-ma/README.md b/registry/dy-ma/README.md new file mode 100644 index 000000000..c6b5106c3 --- /dev/null +++ b/registry/dy-ma/README.md @@ -0,0 +1,14 @@ +--- +display_name: "Dylan Mou Ang" +bio: "First time contributor. Got tired of copy-pasting scripts." +github: "dy-ma" +avatar: "./.images/avatar.png" +linkedin: "https://www.linkedin.com/in/dylan-mou-ang" +website: "https://www.dyma.dev" +support_email: "dylanmouang@gmail.com" +status: "community" +--- + +# Dylan Mou Ang + +First time contributor. Got tired of copy-pasting scripts. diff --git a/registry/dy-ma/modules/tailscale/README.md b/registry/dy-ma/modules/tailscale/README.md new file mode 100644 index 000000000..323c1736f --- /dev/null +++ b/registry/dy-ma/modules/tailscale/README.md @@ -0,0 +1,151 @@ +--- +display_name: Tailscale +description: Joins the workspace to your Tailscale network using OAuth or a pre-generated auth key. +icon: ../../../../.icons/tailscale.svg +verified: false +tags: [networking, tailscale] +--- + +# Tailscale + +Installs [Tailscale](https://tailscale.com) and joins the workspace to your tailnet on start. Supports kernel and userspace networking, and works with both Tailscale's hosted service and self-hosted [Headscale](https://headscale.net). + +```tf +module "tailscale" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/dy-ma/tailscale/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + oauth_client_id = "kFvxxxxxxxxxx" + oauth_client_secret = "tskey-client-xxxx" +} +``` + +> Do not hardcode credentials in your template. Pass them via Terraform variables, `TF_VAR_*` environment variables, or your preferred secrets manager. +> +> **Creating OAuth credentials:** In the Tailscale admin console go to **Settings → OAuth Clients** and create a client with the `auth_keys` scope and the ACL tags your workspaces will use (e.g. `tag:coder-workspace`). + +## Examples + +### VM workspace (persistent identity) + +For VMs or long-lived containers where you want the node to keep its identity across workspace stop/start: + +```tf +module "tailscale" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/dy-ma/tailscale/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + oauth_client_id = "kFvxxxxxxxxxx" + oauth_client_secret = "tskey-client-xxxx" + ephemeral = false + networking_mode = "kernel" + state_dir = "/var/lib/tailscale" +} +``` + +### Ephemeral pod / unprivileged container + +For Kubernetes pods or Docker containers without access to `/dev/net/tun`. Userspace mode exposes a SOCKS5 proxy on port `1080` and an HTTP proxy on port `3128` for outbound tailnet access: + +```tf +module "tailscale" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/dy-ma/tailscale/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + oauth_client_id = "kFvxxxxxxxxxx" + oauth_client_secret = "tskey-client-xxxx" + ephemeral = true + networking_mode = "userspace" + state_dir = "/tmp/tailscale-state" +} +``` + +### Pre-generated auth key + +If you prefer to manage key rotation externally, pass an auth key directly and skip the OAuth flow: + +```tf +module "tailscale" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/dy-ma/tailscale/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + auth_key = "tskey-auth-xxxx" +} +``` + +### Headscale + +Point `tailscale_api_url` at your Headscale server and pass a pre-generated auth key: + +```tf +module "tailscale" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/dy-ma/tailscale/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + auth_key = "tskey-auth-xxxx" + tailscale_api_url = "https://headscale.example.com" +} +``` + +### Tailscale SSH + +Enable Tailscale SSH so tailnet members can reach workspaces directly without managing keys. The `tags` variable (default `["tag:coder-workspace"]`) controls which ACL tag the node advertises — override it if your policy uses a different tag. + +```tf +module "tailscale" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/dy-ma/tailscale/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + oauth_client_id = "kFvxxxxxxxxxx" + oauth_client_secret = "tskey-client-xxxx" + ssh = true + tags = ["tag:coder-workspace"] # override if needed +} +``` + +You also need to allow SSH access in your [Tailscale ACL policy](https://login.tailscale.com/admin/acls). At minimum, add an SSH rule and a traffic rule for the tag: + +```json +{ + "tagOwners": { + "tag:coder-workspace": ["autogroup:admin"] + }, + "acls": [ + { + "action": "accept", + "src": ["autogroup:member"], + "dst": ["tag:coder-workspace:*"] + } + ], + "ssh": [ + { + "action": "check", + "src": ["autogroup:member"], + "dst": ["tag:coder-workspace"], + "users": ["autogroup:nonroot", "root"] + } + ] +} +``` + +### Extra flags + +Pass any additional `tailscale up` flags not covered by dedicated variables: + +```tf +module "tailscale" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/dy-ma/tailscale/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + oauth_client_id = "kFvxxxxxxxxxx" + oauth_client_secret = "tskey-client-xxxx" + extra_flags = "--exit-node=100.64.0.1" +} +``` diff --git a/registry/dy-ma/modules/tailscale/main.test.ts b/registry/dy-ma/modules/tailscale/main.test.ts new file mode 100644 index 000000000..f4d32f69d --- /dev/null +++ b/registry/dy-ma/modules/tailscale/main.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "~test"; + +describe("tailscale", async () => { + type TestVariables = { + agent_id: string; + auth_key?: string; + tailscale_api_url?: string; + oauth_client_id?: string; + oauth_client_secret?: string; + tailnet?: string; + hostname?: string; + tags?: string; + ephemeral?: boolean; + preauthorized?: boolean; + networking_mode?: string; + socks5_proxy_port?: number; + http_proxy_port?: number; + accept_dns?: boolean; + accept_routes?: boolean; + advertise_routes?: string; + ssh?: boolean; + extra_flags?: string; + state_dir?: string; + }; + + await runTerraformInit(import.meta.dir); + + // Only agent_id has no default — all other vars are optional. + testRequiredVariables(import.meta.dir, { + agent_id: "some-agent-id", + }); + + // ── Outputs ─────────────────────────────────────────────────────────────── + + it("uses explicit hostname", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + hostname: "my-workspace", + }); + expect(state.outputs.hostname.value).toBe("my-workspace"); + }); + + it("defaults state_dir to empty string", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + }); + expect(state.outputs.state_dir.value).toBe(""); + }); + + it("uses explicit state_dir", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + state_dir: "/tmp/tailscale-state", + }); + expect(state.outputs.state_dir.value).toBe("/tmp/tailscale-state"); + }); + + // ── Validation ──────────────────────────────────────────────────────────── + + it("rejects invalid networking_mode", async () => { + try { + await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + networking_mode: "invalid", + }); + throw new Error("expected apply to fail"); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } + }); + + it("accepts all valid networking modes", async () => { + for (const mode of ["auto", "kernel", "userspace"]) { + await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + networking_mode: mode, + }); + } + }); + + it("rejects tags without tag: prefix", async () => { + try { + await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + tags: '["no-prefix"]', + }); + throw new Error("expected apply to fail"); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } + }); + + it("accepts tags with tag: prefix", async () => { + await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + tags: '["tag:coder", "tag:staging"]', + }); + }); +}); diff --git a/registry/dy-ma/modules/tailscale/main.tf b/registry/dy-ma/modules/tailscale/main.tf new file mode 100644 index 000000000..33df59cf6 --- /dev/null +++ b/registry/dy-ma/modules/tailscale/main.tf @@ -0,0 +1,219 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +data "coder_workspace" "me" {} + +locals { + icon_url = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/tailscale.svg" + hostname = var.hostname != "" ? var.hostname : data.coder_workspace.me.name + tags_json = jsonencode(var.tags) + tags_csv = join(",", var.tags) +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "auth_key" { + type = string + sensitive = true + default = "" + description = <<-EOF + A pre-generated Tailscale or Headscale auth key. When set, the OAuth + client credentials flow is skipped and this key is passed directly to + tailscale up. Use this for Headscale or when you prefer to manage key + rotation externally (e.g. via Vault). + + Either auth_key or both oauth_client_id and oauth_client_secret must be + provided. If auth_key is set, oauth_client_id and oauth_client_secret are + ignored. + EOF +} + +variable "tailscale_api_url" { + type = string + default = "https://api.tailscale.com" + description = <<-EOF + Base URL of the control server. Defaults to Tailscale's hosted service. + Set this to your own server URL (e.g. a Headscale instance). + EOF +} + +variable "oauth_client_id" { + type = string + sensitive = true + default = "" + description = "Tailscale OAuth client ID with the auth_keys scope." +} + +variable "oauth_client_secret" { + type = string + sensitive = true + default = "" + description = "Tailscale OAuth client secret with the auth_keys scope." +} + +variable "tailnet" { + type = string + default = "-" + description = "Tailnet name. Defaults to '-' which resolves to the default tailnet for the Oauth client." +} + +variable "hostname" { + type = string + default = "" + description = "Hostname to register in the tailnet. Leave blank to use the workspace name." +} + +variable "tags" { + type = list(string) + default = ["tag:coder-workspace"] + description = "ACL tags to apply to the node." + validation { + condition = alltrue([for t in var.tags : startswith(t, "tag:")]) + error_message = "All tags must start with \"tag:\"." + } +} + +variable "ephemeral" { + type = bool + default = true + description = "Whether to register the node as ephemeral." +} + +variable "preauthorized" { + type = bool + default = true + description = "Skip manual device approval when the node joins the tailnet" +} + +variable "networking_mode" { + type = string + default = "auto" + description = <<-EOF + Tailscale networking mode. + + auto — detect from environment. Uses kernel networking if + /dev/net/tun is accessible, userspace otherwise. + kernel — force kernel networking (requires TUN device). Suitable + for VMs and privileged containers. + userspace — force userspace networking. Required for unprivileged + containers. Enables SOCKS5/HTTP proxies for outbound + tailnet access. + EOF + validation { + condition = contains(["auto", "kernel", "userspace"], var.networking_mode) + error_message = "networking_mode must be one of: auto, kernel, userspace." + } +} + +variable "socks5_proxy_port" { + type = number + default = 1080 + description = <<-EOF + Port for the SOCKS5 proxy exposed by tailscaled in userspace mode. + Set to 0 to disable. Only active when networking_mode resolves to userspace. + EOF +} + +variable "http_proxy_port" { + type = number + default = 3128 + description = <<-EOF + Port for the HTTP proxy exposed by tailscaled in userspace mode. + Set to 0 to disable. Only active when networking_mode resolves to userspace. + EOF +} + +variable "accept_dns" { + type = bool + default = true + description = "Accept DNS configuration from the tailnet (MagicDNS)." +} + +variable "accept_routes" { + type = bool + default = false + description = "Accept subnet routes advertised by other nodes in the tailnet" +} + +variable "advertise_routes" { + type = list(string) + default = [] + description = "CIDR ranges this workspace should advertise as subnet routes." +} + +variable "ssh" { + type = bool + default = false + description = "Enable Tailscale SSH. Allows other tailnet nodes to ssh into this workspace as defined by your tailnet policy." +} + +variable "extra_flags" { + type = string + default = "" + description = <<-EOF + Additional flags to append to the `tailscale up` command verbatim. + Use this for any options not covered by dedicated variables, e.g. + `--exit-node=100.x.y.z` or `--shields-up`. + EOF +} + +variable "state_dir" { + type = string + default = "" + description = <<-EOF + Directory for tailscaled state files. Leave empty to use tailscaled's + default location. Override to a persistent path on VMs (e.g. + /var/lib/tailscale) or a non-persistent path on ephemeral pods + (e.g. /tmp/tailscale-state). + EOF +} + +resource "coder_script" "install_tailscale" { + agent_id = var.agent_id + display_name = "Tailscale" + icon = local.icon_url + script = templatefile("${path.module}/run.sh", { + TAILSCALE_API_URL = var.tailscale_api_url + AUTH_KEY = var.auth_key + OAUTH_CLIENT_ID = var.oauth_client_id + OAUTH_CLIENT_SECRET = var.oauth_client_secret + TAILNET = var.tailnet + HOSTNAME = local.hostname + TAGS_JSON = local.tags_json + TAGS_CSV = local.tags_csv + EPHEMERAL = var.ephemeral + PREAUTHORIZED = var.preauthorized + NETWORKING_MODE = var.networking_mode + SOCKS5_PORT = var.socks5_proxy_port + HTTP_PROXY_PORT = var.http_proxy_port + ACCEPT_DNS = var.accept_dns + ACCEPT_ROUTES = var.accept_routes + ADVERTISE_ROUTES = join(",", var.advertise_routes) + SSH = var.ssh + EXTRA_FLAGS = var.extra_flags + STATE_DIR = var.state_dir + }) + run_on_start = true + run_on_stop = false +} + +output "hostname" { + description = "Hostname registered in tailnet." + value = local.hostname +} + +output "state_dir" { + description = "Directory where tailscaled state is persisted. Empty string means tailscaled's default location." + value = var.state_dir +} \ No newline at end of file diff --git a/registry/dy-ma/modules/tailscale/run.sh b/registry/dy-ma/modules/tailscale/run.sh new file mode 100755 index 000000000..d90db7d27 --- /dev/null +++ b/registry/dy-ma/modules/tailscale/run.sh @@ -0,0 +1,233 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Values injected by templatefile() in main.tf +TAILSCALE_API_URL="${TAILSCALE_API_URL}" +AUTH_KEY="${AUTH_KEY}" +OAUTH_CLIENT_ID="${OAUTH_CLIENT_ID}" +OAUTH_CLIENT_SECRET="${OAUTH_CLIENT_SECRET}" +TAILNET="${TAILNET}" +TS_HOSTNAME="${HOSTNAME}" +TAGS_JSON='${TAGS_JSON}' +TAGS_CSV="${TAGS_CSV}" +EPHEMERAL="${EPHEMERAL}" +PREAUTHORIZED="${PREAUTHORIZED}" +NETWORKING_MODE="${NETWORKING_MODE}" +SOCKS5_PORT="${SOCKS5_PORT}" +HTTP_PROXY_PORT="${HTTP_PROXY_PORT}" +ACCEPT_DNS="${ACCEPT_DNS}" +ACCEPT_ROUTES="${ACCEPT_ROUTES}" +ADVERTISE_ROUTES="${ADVERTISE_ROUTES}" +SSH="${SSH}" +EXTRA_FLAGS="${EXTRA_FLAGS}" +STATE_DIR="${STATE_DIR}" + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +log() { echo "[tailscale] $*" >&2; } +die() { + echo "[tailscale] ERROR: $*" >&2 + exit 1 +} +has() { command -v "$1" &> /dev/null; } + +# ── 1. Install Tailscale ────────────────────────────────────────────────────── + +install_tailscale() { + if has tailscale; then + log "Tailscale already installed ($(tailscale version 2> /dev/null | awk 'NR==1{print $1}')), skipping." + return + fi + + log "Installing Tailscale..." + curl -fsSL https://tailscale.com/install.sh | sh + log "Installed: $(tailscale version | head -1)" +} + +# ── 2. Detect networking mode ───────────────────────────────────────────────── + +resolve_networking_mode() { + if [ "$NETWORKING_MODE" != "auto" ]; then + echo "$NETWORKING_MODE" + return + fi + if [ -c /dev/net/tun ] && [ -r /dev/net/tun ] && [ -w /dev/net/tun ]; then + echo "kernel" + else + echo "userspace" + fi +} + +# ── 3. Start tailscaled ─────────────────────────────────────────────────────── + +start_tailscaled() { + local mode="$1" + + # Build daemon flags + local daemon_flags="--socket=/var/run/tailscale/tailscaled.sock" + if [ -n "$STATE_DIR" ]; then + mkdir -p "$STATE_DIR" + daemon_flags="--state=$STATE_DIR/tailscaled.state $daemon_flags" + fi + if [ "$mode" = "userspace" ]; then + daemon_flags="$daemon_flags --tun=userspace-networking" + [ "$SOCKS5_PORT" != "0" ] && daemon_flags="$daemon_flags --socks5-server=localhost:$SOCKS5_PORT" + [ "$HTTP_PROXY_PORT" != "0" ] && daemon_flags="$daemon_flags --outbound-http-proxy-listen=localhost:$HTTP_PROXY_PORT" + fi + + if has systemctl && systemctl is-system-running --quiet 2> /dev/null; then + if [ "$mode" = "userspace" ]; then + # Drop-in override so we don't touch the upstream unit file + sudo mkdir -p /etc/systemd/system/tailscaled.service.d + printf '[Service]\nExecStart=\nExecStart=-/usr/sbin/tailscaled %s\n' \ + "$daemon_flags" \ + | sudo tee /etc/systemd/system/tailscaled.service.d/coder.conf > /dev/null + sudo systemctl daemon-reload + fi + sudo systemctl enable --now tailscaled + log "tailscaled started via systemd." + else + if pgrep -x tailscaled &> /dev/null; then + log "tailscaled already running." + return + fi + sudo mkdir -p /var/run/tailscale + # shellcheck disable=SC2086 + sudo tailscaled $daemon_flags &> /tmp/tailscaled.log & + sleep 2 + log "tailscaled started in background." + fi +} + +# ── 4. Generate a single-use auth key ───────────────────────────────────────── +# OAuth creds stay on this machine. We exchange them for a short-lived +# access token, use that to create a 5-minute single-use auth key, then +# discard both. The auth key is the only thing passed to tailscale up. + +generate_auth_key() { + has curl || die "curl is required." + has jq || die "jq is required." + + log "Fetching Tailscale access token..." + local token_response + token_response=$(curl -fsSL \ + -d "client_id=$OAUTH_CLIENT_ID" \ + -d "client_secret=$OAUTH_CLIENT_SECRET" \ + "$TAILSCALE_API_URL/api/v2/oauth/token") \ + || die "Failed to fetch OAuth access token." + + local access_token + access_token=$(echo "$token_response" | jq -r '.access_token') + [ "$access_token" = "null" ] || [ -z "$access_token" ] \ + && die "OAuth token response did not contain an access_token. Check your client ID and secret." + + log "Generating single-use auth key..." + local key_response http_status + key_response=$(curl -sSL -w "\n%%{http_code}" -X POST \ + -H "Authorization: Bearer $access_token" \ + -H "Content-Type: application/json" \ + -d "{ + \"capabilities\": { + \"devices\": { + \"create\": { + \"reusable\": false, + \"ephemeral\": $EPHEMERAL, + \"preauthorized\": $PREAUTHORIZED, + \"tags\": $TAGS_JSON + } + } + }, + \"expirySeconds\": 300 + }" \ + "$TAILSCALE_API_URL/api/v2/tailnet/$TAILNET/keys") + http_status=$(echo "$key_response" | tail -1) + key_response=$(echo "$key_response" | head -n -1) + if [ "$http_status" != "200" ]; then + die "Failed to generate auth key (HTTP $http_status): $key_response" + fi + + local auth_key + auth_key=$(echo "$key_response" | jq -r '.key') + [ "$auth_key" = "null" ] || [ -z "$auth_key" ] \ + && die "Key response did not contain a key. Response: $key_response" + + echo "$auth_key" +} + +# ── 5. Bring up Tailscale ───────────────────────────────────────────────────── + +bring_up() { + local auth_key="$1" + local mode="$2" + + # Assemble tailscale up flags + local flags="--hostname=$TS_HOSTNAME" + flags="$flags --advertise-tags=$TAGS_CSV" + flags="$flags --accept-dns=$ACCEPT_DNS" + [ "$TAILSCALE_API_URL" != "https://api.tailscale.com" ] && flags="$flags --login-server=$TAILSCALE_API_URL" + [ "$ACCEPT_ROUTES" = "true" ] && flags="$flags --accept-routes" + [ -n "$ADVERTISE_ROUTES" ] && flags="$flags --advertise-routes=$ADVERTISE_ROUTES" + [ "$SSH" = "true" ] && flags="$flags --ssh" + [ "$mode" = "userspace" ] && flags="$flags --netfilter-mode=off" + [ -n "$EXTRA_FLAGS" ] && flags="$flags $EXTRA_FLAGS" + + if [ -n "$auth_key" ]; then + # shellcheck disable=SC2086 + sudo tailscale up --auth-key="$auth_key" $flags + else + # Already authenticated — re-apply flags only, no re-auth + # shellcheck disable=SC2086 + sudo tailscale up $flags + fi +} + +# ── 6. Set proxy env vars (userspace only) ──────────────────────────────────── + +configure_proxy_env() { + local mode="$1" + [ "$mode" != "userspace" ] && return + + local lines="" + [ "$SOCKS5_PORT" != "0" ] \ + && lines="$lines"$'\n'"export ALL_PROXY=socks5://localhost:$SOCKS5_PORT" + [ "$HTTP_PROXY_PORT" != "0" ] \ + && lines="$lines"$'\n'"export http_proxy=http://localhost:$HTTP_PROXY_PORT"$'\n'"export https_proxy=http://localhost:$HTTP_PROXY_PORT" + + if [ -n "$lines" ]; then + printf '# Set by tailscale Coder module%s\n' "$lines" \ + | sudo tee /etc/profile.d/tailscale-proxy.sh > /dev/null + log "Proxy env vars written to /etc/profile.d/tailscale-proxy.sh" + fi +} + +# ── Main ────────────────────────────────────────────────────────────────────── + +main() { + install_tailscale + + local mode + mode=$(resolve_networking_mode) + log "Networking mode: $mode" + + start_tailscaled "$mode" + + local auth_key="" + if [ -n "$AUTH_KEY" ]; then + log "Using provided auth key." + auth_key="$AUTH_KEY" + elif sudo tailscale status --json 2> /dev/null | grep -q '"BackendState":"Running"'; then + log "Tailscale already connected. Re-applying flags..." + # auth_key stays empty — bring_up will skip --auth-key + else + log "Not connected. Generating auth key via OAuth..." + auth_key=$(generate_auth_key) + fi + + bring_up "$auth_key" "$mode" + configure_proxy_env "$mode" + + log "Status:" + tailscale status +} + +main diff --git a/registry/dy-ma/modules/tailscale/tailscale.tftest.hcl b/registry/dy-ma/modules/tailscale/tailscale.tftest.hcl new file mode 100644 index 000000000..e2e40cc22 --- /dev/null +++ b/registry/dy-ma/modules/tailscale/tailscale.tftest.hcl @@ -0,0 +1,46 @@ +run "plan_with_required_vars" { + command = plan + + variables { + agent_id = "example-agent-id" + } +} + +run "plan_with_oauth" { + command = plan + + variables { + agent_id = "example-agent-id" + oauth_client_id = "tskey-client-xxxx" + oauth_client_secret = "tskey-secret-xxxx" + } +} + +run "plan_with_auth_key" { + command = plan + + variables { + agent_id = "example-agent-id" + auth_key = "tskey-auth-xxxx" + } +} + +run "plan_userspace_mode" { + command = plan + + variables { + agent_id = "example-agent-id" + auth_key = "tskey-auth-xxxx" + networking_mode = "userspace" + } +} + +run "plan_with_extra_flags" { + command = plan + + variables { + agent_id = "example-agent-id" + auth_key = "tskey-auth-xxxx" + extra_flags = "--shields-up" + } +}