From d027a3f51b2fba8c02f0c7330ccee4e3847f7236 Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:02:35 +0500 Subject: [PATCH 1/7] chore: add rdp icon (cherry-pick #18736) (#18737) Cherry-picked chore: add rdp icon (#18736) Co-authored-by: Atif Ali --- site/src/theme/icons.json | 1 + site/static/icon/rdp.svg | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 site/static/icon/rdp.svg diff --git a/site/src/theme/icons.json b/site/src/theme/icons.json index a8aeb780b6b51..e2a7377383eca 100644 --- a/site/src/theme/icons.json +++ b/site/src/theme/icons.json @@ -89,6 +89,7 @@ "pycharm.svg", "python.svg", "pytorch.svg", + "rdp.svg", "rider.svg", "rockylinux.svg", "rstudio.svg", diff --git a/site/static/icon/rdp.svg b/site/static/icon/rdp.svg new file mode 100644 index 0000000000000..a67223263ac1d --- /dev/null +++ b/site/static/icon/rdp.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5096582ddac8b10a352e7d30e749f5a9b6b563a8 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 15 Jul 2025 18:23:07 +1000 Subject: [PATCH 2/7] cherry: feat: sign coder binaries with the release key using GPG (#18774) (#18868) (cherry picked from commit dc0919da332f27364969b9d5be63d8045ca0d993) Co-authored-by: Jakub Domeracki --- .github/workflows/ci.yaml | 2 ++ .github/workflows/release.yaml | 2 ++ Makefile | 4 +++ scripts/build_go.sh | 13 ++++++++ scripts/release/publish.sh | 21 ++---------- scripts/sign_with_gpg.sh | 59 ++++++++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 19 deletions(-) create mode 100755 scripts/sign_with_gpg.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b0a27b2db4de7..7a0bdbd435f98 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1259,6 +1259,8 @@ jobs: # do (see above). CODER_SIGN_WINDOWS: "1" CODER_WINDOWS_RESOURCES: "1" + CODER_SIGN_GPG: "1" + CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }} EV_KEY: ${{ secrets.EV_KEY }} EV_KEYSTORE: ${{ secrets.EV_KEYSTORE }} EV_TSA_URL: ${{ secrets.EV_TSA_URL }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 69043dc833362..71567cd540502 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -323,6 +323,8 @@ jobs: env: CODER_SIGN_WINDOWS: "1" CODER_SIGN_DARWIN: "1" + CODER_SIGN_GPG: "1" + CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }} CODER_WINDOWS_RESOURCES: "1" AC_CERTIFICATE_FILE: /tmp/apple_cert.p12 AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt diff --git a/Makefile b/Makefile index b6e69ac28f223..70ad1f16f0042 100644 --- a/Makefile +++ b/Makefile @@ -252,6 +252,10 @@ $(CODER_ALL_BINARIES): go.mod go.sum \ fi cp "$@" "./site/out/bin/coder-$$os-$$arch$$dot_ext" + + if [[ "$${CODER_SIGN_GPG:-0}" == "1" ]]; then + cp "$@.asc" "./site/out/bin/coder-$$os-$$arch$$dot_ext.asc" + fi fi # This task builds Coder Desktop dylibs diff --git a/scripts/build_go.sh b/scripts/build_go.sh index 97d9431beb544..b3b074b183f91 100755 --- a/scripts/build_go.sh +++ b/scripts/build_go.sh @@ -20,6 +20,9 @@ # binary will be signed using ./sign_darwin.sh. Read that file for more details # on the requirements. # +# If the --sign-gpg parameter is specified, the output binary will be signed using ./sign_with_gpg.sh. +# Read that file for more details on the requirements. +# # If the --agpl parameter is specified, builds only the AGPL-licensed code (no # Coder enterprise features). # @@ -41,6 +44,7 @@ slim="${CODER_SLIM_BUILD:-0}" agpl="${CODER_BUILD_AGPL:-0}" sign_darwin="${CODER_SIGN_DARWIN:-0}" sign_windows="${CODER_SIGN_WINDOWS:-0}" +sign_gpg="${CODER_SIGN_GPG:-0}" boringcrypto=${CODER_BUILD_BORINGCRYPTO:-0} dylib=0 windows_resources="${CODER_WINDOWS_RESOURCES:-0}" @@ -85,6 +89,10 @@ while true; do sign_windows=1 shift ;; + --sign-gpg) + sign_gpg=1 + shift + ;; --boringcrypto) boringcrypto=1 shift @@ -319,4 +327,9 @@ if [[ "$sign_windows" == 1 ]] && [[ "$os" == "windows" ]]; then execrelative ./sign_windows.sh "$output_path" 1>&2 fi +# Platform agnostic signing +if [[ "$sign_gpg" == 1 ]]; then + execrelative ./sign_with_gpg.sh "$output_path" 1>&2 +fi + echo "$output_path" diff --git a/scripts/release/publish.sh b/scripts/release/publish.sh index df28d46ad2710..5ffd40aeb65cb 100755 --- a/scripts/release/publish.sh +++ b/scripts/release/publish.sh @@ -129,26 +129,9 @@ if [[ "$dry_run" == 0 ]] && [[ "${CODER_GPG_RELEASE_KEY_BASE64:-}" != "" ]]; the log "--- Signing checksums file" log - # Import the GPG key. - old_gnupg_home="${GNUPGHOME:-}" - gnupg_home_temp="$(mktemp -d)" - export GNUPGHOME="$gnupg_home_temp" - echo "$CODER_GPG_RELEASE_KEY_BASE64" | base64 -d | gpg --import 1>&2 - - # Sign the checksums file. This generates a file in the same directory and - # with the same name as the checksums file but ending in ".asc". - # - # We pipe `true` into `gpg` so that it never tries to be interactive (i.e. - # ask for a passphrase). The key we import above is not password protected. - true | gpg --detach-sign --armor "${temp_dir}/${checksum_file}" 1>&2 - - rm -rf "$gnupg_home_temp" - unset GNUPGHOME - if [[ "$old_gnupg_home" != "" ]]; then - export GNUPGHOME="$old_gnupg_home" - fi - + execrelative ../sign_with_gpg.sh "${temp_dir}/${checksum_file}" signed_checksum_path="${temp_dir}/${checksum_file}.asc" + if [[ ! -e "$signed_checksum_path" ]]; then log "Signed checksum file not found: ${signed_checksum_path}" log diff --git a/scripts/sign_with_gpg.sh b/scripts/sign_with_gpg.sh new file mode 100755 index 0000000000000..fb75df5ca1bb9 --- /dev/null +++ b/scripts/sign_with_gpg.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +# This script signs a given binary using GPG. +# It expects the binary to be signed as the first argument. +# +# Usage: ./sign_with_gpg.sh path/to/binary +# +# On success, the input file will be signed using the GPG key and the signature output file will moved to /site/out/bin/ (happens in the Makefile) +# +# Depends on the GPG utility. Requires the following environment variables to be set: +# - $CODER_GPG_RELEASE_KEY_BASE64: The base64 encoded private key to use. + +set -euo pipefail +# shellcheck source=scripts/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +requiredenvs CODER_GPG_RELEASE_KEY_BASE64 + +FILE_TO_SIGN="$1" + +if [[ -z "$FILE_TO_SIGN" ]]; then + error "Usage: $0 " +fi + +if [[ ! -f "$FILE_TO_SIGN" ]]; then + error "File not found: $FILE_TO_SIGN" +fi + +# Import the GPG key. +old_gnupg_home="${GNUPGHOME:-}" +gnupg_home_temp="$(mktemp -d)" +export GNUPGHOME="$gnupg_home_temp" + +# Ensure GPG uses the temporary directory +echo "$CODER_GPG_RELEASE_KEY_BASE64" | base64 -d | gpg --homedir "$gnupg_home_temp" --import 1>&2 + +# Sign the binary. This generates a file in the same directory and +# with the same name as the binary but ending in ".asc". +# +# We pipe `true` into `gpg` so that it never tries to be interactive (i.e. +# ask for a passphrase). The key we import above is not password protected. +true | gpg --homedir "$gnupg_home_temp" --detach-sign --armor "$FILE_TO_SIGN" 1>&2 + +# Verify the signature and capture the exit status +gpg --homedir "$gnupg_home_temp" --verify "${FILE_TO_SIGN}.asc" "$FILE_TO_SIGN" 1>&2 +verification_result=$? + +# Clean up the temporary GPG home +rm -rf "$gnupg_home_temp" +unset GNUPGHOME +if [[ "$old_gnupg_home" != "" ]]; then + export GNUPGHOME="$old_gnupg_home" +fi + +if [[ $verification_result -eq 0 ]]; then + echo "${FILE_TO_SIGN}.asc" +else + error "Signature verification failed!" +fi From 3c602b0e2987d8a13da9a57059f0dd66dd555a1a Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:41:58 +0500 Subject: [PATCH 3/7] chore: add kiro icon (cherry-pick #18881) (#18885) Co-authored-by: Atif Ali --- site/src/theme/icons.json | 1 + site/static/icon/kiro.svg | 1 + 2 files changed, 2 insertions(+) create mode 100644 site/static/icon/kiro.svg diff --git a/site/src/theme/icons.json b/site/src/theme/icons.json index e2a7377383eca..d687b1f5991fc 100644 --- a/site/src/theme/icons.json +++ b/site/src/theme/icons.json @@ -68,6 +68,7 @@ "k8s.png", "kasmvnc.svg", "keycloak.svg", + "kiro.svg", "kotlin.svg", "lakefs.svg", "lxc.svg", diff --git a/site/static/icon/kiro.svg b/site/static/icon/kiro.svg new file mode 100644 index 0000000000000..13268494cba07 --- /dev/null +++ b/site/static/icon/kiro.svg @@ -0,0 +1 @@ + From 33885afbff871710fc3f173f721f0a4698c4629a Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:48:58 +0500 Subject: [PATCH 4/7] chore: add kiro: protocol to external app whitelist (cherry-pick #18884) (#18886) Co-authored-by: blink-so[bot] Co-authored-by: matifali <10648092+matifali@users.noreply.github.com> Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> --- site/src/modules/apps/apps.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/modules/apps/apps.ts b/site/src/modules/apps/apps.ts index d154b632dc1ca..2576422ea46ab 100644 --- a/site/src/modules/apps/apps.ts +++ b/site/src/modules/apps/apps.ts @@ -22,6 +22,7 @@ const ALLOWED_EXTERNAL_APP_PROTOCOLS = [ "cursor:", "jetbrains-gateway:", "jetbrains:", + "kiro:", ]; type GetVSCodeHrefParams = { From 51e60b74d1fb4c73c463d382b3e3c7c4c06bf5db Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:47:05 -0500 Subject: [PATCH 5/7] fix: exclude prebuilt workspaces from lifecycle executor (cherry-pick #18762) (#18858) Co-authored-by: Susana Ferreira Co-authored-by: Stephen Kirby <58410745+stirby@users.noreply.github.com> Fixes: https://github.com/coder/coder/issues/18740 --- coderd/apidoc/docs.go | 6 +- coderd/apidoc/swagger.json | 5 +- coderd/autobuild/lifecycle_executor.go | 2 + coderd/autobuild/lifecycle_executor_test.go | 349 ++++++++ coderd/database/dbgen/dbgen.go | 22 + coderd/database/queries.sql.go | 7 +- coderd/database/queries/workspaces.sql | 7 +- coderd/schedule/autostart.go | 2 + codersdk/workspacebuilds.go | 9 +- .../prebuilt-workspaces.md | 11 +- docs/reference/api/schemas.md | 1 + enterprise/coderd/workspaces_test.go | 794 ++++++++++++++++++ site/src/api/typesGenerated.ts | 3 +- site/src/utils/workspace.tsx | 4 +- 14 files changed, 1204 insertions(+), 18 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 522ba671a9a63..264c038f096b2 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11073,12 +11073,14 @@ const docTemplate = `{ "enum": [ "initiator", "autostart", - "autostop" + "autostop", + "dormancy" ], "x-enum-varnames": [ "BuildReasonInitiator", "BuildReasonAutostart", - "BuildReasonAutostop" + "BuildReasonAutostop", + "BuildReasonDormancy" ] }, "codersdk.ChangePasswordWithOneTimePasscodeRequest": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index abcae550a4ec5..147e107bf6d9c 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9858,11 +9858,12 @@ }, "codersdk.BuildReason": { "type": "string", - "enum": ["initiator", "autostart", "autostop"], + "enum": ["initiator", "autostart", "autostop", "dormancy"], "x-enum-varnames": [ "BuildReasonInitiator", "BuildReasonAutostart", - "BuildReasonAutostop" + "BuildReasonAutostop", + "BuildReasonDormancy" ] }, "codersdk.ChangePasswordWithOneTimePasscodeRequest": { diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index 1846b1ea18284..d49bf831515d0 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -520,6 +520,8 @@ func isEligibleForAutostart(user database.User, ws database.Workspace, build dat return false } + // Get the next allowed autostart time after the build's creation time, + // based on the workspace's schedule and the template's allowed days. nextTransition, err := schedule.NextAllowedAutostart(build.CreatedAt, ws.AutostartSchedule.String, templateSchedule) if err != nil { return false diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go index 3bca6856534fa..0229a907cbb2e 100644 --- a/coderd/autobuild/lifecycle_executor_test.go +++ b/coderd/autobuild/lifecycle_executor_test.go @@ -2,9 +2,16 @@ package autobuild_test import ( "context" + "database/sql" + "errors" "testing" "time" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/quartz" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1183,6 +1190,348 @@ func TestNotifications(t *testing.T) { }) } +// TestExecutorPrebuilds verifies AGPL behavior for prebuilt workspaces. +// It ensures that workspace schedules do not trigger while the workspace +// is still in a prebuilt state. Scheduling behavior only applies after the +// workspace has been claimed and becomes a regular user workspace. +// For enterprise-related functionality, see enterprise/coderd/workspaces_test.go. +func TestExecutorPrebuilds(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("this test requires postgres") + } + + // Prebuild workspaces should not be autostopped when the deadline is reached. + // After being claimed, the workspace should stop at the deadline. + t.Run("OnlyStopsAfterClaimed", func(t *testing.T) { + t.Parallel() + + // Setup + ctx := testutil.Context(t, testutil.WaitShort) + clock := quartz.NewMock(t) + db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + var ( + tickCh = make(chan time.Time) + statsCh = make(chan autobuild.Stats) + client = coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: pb, + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + }) + ) + + // Setup user, template and template version + owner := coderdtest.CreateFirstUser(t, client) + _, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember()) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + + // Database setup of a preset with a prebuild instance + preset := setupTestDBPreset(t, db, version.ID, int32(1)) + + // Given: a running prebuilt workspace with a deadline and ready to be claimed + dbPrebuild := setupTestDBPrebuiltWorkspace( + ctx, t, clock, db, pb, + owner.OrganizationID, + template.ID, + version.ID, + preset.ID, + ) + prebuild := coderdtest.MustWorkspace(t, client, dbPrebuild.ID) + require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition) + require.NotZero(t, prebuild.LatestBuild.Deadline) + + // When: the autobuild executor ticks *after* the deadline: + go func() { + tickCh <- prebuild.LatestBuild.Deadline.Time.Add(time.Minute) + }() + + // Then: the prebuilt workspace should remain in a start transition + prebuildStats := <-statsCh + require.Len(t, prebuildStats.Errors, 0) + require.Len(t, prebuildStats.Transitions, 0) + require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition) + prebuild = coderdtest.MustWorkspace(t, client, prebuild.ID) + require.Equal(t, codersdk.BuildReasonInitiator, prebuild.LatestBuild.Reason) + + // Given: a user claims the prebuilt workspace + dbWorkspace := dbgen.ClaimPrebuild(t, db, user.ID, "claimedWorkspace-autostop", preset.ID) + workspace := coderdtest.MustWorkspace(t, client, dbWorkspace.ID) + + // When: the autobuild executor ticks *after* the deadline: + go func() { + tickCh <- workspace.LatestBuild.Deadline.Time.Add(time.Minute) + close(tickCh) + }() + + // Then: the workspace should be stopped + workspaceStats := <-statsCh + require.Len(t, workspaceStats.Errors, 0) + require.Len(t, workspaceStats.Transitions, 1) + require.Contains(t, workspaceStats.Transitions, workspace.ID) + require.Equal(t, database.WorkspaceTransitionStop, workspaceStats.Transitions[workspace.ID]) + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + require.Equal(t, codersdk.BuildReasonAutostop, workspace.LatestBuild.Reason) + }) + + // Prebuild workspaces should not be autostarted when the autostart scheduled is reached. + // After being claimed, the workspace should autostart at the schedule. + t.Run("OnlyStartsAfterClaimed", func(t *testing.T) { + t.Parallel() + + // Setup + ctx := testutil.Context(t, testutil.WaitShort) + clock := quartz.NewMock(t) + db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + var ( + tickCh = make(chan time.Time) + statsCh = make(chan autobuild.Stats) + client = coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: pb, + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + }) + ) + + // Setup user, template and template version + owner := coderdtest.CreateFirstUser(t, client) + _, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember()) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + + // Database setup of a preset with a prebuild instance + preset := setupTestDBPreset(t, db, version.ID, int32(1)) + + // Given: prebuilt workspace is stopped and set to autostart daily at midnight + sched := mustSchedule(t, "CRON_TZ=UTC 0 0 * * *") + autostartSched := sql.NullString{ + String: sched.String(), + Valid: true, + } + dbPrebuild := setupTestDBPrebuiltWorkspace( + ctx, t, clock, db, pb, + owner.OrganizationID, + template.ID, + version.ID, + preset.ID, + WithAutostartSchedule(autostartSched), + WithIsStopped(true), + ) + prebuild := coderdtest.MustWorkspace(t, client, dbPrebuild.ID) + require.Equal(t, codersdk.WorkspaceTransitionStop, prebuild.LatestBuild.Transition) + require.NotNil(t, prebuild.AutostartSchedule) + + // Tick at the next scheduled time after the prebuild’s LatestBuild.CreatedAt, + // since the next allowed autostart is calculated starting from that point. + // When: the autobuild executor ticks after the scheduled time + go func() { + tickCh <- sched.Next(prebuild.LatestBuild.CreatedAt).Add(time.Minute) + }() + + // Then: the prebuilt workspace should remain in a stop transition + prebuildStats := <-statsCh + require.Len(t, prebuildStats.Errors, 0) + require.Len(t, prebuildStats.Transitions, 0) + require.Equal(t, codersdk.WorkspaceTransitionStop, prebuild.LatestBuild.Transition) + prebuild = coderdtest.MustWorkspace(t, client, prebuild.ID) + require.Equal(t, codersdk.BuildReasonInitiator, prebuild.LatestBuild.Reason) + + // Given: prebuilt workspace is in a start status + setupTestDBWorkspaceBuild( + ctx, t, clock, db, pb, + owner.OrganizationID, + prebuild.ID, + version.ID, + preset.ID, + database.WorkspaceTransitionStart) + + // Given: a user claims the prebuilt workspace + dbWorkspace := dbgen.ClaimPrebuild(t, db, user.ID, "claimedWorkspace-autostart", preset.ID) + workspace := coderdtest.MustWorkspace(t, client, dbWorkspace.ID) + + // Given: the prebuilt workspace goes to a stop status + setupTestDBWorkspaceBuild( + ctx, t, clock, db, pb, + owner.OrganizationID, + prebuild.ID, + version.ID, + preset.ID, + database.WorkspaceTransitionStop) + + // Tick at the next scheduled time after the prebuild’s LatestBuild.CreatedAt, + // since the next allowed autostart is calculated starting from that point. + // When: the autobuild executor ticks after the scheduled time + go func() { + tickCh <- sched.Next(workspace.LatestBuild.CreatedAt).Add(time.Minute) + close(tickCh) + }() + + // Then: the workspace should eventually be started + workspaceStats := <-statsCh + require.Len(t, workspaceStats.Errors, 0) + require.Len(t, workspaceStats.Transitions, 1) + require.Contains(t, workspaceStats.Transitions, workspace.ID) + require.Equal(t, database.WorkspaceTransitionStart, workspaceStats.Transitions[workspace.ID]) + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + require.Equal(t, codersdk.BuildReasonAutostart, workspace.LatestBuild.Reason) + }) +} + +func setupTestDBPreset( + t *testing.T, + db database.Store, + templateVersionID uuid.UUID, + desiredInstances int32, +) database.TemplateVersionPreset { + t.Helper() + + preset := dbgen.Preset(t, db, database.InsertPresetParams{ + TemplateVersionID: templateVersionID, + Name: "preset-test", + DesiredInstances: sql.NullInt32{ + Valid: true, + Int32: desiredInstances, + }, + }) + dbgen.PresetParameter(t, db, database.InsertPresetParametersParams{ + TemplateVersionPresetID: preset.ID, + Names: []string{"test-name"}, + Values: []string{"test-value"}, + }) + + return preset +} + +type SetupPrebuiltOptions struct { + AutostartSchedule sql.NullString + IsStopped bool +} + +func WithAutostartSchedule(sched sql.NullString) func(*SetupPrebuiltOptions) { + return func(o *SetupPrebuiltOptions) { + o.AutostartSchedule = sched + } +} + +func WithIsStopped(isStopped bool) func(*SetupPrebuiltOptions) { + return func(o *SetupPrebuiltOptions) { + o.IsStopped = isStopped + } +} + +func setupTestDBWorkspaceBuild( + ctx context.Context, + t *testing.T, + clock quartz.Clock, + db database.Store, + ps pubsub.Pubsub, + orgID uuid.UUID, + workspaceID uuid.UUID, + templateVersionID uuid.UUID, + presetID uuid.UUID, + transition database.WorkspaceTransition, +) (database.ProvisionerJob, database.WorkspaceBuild) { + t.Helper() + + var buildNumber int32 = 1 + latestWorkspaceBuild, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID) + if !errors.Is(err, sql.ErrNoRows) { + buildNumber = latestWorkspaceBuild.BuildNumber + 1 + } + + job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + InitiatorID: database.PrebuildsSystemUserID, + CreatedAt: clock.Now().Add(-time.Hour * 2), + StartedAt: sql.NullTime{Time: clock.Now().Add(-time.Hour * 2), Valid: true}, + CompletedAt: sql.NullTime{Time: clock.Now().Add(-time.Hour), Valid: true}, + OrganizationID: orgID, + JobStatus: database.ProvisionerJobStatusSucceeded, + }) + workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspaceID, + InitiatorID: database.PrebuildsSystemUserID, + TemplateVersionID: templateVersionID, + BuildNumber: buildNumber, + JobID: job.ID, + TemplateVersionPresetID: uuid.NullUUID{UUID: presetID, Valid: true}, + Transition: transition, + CreatedAt: clock.Now(), + }) + dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{ + { + WorkspaceBuildID: workspaceBuild.ID, + Name: "test", + Value: "test", + }, + }) + + workspaceResource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: job.ID, + Transition: database.WorkspaceTransitionStart, + Type: "compute", + Name: "main", + }) + + // Workspaces are eligible to be claimed once their agent is marked "ready" + dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + Name: "test", + ResourceID: workspaceResource.ID, + Architecture: "i386", + OperatingSystem: "linux", + LifecycleState: database.WorkspaceAgentLifecycleStateReady, + StartedAt: sql.NullTime{Time: time.Now().Add(time.Hour), Valid: true}, + ReadyAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true}, + APIKeyScope: database.AgentKeyScopeEnumAll, + }) + + return job, workspaceBuild +} + +func setupTestDBPrebuiltWorkspace( + ctx context.Context, + t *testing.T, + clock quartz.Clock, + db database.Store, + ps pubsub.Pubsub, + orgID uuid.UUID, + templateID uuid.UUID, + templateVersionID uuid.UUID, + presetID uuid.UUID, + opts ...func(*SetupPrebuiltOptions), +) database.WorkspaceTable { + t.Helper() + + // Optional parameters + options := &SetupPrebuiltOptions{} + for _, opt := range opts { + opt(options) + } + + buildTransition := database.WorkspaceTransitionStart + if options.IsStopped { + buildTransition = database.WorkspaceTransitionStop + } + + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + TemplateID: templateID, + OrganizationID: orgID, + OwnerID: database.PrebuildsSystemUserID, + Deleted: false, + CreatedAt: time.Now().Add(-time.Hour * 2), + AutostartSchedule: options.AutostartSchedule, + }) + setupTestDBWorkspaceBuild(ctx, t, clock, db, ps, orgID, workspace.ID, templateVersionID, presetID, buildTransition) + + return workspace +} + func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace { t.Helper() user := coderdtest.CreateFirstUser(t, client) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index fb3adc9e6f057..fd842b73348c5 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -203,6 +203,17 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen require.NoError(t, err, "update workspace agent first connected at") } + // If the lifecycle state is "ready", update the agent with the corresponding timestamps + if orig.LifecycleState == database.WorkspaceAgentLifecycleStateReady && orig.StartedAt.Valid && orig.ReadyAt.Valid { + err := db.UpdateWorkspaceAgentLifecycleStateByID(genCtx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{ + ID: agt.ID, + LifecycleState: orig.LifecycleState, + StartedAt: orig.StartedAt, + ReadyAt: orig.ReadyAt, + }) + require.NoError(t, err, "update workspace agent lifecycle state") + } + if orig.ParentID.UUID == uuid.Nil { // Add a test antagonist. For every agent we add a deleted sub agent // to discover cases where deletion should be handled. @@ -1326,6 +1337,17 @@ func PresetParameter(t testing.TB, db database.Store, seed database.InsertPreset return parameters } +func ClaimPrebuild(t testing.TB, db database.Store, newUserID uuid.UUID, newName string, presetID uuid.UUID) database.ClaimPrebuiltWorkspaceRow { + claimedWorkspace, err := db.ClaimPrebuiltWorkspace(genCtx, database.ClaimPrebuiltWorkspaceParams{ + NewUserID: newUserID, + NewName: newName, + PresetID: presetID, + }) + require.NoError(t, err, "claim prebuilt workspace") + + return claimedWorkspace +} + func provisionerJobTiming(t testing.TB, db database.Store, seed database.ProvisionerJobTiming) database.ProvisionerJobTiming { timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{ JobID: takeFirst(seed.JobID, uuid.New()), diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 733b42db7a461..396a044a3484b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -19358,7 +19358,12 @@ WHERE provisioner_jobs.completed_at IS NOT NULL AND ($1 :: timestamptz) - provisioner_jobs.completed_at > (INTERVAL '1 millisecond' * (templates.failure_ttl / 1000000)) ) - ) AND workspaces.deleted = 'false' + ) + AND workspaces.deleted = 'false' + -- Prebuilt workspaces (identified by having the prebuilds system user as owner_id) + -- should not be considered by the lifecycle executor, as they are handled by the + -- prebuilds reconciliation loop. + AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID ` type GetWorkspacesEligibleForTransitionRow struct { diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 25e4d4f97a46b..f166d16f742cd 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -758,7 +758,12 @@ WHERE provisioner_jobs.completed_at IS NOT NULL AND (@now :: timestamptz) - provisioner_jobs.completed_at > (INTERVAL '1 millisecond' * (templates.failure_ttl / 1000000)) ) - ) AND workspaces.deleted = 'false'; + ) + AND workspaces.deleted = 'false' + -- Prebuilt workspaces (identified by having the prebuilds system user as owner_id) + -- should not be considered by the lifecycle executor, as they are handled by the + -- prebuilds reconciliation loop. + AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID; -- name: UpdateWorkspaceDormantDeletingAt :one UPDATE diff --git a/coderd/schedule/autostart.go b/coderd/schedule/autostart.go index 0a7f583e4f9b2..538d3dd346fcd 100644 --- a/coderd/schedule/autostart.go +++ b/coderd/schedule/autostart.go @@ -33,6 +33,8 @@ func NextAutostart(at time.Time, wsSchedule string, templateSchedule TemplateSch return zonedTransition, allowed } +// NextAllowedAutostart returns the next valid autostart time after 'at', based on the workspace's +// cron schedule and the template's allowed days. It searches up to 7 days ahead to find a match. func NextAllowedAutostart(at time.Time, wsSchedule string, templateSchedule TemplateScheduleOptions) (time.Time, error) { next := at diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 328b8bc26566f..db736d431fd02 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -37,15 +37,18 @@ const ( type BuildReason string const ( - // "initiator" is used when a workspace build is triggered by a user. + // BuildReasonInitiator "initiator" is used when a workspace build is triggered by a user. // Combined with the initiator id/username, it indicates which user initiated the build. BuildReasonInitiator BuildReason = "initiator" - // "autostart" is used when a build to start a workspace is triggered by Autostart. + // BuildReasonAutostart "autostart" is used when a build to start a workspace is triggered by Autostart. // The initiator id/username in this case is the workspace owner and can be ignored. BuildReasonAutostart BuildReason = "autostart" - // "autostop" is used when a build to stop a workspace is triggered by Autostop. + // BuildReasonAutostop "autostop" is used when a build to stop a workspace is triggered by Autostop. // The initiator id/username in this case is the workspace owner and can be ignored. BuildReasonAutostop BuildReason = "autostop" + // BuildReasonDormancy "dormancy" is used when a build to stop a workspace is triggered due to inactivity (dormancy). + // The initiator id/username in this case is the workspace owner and can be ignored. + BuildReasonDormancy BuildReason = "dormancy" ) // WorkspaceBuild is an at-point representation of a workspace state. diff --git a/docs/admin/templates/extending-templates/prebuilt-workspaces.md b/docs/admin/templates/extending-templates/prebuilt-workspaces.md index 2c5e73ad289b4..8e61687ce0f01 100644 --- a/docs/admin/templates/extending-templates/prebuilt-workspaces.md +++ b/docs/admin/templates/extending-templates/prebuilt-workspaces.md @@ -2,13 +2,10 @@ > [!WARNING] > Prebuilds Compatibility Limitations: -> Prebuilt workspaces are currently not compatible with configurations that have Workspace schedule (autostart/autostop), or Dormancy enabled. -> If these features are configured, prebuilt workspaces may fail to run correctly. +> Prebuilt workspaces currently do not work reliably with [DevContainers feature](../managing-templates/devcontainers/index.md). +> If your project relies on DevContainer configuration, we recommend disabling prebuilds or carefully testing behavior before enabling them. > -> In addition, prebuilds currently do not work reliably with [DevContainers feature](../managing-templates/devcontainers/index.md). -> If your project relies on DevContainer configuration, we recommend disabling prebuilds or carefully testing behavior before enabling them in production. -> -> We’re actively working to improve compatibility, but for now, please avoid using prebuilds with these features to ensure stability and expected behavior. +> We’re actively working to improve compatibility, but for now, please avoid using prebuilds with this feature to ensure stability and expected behavior. Prebuilt workspaces allow template administrators to improve the developer experience by reducing workspace creation time with an automatically maintained pool of ready-to-use workspaces for specific parameter presets. @@ -26,7 +23,7 @@ Prebuilt workspaces are: ## Relationship to workspace presets -Prebuilt workspaces are tightly integrated with [workspace presets](./parameters.md#workspace-presets-beta): +Prebuilt workspaces are tightly integrated with [workspace presets](./parameters.md#workspace-presets): 1. Each prebuilt workspace is associated with a specific template preset. 1. The preset must define all required parameters needed to build the workspace. diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 79c6f817bc776..ef55f884aa597 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1049,6 +1049,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `initiator` | | `autostart` | | `autostop` | +| `dormancy` | ## codersdk.ChangePasswordWithOneTimePasscodeRequest diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 3bed052702637..5d417f0dfb829 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -10,10 +10,17 @@ import ( "os" "os/exec" "path/filepath" + "strings" "sync/atomic" "testing" "time" + "github.com/prometheus/client_golang/prometheus" + + "github.com/coder/coder/v2/coderd/files" + agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/enterprise/coderd/prebuilds" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1713,6 +1720,793 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) { }) } +func TestExecutorPrebuilds(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("this test requires postgres") + } + + getRunningPrebuilds := func( + t *testing.T, + ctx context.Context, + db database.Store, + prebuildInstances int, + ) []database.GetRunningPrebuiltWorkspacesRow { + t.Helper() + + var runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow + testutil.Eventually(ctx, t, func(context.Context) bool { + rows, err := db.GetRunningPrebuiltWorkspaces(ctx) + if err != nil { + return false + } + + for _, row := range rows { + runningPrebuilds = append(runningPrebuilds, row) + + agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, row.ID) + if err != nil { + return false + } + + for _, agent := range agents { + err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{ + ID: agent.ID, + LifecycleState: database.WorkspaceAgentLifecycleStateReady, + StartedAt: sql.NullTime{Time: time.Now().Add(time.Hour), Valid: true}, + ReadyAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true}, + }) + if err != nil { + return false + } + } + } + + t.Logf("found %d running prebuilds so far, want %d", len(runningPrebuilds), prebuildInstances) + return len(runningPrebuilds) == prebuildInstances + }, testutil.IntervalSlow, "prebuilds not running") + + return runningPrebuilds + } + + runReconciliationLoop := func( + t *testing.T, + ctx context.Context, + db database.Store, + reconciler *prebuilds.StoreReconciler, + presets []codersdk.Preset, + ) { + t.Helper() + + state, err := reconciler.SnapshotState(ctx, db) + require.NoError(t, err) + ps, err := state.FilterByPreset(presets[0].ID) + require.NoError(t, err) + require.NotNil(t, ps) + actions, err := reconciler.CalculateActions(ctx, *ps) + require.NoError(t, err) + require.NotNil(t, actions) + require.NoError(t, reconciler.ReconcilePreset(ctx, *ps)) + } + + claimPrebuild := func( + t *testing.T, + ctx context.Context, + client *codersdk.Client, + userClient *codersdk.Client, + username string, + version codersdk.TemplateVersion, + presetID uuid.UUID, + ) codersdk.Workspace { + t.Helper() + + workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-") + userWorkspace, err := userClient.CreateUserWorkspace(ctx, username, codersdk.CreateWorkspaceRequest{ + TemplateVersionID: version.ID, + Name: workspaceName, + TemplateVersionPresetID: presetID, + }) + require.NoError(t, err) + build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, userWorkspace.LatestBuild.ID) + require.Equal(t, build.Job.Status, codersdk.ProvisionerJobSucceeded) + workspace := coderdtest.MustWorkspace(t, client, userWorkspace.ID) + assert.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition) + + return workspace + } + + // Prebuilt workspaces should not be autostopped based on the default TTL. + // This test ensures that DefaultTTLMillis is ignored while the workspace is in a prebuild state. + // Once the workspace is claimed, the default autostop timer should take effect. + t.Run("DefaultTTLOnlyTriggersAfterClaim", func(t *testing.T) { + t.Parallel() + + // Set the clock to Monday, January 1st, 2024 at 8:00 AM UTC to keep the test deterministic + clock := quartz.NewMock(t) + clock.Set(time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC)) + + // Setup + ctx := testutil.Context(t, testutil.WaitSuperLong) + db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + logger := testutil.Logger(t) + tickCh := make(chan time.Time) + statsCh := make(chan autobuild.Stats) + notificationsNoop := notifications.NewNoopEnqueuer() + client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + Database: db, + Pubsub: pb, + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + Clock: clock, + TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore( + agplUserQuietHoursScheduleStore(), + notificationsNoop, + logger, + clock, + ), + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1}, + }, + }) + + // Setup Prebuild reconciler + cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{}) + reconciler := prebuilds.NewStoreReconciler( + db, pb, cache, + codersdk.PrebuildsConfig{}, + logger, + clock, + prometheus.NewRegistry(), + notificationsNoop, + ) + var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(db) + api.AGPL.PrebuildsClaimer.Store(&claimer) + + // Setup user, template and template version with a preset with 1 prebuild instance + prebuildInstances := int32(1) + ttlTime := 2 * time.Hour + userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember()) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(prebuildInstances)) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { + // Set a template level TTL to trigger the autostop + // Template level TTL can only be set if autostop is disabled for users + ctr.AllowUserAutostop = ptr.Ref[bool](false) + ctr.DefaultTTLMillis = ptr.Ref[int64](ttlTime.Milliseconds()) + }) + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Len(t, presets, 1) + + // Given: Reconciliation loop runs and starts prebuilt workspace + runReconciliationLoop(t, ctx, db, reconciler, presets) + runningPrebuilds := getRunningPrebuilds(t, ctx, db, int(prebuildInstances)) + require.Len(t, runningPrebuilds, int(prebuildInstances)) + + // Given: a running prebuilt workspace with a deadline, ready to be claimed + prebuild := coderdtest.MustWorkspace(t, client, runningPrebuilds[0].ID) + require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition) + require.NotZero(t, prebuild.LatestBuild.Deadline) + + // When: the autobuild executor ticks *after* the deadline + next := prebuild.LatestBuild.Deadline.Time.Add(time.Minute) + clock.Set(next) + go func() { + tickCh <- next + }() + + // Then: the prebuilt workspace should remain in a start transition + prebuildStats := <-statsCh + require.Len(t, prebuildStats.Errors, 0) + require.Len(t, prebuildStats.Transitions, 0) + require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition) + prebuild = coderdtest.MustWorkspace(t, client, prebuild.ID) + require.Equal(t, codersdk.BuildReasonInitiator, prebuild.LatestBuild.Reason) + + // Given: a user claims the prebuilt workspace sometime later + clock.Set(clock.Now().Add(ttlTime)) + workspace := claimPrebuild(t, ctx, client, userClient, user.Username, version, presets[0].ID) + require.Equal(t, prebuild.ID, workspace.ID) + // Workspace deadline must be ttlTime from the time it is claimed + require.True(t, workspace.LatestBuild.Deadline.Time.Equal(clock.Now().Add(ttlTime))) + + // When: the autobuild executor ticks *after* the deadline + next = workspace.LatestBuild.Deadline.Time.Add(time.Minute) + clock.Set(next) + go func() { + tickCh <- next + close(tickCh) + }() + + // Then: the workspace should be stopped + workspaceStats := <-statsCh + require.Len(t, workspaceStats.Errors, 0) + require.Len(t, workspaceStats.Transitions, 1) + require.Contains(t, workspaceStats.Transitions, workspace.ID) + require.Equal(t, database.WorkspaceTransitionStop, workspaceStats.Transitions[workspace.ID]) + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + require.Equal(t, codersdk.BuildReasonAutostop, workspace.LatestBuild.Reason) + }) + + // Prebuild workspaces should not follow the autostop schedule. + // This test verifies that AutostopRequirement (autostop schedule) is ignored while the workspace is a prebuild. + // After being claimed, the workspace should be stopped according to the autostop schedule. + t.Run("AutostopScheduleOnlyTriggersAfterClaim", func(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + isClaimedBeforeDeadline bool + }{ + // If the prebuild is claimed before the scheduled deadline, + // the claimed workspace should inherit and respect that same deadline. + { + name: "ClaimedBeforeDeadline_UsesSameDeadline", + isClaimedBeforeDeadline: true, + }, + // If the prebuild is claimed after the scheduled deadline, + // the workspace should not stop immediately, but instead respect the next + // valid scheduled deadline (the next day). + { + name: "ClaimedAfterDeadline_SchedulesForNextDay", + isClaimedBeforeDeadline: false, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Set the clock to Monday, January 1st, 2024 at 8:00 AM UTC to keep the test deterministic + clock := quartz.NewMock(t) + clock.Set(time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC)) + + // Setup + ctx := testutil.Context(t, testutil.WaitSuperLong) + db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + logger := testutil.Logger(t) + tickCh := make(chan time.Time) + statsCh := make(chan autobuild.Stats) + notificationsNoop := notifications.NewNoopEnqueuer() + client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + Database: db, + Pubsub: pb, + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + Clock: clock, + TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore( + agplUserQuietHoursScheduleStore(), + notificationsNoop, + logger, + clock, + ), + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1}, + }, + }) + + // Setup Prebuild reconciler + cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{}) + reconciler := prebuilds.NewStoreReconciler( + db, pb, cache, + codersdk.PrebuildsConfig{}, + logger, + clock, + prometheus.NewRegistry(), + notificationsNoop, + ) + var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(db) + api.AGPL.PrebuildsClaimer.Store(&claimer) + + // Setup user, template and template version with a preset with 1 prebuild instance + prebuildInstances := int32(1) + userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember()) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(prebuildInstances)) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { + // Set a template level Autostop schedule to trigger the autostop daily + ctr.AutostopRequirement = ptr.Ref[codersdk.TemplateAutostopRequirement]( + codersdk.TemplateAutostopRequirement{ + DaysOfWeek: []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, + Weeks: 1, + }) + }) + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Len(t, presets, 1) + + // Given: Reconciliation loop runs and starts prebuilt workspace + runReconciliationLoop(t, ctx, db, reconciler, presets) + runningPrebuilds := getRunningPrebuilds(t, ctx, db, int(prebuildInstances)) + require.Len(t, runningPrebuilds, int(prebuildInstances)) + + // Given: a running prebuilt workspace with a deadline, ready to be claimed + prebuild := coderdtest.MustWorkspace(t, client, runningPrebuilds[0].ID) + require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition) + require.NotZero(t, prebuild.LatestBuild.Deadline) + + next := clock.Now() + if tc.isClaimedBeforeDeadline { + // When: the autobuild executor ticks *before* the deadline: + next = next.Add(time.Minute) + } else { + // When: the autobuild executor ticks *after* the deadline: + next = next.Add(24 * time.Hour) + } + + clock.Set(next) + go func() { + tickCh <- next + }() + + // Then: the prebuilt workspace should remain in a start transition + prebuildStats := <-statsCh + require.Len(t, prebuildStats.Errors, 0) + require.Len(t, prebuildStats.Transitions, 0) + require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition) + prebuild = coderdtest.MustWorkspace(t, client, prebuild.ID) + require.Equal(t, codersdk.BuildReasonInitiator, prebuild.LatestBuild.Reason) + + // Given: a user claims the prebuilt workspace + workspace := claimPrebuild(t, ctx, client, userClient, user.Username, version, presets[0].ID) + require.Equal(t, prebuild.ID, workspace.ID) + + if tc.isClaimedBeforeDeadline { + // Then: the claimed workspace should inherit and respect that same deadline. + require.True(t, workspace.LatestBuild.Deadline.Time.Equal(prebuild.LatestBuild.Deadline.Time)) + } else { + // Then: the claimed workspace should respect the next valid scheduled deadline (next day). + require.True(t, workspace.LatestBuild.Deadline.Time.Equal(clock.Now().Truncate(24*time.Hour).Add(24*time.Hour))) + } + + // When: the autobuild executor ticks *after* the deadline: + next = workspace.LatestBuild.Deadline.Time.Add(time.Minute) + clock.Set(next) + go func() { + tickCh <- next + close(tickCh) + }() + + // Then: the workspace should be stopped + workspaceStats := <-statsCh + require.Len(t, workspaceStats.Errors, 0) + require.Len(t, workspaceStats.Transitions, 1) + require.Contains(t, workspaceStats.Transitions, workspace.ID) + require.Equal(t, database.WorkspaceTransitionStop, workspaceStats.Transitions[workspace.ID]) + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + require.Equal(t, codersdk.BuildReasonAutostop, workspace.LatestBuild.Reason) + }) + } + }) + + // Prebuild workspaces should not follow the autostart schedule. + // This test verifies that AutostartRequirement (autostart schedule) is ignored while the workspace is a prebuild. + t.Run("AutostartScheduleOnlyTriggersAfterClaim", func(t *testing.T) { + t.Parallel() + + // Set the clock to dbtime.Now() to match the workspace build's CreatedAt + clock := quartz.NewMock(t) + clock.Set(dbtime.Now()) + + // Setup + ctx := testutil.Context(t, testutil.WaitSuperLong) + db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + logger := testutil.Logger(t) + tickCh := make(chan time.Time) + statsCh := make(chan autobuild.Stats) + notificationsNoop := notifications.NewNoopEnqueuer() + client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + Database: db, + Pubsub: pb, + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + Clock: clock, + TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore( + agplUserQuietHoursScheduleStore(), + notificationsNoop, + logger, + clock, + ), + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1}, + }, + }) + + // Setup Prebuild reconciler + cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{}) + reconciler := prebuilds.NewStoreReconciler( + db, pb, cache, + codersdk.PrebuildsConfig{}, + logger, + clock, + prometheus.NewRegistry(), + notificationsNoop, + ) + var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(db) + api.AGPL.PrebuildsClaimer.Store(&claimer) + + // Setup user, template and template version with a preset with 1 prebuild instance + prebuildInstances := int32(1) + userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember()) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(prebuildInstances)) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { + // Set a template level Autostart schedule to trigger the autostart daily + ctr.AllowUserAutostart = ptr.Ref[bool](true) + ctr.AutostartRequirement = &codersdk.TemplateAutostartRequirement{DaysOfWeek: codersdk.AllDaysOfWeek} + }) + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Len(t, presets, 1) + + // Given: Reconciliation loop runs and starts prebuilt workspace + runReconciliationLoop(t, ctx, db, reconciler, presets) + runningPrebuilds := getRunningPrebuilds(t, ctx, db, int(prebuildInstances)) + require.Len(t, runningPrebuilds, int(prebuildInstances)) + + // Given: prebuilt workspace has autostart schedule daily at midnight + prebuild := coderdtest.MustWorkspace(t, client, runningPrebuilds[0].ID) + sched, err := cron.Weekly("CRON_TZ=UTC 0 0 * * *") + require.NoError(t, err) + err = client.UpdateWorkspaceAutostart(ctx, prebuild.ID, codersdk.UpdateWorkspaceAutostartRequest{ + Schedule: ptr.Ref(sched.String()), + }) + require.NoError(t, err) + + // Given: prebuilt workspace is stopped + prebuild = coderdtest.MustTransitionWorkspace(t, client, prebuild.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, prebuild.LatestBuild.ID) + + // Tick at the next scheduled time after the prebuild’s LatestBuild.CreatedAt, + // since the next allowed autostart is calculated starting from that point. + // When: the autobuild executor ticks after the scheduled time + go func() { + tickCh <- sched.Next(prebuild.LatestBuild.CreatedAt).Add(time.Minute) + }() + + // Then: the prebuilt workspace should remain in a stop transition + prebuildStats := <-statsCh + require.Len(t, prebuildStats.Errors, 0) + require.Len(t, prebuildStats.Transitions, 0) + require.Equal(t, codersdk.WorkspaceTransitionStop, prebuild.LatestBuild.Transition) + prebuild = coderdtest.MustWorkspace(t, client, prebuild.ID) + require.Equal(t, codersdk.BuildReasonInitiator, prebuild.LatestBuild.Reason) + + // Given: a prebuilt workspace that is running and ready to be claimed + prebuild = coderdtest.MustTransitionWorkspace(t, client, prebuild.ID, codersdk.WorkspaceTransitionStop, codersdk.WorkspaceTransitionStart) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, prebuild.LatestBuild.ID) + + // Make sure the workspace's agent is again ready + getRunningPrebuilds(t, ctx, db, int(prebuildInstances)) + + // Given: a user claims the prebuilt workspace + workspace := claimPrebuild(t, ctx, client, userClient, user.Username, version, presets[0].ID) + require.Equal(t, prebuild.ID, workspace.ID) + require.NotNil(t, workspace.NextStartAt) + + // Given: workspace is stopped + workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + // Then: the claimed workspace should inherit and respect that same NextStartAt + require.True(t, workspace.NextStartAt.Equal(*prebuild.NextStartAt)) + + // Tick at the next scheduled time after the prebuild’s LatestBuild.CreatedAt, + // since the next allowed autostart is calculated starting from that point. + // When: the autobuild executor ticks after the scheduled time + go func() { + tickCh <- sched.Next(prebuild.LatestBuild.CreatedAt).Add(time.Minute) + }() + + // Then: the workspace should have a NextStartAt equal to the next autostart schedule + workspaceStats := <-statsCh + require.Len(t, workspaceStats.Errors, 0) + require.Len(t, workspaceStats.Transitions, 1) + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + require.NotNil(t, workspace.NextStartAt) + require.Equal(t, sched.Next(clock.Now()), workspace.NextStartAt.UTC()) + }) + + // Prebuild workspaces should not transition to dormant when the inactive TTL is reached. + // This test verifies that TimeTilDormantMillis is ignored while the workspace is a prebuild. + // After being claimed, the workspace should become dormant according to the configured inactivity period. + t.Run("DormantOnlyAfterClaimed", func(t *testing.T) { + t.Parallel() + + // Set the clock to Monday, January 1st, 2024 at 8:00 AM UTC to keep the test deterministic + clock := quartz.NewMock(t) + clock.Set(time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC)) + + // Setup + ctx := testutil.Context(t, testutil.WaitSuperLong) + db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + logger := testutil.Logger(t) + tickCh := make(chan time.Time) + statsCh := make(chan autobuild.Stats) + notificationsNoop := notifications.NewNoopEnqueuer() + client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + Database: db, + Pubsub: pb, + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + Clock: clock, + TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore( + agplUserQuietHoursScheduleStore(), + notificationsNoop, + logger, + clock, + ), + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1}, + }, + }) + + // Setup Prebuild reconciler + cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{}) + reconciler := prebuilds.NewStoreReconciler( + db, pb, cache, + codersdk.PrebuildsConfig{}, + logger, + clock, + prometheus.NewRegistry(), + notificationsNoop, + ) + var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(db) + api.AGPL.PrebuildsClaimer.Store(&claimer) + + // Setup user, template and template version with a preset with 1 prebuild instance + prebuildInstances := int32(1) + inactiveTTL := 2 * time.Hour + userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember()) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(prebuildInstances)) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { + // Set a template level inactive TTL to trigger dormancy + ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) + }) + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Len(t, presets, 1) + + // Given: reconciliation loop runs and starts prebuilt workspace + runReconciliationLoop(t, ctx, db, reconciler, presets) + runningPrebuilds := getRunningPrebuilds(t, ctx, db, int(prebuildInstances)) + require.Len(t, runningPrebuilds, int(prebuildInstances)) + + // Given: a running prebuilt workspace, ready to be claimed + prebuild := coderdtest.MustWorkspace(t, client, runningPrebuilds[0].ID) + require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition) + + // When: the autobuild executor ticks *after* the inactive TTL + go func() { + tickCh <- prebuild.LastUsedAt.Add(inactiveTTL).Add(time.Minute) + }() + + // Then: the prebuilt workspace should remain in a start transition + prebuildStats := <-statsCh + require.Len(t, prebuildStats.Errors, 0) + require.Len(t, prebuildStats.Transitions, 0) + require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition) + prebuild = coderdtest.MustWorkspace(t, client, prebuild.ID) + require.Equal(t, codersdk.BuildReasonInitiator, prebuild.LatestBuild.Reason) + + // Given: a user claims the prebuilt workspace sometime later + clock.Set(clock.Now().Add(inactiveTTL)) + workspace := claimPrebuild(t, ctx, client, userClient, user.Username, version, presets[0].ID) + require.Equal(t, prebuild.ID, workspace.ID) + require.Nil(t, prebuild.DormantAt) + + // When: the autobuild executor ticks *after* the inactive TTL + go func() { + tickCh <- prebuild.LastUsedAt.Add(inactiveTTL).Add(time.Minute) + close(tickCh) + }() + + // Then: the workspace should transition to stopped state for breaching failure TTL + workspaceStats := <-statsCh + require.Len(t, workspaceStats.Errors, 0) + require.Len(t, workspaceStats.Transitions, 1) + require.Contains(t, workspaceStats.Transitions, workspace.ID) + require.Equal(t, database.WorkspaceTransitionStop, workspaceStats.Transitions[workspace.ID]) + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + require.Equal(t, codersdk.BuildReasonDormancy, workspace.LatestBuild.Reason) + require.NotNil(t, workspace.DormantAt) + }) + + // Prebuild workspaces should not be deleted when the failure TTL is reached. + // This test verifies that FailureTTLMillis is ignored while the workspace is a prebuild. + t.Run("FailureTTLOnlyAfterClaimed", func(t *testing.T) { + t.Parallel() + + // Set the clock to Monday, January 1st, 2024 at 8:00 AM UTC to keep the test deterministic + clock := quartz.NewMock(t) + clock.Set(time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC)) + + // Setup + ctx := testutil.Context(t, testutil.WaitSuperLong) + db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + logger := testutil.Logger(t) + tickCh := make(chan time.Time) + statsCh := make(chan autobuild.Stats) + notificationsNoop := notifications.NewNoopEnqueuer() + client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + Database: db, + Pubsub: pb, + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + Clock: clock, + TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore( + agplUserQuietHoursScheduleStore(), + notificationsNoop, + logger, + clock, + ), + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + }, + }, + }) + + // Setup Prebuild reconciler + cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{}) + reconciler := prebuilds.NewStoreReconciler( + db, pb, cache, + codersdk.PrebuildsConfig{}, + logger, + clock, + prometheus.NewRegistry(), + notificationsNoop, + ) + var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(db) + api.AGPL.PrebuildsClaimer.Store(&claimer) + + // Setup user, template and template version with a preset with 1 prebuild instance + prebuildInstances := int32(1) + failureTTL := 2 * time.Hour + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithFailedResponseAndPresetsWithPrebuilds(prebuildInstances)) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { + // Set a template level Failure TTL to trigger workspace deletion + ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds()) + }) + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Len(t, presets, 1) + + // Given: reconciliation loop runs and starts prebuilt workspace in failed state + runReconciliationLoop(t, ctx, db, reconciler, presets) + + var failedWorkspaceBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow + require.Eventually(t, func() bool { + rows, err := db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{ + TemplateID: template.ID, + }) + if err != nil { + return false + } + + failedWorkspaceBuilds = append(failedWorkspaceBuilds, rows...) + + t.Logf("found %d failed prebuilds so far, want %d", len(failedWorkspaceBuilds), prebuildInstances) + return len(failedWorkspaceBuilds) == int(prebuildInstances) + }, testutil.WaitSuperLong, testutil.IntervalSlow) + require.Len(t, failedWorkspaceBuilds, int(prebuildInstances)) + + // Given: a failed prebuilt workspace + prebuild := coderdtest.MustWorkspace(t, client, failedWorkspaceBuilds[0].WorkspaceID) + require.Equal(t, codersdk.WorkspaceStatusFailed, prebuild.LatestBuild.Status) + + // When: the autobuild executor ticks *after* the failure TTL + go func() { + tickCh <- prebuild.LatestBuild.Job.CompletedAt.Add(failureTTL * 2) + }() + + // Then: the prebuilt workspace should remain in a start transition + prebuildStats := <-statsCh + require.Len(t, prebuildStats.Errors, 0) + require.Len(t, prebuildStats.Transitions, 0) + require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition) + prebuild = coderdtest.MustWorkspace(t, client, prebuild.ID) + require.Equal(t, codersdk.BuildReasonInitiator, prebuild.LatestBuild.Reason) + }) +} + +func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses { + return &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Presets: []*proto.Preset{ + { + Name: "preset-test", + Parameters: []*proto.PresetParameter{ + { + Name: "k1", + Value: "v1", + }, + }, + Prebuild: &proto.Prebuild{ + Instances: desiredInstances, + }, + }, + }, + }, + }, + }, + }, + ProvisionApply: []*proto.Response{ + { + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func templateWithFailedResponseAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses { + return &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Presets: []*proto.Preset{ + { + Name: "preset-test", + Parameters: []*proto.PresetParameter{ + { + Name: "k1", + Value: "v1", + }, + }, + Prebuild: &proto.Prebuild{ + Instances: desiredInstances, + }, + }, + }, + }, + }, + }, + }, + ProvisionApply: echo.ApplyFailed, + } +} + // TestWorkspaceTemplateParamsChange tests a workspace with a parameter that // validation changes on apply. The params used in create workspace are invalid // according to the static params on import. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0e6a481406d8b..541f885b62dc9 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -275,11 +275,12 @@ export interface BuildInfoResponse { } // From codersdk/workspacebuilds.go -export type BuildReason = "autostart" | "autostop" | "initiator"; +export type BuildReason = "autostart" | "autostop" | "dormancy" | "initiator"; export const BuildReasons: BuildReason[] = [ "autostart", "autostop", + "dormancy", "initiator", ]; diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 135b965589054..c88ffc9d8edaa 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -75,14 +75,16 @@ export const getDisplayWorkspaceBuildStatus = ( export const getDisplayWorkspaceBuildInitiatedBy = ( build: TypesGen.WorkspaceBuild, -): string => { +): string | undefined => { switch (build.reason) { case "initiator": return build.initiator_name; case "autostart": case "autostop": + case "dormancy": return "Coder"; } + return undefined; }; const getWorkspaceBuildDurationInSeconds = ( From 0ead64f264f6ae53f410ef48a0e2a6a76ccc7b2d Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 01:16:21 +0500 Subject: [PATCH 6/7] chore: add image styles for kiro.svg (cherry-pick #18889) (#18890) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ケイラ --- site/src/theme/externalImages.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/theme/externalImages.ts b/site/src/theme/externalImages.ts index 9f287413cad9f..f736e91e7b745 100644 --- a/site/src/theme/externalImages.ts +++ b/site/src/theme/externalImages.ts @@ -154,6 +154,7 @@ export const defaultParametersForBuiltinIcons = new Map([ ["/icon/image.svg", "monochrome"], ["/icon/jupyter.svg", "blackWithColor"], ["/icon/kasmvnc.svg", "whiteWithColor"], + ["/icon/kiro.svg", "whiteWithColor"], ["/icon/memory.svg", "monochrome"], ["/icon/rust.svg", "monochrome"], ["/icon/terminal.svg", "monochrome"], From 3e0645cdeb77cf91e46f62af69a6d91e27b8a089 Mon Sep 17 00:00:00 2001 From: Jakub Domeracki Date: Wed, 16 Jul 2025 14:15:48 +0200 Subject: [PATCH 7/7] chore: publish CLI binaries and detached signatures to releases.coder.com (#18901) Cherry pick (https://github.com/coder/coder/commit/e4d3453e2b55edfc5a9650083f4bffc765423b1c) Starting with version 2.24.X , Coder CLI binaries & corresponding detached signatures will get published to the GCS bucket releases.coder.com. --- .github/workflows/release.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 71567cd540502..1d5ebb5aa23b8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -634,6 +634,29 @@ jobs: - name: ls build run: ls -lh build + - name: Publish Coder CLI binaries and detached signatures to GCS + if: ${{ !inputs.dry_run && github.ref == 'refs/heads/main' && github.repository_owner == 'coder'}} + run: | + set -euxo pipefail + + version="$(./scripts/version.sh)" + + binaries=( + "coder-darwin-amd64" + "coder-darwin-arm64" + "coder-linux-amd64" + "coder-linux-arm64" + "coder-linux-armv7" + "coder-windows-amd64.exe" + "coder-windows-arm64.exe" + ) + + for binary in "${binaries[@]}"; do + detached_signature="${binary}.asc" + gcloud storage cp "./site/out/bin/${binary}" "gs://releases.coder.com/coder-cli/${version}/${binary}" + gcloud storage cp "./site/out/bin/${detached_signature}" "gs://releases.coder.com/coder-cli/${version}/${detached_signature}" + done + - name: Publish release run: | set -euo pipefail pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy