From 67e1567b47f75b696ac04a82eb34d6abcb78cdb3 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 12 Aug 2025 16:55:59 +0200 Subject: [PATCH 1/5] chore: specify `packageManager` explicitly to match pinned version in `Dockerfile` (#19311) --- package.json | 2 +- site/package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ee5cba7ecf538..4b77296584c1f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "_comment": "This version doesn't matter, it's just to allow importing from other repos.", "name": "coder", "version": "0.0.0", - "packageManager": "pnpm@9.14.4", + "packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf", "scripts": { "format-docs": "markdown-table-formatter $(find docs -name '*.md') *.md", "lint-docs": "markdownlint-cli2 --fix $(find docs -name '*.md') *.md", diff --git a/site/package.json b/site/package.json index 534087e25b81f..93f3c775a916b 100644 --- a/site/package.json +++ b/site/package.json @@ -4,6 +4,7 @@ "repository": "https://github.com/coder/coder", "private": true, "license": "AGPL-3.0", + "packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf", "scripts": { "build": "NODE_ENV=production pnpm vite build", "check": "biome check --error-on-warnings .", From 064436a3005dfe1dd4a9049032a3ba32e8dc68e0 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 12 Aug 2025 12:30:09 -0400 Subject: [PATCH 2/5] chore: fix formdata for apidocgen (#19319) --- scripts/apidocgen/package.json | 3 +- scripts/apidocgen/pnpm-lock.yaml | 126 ++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 5 deletions(-) diff --git a/scripts/apidocgen/package.json b/scripts/apidocgen/package.json index cf8072904ba8a..4ab69c8f72442 100644 --- a/scripts/apidocgen/package.json +++ b/scripts/apidocgen/package.json @@ -8,7 +8,8 @@ }, "pnpm": { "overrides": { - "@babel/runtime": "7.26.10" + "@babel/runtime": "7.26.10", + "form-data": "4.0.4" } } } diff --git a/scripts/apidocgen/pnpm-lock.yaml b/scripts/apidocgen/pnpm-lock.yaml index 9d729e02a4bb9..619e9dc9f6a6c 100644 --- a/scripts/apidocgen/pnpm-lock.yaml +++ b/scripts/apidocgen/pnpm-lock.yaml @@ -8,6 +8,7 @@ overrides: semver: 7.5.3 jsonpointer: 5.0.1 '@babel/runtime': 7.26.10 + form-data: 4.0.4 importers: @@ -82,6 +83,10 @@ packages: peerDependencies: ajv: 4.11.8 - 6 + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} @@ -167,6 +172,10 @@ packages: engines: {'0': node >=0.2.6} hasBin: true + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -179,6 +188,22 @@ packages: entities@2.0.3: resolution: {integrity: sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} @@ -220,8 +245,8 @@ packages: foreach@2.0.6: resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==} - form-data@3.0.0: - resolution: {integrity: sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} from@0.1.7: @@ -234,6 +259,9 @@ packages: resolution: {integrity: sha512-yI+wDwj0FsgX7tyIQJR+EP60R64evMSixtGb9AzGWjJVKlF5tCet95KomfqGBg/aIAG1Dhd6wjCOQe5HbX/qLA==} engines: {node: '>=0.10'} + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@1.0.3: resolution: {integrity: sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==} @@ -241,13 +269,25 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-own-enumerable-property-symbols@3.0.2: resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@4.1.0: resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} engines: {node: '>=6'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -271,6 +311,18 @@ packages: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + highlightjs@9.16.2: resolution: {integrity: sha512-FK1vmMj8BbEipEy8DLIvp71t5UsC7n2D6En/UfM/91PCwmOpj6f2iu0Y0coRC62KSRHHC+dquM2xMULV/X7NFg==} deprecated: Use the 'highlight.js' package instead https://npm.im/highlight.js @@ -375,6 +427,10 @@ packages: resolution: {integrity: sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==} hasBin: true + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdurl@1.0.1: resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} @@ -786,6 +842,11 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-me-maybe@1.0.2: {} camelcase@5.3.1: {} @@ -866,6 +927,12 @@ snapshots: dot@1.1.3: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer@0.1.2: {} emoji-regex@8.0.0: {} @@ -876,6 +943,21 @@ snapshots: entities@2.0.3: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es6-promise@3.3.1: {} escalade@3.1.1: {} @@ -921,10 +1003,12 @@ snapshots: foreach@2.0.6: {} - form-data@3.0.0: + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 from@0.1.7: {} @@ -940,16 +1024,38 @@ snapshots: transitivePeerDependencies: - mkdirp + function-bind@1.1.2: {} + get-caller-file@1.0.3: {} get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-own-enumerable-property-symbols@3.0.2: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@4.1.0: dependencies: pump: 3.0.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} grapheme-splitter@1.0.4: {} @@ -967,6 +1073,16 @@ snapshots: has-flag@3.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + highlightjs@9.16.2: {} http2-client@1.3.5: {} @@ -977,7 +1093,7 @@ snapshots: commander: 2.20.3 debug: 2.6.9 event-stream: 3.3.4 - form-data: 3.0.0 + form-data: 4.0.4 fs-readfile-promise: 2.0.1 fs-writefile-promise: 1.0.3(mkdirp@3.0.1) har-validator: 5.1.5 @@ -1063,6 +1179,8 @@ snapshots: mdurl: 1.0.1 uc.micro: 1.0.6 + math-intrinsics@1.1.0: {} + mdurl@1.0.1: {} mem@4.3.0: From aab2ccdb38065e3dc7d7895609b9728fb3548716 Mon Sep 17 00:00:00 2001 From: Rafael Rodriguez Date: Tue, 12 Aug 2025 11:37:44 -0500 Subject: [PATCH 3/5] fix!: support empty or default fields when updating templates (#19256) Breaking change: Field types in `codersdk.UpdateTemplateMeta` for `Icon`, `Description`, and `DisplayName` moved to `*string` ## Summary In this pull request we're updating the `UpdateTemplateMeta` struct to allow `DisplayName`, `Description`, and `Icon` to be set as empty `""` or default to the value from the template if not provided in an update call. Fixes https://github.com/coder/coder/issues/19036 ### The bug The reported bug occurred when clients were attempting to update a metadata field in a template via an edit call. When the request was decoded into an `UpdateTemplateMeta` struct the default values for fields in the struct were used to update the template even if they weren't provided. This led to fields like `Icon` being set to `""` (the default value). ### Changes To allow for specific fields to be set to `""` these fields were updated to be `*string` as opposed to `string`. This allows for clients to set these fields as `""` in an update request or they will default to the template value if they are not provided in the update request (will be `nil`). Added tests to confirm empty and nil values and updated other tests that use these fields. --- cli/templateedit.go | 6 +- coderd/templates.go | 16 ++- coderd/templates_test.go | 166 ++++++++++++++++++++++------ codersdk/templates.go | 10 +- enterprise/cli/templateedit_test.go | 12 +- enterprise/coderd/templates_test.go | 42 +++---- 6 files changed, 178 insertions(+), 74 deletions(-) diff --git a/cli/templateedit.go b/cli/templateedit.go index b115350ab4437..fe0323449c9be 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -169,9 +169,9 @@ func (r *RootCmd) templateEdit() *serpent.Command { req := codersdk.UpdateTemplateMeta{ Name: name, - DisplayName: displayName, - Description: description, - Icon: icon, + DisplayName: &displayName, + Description: &description, + Icon: &icon, DefaultTTLMillis: defaultTTL.Milliseconds(), ActivityBumpMillis: activityBump.Milliseconds(), AutostopRequirement: &codersdk.TemplateAutostopRequirement{ diff --git a/coderd/templates.go b/coderd/templates.go index f9c5d8271a1e6..16ab5b3fa37a5 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -771,12 +771,16 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { classicTemplateFlow = *req.UseClassicParameterFlow } + displayName := ptr.NilToDefault(req.DisplayName, template.DisplayName) + description := ptr.NilToDefault(req.Description, template.Description) + icon := ptr.NilToDefault(req.Icon, template.Icon) + var updated database.Template err = api.Database.InTx(func(tx database.Store) error { if req.Name == template.Name && - req.Description == template.Description && - req.DisplayName == template.DisplayName && - req.Icon == template.Icon && + description == template.Description && + displayName == template.DisplayName && + icon == template.Icon && req.AllowUserAutostart == template.AllowUserAutostart && req.AllowUserAutostop == template.AllowUserAutostop && req.AllowUserCancelWorkspaceJobs == template.AllowUserCancelWorkspaceJobs && @@ -827,9 +831,9 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { ID: template.ID, UpdatedAt: dbtime.Now(), Name: name, - DisplayName: req.DisplayName, - Description: req.Description, - Icon: req.Icon, + DisplayName: displayName, + Description: description, + Icon: icon, AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs, GroupACL: groupACL, MaxPortSharingLevel: maxPortShareLevel, diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 050ae77f8ca49..325de6a18c8e3 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -901,9 +901,9 @@ func TestPatchTemplateMeta(t *testing.T) { req := codersdk.UpdateTemplateMeta{ Name: "new-template-name", - DisplayName: "Displayed Name 456", - Description: "lorem ipsum dolor sit amet et cetera", - Icon: "/icon/new-icon.png", + DisplayName: ptr.Ref("Displayed Name 456"), + Description: ptr.Ref("lorem ipsum dolor sit amet et cetera"), + Icon: ptr.Ref("/icon/new-icon.png"), DefaultTTLMillis: 12 * time.Hour.Milliseconds(), ActivityBumpMillis: 3 * time.Hour.Milliseconds(), AllowUserCancelWorkspaceJobs: false, @@ -918,9 +918,9 @@ func TestPatchTemplateMeta(t *testing.T) { require.NoError(t, err) assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, req.Name, updated.Name) - assert.Equal(t, req.DisplayName, updated.DisplayName) - assert.Equal(t, req.Description, updated.Description) - assert.Equal(t, req.Icon, updated.Icon) + assert.Equal(t, *req.DisplayName, updated.DisplayName) + assert.Equal(t, *req.Description, updated.Description) + assert.Equal(t, *req.Icon, updated.Icon) assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, req.ActivityBumpMillis, updated.ActivityBumpMillis) assert.False(t, req.AllowUserCancelWorkspaceJobs) @@ -930,9 +930,9 @@ func TestPatchTemplateMeta(t *testing.T) { require.NoError(t, err) assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, req.Name, updated.Name) - assert.Equal(t, req.DisplayName, updated.DisplayName) - assert.Equal(t, req.Description, updated.Description) - assert.Equal(t, req.Icon, updated.Icon) + assert.Equal(t, *req.DisplayName, updated.DisplayName) + assert.Equal(t, *req.Description, updated.Description) + assert.Equal(t, *req.Icon, updated.Icon) assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, req.ActivityBumpMillis, updated.ActivityBumpMillis) assert.False(t, req.AllowUserCancelWorkspaceJobs) @@ -1167,9 +1167,9 @@ func TestPatchTemplateMeta(t *testing.T) { got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, DefaultTTLMillis: 0, AutostopRequirement: &template.AutostopRequirement, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, @@ -1202,9 +1202,9 @@ func TestPatchTemplateMeta(t *testing.T) { got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, DefaultTTLMillis: template.DefaultTTLMillis, AutostopRequirement: &template.AutostopRequirement, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, @@ -1263,9 +1263,9 @@ func TestPatchTemplateMeta(t *testing.T) { allowAutostop.Store(false) got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, DefaultTTLMillis: template.DefaultTTLMillis, AutostopRequirement: &template.AutostopRequirement, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, @@ -1294,9 +1294,9 @@ func TestPatchTemplateMeta(t *testing.T) { got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, // Increase the default TTL to avoid error "not modified". DefaultTTLMillis: template.DefaultTTLMillis + 1, AutostopRequirement: &template.AutostopRequirement, @@ -1326,8 +1326,8 @@ func TestPatchTemplateMeta(t *testing.T) { req := codersdk.UpdateTemplateMeta{ Name: template.Name, - Description: template.Description, - Icon: template.Icon, + Description: &template.Description, + Icon: &template.Icon, DefaultTTLMillis: template.DefaultTTLMillis, ActivityBumpMillis: template.ActivityBumpMillis, AutostopRequirement: nil, @@ -1387,7 +1387,7 @@ func TestPatchTemplateMeta(t *testing.T) { ctr.Icon = "/icon/code.png" }) req := codersdk.UpdateTemplateMeta{ - Icon: "", + Icon: ptr.Ref(""), } ctx := testutil.Context(t, testutil.WaitLong) @@ -1442,9 +1442,9 @@ func TestPatchTemplateMeta(t *testing.T) { require.EqualValues(t, 1, template.AutostopRequirement.Weeks) req := codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, DefaultTTLMillis: time.Hour.Milliseconds(), AutostopRequirement: &codersdk.TemplateAutostopRequirement{ @@ -1519,9 +1519,9 @@ func TestPatchTemplateMeta(t *testing.T) { require.EqualValues(t, 2, template.AutostopRequirement.Weeks) req := codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, DefaultTTLMillis: time.Hour.Milliseconds(), AutostopRequirement: &codersdk.TemplateAutostopRequirement{ @@ -1556,9 +1556,9 @@ func TestPatchTemplateMeta(t *testing.T) { require.EqualValues(t, 1, template.AutostopRequirement.Weeks) req := codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, DefaultTTLMillis: time.Hour.Milliseconds(), AutostopRequirement: &codersdk.TemplateAutostopRequirement{ @@ -1618,6 +1618,106 @@ func TestPatchTemplateMeta(t *testing.T) { require.NoError(t, err) assert.False(t, updated.UseClassicParameterFlow, "expected false") }) + + t.Run("SupportEmptyOrDefaultFields", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + + displayName := "Test Display Name" + description := "test-description" + icon := "/icon/icon.png" + defaultTTLMillis := 10 * time.Hour.Milliseconds() + + reference := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { + ctr.DisplayName = displayName + ctr.Description = description + ctr.Icon = icon + ctr.DefaultTTLMillis = ptr.Ref(defaultTTLMillis) + }) + require.Equal(t, displayName, reference.DisplayName) + require.Equal(t, description, reference.Description) + require.Equal(t, icon, reference.Icon) + + restoreReq := codersdk.UpdateTemplateMeta{ + DisplayName: &displayName, + Description: &description, + Icon: &icon, + DefaultTTLMillis: defaultTTLMillis, + } + + type expected struct { + displayName string + description string + icon string + defaultTTLMillis int64 + } + + type testCase struct { + name string + req codersdk.UpdateTemplateMeta + expected expected + } + + tests := []testCase{ + { + name: "Only update default_ttl_ms", + req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: 99 * time.Hour.Milliseconds()}, + expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 99 * time.Hour.Milliseconds()}, + }, + { + name: "Clear display name", + req: codersdk.UpdateTemplateMeta{DisplayName: ptr.Ref("")}, + expected: expected{displayName: "", description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0}, + }, + { + name: "Clear description", + req: codersdk.UpdateTemplateMeta{Description: ptr.Ref("")}, + expected: expected{displayName: reference.DisplayName, description: "", icon: reference.Icon, defaultTTLMillis: 0}, + }, + { + name: "Clear icon", + req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("")}, + expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: "", defaultTTLMillis: 0}, + }, + { + name: "Nil display name defaults to reference display name", + req: codersdk.UpdateTemplateMeta{DisplayName: nil}, + expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0}, + }, + { + name: "Nil description defaults to reference description", + req: codersdk.UpdateTemplateMeta{Description: nil}, + expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0}, + }, + { + name: "Nil icon defaults to reference icon", + req: codersdk.UpdateTemplateMeta{Icon: nil}, + expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0}, + }, + } + + for _, tc := range tests { + //nolint:tparallel,paralleltest + t.Run(tc.name, func(t *testing.T) { + defer func() { + ctx := testutil.Context(t, testutil.WaitLong) + // Restore reference after each test case + _, err := client.UpdateTemplateMeta(ctx, reference.ID, restoreReq) + require.NoError(t, err) + }() + ctx := testutil.Context(t, testutil.WaitLong) + updated, err := client.UpdateTemplateMeta(ctx, reference.ID, tc.req) + require.NoError(t, err) + assert.Equal(t, tc.expected.displayName, updated.DisplayName) + assert.Equal(t, tc.expected.description, updated.Description) + assert.Equal(t, tc.expected.icon, updated.Icon) + assert.Equal(t, tc.expected.defaultTTLMillis, updated.DefaultTTLMillis) + }) + } + }) } func TestDeleteTemplate(t *testing.T) { diff --git a/codersdk/templates.go b/codersdk/templates.go index 2e77d999003ed..cc9314e44794d 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -208,11 +208,11 @@ type ACLAvailable struct { } type UpdateTemplateMeta struct { - Name string `json:"name,omitempty" validate:"omitempty,template_name"` - DisplayName string `json:"display_name,omitempty" validate:"omitempty,template_display_name"` - Description string `json:"description,omitempty"` - Icon string `json:"icon,omitempty"` - DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"` + Name string `json:"name,omitempty" validate:"omitempty,template_name"` + DisplayName *string `json:"display_name,omitempty" validate:"omitempty,template_display_name"` + Description *string `json:"description,omitempty"` + Icon *string `json:"icon,omitempty"` + DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"` // ActivityBumpMillis allows optionally specifying the activity bump // duration for all workspaces created from this template. Defaults to 1h // but can be set to 0 to disable activity bumping. diff --git a/enterprise/cli/templateedit_test.go b/enterprise/cli/templateedit_test.go index fbff3e75dffcf..01d4784fd3c1e 100644 --- a/enterprise/cli/templateedit_test.go +++ b/enterprise/cli/templateedit_test.go @@ -219,9 +219,9 @@ func TestTemplateEdit(t *testing.T) { template, err := ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{ Name: expectedName, - DisplayName: expectedDisplayName, - Description: expectedDescription, - Icon: expectedIcon, + DisplayName: &expectedDisplayName, + Description: &expectedDescription, + Icon: &expectedIcon, DefaultTTLMillis: expectedDefaultTTLMillis, AllowUserAutostop: expectedAllowAutostop, AllowUserAutostart: expectedAllowAutostart, @@ -267,9 +267,9 @@ func TestTemplateEdit(t *testing.T) { template, err = ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{ Name: expectedName, - DisplayName: expectedDisplayName, - Description: expectedDescription, - Icon: expectedIcon, + DisplayName: &expectedDisplayName, + Description: &expectedDescription, + Icon: &expectedIcon, DefaultTTLMillis: expectedDefaultTTLMillis, AllowUserAutostop: expectedAllowAutostop, AllowUserAutostart: expectedAllowAutostart, diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 30b04eaa007b4..e5eafa82f8d1c 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -259,9 +259,9 @@ func TestTemplates(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, AutostartRequirement: &codersdk.TemplateAutostartRequirement{ DaysOfWeek: []string{"monday", "saturday"}, }, @@ -276,9 +276,9 @@ func TestTemplates(t *testing.T) { // Ensure a missing field is a noop updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon + "something", + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: ptr.Ref(template.Icon + "something"), }) require.NoError(t, err) require.Equal(t, []string{"monday", "saturday"}, updated.AutostartRequirement.DaysOfWeek) @@ -313,9 +313,9 @@ func TestTemplates(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) _, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, AutostartRequirement: &codersdk.TemplateAutostartRequirement{ DaysOfWeek: []string{"foobar", "saturday"}, }, @@ -349,9 +349,9 @@ func TestTemplates(t *testing.T) { ctx := context.Background() updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, DefaultTTLMillis: time.Hour.Milliseconds(), AutostopRequirement: &codersdk.TemplateAutostopRequirement{ @@ -403,9 +403,9 @@ func TestTemplates(t *testing.T) { updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, TimeTilDormantMillis: inactivityTTL.Milliseconds(), FailureTTLMillis: failureTTL.Milliseconds(), @@ -472,9 +472,9 @@ func TestTemplates(t *testing.T) { t.Run(c.Name, func(t *testing.T) { _, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, TimeTilDormantMillis: c.TimeTilDormantMS, FailureTTLMillis: c.FailureTTLMS, @@ -1004,9 +1004,9 @@ func TestTemplateACL(t *testing.T) { require.Equal(t, 1, len(acl.Groups)) _, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, + DisplayName: &template.DisplayName, + Description: &template.Description, + Icon: &template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, DisableEveryoneGroupAccess: true, }) From f4f4e52173f91a54cc7895fb9de84799abcd4afd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 12 Aug 2025 12:59:47 -0500 Subject: [PATCH 4/5] docs: remove beta badge from docs about dynamic parameters (#19325) --- docs/admin/templates/extending-templates/parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/admin/templates/extending-templates/parameters.md b/docs/admin/templates/extending-templates/parameters.md index 5b380645c1b36..43a477632e7db 100644 --- a/docs/admin/templates/extending-templates/parameters.md +++ b/docs/admin/templates/extending-templates/parameters.md @@ -391,7 +391,7 @@ parameters in one of two ways: Or set the [environment variable](../../setup/index.md), `CODER_EXPERIMENTS=auto-fill-parameters` -## Dynamic Parameters (beta) +## Dynamic Parameters Coder v2.24.0 introduces [Dynamic Parameters](./dynamic-parameters.md) to extend the existing parameter system with conditional form controls, enriched input types, and user identity awareness. From ea7025b562dbc2cdf2c9c01a4f10f95d69f159e6 Mon Sep 17 00:00:00 2001 From: DevCats Date: Tue, 12 Aug 2025 13:02:40 -0500 Subject: [PATCH 5/5] docs(admin/users): add google provider-specific guide (#19309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add a provider-specific guide for configuring Google as an OIDC provider - Document refresh token setup via CODER_OIDC_AUTH_URL_PARAMS - Add page to docs navigation under Users → OIDC Authentication ## Test plan - Docs site builds: `docs/admin/users/oidc-auth/google.md` renders - Nav shows 'Google' under OIDC Authentication - Links to OIDC overview and refresh tokens work Fixes #13508 --------- Co-authored-by: Atif Ali --- docs/admin/users/oidc-auth/google.md | 62 ++++++++++++++++++++++++++++ docs/manifest.json | 5 +++ 2 files changed, 67 insertions(+) create mode 100644 docs/admin/users/oidc-auth/google.md diff --git a/docs/admin/users/oidc-auth/google.md b/docs/admin/users/oidc-auth/google.md new file mode 100644 index 0000000000000..298497b27bebc --- /dev/null +++ b/docs/admin/users/oidc-auth/google.md @@ -0,0 +1,62 @@ +# Google authentication (OIDC) + +This guide shows how to configure Coder to authenticate users with Google using OpenID Connect (OIDC). + +## Prerequisites + +- A Google Cloud project with the OAuth consent screen configured +- Permission to create OAuth 2.0 Client IDs in Google Cloud + +## Step 1: Create an OAuth client in Google Cloud + +1. Open Google Cloud Console → APIs & Services → Credentials → Create Credentials → OAuth client ID. +2. Application type: Web application. +3. Authorized redirect URIs: add your Coder callback URL: + - `https://coder.example.com/api/v2/users/oidc/callback` +4. Save and note the Client ID and Client secret. + +## Step 2: Configure Coder OIDC for Google + +Set the following environment variables on your Coder deployment and restart Coder: + +```env +CODER_OIDC_ISSUER_URL=https://accounts.google.com +CODER_OIDC_CLIENT_ID= +CODER_OIDC_CLIENT_SECRET= +# Restrict to one or more email domains (comma-separated) +CODER_OIDC_EMAIL_DOMAIN="example.com" +# Standard OIDC scopes for Google +CODER_OIDC_SCOPES=openid,profile,email +# Optional: customize the login button +CODER_OIDC_SIGN_IN_TEXT="Sign in with Google" +CODER_OIDC_ICON_URL=/icon/google.svg +``` + +> [!NOTE] +> The redirect URI must exactly match what you configured in Google Cloud. + +## Enable refresh tokens (recommended) + +Google uses auth URL parameters to issue refresh tokens. Configure: + +```env +# Keep standard scopes +CODER_OIDC_SCOPES=openid,profile,email +# Add Google-specific auth URL params +CODER_OIDC_AUTH_URL_PARAMS='{"access_type": "offline", "prompt": "consent"}' +``` + +After changing settings, users must log out and back in once to obtain refresh tokens. + +Learn more in [Configure OIDC refresh tokens](./refresh-tokens.md). + +## Troubleshooting + +- "invalid redirect_uri": ensure the redirect URI in Google Cloud matches `https:///api/v2/users/oidc/callback`. +- Domain restriction: if users from unexpected domains can log in, verify `CODER_OIDC_EMAIL_DOMAIN`. +- Claims: to inspect claims returned by Google, see guidance in the [OIDC overview](./index.md#oidc-claims). + +## See also + +- [OIDC overview](./index.md) +- [Configure OIDC refresh tokens](./refresh-tokens.md) diff --git a/docs/manifest.json b/docs/manifest.json index ce03ef0ff2de1..6e943aa56f697 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -416,6 +416,11 @@ "description": "Configure OpenID Connect authentication with identity providers like Okta or Active Directory", "path": "./admin/users/oidc-auth/index.md", "children": [ + { + "title": "Google", + "description": "Configure Google as an OIDC provider", + "path": "./admin/users/oidc-auth/google.md" + }, { "title": "Configure OIDC refresh tokens", "description": "How to configure OIDC refresh tokens",