8000 feat: run a terraform plan before creating workspaces with the given template parameters by deansheather · Pull Request #1732 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: run a terraform plan before creating workspaces with the given template parameters #1732

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 1, 2022
Prev Previous commit
Next Next commit
big tests
  • Loading branch information
deansheather committed May 27, 2022
commit cfe34afcde3e257f497acf3d2a2aeba0459ae1ae
4 changes: 2 additions & 2 deletions cli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ func create() *cobra.Command {
Silent: true,
})
if err != nil {
// TODO: reprompt for parameter values if we deem it to be a
// validation error
// TODO (Dean): reprompt for parameter values if we deem it to
// be a validation error
Comment on lines +197 to +198
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: open an issue

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened two issues:

#1872 - for integrating with frontend
#1873 - for reprompting for values in CLI

return xerrors.Errorf("plan workspace: %w", err)
}

Expand Down
48 changes: 48 additions & 0 deletions cli/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli_test

import (
"context"
"database/sql"
"fmt"
"os"
"testing"
Expand All @@ -12,6 +13,8 @@ import (

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/pty/ptytest"
Expand Down Expand Up @@ -249,6 +252,7 @@ func TestCreate(t *testing.T) {
<-doneChan
removeTmpDirUntilSuccess(t, tempDir)
})

t.Run("WithParameterFileNotContainingTheValue", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
Expand Down Expand Up @@ -279,6 +283,50 @@ func TestCreate(t *testing.T) {
<-doneChan
removeTmpDirUntilSuccess(t, tempDir)
})

t.Run("FailedPlan", func(t *testing.T) {
t.Parallel()
client, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionDryRun: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Error: "test error",
},
},
},
},
})

// The template import job should end up failed, but we need it to be
// succeeded so the plan can begin.
version = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
require.Equal(t, codersdk.ProvisionerJobFailed, version.Job.Status, "job is not failed")
err := api.Database.UpdateProvisionerJobWithCompleteByID(context.Background(), database.UpdateProvisionerJobWithCompleteByIDParams{
ID: version.Job.ID,
CompletedAt: sql.NullTime{
Time: time.Now(),
Valid: true,
},
UpdatedAt: time.Now(),
Error: sql.NullString{},
})
require.NoError(t, err, "update provisioner job")

_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "create", "test")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())

err = cmd.Execute()
require.Error(t, err)
require.ErrorContains(t, err, "plan workspace")
})
}

func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Response {
Expand Down
26 changes: 26 additions & 0 deletions coderd/coderd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
require.NoError(t, err, "upload file")
workspaceResources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err, "workspace resources")
templateVersionPlanJob, err := client.CreateTemplateVersionPlan(ctx, version.ID, codersdk.CreateTemplateVersionPlanRequest{
ParameterValues: []codersdk.CreateParameterRequest{},
})
require.NoError(t, err, "template version plan")

