diff --git a/.github/workflows/drupal-contrib-integration-test.yml b/.github/workflows/drupal-contrib-integration-test.yml index 012ebb3..ed37870 100644 --- a/.github/workflows/drupal-contrib-integration-test.yml +++ b/.github/workflows/drupal-contrib-integration-test.yml @@ -132,7 +132,7 @@ jobs: --parameter issue_fork= \ --parameter issue_branch= \ --parameter install_profile=minimal \ - --parameter share_drupal_site=owner \ + --parameter share_drupal_site=false \ --use-parameter-defaults \ --yes @@ -201,6 +201,17 @@ jobs: fi echo "OK: Drupal site is accessible and shows login page" + # share_drupal_site defaults to false: an anonymous request (no + # session token) must be bounced to Coder's own login, never reach + # Drupal. This is the private-by-default half of issue #204's fix. + ANON_FINAL_URL=$(curl -sL --max-time 30 -o /dev/null -w "%{url_effective}" "$SITE_URL") + echo "Anonymous request final URL: $ANON_FINAL_URL" + if [[ "$ANON_FINAL_URL" != *"/login"* ]]; then + echo "ERROR: anonymous request was not redirected to Coder login — share_drupal_site=false is not private" >&2 + exit 1 + fi + echo "OK: anonymous request redirected to Coder login (site is private by default)" + - name: Delete workspace if: always() run: coder delete ${{ env.WORKSPACE_NAME }} --yes || true @@ -328,7 +339,7 @@ jobs: --parameter issue_fork=${{ env.ISSUE_NUMBER }} \ --parameter issue_branch=${{ env.ISSUE_BRANCH }} \ --parameter install_profile=minimal \ - --parameter share_drupal_site=owner \ + --parameter share_drupal_site=false \ --use-parameter-defaults \ --yes @@ -395,6 +406,17 @@ jobs: fi echo "OK: Drupal site is accessible and shows login page" + # share_drupal_site defaults to false: an anonymous request (no + # session token) must be bounced to Coder's own login, never reach + # Drupal. This is the private-by-default half of issue #204's fix. + ANON_FINAL_URL=$(curl -sL --max-time 30 -o /dev/null -w "%{url_effective}" "$SITE_URL") + echo "Anonymous request final URL: $ANON_FINAL_URL" + if [[ "$ANON_FINAL_URL" != *"/login"* ]]; then + echo "ERROR: anonymous request was not redirected to Coder login — share_drupal_site=false is not private" >&2 + exit 1 + fi + echo "OK: anonymous request redirected to Coder login (site is private by default)" + - name: Delete workspace if: always() run: coder delete ${{ env.WORKSPACE_NAME }} --yes || true @@ -489,7 +511,7 @@ jobs: --parameter issue_fork= \ --parameter issue_branch= \ --parameter install_profile=minimal \ - --parameter share_drupal_site=owner \ + --parameter share_drupal_site=false \ --use-parameter-defaults \ --yes @@ -557,6 +579,64 @@ jobs: fi echo "OK: Drupal site is accessible and shows login page" + # share_drupal_site defaults to false: an anonymous request (no + # session token) must be bounced to Coder's own login, never reach + # Drupal. This is the private-by-default half of issue #204's fix. + ANON_FINAL_URL=$(curl -sL --max-time 30 -o /dev/null -w "%{url_effective}" "$SITE_URL") + echo "Anonymous request final URL: $ANON_FINAL_URL" + if [[ "$ANON_FINAL_URL" != *"/login"* ]]; then + echo "ERROR: anonymous request was not redirected to Coder login — share_drupal_site=false is not private" >&2 + exit 1 + fi + echo "OK: anonymous request redirected to Coder login (site is private by default)" + + - name: Enable public sharing and rebuild + # `coder update`/`coder restart` alone silently ignore --parameter + # unless the workspace is otherwise outdated. --always-prompt forces + # every MUTABLE rich parameter to be re-resolved (never pulled from + # the existing build), so every mutable value from the original + # `coder create` must be repeated here — anything left out falls back + # to the template default via --use-parameter-defaults, not its prior + # value. project_name/project_type are immutable, so they must NOT be + # passed here at all — Coder rejects --always-prompt attempting to + # "change" an immutable parameter even to its current value, but + # silently keeps the existing value when it's simply omitted. + run: | + coder restart ${{ env.WORKSPACE_NAME }} --always-prompt --use-parameter-defaults \ + --parameter "vscode_extensions=[]" \ + --parameter drupal_version=${{ matrix.drupal_version }} \ + --parameter issue_fork= \ + --parameter issue_branch= \ + --parameter install_profile=minimal \ + --parameter share_drupal_site=true \ + --yes + + - name: Verify workspace — agent reconnected after restart + run: coder ssh ${{ env.WORKSPACE_NAME }} --wait=yes -- echo "Agent reconnected" + + - name: Verify workspace — Drupal site public when share_drupal_site=true + run: | + CODER_DOMAIN="${{ vars.TEST_CODER_URL }}" + CODER_DOMAIN="${CODER_DOMAIN#https://}" + SITE_URL="https://drupal-site--${{ env.WORKSPACE_NAME }}--${OWNER}.${CODER_DOMAIN}" + echo "Checking $SITE_URL anonymously" + HTTP_STATUS=$(curl -sL --max-time 30 --retry 3 --retry-delay 5 \ + -o /tmp/drupal-public-response.html \ + -w "%{http_code}" \ + "$SITE_URL") + echo "HTTP status: $HTTP_STATUS" + if [[ ! "$HTTP_STATUS" =~ ^2 ]]; then + echo "ERROR: unexpected HTTP status $HTTP_STATUS for a public anonymous request" >&2 + head -50 /tmp/drupal-public-response.html >&2 || true + exit 1 + fi + if ! grep -qi "drupal" /tmp/drupal-public-response.html; then + echo "ERROR: anonymous response does not look like the Drupal site — share_drupal_site=true did not make it public" >&2 + head -50 /tmp/drupal-public-response.html >&2 + exit 1 + fi + echo "OK: anonymous request reached the public Drupal site (share_drupal_site=true works)" + - name: Delete workspace if: always() run: coder delete ${{ env.WORKSPACE_NAME }} --yes || true @@ -668,7 +748,7 @@ jobs: --parameter issue_fork=${{ env.ISSUE_NUMBER }} \ --parameter issue_branch=${{ env.ISSUE_BRANCH }} \ --parameter install_profile=minimal \ - --parameter share_drupal_site=owner \ + --parameter share_drupal_site=false \ --use-parameter-defaults \ --yes @@ -744,6 +824,17 @@ jobs: fi echo "OK: Drupal site is accessible and shows login page" + # share_drupal_site defaults to false: an anonymous request (no + # session token) must be bounced to Coder's own login, never reach + # Drupal. This is the private-by-default half of issue #204's fix. + ANON_FINAL_URL=$(curl -sL --max-time 30 -o /dev/null -w "%{url_effective}" "$SITE_URL") + echo "Anonymous request final URL: $ANON_FINAL_URL" + if [[ "$ANON_FINAL_URL" != *"/login"* ]]; then + echo "ERROR: anonymous request was not redirected to Coder login — share_drupal_site=false is not private" >&2 + exit 1 + fi + echo "OK: anonymous request redirected to Coder login (site is private by default)" + - name: Delete workspace if: always() run: coder delete ${{ env.WORKSPACE_NAME }} --yes || true @@ -863,7 +954,7 @@ jobs: --parameter issue_fork=${{ env.ISSUE_NUMBER }} \ --parameter issue_branch=${{ env.ISSUE_BRANCH }} \ --parameter install_profile=minimal \ - --parameter share_drupal_site=owner \ + --parameter share_drupal_site=false \ --use-parameter-defaults \ --yes @@ -953,6 +1044,17 @@ jobs: fi echo "OK: Drupal site is accessible and shows login page" + # share_drupal_site defaults to false: an anonymous request (no + # session token) must be bounced to Coder's own login, never reach + # Drupal. This is the private-by-default half of issue #204's fix. + ANON_FINAL_URL=$(curl -sL --max-time 30 -o /dev/null -w "%{url_effective}" "$SITE_URL") + echo "Anonymous request final URL: $ANON_FINAL_URL" + if [[ "$ANON_FINAL_URL" != *"/login"* ]]; then + echo "ERROR: anonymous request was not redirected to Coder login — share_drupal_site=false is not private" >&2 + exit 1 + fi + echo "OK: anonymous request redirected to Coder login (site is private by default)" + - name: Delete workspace if: always() run: coder delete ${{ env.WORKSPACE_NAME }} --yes || true diff --git a/.github/workflows/drupal-integration-test.yml b/.github/workflows/drupal-integration-test.yml index bb2bd1f..d90025f 100644 --- a/.github/workflows/drupal-integration-test.yml +++ b/.github/workflows/drupal-integration-test.yml @@ -131,7 +131,7 @@ jobs: --parameter issue_fork= \ --parameter issue_branch= \ --parameter install_profile=minimal \ - --parameter share_drupal_site=owner \ + --parameter share_drupal_site=false \ --use-parameter-defaults \ --yes @@ -188,6 +188,17 @@ jobs: fi echo "OK: Drupal site is accessible and shows login page" + # share_drupal_site defaults to false: an anonymous request (no + # session token) must be bounced to Coder's own login, never reach + # Drupal. This is the private-by-default half of issue #204's fix. + ANON_FINAL_URL=$(curl -sL --max-time 30 -o /dev/null -w "%{url_effective}" "$SITE_URL") + echo "Anonymous request final URL: $ANON_FINAL_URL" + if [[ "$ANON_FINAL_URL" != *"/login"* ]]; then + echo "ERROR: anonymous request was not redirected to Coder login — share_drupal_site=false is not private" >&2 + exit 1 + fi + echo "OK: anonymous request redirected to Coder login (site is private by default)" + - &delete-workspace name: Delete workspace if: always() @@ -281,7 +292,7 @@ jobs: --parameter issue_fork= \ --parameter issue_branch= \ --parameter install_profile=minimal \ - --parameter share_drupal_site=owner \ + --parameter share_drupal_site=false \ --use-parameter-defaults \ --yes @@ -292,6 +303,50 @@ jobs: - *verify-drush - *record-host-dir - *verify-http + + - name: Enable public sharing and rebuild + # `coder update`/`coder restart` alone silently ignore --parameter + # unless the workspace is otherwise outdated. --always-prompt forces + # every rich parameter to be re-resolved (never pulled from the + # existing build), so every value from the original `coder create` + # must be repeated here — anything left out falls back to the + # template default via --use-parameter-defaults, not its prior value. + run: | + coder restart ${{ env.WORKSPACE_NAME }} --always-prompt --use-parameter-defaults \ + --parameter "vscode_extensions=[]" \ + --parameter drupal_version=${{ matrix.drupal_version }} \ + --parameter issue_fork= \ + --parameter issue_branch= \ + --parameter install_profile=minimal \ + --parameter share_drupal_site=true \ + --yes + + - name: Verify workspace — agent reconnected after restart + run: coder ssh ${{ env.WORKSPACE_NAME }} --wait=yes -- echo "Agent reconnected" + + - name: Verify workspace — Drupal site public when share_drupal_site=true + run: | + CODER_DOMAIN="${{ vars.TEST_CODER_URL }}" + CODER_DOMAIN="${CODER_DOMAIN#https://}" + SITE_URL="https://drupal-site--${{ env.WORKSPACE_NAME }}--${OWNER}.${CODER_DOMAIN}" + echo "Checking $SITE_URL anonymously" + HTTP_STATUS=$(curl -sL --max-time 30 --retry 3 --retry-delay 5 \ + -o /tmp/drupal-public-response.html \ + -w "%{http_code}" \ + "$SITE_URL") + echo "HTTP status: $HTTP_STATUS" + if [[ ! "$HTTP_STATUS" =~ ^2 ]]; then + echo "ERROR: unexpected HTTP status $HTTP_STATUS for a public anonymous request" >&2 + head -50 /tmp/drupal-public-response.html >&2 || true + exit 1 + fi + if ! grep -qi "drupal" /tmp/drupal-public-response.html; then + echo "ERROR: anonymous response does not look like the Drupal site — share_drupal_site=true did not make it public" >&2 + head -50 /tmp/drupal-public-response.html >&2 + exit 1 + fi + echo "OK: anonymous request reached the public Drupal site (share_drupal_site=true works)" + - *delete-workspace - name: Release staging box lock @@ -389,7 +444,7 @@ jobs: --parameter issue_fork=${{ env.ISSUE_FORK }} \ --parameter issue_branch=${{ env.ISSUE_BRANCH }} \ --parameter install_profile=minimal \ - --parameter share_drupal_site=owner \ + --parameter share_drupal_site=false \ --use-parameter-defaults \ --yes @@ -445,6 +500,17 @@ jobs: fi echo "OK: Drupal site is accessible and shows login page" + # share_drupal_site defaults to false: an anonymous request (no + # session token) must be bounced to Coder's own login, never reach + # Drupal. This is the private-by-default half of issue #204's fix. + ANON_FINAL_URL=$(curl -sL --max-time 30 -o /dev/null -w "%{url_effective}" "$SITE_URL") + echo "Anonymous request final URL: $ANON_FINAL_URL" + if [[ "$ANON_FINAL_URL" != *"/login"* ]]; then + echo "ERROR: anonymous request was not redirected to Coder login — share_drupal_site=false is not private" >&2 + exit 1 + fi + echo "OK: anonymous request redirected to Coder login (site is private by default)" + - name: Delete workspace if: always() run: coder delete ${{ env.WORKSPACE_NAME }} --yes || true @@ -548,7 +614,7 @@ jobs: --parameter issue_fork=${{ env.ISSUE_FORK }} \ --parameter issue_branch=${{ env.ISSUE_BRANCH }} \ --parameter install_profile=minimal \ - --parameter share_drupal_site=owner \ + --parameter share_drupal_site=false \ --use-parameter-defaults \ --yes @@ -612,6 +678,17 @@ jobs: fi echo "OK: Drupal site is accessible and shows login page" + # share_drupal_site defaults to false: an anonymous request (no + # session token) must be bounced to Coder's own login, never reach + # Drupal. This is the private-by-default half of issue #204's fix. + ANON_FINAL_URL=$(curl -sL --max-time 30 -o /dev/null -w "%{url_effective}" "$SITE_URL") + echo "Anonymous request final URL: $ANON_FINAL_URL" + if [[ "$ANON_FINAL_URL" != *"/login"* ]]; then + echo "ERROR: anonymous request was not redirected to Coder login — share_drupal_site=false is not private" >&2 + exit 1 + fi + echo "OK: anonymous request redirected to Coder login (site is private by default)" + - name: Delete workspace if: always() run: coder delete ${{ env.WORKSPACE_NAME }} --yes || true diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index b60374d..ed2838c 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -480,6 +480,73 @@ jobs: "${{ env.CI_TAG }}" "${{ env.WORKSPACE_NAME }}" "${OWNER}" "${CODER_DOMAIN}" \ < /dev/null + - name: Verify freeform sharing — private by default + if: ${{ matrix.template == 'freeform' }} + run: | + CODER_DOMAIN="${{ vars.TEST_CODER_URL }}" + CODER_DOMAIN="${CODER_DOMAIN#https://}" + SITE_URL="https://ci-site1-${{ env.CI_TAG }}--${{ env.WORKSPACE_NAME }}--${OWNER}.${CODER_DOMAIN}" + FINAL_URL=$(curl -sL --max-time 30 -o /dev/null -w "%{url_effective}" "$SITE_URL") + echo "Anonymous request final URL: $FINAL_URL" + if [[ "$FINAL_URL" != *"/login"* ]]; then + echo "ERROR: anonymous request was not redirected to Coder login — share_projects=false is not private" >&2 + exit 1 + fi + echo "OK: anonymous request redirected to Coder login (freeform sites are private by default)" + + - name: Enable public sharing and rebuild (freeform) + if: ${{ matrix.template == 'freeform' }} + # `coder update`/`coder restart` alone silently ignore --parameter + # unless the workspace is otherwise outdated. --always-prompt forces + # every rich parameter to be re-resolved (never pulled from the + # existing build), so every value from the original `coder create` + # (project_names, vscode_extensions) must be repeated here. Uses + # --rich-parameter-file, not inline --parameter, because project_names + # is a comma-separated value and coder's --parameter flag itself + # splits on commas (same reason the original "Create freeform + # workspace" step above uses a file instead of --parameter). + run: | + cat > /tmp/freeform-share-gh-${{ env.CI_TAG }}.yaml << EOF + project_names: "ci-site1-${{ env.CI_TAG }},ci-site2-${{ env.CI_TAG }}" + vscode_extensions: "[]" + share_projects: "true" + EOF + coder restart ${{ env.WORKSPACE_NAME }} --always-prompt --use-parameter-defaults \ + --rich-parameter-file /tmp/freeform-share-gh-${{ env.CI_TAG }}.yaml \ + --yes + + - name: Verify workspace — agent reconnected after restart + if: ${{ matrix.template == 'freeform' }} + run: coder ssh ${{ env.WORKSPACE_NAME }} --wait=yes -- echo "Agent reconnected" + + - name: Recreate freeform test projects after rebuild + if: ${{ matrix.template == 'freeform' }} + # test-freeform-start.sh puts projects under /tmp, which is wiped + # when the container is recreated by the restart above — the DDEV + # projects (and ddev-router) must be brought up again from scratch. + run: | + scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + -o ProxyCommand="coder ssh --stdio ${{ env.WORKSPACE_NAME }}" \ + freeform/scripts/test-freeform-start.sh \ + coder@workspace:/tmp/test-freeform-start.sh + coder ssh ${{ env.WORKSPACE_NAME }} -- \ + env CI=${{ env.CI }} DDEV_NONINTERACTIVE=${{ env.DDEV_NONINTERACTIVE }} NO_COLOR=${{ env.NO_COLOR }} \ + bash /tmp/test-freeform-start.sh "${{ env.CI_TAG }}" < /dev/null + + - name: Verify freeform sharing — public when share_projects=true + if: ${{ matrix.template == 'freeform' }} + run: | + CODER_DOMAIN="${{ vars.TEST_CODER_URL }}" + CODER_DOMAIN="${CODER_DOMAIN#https://}" + SITE_URL="https://ci-site1-${{ env.CI_TAG }}--${{ env.WORKSPACE_NAME }}--${OWNER}.${CODER_DOMAIN}" + FINAL_URL=$(curl -sL --max-time 30 --retry 3 --retry-delay 5 -o /dev/null -w "%{url_effective}" "$SITE_URL") + echo "Anonymous request final URL: $FINAL_URL" + if [[ "$FINAL_URL" == *"/login"* ]]; then + echo "ERROR: anonymous request was redirected to Coder login — share_projects=true did not make the site public" >&2 + exit 1 + fi + echo "OK: anonymous request was not redirected to Coder login (share_projects=true works)" + - name: Cleanup freeform PHP projects if: ${{ always() && matrix.template == 'freeform' }} run: | diff --git a/CLAUDE.md b/CLAUDE.md index 64c0495..e00f8cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,8 @@ When writing or reviewing `coder` CLI commands in documentation, **always verify - `coder tokens` uses `remove` not `revoke`; `create` takes `--name`, not a positional arg - `coder templates list` has no `--organization` flag — use the global `--org` - `coder sharing add` requires `--user :` (role required) +- `coder update --parameter X=Y` / `coder restart --parameter X=Y` **silently no-op** on the parameter unless `--always-prompt` is also passed — without it, Coder just reuses the workspace's existing value and (for `update`) may skip the rebuild entirely if the template version hasn't changed. With `--always-prompt`, every *mutable* parameter must be supplied via `--parameter` or it falls back to the template default via `--use-parameter-defaults` (not the workspace's current value) — and any *immutable* parameter (e.g. drupal-contrib's `project_name`) must be **omitted entirely**, since `--always-prompt` errors trying to "change" it even to its current value. Verified live against staging-coder.ddev.com while building the sharing feature (see PR for #204). +- `coder create`/`coder update`/`coder restart --parameter name=value` **splits the value on commas** (it's a `string-array` flag) — a parameter whose value legitimately contains a comma (e.g. freeform's `project_names="a,b"`) fails with `unable to parse rich parameters: format key=value expected, but got b`. Use `--rich-parameter-file ` instead for any comma-containing value; this bit CI on the first attempt at the freeform sharing test (PR #205). ## DDEV Command Gotchas diff --git a/docs/reference/coder-url-patterns.md b/docs/reference/coder-url-patterns.md index 1ced93d..305d181 100644 --- a/docs/reference/coder-url-patterns.md +++ b/docs/reference/coder-url-patterns.md @@ -52,6 +52,8 @@ The Coder server terminates TLS and reverse-proxies to the `url` configured on t | `"authenticated"` | Any logged-in Coder user | | `"public"` | Anyone with the URL | +All three attribute values above are still valid at the Terraform level, but the `drupal-core`, `drupal-contrib`, and `freeform` templates only ever expose a binary Private/Public switch (a `coder_parameter` of `type = "bool"`) through their workspace UI — they never set `share = "authenticated"`. + **Limits:** No documented per-workspace limit on `coder_app` resources. Use the `order` attribute (integer) and `group` attribute (string, max 64 chars) to control dashboard layout. --- diff --git a/drupal-contrib/README.md b/drupal-contrib/README.md index a0fb10c..3f45fde 100644 --- a/drupal-contrib/README.md +++ b/drupal-contrib/README.md @@ -63,6 +63,7 @@ The module/theme repo is the project root. Drupal core is installed as a dev dep | `issue_fork` | — | Issue number; empty = plain HEAD | | `issue_branch` | — | Branch name; empty = default branch HEAD | | `install_profile` | `minimal` | `minimal`, `standard`, or `demo_umami` | +| `share_drupal_site` | `false` | Public Sharing switch — see [Sharing Your Site](#sharing-your-site) | ## Access @@ -73,6 +74,10 @@ After setup, access your Drupal site through: **Admin credentials:** admin / admin +### Sharing Your Site + +By default the "Drupal Site" URL only works for you (the workspace owner). To share a work-in-progress with someone outside Coder, flip the **Public Sharing** switch in the workspace's parameters and restart the workspace — the change only takes effect on the next build. Turn it back off the same way when you're done. + ## Development Commands ```bash diff --git a/drupal-contrib/template.tf b/drupal-contrib/template.tf index d74bdc9..845e30e 100644 --- a/drupal-contrib/template.tf +++ b/drupal-contrib/template.tf @@ -163,25 +163,13 @@ data "coder_parameter" "install_profile" { data "coder_parameter" "share_drupal_site" { name = "share_drupal_site" - display_name = "Drupal Site Sharing" - description = "Who can access the Drupal site URL. Change to 'public' when you want to share a work-in-progress with someone outside Coder." - type = "string" - default = "owner" + display_name = "Public Sharing" + description = "Make the Drupal site URL public (anyone with the link, no Coder sign-in) so you can share a work-in-progress with someone outside Coder. Takes effect the next time this workspace restarts." + type = "bool" + form_type = "switch" + default = "false" mutable = true order = 90 - - option { - name = "Private (owner only)" - value = "owner" - } - option { - name = "Authenticated (any Coder user)" - value = "authenticated" - } - option { - name = "Public (anyone with the link)" - value = "public" - } } data "coder_parameter" "vscode_extensions" { @@ -213,9 +201,10 @@ locals { project_dir = "/home/coder/${data.coder_parameter.project_name.value}" issue_fork = data.coder_parameter.issue_fork.value issue_url = local.issue_fork != "" ? "https://www.drupal.org/project/${local.project_name}/issues/${local.issue_fork}" : "" - # Coerce share value — mock_data in tftest returns "[]" for all parameters; - # fall back to "owner" if the value is not a valid share level. - drupal_site_share = contains(["owner", "authenticated", "public"], data.coder_parameter.share_drupal_site.value) ? data.coder_parameter.share_drupal_site.value : "owner" + # mock_data in tftest returns "[]" for all parameters regardless of declared + # type; try() catches tobool("[]") failing and falls back to private/"owner". + drupal_site_share_public = try(tobool(data.coder_parameter.share_drupal_site.value), false) + drupal_site_share = local.drupal_site_share_public ? "public" : "owner" } locals { @@ -1125,37 +1114,25 @@ module "claude_remote_control" { agent_id = coder_agent.main.id } -resource "coder_app" "ddev-web" { - agent_id = coder_agent.main.id - slug = "ddev-web" - display_name = "DDEV Web" - order = 1 - url = "http://localhost:8080" - icon = "https://raw.githubusercontent.com/ddev/ddev/main/docs/content/developers/logos/SVG/Logo.svg" - subdomain = true - share = "owner" - - healthcheck { - url = "http://localhost:8080" - interval = 10 - threshold = 30 - } -} - +# Drupal Site (HTTP) - appears when DDEV project is running. +# Uses subdomain routing for unique URLs per workspace. Sharing is controlled +# by data.coder_parameter.share_drupal_site (Public Sharing switch above). resource "coder_app" "drupal-site" { agent_id = coder_agent.main.id slug = "drupal-site" display_name = "Drupal Site" - order = 2 + order = 1 url = "http://localhost:8080" - icon = "https://api.iconify.design/heroicons:check-circle.svg?color=white" + icon = "https://raw.githubusercontent.com/ddev/ddev/main/docs/content/developers/logos/SVG/Logo.svg" subdomain = true share = local.drupal_site_share + # Healthy when ddev is running and the web server responds (any 2xx/3xx). + # Lights up as soon as `ddev start` completes, before Drupal is installed. healthcheck { - url = "http://localhost:8080/user/login" + url = "http://localhost:8080" interval = 10 - threshold = 3 + threshold = 30 } } @@ -1284,4 +1261,8 @@ resource "coder_metadata" "workspace_info" { key = "issue_url" value = local.issue_url } + item { + key = "sharing" + value = local.drupal_site_share_public ? "Public (anyone with the link)" : "Private (owner only)" + } } diff --git a/drupal-contrib/tests/validate.tftest.hcl b/drupal-contrib/tests/validate.tftest.hcl index 9bc20b6..329ff3a 100644 --- a/drupal-contrib/tests/validate.tftest.hcl +++ b/drupal-contrib/tests/validate.tftest.hcl @@ -117,6 +117,28 @@ run "claude_code_enabled" { } } +run "sharing_disabled_by_default" { + command = plan + assert { + condition = coder_app.drupal-site.share == "owner" + error_message = "coder_app.drupal-site must default to share=owner" + } +} + +run "sharing_enabled" { + command = plan + override_data { + target = data.coder_parameter.share_drupal_site + values = { + value = "true" + } + } + assert { + condition = coder_app.drupal-site.share == "public" + error_message = "coder_app.drupal-site must be share=public when share_drupal_site=true" + } +} + run "claude_code_enabled_skip_permissions" { command = plan override_data { diff --git a/drupal-core/README.md b/drupal-core/README.md index fe2ab1d..7733d0b 100644 --- a/drupal-core/README.md +++ b/drupal-core/README.md @@ -23,7 +23,7 @@ Automated Coder workspace for Drupal core development using the [amateescu/ddev- **Standard Drupal core workspace:** ```bash coder create --template drupal-core my-drupal-dev -# Then access via Coder dashboard "DDEV Web" app +# Then access via Coder dashboard "Drupal Site" app ``` **Working on a specific issue:** @@ -40,10 +40,14 @@ coder create --template drupal-core my-issue-3568144 \ ## Access -- **Website**: Click "DDEV Web" in Coder dashboard +- **Website**: Click "Drupal Site" in Coder dashboard - **Admin Login**: Username `admin`, Password `admin` - **One-time Login**: Run `ddev drush uli` in terminal +### Sharing Your Site + +By default the "Drupal Site" URL only works for you (the workspace owner). To share a work-in-progress with someone outside Coder, flip the **Public Sharing** switch in the workspace's parameters and restart the workspace — the change only takes effect on the next build. Turn it back off the same way when you're done. + ## Project Structure ``` @@ -165,7 +169,7 @@ Create scripts in `~/.ddev/commands/host/` or `.ddev/commands/web/` 1. Make changes in VS Code (automatically opens to `/home/coder/drupal-core`) 2. Edit Drupal core files directly in `~/drupal-core/` -3. Test changes via DDEV Web app +3. Test changes via Drupal Site app 4. Run tests: `ddev phpunit core/modules/...` 5. Commit: `cd ~/drupal-core && git add . && git commit -m "..."` 6. Push to fork: `git push issue HEAD` diff --git a/drupal-core/template.tf b/drupal-core/template.tf index 6741a13..1068790 100644 --- a/drupal-core/template.tf +++ b/drupal-core/template.tf @@ -145,25 +145,13 @@ data "coder_parameter" "install_profile" { data "coder_parameter" "share_drupal_site" { name = "share_drupal_site" - display_name = "Drupal Site Sharing" - description = "Who can access the Drupal site URL. Change to 'public' when you want to share a work-in-progress with someone outside Coder." - type = "string" - default = "owner" + display_name = "Public Sharing" + description = "Make the Drupal site URL public (anyone with the link, no Coder sign-in) so you can share a work-in-progress with someone outside Coder. Takes effect the next time this workspace restarts." + type = "bool" + form_type = "switch" + default = "false" mutable = true order = 90 - - option { - name = "Private (owner only)" - value = "owner" - } - option { - name = "Authenticated (any Coder user)" - value = "authenticated" - } - option { - name = "Public (anyone with the link)" - value = "public" - } } data "coder_parameter" "vscode_extensions" { @@ -206,9 +194,10 @@ locals { selected_extensions = jsondecode(data.coder_parameter.vscode_extensions.value) issue_fork_clean = trimprefix(data.coder_parameter.issue_fork.value, "drupal-") issue_url = local.issue_fork_clean != "" ? "https://www.drupal.org/project/drupal/issues/${local.issue_fork_clean}" : "" - # Coerce share value — mock_data in tftest returns "[]" for all parameters; - # fall back to "owner" if the value is not a valid share level. - drupal_site_share = contains(["owner", "authenticated", "public"], data.coder_parameter.share_drupal_site.value) ? data.coder_parameter.share_drupal_site.value : "owner" + # mock_data in tftest returns "[]" for all parameters regardless of declared + # type; try() catches tobool("[]") failing and falls back to private/"owner". + drupal_site_share_public = try(tobool(data.coder_parameter.share_drupal_site.value), false) + drupal_site_share = local.drupal_site_share_public ? "public" : "owner" } locals { @@ -1087,7 +1076,7 @@ LAUNCH_EOF log_setup " $DRUPAL_DIR" log_setup "" log_setup "🌐 Access Your Site:" - log_setup " - Click 'DDEV Web' in Coder dashboard" + log_setup " - Click 'Drupal Site' in Coder dashboard" log_setup " - Or run: ddev launch" log_setup "" log_setup "🔐 Admin Credentials:" @@ -1264,7 +1253,7 @@ BASHCOMP echo "📄 Welcome message saved to ~/WELCOME.txt" echo "" echo "Next steps:" - echo " 1. Click 'DDEV Web' app to access your site" + echo " 1. Click 'Drupal Site' app to access your site" echo " 2. Log in with admin/admin" echo " 3. Run 'ddev drush uli' for one-time login link" echo "" @@ -1328,17 +1317,18 @@ module "claude_remote_control" { agent_id = coder_agent.main.id } -# DDEV Web Server (HTTP) - appears when DDEV project is running -# Uses subdomain routing for unique URLs per workspace -resource "coder_app" "ddev-web" { +# Drupal Site (HTTP) - appears when DDEV project is running. +# Uses subdomain routing for unique URLs per workspace. Sharing is controlled +# by data.coder_parameter.share_drupal_site (Public Sharing switch above). +resource "coder_app" "drupal-site" { agent_id = coder_agent.main.id - slug = "ddev-web" - display_name = "DDEV Web" + slug = "drupal-site" + display_name = "Drupal Site" order = 1 url = "http://localhost:80" icon = "https://raw.githubusercontent.com/ddev/ddev/main/docs/content/developers/logos/SVG/Logo.svg" subdomain = true - share = "owner" + share = local.drupal_site_share # Healthy when ddev is running and the web server responds (any 2xx/3xx). # Lights up as soon as `ddev start` completes, before Drupal is installed. @@ -1349,27 +1339,6 @@ resource "coder_app" "ddev-web" { } } -resource "coder_app" "drupal-site" { - agent_id = coder_agent.main.id - slug = "drupal-site" - display_name = "Drupal Site" - order = 2 - url = "http://localhost:80" - icon = "https://api.iconify.design/heroicons:check-circle.svg?color=white" - subdomain = true - share = local.drupal_site_share - - # Healthy only when Drupal returns 200. /user/login returns 500 when the - # database isn't set up (before drush si) and 200 when Drupal is fully - # installed — giving a "site is actually working" indicator distinct from - # "web server is up". - healthcheck { - url = "http://localhost:80/user/login" - interval = 10 - threshold = 3 - } -} - resource "coder_app" "mailpit" { agent_id = coder_agent.main.id slug = "mailpit" @@ -1534,6 +1503,10 @@ resource "coder_metadata" "workspace_info" { key = "issue_url" value = local.issue_url } + item { + key = "sharing" + value = local.drupal_site_share_public ? "Public (anyone with the link)" : "Private (owner only)" + } } # Output for Vault integration status (visible in Terraform logs) diff --git a/drupal-core/tests/validate.tftest.hcl b/drupal-core/tests/validate.tftest.hcl index c2b4316..bb52782 100644 --- a/drupal-core/tests/validate.tftest.hcl +++ b/drupal-core/tests/validate.tftest.hcl @@ -130,6 +130,34 @@ run "claude_code_enabled" { } } +run "sharing_disabled_by_default" { + command = plan + variables { + cache_path = "/tmp/mock-cache" + } + assert { + condition = coder_app.drupal-site.share == "owner" + error_message = "coder_app.drupal-site must default to share=owner" + } +} + +run "sharing_enabled" { + command = plan + variables { + cache_path = "/tmp/mock-cache" + } + override_data { + target = data.coder_parameter.share_drupal_site + values = { + value = "true" + } + } + assert { + condition = coder_app.drupal-site.share == "public" + error_message = "coder_app.drupal-site must be share=public when share_drupal_site=true" + } +} + run "claude_code_enabled_skip_permissions" { command = plan variables { diff --git a/freeform/README.md b/freeform/README.md index 59aedbc..e9d9a89 100644 --- a/freeform/README.md +++ b/freeform/README.md @@ -49,6 +49,10 @@ ddev start Then click **DDEV Web** or **Mailpit** in the Coder dashboard. +### Sharing Your Site + +By default every project's web app URL only works for you (the workspace owner). To share a work-in-progress with someone outside Coder, flip the **Public Sharing** switch in the workspace's parameters and restart the workspace — the change only takes effect on the next build. It applies to every project listed in **DDEV project names** at once (there's no per-project switch); Mailpit, xhgui, and Adminer always stay private regardless. Turn it back off the same way when you're done. + ## Project Structure - `/` — your project directory (any name, any location) diff --git a/freeform/template.tf b/freeform/template.tf index ab84525..cad0f17 100644 --- a/freeform/template.tf +++ b/freeform/template.tf @@ -76,6 +76,17 @@ data "coder_parameter" "project_names" { order = 1 } +data "coder_parameter" "share_projects" { + name = "share_projects" + display_name = "Public Sharing" + description = "Make every listed DDEV project's site public (anyone with the link, no Coder sign-in) so you can share work-in-progress outside Coder. Mailpit, xhgui, and Adminer always stay private. Applies to all projects listed above. Takes effect the next time this workspace restarts." + type = "bool" + form_type = "switch" + default = "false" + mutable = true + order = 2 +} + data "coder_parameter" "vscode_extensions" { name = "vscode_extensions" display_name = "VS Code Extensions" @@ -111,6 +122,11 @@ locals { ? [for s in split(",", local._project_names_raw) : trimspace(s) if trimspace(s) != ""] : [data.coder_workspace.me.name] ) + + # mock_data in tftest returns "[]" for all parameters regardless of declared + # type; try() catches tobool("[]") failing and falls back to private/"owner". + share_projects_public = try(tobool(data.coder_parameter.share_projects.value), false) + project_share_level = local.share_projects_public ? "public" : "owner" } variable "workspace_image_registry" { @@ -420,6 +436,8 @@ module "claude_remote_control" { # One coder_app per project name. All route to ddev-router on port 8080. # ddev-router dispatches by Host header: {slug}--{workspace}--{owner}.{domain} # The DDEV project name must equal the slug for coder-routes to build the correct rule. +# Sharing is controlled by data.coder_parameter.share_projects (Public Sharing +# switch above) and applies to every listed project's primary site alike. resource "coder_app" "ddev_web" { for_each = toset(local.project_names) agent_id = coder_agent.main.id @@ -429,7 +447,7 @@ resource "coder_app" "ddev_web" { url = "http://localhost:8080" icon = "https://raw.githubusercontent.com/ddev/ddev/main/docs/content/developers/logos/SVG/Logo.svg" subdomain = true - share = "owner" + share = local.project_share_level healthcheck { url = "http://localhost:8080" @@ -571,6 +589,10 @@ resource "coder_metadata" "workspace_info" { key = "ddev_projects" value = join(", ", local.project_names) } + item { + key = "sharing" + value = local.share_projects_public ? "Public (anyone with the link)" : "Private (owner only)" + } item { key = "cpu" value = "${var.cpu} vCPU (soft limit)" diff --git a/freeform/tests/validate.tftest.hcl b/freeform/tests/validate.tftest.hcl index f0c84ab..b3873e8 100644 --- a/freeform/tests/validate.tftest.hcl +++ b/freeform/tests/validate.tftest.hcl @@ -82,6 +82,46 @@ run "two_projects_with_spaces" { } } +run "sharing_disabled_by_default" { + command = plan + assert { + condition = coder_app.ddev_web["test-workspace"].share == "owner" + error_message = "coder_app.ddev_web must default to share=owner" + } +} + +run "sharing_enabled_for_all_projects" { + command = plan + override_data { + target = data.coder_parameter.project_names + values = { + value = "drupal,wordpress" + } + } + override_data { + target = data.coder_parameter.share_projects + values = { + value = "true" + } + } + assert { + condition = coder_app.ddev_web["drupal"].share == "public" + error_message = "coder_app.ddev_web[\"drupal\"] must be share=public when share_projects=true" + } + assert { + condition = coder_app.ddev_web["wordpress"].share == "public" + error_message = "coder_app.ddev_web[\"wordpress\"] must be share=public when share_projects=true" + } + assert { + condition = coder_app.mailpit["drupal"].share == "owner" + error_message = "coder_app.mailpit must stay share=owner even when share_projects=true" + } + assert { + condition = coder_app.xhgui["drupal"].share == "owner" + error_message = "coder_app.xhgui must stay share=owner even when share_projects=true" + } +} + run "adminer_off_by_default" { command = plan assert {