feat!: move workspace renames behind flag, disable by default (#11189)

This commit is contained in:
Garrett Delfosse 2023-12-15 13:38:47 -05:00 committed by GitHub
parent e63de9a259
commit 7924bb2a56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 173 additions and 21 deletions

View File

@ -15,7 +15,7 @@ import (
func TestRename(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, AllowWorkspaceRenames: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)

View File

@ -583,6 +583,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
HostnamePrefix: vals.SSHConfig.DeploymentName.String(),
SSHConfigOptions: configSSHOptions,
},
AllowWorkspaceRenames: vals.AllowWorkspaceRenames.Value(),
}
if httpServers.TLSConfig != nil {
options.TLSCertificates = httpServers.TLSConfig.Certificates

View File

@ -59,6 +59,7 @@
"healthy": true,
"failing_agents": []
},
"automatic_updates": "never"
"automatic_updates": "never",
"allow_renames": false
}
]

View File

@ -14,6 +14,11 @@ SUBCOMMANDS:
PostgreSQL deployment.
OPTIONS:
--allow-workspace-renames bool, $CODER_ALLOW_WORKSPACE_RENAMES (default: false)
DEPRECATED: Allow users to rename their workspaces. Use only for
temporary compatibility reasons, this will be removed in a future
release.
--cache-dir string, $CODER_CACHE_DIRECTORY (default: [cache dir])
The directory to cache temporary files. If unspecified and
$CACHE_DIRECTORY is set, it will be used for compatibility with

View File

@ -462,3 +462,7 @@ userQuietHoursSchedule:
# change their quiet hours schedule and the site default is always used.
# (default: true, type: bool)
allowCustomQuietHours: true
# DEPRECATED: Allow users to rename their workspaces. Use only for temporary
# compatibility reasons, this will be removed in a future release.
# (default: false, type: bool)
allowWorkspaceRenames: false

6
coderd/apidoc/docs.go generated
View File

@ -8558,6 +8558,9 @@ const docTemplate = `{
"agent_stat_refresh_interval": {
"type": "integer"
},
"allow_workspace_renames": {
"type": "boolean"
},
"autobuild_poll_interval": {
"type": "integer"
},
@ -11443,6 +11446,9 @@ const docTemplate = `{
"codersdk.Workspace": {
"type": "object",
"properties": {
"allow_renames": {
"type": "boolean"
},
"automatic_updates": {
"enum": [
"always",

View File

@ -7642,6 +7642,9 @@
"agent_stat_refresh_interval": {
"type": "integer"
},
"allow_workspace_renames": {
"type": "boolean"
},
"autobuild_poll_interval": {
"type": "integer"
},
@ -10372,6 +10375,9 @@
"codersdk.Workspace": {
"type": "object",
"properties": {
"allow_renames": {
"type": "boolean"
},
"automatic_updates": {
"enum": ["always", "never"],
"allOf": [

View File

@ -179,7 +179,8 @@ type Options struct {
// This janky function is used in telemetry to parse fields out of the raw
// JWT. It needs to be passed through like this because license parsing is
// under the enterprise license, and can't be imported into AGPL.
ParseLicenseClaims func(rawJWT string) (email string, trial bool, err error)
ParseLicenseClaims func(rawJWT string) (email string, trial bool, err error)
AllowWorkspaceRenames bool
}
// @title Coder API

View File

@ -144,6 +144,7 @@ type Options struct {
StatsBatcher *batchstats.Batcher
WorkspaceAppsStatsCollectorOptions workspaceapps.StatsCollectorOptions
AllowWorkspaceRenames bool
}
// New constructs a codersdk client connected to an in-memory API instance.
@ -449,6 +450,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
HealthcheckRefresh: options.HealthcheckRefresh,
StatsBatcher: options.StatsBatcher,
WorkspaceAppsStatsCollectorOptions: options.WorkspaceAppsStatsCollectorOptions,
AllowWorkspaceRenames: options.AllowWorkspaceRenames,
}
}

View File

@ -106,6 +106,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
data.builds[0],
data.templates[0],
ownerName,
api.Options.AllowWorkspaceRenames,
))
}
@ -277,6 +278,7 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
data.builds[0],
data.templates[0],
ownerName,
api.Options.AllowWorkspaceRenames,
))
}
@ -585,6 +587,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
apiBuild,
template,
member.Username,
api.Options.AllowWorkspaceRenames,
))
}
@ -628,6 +631,12 @@ func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) {
// patched in the future, it's enough if one changes.
name := workspace.Name
if req.Name != "" || req.Name != workspace.Name {
if !api.Options.AllowWorkspaceRenames {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Workspace renames are not allowed.",
})
return
}
name = req.Name
}
@ -917,6 +926,7 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
data.builds[0],
data.templates[0],
ownerName,
api.Options.AllowWorkspaceRenames,
))
}
@ -1242,6 +1252,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
data.builds[0],
data.templates[0],
ownerName,
api.Options.AllowWorkspaceRenames,
),
})
}
@ -1293,9 +1304,10 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
}
type workspaceData struct {
templates []database.Template
builds []codersdk.WorkspaceBuild
users []database.User
templates []database.Template
builds []codersdk.WorkspaceBuild
users []database.User
allowRenames bool
}
// workspacesData only returns the data the caller can access. If the caller
@ -1347,9 +1359,10 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa
}
return workspaceData{
templates: templates,
builds: apiBuilds,
users: data.users,
templates: templates,
builds: apiBuilds,
users: data.users,
allowRenames: api.Options.AllowWorkspaceRenames,
}, nil
}
@ -1392,6 +1405,7 @@ func convertWorkspaces(workspaces []database.Workspace, data workspaceData) ([]c
build,
template,
owner.Username,
data.allowRenames,
))
}
return apiWorkspaces, nil
@ -1402,6 +1416,7 @@ func convertWorkspace(
workspaceBuild codersdk.WorkspaceBuild,
template database.Template,
ownerName string,
allowRenames bool,
) codersdk.Workspace {
var autostartSchedule *string
if workspace.AutostartSchedule.Valid {
@ -1456,6 +1471,7 @@ func convertWorkspace(
FailingAgents: failingAgents,
},
AutomaticUpdates: codersdk.AutomaticUpdates(workspace.AutomaticUpdates),
AllowRenames: allowRenames,
}
}