// Always fail auth from this point forward
authorizer.AlwaysReturn = rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil)
Expand Down Expand Up @@ -264,6 +268,27 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID).WithID(template.ID.String()),
},
"POST:/api/v2/templateversions/{templateversion}/plan": {
// The first check is to read the template
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
},
"GET:/api/v2/templateversions/{templateversion}/plan/{templateversionplan}": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
},
"GET:/api/v2/templateversions/{templateversion}/plan/{templateversionplan}/resources": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
},
"GET:/api/v2/templateversions/{templateversion}/plan/{templateversionplan}/logs": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
},
"PATCH:/api/v2/templateversions/{templateversion}/plan/{templateversionplan}/cancel": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
},
"GET:/api/v2/provisionerdaemons": {
StatusCode: http.StatusOK,
AssertObject: rbac.ResourceProvisionerDaemon.WithID(provisionerds[0].ID.String()),
Expand Down Expand Up @@ -326,6 +351,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
route = strings.ReplaceAll(route, "{hash}", file.Hash)
route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String())
route = strings.ReplaceAll(route, "{templateversion}", version.ID.String())
route = strings.ReplaceAll(route, "{templateversionplan}", templateVersionPlanJob.ID.String())
route = strings.ReplaceAll(route, "{templatename}", template.Name)
// Only checking org scoped params here
route = strings.ReplaceAll(route, "{scope}", string(organizationParam.Scope))
Expand Down
52 changes: 35 additions & 17 deletions coderd/templateversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ func (api *API) templateVersionParameters(rw http.ResponseWriter, r *http.Reques
func (api *API) createTemplateVersionPlan(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
templateVersion := httpmw.TemplateVersionParam(r)
if !api.Authorize(rw, r, rbac.ActionRead, templateVersion) {
return
}
// We use the workspace RBAC check since we don't want to allow plans if the
// user can't create workspaces.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically correct - the best kind of correct.

if !api.Authorize(rw, r, rbac.ActionCreate,
rbac.ResourceWorkspace.InOrg(templateVersion.OrganizationID).WithOwner(apiKey.UserID.String())) {
return
}

var req codersdk.CreateTemplateVersionPlanRequest
if !httpapi.Read(rw, r, &req) {
Expand Down Expand Up @@ -238,7 +247,7 @@ func (api *API) createTemplateVersionPlan(rw http.ResponseWriter, r *http.Reques
}

func (api *API) templateVersionPlan(rw http.ResponseWriter, r *http.Request) {
job, ok := getTemplateVersionPlanJob(api.Database, rw, r)
job, ok := api.fetchTemplateVersionPlanJob(rw, r)
if !ok {
return
}
Expand All @@ -247,7 +256,7 @@ func (api *API) templateVersionPlan(rw http.ResponseWriter, r *http.Request) {
}

func (api *API) templateVersionPlanResources(rw http.ResponseWriter, r *http.Request) {
job, ok := getTemplateVersionPlanJob(api.Database, rw, r)
job, ok := api.fetchTemplateVersionPlanJob(rw, r)
if !ok {
return
}
Expand All @@ -256,7 +265,7 @@ func (api *API) templateVersionPlanResources(rw http.ResponseWriter, r *http.Req
}

func (api *API) templateVersionPlanLogs(rw http.ResponseWriter, r *http.Request) {
job, ok := getTemplateVersionPlanJob(api.Database, rw, r)
job, ok := api.fetchTemplateVersionPlanJob(rw, r)
if !ok {
return
}
Expand All @@ -265,10 +274,16 @@ func (api *API) templateVersionPlanLogs(rw http.ResponseWriter, r *http.Request)
}

func (api *API) templateVersionPlanCancel(rw http.ResponseWriter, r *http.Request) {
job, ok := getTemplateVersionPlanJob(api.Database, rw, r)
templateVersion := httpmw.TemplateVersionParam(r)

job, ok := api.fetchTemplateVersionPlanJob(rw, r)
if !ok {
return
}
if !api.Authorize(rw, r, rbac.ActionUpdate,
rbac.ResourceWorkspace.InOrg(templateVersion.OrganizationID).WithOwner(job.InitiatorID.String())) {
return
}

if job.CompletedAt.Valid {
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Expand Down Expand Up @@ -302,12 +317,14 @@ func (api *API) templateVersionPlanCancel(rw http.ResponseWriter, r *http.Reques
})
}

func getTemplateVersionPlanJob(db database.Store, rw http.ResponseWriter, r *http.Request) (database.ProvisionerJob, bool) {
func (api *API) fetchTemplateVersionPlanJob(rw http.ResponseWriter, r *http.Request) (database.ProvisionerJob, bool) {
var (
apiKey = httpmw.APIKey(r)
templateVersion = httpmw.TemplateVersionParam(r)
jobID = chi.URLParam(r, "jobID")
)
if !api.Authorize(rw, r, rbac.ActionRead, templateVersion) {
return database.ProvisionerJob{}, false
}

jobUUID, err := uuid.Parse(jobID)
if err != nil {
Expand All @@ -317,7 +334,7 @@ func getTemplateVersionPlanJob(db database.Store, rw http.ResponseWriter, r *htt
return database.ProvisionerJob{}, false
}

job, err := db.GetProvisionerJobByID(r.Context(), jobUUID)
job, err := api.Database.GetProvisionerJobByID(r.Context(), jobUUID)
if xerrors.Is(err, sql.ErrNoRows) {
httpapi.Forbidden(rw)
return database.ProvisionerJob{}, false
Expand All @@ -332,9 +349,9 @@ func getTemplateVersionPlanJob(db database.Store, rw http.ResponseWriter, r *htt
httpapi.Forbidden(rw)
return database.ProvisionerJob{}, false
}
// TODO: real RBAC
if job.InitiatorID != apiKey.UserID {
httpapi.Forbidden(rw)
// Do a workspace resource check since it's basically a workspace plan.
if !api.Authorize(rw, r, rbac.ActionRead,
rbac.ResourceWorkspace.InOrg(templateVersion.OrganizationID).WithOwner(job.InitiatorID.String())) {
return database.ProvisionerJob{}, false
}

Expand Down Expand Up @@ -645,12 +662,13 @@ func (api *API) templateVersionLogs(rw http.ResponseWriter, r *http.Request) {

func convertTemplateVersion(version database.TemplateVersion, job codersdk.ProvisionerJob) codersdk.TemplateVersion {
return codersdk.TemplateVersion{
ID: version.ID,
TemplateID: &version.TemplateID.UUID,
CreatedAt: version.CreatedAt,
UpdatedAt: version.UpdatedAt,
Name: version.Name,
Job: job,
Readme: version.Readme,
ID: version.ID,
TemplateID: &version.TemplateID.UUID,
OrganizationID: version.OrganizationID,
CreatedAt: version.CreatedAt,
UpdatedAt: version.UpdatedAt,
Name: version.Name,
Job: job,
Readme: version.Readme,
}
}
Loading
0