diff --git a/agent/agent.go b/agent/agent.go
index 27219c7ef0c1e..a971c0e7987b6 100644
--- a/agent/agent.go
+++ b/agent/agent.go
@@ -1200,7 +1200,7 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
network := a.network
a.closeMutex.Unlock()
if network == nil {
- keySeed, err := SSHKeySeed(manifest.OwnerUsername, manifest.WorkspaceName, manifest.AgentName)
+ keySeed, err := SSHKeySeed(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName)
if err != nil {
return xerrors.Errorf("generate SSH key seed: %w", err)
}
diff --git a/cli/exp_scaletest.go b/cli/exp_scaletest.go
index 9ab18e168e16a..a844a7e8c6258 100644
--- a/cli/exp_scaletest.go
+++ b/cli/exp_scaletest.go
@@ -1027,8 +1027,8 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Command {
BytesPerTick: bytesPerTick,
Duration: strategy.timeout,
TickInterval: tickInterval,
- ReadMetrics: metrics.ReadMetrics(ws.OwnerUsername, ws.Name, agent.Name),
- WriteMetrics: metrics.WriteMetrics(ws.OwnerUsername, ws.Name, agent.Name),
+ ReadMetrics: metrics.ReadMetrics(ws.OwnerName, ws.Name, agent.Name),
+ WriteMetrics: metrics.WriteMetrics(ws.OwnerName, ws.Name, agent.Name),
SSH: ssh,
Echo: ssh,
App: appConfig,
@@ -1420,7 +1420,7 @@ func isScaleTestUser(user codersdk.User) bool {
}
func isScaleTestWorkspace(workspace codersdk.Workspace) bool {
- return strings.HasPrefix(workspace.OwnerUsername, "scaletest-") ||
+ return strings.HasPrefix(workspace.OwnerName, "scaletest-") ||
strings.HasPrefix(workspace.Name, "scaletest-")
}
@@ -1592,7 +1592,7 @@ func createWorkspaceAppConfig(client *codersdk.Client, appHost, app string, work
c.URL = fmt.Sprintf("%s://%s", client.URL.Scheme, strings.Replace(appHost, "*", agent.Apps[i].SubdomainName, 1))
} else {
- c.URL = fmt.Sprintf("%s/@%s/%s.%s/apps/%s", client.URL.String(), workspace.OwnerUsername, workspace.Name, agent.Name, agent.Apps[i].Slug)
+ c.URL = fmt.Sprintf("%s/@%s/%s.%s/apps/%s", client.URL.String(), workspace.OwnerName, workspace.Name, agent.Name, agent.Apps[i].Slug)
}
return c, nil
diff --git a/cli/list.go b/cli/list.go
index 7eb90b4b6b547..083d32c6e8fa1 100644
--- a/cli/list.go
+++ b/cli/list.go
@@ -54,7 +54,7 @@ func workspaceListRowFromWorkspace(now time.Time, workspace codersdk.Workspace)
if workspace.Favorite {
favIco = "★"
}
- workspaceName := favIco + " " + workspace.OwnerUsername + "/" + workspace.Name
+ workspaceName := favIco + " " + workspace.OwnerName + "/" + workspace.Name
return workspaceListRow{
Favorite: workspace.Favorite,
Workspace: workspace,
diff --git a/cli/open.go b/cli/open.go
index 1d47241564fa9..ff950b552a853 100644
--- a/cli/open.go
+++ b/cli/open.go
@@ -403,7 +403,7 @@ func buildVSCodeWorkspaceLink(
) (*url.URL, url.Values) {
qp := url.Values{}
qp.Add("url", clientURL)
- qp.Add("owner", workspace.OwnerUsername)
+ qp.Add("owner", workspace.OwnerName)
qp.Add("workspace", workspace.Name)
qp.Add("agent", workspaceAgent.Name)
@@ -435,7 +435,7 @@ func buildVSCodeWorkspaceDevContainerLink(
qp := url.Values{}
qp.Add("url", clientURL)
- qp.Add("owner", workspace.OwnerUsername)
+ qp.Add("owner", workspace.OwnerName)
qp.Add("workspace", workspace.Name)
qp.Add("agent", workspaceAgent.Name)
qp.Add("devContainerName", containerName)
@@ -596,7 +596,7 @@ func buildAppLinkURL(baseURL *url.URL, workspace codersdk.Workspace, agent coder
u.Path = fmt.Sprintf(
"%s/@%s/%s.%s/apps/%s/",
preferredPathBase,
- workspace.OwnerUsername,
+ workspace.OwnerName,
workspace.Name,
agent.Name,
url.PathEscape(app.Slug),
@@ -606,7 +606,7 @@ func buildAppLinkURL(baseURL *url.URL, workspace codersdk.Workspace, agent coder
u.Path = fmt.Sprintf(
"%s/@%s/%s.%s/terminal",
preferredPathBase,
- workspace.OwnerUsername,
+ workspace.OwnerName,
workspace.Name,
agent.Name,
)
diff --git a/cli/open_internal_test.go b/cli/open_internal_test.go
index 7cee89ee78727..7af4359a56bc2 100644
--- a/cli/open_internal_test.go
+++ b/cli/open_internal_test.go
@@ -91,8 +91,8 @@ func Test_buildAppLinkURL(t *testing.T) {
name: "without subdomain",
baseURL: "https://coder.tld",
workspace: codersdk.Workspace{
- Name: "Test-Workspace",
- OwnerUsername: "username",
+ Name: "Test-Workspace",
+ OwnerName: "username",
},
agent: codersdk.WorkspaceAgent{
Name: "a-workspace-agent",
@@ -108,8 +108,8 @@ func Test_buildAppLinkURL(t *testing.T) {
name: "with command",
baseURL: "https://coder.tld",
workspace: codersdk.Workspace{
- Name: "Test-Workspace",
- OwnerUsername: "username",
+ Name: "Test-Workspace",
+ OwnerName: "username",
},
agent: codersdk.WorkspaceAgent{
Name: "a-workspace-agent",
@@ -123,8 +123,8 @@ func Test_buildAppLinkURL(t *testing.T) {
name: "with subdomain",
baseURL: "ftps://coder.tld",
workspace: codersdk.Workspace{
- Name: "Test-Workspace",
- OwnerUsername: "username",
+ Name: "Test-Workspace",
+ OwnerName: "username",
},
agent: codersdk.WorkspaceAgent{
Name: "a-workspace-agent",
@@ -141,8 +141,8 @@ func Test_buildAppLinkURL(t *testing.T) {
name: "with subdomain, but not apps host",
baseURL: "https://coder.tld",
workspace: codersdk.Workspace{
- Name: "Test-Workspace",
- OwnerUsername: "username",
+ Name: "Test-Workspace",
+ OwnerName: "username",
},
agent: codersdk.WorkspaceAgent{
Name: "a-workspace-agent",
diff --git a/cli/restart_test.go b/cli/restart_test.go
index 46d3c7b675949..d69344435bf28 100644
--- a/cli/restart_test.go
+++ b/cli/restart_test.go
@@ -342,7 +342,7 @@ func TestRestartWithParameters(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
- workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerUsername, workspace.Name, codersdk.WorkspaceOptions{})
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
@@ -393,7 +393,7 @@ func TestRestartWithParameters(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
- workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerUsername, workspace.Name, codersdk.WorkspaceOptions{})
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
diff --git a/cli/schedule.go b/cli/schedule.go
index 02445576f427f..9ade82b9c4a36 100644
--- a/cli/schedule.go
+++ b/cli/schedule.go
@@ -327,7 +327,7 @@ func scheduleListRowFromWorkspace(now time.Time, workspace codersdk.Workspace) s
}
}
return scheduleListRow{
- WorkspaceName: workspace.OwnerUsername + "/" + workspace.Name,
+ WorkspaceName: workspace.OwnerName + "/" + workspace.Name,
StartsAt: autostartDisplay,
StartsNext: nextStartDisplay,
StopsAfter: autostopDisplay,
diff --git a/cli/schedule_test.go b/cli/schedule_test.go
index e702c0a8d65de..60fbf19f4db08 100644
--- a/cli/schedule_test.go
+++ b/cli/schedule_test.go
@@ -71,8 +71,8 @@ func setupTestSchedule(t *testing.T, sched *cron.Schedule) (ownerClient, memberC
// Ensure same order as in CLI output
ws = resp.Workspaces
sort.Slice(ws, func(i, j int) bool {
- a := ws[i].OwnerUsername + "/" + ws[i].Name
- b := ws[j].OwnerUsername + "/" + ws[j].Name
+ a := ws[i].OwnerName + "/" + ws[i].Name
+ b := ws[j].OwnerName + "/" + ws[j].Name
return a < b
})
@@ -102,13 +102,13 @@ func TestScheduleShow(t *testing.T) {
// Then: they should see their own workspaces.
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
- pty.ExpectMatch(ws[0].OwnerUsername + "/" + ws[0].Name)
+ pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: b-owner-ws2 has only autostart enabled.
- pty.ExpectMatch(ws[1].OwnerUsername + "/" + ws[1].Name)
+ pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
})
@@ -123,21 +123,21 @@ func TestScheduleShow(t *testing.T) {
// Then: they should see all workspaces
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
- pty.ExpectMatch(ws[0].OwnerUsername + "/" + ws[0].Name)
+ pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: b-owner-ws2 has only autostart enabled.
- pty.ExpectMatch(ws[1].OwnerUsername + "/" + ws[1].Name)
+ pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
// 3rd workspace: c-member-ws3 has only autostop enabled.
- pty.ExpectMatch(ws[2].OwnerUsername + "/" + ws[2].Name)
+ pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 4th workspace: d-member-ws4 has neither autostart nor autostop enabled.
- pty.ExpectMatch(ws[3].OwnerUsername + "/" + ws[3].Name)
+ pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
})
t.Run("OwnerSearchByName", func(t *testing.T) {
@@ -150,14 +150,14 @@ func TestScheduleShow(t *testing.T) {
// Then: they should see workspaces matching that query
// 2nd workspace: b-owner-ws2 has only autostart enabled.
- pty.ExpectMatch(ws[1].OwnerUsername + "/" + ws[1].Name)
+ pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
})
t.Run("OwnerOneArg", func(t *testing.T) {
// When: owner asks for a specific workspace by name
- inv, root := clitest.New(t, "schedule", "show", ws[2].OwnerUsername+"/"+ws[2].Name)
+ inv, root := clitest.New(t, "schedule", "show", ws[2].OwnerName+"/"+ws[2].Name)
//nolint:gocritic // Testing that owner user sees all
clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
@@ -165,7 +165,7 @@ func TestScheduleShow(t *testing.T) {
// Then: they should see that workspace
// 3rd workspace: c-member-ws3 has only autostop enabled.
- pty.ExpectMatch(ws[2].OwnerUsername + "/" + ws[2].Name)
+ pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
})
@@ -179,11 +179,11 @@ func TestScheduleShow(t *testing.T) {
// Then: they should see their own workspaces
// 1st workspace: c-member-ws3 has only autostop enabled.
- pty.ExpectMatch(ws[2].OwnerUsername + "/" + ws[2].Name)
+ pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: d-member-ws4 has neither autostart nor autostop enabled.
- pty.ExpectMatch(ws[3].OwnerUsername + "/" + ws[3].Name)
+ pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
})
t.Run("MemberAll", func(t *testing.T) {
@@ -200,11 +200,11 @@ func TestScheduleShow(t *testing.T) {
// Then: they should only see their own
// 1st workspace: c-member-ws3 has only autostop enabled.
- pty.ExpectMatch(ws[2].OwnerUsername + "/" + ws[2].Name)
+ pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: d-member-ws4 has neither autostart nor autostop enabled.
- pty.ExpectMatch(ws[3].OwnerUsername + "/" + ws[3].Name)
+ pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
})
t.Run("JSON", func(t *testing.T) {
@@ -231,25 +231,25 @@ func TestScheduleShow(t *testing.T) {
return a < b
})
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
- assert.Equal(t, ws[0].OwnerUsername+"/"+ws[0].Name, parsed[0]["workspace"])
+ assert.Equal(t, ws[0].OwnerName+"/"+ws[0].Name, parsed[0]["workspace"])
assert.Equal(t, sched.Humanize(), parsed[0]["starts_at"])
assert.Equal(t, sched.Next(now).In(loc).Format(time.RFC3339), parsed[0]["starts_next"])
assert.Equal(t, "8h", parsed[0]["stops_after"])
assert.Equal(t, ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339), parsed[0]["stops_next"])
// 2nd workspace: b-owner-ws2 has only autostart enabled.
- assert.Equal(t, ws[1].OwnerUsername+"/"+ws[1].Name, parsed[1]["workspace"])
+ assert.Equal(t, ws[1].OwnerName+"/"+ws[1].Name, parsed[1]["workspace"])
assert.Equal(t, sched.Humanize(), parsed[1]["starts_at"])
assert.Equal(t, sched.Next(now).In(loc).Format(time.RFC3339), parsed[1]["starts_next"])
assert.Empty(t, parsed[1]["stops_after"])
assert.Empty(t, parsed[1]["stops_next"])
// 3rd workspace: c-member-ws3 has only autostop enabled.
- assert.Equal(t, ws[2].OwnerUsername+"/"+ws[2].Name, parsed[2]["workspace"])
+ assert.Equal(t, ws[2].OwnerName+"/"+ws[2].Name, parsed[2]["workspace"])
assert.Empty(t, parsed[2]["starts_at"])
assert.Empty(t, parsed[2]["starts_next"])
assert.Equal(t, "8h", parsed[2]["stops_after"])
assert.Equal(t, ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339), parsed[2]["stops_next"])
// 4th workspace: d-member-ws4 has neither autostart nor autostop enabled.
- assert.Equal(t, ws[3].OwnerUsername+"/"+ws[3].Name, parsed[3]["workspace"])
+ assert.Equal(t, ws[3].OwnerName+"/"+ws[3].Name, parsed[3]["workspace"])
assert.Empty(t, parsed[3]["starts_at"])
assert.Empty(t, parsed[3]["starts_next"])
assert.Empty(t, parsed[3]["stops_after"])
@@ -272,7 +272,7 @@ func TestScheduleModify(t *testing.T) {
t.Run("SetStart", func(t *testing.T) {
// When: we set the start schedule
inv, root := clitest.New(t,
- "schedule", "start", ws[3].OwnerUsername+"/"+ws[3].Name, "7:30AM", "Mon-Fri", "Europe/Dublin",
+ "schedule", "start", ws[3].OwnerName+"/"+ws[3].Name, "7:30AM", "Mon-Fri", "Europe/Dublin",
)
//nolint:gocritic // this workspace is not owned by the same user
clitest.SetupConfig(t, ownerClient, root)
@@ -280,7 +280,7 @@ func TestScheduleModify(t *testing.T) {
require.NoError(t, inv.Run())
// Then: the updated schedule should be shown
- pty.ExpectMatch(ws[3].OwnerUsername + "/" + ws[3].Name)
+ pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
})
@@ -288,7 +288,7 @@ func TestScheduleModify(t *testing.T) {
t.Run("SetStop", func(t *testing.T) {
// When: we set the stop schedule
inv, root := clitest.New(t,
- "schedule", "stop", ws[2].OwnerUsername+"/"+ws[2].Name, "8h30m",
+ "schedule", "stop", ws[2].OwnerName+"/"+ws[2].Name, "8h30m",
)
//nolint:gocritic // this workspace is not owned by the same user
clitest.SetupConfig(t, ownerClient, root)
@@ -296,7 +296,7 @@ func TestScheduleModify(t *testing.T) {
require.NoError(t, inv.Run())
// Then: the updated schedule should be shown
- pty.ExpectMatch(ws[2].OwnerUsername + "/" + ws[2].Name)
+ pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h30m")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
})
@@ -304,7 +304,7 @@ func TestScheduleModify(t *testing.T) {
t.Run("UnsetStart", func(t *testing.T) {
// When: we unset the start schedule
inv, root := clitest.New(t,
- "schedule", "start", ws[1].OwnerUsername+"/"+ws[1].Name, "manual",
+ "schedule", "start", ws[1].OwnerName+"/"+ws[1].Name, "manual",
)
//nolint:gocritic // this workspace is owned by owner
clitest.SetupConfig(t, ownerClient, root)
@@ -312,13 +312,13 @@ func TestScheduleModify(t *testing.T) {
require.NoError(t, inv.Run())
// Then: the updated schedule should be shown
- pty.ExpectMatch(ws[1].OwnerUsername + "/" + ws[1].Name)
+ pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
})
t.Run("UnsetStop", func(t *testing.T) {
// When: we unset the stop schedule
inv, root := clitest.New(t,
- "schedule", "stop", ws[0].OwnerUsername+"/"+ws[0].Name, "manual",
+ "schedule", "stop", ws[0].OwnerName+"/"+ws[0].Name, "manual",
)
//nolint:gocritic // this workspace is owned by owner
clitest.SetupConfig(t, ownerClient, root)
@@ -326,7 +326,7 @@ func TestScheduleModify(t *testing.T) {
require.NoError(t, inv.Run())
// Then: the updated schedule should be shown
- pty.ExpectMatch(ws[0].OwnerUsername + "/" + ws[0].Name)
+ pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
})
}
@@ -359,7 +359,7 @@ func TestScheduleOverride(t *testing.T) {
// When: we override the stop schedule
inv, root := clitest.New(t,
- "schedule", tt.command, ws[0].OwnerUsername+"/"+ws[0].Name, "10h",
+ "schedule", tt.command, ws[0].OwnerName+"/"+ws[0].Name, "10h",
)
clitest.SetupConfig(t, ownerClient, root)
@@ -367,7 +367,7 @@ func TestScheduleOverride(t *testing.T) {
require.NoError(t, inv.Run())
// Then: the updated schedule should be shown
- pty.ExpectMatch(ws[0].OwnerUsername + "/" + ws[0].Name)
+ pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
diff --git a/cli/ssh.go b/cli/ssh.go
index f16cbba9316b7..51f53e10bcbd2 100644
--- a/cli/ssh.go
+++ b/cli/ssh.go
@@ -305,7 +305,7 @@ func (r *RootCmd) ssh() *serpent.Command {
return xerrors.Errorf("get agent connection info: %w", err)
}
coderConnectHost := fmt.Sprintf("%s.%s.%s.%s",
- workspaceAgent.Name, workspace.Name, workspace.OwnerUsername, connInfo.HostnameSuffix)
+ workspaceAgent.Name, workspace.Name, workspace.OwnerName, connInfo.HostnameSuffix)
exists, _ := workspacesdk.ExistsViaCoderConnect(ctx, coderConnectHost)
if exists {
defer cancel()
@@ -1022,7 +1022,7 @@ func verifyWorkspaceOutdated(client *codersdk.Client, workspace codersdk.Workspa
// Build the user workspace link which navigates to the Coder web UI.
func buildWorkspaceLink(serverURL *url.URL, workspace codersdk.Workspace) *url.URL {
- return serverURL.ResolveReference(&url.URL{Path: fmt.Sprintf("@%s/%s", workspace.OwnerUsername, workspace.Name)})
+ return serverURL.ResolveReference(&url.URL{Path: fmt.Sprintf("@%s/%s", workspace.OwnerName, workspace.Name)})
}
// runLocal runs a command on the local machine.
diff --git a/cli/ssh_internal_test.go b/cli/ssh_internal_test.go
index c445d4fadf44b..003bc697a4052 100644
--- a/cli/ssh_internal_test.go
+++ b/cli/ssh_internal_test.go
@@ -25,7 +25,7 @@ import (
)
const (
- fakeOwnerUsername = "fake-owner-name"
+ fakeOwnerName = "fake-owner-name"
fakeServerURL = "https://fake-foo-url"
fakeWorkspaceName = "fake-workspace-name"
)
@@ -41,7 +41,7 @@ func TestVerifyWorkspaceOutdated(t *testing.T) {
t.Run("Up-to-date", func(t *testing.T) {
t.Parallel()
- workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerUsername: fakeOwnerUsername}
+ workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerName: fakeOwnerName}
_, outdated := verifyWorkspaceOutdated(&client, workspace)
@@ -50,7 +50,7 @@ func TestVerifyWorkspaceOutdated(t *testing.T) {
t.Run("Outdated", func(t *testing.T) {
t.Parallel()
- workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerUsername: fakeOwnerUsername, Outdated: true}
+ workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerName: fakeOwnerName, Outdated: true}
updateWorkspaceBanner, outdated := verifyWorkspaceOutdated(&client, workspace)
@@ -65,10 +65,10 @@ func TestBuildWorkspaceLink(t *testing.T) {
serverURL, err := url.Parse(fakeServerURL)
require.NoError(t, err)
- workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerUsername: fakeOwnerUsername}
+ workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerName: fakeOwnerName}
workspaceLink := buildWorkspaceLink(serverURL, workspace)
- assert.Equal(t, workspaceLink.String(), fakeServerURL+"/@"+fakeOwnerUsername+"/"+fakeWorkspaceName)
+ assert.Equal(t, workspaceLink.String(), fakeServerURL+"/@"+fakeOwnerName+"/"+fakeWorkspaceName)
}
func TestCloserStack_Mainline(t *testing.T) {
diff --git a/cli/start_test.go b/cli/start_test.go
index d15dcbd356711..29fa4cdb46e5f 100644
--- a/cli/start_test.go
+++ b/cli/start_test.go
@@ -148,7 +148,7 @@ func TestStart(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
- workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerUsername, workspace.Name, codersdk.WorkspaceOptions{})
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
@@ -191,7 +191,7 @@ func TestStart(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
- workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerUsername, workspace.Name, codersdk.WorkspaceOptions{})
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
@@ -247,7 +247,7 @@ func TestStartWithParameters(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
- workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerUsername, workspace.Name, codersdk.WorkspaceOptions{})
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
@@ -302,7 +302,7 @@ func TestStartWithParameters(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
- workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerUsername, workspace.Name, codersdk.WorkspaceOptions{})
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden
index 80180c6262b21..d8e6a306cabcf 100644
--- a/cli/testdata/coder_list_--output_json.golden
+++ b/cli/testdata/coder_list_--output_json.golden
@@ -4,7 +4,7 @@
"created_at": "====[timestamp]=====",
"updated_at": "====[timestamp]=====",
"owner_id": "==========[first user ID]===========",
- "owner_username": "testuser",
+ "owner_name": "testuser",
"owner_avatar_url": "",
"organization_id": "===========[first org ID]===========",
"organization_name": "coder",
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 0cc518d09399f..5e8b8d6afa89e 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -16095,6 +16095,23 @@ const docTemplate = `{
"ephemeral": {
"type": "boolean"
},
+ "form_type": {
+ "description": "FormType has an enum value of empty string, ` + "`" + `\"\"` + "`" + `.\nKeep the leading comma in the enums struct tag.",
+ "type": "string",
+ "enum": [
+ "",
+ "radio",
+ "dropdown",
+ "input",
+ "textarea",
+ "slider",
+ "checkbox",
+ "switch",
+ "tag-select",
+ "multi-select",
+ "error"
+ ]
+ },
"icon": {
"type": "string"
},
@@ -17002,9 +17019,7 @@ const docTemplate = `{
"format": "uuid"
},
"owner_name": {
- "type": "string"
- },
- "owner_username": {
+ "description": "OwnerName is the username of the owner of the workspace.",
"type": "string"
},
"template_active_version_id": {
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 0fdc08d22e228..ef32dcd24f375 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -14662,6 +14662,23 @@
"ephemeral": {
"type": "boolean"
},
+ "form_type": {
+ "description": "FormType has an enum value of empty string, `\"\"`.\nKeep the leading comma in the enums struct tag.",
+ "type": "string",
+ "enum": [
+ "",
+ "radio",
+ "dropdown",
+ "input",
+ "textarea",
+ "slider",
+ "checkbox",
+ "switch",
+ "tag-select",
+ "multi-select",
+ "error"
+ ]
+ },
"icon": {
"type": "string"
},
@@ -15507,9 +15524,7 @@
"format": "uuid"
},
"owner_name": {
- "type": "string"
- },
- "owner_username": {
+ "description": "OwnerName is the username of the owner of the workspace.",
"type": "string"
},
"template_active_version_id": {
diff --git a/coderd/audit.go b/coderd/audit.go
index ee647fba2f39b..63b6e49ebb05a 100644
--- a/coderd/audit.go
+++ b/coderd/audit.go
@@ -462,7 +462,7 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit
if getWorkspaceErr != nil {
return ""
}
- return fmt.Sprintf("/@%s/%s", workspace.OwnerUsername, workspace.Name)
+ return fmt.Sprintf("/@%s/%s", workspace.OwnerName, workspace.Name)
case database.ResourceTypeWorkspaceApp:
if additionalFields.WorkspaceOwner != "" && additionalFields.WorkspaceName != "" {
@@ -472,7 +472,7 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit
if getWorkspaceErr != nil {
return ""
}
- return fmt.Sprintf("/@%s/%s", workspace.OwnerUsername, workspace.Name)
+ return fmt.Sprintf("/@%s/%s", workspace.OwnerName, workspace.Name)
case database.ResourceTypeOauth2ProviderApp:
return fmt.Sprintf("/deployment/oauth2-provider/apps/%s", alog.AuditLog.ResourceID)
diff --git a/coderd/audit_test.go b/coderd/audit_test.go
index c61ded694171d..18bcd78b38807 100644
--- a/coderd/audit_test.go
+++ b/coderd/audit_test.go
@@ -139,7 +139,7 @@ func TestAuditLogs(t *testing.T) {
require.NoError(t, err)
buildNumberString := strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10)
require.Equal(t, auditLogs.AuditLogs[0].ResourceLink, fmt.Sprintf("/@%s/%s/builds/%s",
- workspace.OwnerUsername, workspace.Name, buildNumberString))
+ workspace.OwnerName, workspace.Name, buildNumberString))
})
t.Run("Organization", func(t *testing.T) {
diff --git a/coderd/coderd.go b/coderd/coderd.go
index 37e7d22a6d080..0aab4b26262ea 100644
--- a/coderd/coderd.go
+++ b/coderd/coderd.go
@@ -1532,17 +1532,19 @@ func New(options *Options) *API {
// Add CSP headers to all static assets and pages. CSP headers only affect
// browsers, so these don't make sense on api routes.
- cspMW := httpmw.CSPHeaders(options.Telemetry.Enabled(), func() []string {
- if api.DeploymentValues.Dangerous.AllowAllCors {
- // In this mode, allow all external requests
- return []string{"*"}
- }
- if f := api.WorkspaceProxyHostsFn.Load(); f != nil {
- return (*f)()
- }
- // By default we do not add extra websocket connections to the CSP
- return []string{}
- }, additionalCSPHeaders)
+ cspMW := httpmw.CSPHeaders(
+ api.Experiments,
+ options.Telemetry.Enabled(), func() []string {
+ if api.DeploymentValues.Dangerous.AllowAllCors {
+ // In this mode, allow all external requests
+ return []string{"*"}
+ }
+ if f := api.WorkspaceProxyHostsFn.Load(); f != nil {
+ return (*f)()
+ }
+ // By default we do not add extra websocket connections to the CSP
+ return []string{}
+ }, additionalCSPHeaders)
// Static file handler must be wrapped with HSTS handler if the
// StrictTransportSecurityAge is set. We only need to set this header on
diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go
index f30cec2c84830..40b1423a0f730 100644
--- a/coderd/database/db2sdk/db2sdk.go
+++ b/coderd/database/db2sdk/db2sdk.go
@@ -92,13 +92,13 @@ func WorkspaceBuildParameters(params []database.WorkspaceBuildParameter) []coder
}
func TemplateVersionParameters(params []database.TemplateVersionParameter) ([]codersdk.TemplateVersionParameter, error) {
- out := make([]codersdk.TemplateVersionParameter, len(params))
- var err error
- for i, p := range params {
- out[i], err = TemplateVersionParameter(p)
+ out := make([]codersdk.TemplateVersionParameter, 0, len(params))
+ for _, p := range params {
+ np, err := TemplateVersionParameter(p)
if err != nil {
return nil, xerrors.Errorf("convert template version parameter %q: %w", p.Name, err)
}
+ out = append(out, np)
}
return out, nil
@@ -131,6 +131,7 @@ func TemplateVersionParameter(param database.TemplateVersionParameter) (codersdk
Description: param.Description,
DescriptionPlaintext: descriptionPlaintext,
Type: param.Type,
+ FormType: string(param.FormType),
Mutable: param.Mutable,
DefaultValue: param.DefaultValue,
Icon: param.Icon,
@@ -293,7 +294,8 @@ func templateVersionParameterOptions(rawOptions json.RawMessage) ([]codersdk.Tem
if err != nil {
return nil, err
}
- var options []codersdk.TemplateVersionParameterOption
+
+ options := make([]codersdk.TemplateVersionParameterOption, 0)
for _, option := range protoOptions {
options = append(options, codersdk.TemplateVersionParameterOption{
Name: option.Name,
diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go
index 8a6f312e650a8..c85db83a2adc9 100644
--- a/coderd/database/dbgen/dbgen.go
+++ b/coderd/database/dbgen/dbgen.go
@@ -992,6 +992,7 @@ func TemplateVersionParameter(t testing.TB, db database.Store, orig database.Tem
Name: takeFirst(orig.Name, testutil.GetRandomName(t)),
Description: takeFirst(orig.Description, testutil.GetRandomName(t)),
Type: takeFirst(orig.Type, "string"),
+ FormType: orig.FormType, // empty string is ok!
Mutable: takeFirst(orig.Mutable, false),
DefaultValue: takeFirst(orig.DefaultValue, testutil.GetRandomName(t)),
Icon: takeFirst(orig.Icon, testutil.GetRandomName(t)),
diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go
index 0833fa1677c3b..69bb3a540eccf 100644
--- a/coderd/database/dbmem/dbmem.go
+++ b/coderd/database/dbmem/dbmem.go
@@ -9393,6 +9393,7 @@ func (q *FakeQuerier) InsertTemplateVersionParameter(_ context.Context, arg data
DisplayName: arg.DisplayName,
Description: arg.Description,
Type: arg.Type,
+ FormType: arg.FormType,
Mutable: arg.Mutable,
DefaultValue: arg.DefaultValue,
Icon: arg.Icon,
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index acb9780b82ea6..22a0b3d5a8adc 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -132,6 +132,22 @@ CREATE TYPE parameter_destination_scheme AS ENUM (
'provisioner_variable'
);
+CREATE TYPE parameter_form_type AS ENUM (
+ '',
+ 'error',
+ 'radio',
+ 'dropdown',
+ 'input',
+ 'textarea',
+ 'slider',
+ 'checkbox',
+ 'switch',
+ 'tag-select',
+ 'multi-select'
+);
+
+COMMENT ON TYPE parameter_form_type IS 'Enum set should match the terraform provider set. This is defined as future form_types are not supported, and should be rejected. Always include the empty string for using the default form type.';
+
CREATE TYPE parameter_scope AS ENUM (
'template',
'import_job',
@@ -1434,6 +1450,7 @@ CREATE TABLE template_version_parameters (
display_name text DEFAULT ''::text NOT NULL,
display_order integer DEFAULT 0 NOT NULL,
ephemeral boolean DEFAULT false NOT NULL,
+ form_type parameter_form_type DEFAULT ''::parameter_form_type NOT NULL,
CONSTRAINT validation_monotonic_order CHECK ((validation_monotonic = ANY (ARRAY['increasing'::text, 'decreasing'::text, ''::text])))
);
@@ -1469,6 +1486,8 @@ COMMENT ON COLUMN template_version_parameters.display_order IS 'Specifies the or
COMMENT ON COLUMN template_version_parameters.ephemeral IS 'The value of an ephemeral parameter will not be preserved between consecutive workspace builds.';
+COMMENT ON COLUMN template_version_parameters.form_type IS 'Specify what form_type should be used to render the parameter in the UI. Unsupported values are rejected.';
+
CREATE TABLE template_version_preset_parameters (
id uuid DEFAULT gen_random_uuid() NOT NULL,
template_version_preset_id uuid NOT NULL,
diff --git a/coderd/database/migrations/000333_parameter_form_type.down.sql b/coderd/database/migrations/000333_parameter_form_type.down.sql
new file mode 100644
index 0000000000000..906d9c0cba610
--- /dev/null
+++ b/coderd/database/migrations/000333_parameter_form_type.down.sql
@@ -0,0 +1,2 @@
+ALTER TABLE template_version_parameters DROP COLUMN form_type;
+DROP TYPE parameter_form_type;
diff --git a/coderd/database/migrations/000333_parameter_form_type.up.sql b/coderd/database/migrations/000333_parameter_form_type.up.sql
new file mode 100644
index 0000000000000..fce755eb5193e
--- /dev/null
+++ b/coderd/database/migrations/000333_parameter_form_type.up.sql
@@ -0,0 +1,11 @@
+CREATE TYPE parameter_form_type AS ENUM ('', 'error', 'radio', 'dropdown', 'input', 'textarea', 'slider', 'checkbox', 'switch', 'tag-select', 'multi-select');
+COMMENT ON TYPE parameter_form_type
+ IS 'Enum set should match the terraform provider set. This is defined as future form_types are not supported, and should be rejected. '
+ 'Always include the empty string for using the default form type.';
+
+-- Intentionally leaving the default blank. The provisioner will not re-run any
+-- imports to backfill these values. Missing values just have to be handled.
+ALTER TABLE template_version_parameters ADD COLUMN form_type parameter_form_type NOT NULL DEFAULT '';
+
+COMMENT ON COLUMN template_version_parameters.form_type
+ IS 'Specify what form_type should be used to render the parameter in the UI. Unsupported values are rejected.';
diff --git a/coderd/database/models.go b/coderd/database/models.go
index 367c46b5b71d7..69ae70b6c3bd3 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -1108,6 +1108,92 @@ func AllParameterDestinationSchemeValues() []ParameterDestinationScheme {
}
}
+// Enum set should match the terraform provider set. This is defined as future form_types are not supported, and should be rejected. Always include the empty string for using the default form type.
+type ParameterFormType string
+
+const (
+ ParameterFormTypeValue0 ParameterFormType = ""
+ ParameterFormTypeError ParameterFormType = "error"
+ ParameterFormTypeRadio ParameterFormType = "radio"
+ ParameterFormTypeDropdown ParameterFormType = "dropdown"
+ ParameterFormTypeInput ParameterFormType = "input"
+ ParameterFormTypeTextarea ParameterFormType = "textarea"
+ ParameterFormTypeSlider ParameterFormType = "slider"
+ ParameterFormTypeCheckbox ParameterFormType = "checkbox"
+ ParameterFormTypeSwitch ParameterFormType = "switch"
+ ParameterFormTypeTagSelect ParameterFormType = "tag-select"
+ ParameterFormTypeMultiSelect ParameterFormType = "multi-select"
+)
+
+func (e *ParameterFormType) Scan(src interface{}) error {
+ switch s := src.(type) {
+ case []byte:
+ *e = ParameterFormType(s)
+ case string:
+ *e = ParameterFormType(s)
+ default:
+ return fmt.Errorf("unsupported scan type for ParameterFormType: %T", src)
+ }
+ return nil
+}
+
+type NullParameterFormType struct {
+ ParameterFormType ParameterFormType `json:"parameter_form_type"`
+ Valid bool `json:"valid"` // Valid is true if ParameterFormType is not NULL
+}
+
+// Scan implements the Scanner interface.
+func (ns *NullParameterFormType) Scan(value interface{}) error {
+ if value == nil {
+ ns.ParameterFormType, ns.Valid = "", false
+ return nil
+ }
+ ns.Valid = true
+ return ns.ParameterFormType.Scan(value)
+}
+
+// Value implements the driver Valuer interface.
+func (ns NullParameterFormType) Value() (driver.Value, error) {
+ if !ns.Valid {
+ return nil, nil
+ }
+ return string(ns.ParameterFormType), nil
+}
+
+func (e ParameterFormType) Valid() bool {
+ switch e {
+ case ParameterFormTypeValue0,
+ ParameterFormTypeError,
+ ParameterFormTypeRadio,
+ ParameterFormTypeDropdown,
+ ParameterFormTypeInput,
+ ParameterFormTypeTextarea,
+ ParameterFormTypeSlider,
+ ParameterFormTypeCheckbox,
+ ParameterFormTypeSwitch,
+ ParameterFormTypeTagSelect,
+ ParameterFormTypeMultiSelect:
+ return true
+ }
+ return false
+}
+
+func AllParameterFormTypeValues() []ParameterFormType {
+ return []ParameterFormType{
+ ParameterFormTypeValue0,
+ ParameterFormTypeError,
+ ParameterFormTypeRadio,
+ ParameterFormTypeDropdown,
+ ParameterFormTypeInput,
+ ParameterFormTypeTextarea,
+ ParameterFormTypeSlider,
+ ParameterFormTypeCheckbox,
+ ParameterFormTypeSwitch,
+ ParameterFormTypeTagSelect,
+ ParameterFormTypeMultiSelect,
+ }
+}
+
type ParameterScope string
const (
@@ -3308,6 +3394,8 @@ type TemplateVersionParameter struct {
DisplayOrder int32 `db:"display_order" json:"display_order"`
// The value of an ephemeral parameter will not be preserved between consecutive workspace builds.
Ephemeral bool `db:"ephemeral" json:"ephemeral"`
+ // Specify what form_type should be used to render the parameter in the UI. Unsupported values are rejected.
+ FormType ParameterFormType `db:"form_type" json:"form_type"`
}
type TemplateVersionPreset struct {
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index ed8551691efd6..eec91c7586d61 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -11178,7 +11178,7 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT
}
const getTemplateVersionParameters = `-- name: GetTemplateVersionParameters :many
-SELECT template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic, required, display_name, display_order, ephemeral FROM template_version_parameters WHERE template_version_id = $1 ORDER BY display_order ASC, LOWER(name) ASC
+SELECT template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic, required, display_name, display_order, ephemeral, form_type FROM template_version_parameters WHERE template_version_id = $1 ORDER BY display_order ASC, LOWER(name) ASC
`
func (q *sqlQuerier) GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error) {
@@ -11208,6 +11208,7 @@ func (q *sqlQuerier) GetTemplateVersionParameters(ctx context.Context, templateV
&i.DisplayName,
&i.DisplayOrder,
&i.Ephemeral,
+ &i.FormType,
); err != nil {
return nil, err
}
@@ -11229,6 +11230,7 @@ INSERT INTO
name,
description,
type,
+ form_type,
mutable,
default_value,
icon,
@@ -11261,28 +11263,30 @@ VALUES
$14,
$15,
$16,
- $17
- ) RETURNING template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic, required, display_name, display_order, ephemeral
+ $17,
+ $18
+ ) RETURNING template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic, required, display_name, display_order, ephemeral, form_type
`
type InsertTemplateVersionParameterParams struct {
- TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
- Name string `db:"name" json:"name"`
- Description string `db:"description" json:"description"`
- Type string `db:"type" json:"type"`
- Mutable bool `db:"mutable" json:"mutable"`
- DefaultValue string `db:"default_value" json:"default_value"`
- Icon string `db:"icon" json:"icon"`
- Options json.RawMessage `db:"options" json:"options"`
- ValidationRegex string `db:"validation_regex" json:"validation_regex"`
- ValidationMin sql.NullInt32 `db:"validation_min" json:"validation_min"`
- ValidationMax sql.NullInt32 `db:"validation_max" json:"validation_max"`
- ValidationError string `db:"validation_error" json:"validation_error"`
- ValidationMonotonic string `db:"validation_monotonic" json:"validation_monotonic"`
- Required bool `db:"required" json:"required"`
- DisplayName string `db:"display_name" json:"display_name"`
- DisplayOrder int32 `db:"display_order" json:"display_order"`
- Ephemeral bool `db:"ephemeral" json:"ephemeral"`
+ TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
+ Name string `db:"name" json:"name"`
+ Description string `db:"description" json:"description"`
+ Type string `db:"type" json:"type"`
+ FormType ParameterFormType `db:"form_type" json:"form_type"`
+ Mutable bool `db:"mutable" json:"mutable"`
+ DefaultValue string `db:"default_value" json:"default_value"`
+ Icon string `db:"icon" json:"icon"`
+ Options json.RawMessage `db:"options" json:"options"`
+ ValidationRegex string `db:"validation_regex" json:"validation_regex"`
+ ValidationMin sql.NullInt32 `db:"validation_min" json:"validation_min"`
+ ValidationMax sql.NullInt32 `db:"validation_max" json:"validation_max"`
+ ValidationError string `db:"validation_error" json:"validation_error"`
+ ValidationMonotonic string `db:"validation_monotonic" json:"validation_monotonic"`
+ Required bool `db:"required" json:"required"`
+ DisplayName string `db:"display_name" json:"display_name"`
+ DisplayOrder int32 `db:"display_order" json:"display_order"`
+ Ephemeral bool `db:"ephemeral" json:"ephemeral"`
}
func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) {
@@ -11291,6 +11295,7 @@ func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg Ins
arg.Name,
arg.Description,
arg.Type,
+ arg.FormType,
arg.Mutable,
arg.DefaultValue,
arg.Icon,
@@ -11324,6 +11329,7 @@ func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg Ins
&i.DisplayName,
&i.DisplayOrder,
&i.Ephemeral,
+ &i.FormType,
)
return i, err
}
diff --git a/coderd/database/queries/templateversionparameters.sql b/coderd/database/queries/templateversionparameters.sql
index 039070b8a3515..549d9eafa1899 100644
--- a/coderd/database/queries/templateversionparameters.sql
+++ b/coderd/database/queries/templateversionparameters.sql
@@ -5,6 +5,7 @@ INSERT INTO
name,
description,
type,
+ form_type,
mutable,
default_value,
icon,
@@ -37,7 +38,8 @@ VALUES
$14,
$15,
$16,
- $17
+ $17,
+ $18
) RETURNING *;
-- name: GetTemplateVersionParameters :many
diff --git a/coderd/httpmw/csp.go b/coderd/httpmw/csp.go
index e6864b7448c41..afc19ddaf0c1f 100644
--- a/coderd/httpmw/csp.go
+++ b/coderd/httpmw/csp.go
@@ -4,6 +4,8 @@ import (
"fmt"
"net/http"
"strings"
+
+ "github.com/coder/coder/v2/codersdk"
)
// cspDirectives is a map of all csp fetch directives to their values.
@@ -37,6 +39,7 @@ const (
CSPDirectiveFormAction CSPFetchDirective = "form-action"
CSPDirectiveMediaSrc CSPFetchDirective = "media-src"
CSPFrameAncestors CSPFetchDirective = "frame-ancestors"
+ CSPFrameSource CSPFetchDirective = "frame-src"
CSPDirectiveWorkerSrc CSPFetchDirective = "worker-src"
)
@@ -55,7 +58,7 @@ const (
// Example: https://github.com/coder/coder/issues/15118
//
//nolint:revive
-func CSPHeaders(telemetry bool, websocketHosts func() []string, staticAdditions map[CSPFetchDirective][]string) func(next http.Handler) http.Handler {
+func CSPHeaders(experiments codersdk.Experiments, telemetry bool, websocketHosts func() []string, staticAdditions map[CSPFetchDirective][]string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Content-Security-Policy disables loading certain content types and can prevent XSS injections.
@@ -88,13 +91,21 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string, staticAdditions
CSPDirectiveMediaSrc: {"'self'"},
// Report all violations back to the server to log
CSPDirectiveReportURI: {"/api/v2/csp/reports"},
- CSPFrameAncestors: {"'none'"},
// Only scripts can manipulate the dom. This prevents someone from
// naming themselves something like ''.
// "require-trusted-types-for" : []string{"'script'"},
}
+ if experiments.Enabled(codersdk.ExperimentAITasks) {
+ // AI tasks use iframe embeds of local apps.
+ // TODO: Handle region domains too, not just path based apps
+ cspSrcs.Append(CSPFrameAncestors, `'self'`)
+ cspSrcs.Append(CSPFrameSource, `'self'`)
+ } else {
+ cspSrcs.Append(CSPFrameAncestors, `'none'`)
+ }
+
if telemetry {
// If telemetry is enabled, we report to coder.com.
cspSrcs.Append(CSPDirectiveConnectSrc, "https://coder.com")
diff --git a/coderd/httpmw/csp_test.go b/coderd/httpmw/csp_test.go
index c5000d3a29370..bef6ab196eb6e 100644
--- a/coderd/httpmw/csp_test.go
+++ b/coderd/httpmw/csp_test.go
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
func TestCSPConnect(t *testing.T) {
@@ -20,7 +21,7 @@ func TestCSPConnect(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/", nil)
rw := httptest.NewRecorder()
- httpmw.CSPHeaders(false, func() []string {
+ httpmw.CSPHeaders(codersdk.Experiments{}, false, func() []string {
return expected
}, map[httpmw.CSPFetchDirective][]string{
httpmw.CSPDirectiveMediaSrc: expectedMedia,
diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go
index b220cae90b629..111f4185d3910 100644
--- a/coderd/provisionerdserver/provisionerdserver.go
+++ b/coderd/provisionerdserver/provisionerdserver.go
@@ -28,6 +28,7 @@ import (
protobuf "google.golang.org/protobuf/proto"
"cdr.dev/slog"
+ "github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk/drpcsdk"
@@ -1453,12 +1454,24 @@ func (s *server) completeTemplateImportJob(ctx context.Context, job database.Pro
}
}
+ pft, err := sdkproto.ProviderFormType(richParameter.FormType)
+ if err != nil {
+ return xerrors.Errorf("parameter %q: %w", richParameter.Name, err)
+ }
+
+ dft := database.ParameterFormType(pft)
+ if !dft.Valid() {
+ list := strings.Join(slice.ToStrings(database.AllParameterFormTypeValues()), ", ")
+ return xerrors.Errorf("parameter %q field 'form_type' not valid, currently supported: %s", richParameter.Name, list)
+ }
+
_, err = db.InsertTemplateVersionParameter(ctx, database.InsertTemplateVersionParameterParams{
TemplateVersionID: input.TemplateVersionID,
Name: richParameter.Name,
DisplayName: richParameter.DisplayName,
Description: richParameter.Description,
Type: richParameter.Type,
+ FormType: dft,
Mutable: richParameter.Mutable,
DefaultValue: richParameter.DefaultValue,
Icon: richParameter.Icon,
diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go
index 087f6aa21a5d7..1756aa68e15fc 100644
--- a/coderd/provisionerdserver/provisionerdserver_test.go
+++ b/coderd/provisionerdserver/provisionerdserver_test.go
@@ -1384,6 +1384,60 @@ func TestCompleteJob(t *testing.T) {
})
})
+ t.Run("WorkspaceBuild_BadFormType", func(t *testing.T) {
+ t.Parallel()
+ srv, db, _, pd := setup(t, false, &overrides{})
+ jobID := uuid.New()
+ versionID := uuid.New()
+ err := db.InsertTemplateVersion(ctx, database.InsertTemplateVersionParams{
+ ID: versionID,
+ JobID: jobID,
+ OrganizationID: pd.OrganizationID,
+ })
+ require.NoError(t, err)
+ job, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
+ ID: jobID,
+ Provisioner: database.ProvisionerTypeEcho,
+ Input: []byte(`{"template_version_id": "` + versionID.String() + `"}`),
+ StorageMethod: database.ProvisionerStorageMethodFile,
+ Type: database.ProvisionerJobTypeWorkspaceBuild,
+ OrganizationID: pd.OrganizationID,
+ })
+ require.NoError(t, err)
+ _, err = db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
+ OrganizationID: pd.OrganizationID,
+ WorkerID: uuid.NullUUID{
+ UUID: pd.ID,
+ Valid: true,
+ },
+ Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
+ })
+ require.NoError(t, err)
+
+ _, err = srv.CompleteJob(ctx, &proto.CompletedJob{
+ JobId: job.ID.String(),
+ Type: &proto.CompletedJob_TemplateImport_{
+ TemplateImport: &proto.CompletedJob_TemplateImport{
+ StartResources: []*sdkproto.Resource{{
+ Name: "hello",
+ Type: "aws_instance",
+ }},
+ StopResources: []*sdkproto.Resource{},
+ RichParameters: []*sdkproto.RichParameter{
+ {
+ Name: "parameter",
+ Type: "string",
+ FormType: -1,
+ },
+ },
+ Plan: []byte("{}"),
+ },
+ },
+ })
+ require.Error(t, err)
+ require.ErrorContains(t, err, "unsupported form type")
+ })
+
t.Run("TemplateImport_MissingGitAuth", func(t *testing.T) {
t.Parallel()
srv, db, _, pd := setup(t, false, &overrides{})
diff --git a/coderd/templateversions.go b/coderd/templateversions.go
index 8dd523374d69f..d79f86f1f6626 100644
--- a/coderd/templateversions.go
+++ b/coderd/templateversions.go
@@ -31,14 +31,12 @@ import (
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
- "github.com/coder/coder/v2/coderd/render"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/examples"
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
"github.com/coder/coder/v2/provisionersdk"
- sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
)
// @Summary Get template version by ID
@@ -307,7 +305,7 @@ func (api *API) templateVersionRichParameters(rw http.ResponseWriter, r *http.Re
return
}
- templateVersionParameters, err := convertTemplateVersionParameters(dbTemplateVersionParameters)
+ templateVersionParameters, err := db2sdk.TemplateVersionParameters(dbTemplateVersionParameters)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting template version parameter.",
@@ -1869,67 +1867,6 @@ func convertTemplateVersion(version database.TemplateVersion, job codersdk.Provi
}
}
-func convertTemplateVersionParameters(dbParams []database.TemplateVersionParameter) ([]codersdk.TemplateVersionParameter, error) {
- params := make([]codersdk.TemplateVersionParameter, 0)
- for _, dbParameter := range dbParams {
- param, err := convertTemplateVersionParameter(dbParameter)
- if err != nil {
- return nil, err
- }
- params = append(params, param)
- }
- return params, nil
-}
-
-func convertTemplateVersionParameter(param database.TemplateVersionParameter) (codersdk.TemplateVersionParameter, error) {
- var protoOptions []*sdkproto.RichParameterOption
- err := json.Unmarshal(param.Options, &protoOptions)
- if err != nil {
- return codersdk.TemplateVersionParameter{}, err
- }
- options := make([]codersdk.TemplateVersionParameterOption, 0)
- for _, option := range protoOptions {
- options = append(options, codersdk.TemplateVersionParameterOption{
- Name: option.Name,
- Description: option.Description,
- Value: option.Value,
- Icon: option.Icon,
- })
- }
-
- descriptionPlaintext, err := render.PlaintextFromMarkdown(param.Description)
- if err != nil {
- return codersdk.TemplateVersionParameter{}, err
- }
-
- var validationMin, validationMax *int32
- if param.ValidationMin.Valid {
- validationMin = ¶m.ValidationMin.Int32
- }
- if param.ValidationMax.Valid {
- validationMax = ¶m.ValidationMax.Int32
- }
-
- return codersdk.TemplateVersionParameter{
- Name: param.Name,
- DisplayName: param.DisplayName,
- Description: param.Description,
- DescriptionPlaintext: descriptionPlaintext,
- Type: param.Type,
- Mutable: param.Mutable,
- DefaultValue: param.DefaultValue,
- Icon: param.Icon,
- Options: options,
- ValidationRegex: param.ValidationRegex,
- ValidationMin: validationMin,
- ValidationMax: validationMax,
- ValidationError: param.ValidationError,
- ValidationMonotonic: codersdk.ValidationMonotonicOrder(param.ValidationMonotonic),
- Required: param.Required,
- Ephemeral: param.Ephemeral,
- }, nil
-}
-
func convertTemplateVersionVariables(dbVariables []database.TemplateVersionVariable) []codersdk.TemplateVersionVariable {
variables := make([]codersdk.TemplateVersionVariable, 0)
for _, dbVariable := range dbVariables {
diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go
index 7137dc16a1b43..a9b981f820be2 100644
--- a/coderd/workspaceagents_test.go
+++ b/coderd/workspaceagents_test.go
@@ -1771,7 +1771,7 @@ func TestWorkspaceAgent_Metadata(t *testing.T) {
// Verify manifest API response.
require.Equal(t, workspace.ID, manifest.WorkspaceID)
- require.Equal(t, workspace.OwnerUsername, manifest.OwnerUsername)
+ require.Equal(t, workspace.OwnerName, manifest.OwnerName)
require.Equal(t, "First Meta", manifest.Metadata[0].DisplayName)
require.Equal(t, "foo1", manifest.Metadata[0].Key)
require.Equal(t, "echo hi", manifest.Metadata[0].Script)
diff --git a/coderd/workspaces.go b/coderd/workspaces.go
index cb954c8690685..fe0c2d3f609a2 100644
--- a/coderd/workspaces.go
+++ b/coderd/workspaces.go
@@ -2248,8 +2248,7 @@ func convertWorkspace(
CreatedAt: workspace.CreatedAt,
UpdatedAt: workspace.UpdatedAt,
OwnerID: workspace.OwnerID,
- OwnerName: workspace.OwnerName,
- OwnerUsername: workspace.OwnerUsername,
+ OwnerName: workspace.OwnerUsername,
OwnerAvatarURL: workspace.OwnerAvatarUrl,
OrganizationID: workspace.OrganizationID,
OrganizationName: workspace.OrganizationName,
diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go
index fedbee56942cb..018dd363bdee6 100644
--- a/coderd/workspaces_test.go
+++ b/coderd/workspaces_test.go
@@ -42,6 +42,7 @@ import (
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/testutil"
+ "github.com/coder/terraform-provider-coder/v2/provider"
)
func TestWorkspace(t *testing.T) {
@@ -1379,12 +1380,12 @@ func TestWorkspaceByOwnerAndName(t *testing.T) {
// Then:
// When we call without includes_deleted, we don't expect to get the workspace back
- _, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerUsername, workspace.Name, codersdk.WorkspaceOptions{})
+ _, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
require.ErrorContains(t, err, "404")
// Then:
// When we call with includes_deleted, we should get the workspace back
- workspaceNew, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerUsername, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true})
+ workspaceNew, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true})
require.NoError(t, err)
require.Equal(t, workspace.ID, workspaceNew.ID)
@@ -1402,7 +1403,7 @@ func TestWorkspaceByOwnerAndName(t *testing.T) {
// Then:
// We can fetch the most recent workspace
- workspaceNew, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerUsername, workspace.Name, codersdk.WorkspaceOptions{})
+ workspaceNew, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
require.Equal(t, workspace.ID, workspaceNew.ID)
@@ -1416,7 +1417,7 @@ func TestWorkspaceByOwnerAndName(t *testing.T) {
// Then:
// When we fetch the deleted workspace, we get the most recently deleted one
- workspaceNew, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerUsername, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true})
+ workspaceNew, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true})
require.NoError(t, err)
require.Equal(t, workspace.ID, workspaceNew.ID)
})
@@ -1901,7 +1902,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
require.NoError(t, err)
require.Len(t, res.Workspaces, len(workspaces))
for _, found := range res.Workspaces {
- require.Equal(t, found.OwnerUsername, sdkUser.Username)
+ require.Equal(t, found.OwnerName, sdkUser.Username)
}
})
t.Run("IDs", func(t *testing.T) {
@@ -2033,7 +2034,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
// single workspace
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
- FilterQuery: fmt.Sprintf("template:%s %s/%s", template.Name, workspace.OwnerUsername, workspace.Name),
+ FilterQuery: fmt.Sprintf("template:%s %s/%s", template.Name, workspace.OwnerName, workspace.Name),
})
require.NoError(t, err)
require.Len(t, res.Workspaces, 1)
@@ -3527,6 +3528,12 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
secondParameterDescription = "_This_ is second *parameter*"
secondParameterValue = "2"
secondParameterValidationMonotonic = codersdk.MonotonicOrderIncreasing
+
+ thirdParameterName = "third_parameter"
+ thirdParameterType = "list(string)"
+ thirdParameterFormType = proto.ParameterFormType_MULTISELECT
+ thirdParameterDefault = `["red"]`
+ thirdParameterOption = "red"
)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
@@ -3542,6 +3549,7 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
Name: firstParameterName,
Type: firstParameterType,
Description: firstParameterDescription,
+ FormType: proto.ParameterFormType_INPUT,
},
{
Name: secondParameterName,
@@ -3551,6 +3559,19 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
ValidationMin: ptr.Ref(int32(1)),
ValidationMax: ptr.Ref(int32(3)),
ValidationMonotonic: string(secondParameterValidationMonotonic),
+ FormType: proto.ParameterFormType_INPUT,
+ },
+ {
+ Name: thirdParameterName,
+ Type: thirdParameterType,
+ DefaultValue: thirdParameterDefault,
+ Options: []*proto.RichParameterOption{
+ {
+ Name: thirdParameterOption,
+ Value: thirdParameterOption,
+ },
+ },
+ FormType: thirdParameterFormType,
},
},
},
@@ -3575,12 +3596,13 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
templateRichParameters, err := client.TemplateVersionRichParameters(ctx, version.ID)
require.NoError(t, err)
- require.Len(t, templateRichParameters, 2)
+ require.Len(t, templateRichParameters, 3)
require.Equal(t, firstParameterName, templateRichParameters[0].Name)
require.Equal(t, firstParameterType, templateRichParameters[0].Type)
require.Equal(t, firstParameterDescription, templateRichParameters[0].Description)
require.Equal(t, firstParameterDescriptionPlaintext, templateRichParameters[0].DescriptionPlaintext)
require.Equal(t, codersdk.ValidationMonotonicOrder(""), templateRichParameters[0].ValidationMonotonic) // no validation for string
+
require.Equal(t, secondParameterName, templateRichParameters[1].Name)
require.Equal(t, secondParameterDisplayName, templateRichParameters[1].DisplayName)
require.Equal(t, secondParameterType, templateRichParameters[1].Type)
@@ -3588,9 +3610,18 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
require.Equal(t, secondParameterDescriptionPlaintext, templateRichParameters[1].DescriptionPlaintext)
require.Equal(t, secondParameterValidationMonotonic, templateRichParameters[1].ValidationMonotonic)
+ third := templateRichParameters[2]
+ require.Equal(t, thirdParameterName, third.Name)
+ require.Equal(t, thirdParameterType, third.Type)
+ require.Equal(t, string(database.ParameterFormTypeMultiSelect), third.FormType)
+ require.Equal(t, thirdParameterDefault, third.DefaultValue)
+ require.Equal(t, thirdParameterOption, third.Options[0].Name)
+ require.Equal(t, thirdParameterOption, third.Options[0].Value)
+
expectedBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: firstParameterName, Value: firstParameterValue},
{Name: secondParameterName, Value: secondParameterValue},
+ {Name: thirdParameterName, Value: thirdParameterDefault},
}
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -3606,6 +3637,72 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters)
}
+func TestWorkspaceWithMultiSelectFailure(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: []*proto.Response{
+ {
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
+ Parameters: []*proto.RichParameter{
+ {
+ Name: "param",
+ Type: provider.OptionTypeListString,
+ DefaultValue: `["red"]`,
+ Options: []*proto.RichParameterOption{
+ {
+ Name: "red",
+ Value: "red",
+ },
+ },
+ FormType: proto.ParameterFormType_MULTISELECT,
+ },
+ },
+ },
+ },
+ },
+ },
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{},
+ },
+ }},
+ })
+ coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ templateRichParameters, err := client.TemplateVersionRichParameters(ctx, version.ID)
+ require.NoError(t, err)
+ require.Len(t, templateRichParameters, 1)
+
+ expectedBuildParameters := []codersdk.WorkspaceBuildParameter{
+ // purple is not in the response set
+ {Name: "param", Value: `["red", "purple"]`},
+ }
+
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ req := codersdk.CreateWorkspaceRequest{
+ TemplateID: template.ID,
+ Name: coderdtest.RandomUsername(t),
+ AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"),
+ TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()),
+ AutomaticUpdates: codersdk.AutomaticUpdatesNever,
+ RichParameterValues: expectedBuildParameters,
+ }
+
+ _, err = client.CreateUserWorkspace(context.Background(), codersdk.Me, req)
+ require.Error(t, err)
+ var apiError *codersdk.Error
+ require.ErrorAs(t, err, &apiError)
+ require.Equal(t, http.StatusBadRequest, apiError.StatusCode())
+}
+
func TestWorkspaceWithOptionalRichParameters(t *testing.T) {
t.Parallel()
diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go
index 1178a193a21a0..e3b036dcdf00a 100644
--- a/codersdk/agentsdk/agentsdk.go
+++ b/codersdk/agentsdk/agentsdk.go
@@ -107,7 +107,7 @@ type Manifest struct {
// OwnerUsername and WorkspaceID are used by an open-source user to identify the workspace.
// We do not provide insurance that this will not be removed in the future,
// but if it's easy to persist lets keep it around.
- OwnerUsername string `json:"owner_username"`
+ OwnerName string `json:"owner_name"`
WorkspaceID uuid.UUID `json:"workspace_id"`
WorkspaceName string `json:"workspace_name"`
// GitAuthConfigs stores the number of Git configurations
diff --git a/codersdk/agentsdk/convert.go b/codersdk/agentsdk/convert.go
index f67ac63f3861b..2b7dff950a3e7 100644
--- a/codersdk/agentsdk/convert.go
+++ b/codersdk/agentsdk/convert.go
@@ -38,7 +38,7 @@ func ManifestFromProto(manifest *proto.Manifest) (Manifest, error) {
return Manifest{
AgentID: agentID,
AgentName: manifest.AgentName,
- OwnerUsername: manifest.OwnerUsername,
+ OwnerName: manifest.OwnerUsername,
WorkspaceID: workspaceID,
WorkspaceName: manifest.WorkspaceName,
Apps: apps,
@@ -64,7 +64,7 @@ func ProtoFromManifest(manifest Manifest) (*proto.Manifest, error) {
return &proto.Manifest{
AgentId: manifest.AgentID[:],
AgentName: manifest.AgentName,
- OwnerUsername: manifest.OwnerUsername,
+ OwnerUsername: manifest.OwnerName,
WorkspaceId: manifest.WorkspaceID[:],
WorkspaceName: manifest.WorkspaceName,
// #nosec G115 - Safe conversion for GitAuthConfigs which is expected to be small and positive
diff --git a/codersdk/agentsdk/convert_test.go b/codersdk/agentsdk/convert_test.go
index dcafe3127a3df..09482b1694910 100644
--- a/codersdk/agentsdk/convert_test.go
+++ b/codersdk/agentsdk/convert_test.go
@@ -21,7 +21,7 @@ func TestManifest(t *testing.T) {
manifest := agentsdk.Manifest{
AgentID: uuid.New(),
AgentName: "test-agent",
- OwnerUsername: "test-owner",
+ OwnerName: "test-owner",
WorkspaceID: uuid.New(),
WorkspaceName: "test-workspace",
GitAuthConfigs: 3,
@@ -144,7 +144,7 @@ func TestManifest(t *testing.T) {
require.NoError(t, err)
require.Equal(t, manifest.AgentID, back.AgentID)
require.Equal(t, manifest.AgentName, back.AgentName)
- require.Equal(t, manifest.OwnerUsername, back.OwnerUsername)
+ require.Equal(t, manifest.OwnerName, back.OwnerName)
require.Equal(t, manifest.WorkspaceID, back.WorkspaceID)
require.Equal(t, manifest.WorkspaceName, back.WorkspaceName)
require.Equal(t, manifest.GitAuthConfigs, back.GitAuthConfigs)
diff --git a/codersdk/richparameters.go b/codersdk/richparameters.go
index f00c947715f9d..db109316fdfc0 100644
--- a/codersdk/richparameters.go
+++ b/codersdk/richparameters.go
@@ -1,9 +1,12 @@
package codersdk
import (
+ "encoding/json"
+
"golang.org/x/xerrors"
"tailscale.com/types/ptr"
+ "github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/terraform-provider-coder/v2/provider"
)
@@ -66,18 +69,8 @@ func validateBuildParameter(richParameter TemplateVersionParameter, buildParamet
current = richParameter.DefaultValue
}
- if len(richParameter.Options) > 0 {
- var matched bool
- for _, opt := range richParameter.Options {
- if opt.Value == current {
- matched = true
- break
- }
- }
-
- if !matched {
- return xerrors.Errorf("parameter value must match one of options: %s", parameterValuesAsArray(richParameter.Options))
- }
+ if len(richParameter.Options) > 0 && !inOptionSet(richParameter, current) {
+ return xerrors.Errorf("parameter value must match one of options: %s", parameterValuesAsArray(richParameter.Options))
}
if !validationEnabled(richParameter) {
@@ -104,6 +97,37 @@ func validateBuildParameter(richParameter TemplateVersionParameter, buildParamet
return validation.Valid(richParameter.Type, current, previous)
}
+// inOptionSet returns if the value given is in the set of options for a parameter.
+func inOptionSet(richParameter TemplateVersionParameter, value string) bool {
+ optionValues := make([]string, 0, len(richParameter.Options))
+ for _, option := range richParameter.Options {
+ optionValues = append(optionValues, option.Value)
+ }
+
+ // If the type is `list(string)` and the form_type is `multi-select`, then we check each individual
+ // value in the list against the option set.
+ isMultiSelect := richParameter.Type == provider.OptionTypeListString && richParameter.FormType == string(provider.ParameterFormTypeMultiSelect)
+
+ if !isMultiSelect {
+ // This is the simple case. Just checking if the value is in the option set.
+ return slice.Contains(optionValues, value)
+ }
+
+ var checks []string
+ err := json.Unmarshal([]byte(value), &checks)
+ if err != nil {
+ return false
+ }
+
+ for _, check := range checks {
+ if !slice.Contains(optionValues, check) {
+ return false
+ }
+ }
+
+ return true
+}
+
func findBuildParameter(params []WorkspaceBuildParameter, parameterName string) (*WorkspaceBuildParameter, bool) {
if params == nil {
return nil, false
diff --git a/codersdk/richparameters_internal_test.go b/codersdk/richparameters_internal_test.go
new file mode 100644
index 0000000000000..038e89c7442b3
--- /dev/null
+++ b/codersdk/richparameters_internal_test.go
@@ -0,0 +1,149 @@
+package codersdk
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/coder/terraform-provider-coder/v2/provider"
+)
+
+func Test_inOptionSet(t *testing.T) {
+ t.Parallel()
+
+ options := func(vals ...string) []TemplateVersionParameterOption {
+ opts := make([]TemplateVersionParameterOption, 0, len(vals))
+ for _, val := range vals {
+ opts = append(opts, TemplateVersionParameterOption{
+ Name: val,
+ Value: val,
+ })
+ }
+ return opts
+ }
+
+ tests := []struct {
+ name string
+ param TemplateVersionParameter
+ value string
+ want bool
+ }{
+ // The function should never be called with 0 options, but if it is,
+ // it should always return false.
+ {
+ name: "empty",
+ want: false,
+ },
+ {
+ name: "no-options",
+ param: TemplateVersionParameter{
+ Options: make([]TemplateVersionParameterOption, 0),
+ },
+ },
+ {
+ name: "no-options-multi",
+ param: TemplateVersionParameter{
+ Type: provider.OptionTypeListString,
+ FormType: string(provider.ParameterFormTypeMultiSelect),
+ Options: make([]TemplateVersionParameterOption, 0),
+ },
+ want: false,
+ },
+ {
+ name: "no-options-list(string)",
+ param: TemplateVersionParameter{
+ Type: provider.OptionTypeListString,
+ FormType: "",
+ Options: make([]TemplateVersionParameterOption, 0),
+ },
+ want: false,
+ },
+ {
+ name: "list(string)-no-form",
+ param: TemplateVersionParameter{
+ Type: provider.OptionTypeListString,
+ FormType: "",
+ Options: options("red", "green", "blue"),
+ },
+ want: false,
+ value: `["red", "blue", "green"]`,
+ },
+ // now for some reasonable values
+ {
+ name: "list(string)-multi",
+ param: TemplateVersionParameter{
+ Type: provider.OptionTypeListString,
+ FormType: string(provider.ParameterFormTypeMultiSelect),
+ Options: options("red", "green", "blue"),
+ },
+ want: true,
+ value: `["red", "blue", "green"]`,
+ },
+ {
+ name: "string with json",
+ param: TemplateVersionParameter{
+ Type: provider.OptionTypeString,
+ Options: options(`["red","blue","green"]`, `["red","orange"]`),
+ },
+ want: true,
+ value: `["red","blue","green"]`,
+ },
+ {
+ name: "string",
+ param: TemplateVersionParameter{
+ Type: provider.OptionTypeString,
+ Options: options("red", "green", "blue"),
+ },
+ want: true,
+ value: "red",
+ },
+ // False values
+ {
+ name: "list(string)-multi",
+ param: TemplateVersionParameter{
+ Type: provider.OptionTypeListString,
+ FormType: string(provider.ParameterFormTypeMultiSelect),
+ Options: options("red", "green", "blue"),
+ },
+ want: false,
+ value: `["red", "blue", "purple"]`,
+ },
+ {
+ name: "string with json",
+ param: TemplateVersionParameter{
+ Type: provider.OptionTypeString,
+ Options: options(`["red","blue"]`, `["red","orange"]`),
+ },
+ want: false,
+ value: `["red","blue","green"]`,
+ },
+ {
+ name: "string",
+ param: TemplateVersionParameter{
+ Type: provider.OptionTypeString,
+ Options: options("red", "green", "blue"),
+ },
+ want: false,
+ value: "purple",
+ },
+ {
+ name: "list(string)-multi-scalar-value",
+ param: TemplateVersionParameter{
+ Type: provider.OptionTypeListString,
+ FormType: string(provider.ParameterFormTypeMultiSelect),
+ Options: options("red", "green", "blue"),
+ },
+ want: false,
+ value: "green",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ got := inOptionSet(tt.param, tt.value)
+ require.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/codersdk/templateversions.go b/codersdk/templateversions.go
index de8bb7b970957..a47cbb685898b 100644
--- a/codersdk/templateversions.go
+++ b/codersdk/templateversions.go
@@ -54,22 +54,25 @@ const (
// TemplateVersionParameter represents a parameter for a template version.
type TemplateVersionParameter struct {
- Name string `json:"name"`
- DisplayName string `json:"display_name,omitempty"`
- Description string `json:"description"`
- DescriptionPlaintext string `json:"description_plaintext"`
- Type string `json:"type" enums:"string,number,bool,list(string)"`
- Mutable bool `json:"mutable"`
- DefaultValue string `json:"default_value"`
- Icon string `json:"icon"`
- Options []TemplateVersionParameterOption `json:"options"`
- ValidationError string `json:"validation_error,omitempty"`
- ValidationRegex string `json:"validation_regex,omitempty"`
- ValidationMin *int32 `json:"validation_min,omitempty"`
- ValidationMax *int32 `json:"validation_max,omitempty"`
- ValidationMonotonic ValidationMonotonicOrder `json:"validation_monotonic,omitempty" enums:"increasing,decreasing"`
- Required bool `json:"required"`
- Ephemeral bool `json:"ephemeral"`
+ Name string `json:"name"`
+ DisplayName string `json:"display_name,omitempty"`
+ Description string `json:"description"`
+ DescriptionPlaintext string `json:"description_plaintext"`
+ Type string `json:"type" enums:"string,number,bool,list(string)"`
+ // FormType has an enum value of empty string, `""`.
+ // Keep the leading comma in the enums struct tag.
+ FormType string `json:"form_type" enums:",radio,dropdown,input,textarea,slider,checkbox,switch,tag-select,multi-select,error"`
+ Mutable bool `json:"mutable"`
+ DefaultValue string `json:"default_value"`
+ Icon string `json:"icon"`
+ Options []TemplateVersionParameterOption `json:"options"`
+ ValidationError string `json:"validation_error,omitempty"`
+ ValidationRegex string `json:"validation_regex,omitempty"`
+ ValidationMin *int32 `json:"validation_min,omitempty"`
+ ValidationMax *int32 `json:"validation_max,omitempty"`
+ ValidationMonotonic ValidationMonotonicOrder `json:"validation_monotonic,omitempty" enums:"increasing,decreasing"`
+ Required bool `json:"required"`
+ Ephemeral bool `json:"ephemeral"`
}
// TemplateVersionParameterOption represents a selectable option for a template parameter.
diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go
index ee762ddcd3d30..2c73d60a2696c 100644
--- a/codersdk/workspaces.go
+++ b/codersdk/workspaces.go
@@ -26,12 +26,12 @@ const (
// Workspace is a deployment of a template. It references a specific
// version and can be updated.
type Workspace struct {
- ID uuid.UUID `json:"id" format:"uuid"`
- CreatedAt time.Time `json:"created_at" format:"date-time"`
- UpdatedAt time.Time `json:"updated_at" format:"date-time"`
- OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
- OwnerName string `json:"owner_name,omitempty"`
- OwnerUsername string `json:"owner_username"`
+ ID uuid.UUID `json:"id" format:"uuid"`
+ CreatedAt time.Time `json:"created_at" format:"date-time"`
+ UpdatedAt time.Time `json:"updated_at" format:"date-time"`
+ OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
+ // OwnerName is the username of the owner of the workspace.
+ OwnerName string `json:"owner_name"`
OwnerAvatarURL string `json:"owner_avatar_url"`
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
OrganizationName string `json:"organization_name"`
@@ -50,7 +50,6 @@ type Workspace struct {
AutostartSchedule *string `json:"autostart_schedule,omitempty"`
TTLMillis *int64 `json:"ttl_ms,omitempty"`
LastUsedAt time.Time `json:"last_used_at" format:"date-time"`
-
// DeletingAt indicates the time at which the workspace will be permanently deleted.
// A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value)
// and a value has been specified for time_til_dormant_autodelete on its template.
@@ -70,7 +69,7 @@ type Workspace struct {
}
func (w Workspace) FullName() string {
- return fmt.Sprintf("%s/%s", w.OwnerUsername, w.Name)
+ return fmt.Sprintf("%s/%s", w.OwnerName, w.Name)
}
type WorkspaceHealth struct {
diff --git a/docs/admin/users/github-auth.md b/docs/admin/users/github-auth.md
index c556c87a2accb..57ed6f9eeb37a 100644
--- a/docs/admin/users/github-auth.md
+++ b/docs/admin/users/github-auth.md
@@ -1,80 +1,91 @@
# GitHub
-## Default Configuration
-
By default, new Coder deployments use a Coder-managed GitHub app to authenticate
-users. We provide it for convenience, allowing you to experiment with Coder
-without setting up your own GitHub OAuth app. Once you authenticate with it, you
-grant Coder server read access to:
+users.
+We provide it for convenience, allowing you to experiment with Coder
+without setting up your own GitHub OAuth app.
-- Your GitHub user email
-- Your GitHub organization membership
-- Other metadata listed during the authentication flow
+If you authenticate with it, you grant Coder server read access to your GitHub
+user email and other metadata listed during the authentication flow.
This access is necessary for the Coder server to complete the authentication
-process. To the best of our knowledge, Coder, the company, does not gain access
+process.
+To the best of our knowledge, Coder, the company, does not gain access
to this data by administering the GitHub app.
+## Default Configuration
+
> [!IMPORTANT]
-> The default GitHub app requires [device flow](#device-flow) to authenticate.
-> This is enabled by default when using the default GitHub app. If you disable
-> device flow using `CODER_OAUTH2_GITHUB_DEVICE_FLOW=false`, it will be ignored.
+> Installation of the default GitHub app grants Coder (the company) access to your organization's GitHub data.
+>
+> For production environments, we strongly recommend that you
+> [configure your own GitHub OAuth app](#step-1-configure-the-oauth-application-in-github)
+> to ensure that your data is not shared with Coder (the company).
-By default, only the admin user can sign up. To allow additional users to sign
-up with GitHub, add the following environment variable:
+To use the default configuration:
-```env
-CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS=true
-```
+1. [Install the GitHub app](https://github.com/apps/coder/installations/select_target)
+ in any GitHub organization that you want to use with Coder.
-To limit sign ups to members of specific GitHub organizations, set:
+ The default GitHub app requires [device flow](#device-flow) to authenticate.
+ This is enabled by default when using the default GitHub app.
+ If you disable device flow using `CODER_OAUTH2_GITHUB_DEVICE_FLOW=false`, it will be ignored.
-```env
-CODER_OAUTH2_GITHUB_ALLOWED_ORGS="your-org"
-```
+1. By default, only the admin user can sign up.
+ To allow additional users to sign up with GitHub, add:
+
+ ```shell
+ CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS=true
+ ```
-For production deployments, we recommend configuring your own GitHub OAuth app
-as outlined below. The default is automatically disabled if you configure your
-own app or set:
+1. (Optional) If you want to limit sign-ups to specific GitHub organizations, set:
-```env
+ ```shell
+ CODER_OAUTH2_GITHUB_ALLOWED_ORGS="your-org"
+ ```
+
+## Disable the Default GitHub App
+
+You can disable the default GitHub app by [configuring your own app](#step-1-configure-the-oauth-application-in-github)
+or by adding the following environment variable to your [Coder server configuration](../../reference/cli/server.md#options):
+
+```shell
CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE=false
```
> [!NOTE]
-> After you disable the default GitHub provider with the setting above, the
-> **Sign in with GitHub** button might still appear on your login page even though
-> the authentication flow is disabled.
+> After you disable the default GitHub provider, the **Sign in with GitHub** button
+> might still appear on your login page even though the authentication flow is disabled.
>
-> To completely hide the GitHub sign-in button, you must both disable the default
-> provider and ensure you don't have a custom GitHub OAuth app configured.
+> To completely hide the GitHub sign-in button, you must disable the default provider
+> and ensure you don't have a custom GitHub OAuth app configured.
## Step 1: Configure the OAuth application in GitHub
-First,
-[register a GitHub OAuth app](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/).
-GitHub will ask you for the following Coder parameters:
+1. [Register a GitHub OAuth app](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/).
+
+1. GitHub will ask you for the following Coder parameters:
-- **Homepage URL**: Set to your Coder deployments
- [`CODER_ACCESS_URL`](../../reference/cli/server.md#--access-url) (e.g.
- `https://coder.domain.com`)
-- **User Authorization Callback URL**: Set to `https://coder.domain.com`
+ - **Homepage URL**: Set to your Coder deployment's
+ [`CODER_ACCESS_URL`](../../reference/cli/server.md#--access-url) (e.g.
+ `https://coder.domain.com`)
+ - **User Authorization Callback URL**: Set to `https://coder.domain.com`
-If you want to allow multiple Coder deployments hosted on subdomains, such as
-`coder1.domain.com`, `coder2.domain.com`, to authenticate with the
-same GitHub OAuth app, then you can set **User Authorization Callback URL** to
-the `https://domain.com`
+ If you want to allow multiple Coder deployments hosted on subdomains, such as
+ `coder1.domain.com`, `coder2.domain.com`, to authenticate with the
+ same GitHub OAuth app, then you can set **User Authorization Callback URL** to
+ the `https://domain.com`
-Take note of the Client ID and Client Secret generated by GitHub. You will use these
-values in the next step.
+1. Take note of the Client ID and Client Secret generated by GitHub.
+ You will use these values in the next step.
-Coder will need permission to access user email addresses. Find the "Account
-Permissions" settings for your app and select "read-only" for "Email addresses".
+1. Coder needs permission to access user email addresses.
+
+ Find the **Account Permissions** settings for your app and select **read-only** for **Email addresses**.
## Step 2: Configure Coder with the OAuth credentials
-Navigate to your Coder host and run the following command to start up the Coder
-server:
+Go to your Coder host and run the following command to start up the Coder server:
```shell
coder server --oauth2-github-allow-signups=true --oauth2-github-allowed-orgs="your-org" --oauth2-github-client-id="8d1...e05" --oauth2-github-client-secret="57ebc9...02c24c"
@@ -87,7 +98,7 @@ Alternatively, if you are running Coder as a system service, you can achieve the
same result as the command above by adding the following environment variables
to the `/etc/coder.d/coder.env` file:
-```env
+```shell
CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS=true
CODER_OAUTH2_GITHUB_ALLOWED_ORGS="your-org"
CODER_OAUTH2_GITHUB_CLIENT_ID="8d1...e05"
@@ -97,7 +108,7 @@ CODER_OAUTH2_GITHUB_CLIENT_SECRET="57ebc9...02c24c"
> [!TIP]
> To allow everyone to sign up using GitHub, set:
>
-> ```env
+> ```shell
> CODER_OAUTH2_GITHUB_ALLOW_EVERYONE=true
> ```
@@ -129,23 +140,24 @@ To upgrade Coder, run:
helm upgrade coder-v2/coder -n -f values.yaml
```
-We recommend requiring and auditing MFA usage for all users in your GitHub
-organizations. This can be enforced from the organization settings page in the
-"Authentication security" sidebar tab.
+We recommend requiring and auditing MFA usage for all users in your GitHub organizations.
+This can be enforced from the organization settings page in the **Authentication security** sidebar tab.
## Device Flow
Coder supports
[device flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)
-for GitHub OAuth. This is enabled by default for the default GitHub app and cannot be disabled
-for that app. For your own custom GitHub OAuth app, you can enable device flow by setting:
+for GitHub OAuth.
+This is enabled by default for the default GitHub app and cannot be disabled for that app.
-```env
+For your own custom GitHub OAuth app, you can enable device flow by setting:
+
+```shell
CODER_OAUTH2_GITHUB_DEVICE_FLOW=true
```
-Device flow is optional for custom GitHub OAuth apps. We generally recommend using
-the standard OAuth flow instead, as it is more convenient for end users.
+Device flow is optional for custom GitHub OAuth apps.
+We generally recommend using the standard OAuth flow instead, as it is more convenient for end users.
> [!NOTE]
> If you're using the default GitHub app, device flow is always enabled regardless of
diff --git a/docs/manifest.json b/docs/manifest.json
index 1ec955c6244cc..a7a45bb489563 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -198,7 +198,15 @@
"description": "Use Coder Desktop to access your workspace like it's a local machine",
"path": "./user-guides/desktop/index.md",
"icon_path": "./images/icons/computer-code.svg",
- "state": ["beta"]
+ "state": ["beta"],
+ "children": [
+ {
+ "title": "Coder Desktop connect and sync",
+ "description": "Use Coder Desktop to manage your workspace code and files locally",
+ "path": "./user-guides/desktop/desktop-connect-sync.md",
+ "state": ["beta"]
+ }
+ ]
},
{
"title": "Workspace Management",
diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md
index 84681a9134530..65e3b5f7c8ec8 100644
--- a/docs/reference/api/schemas.md
+++ b/docs/reference/api/schemas.md
@@ -7185,6 +7185,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
"description_plaintext": "string",
"display_name": "string",
"ephemeral": true,
+ "form_type": "",
"icon": "string",
"mutable": true,
"name": "string",
@@ -7208,29 +7209,41 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
### Properties
-| Name | Type | Required | Restrictions | Description |
-|-------------------------|---------------------------------------------------------------------------------------------|----------|--------------|-------------|
-| `default_value` | string | false | | |
-| `description` | string | false | | |
-| `description_plaintext` | string | false | | |
-| `display_name` | string | false | | |
-| `ephemeral` | boolean | false | | |
-| `icon` | string | false | | |
-| `mutable` | boolean | false | | |
-| `name` | string | false | | |
-| `options` | array of [codersdk.TemplateVersionParameterOption](#codersdktemplateversionparameteroption) | false | | |
-| `required` | boolean | false | | |
-| `type` | string | false | | |
-| `validation_error` | string | false | | |
-| `validation_max` | integer | false | | |
-| `validation_min` | integer | false | | |
-| `validation_monotonic` | [codersdk.ValidationMonotonicOrder](#codersdkvalidationmonotonicorder) | false | | |
-| `validation_regex` | string | false | | |
+| Name | Type | Required | Restrictions | Description |
+|-------------------------|---------------------------------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------|
+| `default_value` | string | false | | |
+| `description` | string | false | | |
+| `description_plaintext` | string | false | | |
+| `display_name` | string | false | | |
+| `ephemeral` | boolean | false | | |
+| `form_type` | string | false | | Form type has an enum value of empty string, `""`. Keep the leading comma in the enums struct tag. |
+| `icon` | string | false | | |
+| `mutable` | boolean | false | | |
+| `name` | string | false | | |
+| `options` | array of [codersdk.TemplateVersionParameterOption](#codersdktemplateversionparameteroption) | false | | |
+| `required` | boolean | false | | |
+| `type` | string | false | | |
+| `validation_error` | string | false | | |
+| `validation_max` | integer | false | | |
+| `validation_min` | integer | false | | |
+| `validation_monotonic` | [codersdk.ValidationMonotonicOrder](#codersdkvalidationmonotonicorder) | false | | |
+| `validation_regex` | string | false | | |
#### Enumerated Values
| Property | Value |
|------------------------|----------------|
+| `form_type` | `` |
+| `form_type` | `radio` |
+| `form_type` | `dropdown` |
+| `form_type` | `input` |
+| `form_type` | `textarea` |
+| `form_type` | `slider` |
+| `form_type` | `checkbox` |
+| `form_type` | `switch` |
+| `form_type` | `tag-select` |
+| `form_type` | `multi-select` |
+| `form_type` | `error` |
| `type` | `string` |
| `type` | `number` |
| `type` | `bool` |
@@ -8420,7 +8433,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
- "owner_username": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
@@ -8457,8 +8469,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `outdated` | boolean | false | | |
| `owner_avatar_url` | string | false | | |
| `owner_id` | string | false | | |
-| `owner_name` | string | false | | |
-| `owner_username` | string | false | | |
+| `owner_name` | string | false | | Owner name is the username of the owner of the workspace. |
| `template_active_version_id` | string | false | | |
| `template_allow_user_cancel_workspace_jobs` | boolean | false | | |
| `template_display_name` | string | false | | |
@@ -10125,7 +10136,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
- "owner_username": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md
index 1b16556277edb..6075af775c9bc 100644
--- a/docs/reference/api/templates.md
+++ b/docs/reference/api/templates.md
@@ -3165,6 +3165,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r
"description_plaintext": "string",
"display_name": "string",
"ephemeral": true,
+ "form_type": "",
"icon": "string",
"mutable": true,
"name": "string",
@@ -3197,34 +3198,46 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r
Status Code **200**
-| Name | Type | Required | Restrictions | Description |
-|---------------------------|----------------------------------------------------------------------------------|----------|--------------|-------------|
-| `[array item]` | array | false | | |
-| `» default_value` | string | false | | |
-| `» description` | string | false | | |
-| `» description_plaintext` | string | false | | |
-| `» display_name` | string | false | | |
-| `» ephemeral` | boolean | false | | |
-| `» icon` | string | false | | |
-| `» mutable` | boolean | false | | |
-| `» name` | string | false | | |
-| `» options` | array | false | | |
-| `»» description` | string | false | | |
-| `»» icon` | string | false | | |
-| `»» name` | string | false | | |
-| `»» value` | string | false | | |
-| `» required` | boolean | false | | |
-| `» type` | string | false | | |
-| `» validation_error` | string | false | | |
-| `» validation_max` | integer | false | | |
-| `» validation_min` | integer | false | | |
-| `» validation_monotonic` | [codersdk.ValidationMonotonicOrder](schemas.md#codersdkvalidationmonotonicorder) | false | | |
-| `» validation_regex` | string | false | | |
+| Name | Type | Required | Restrictions | Description |
+|---------------------------|----------------------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------|
+| `[array item]` | array | false | | |
+| `» default_value` | string | false | | |
+| `» description` | string | false | | |
+| `» description_plaintext` | string | false | | |
+| `» display_name` | string | false | | |
+| `» ephemeral` | boolean | false | | |
+| `» form_type` | string | false | | Form type has an enum value of empty string, `""`. Keep the leading comma in the enums struct tag. |
+| `» icon` | string | false | | |
+| `» mutable` | boolean | false | | |
+| `» name` | string | false | | |
+| `» options` | array | false | | |
+| `»» description` | string | false | | |
+| `»» icon` | string | false | | |
+| `»» name` | string | false | | |
+| `»» value` | string | false | | |
+| `» required` | boolean | false | | |
+| `» type` | string | false | | |
+| `» validation_error` | string | false | | |
+| `» validation_max` | integer | false | | |
+| `» validation_min` | integer | false | | |
+| `» validation_monotonic` | [codersdk.ValidationMonotonicOrder](schemas.md#codersdkvalidationmonotonicorder) | false | | |
+| `» validation_regex` | string | false | | |
#### Enumerated Values
| Property | Value |
|------------------------|----------------|
+| `form_type` | `` |
+| `form_type` | `radio` |
+| `form_type` | `dropdown` |
+| `form_type` | `input` |
+| `form_type` | `textarea` |
+| `form_type` | `slider` |
+| `form_type` | `checkbox` |
+| `form_type` | `switch` |
+| `form_type` | `tag-select` |
+| `form_type` | `multi-select` |
+| `form_type` | `error` |
| `type` | `string` |
| `type` | `number` |
| `type` | `bool` |
diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md
index 6f3b2ec9ce76d..1e73787dfb77e 100644
--- a/docs/reference/api/workspaces.md
+++ b/docs/reference/api/workspaces.md
@@ -291,7 +291,6 @@ of the template will be used.
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
- "owner_username": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
@@ -577,7 +576,6 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
- "owner_username": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
@@ -889,7 +887,6 @@ of the template will be used.
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
- "owner_username": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
@@ -1161,7 +1158,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
- "owner_username": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
@@ -1448,7 +1444,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
- "owner_username": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
@@ -1850,7 +1845,6 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
- "owner_username": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
diff --git a/docs/user-guides/desktop/desktop-connect-sync.md b/docs/user-guides/desktop/desktop-connect-sync.md
new file mode 100644
index 0000000000000..1a09c9b7c5f5d
--- /dev/null
+++ b/docs/user-guides/desktop/desktop-connect-sync.md
@@ -0,0 +1,175 @@
+# Coder Desktop Connect and Sync
+
+Use Coder Desktop to work on your workspaces and files as though they're on your LAN.
+
+> [!NOTE]
+> Coder Desktop requires a Coder deployment running [v2.20.0](https://github.com/coder/coder/releases/tag/v2.20.0) or later.
+
+## Coder Connect
+
+While active, Coder Connect will list the workspaces you own and will configure your system to connect to them over private IPv6 addresses and custom hostnames ending in `.coder`.
+
+
+
+To copy the `.coder` hostname of a workspace agent, you can click the copy icon beside it.
+
+You can also connect to the SSH server in your workspace using any SSH client, such as OpenSSH or PuTTY:
+
+ ```shell
+ ssh your-workspace.coder
+ ```
+
+Any services listening on ports in your workspace will be available on the same hostname. For example, you can access a web server on port `8080` by visiting `http://your-workspace.coder:8080` in your browser.
+
+> [!NOTE]
+> For Coder versions v2.21.3 and earlier: the Coder IDE extensions for VSCode and JetBrains create their own tunnel and do not utilize the Coder Connect tunnel to connect to workspaces.
+
+### Ping your workspace
+
+
+
+### macOS
+
+Use `ping6` in your terminal to verify the connection to your workspace:
+
+ ```shell
+ ping6 -c 5 your-workspace.coder
+ ```
+
+### Windows
+
+Use `ping` in a Command Prompt or PowerShell terminal to verify the connection to your workspace:
+
+ ```shell
+ ping -n 5 your-workspace.coder
+ ```
+
+
+
+## Sync a local directory with your workspace
+
+Coder Desktop file sync provides bidirectional synchronization between a local directory and your workspace.
+You can work offline, add screenshots to documentation, or use local development tools while keeping your files in sync with your workspace.
+
+1. Create a new local directory.
+
+ If you select an existing clone of your repository, Desktop will recognize it as conflicting files.
+
+1. In the Coder Desktop app, select **File sync**.
+
+ 
+
+1. Select the **+** in the corner to select the local path, workspace, and remote path, then select **Add**:
+
+ 
+
+1. File sync clones your workspace directory to your local directory, then watches for changes:
+
+ 
+
+ For more information about the current status, hover your mouse over the status.
+
+File sync excludes version control system directories like `.git/` from synchronization, so keep your Git-cloned repository wherever you run Git commands.
+This means that if you use an IDE with a built-in terminal to edit files on your remote workspace, that should be the Git clone and your local directory should be for file syncs.
+
+> [!NOTE]
+> Coder Desktop uses `alpha` and `beta` to distinguish between the:
+>
+> - Local directory: `alpha`
+> - Remote directory: `beta`
+
+### File sync conflicts
+
+File sync shows a `Conflicts` status when it detects conflicting files.
+
+You can hover your mouse over the status for the list of conflicts:
+
+
+
+If you encounter a synchronization conflict, delete the conflicting file that contains changes you don't want to keep.
+
+## Accessing web apps in a secure browser context
+
+Some web applications require a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) to function correctly.
+A browser typically considers an origin secure if the connection is to `localhost`, or over `HTTPS`.
+
+As Coder Connect uses its own hostnames and does not provide TLS to the browser, Google Chrome and Firefox will not allow any web APIs that require a secure context.
+
+> [!NOTE]
+> Despite the browser showing an insecure connection without `HTTPS`, the underlying tunnel is encrypted with WireGuard in the same fashion as other Coder workspace connections (e.g. `coder port-forward`).
+
+If you require secure context web APIs, you will need to mark the workspace hostnames as secure in your browser settings.
+
+We are planning some changes to Coder Desktop that will make accessing secure context web apps easier. Stay tuned for updates.
+
+
+
+### Chrome
+
+1. Open Chrome and visit `chrome://flags/#unsafely-treat-insecure-origin-as-secure`.
+
+1. Enter the full workspace hostname, including the `http` scheme and the port (e.g. `http://your-workspace.coder:8080`), into the **Insecure origins treated as secure** text field.
+
+ If you need to enter multiple URLs, use a comma to separate them.
+
+ 
+
+1. Ensure that the dropdown to the right of the text field is set to **Enabled**.
+
+1. You will be prompted to relaunch Google Chrome at the bottom of the page. Select **Relaunch** to restart Google Chrome.
+
+1. On relaunch and subsequent launches, Google Chrome will show a banner stating "You are using an unsupported command-line flag". This banner can be safely dismissed.
+
+1. Web apps accessed on the configured hostnames and ports will now function correctly in a secure context.
+
+### Firefox
+
+1. Open Firefox and visit `about:config`.
+
+1. Read the warning and select **Accept the Risk and Continue** to access the Firefox configuration page.
+
+1. Enter `dom.securecontext.allowlist` into the search bar at the top.
+
+1. Select **String** on the entry with the same name at the bottom of the list, then select the plus icon on the right.
+
+1. In the text field, enter the full workspace hostname, without the `http` scheme and port: `your-workspace.coder`. Then select the tick icon.
+
+ If you need to enter multiple URLs, use a comma to separate them.
+
+ 
+
+1. Web apps accessed on the configured hostnames will now function correctly in a secure context without requiring a restart.
+
+
+
+## Troubleshooting
+
+### Mac: Issues updating Coder Desktop
+
+> No workspaces!
+
+And
+
+> Internal Error: The VPN must be started with the app open during first-time setup.
+
+Due to an issue with the way Coder Desktop works with the macOS [interprocess communication mechanism](https://developer.apple.com/documentation/xpc)(XPC) system network extension, core Desktop functionality can break when you upgrade the application.
+
+
+
+The resolution depends on which version of macOS you use:
+
+### macOS <=14
+
+1. Delete the application from `/Applications`.
+1. Restart your device.
+
+### macOS 15+
+
+1. Open **System Settings**
+1. Select **General**
+1. Select **Login Items & Extensions**
+1. Scroll down, and select the **ⓘ** for **Network Extensions**
+1. Select the **...** next to Coder Desktop, then **Delete Extension**, and follow the prompts.
+1. Re-open Coder Desktop and follow the prompts to reinstall the network extension.
+
+
diff --git a/docs/user-guides/desktop/index.md b/docs/user-guides/desktop/index.md
index 69a32837a8b87..1f28f46d7c733 100644
--- a/docs/user-guides/desktop/index.md
+++ b/docs/user-guides/desktop/index.md
@@ -121,171 +121,6 @@ Before you can use Coder Desktop, you will need to sign in.
1. Coder Connect is now running!
-## Coder Connect
+## Next Steps
-While active, Coder Connect will list the workspaces you own and will configure your system to connect to them over private IPv6 addresses and custom hostnames ending in `.coder`.
-
-
-
-To copy the `.coder` hostname of a workspace agent, you can click the copy icon beside it.
-
-You can also connect to the SSH server in your workspace using any SSH client, such as OpenSSH or PuTTY:
-
- ```shell
- ssh your-workspace.coder
- ```
-
-Any services listening on ports in your workspace will be available on the same hostname. For example, you can access a web server on port `8080` by visiting `http://your-workspace.coder:8080` in your browser.
-
-> [!NOTE]
-> Currently, the Coder IDE extensions for VSCode and JetBrains create their own tunnel and do not utilize the Coder Connect tunnel to connect to workspaces.
-
-### Ping your workspace
-
-
-
-### macOS
-
-Use `ping6` in your terminal to verify the connection to your workspace:
-
- ```shell
- ping6 -c 5 your-workspace.coder
- ```
-
-### Windows
-
-Use `ping` in a Command Prompt or PowerShell terminal to verify the connection to your workspace:
-
- ```shell
- ping -n 5 your-workspace.coder
- ```
-
-
-
-## Sync a local directory with your workspace
-
-Coder Desktop file sync provides bidirectional synchronization between a local directory and your workspace.
-You can work offline, add screenshots to documentation, or use local development tools while keeping your files in sync with your workspace.
-
-1. Create a new local directory.
-
- If you select an existing clone of your repository, Desktop will recognize it as conflicting files.
-
-1. In the Coder Desktop app, select **File sync**.
-
- 
-
-1. Select the **+** in the corner to select the local path, workspace, and remote path, then select **Add**:
-
- 
-
-1. File sync clones your workspace directory to your local directory, then watches for changes:
-
- 
-
- For more information about the current status, hover your mouse over the status.
-
-File sync excludes version control system directories like `.git/` from synchronization, so keep your Git-cloned repository wherever you run Git commands.
-This means that if you use an IDE with a built-in terminal to edit files on your remote workspace, that should be the Git clone and your local directory should be for file syncs.
-
-> [!NOTE]
-> Coder Desktop uses `alpha` and `beta` to distinguish between the:
->
-> - Local directory: `alpha`
-> - Remote directory: `beta`
-
-### File sync conflicts
-
-File sync shows a `Conflicts` status when it detects conflicting files.
-
-You can hover your mouse over the status for the list of conflicts:
-
-
-
-If you encounter a synchronization conflict, delete the conflicting file that contains changes you don't want to keep.
-
-## Accessing web apps in a secure browser context
-
-Some web applications require a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) to function correctly.
-A browser typically considers an origin secure if the connection is to `localhost`, or over `HTTPS`.
-
-As Coder Connect uses its own hostnames and does not provide TLS to the browser, Google Chrome and Firefox will not allow any web APIs that require a secure context.
-
-> [!NOTE]
-> Despite the browser showing an insecure connection without `HTTPS`, the underlying tunnel is encrypted with WireGuard in the same fashion as other Coder workspace connections (e.g. `coder port-forward`).
-
-If you require secure context web APIs, you will need to mark the workspace hostnames as secure in your browser settings.
-
-We are planning some changes to Coder Desktop that will make accessing secure context web apps easier. Stay tuned for updates.
-
-
-
-### Chrome
-
-1. Open Chrome and visit `chrome://flags/#unsafely-treat-insecure-origin-as-secure`.
-
-1. Enter the full workspace hostname, including the `http` scheme and the port (e.g. `http://your-workspace.coder:8080`), into the **Insecure origins treated as secure** text field.
-
- If you need to enter multiple URLs, use a comma to separate them.
-
- 
-
-1. Ensure that the dropdown to the right of the text field is set to **Enabled**.
-
-1. You will be prompted to relaunch Google Chrome at the bottom of the page. Select **Relaunch** to restart Google Chrome.
-
-1. On relaunch and subsequent launches, Google Chrome will show a banner stating "You are using an unsupported command-line flag". This banner can be safely dismissed.
-
-1. Web apps accessed on the configured hostnames and ports will now function correctly in a secure context.
-
-### Firefox
-
-1. Open Firefox and visit `about:config`.
-
-1. Read the warning and select **Accept the Risk and Continue** to access the Firefox configuration page.
-
-1. Enter `dom.securecontext.allowlist` into the search bar at the top.
-
-1. Select **String** on the entry with the same name at the bottom of the list, then select the plus icon on the right.
-
-1. In the text field, enter the full workspace hostname, without the `http` scheme and port: `your-workspace.coder`. Then select the tick icon.
-
- If you need to enter multiple URLs, use a comma to separate them.
-
- 
-
-1. Web apps accessed on the configured hostnames will now function correctly in a secure context without requiring a restart.
-
-
-
-## Troubleshooting
-
-### Mac: Issues updating Coder Desktop
-
-> No workspaces!
-
-And
-
-> Internal Error: The VPN must be started with the app open during first-time setup.
-
-Due to an issue with the way Coder Desktop works with the macOS [interprocess communication mechanism](https://developer.apple.com/documentation/xpc)(XPC) system network extension, core Desktop functionality can break when you upgrade the application.
-
-
-
-The resolution depends on which version of macOS you use:
-
-### macOS <=14
-
-1. Delete the application from `/Applications`.
-1. Restart your device.
-
-### macOS 15+
-
-1. Open **System Settings**
-1. Select **General**
-1. Select **Login Items & Extensions**
-1. Scroll down, and select the **ⓘ** for **Network Extensions**
-1. Select the **...** next to Coder Desktop, then **Delete Extension**, and follow the prompts.
-1. Re-open Coder Desktop and follow the prompts to reinstall the network extension.
-
-