Skip to content

Commit 36f972a

Browse files
committed
Move Orphan checks into the backend
1 parent f7fb54c commit 36f972a

File tree

7 files changed

+125
-57
lines changed

7 files changed

+125
-57
lines changed

cli/delete.go

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,16 @@ func deleteWorkspace() *cobra.Command {
4141
var state []byte
4242

4343
if orphan {
44-
cliui.Warn(cmd.ErrOrStderr(), "Orphaning workspace",
44+
cliui.Warn(cmd.ErrOrStderr(), "Orphaning workspace required template edit permission",
4545
"Template edit permission is required to orphan workspaces.",
4646
)
47-
48-
state, err = client.WorkspaceBuildState(cmd.Context(), workspace.LatestBuild.ID)
49-
if err != nil {
50-
return err
51-
}
52-
// If there's alreay no state, orphanage makes no sense.
53-
if len(state) > 0 {
54-
state, err = codersdk.OrphanTerraformState(state)
55-
if err != nil {
56-
return err
57-
}
58-
}
5947
}
6048

6149
before := time.Now()
6250
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
6351
Transition: codersdk.WorkspaceTransitionDelete,
6452
ProvisionerState: state,
53+
Orphan: orphan,
6554
})
6655
if err != nil {
6756
return err

coderd/coderdtest/coderdtest.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"encoding/base64"
1414
"encoding/json"
1515
"encoding/pem"
16+
"errors"
1617
"fmt"
1718
"io"
1819
"math/big"
@@ -755,3 +756,10 @@ func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
755756
type nopcloser struct{}
756757

757758
func (nopcloser) Close() error { return nil }
759+
760+
// SDKError coerces err into an SDK error.
761+
func SDKError(t *testing.T, err error) *codersdk.Error {
762+
var cerr *codersdk.Error
763+
require.True(t, errors.As(err, &cerr))
764+
return cerr
765+
}

coderd/workspacebuilds.go

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/coder/coder/coderd/httpmw"
2020
"github.com/coder/coder/coderd/rbac"
2121
"github.com/coder/coder/codersdk"
22+
"github.com/coder/coder/provisionersdk"
2223
)
2324

2425
func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
@@ -372,17 +373,6 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
372373
return
373374
}
374375

375-
// If custom state, deny request since user could be orphaning their
376-
// cloud resources.
377-
if createBuild.ProvisionerState != nil {
378-
if !api.Authorize(r, rbac.ActionUpdate, template.RBACObject()) {
379-
httpapi.Write(rw, http.StatusForbidden, codersdk.Response{
380-
Message: "Only template managers may provide custom state",
381-
})
382-
return
383-
}
384-
}
385-
386376
// Store prior build number to compute new build number
387377
var priorBuildNum int32
388378
priorHistory, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
@@ -404,6 +394,42 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
404394
return
405395
}
406396

397+
if createBuild.Orphan {
398+
if createBuild.Transition != codersdk.WorkspaceTransitionDelete {
399+
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
400+
Message: "Orphan is only permitted when deleting a workspace.",
401+
Detail: err.Error(),
402+
})
403+
return
404+
}
405+
if createBuild.ProvisionerState != nil && createBuild.Orphan {
406+
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
407+
Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.",
408+
})
409+
return
410+
}
411+
412+
createBuild.ProvisionerState, err = provisionersdk.OrphanState(priorHistory.ProvisionerState)
413+
if err != nil {
414+
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
415+
Message: "Failed to manipulate state.",
416+
Detail: err.Error(),
417+
})
418+
return
419+
}
420+
}
421+
422+
// If custom state, deny request since user could be orphaning their
423+
// cloud resources.
424+
if createBuild.ProvisionerState != nil {
425+
if !api.Authorize(r, rbac.ActionUpdate, template.RBACObject()) {
426+
httpapi.Write(rw, http.StatusForbidden, codersdk.Response{
427+
Message: "Only template managers may provide custom state",
428+
})
429+
return
430+
}
431+
}
432+
407433
var workspaceBuild database.WorkspaceBuild
408434
var provisionerJob database.ProvisionerJob
409435
// This must happen in a transaction to ensure history can be inserted, and

coderd/workspacebuilds_test.go

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,10 @@ func TestWorkspaceBuilds(t *testing.T) {
231231
})
232232
}
233233