View File

@ -100,7 +100,10 @@ func TestWorkspace(t *testing.T) {
t.Run("Rename", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
AllowWorkspaceRenames: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@ -134,6 +137,29 @@ func TestWorkspace(t *testing.T) {
require.Error(t, err, "workspace rename should have failed")
})
t.Run("RenameDisabled", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
AllowWorkspaceRenames: false,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ws1 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws1.LatestBuild.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancel()
want := "new-name"
err := client.UpdateWorkspace(ctx, ws1.ID, codersdk.UpdateWorkspaceRequest{
Name: want,
})
require.ErrorContains(t, err, "Workspace renames are not allowed")
})
t.Run("TemplateProperties", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
@ -2152,7 +2178,10 @@ func TestUpdateWorkspaceAutomaticUpdates_NotFound(t *testing.T) {
func TestWorkspaceWatcher(t *testing.T) {
t.Parallel()
client, closeFunc := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
client, closeFunc := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
AllowWorkspaceRenames: true,
})
defer closeFunc.Close()
user := coderdtest.CreateFirstUser(t, client)
authToken := uuid.NewString()

View File

@ -181,6 +181,7 @@ type DeploymentValues struct {
EnableTerraformDebugMode clibase.Bool `json:"enable_terraform_debug_mode,omitempty" typescript:",notnull"`
UserQuietHoursSchedule UserQuietHoursScheduleConfig `json:"user_quiet_hours_schedule,omitempty" typescript:",notnull"`
WebTerminalRenderer clibase.String `json:"web_terminal_renderer,omitempty" typescript:",notnull"`
AllowWorkspaceRenames clibase.Bool `json:"allow_workspace_renames,omitempty" typescript:",notnull"`
Healthcheck HealthcheckConfig `json:"healthcheck,omitempty" typescript:",notnull"`
Config clibase.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
@ -1842,6 +1843,15 @@ Write out the current server config as YAML to stdout.`,
Group: &deploymentGroupClient,
YAML: "webTerminalRenderer",
},
{
Name: "Allow Workspace Renames",
Description: "DEPRECATED: Allow users to rename their workspaces. Use only for temporary compatibility reasons, this will be removed in a future release.",
Flag: "allow-workspace-renames",
Env: "CODER_ALLOW_WORKSPACE_RENAMES",
Default: "false",
Value: &c.AllowWorkspaceRenames,
YAML: "allowWorkspaceRenames",
},
// Healthcheck Options
{
Name: "Health Check Refresh",

View File

@ -57,6 +57,7 @@ type Workspace struct {
// what is causing an unhealthy status.
Health WorkspaceHealth `json:"health"`
AutomaticUpdates AutomaticUpdates `json:"automatic_updates" enums:"always,never"`
AllowRenames bool `json:"allow_renames"`
}
func (w Workspace) FullName() string {

1
docs/api/general.md generated
View File

@ -153,6 +153,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"user": {}
},
"agent_stat_refresh_interval": 0,
"allow_workspace_renames": true,
"autobuild_poll_interval": 0,
"browser_only": true,
"cache_directory": "string",

6
docs/api/schemas.md generated
View File

@ -2083,6 +2083,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"user": {}
},
"agent_stat_refresh_interval": 0,
"allow_workspace_renames": true,
"autobuild_poll_interval": 0,
"browser_only": true,
"cache_directory": "string",
@ -2460,6 +2461,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"user": {}
},
"agent_stat_refresh_interval": 0,
"allow_workspace_renames": true,
"autobuild_poll_interval": 0,
"browser_only": true,
"cache_directory": "string",
@ -2732,6 +2734,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `address` | [clibase.HostPort](#clibasehostport) | false | | Address Use HTTPAddress or TLS.Address instead. |
| `agent_fallback_troubleshooting_url` | [clibase.URL](#clibaseurl) | false | | |
| `agent_stat_refresh_interval` | integer | false | | |
| `allow_workspace_renames` | boolean | false | | |
| `autobuild_poll_interval` | integer | false | | |
| `browser_only` | boolean | false | | |
| `cache_directory` | string | false | | |
@ -5764,6 +5767,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
```json
{
"allow_renames": true,
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
@ -5943,6 +5947,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| Name | Type | Required | Restrictions | Description |
| ------------------------------------------- | ------------------------------------------------------ | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allow_renames` | boolean | false | | |
| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | |
| `autostart_schedule` | string | false | | |
| `created_at` | string | false | | |
@ -7025,6 +7030,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"count": 0,
"workspaces": [
{
"allow_renames": true,
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",

View File

@ -47,6 +47,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
```json
{
"allow_renames": true,
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
@ -257,6 +258,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
```json
{
"allow_renames": true,
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
@ -470,6 +472,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
"count": 0,
"workspaces": [
{
"allow_renames": true,
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
@ -677,6 +680,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
```json
{
"allow_renames": true,
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
@ -1003,6 +1007,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
```json
{
"allow_renames": true,
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",

11
docs/cli/server.md generated
View File

@ -42,6 +42,17 @@ The URL that users will use to access the Coder deployment.
Allow users to set their own quiet hours schedule for workspaces to stop in (depending on template autostop requirement settings). If false, users can't change their quiet hours schedule and the site default is always used.
### --allow-workspace-renames
| | |
| ----------- | ------------------------------------------- |
| Type | <code>bool</code> |
| Environment | <code>$CODER_ALLOW_WORKSPACE_RENAMES</code> |
| YAML | <code>allowWorkspaceRenames</code> |
| Default | <code>false</code> |
DEPRECATED: Allow users to rename their workspaces. Use only for temporary compatibility reasons, this will be removed in a future release.
### --block-direct-connections
| | |

View File

@ -15,6 +15,11 @@ SUBCOMMANDS:
PostgreSQL deployment.
OPTIONS:
--allow-workspace-renames bool, $CODER_ALLOW_WORKSPACE_RENAMES (default: false)
DEPRECATED: Allow users to rename their workspaces. Use only for
temporary compatibility reasons, this will be removed in a future
release.
--cache-dir string, $CODER_CACHE_DIRECTORY (default: [cache dir])
The directory to cache temporary files. If unspecified and
$CACHE_DIRECTORY is set, it will be used for compatibility with

View File

@ -424,6 +424,7 @@ export interface DeploymentValues {
readonly enable_terraform_debug_mode?: boolean;
readonly user_quiet_hours_schedule?: UserQuietHoursScheduleConfig;
readonly web_terminal_renderer?: string;
readonly allow_workspace_renames?: boolean;
readonly healthcheck?: HealthcheckConfig;
readonly config?: string;
readonly write_config?: boolean;
@ -1429,6 +1430,7 @@ export interface Workspace {
readonly dormant_at?: string;
readonly health: WorkspaceHealth;
readonly automatic_updates: AutomaticUpdates;
readonly allow_renames: boolean;
}
// From codersdk/workspaceagents.go

View File

@ -18,9 +18,9 @@ import {
AutomaticUpdateses,
Workspace,
} from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import MenuItem from "@mui/material/MenuItem";
import upperFirst from "lodash/upperFirst";
import { type Theme } from "@emotion/react";
export type WorkspaceSettingsFormValues = {
name: string;
@ -34,6 +34,10 @@ export const WorkspaceSettingsForm: FC<{
onCancel: () => void;
onSubmit: (values: WorkspaceSettingsFormValues) => Promise<void>;
}> = ({ onCancel, onSubmit, workspace, error, templatePoliciesEnabled }) => {
const formEnabled =
(templatePoliciesEnabled && !workspace.template_require_active_version) ||
workspace.allow_renames;
const form = useFormik<WorkspaceSettingsFormValues>({
onSubmit,
initialValues: {
@ -59,18 +63,19 @@ export const WorkspaceSettingsForm: FC<{
<FormFields>
<TextField
{...getFieldHelpers("name")}
disabled={form.isSubmitting}
disabled={!workspace.allow_renames || form.isSubmitting}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label="Name"
css={workspace.allow_renames && styles.nameWarning}
helperText={
workspace.allow_renames
? form.values.name !== form.initialValues.name &&
"Depending on the template, renaming your workspace may be destructive"
: "Renaming your workspace can be destructive and has not been enabled for this deployment."
}
/>
{form.values.name !== form.initialValues.name && (
<Alert severity="warning">
Depending on the template, renaming your workspace may be
destructive
</Alert>
)}
</FormFields>
</FormSection>
{templatePoliciesEnabled && (
@ -106,7 +111,17 @@ export const WorkspaceSettingsForm: FC<{
</FormFields>
</FormSection>
)}
<FormFooter onCancel={onCancel} isLoading={form.isSubmitting} />
{formEnabled && (
<FormFooter onCancel={onCancel} isLoading={form.isSubmitting} />
)}
</HorizontalForm>
);
};
const styles = {
nameWarning: (theme: Theme) => ({
"& .MuiFormHelperText-root": {
color: theme.palette.warning.light,
},
}),
};

View File

@ -12,7 +12,7 @@ test("Submit the workspace settings page successfully", async () => {
// Mock the API calls that loads data
jest
.spyOn(api, "getWorkspaceByOwnerAndName")
.mockResolvedValueOnce(MockWorkspace);
.mockResolvedValueOnce({ ...MockWorkspace });
// Mock the API calls that submit data
const patchWorkspaceSpy = jest
.spyOn(api, "patchWorkspace")
@ -39,3 +39,21 @@ test("Submit the workspace settings page successfully", async () => {
});
});
});
test("Name field is disabled if renames are disabled", async () => {
// Mock the API calls that loads data
jest
.spyOn(api, "getWorkspaceByOwnerAndName")
.mockResolvedValueOnce({ ...MockWorkspace, allow_renames: false });
renderWithWorkspaceSettingsLayout(<WorkspaceSettingsPage />, {
route: "/@test-user/test-workspace/settings",
path: "/:username/:workspace/settings",
// Need this because after submit the user is redirected
extraRoutes: [{ path: "/:username/:workspace", element: <div /> }],
});
await waitForLoaderToBeRemoved();
// Fill the form and submit
const form = screen.getByTestId("form");
const name = within(form).getByLabelText("Name");
expect(name).toBeDisabled();
});

View File

@ -21,3 +21,9 @@ export const AutoUpdates: Story = {
templatePoliciesEnabled: true,
},
};
export const RenamesDisabled: Story = {
args: {
workspace: { ...MockWorkspace, allow_renames: false },
},
};

View File

@ -1006,6 +1006,7 @@ export const MockWorkspace: TypesGen.Workspace = {
failing_agents: [],
},
automatic_updates: "never",
allow_renames: true,
};
export const MockStoppedWorkspace: TypesGen.Workspace = {