234-
func TestWorkspaceBuilds_CustomState(t *testing.T) {
234+
func TestWorkspaceBuilds_State(t *testing.T) {
235235
t.Parallel()
236236

237-
t.Run("Forbidden", func(t *testing.T) {
237+
t.Run("Permissions", func(t *testing.T) {
238238
t.Parallel()
239239
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
240240
first := coderdtest.CreateFirstUser(t, client)
@@ -246,12 +246,26 @@ func TestWorkspaceBuilds_CustomState(t *testing.T) {
246246
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
247247
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
248248

249+
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
250+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
251+
252+
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
253+
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
254+
Transition: codersdk.WorkspaceTransitionDelete,
255+
ProvisionerState: []byte(" "),
256+
})
257+
require.Nil(t, err)
258+
259+
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
260+
261+
// A regular user on the very same template must not be able to modify the
262+
// state.
249263
regularUser := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
250264

251-
workspace := coderdtest.CreateWorkspace(t, regularUser, first.OrganizationID, template.ID)
265+
workspace = coderdtest.CreateWorkspace(t, regularUser, first.OrganizationID, template.ID)
252266
coderdtest.AwaitWorkspaceBuildJob(t, regularUser, workspace.LatestBuild.ID)
253267

254-
_, err := regularUser.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
268+
_, err = regularUser.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
255269
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
256270
Transition: workspace.LatestBuild.Transition,
257271
ProvisionerState: []byte(" "),
@@ -265,7 +279,7 @@ func TestWorkspaceBuilds_CustomState(t *testing.T) {
265279
require.Equal(t, http.StatusForbidden, code, "unexpected status %s", http.StatusText(code))
266280
})
267281

268-
t.Run("Success", func(t *testing.T) {
282+
t.Run("Orphan", func(t *testing.T) {
269283
t.Parallel()
270284
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
271285
first := coderdtest.CreateFirstUser(t, client)
@@ -280,14 +294,29 @@ func TestWorkspaceBuilds_CustomState(t *testing.T) {
280294
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
281295
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
282296

283-
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
297+
// Providing both state and orphan fails.
298+
_, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
284299
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
285300
Transition: codersdk.WorkspaceTransitionDelete,
286301
ProvisionerState: []byte(" "),
302+
Orphan: true,
287303
})
288-
require.Nil(t, err)
304+
require.Error(t, err)
305+
cerr := coderdtest.SDKError(t, err)
306+
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
289307

308+
// Regular orphan operation succeeds.
309+
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
310+
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
311+
Transition: codersdk.WorkspaceTransitionDelete,
312+
Orphan: true,
313+
})
314+
require.NoError(t, err)
290315
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
316+
317+
_, err = client.Workspace(ctx, workspace.ID)
318+
require.Error(t, err)
319+
require.Equal(t, http.StatusGone, coderdtest.SDKError(t, err).StatusCode())
291320
})
292321
}
293322

codersdk/workspaces.go

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,32 +33,14 @@ type Workspace struct {
3333
LastUsedAt time.Time `json:"last_used_at"`
3434
}
3535

36-
// OrphanTerraformState removes all the resources from the provided Terraform
37-
// state. When the new state is used, Terraform will operate as if none
38-
// of the resources in the original state exist.
39-
func OrphanTerraformState(state []byte) ([]byte, error) {
40-
stateMap := make(map[string]interface{})
41-
err := json.Unmarshal(state, &stateMap)
42-
if err != nil {
43-
return nil, err
44-
}
45-
46-
_, ok := stateMap["resources"]
47-
if !ok {
48-
return nil, xerrors.Errorf("no resources detected, is this terraform state?")
49-
}
50-
51-
stateMap["resources"] = []int{}
52-
53-
return json.Marshal(stateMap)
54-
}
55-
5636
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
5737
type CreateWorkspaceBuildRequest struct {
5838
TemplateVersionID uuid.UUID `json:"template_version_id,omitempty"`
5939
Transition WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"`
6040
DryRun bool `json:"dry_run,omitempty"`
6141
ProvisionerState []byte `json:"state,omitempty"`
42+
// Orphan may be set for the Destroy transition.
43+
Orphan bool `json:"orphan,omitempty"`
6244
// ParameterValues are optional. It will write params to the 'workspace' scope.
6345
// This will overwrite any existing parameters with the same name.
6446
// This will not delete old params not included in this list.

provisionersdk/orphan.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package provisionersdk
2+
3+
import (
4+
"encoding/json"
5+
6+
"golang.org/x/xerrors"
7+
)
8+
9+
// OrphanState removes all the resources from the provided
10+
// state. When the new state is used, the provisioner will operate as if none
11+
// of the resources in the original state exist.
12+
func OrphanState(state []byte) ([]byte, error) {
13+
if len(state) == 0 {
14+
// Presume that state is already orphaned, or we're using
15+
// a no-op provisioner.
16+
return state, nil
17+
}
18+
19+
stateMap := make(map[string]interface{})
20+
err := json.Unmarshal(state, &stateMap)
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
_, ok := stateMap["resources"]
26+
if !ok {
27+
return nil, xerrors.Errorf("no resources detected, is this terraform state?")
28+
}
29+
30+
// Terraform wants a resources array.
31+
stateMap["resources"] = []struct{}{}
32+
33+
return json.Marshal(stateMap)
34+
}

codersdk/workspaces_test.go renamed to provisionersdk/orphan_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
package codersdk_test
1+
package provisionersdk_test
22

33
import (
44
"reflect"
55
"testing"
66

7-
"github.com/coder/coder/codersdk"
7+
"github.com/coder/coder/provisionersdk"
88
)
99

10-
func TestOrphanTerraformState(t *testing.T) {
10+
func TestOrphanState(t *testing.T) {
1111
t.Parallel()
1212

1313
type args struct {
@@ -28,13 +28,13 @@ func TestOrphanTerraformState(t *testing.T) {
2828
t.Run(tt.name, func(t *testing.T) {
2929
t.Parallel()
3030

31-
got, err := codersdk.OrphanTerraformState(tt.args.state)
31+
got, err := provisionersdk.OrphanState(tt.args.state)
3232
if (err != nil) != tt.wantErr {
33-
t.Errorf("OrphanTerraformState() error = %v, wantErr %v", err, tt.wantErr)
33+
t.Errorf("OrphanState() error = %v, wantErr %v", err, tt.wantErr)
3434
return
3535
}
3636
if !reflect.DeepEqual(got, tt.want) {
37-
t.Errorf("OrphanTerraformState() = %s, want %s", got, tt.want)
37+
t.Errorf("OrphanState() = %s, want %s", got, tt.want)
3838
}
3939
})
4040
}

0 commit comments

Comments
 (0)