chore: distinct operations for provisioner's 'parse', 'init', 'plan', 'apply', 'graph' (#21064)

Provisioner steps broken into smaller granular actions.
Changes:
- `ExtractArchive` moved to `init` request (was in `configure`)
- Writing `tfstate` moved to `plan` (was in `configure`)
- Moved most plan/apply outputs to `GraphComplete`
This commit is contained in:
Steven Masley
2025-12-15 11:26:41 -06:00
committed by GitHub
parent 103967ed02
commit 3194bcfc9e
79 changed files with 3444 additions and 2164 deletions
+12 -8
View File
@@ -301,11 +301,13 @@ func TestCreate(t *testing.T) {
func prepareEchoResponses(parameters []*proto.RichParameter, presets ...*proto.Preset) *echo.Responses { func prepareEchoResponses(parameters []*proto.RichParameter, presets ...*proto.Preset) *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: parameters, Parameters: parameters,
Presets: presets, Presets: presets,
}, },
@@ -1573,11 +1575,13 @@ func TestCreateValidateRichParameters(t *testing.T) {
func TestCreateWithGitAuth(t *testing.T) { func TestCreateWithGitAuth(t *testing.T) {
t.Parallel() t.Parallel()
echoResponses := &echo.Responses{ echoResponses := &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
ExternalAuthProviders: []*proto.ExternalAuthProviderResource{{Id: "github"}}, ExternalAuthProviders: []*proto.ExternalAuthProviderResource{{Id: "github"}},
}, },
}, },
+3 -3
View File
@@ -306,10 +306,10 @@ func TestRestartWithParameters(t *testing.T) {
echoResponses := func() *echo.Responses { echoResponses := func() *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: immutableParameterName, Name: immutableParameterName,
+5 -5
View File
@@ -155,7 +155,7 @@ func TestSSH(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
@@ -244,7 +244,7 @@ func TestSSH(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
@@ -305,7 +305,7 @@ func TestSSH(t *testing.T) {
echoResponses := &echo.Responses{ echoResponses := &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
} }
version := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, echoResponses) version := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, echoResponses)
@@ -326,7 +326,7 @@ func TestSSH(t *testing.T) {
echoResponses2 := &echo.Responses{ echoResponses2 := &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken2), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken2),
} }
version = coderdtest.UpdateTemplateVersion(t, ownerClient, owner.OrganizationID, echoResponses2, template.ID) version = coderdtest.UpdateTemplateVersion(t, ownerClient, owner.OrganizationID, echoResponses2, template.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version.ID)
@@ -655,7 +655,7 @@ func TestSSH(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
+12 -10
View File
@@ -36,10 +36,10 @@ const (
func mutableParamsResponse() *echo.Responses { func mutableParamsResponse() *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: mutableParameterName, Name: mutableParameterName,
@@ -59,10 +59,10 @@ func mutableParamsResponse() *echo.Responses {
func immutableParamsResponse() *echo.Responses { func immutableParamsResponse() *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: immutableParameterName, Name: immutableParameterName,
@@ -83,11 +83,13 @@ func TestStart(t *testing.T) {
echoResponses := func() *echo.Responses { echoResponses := func() *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: ephemeralParameterName, Name: ephemeralParameterName,
+4 -12
View File
@@ -285,19 +285,10 @@ func createAITaskTemplate(t *testing.T, client *codersdk.Client, orgID uuid.UUID
taskAppID := uuid.New() taskAppID := uuid.New()
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
HasAiTasks: true,
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Name: "example", Name: "example",
@@ -321,6 +312,7 @@ func createAITaskTemplate(t *testing.T, client *codersdk.Client, orgID uuid.UUID
}, },
}, },
}, },
HasAiTasks: true,
AiTasks: []*proto.AITask{ AiTasks: []*proto.AITask{
{ {
AppId: taskAppID.String(), AppId: taskAppID.String(),
+3 -3
View File
@@ -282,10 +282,10 @@ func TestTemplatePresets(t *testing.T) {
func templateWithPresets(presets []*proto.Preset) *echo.Responses { func templateWithPresets(presets []*proto.Preset) *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Presets: presets, Presets: presets,
}, },
}, },
+3 -24
View File
@@ -1306,31 +1306,10 @@ func createEchoResponsesWithTemplateVariables(templateVariables []*proto.Templat
func completeWithAgent() *echo.Responses { func completeWithAgent() *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
},
},
},
},
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Type: "compute", Type: "compute",
+1
View File
@@ -71,6 +71,7 @@ func TestTemplateVersionsArchive(t *testing.T) {
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyFailed, ProvisionApply: echo.ApplyFailed,
ProvisionPlan: echo.PlanFailed, ProvisionPlan: echo.PlanFailed,
ProvisionInit: echo.InitComplete,
}, func(request *codersdk.CreateTemplateVersionRequest) { }, func(request *codersdk.CreateTemplateVersionRequest) {
request.TemplateID = template.ID request.TemplateID = template.ID
}) })
+1 -1
View File
@@ -58,7 +58,7 @@ func TestWorkspaceActivityBump(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(agentToken), ProvisionGraph: echo.ProvisionGraphWithAgent(agentToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+19 -27
View File
@@ -61,19 +61,11 @@ func TestTasks(t *testing.T) {
taskAppID := uuid.New() taskAppID := uuid.New()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
HasAiTasks: true, HasAiTasks: true,
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Name: "example", Name: "example",
@@ -951,8 +943,8 @@ func TestTasksCreate(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -995,8 +987,8 @@ func TestTasksCreate(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}}, Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
@@ -1097,8 +1089,8 @@ func TestTasksCreate(t *testing.T) {
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -1218,8 +1210,8 @@ func TestTasksCreate(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -1275,8 +1267,8 @@ func TestTasksCreate(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -1309,8 +1301,8 @@ func TestTasksCreate(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -1359,8 +1351,8 @@ func TestTasksCreate(t *testing.T) {
version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -1371,8 +1363,8 @@ func TestTasksCreate(t *testing.T) {
version2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
+3 -30
View File
@@ -476,37 +476,10 @@ func TestAuditLogsFilter(t *testing.T) {
func completeWithAgentAndApp() *echo.Responses { func completeWithAgentAndApp() *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
Apps: []*proto.App{
{
Slug: "app",
DisplayName: "App",
},
},
},
},
},
},
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Type: "compute", Type: "compute",
+12 -16
View File
@@ -233,9 +233,9 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
// Since initial version has no parameters, any parameters in the new version will be incompatible // Since initial version has no parameters, any parameters in the new version will be incompatible
res = &echo.Responses{ res = &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: "new", Name: "new",
@@ -1105,8 +1105,10 @@ func TestExecutorFailedWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyFailed, ProvisionApply: echo.ApplyFailed,
ProvisionGraph: echo.GraphComplete,
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds()) ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds())
@@ -1644,10 +1646,10 @@ func mustProvisionWorkspaceWithParameters(t *testing.T, client *codersdk.Client,
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: richParameters, Parameters: richParameters,
}, },
}, },
@@ -1774,17 +1776,10 @@ func TestExecutorTaskWorkspace(t *testing.T) {
taskAppID := uuid.New() taskAppID := uuid.New()
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{HasAiTasks: true}, Graph: &proto.GraphComplete{
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Agents: []*proto.Agent{ Agents: []*proto.Agent{
@@ -1804,6 +1799,7 @@ func TestExecutorTaskWorkspace(t *testing.T) {
}, },
}, },
}, },
HasAiTasks: true,
AiTasks: []*proto.AITask{ AiTasks: []*proto.AITask{
{ {
AppId: taskAppID.String(), AppId: taskAppID.String(),
+1 -1
View File
@@ -199,7 +199,7 @@ func TestDERPForceWebSockets(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+15 -3
View File
@@ -50,12 +50,24 @@ func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UU
} }
files := echo.WithExtraFiles(extraFiles) files := echo.WithExtraFiles(extraFiles)
files.ProvisionInit = []*proto.Response{{
Type: &proto.Response_Init{
Init: &proto.InitComplete{
ModuleFiles: args.ModulesArchive,
},
},
}}
files.ProvisionPlan = []*proto.Response{{ files.ProvisionPlan = []*proto.Response{{
Type: &proto.Response_Plan{ Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{ Plan: &proto.PlanComplete{
Plan: args.Plan, Plan: args.Plan,
ModuleFiles: args.ModulesArchive, },
Parameters: args.StaticParams, },
}}
files.ProvisionGraph = []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: args.StaticParams,
}, },
}, },
}} }}
+6 -6
View File
@@ -476,7 +476,7 @@ func TestExternalAuthCallback(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -507,7 +507,7 @@ func TestExternalAuthCallback(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -607,7 +607,7 @@ func TestExternalAuthCallback(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -668,7 +668,7 @@ func TestExternalAuthCallback(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -714,7 +714,7 @@ func TestExternalAuthCallback(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -779,7 +779,7 @@ func TestExternalAuthCallback(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgentAndAPIKeyScope(authToken, tt.apiKeyScope), ProvisionGraph: echo.ProvisionGraphWithAgentAndAPIKeyScope(authToken, tt.apiKeyScope),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+2 -2
View File
@@ -111,7 +111,7 @@ func TestAgentGitSSHKey(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
project := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) project := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -149,7 +149,7 @@ func TestAgentGitSSHKey_APIKeyScopes(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgentAndAPIKeyScope(authToken, tt.apiKeyScope), ProvisionGraph: echo.ProvisionGraphWithAgentAndAPIKeyScope(authToken, tt.apiKeyScope),
}) })
project := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) project := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+10 -16
View File
@@ -78,7 +78,7 @@ func TestDeploymentInsights(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart]) require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
@@ -168,7 +168,7 @@ func TestUserActivityInsights_SanityCheck(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart]) require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
@@ -266,7 +266,7 @@ func TestUserLatencyInsights(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart]) require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
@@ -641,22 +641,16 @@ func TestTemplateInsights_Golden(t *testing.T) {
// Create the template version and template. // Create the template version and template.
version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: parameters, Parameters: parameters,
Resources: resources,
}, },
}, },
}, },
}, },
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: resources,
},
},
}},
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -1561,9 +1555,9 @@ func TestUserActivityInsights_Golden(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: resources, Resources: resources,
}, },
}, },
@@ -536,9 +536,9 @@ func TestAgents(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
@@ -866,7 +866,7 @@ func prepareWorkspaceAndAgent(ctx context.Context, t *testing.T, client *codersd
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+1 -1
View File
@@ -1771,7 +1771,7 @@ func TestTemplateMetrics(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Equal(t, -1, template.ActiveUserCount) require.Equal(t, -1, template.ActiveUserCount)
+21 -20
View File
@@ -760,7 +760,7 @@ func TestPatchCancelTemplateVersion(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{}, Log: &proto.Log{},
}, },
@@ -793,7 +793,7 @@ func TestPatchCancelTemplateVersion(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{}, Log: &proto.Log{},
}, },
@@ -857,9 +857,9 @@ func TestTemplateVersionsExternalAuth(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
ExternalAuthProviders: []*proto.ExternalAuthProviderResource{{Id: "github", Optional: true}}, ExternalAuthProviders: []*proto.ExternalAuthProviderResource{{Id: "github", Optional: true}},
}, },
}, },
@@ -912,9 +912,9 @@ func TestTemplateVersionResources(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -953,7 +953,7 @@ func TestTemplateVersionLogs(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{ Log: &proto.Log{
Level: proto.LogLevel_INFO, Level: proto.LogLevel_INFO,
@@ -961,8 +961,8 @@ func TestTemplateVersionLogs(t *testing.T) {
}, },
}, },
}, { }, {
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -1211,15 +1211,15 @@ func TestTemplateVersionDryRun(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{}, Log: &proto.Log{},
}, },
}, },
{ {
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{resource}, Resources: []*proto.Resource{resource},
}, },
}, },
@@ -2060,10 +2060,10 @@ func TestTemplateVersionParameters_Order(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: firstParameterName, Name: firstParameterName,
@@ -2133,6 +2133,7 @@ func TestTemplateArchiveVersions(t *testing.T) {
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanFailed, ProvisionPlan: echo.PlanFailed,
ProvisionApply: echo.ApplyFailed, ProvisionApply: echo.ApplyFailed,
ProvisionInit: echo.InitComplete,
}, func(req *codersdk.CreateTemplateVersionRequest) { }, func(req *codersdk.CreateTemplateVersionRequest) {
req.TemplateID = template.ID req.TemplateID = template.ID
}) })
@@ -2228,10 +2229,10 @@ func TestTemplateVersionHasExternalAgent(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitMedium) ctx := testutil.Context(t, testutil.WaitMedium)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Name: "example", Name: "example",
+4 -4
View File
@@ -495,7 +495,7 @@ func TestWorkspaceAgentConnectRPC(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -506,9 +506,9 @@ func TestWorkspaceAgentConnectRPC(t *testing.T) {
version = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
+3 -3
View File
@@ -463,9 +463,9 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
+3 -3
View File
@@ -121,9 +121,9 @@ func Test_ResolveRequest(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
+42 -22
View File
@@ -556,13 +556,16 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
ProvisionPlan: echo.PlanComplete,
// Echo will never applying since there is no complete message
ProvisionApply: []*proto.Response{{ ProvisionApply: []*proto.Response{{
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{}, Log: &proto.Log{},
}, },
}}, }},
ProvisionPlan: echo.PlanComplete,
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -603,13 +606,16 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Logger: &logger}) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Logger: &logger})
owner := coderdtest.CreateFirstUser(t, client) owner := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
ProvisionPlan: echo.PlanComplete,
// Echo will never applying
ProvisionApply: []*proto.Response{{ ProvisionApply: []*proto.Response{{
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{}, Log: &proto.Log{},
}, },
}}, }},
ProvisionPlan: echo.PlanComplete,
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
@@ -694,13 +700,16 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
ProvisionPlan: echo.PlanComplete,
// Echo will never applying
ProvisionApply: []*proto.Response{{ ProvisionApply: []*proto.Response{{
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{}, Log: &proto.Log{},
}, },
}}, }},
ProvisionPlan: echo.PlanComplete,
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -791,13 +800,16 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
ProvisionPlan: echo.PlanComplete,
// Echo will never applying
ProvisionApply: []*proto.Response{{ ProvisionApply: []*proto.Response{{
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{}, Log: &proto.Log{},
}, },
}}, }},
ProvisionPlan: echo.PlanComplete,
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -825,9 +837,9 @@ func TestWorkspaceBuildResources(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "first_resource", Name: "first_resource",
Type: "example", Type: "example",
@@ -1032,7 +1044,7 @@ func TestWorkspaceBuildLogs(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{ Log: &proto.Log{
Level: proto.LogLevel_INFO, Level: proto.LogLevel_INFO,
@@ -1040,8 +1052,8 @@ func TestWorkspaceBuildLogs(t *testing.T) {
}, },
}, },
}, { }, {
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -1208,9 +1220,9 @@ func TestWorkspaceDeleteSuspendedUser(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Error: "", Error: "",
Resources: nil, Resources: nil,
Parameters: nil, Parameters: nil,
@@ -1488,10 +1500,18 @@ func TestPostWorkspaceBuild(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
ProvisionApply: []*proto.Response{{}}, ProvisionPlan: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Error: "failed to plan",
},
},
},
},
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) version = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel() defer cancel()
@@ -1642,9 +1662,9 @@ func TestPostWorkspaceBuild(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Presets: []*proto.Preset{ Presets: []*proto.Preset{
{ {
Name: "autodetected", Name: "autodetected",
+9 -9
View File
@@ -26,9 +26,9 @@ func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "somename", Name: "somename",
Type: "someinstance", Type: "someinstance",
@@ -70,9 +70,9 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "somename", Name: "somename",
Type: "someinstance", Type: "someinstance",
@@ -151,9 +151,9 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "somename", Name: "somename",
Type: "someinstance", Type: "someinstance",
+67 -65
View File
@@ -213,9 +213,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -254,9 +254,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -299,9 +299,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -357,9 +357,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -735,9 +735,9 @@ func TestWorkspace(t *testing.T) {
authz := coderdtest.AssertRBAC(t, api, client) authz := coderdtest.AssertRBAC(t, api, client)
// Create a plan response with the specified presets and parameters // Create a plan response with the specified presets and parameters
planResponse := &proto.Response{ graphResponse := &proto.Response{
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Presets: tc.presets, Presets: tc.presets,
Parameters: tc.templateVersionParameters, Parameters: tc.templateVersionParameters,
}, },
@@ -746,7 +746,7 @@ func TestWorkspace(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{planResponse}, ProvisionGraph: []*proto.Response{graphResponse},
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -2269,7 +2269,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -2297,7 +2297,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -2328,9 +2328,9 @@ func TestWorkspaceFilterManual(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
@@ -2420,7 +2420,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -2525,10 +2525,10 @@ func TestWorkspaceFilterManual(t *testing.T) {
makeParameters := func(extra ...*proto.RichParameter) *echo.Responses { makeParameters := func(extra ...*proto.RichParameter) *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: append([]*proto.RichParameter{ Parameters: append([]*proto.RichParameter{
{Name: paramOneName, Description: "", Mutable: true, Type: "string"}, {Name: paramOneName, Description: "", Mutable: true, Type: "string"},
{Name: paramTwoName, DisplayName: "", Description: "", Mutable: true, Type: "string"}, {Name: paramTwoName, DisplayName: "", Description: "", Mutable: true, Type: "string"},
@@ -3382,9 +3382,9 @@ func TestWorkspaceWatcher(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
@@ -3467,8 +3467,10 @@ func TestWorkspaceWatcher(t *testing.T) {
// Add a new version that will fail. // Add a new version that will fail.
badVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ badVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
ProvisionApply: []*proto.Response{{ ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{ Apply: &proto.ApplyComplete{
@@ -3536,9 +3538,9 @@ func TestWorkspaceResource(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "beta", Name: "beta",
Type: "example", Type: "example",
@@ -3604,9 +3606,9 @@ func TestWorkspaceResource(t *testing.T) {
} }
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -3679,9 +3681,9 @@ func TestWorkspaceResource(t *testing.T) {
} }
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -3723,9 +3725,9 @@ func TestWorkspaceResource(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -3803,10 +3805,10 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: firstParameterName, Name: firstParameterName,
@@ -3907,10 +3909,10 @@ func TestWorkspaceWithMultiSelectFailure(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: "param", Name: "param",
@@ -3986,10 +3988,10 @@ func TestWorkspaceWithOptionalRichParameters(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: firstParameterName, Name: firstParameterName,
@@ -4077,10 +4079,10 @@ func TestWorkspaceWithEphemeralRichParameters(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: firstParameterName, Name: firstParameterName,
@@ -4879,8 +4881,8 @@ func TestWorkspaceListTasks(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -4949,9 +4951,9 @@ func TestWorkspaceAppUpsertRestart(t *testing.T) {
// Create template version with workspace app // Create template version with workspace app
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "test-resource", Name: "test-resource",
Type: "example", Type: "example",
@@ -5023,9 +5025,9 @@ func TestMultipleAITasksDisallowed(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
HasAiTasks: true, HasAiTasks: true,
AiTasks: []*proto.AITask{ AiTasks: []*proto.AITask{
{ {
@@ -5320,10 +5322,10 @@ func TestWorkspaceCreateWithImplicitPreset(t *testing.T) {
createTemplateWithPresets := func(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse, presets []*proto.Preset) (codersdk.Template, codersdk.TemplateVersion) { createTemplateWithPresets := func(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse, presets []*proto.Preset) (codersdk.Template, codersdk.TemplateVersion) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Presets: presets, Presets: presets,
}, },
}, },
+2 -2
View File
@@ -1015,8 +1015,8 @@ func TestTools(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{{Name: "AI Prompt", Type: "string"}}, Parameters: []*proto.RichParameter{{Name: "AI Prompt", Type: "string"}},
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
+3 -11
View File
@@ -560,20 +560,12 @@ func TestEnterpriseCreateWithPreset(t *testing.T) {
func prepareEchoResponses(parameters []*proto.RichParameter, presets ...*proto.Preset) *echo.Responses { func prepareEchoResponses(parameters []*proto.RichParameter, presets ...*proto.Preset) *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: parameters, Parameters: parameters,
Presets: presets, Presets: presets,
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Type: "compute", Type: "compute",
+6 -48
View File
@@ -24,10 +24,10 @@ import (
func completeWithExternalAgent() *echo.Responses { func completeWithExternalAgent() *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Type: "coder_external_agent", Type: "coder_external_agent",
@@ -46,27 +46,6 @@ func completeWithExternalAgent() *echo.Responses {
}, },
}, },
}, },
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{
{
Type: "coder_external_agent",
Name: "main",
Agents: []*proto.Agent{
{
Name: "external-agent",
OperatingSystem: "linux",
Architecture: "amd64",
},
},
},
},
},
},
},
},
} }
} }
@@ -74,31 +53,10 @@ func completeWithExternalAgent() *echo.Responses {
func completeWithRegularAgent() *echo.Responses { func completeWithRegularAgent() *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "regular-agent",
OperatingSystem: "linux",
Architecture: "amd64",
},
},
},
},
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Type: "compute", Type: "compute",
+7 -7
View File
@@ -660,21 +660,21 @@ func TestManagedAgentLimit(t *testing.T) {
// build. // build.
appID := uuid.NewString() appID := uuid.NewString()
echoRes := &echo.Responses{ echoRes := &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: []*proto.Response{ ProvisionPlan: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{ Plan: &proto.PlanComplete{
Plan: []byte("{}"), Plan: []byte("{}"),
ModuleFiles: []byte{},
HasAiTasks: true,
}, },
}, },
}, },
}, },
ProvisionApply: []*proto.Response{{ ProvisionApply: echo.ApplyComplete,
Type: &proto.Response_Apply{ ProvisionGraph: []*proto.Response{{
Apply: &proto.ApplyComplete{ Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
+1 -1
View File
@@ -62,7 +62,7 @@ func TestAgentGitSSHKeyCustomRoles(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, org.ID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, org.ID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
project := coderdtest.CreateTemplate(t, client, org.ID, version.ID) project := coderdtest.CreateTemplate(t, client, org.ID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+3 -24
View File
@@ -384,10 +384,10 @@ func TestClaimPrebuild(t *testing.T) {
func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses { func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Type: "compute", Type: "compute",
@@ -442,26 +442,5 @@ func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Resp
}, },
}, },
}, },
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
},
},
},
},
},
},
},
},
} }
} }
+13 -18
View File
@@ -256,21 +256,16 @@ func TestProvisionerDaemonServe(t *testing.T) {
authToken := uuid.NewString() authToken := uuid.NewString()
data, err := echo.Tar(&echo.Responses{ data, err := echo.Tar(&echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*sdkproto.Response{{ ProvisionGraph: echo.ProvisionGraphWithAgent(authToken, func(g *sdkproto.GraphComplete) {
Type: &sdkproto.Response_Plan{ g.Resources = []*sdkproto.Resource{{
Plan: &sdkproto.PlanComplete{ Name: "example",
Resources: []*sdkproto.Resource{{ Type: "aws_instance",
Name: "example", Agents: []*sdkproto.Agent{{
Type: "aws_instance", Id: uuid.NewString(),
Agents: []*sdkproto.Agent{{ Name: "example",
Id: uuid.NewString(), }},
Name: "example", }}
}}, }),
}},
},
},
}},
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
}) })
require.NoError(t, err) require.NoError(t, err)
//nolint:gocritic // Not testing file upload in this test. //nolint:gocritic // Not testing file upload in this test.
@@ -446,9 +441,9 @@ func TestProvisionerDaemonServe(t *testing.T) {
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*sdkproto.Response{{ ProvisionGraph: []*sdkproto.Response{{
Type: &sdkproto.Response_Apply{ Type: &sdkproto.Response_Graph{
Apply: &sdkproto.ApplyComplete{ Graph: &sdkproto.GraphComplete{
Resources: []*sdkproto.Resource{{ Resources: []*sdkproto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
+11 -11
View File
@@ -147,7 +147,7 @@ func TestTemplates(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{ Log: &proto.Log{
Level: proto.LogLevel_INFO, Level: proto.LogLevel_INFO,
@@ -155,8 +155,8 @@ func TestTemplates(t *testing.T) {
}, },
}, },
}, { }, {
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "some", Name: "some",
Type: "example", Type: "example",
@@ -2161,10 +2161,10 @@ func TestInvalidateTemplatePrebuilds(t *testing.T) {
}) })
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin()) templateAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
buildPlanResponse := func(presets ...*proto.Preset) *proto.Response { buildGraphResponse := func(presets ...*proto.Preset) *proto.Response {
return &proto.Response{ return &proto.Response{
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Presets: presets, Presets: presets,
Parameters: templateVersionParameters, Parameters: templateVersionParameters,
}, },
@@ -2174,8 +2174,8 @@ func TestInvalidateTemplatePrebuilds(t *testing.T) {
version1 := coderdtest.CreateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{ version1 := coderdtest.CreateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{buildPlanResponse(presetWithParameters1, presetWithParameters2)},
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionGraph: []*proto.Response{buildGraphResponse(presetWithParameters1, presetWithParameters2)},
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version1.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version1.ID)
template := coderdtest.CreateTemplate(t, templateAdminClient, owner.OrganizationID, version1.ID) template := coderdtest.CreateTemplate(t, templateAdminClient, owner.OrganizationID, version1.ID)
@@ -2193,7 +2193,7 @@ func TestInvalidateTemplatePrebuilds(t *testing.T) {
// Given the template is updated... // Given the template is updated...
version2 := coderdtest.UpdateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{ version2 := coderdtest.UpdateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{buildPlanResponse(presetWithParameters2, presetWithParameters3)}, ProvisionGraph: []*proto.Response{buildGraphResponse(presetWithParameters2, presetWithParameters3)},
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
}, template.ID) }, template.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version2.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version2.ID)
@@ -2239,10 +2239,10 @@ func TestInvalidateTemplatePrebuilds_RegularUser(t *testing.T) {
// Given // Given
version1 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, &echo.Responses{ version1 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Presets: []*proto.Preset{presetWithParameters1}, Presets: []*proto.Preset{presetWithParameters1},
Parameters: templateVersionParameters, Parameters: templateVersionParameters,
}, },
+13 -25
View File
@@ -134,10 +134,10 @@ func TestReinitializeAgent(t *testing.T) {
agentToken := uuid.UUID{3} agentToken := uuid.UUID{3}
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Presets: []*proto.Preset{ Presets: []*proto.Preset{
{ {
Name: "test-preset", Name: "test-preset",
@@ -146,25 +146,6 @@ func TestReinitializeAgent(t *testing.T) {
}, },
}, },
}, },
Resources: []*proto.Resource{
{
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
},
},
},
},
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Type: "compute", Type: "compute",
@@ -191,6 +172,13 @@ func TestReinitializeAgent(t *testing.T) {
}, },
}, },
}, },
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{},
},
},
},
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -273,9 +261,9 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
+2 -2
View File
@@ -633,7 +633,7 @@ func TestIssueSignedAppToken(t *testing.T) {
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -756,7 +756,7 @@ func TestReconnectingPTYSignedToken(t *testing.T) {
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+73 -31
View File
@@ -121,9 +121,16 @@ func TestWorkspaceQuota(t *testing.T) {
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Plan{
Apply: &proto.ApplyComplete{ Plan: &proto.PlanComplete{
DailyCost: 1,
},
},
}},
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
@@ -216,14 +223,17 @@ func TestWorkspaceQuota(t *testing.T) {
verifyQuota(ctx, t, client, user.OrganizationID.String(), 0, 4) verifyQuota(ctx, t, client, user.OrganizationID.String(), 0, 4)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Response{ ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_START: planWithCost(2), proto.WorkspaceTransition_START: planWithCost(2),
proto.WorkspaceTransition_STOP: planWithCost(1), proto.WorkspaceTransition_STOP: planWithCost(1),
}, },
ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{ ProvisionGraphMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_START: applyWithCost(2), proto.WorkspaceTransition_START: graphWithCost(2),
proto.WorkspaceTransition_STOP: applyWithCost(1), proto.WorkspaceTransition_STOP: graphWithCost(1),
}, },
}) })
@@ -422,10 +432,19 @@ func TestWorkspaceQuota(t *testing.T) {
// Create a template with a workspace that costs 1 credit // Create a template with a workspace that costs 1 credit
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionInit: echo.InitComplete,
Type: &proto.Response_Apply{ ProvisionPlan: []*proto.Response{{
Apply: &proto.ApplyComplete{ Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
DailyCost: 1,
},
},
}},
ProvisionApply: echo.ApplyComplete,
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
@@ -458,10 +477,19 @@ func TestWorkspaceQuota(t *testing.T) {
// Test with a template that has zero cost - should pass // Test with a template that has zero cost - should pass
versionZeroCost := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ versionZeroCost := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionInit: echo.InitComplete,
Type: &proto.Response_Apply{ ProvisionPlan: []*proto.Response{{
Apply: &proto.ApplyComplete{ Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
DailyCost: 0,
},
},
}},
ProvisionApply: echo.ApplyComplete,
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
@@ -542,10 +570,19 @@ func TestWorkspaceQuota(t *testing.T) {
// Create templates for both organizations // Create templates for both organizations
authToken := uuid.NewString() authToken := uuid.NewString()
version1 := coderdtest.CreateTemplateVersion(t, owner, first.OrganizationID, &echo.Responses{ version1 := coderdtest.CreateTemplateVersion(t, owner, first.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionInit: echo.InitComplete,
Type: &proto.Response_Apply{ ProvisionPlan: []*proto.Response{{
Apply: &proto.ApplyComplete{ Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
DailyCost: 1,
},
},
}},
ProvisionApply: echo.ApplyComplete,
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
@@ -566,10 +603,19 @@ func TestWorkspaceQuota(t *testing.T) {
template1 := coderdtest.CreateTemplate(t, owner, first.OrganizationID, version1.ID) template1 := coderdtest.CreateTemplate(t, owner, first.OrganizationID, version1.ID)
version2 := coderdtest.CreateTemplateVersion(t, owner, second.ID, &echo.Responses{ version2 := coderdtest.CreateTemplateVersion(t, owner, second.ID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{ ProvisionInit: echo.InitComplete,
Type: &proto.Response_Apply{ ProvisionPlan: []*proto.Response{{
Apply: &proto.ApplyComplete{ Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
DailyCost: 1,
},
},
}},
ProvisionApply: echo.ApplyComplete,
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
@@ -1156,20 +1202,16 @@ func planWithCost(cost int32) []*proto.Response {
return []*proto.Response{{ return []*proto.Response{{
Type: &proto.Response_Plan{ Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{ Plan: &proto.PlanComplete{
Resources: []*proto.Resource{{ DailyCost: cost,
Name: "example",
Type: "aws_instance",
DailyCost: cost,
}},
}, },
}, },
}} }}
} }
func applyWithCost(cost int32) []*proto.Response { func graphWithCost(cost int32) []*proto.Response {
return []*proto.Response{{ return []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
+27 -20
View File
@@ -629,6 +629,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyFailed, ProvisionApply: echo.ApplyFailed,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds()) ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds())
@@ -680,6 +682,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyFailed, ProvisionApply: echo.ApplyFailed,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds()) ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds())
@@ -861,7 +865,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
@@ -1384,6 +1388,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
}) })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -1397,6 +1403,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyFailed, ProvisionApply: echo.ApplyFailed,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
}, func(ctvr *codersdk.CreateTemplateVersionRequest) { }, func(ctvr *codersdk.CreateTemplateVersionRequest) {
ctvr.TemplateID = template.ID ctvr.TemplateID = template.ID
}) })
@@ -2579,21 +2587,11 @@ func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Resp
return r return r
} }
applyResponse := func(withAgent bool) *proto.Response { graphResponse := func(withAgent bool) *proto.Response {
return &proto.Response{ return &proto.Response{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{resource(withAgent)}, Resources: []*proto.Resource{resource(withAgent)},
},
},
}
}
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Presets: []*proto.Preset{{ Presets: []*proto.Preset{{
Name: "preset-test", Name: "preset-test",
Parameters: []*proto.PresetParameter{{Name: "k1", Value: "v1"}}, Parameters: []*proto.PresetParameter{{Name: "k1", Value: "v1"}},
@@ -2601,10 +2599,19 @@ func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Resp
}}, }},
}, },
}, },
}
}
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{},
},
}}, }},
ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{ ProvisionGraphMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_START: {applyResponse(true)}, proto.WorkspaceTransition_START: {graphResponse(true)},
proto.WorkspaceTransition_STOP: {applyResponse(false)}, proto.WorkspaceTransition_STOP: {graphResponse(false)},
}, },
} }
} }
@@ -2612,10 +2619,10 @@ func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Resp
func templateWithFailedResponseAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses { func templateWithFailedResponseAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses {
return &echo.Responses{ return &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Presets: []*proto.Preset{ Presets: []*proto.Preset{
{ {
Name: "preset-test", Name: "preset-test",
@@ -74,10 +74,14 @@ func TestRemoteConnector_Mainline(t *testing.T) {
c := resp.Client c := resp.Client
s, err := c.Session(ctx) s, err := c.Session(ctx)
require.NoError(t, err) require.NoError(t, err)
err = s.Send(&sdkproto.Request{Type: &sdkproto.Request_Config{Config: &sdkproto.Config{ err = s.Send(&sdkproto.Request{Type: &sdkproto.Request_Config{Config: &sdkproto.Config{}}})
require.NoError(t, err)
err = s.Send(&sdkproto.Request{Type: &sdkproto.Request_Init{Init: &sdkproto.InitRequest{
TemplateSourceArchive: arc, TemplateSourceArchive: arc,
}}}) }}})
require.NoError(t, err) require.NoError(t, err)
_, err = s.Recv()
require.NoError(t, err)
err = s.Send(&sdkproto.Request{Type: &sdkproto.Request_Parse{Parse: &sdkproto.ParseRequest{}}}) err = s.Send(&sdkproto.Request{Type: &sdkproto.Request_Parse{Parse: &sdkproto.ParseRequest{}}})
require.NoError(t, err) require.NoError(t, err)
r, err := s.Recv() r, err := s.Recv()
+2 -2
View File
@@ -173,7 +173,7 @@ func TestDERP(t *testing.T) {
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -411,7 +411,7 @@ func TestDERPEndToEnd(t *testing.T) {
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+246 -47
View File
@@ -12,6 +12,7 @@ import (
"text/template" "text/template"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/spf13/afero"
"golang.org/x/xerrors" "golang.org/x/xerrors"
protobuf "google.golang.org/protobuf/proto" protobuf "google.golang.org/protobuf/proto"
@@ -21,12 +22,12 @@ import (
"github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/provisionersdk/proto"
) )
// ProvisionApplyWithAgent returns provision responses that will mock a fake // ProvisionGraphWithAgentAndAPIKeyScope returns provision responses that will mock a fake
// "aws_instance" resource with an agent that has the given auth token. // "aws_instance" resource with an agent that has the given auth token.
func ProvisionApplyWithAgentAndAPIKeyScope(authToken string, apiKeyScope string) []*proto.Response { func ProvisionGraphWithAgentAndAPIKeyScope(authToken string, apiKeyScope string) []*proto.Response {
return []*proto.Response{{ return []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example_with_scope", Name: "example_with_scope",
Type: "aws_instance", Type: "aws_instance",
@@ -44,24 +45,29 @@ func ProvisionApplyWithAgentAndAPIKeyScope(authToken string, apiKeyScope string)
}} }}
} }
// ProvisionApplyWithAgent returns provision responses that will mock a fake // ProvisionGraphWithAgent returns provision responses that will mock a fake
// "aws_instance" resource with an agent that has the given auth token. // "aws_instance" resource with an agent that has the given auth token.
func ProvisionApplyWithAgent(authToken string) []*proto.Response { func ProvisionGraphWithAgent(authToken string, muts ...func(g *proto.GraphComplete)) []*proto.Response {
gc := &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: "example",
Auth: &proto.Agent_Token{
Token: authToken,
},
}},
}},
}
for _, mut := range muts {
mut(gc)
}
return []*proto.Response{{ return []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: gc,
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: "example",
Auth: &proto.Agent_Token{
Token: authToken,
},
}},
}},
},
}, },
}} }}
} }
@@ -73,12 +79,19 @@ var (
Parse: &proto.ParseComplete{}, Parse: &proto.ParseComplete{},
}, },
}} }}
// InitComplete is a helper to indicate an empty init completion.
InitComplete = []*proto.Response{{
Type: &proto.Response_Init{
Init: &proto.InitComplete{
ModuleFiles: []byte{},
},
},
}}
// PlanComplete is a helper to indicate an empty provision completion. // PlanComplete is a helper to indicate an empty provision completion.
PlanComplete = []*proto.Response{{ PlanComplete = []*proto.Response{{
Type: &proto.Response_Plan{ Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{ Plan: &proto.PlanComplete{
Plan: []byte("{}"), Plan: []byte("{}"),
ModuleFiles: []byte{},
}, },
}, },
}} }}
@@ -88,7 +101,19 @@ var (
Apply: &proto.ApplyComplete{}, Apply: &proto.ApplyComplete{},
}, },
}} }}
GraphComplete = []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{},
},
}}
InitFailed = []*proto.Response{{
Type: &proto.Response_Init{
Init: &proto.InitComplete{
Error: "failed!",
},
},
}}
// PlanFailed is a helper to convey a failed plan operation // PlanFailed is a helper to convey a failed plan operation
PlanFailed = []*proto.Response{{ PlanFailed = []*proto.Response{{
Type: &proto.Response_Plan{ Type: &proto.Response_Plan{
@@ -105,6 +130,13 @@ var (
}, },
}, },
}} }}
GraphFailed = []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Error: "failed!",
},
},
}}
) )
// Serve starts the echo provisioner. // Serve starts the echo provisioner.
@@ -174,6 +206,59 @@ func (*echo) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan
return provisionersdk.ParseErrorf("complete response missing") return provisionersdk.ParseErrorf("complete response missing")
} }
func (*echo) Init(sess *provisionersdk.Session, req *proto.InitRequest, canceledOrComplete <-chan struct{}) *proto.InitComplete {
err := sess.Files.ExtractArchive(sess.Context(), sess.Logger, afero.NewOsFs(), req.TemplateSourceArchive)
if err != nil {
return provisionersdk.InitErrorf("extract archive: %s", err.Error())
}
responses, err := readResponses(
sess,
"", // transition not supported for init graph responses
"init.protobuf")
if err != nil {
return &proto.InitComplete{Error: err.Error()}
}
for _, response := range responses {
if log := response.GetLog(); log != nil {
sess.ProvisionLog(log.Level, log.Output)
}
if complete := response.GetInit(); complete != nil {
return complete
}
}
// some tests use Echo without a complete response to test cancel
<-canceledOrComplete
return provisionersdk.InitErrorf("canceled")
}
func (*echo) Graph(sess *provisionersdk.Session, req *proto.GraphRequest, canceledOrComplete <-chan struct{}) *proto.GraphComplete {
responses, err := readResponses(
sess,
strings.ToLower(req.GetMetadata().GetWorkspaceTransition().String()),
"graph.protobuf")
if err != nil {
return &proto.GraphComplete{Error: err.Error()}
}
for _, response := range responses {
if log := response.GetLog(); log != nil {
sess.ProvisionLog(log.Level, log.Output)
}
if complete := response.GetGraph(); complete != nil {
if len(complete.AiTasks) > 0 {
// These two fields are linked; if there are AI tasks, indicate that.
complete.HasAiTasks = true
}
return complete
}
}
// some tests use Echo without a complete response to test cancel
<-canceledOrComplete
return provisionersdk.GraphError("canceled")
}
// Plan reads requests from the provided directory to stream responses. // Plan reads requests from the provided directory to stream responses.
func (*echo) Plan(sess *provisionersdk.Session, req *proto.PlanRequest, canceledOrComplete <-chan struct{}) *proto.PlanComplete { func (*echo) Plan(sess *provisionersdk.Session, req *proto.PlanRequest, canceledOrComplete <-chan struct{}) *proto.PlanComplete {
responses, err := readResponses( responses, err := readResponses(
@@ -228,19 +313,73 @@ func (*echo) Shutdown(_ context.Context, _ *proto.Empty) (*proto.Empty, error) {
type Responses struct { type Responses struct {
Parse []*proto.Response Parse []*proto.Response
// ProvisionApply and ProvisionPlan are used to mock ALL responses of // Used to mock ALL responses regardless of transition.
// Apply and Plan, regardless of transition. ProvisionInit []*proto.Response
ProvisionApply []*proto.Response
ProvisionPlan []*proto.Response ProvisionPlan []*proto.Response
ProvisionApply []*proto.Response
ProvisionGraph []*proto.Response
// ProvisionApplyMap and ProvisionPlanMap are used to mock specific // Used to mock specific transition responses. They are prioritized over the generic responses.
// transition responses. They are prioritized over the generic responses.
ProvisionApplyMap map[proto.WorkspaceTransition][]*proto.Response
ProvisionPlanMap map[proto.WorkspaceTransition][]*proto.Response ProvisionPlanMap map[proto.WorkspaceTransition][]*proto.Response
ProvisionApplyMap map[proto.WorkspaceTransition][]*proto.Response
ProvisionGraphMap map[proto.WorkspaceTransition][]*proto.Response
ExtraFiles map[string][]byte ExtraFiles map[string][]byte
} }
func isType[T any](x any) bool {
_, ok := x.(T)
return ok
}
func (r *Responses) Valid() error {
isLog := isType[*proto.Response_Log]
isParse := isType[*proto.Response_Parse]
isInit := isType[*proto.Response_Init]
isDataUpload := isType[*proto.Response_DataUpload]
isChunkPiece := isType[*proto.Response_ChunkPiece]
isPlan := isType[*proto.Response_Plan]
isApply := isType[*proto.Response_Apply]
isGraph := isType[*proto.Response_Graph]
for _, parse := range r.Parse {
ty := parse.Type
if !(isParse(ty) || isLog(ty)) {
return xerrors.Errorf("invalid parse response type: %T", ty)
}
}
for _, init := range r.ProvisionInit {
ty := init.Type
if !(isInit(ty) || isLog(ty) || isChunkPiece(ty) || isDataUpload(ty)) {
return xerrors.Errorf("invalid init response type: %T", ty)
}
}
for _, plan := range r.ProvisionPlan {
ty := plan.Type
if !(isPlan(ty) || isLog(ty)) {
return xerrors.Errorf("invalid plan response type: %T", ty)
}
}
for _, apply := range r.ProvisionApply {
ty := apply.Type
if !(isApply(ty) || isLog(ty)) {
return xerrors.Errorf("invalid apply response type: %T", ty)
}
}
for _, graph := range r.ProvisionGraph {
ty := graph.Type
if !(isGraph(ty) || isLog(ty)) {
return xerrors.Errorf("invalid graph response type: %T", ty)
}
}
return nil
}
// Tar returns a tar archive of responses to provisioner operations. // Tar returns a tar archive of responses to provisioner operations.
func Tar(responses *Responses) ([]byte, error) { func Tar(responses *Responses) ([]byte, error) {
logger := slog.Make() logger := slog.Make()
@@ -255,31 +394,56 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
if responses == nil { if responses == nil {
responses = &Responses{ responses = &Responses{
Parse: ParseComplete, Parse: ParseComplete,
ProvisionApply: ApplyComplete, ProvisionInit: InitComplete,
ProvisionPlan: PlanComplete, ProvisionPlan: PlanComplete,
ProvisionApply: ApplyComplete,
ProvisionGraph: GraphComplete,
ProvisionApplyMap: nil, ProvisionApplyMap: nil,
ProvisionPlanMap: nil, ProvisionPlanMap: nil,
ExtraFiles: nil, ExtraFiles: nil,
} }
} }
// Apply sane defaults for missing responses.
if responses.Parse == nil {
responses.Parse = ParseComplete
}
if responses.ProvisionInit == nil {
responses.ProvisionInit = InitComplete
}
if responses.ProvisionPlan == nil { if responses.ProvisionPlan == nil {
for _, resp := range responses.ProvisionApply { responses.ProvisionPlan = PlanComplete
// If a graph response exists, make sure it matches the plan.
for _, resp := range responses.ProvisionGraph {
if resp.GetLog() != nil { if resp.GetLog() != nil {
responses.ProvisionPlan = append(responses.ProvisionPlan, resp)
continue continue
} }
responses.ProvisionPlan = append(responses.ProvisionPlan, &proto.Response{ if g := resp.GetGraph(); g != nil {
Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ dailycost := int32(0)
Error: resp.GetApply().GetError(), for _, r := range g.GetResources() {
Resources: resp.GetApply().GetResources(), dailycost += r.DailyCost
Parameters: resp.GetApply().GetParameters(), }
ExternalAuthProviders: resp.GetApply().GetExternalAuthProviders(), responses.ProvisionPlan = []*proto.Response{{
Plan: []byte("{}"), Type: &proto.Response_Plan{
ModuleFiles: []byte{}, Plan: &proto.PlanComplete{
}}, Plan: []byte("{}"),
}) //nolint:gosec // the number of resources will not exceed int32
AiTaskCount: int32(len(g.GetAiTasks())),
DailyCost: dailycost,
},
},
}}
break
}
} }
} }
if responses.ProvisionApply == nil {
responses.ProvisionApply = ApplyComplete
}
if responses.ProvisionGraph == nil {
responses.ProvisionGraph = GraphComplete
}
for _, resp := range responses.ProvisionPlan { for _, resp := range responses.ProvisionPlan {
plan := resp.GetPlan() plan := resp.GetPlan()
@@ -315,6 +479,13 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
if err != nil { if err != nil {
return err return err
} }
response := new(proto.Response)
err = protobuf.Unmarshal(data, response)
if err != nil {
return xerrors.Errorf("you must have saved the wrong type, the proto cannot unmarshal: %w", err)
}
logger.Debug(context.Background(), "proto written", slog.F("name", name), slog.F("bytes_written", n)) logger.Debug(context.Background(), "proto written", slog.F("name", name), slog.F("bytes_written", n))
return nil return nil
@@ -325,6 +496,12 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
return nil, err return nil, err
} }
} }
for index, response := range responses.ProvisionInit {
err := writeProto(fmt.Sprintf("%d.init.protobuf", index), response)
if err != nil {
return nil, err
}
}
for index, response := range responses.ProvisionApply { for index, response := range responses.ProvisionApply {
err := writeProto(fmt.Sprintf("%d.apply.protobuf", index), response) err := writeProto(fmt.Sprintf("%d.apply.protobuf", index), response)
if err != nil { if err != nil {
@@ -337,6 +514,12 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
return nil, err return nil, err
} }
} }
for index, response := range responses.ProvisionGraph {
err := writeProto(fmt.Sprintf("%d.graph.protobuf", index), response)
if err != nil {
return nil, err
}
}
for trans, m := range responses.ProvisionApplyMap { for trans, m := range responses.ProvisionApplyMap {
for i, rs := range m { for i, rs := range m {
err := writeProto(fmt.Sprintf("%d.%s.apply.protobuf", i, strings.ToLower(trans.String())), rs) err := writeProto(fmt.Sprintf("%d.%s.apply.protobuf", i, strings.ToLower(trans.String())), rs)
@@ -360,6 +543,14 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
} }
} }
} }
for trans, m := range responses.ProvisionGraphMap {
for i, resp := range m {
err := writeProto(fmt.Sprintf("%d.%s.graph.protobuf", i, strings.ToLower(trans.String())), resp)
if err != nil {
return nil, err
}
}
}
dirs := []string{} dirs := []string{}
for name, content := range responses.ExtraFiles { for name, content := range responses.ExtraFiles {
logger.Debug(ctx, "extra file", slog.F("name", name)) logger.Debug(ctx, "extra file", slog.F("name", name))
@@ -401,8 +592,8 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
// that matches the parameters defined in the responses. Dynamic parameters // that matches the parameters defined in the responses. Dynamic parameters
// parsed these, even in the echo provisioner. // parsed these, even in the echo provisioner.
var mainTF bytes.Buffer var mainTF bytes.Buffer
for _, respPlan := range responses.ProvisionPlan { for _, respPlan := range responses.ProvisionGraph {
plan := respPlan.GetPlan() plan := respPlan.GetGraph()
if plan == nil { if plan == nil {
continue continue
} }
@@ -440,6 +631,11 @@ terraform {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := responses.Valid(); err != nil {
return nil, xerrors.Errorf("responses invalid: %w", err)
}
return buffer.Bytes(), nil return buffer.Bytes(), nil
} }
@@ -508,13 +704,14 @@ data "coder_parameter" "{{ .Name }}" {
func WithResources(resources []*proto.Resource) *Responses { func WithResources(resources []*proto.Resource) *Responses {
return &Responses{ return &Responses{
Parse: ParseComplete, Parse: ParseComplete,
ProvisionApply: []*proto.Response{{Type: &proto.Response_Apply{Apply: &proto.ApplyComplete{ ProvisionInit: InitComplete,
ProvisionApply: []*proto.Response{{Type: &proto.Response_Apply{Apply: &proto.ApplyComplete{}}}},
ProvisionGraph: []*proto.Response{{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
Resources: resources, Resources: resources,
}}}}, }}}},
ProvisionPlan: []*proto.Response{{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ ProvisionPlan: []*proto.Response{{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
Resources: resources, Plan: []byte("{}"),
Plan: []byte("{}"),
}}}}, }}}},
} }
} }
@@ -522,8 +719,10 @@ func WithResources(resources []*proto.Resource) *Responses {
func WithExtraFiles(extraFiles map[string][]byte) *Responses { func WithExtraFiles(extraFiles map[string][]byte) *Responses {
return &Responses{ return &Responses{
Parse: ParseComplete, Parse: ParseComplete,
ProvisionInit: InitComplete,
ProvisionApply: ApplyComplete, ProvisionApply: ApplyComplete,
ProvisionPlan: PlanComplete, ProvisionPlan: PlanComplete,
ProvisionGraph: GraphComplete,
ExtraFiles: extraFiles, ExtraFiles: extraFiles,
} }
} }
+93 -72
View File
@@ -56,7 +56,8 @@ func TestEcho(t *testing.T) {
}, },
} }
data, err := echo.Tar(&echo.Responses{ data, err := echo.Tar(&echo.Responses{
Parse: responses, Parse: responses,
ProvisionInit: echo.InitComplete,
}) })
require.NoError(t, err) require.NoError(t, err)
client, err := api.Session(ctx) client, err := api.Session(ctx)
@@ -65,13 +66,19 @@ func TestEcho(t *testing.T) {
err := client.Close() err := client.Close()
require.NoError(t, err) require.NoError(t, err)
}() }()
err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{ err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}})
require.NoError(t, err)
err = client.Send(&proto.Request{Type: &proto.Request_Init{Init: &proto.InitRequest{
TemplateSourceArchive: data, TemplateSourceArchive: data,
}}}) }}})
require.NoError(t, err) require.NoError(t, err)
_, err = client.Recv()
require.NoError(t, err)
err = client.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}}) err = client.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}})
require.NoError(t, err) require.NoError(t, err)
log, err := client.Recv() log, err := client.Recv()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, responses[0].GetLog().Output, log.GetLog().Output) require.Equal(t, responses[0].GetLog().Output, log.GetLog().Output)
@@ -85,7 +92,7 @@ func TestEcho(t *testing.T) {
ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort) ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort)
defer cancel() defer cancel()
planResponses := []*proto.Response{ graphResponses := []*proto.Response{
{ {
Type: &proto.Response_Log{ Type: &proto.Response_Log{
Log: &proto.Log{ Log: &proto.Log{
@@ -95,27 +102,8 @@ func TestEcho(t *testing.T) {
}, },
}, },
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "resource",
}},
},
},
},
}
applyResponses := []*proto.Response{
{
Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "log-output",
},
},
},
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "resource", Name: "resource",
}}, }},
@@ -123,9 +111,12 @@ func TestEcho(t *testing.T) {
}, },
}, },
} }
data, err := echo.Tar(&echo.Responses{ data, err := echo.Tar(&echo.Responses{
ProvisionPlan: planResponses, ProvisionGraph: graphResponses,
ProvisionApply: applyResponses, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionInit: echo.InitComplete,
}) })
require.NoError(t, err) require.NoError(t, err)
client, err := api.Session(ctx) client, err := api.Session(ctx)
@@ -134,30 +125,38 @@ func TestEcho(t *testing.T) {
err := client.Close() err := client.Close()
require.NoError(t, err) require.NoError(t, err)
}() }()
err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{ err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}})
require.NoError(t, err)
err = client.Send(&proto.Request{Type: &proto.Request_Init{Init: &proto.InitRequest{
TemplateSourceArchive: data, TemplateSourceArchive: data,
}}}) }}})
require.NoError(t, err) require.NoError(t, err)
_, err = client.Recv()
require.NoError(t, err)
err = client.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}}) err = client.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}})
require.NoError(t, err) require.NoError(t, err)
log, err := client.Recv() _, err = client.Recv()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, planResponses[0].GetLog().Output, log.GetLog().Output)
complete, err := client.Recv()
require.NoError(t, err)
require.Equal(t, planResponses[1].GetPlan().Resources[0].Name,
complete.GetPlan().Resources[0].Name)
err = client.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}}) err = client.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}})
require.NoError(t, err) require.NoError(t, err)
log, err = client.Recv() _, err = client.Recv()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, applyResponses[0].GetLog().Output, log.GetLog().Output)
complete, err = client.Recv() err = client.Send(&proto.Request{Type: &proto.Request_Graph{Graph: &proto.GraphRequest{
Source: proto.GraphSource_SOURCE_STATE,
}}})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, applyResponses[1].GetApply().Resources[0].Name,
complete.GetApply().Resources[0].Name) log, err := client.Recv()
require.NoError(t, err)
require.Equal(t, graphResponses[0].GetLog().Output, log.GetLog().Output)
complete, err := client.Recv()
require.NoError(t, err)
require.Equal(t, graphResponses[1].GetGraph().Resources[0].Name,
complete.GetGraph().Resources[0].Name)
}) })
t.Run("ProvisionStop", func(t *testing.T) { t.Run("ProvisionStop", func(t *testing.T) {
@@ -165,13 +164,11 @@ func TestEcho(t *testing.T) {
// Stop responses should be returned when the workspace is being stopped. // Stop responses should be returned when the workspace is being stopped.
data, err := echo.Tar(&echo.Responses{ data, err := echo.Tar(&echo.Responses{
ProvisionApply: applyCompleteResource("DEFAULT"), ProvisionApply: echo.ApplyComplete,
ProvisionPlan: planCompleteResource("DEFAULT"), ProvisionPlan: echo.PlanComplete,
ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Response{ ProvisionGraph: graphCompleteResource("DEFAULT"),
proto.WorkspaceTransition_STOP: planCompleteResource("STOP"), ProvisionGraphMap: map[proto.WorkspaceTransition][]*proto.Response{
}, proto.WorkspaceTransition_STOP: graphCompleteResource("STOP"),
ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_STOP: applyCompleteResource("STOP"),
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -182,10 +179,15 @@ func TestEcho(t *testing.T) {
err := client.Close() err := client.Close()
require.NoError(t, err) require.NoError(t, err)
}() }()
err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{ err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}})
require.NoError(t, err)
err = client.Send(&proto.Request{Type: &proto.Request_Init{Init: &proto.InitRequest{
TemplateSourceArchive: data, TemplateSourceArchive: data,
}}}) }}})
require.NoError(t, err) require.NoError(t, err)
_, err = client.Recv()
require.NoError(t, err)
// Do stop. // Do stop.
err = client.Send(&proto.Request{ err = client.Send(&proto.Request{
@@ -199,17 +201,32 @@ func TestEcho(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
_, err = client.Recv()
require.NoError(t, err)
err = client.Send(&proto.Request{
Type: &proto.Request_Graph{
Graph: &proto.GraphRequest{
Metadata: &proto.Metadata{
WorkspaceTransition: proto.WorkspaceTransition_STOP,
},
Source: proto.GraphSource_SOURCE_STATE,
},
},
})
require.NoError(t, err)
complete, err := client.Recv() complete, err := client.Recv()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, require.Equal(t,
"STOP", "STOP",
complete.GetPlan().Resources[0].Name, complete.GetGraph().Resources[0].Name,
) )
// Do start. // Do start.
err = client.Send(&proto.Request{ err = client.Send(&proto.Request{
Type: &proto.Request_Plan{ Type: &proto.Request_Graph{
Plan: &proto.PlanRequest{ Graph: &proto.GraphRequest{
Metadata: &proto.Metadata{ Metadata: &proto.Metadata{
WorkspaceTransition: proto.WorkspaceTransition_START, WorkspaceTransition: proto.WorkspaceTransition_START,
}, },
@@ -222,7 +239,7 @@ func TestEcho(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, require.Equal(t,
"DEFAULT", "DEFAULT",
complete.GetPlan().Resources[0].Name, complete.GetGraph().Resources[0].Name,
) )
}) })
@@ -246,8 +263,8 @@ func TestEcho(t *testing.T) {
}, },
}, },
}, { }, {
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "resource", Name: "resource",
}}, }},
@@ -256,7 +273,9 @@ func TestEcho(t *testing.T) {
}} }}
data, err := echo.Tar(&echo.Responses{ data, err := echo.Tar(&echo.Responses{
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: responses, ProvisionApply: echo.ApplyComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: responses,
}) })
require.NoError(t, err) require.NoError(t, err)
client, err := api.Session(ctx) client, err := api.Session(ctx)
@@ -266,11 +285,17 @@ func TestEcho(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
}() }()
err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{ err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{
TemplateSourceArchive: data, ProvisionerLogLevel: "debug",
ProvisionerLogLevel: "debug",
}}}) }}})
require.NoError(t, err) require.NoError(t, err)
err = client.Send(&proto.Request{Type: &proto.Request_Init{Init: &proto.InitRequest{
TemplateSourceArchive: data,
}}})
require.NoError(t, err)
_, err = client.Recv()
require.NoError(t, err)
// Plan is required before apply // Plan is required before apply
err = client.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}}) err = client.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}})
require.NoError(t, err) require.NoError(t, err)
@@ -280,33 +305,29 @@ func TestEcho(t *testing.T) {
err = client.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}}) err = client.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}})
require.NoError(t, err) require.NoError(t, err)
_, err = client.Recv()
require.NoError(t, err)
err = client.Send(&proto.Request{Type: &proto.Request_Graph{Graph: &proto.GraphRequest{
Source: proto.GraphSource_SOURCE_STATE,
}}})
require.NoError(t, err)
log, err := client.Recv() log, err := client.Recv()
require.NoError(t, err) require.NoError(t, err)
// Skip responses[0] as it's trace level // Skip responses[0] as it's trace level
require.Equal(t, responses[1].GetLog().Output, log.GetLog().Output) require.Equal(t, responses[1].GetLog().Output, log.GetLog().Output)
complete, err = client.Recv() complete, err = client.Recv()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, responses[2].GetApply().Resources[0].Name, require.Equal(t, responses[2].GetGraph().Resources[0].Name,
complete.GetApply().Resources[0].Name) complete.GetGraph().Resources[0].Name)
}) })
} }
func planCompleteResource(name string) []*proto.Response { func graphCompleteResource(name string) []*proto.Response {
return []*proto.Response{{ return []*proto.Response{{
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: name,
}},
},
},
}}
}
func applyCompleteResource(name string) []*proto.Response {
return []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: name, Name: name,
}}, }},
+16 -107
View File
@@ -23,7 +23,6 @@ import (
"cdr.dev/slog" "cdr.dev/slog"
"github.com/coder/coder/v2/provisionersdk/tfpath" "github.com/coder/coder/v2/provisionersdk/tfpath"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/provisionersdk/proto"
) )
@@ -283,7 +282,7 @@ func (e *executor) init(ctx, killCtx context.Context, logr logSink) error {
func checksumFileCRC32(ctx context.Context, logger slog.Logger, path string) uint32 { func checksumFileCRC32(ctx context.Context, logger slog.Logger, path string) uint32 {
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
if err != nil { if err != nil {
logger.Debug(ctx, "file %s does not exist or can't be read, skip checksum calculation") logger.Debug(ctx, "file does not exist or can't be read, skip checksum calculation", slog.F("path", path))
return 0 return 0
} }
return crc32.ChecksumIEEE(content) return crc32.ChecksumIEEE(content)
@@ -330,34 +329,16 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
return nil, xerrors.Errorf("terraform plan: %w", err) return nil, xerrors.Errorf("terraform plan: %w", err)
} }
// Capture the duration of the call to `terraform graph`. plan, err := e.parsePlan(ctx, killCtx, planfilePath)
graphTimings := newTimingAggregator(database.ProvisionerJobTimingStageGraph)
graphTimings.ingest(createGraphTimingsEvent(timingGraphStart))
state, plan, err := e.planResources(ctx, killCtx, planfilePath)
if err != nil { if err != nil {
graphTimings.ingest(createGraphTimingsEvent(timingGraphErrored)) return nil, xerrors.Errorf("show terraform plan file: %w", err)
return nil, xerrors.Errorf("plan resources: %w", err)
} }
planJSON, err := json.Marshal(plan) planJSON, err := json.Marshal(plan)
if err != nil { if err != nil {
return nil, xerrors.Errorf("marshal plan: %w", err) return nil, xerrors.Errorf("marshal plan: %w", err)
} }
graphTimings.ingest(createGraphTimingsEvent(timingGraphComplete))
var moduleFiles []byte
// Skipping modules archiving is useful if the caller does not need it, eg during
// a workspace build. This removes some added costs of sending the modules
// payload back to coderd if coderd is just going to ignore it.
if !req.OmitModuleFiles {
moduleFiles, err = GetModulesArchive(os.DirFS(e.files.WorkDirectory()))
if err != nil {
// TODO: we probably want to persist this error or make it louder eventually
e.logger.Warn(ctx, "failed to archive terraform modules", slog.Error(err))
}
}
// When a prebuild claim attempt is made, log a warning if a resource is due to be replaced, since this will obviate // When a prebuild claim attempt is made, log a warning if a resource is due to be replaced, since this will obviate
// the point of prebuilding if the expensive resource is replaced once claimed! // the point of prebuilding if the expensive resource is replaced once claimed!
var ( var (
@@ -384,18 +365,16 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
} }
} }
state, err := ConvertPlanState(plan)
if err != nil {
return nil, xerrors.Errorf("convert plan state: %w", err)
}
msg := &proto.PlanComplete{ msg := &proto.PlanComplete{
Parameters: state.Parameters, Plan: planJSON,
Resources: state.Resources, DailyCost: state.DailyCost,
ExternalAuthProviders: state.ExternalAuthProviders, ResourceReplacements: resReps,
Timings: graphTimings.aggregate(), AiTaskCount: state.AITaskCount,
Presets: state.Presets,
Plan: planJSON,
ResourceReplacements: resReps,
ModuleFiles: moduleFiles,
HasAiTasks: state.HasAITasks,
AiTasks: state.AITasks,
HasExternalAgents: state.HasExternalAgents,
} }
return msg, nil return msg, nil
@@ -418,42 +397,6 @@ func onlyDataResources(sm tfjson.StateModule) tfjson.StateModule {
return filtered return filtered
} }
// planResources must only be called while the lock is held.
func (e *executor) planResources(ctx, killCtx context.Context, planfilePath string) (*State, *tfjson.Plan, error) {
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
defer span.End()
plan, err := e.parsePlan(ctx, killCtx, planfilePath)
if err != nil {
return nil, nil, xerrors.Errorf("show terraform plan file: %w", err)
}
rawGraph, err := e.graph(ctx, killCtx)
if err != nil {
return nil, nil, xerrors.Errorf("graph: %w", err)
}
modules := []*tfjson.StateModule{}
if plan.PriorState != nil {
// We need the data resources for rich parameters. For some reason, they
// only show up in the PriorState.
//
// We don't want all prior resources, because Quotas (and
// future features) would never know which resources are getting
// deleted by a stop.
filtered := onlyDataResources(*plan.PriorState.Values.RootModule)
modules = append(modules, &filtered)
}
modules = append(modules, plan.PlannedValues.RootModule)
state, err := ConvertState(ctx, modules, rawGraph, e.server.logger)
if err != nil {
return nil, nil, err
}
return state, plan, nil
}
// parsePlan must only be called while the lock is held. // parsePlan must only be called while the lock is held.
func (e *executor) parsePlan(ctx, killCtx context.Context, planfilePath string) (*tfjson.Plan, error) { func (e *executor) parsePlan(ctx, killCtx context.Context, planfilePath string) (*tfjson.Plan, error) {
ctx, span := e.server.startTrace(ctx, tracing.FuncName()) ctx, span := e.server.startTrace(ctx, tracing.FuncName())
@@ -541,9 +484,11 @@ func (e *executor) graph(ctx, killCtx context.Context) (string, error) {
// TODO: When the plan is present, we should probably use it? // TODO: When the plan is present, we should probably use it?
// "-plan=" + e.files.PlanFilePath(), // "-plan=" + e.files.PlanFilePath(),
} }
if ver.GreaterThanOrEqual(version170) { if ver.GreaterThanOrEqual(version170) {
args = append(args, "-type=plan") args = append(args, "-type=plan")
} }
var out strings.Builder var out strings.Builder
cmd := exec.CommandContext(killCtx, e.binaryPath, args...) // #nosec cmd := exec.CommandContext(killCtx, e.binaryPath, args...) // #nosec
cmd.Stdout = &out cmd.Stdout = &out
@@ -602,11 +547,6 @@ func (e *executor) apply(
return nil, xerrors.Errorf("terraform apply: %w", err) return nil, xerrors.Errorf("terraform apply: %w", err)
} }
// `terraform show` & `terraform graph`
state, err := e.stateResources(ctx, killCtx)
if err != nil {
return nil, err
}
statefilePath := e.files.StateFilePath() statefilePath := e.files.StateFilePath()
stateContent, err := os.ReadFile(statefilePath) stateContent, err := os.ReadFile(statefilePath)
if err != nil { if err != nil {
@@ -614,41 +554,10 @@ func (e *executor) apply(
} }
return &proto.ApplyComplete{ return &proto.ApplyComplete{
Parameters: state.Parameters, State: stateContent,
Resources: state.Resources,
ExternalAuthProviders: state.ExternalAuthProviders,
State: stateContent,
AiTasks: state.AITasks,
}, nil }, nil
} }
// stateResources must only be called while the lock is held.
func (e *executor) stateResources(ctx, killCtx context.Context) (*State, error) {
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
defer span.End()
state, err := e.state(ctx, killCtx)
if err != nil {
return nil, err
}
rawGraph, err := e.graph(ctx, killCtx)
if err != nil {
return nil, xerrors.Errorf("get terraform graph: %w", err)
}
converted := &State{}
if state.Values == nil {
return converted, nil
}
converted, err = ConvertState(ctx, []*tfjson.StateModule{
state.Values.RootModule,
}, rawGraph, e.server.logger)
if err != nil {
return nil, err
}
return converted, nil
}
// state must only be called while the lock is held. // state must only be called while the lock is held.
func (e *executor) state(ctx, killCtx context.Context) (*tfjson.State, error) { func (e *executor) state(ctx, killCtx context.Context) (*tfjson.State, error) {
ctx, span := e.server.startTrace(ctx, tracing.FuncName()) ctx, span := e.server.startTrace(ctx, tracing.FuncName())
+45 -27
View File
@@ -4,6 +4,8 @@ package terraform_test
import ( import (
"encoding/json" "encoding/json"
"os"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -15,14 +17,19 @@ import (
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
t.Parallel() t.Parallel()
ctx, api := setupProvisioner(t, nil) cwd, err := os.Getwd()
require.NoError(t, err)
ctx, api := setupProvisioner(t, &provisionerServeOptions{
// Fake all actual terraform, since parse doesn't need it.
binaryPath: filepath.Join(cwd, "testdata", "timings-aggregation", "fake-terraform.sh"),
})
testCases := []struct { testCases := []struct {
Name string Name string
Files map[string]string Files map[string]string
Response *proto.ParseComplete Response *proto.ParseComplete
// If ErrorContains is not empty, then the ParseComplete should have an Error containing the given string ParseErrorContains string
ErrorContains string
}{ }{
{ {
Name: "single-variable", Name: "single-variable",
@@ -63,6 +70,7 @@ func TestParse(t *testing.T) {
"main.tf": `variable "A" { "main.tf": `variable "A" {
validation { validation {
condition = var.A == "value" condition = var.A == "value"
error_message = "A must be 'value'"
} }
}`, }`,
}, },
@@ -80,7 +88,7 @@ func TestParse(t *testing.T) {
Files: map[string]string{ Files: map[string]string{
"main.tf": "a;sd;ajsd;lajsd;lasjdf;a", "main.tf": "a;sd;ajsd;lajsd;lasjdf;a",
}, },
ErrorContains: `The ";" character is not valid.`, ParseErrorContains: `The ";" character is not valid.`,
}, },
{ {
Name: "multiple-variables", Name: "multiple-variables",
@@ -205,6 +213,8 @@ func TestParse(t *testing.T) {
{ {
Name: "workspace-tags", Name: "workspace-tags",
Files: map[string]string{ Files: map[string]string{
`main.tf`: `
`,
"parameters.tf": `data "coder_parameter" "os_selector" { "parameters.tf": `data "coder_parameter" "os_selector" {
name = "os_selector" name = "os_selector"
display_name = "Operating System" display_name = "Operating System"
@@ -266,7 +276,6 @@ func TestParse(t *testing.T) {
Name: "workspace-tags-in-a-single-file", Name: "workspace-tags-in-a-single-file",
Files: map[string]string{ Files: map[string]string{
"main.tf": ` "main.tf": `
data "coder_parameter" "os_selector" { data "coder_parameter" "os_selector" {
name = "os_selector" name = "os_selector"
display_name = "Operating System" display_name = "Operating System"
@@ -330,7 +339,6 @@ func TestParse(t *testing.T) {
Name: "workspace-tags-duplicate-tag", Name: "workspace-tags-duplicate-tag",
Files: map[string]string{ Files: map[string]string{
"main.tf": ` "main.tf": `
data "coder_workspace_tags" "custom_workspace_tags" { data "coder_workspace_tags" "custom_workspace_tags" {
tags = { tags = {
"cluster" = "developers" "cluster" = "developers"
@@ -341,23 +349,22 @@ func TestParse(t *testing.T) {
} }
`, `,
}, },
ErrorContains: `workspace tag "debug" is defined multiple times`, ParseErrorContains: `workspace tag "debug" is defined multiple times`,
}, },
{ {
Name: "workspace-tags-wrong-tag-format", Name: "workspace-tags-wrong-tag-format",
Files: map[string]string{ Files: map[string]string{
"main.tf": ` "main.tf": `
data "coder_workspace_tags" "custom_workspace_tags" {
data "coder_workspace_tags" "custom_workspace_tags" { tags {
tags { cluster = "developers"
cluster = "developers" debug = "yes"
debug = "yes" cache = "no-cache"
cache = "no-cache" }
} }
}
`, `,
}, },
ErrorContains: `"tags" attribute is required by coder_workspace_tags`, ParseErrorContains: `"tags" attribute is required by coder_workspace_tags`,
}, },
{ {
Name: "empty-main", Name: "empty-main",
@@ -379,27 +386,38 @@ func TestParse(t *testing.T) {
t.Run(testCase.Name, func(t *testing.T) { t.Run(testCase.Name, func(t *testing.T) {
t.Parallel() t.Parallel()
session := configure(ctx, t, api, &proto.Config{ session := configure(ctx, t, api, &proto.Config{})
TemplateSourceArchive: testutil.CreateTar(t, testCase.Files), err := sendInit(session, testutil.CreateTar(t, testCase.Files))
}) require.NoError(t, err)
err := session.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}}) // Init stage -- a fake terraform, will always succeed quickly.
for {
msg, err := session.Recv()
require.NoError(t, err)
if msgLog, ok := msg.Type.(*proto.Response_Log); ok {
t.Logf("init log: %s", msgLog.Log.Output)
continue
}
break
}
err = session.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}})
require.NoError(t, err) require.NoError(t, err)
for { for {
msg, err := session.Recv() msg, err := session.Recv()
require.NoError(t, err) require.NoError(t, err)
if testCase.ErrorContains != "" {
require.Contains(t, msg.GetParse().GetError(), testCase.ErrorContains)
break
}
// Ignore logs in this test // Ignore logs in this test
if msg.GetLog() != nil { if msg.GetLog() != nil {
continue continue
} }
if testCase.ParseErrorContains != "" {
require.Contains(t, msg.GetParse().GetError(), testCase.ParseErrorContains)
return // Stop test at this point
}
// Ensure the want and got are equivalent! // Ensure the want and got are equivalent!
want, err := json.Marshal(testCase.Response) want, err := json.Marshal(testCase.Response)
require.NoError(t, err) require.NoError(t, err)
+80
View File
@@ -0,0 +1,80 @@
package terraform
import (
tfjson "github.com/hashicorp/terraform-json"
"github.com/mitchellh/mapstructure"
"golang.org/x/xerrors"
)
type PlanState struct {
DailyCost int32
AITaskCount int32
}
func planModules(plan *tfjson.Plan) []*tfjson.StateModule {
modules := []*tfjson.StateModule{}
if plan.PriorState != nil {
// We need the data resources for rich parameters. For some reason, they
// only show up in the PriorState.
//
// We don't want all prior resources, because Quotas (and
// future features) would never know which resources are getting
// deleted by a stop.
filtered := onlyDataResources(*plan.PriorState.Values.RootModule)
modules = append(modules, &filtered)
}
modules = append(modules, plan.PlannedValues.RootModule)
return modules
}
// ConvertPlanState consumes a terraform plan json output and produces a thinner
// version of `State` to be used before `terraform apply`. `ConvertState`
// requires `terraform graph`, this does not.
func ConvertPlanState(plan *tfjson.Plan) (*PlanState, error) {
modules := planModules(plan)
var dailyCost int32
var aiTaskCount int32
for _, mod := range modules {
err := forEachResource(mod, func(res *tfjson.StateResource) error {
switch res.Type {
case "coder_metadata":
var attrs resourceMetadataAttributes
err := mapstructure.Decode(res.AttributeValues, &attrs)
if err != nil {
return xerrors.Errorf("decode metadata attributes: %w", err)
}
dailyCost += attrs.DailyCost
case "coder_ai_task":
aiTaskCount++
}
return nil
})
if err != nil {
return nil, xerrors.Errorf("parse plan: %w", err)
}
}
return &PlanState{
DailyCost: dailyCost,
AITaskCount: aiTaskCount,
}, nil
}
func forEachResource(input *tfjson.StateModule, do func(res *tfjson.StateResource) error) error {
for _, res := range input.Resources {
err := do(res)
if err != nil {
return xerrors.Errorf("in module %s: %w", input.Address, err)
}
}
for _, mod := range input.ChildModules {
err := forEachResource(mod, do)
if err != nil {
return xerrors.Errorf("in module %s: %w", mod.Address, err)
}
}
return nil
}
+137 -41
View File
@@ -12,6 +12,7 @@ import (
"strings" "strings"
"time" "time"
tfjson "github.com/hashicorp/terraform-json"
"github.com/spf13/afero" "github.com/spf13/afero"
"golang.org/x/xerrors" "golang.org/x/xerrors"
@@ -67,51 +68,34 @@ func (s *server) setupContexts(parent context.Context, canceledOrComplete <-chan
return ctx, cancel, killCtx, kill return ctx, cancel, killCtx, kill
} }
func (s *server) Plan( func (s *server) Init(
sess *provisionersdk.Session, request *proto.PlanRequest, canceledOrComplete <-chan struct{}, sess *provisionersdk.Session, request *proto.InitRequest, canceledOrComplete <-chan struct{},
) *proto.PlanComplete { ) *proto.InitComplete {
ctx, span := s.startTrace(sess.Context(), tracing.FuncName()) ctx, span := s.startTrace(sess.Context(), tracing.FuncName())
defer span.End() defer span.End()
ctx, cancel, killCtx, kill := s.setupContexts(ctx, canceledOrComplete) ctx, cancel, killCtx, kill := s.setupContexts(ctx, canceledOrComplete)
defer cancel() defer cancel()
defer kill() defer kill()
e := s.executor(sess.Files, database.ProvisionerJobTimingStagePlan) e := s.executor(sess.Files, database.ProvisionerJobTimingStageInit)
if err := e.checkMinVersion(ctx); err != nil { if err := e.checkMinVersion(ctx); err != nil {
return provisionersdk.PlanErrorf("%s", err.Error()) return provisionersdk.InitErrorf("%s", err.Error())
} }
logTerraformEnvVars(sess) logTerraformEnvVars(sess)
// If we're destroying, exit early if there's no state. This is necessary to // TODO: These logs should probably be streamed back to the provisioner runner.
// avoid any cases where a workspace is "locked out" of terraform due to err := sess.Files.ExtractArchive(ctx, s.logger, afero.NewOsFs(), request.GetTemplateSourceArchive())
// e.g. bad template param values and cannot be deleted. This is just for
// contingency, in the future we will try harder to prevent workspaces being
// broken this hard.
if request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY && len(sess.Config.State) == 0 {
sess.ProvisionLog(proto.LogLevel_INFO, "The terraform state does not exist, there is nothing to do")
return &proto.PlanComplete{}
}
statefilePath := sess.Files.StateFilePath()
if len(sess.Config.State) > 0 {
err := os.WriteFile(statefilePath, sess.Config.State, 0o600)
if err != nil {
return provisionersdk.PlanErrorf("write statefile %q: %s", statefilePath, err)
}
}
err := CleanStaleTerraformPlugins(sess.Context(), s.cachePath, afero.NewOsFs(), time.Now(), s.logger)
if err != nil { if err != nil {
return provisionersdk.PlanErrorf("unable to clean stale Terraform plugins: %s", err) return provisionersdk.InitErrorf("extract template archive: %s", err)
} }
s.logger.Debug(ctx, "running initialization") err = CleanStaleTerraformPlugins(sess.Context(), s.cachePath, afero.NewOsFs(), time.Now(), s.logger)
if err != nil {
// The JSON output of `terraform init` doesn't include discrete fields for capturing timings of each plugin, return provisionersdk.InitErrorf("unable to clean stale Terraform plugins: %s", err)
// so we capture the whole init process. }
initTimings := newTimingAggregator(database.ProvisionerJobTimingStageInit)
endStage := initTimings.startStage(database.ProvisionerJobTimingStageInit)
s.logger.Debug(ctx, "running terraform initialization")
endStage := e.timings.startStage(database.ProvisionerJobTimingStageInit)
err = e.init(ctx, killCtx, sess) err = e.init(ctx, killCtx, sess)
endStage(err) endStage(err)
if err != nil { if err != nil {
@@ -137,7 +121,7 @@ func (s *server) Plan(
slog.F("provider_coder_stacktrace", stacktrace), slog.F("provider_coder_stacktrace", stacktrace),
) )
} }
return provisionersdk.PlanErrorf("initialize terraform: %s", err) return provisionersdk.InitErrorf("initialize terraform: %s", err)
} }
modules, err := getModules(sess.Files) modules, err := getModules(sess.Files)
@@ -147,8 +131,61 @@ func (s *server) Plan(
s.logger.Error(ctx, "failed to get modules from disk", slog.Error(err)) s.logger.Error(ctx, "failed to get modules from disk", slog.Error(err))
} }
var moduleFiles []byte
// Skipping modules archiving is useful if the caller does not need it, eg during
// a workspace build. This removes some added costs of sending the modules
// payload back to coderd if coderd is just going to ignore it.
if !request.OmitModuleFiles {
moduleFiles, err = GetModulesArchive(os.DirFS(e.files.WorkDirectory()))
if err != nil {
// TODO: we probably want to persist this error or make it louder eventually
e.logger.Warn(ctx, "failed to archive terraform modules", slog.Error(err))
}
}
s.logger.Debug(ctx, "ran initialization") s.logger.Debug(ctx, "ran initialization")
return &proto.InitComplete{
Timings: e.timings.aggregate(),
Modules: modules,
ModuleFiles: moduleFiles,
ModuleFilesHash: nil,
}
}
func (s *server) Plan(
sess *provisionersdk.Session, request *proto.PlanRequest, canceledOrComplete <-chan struct{},
) *proto.PlanComplete {
ctx, span := s.startTrace(sess.Context(), tracing.FuncName())
defer span.End()
ctx, cancel, killCtx, kill := s.setupContexts(ctx, canceledOrComplete)
defer cancel()
defer kill()
e := s.executor(sess.Files, database.ProvisionerJobTimingStagePlan)
if err := e.checkMinVersion(ctx); err != nil {
return provisionersdk.PlanErrorf("%s", err.Error())
}
logTerraformEnvVars(sess)
// If we're destroying, exit early if there's no state. This is necessary to
// avoid any cases where a workspace is "locked out" of terraform due to
// e.g. bad template param values and cannot be deleted. This is just for
// contingency, in the future we will try harder to prevent workspaces being
// broken this hard.
if request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY && len(request.GetState()) == 0 {
sess.ProvisionLog(proto.LogLevel_INFO, "The terraform state does not exist, there is nothing to do")
return &proto.PlanComplete{}
}
statefilePath := sess.Files.StateFilePath()
if len(request.GetState()) > 0 {
err := os.WriteFile(statefilePath, request.GetState(), 0o600)
if err != nil {
return provisionersdk.PlanErrorf("write statefile %q: %s", statefilePath, err)
}
}
env, err := provisionEnv(sess.Config, request.Metadata, request.PreviousParameterValues, request.RichParameterValues, request.ExternalAuthProviders) env, err := provisionEnv(sess.Config, request.Metadata, request.PreviousParameterValues, request.RichParameterValues, request.ExternalAuthProviders)
if err != nil { if err != nil {
return provisionersdk.PlanErrorf("setup env: %s", err) return provisionersdk.PlanErrorf("setup env: %s", err)
@@ -160,20 +197,78 @@ func (s *server) Plan(
return provisionersdk.PlanErrorf("plan vars: %s", err) return provisionersdk.PlanErrorf("plan vars: %s", err)
} }
endPlanStage := e.timings.startStage(database.ProvisionerJobTimingStagePlan) endStage := e.timings.startStage(database.ProvisionerJobTimingStagePlan)
resp, err := e.plan(ctx, killCtx, env, vars, sess, request) resp, err := e.plan(ctx, killCtx, env, vars, sess, request)
endPlanStage(err) endStage(err)
if err != nil { if err != nil {
return provisionersdk.PlanErrorf("%s", err.Error()) return provisionersdk.PlanErrorf("%s", err.Error())
} }
// Prepend init timings since they occur prior to plan timings. resp.Timings = e.timings.aggregate()
// Order is irrelevant; this is merely indicative.
resp.Timings = append(resp.Timings, append(initTimings.aggregate(), e.timings.aggregate()...)...)
resp.Modules = modules
return resp return resp
} }
func (s *server) Graph(
sess *provisionersdk.Session, request *proto.GraphRequest, canceledOrComplete <-chan struct{},
) *proto.GraphComplete {
ctx, span := s.startTrace(sess.Context(), tracing.FuncName())
defer span.End()
ctx, cancel, killCtx, kill := s.setupContexts(ctx, canceledOrComplete)
defer cancel()
defer kill()
e := s.executor(sess.Files, database.ProvisionerJobTimingStageGraph)
if err := e.checkMinVersion(ctx); err != nil {
return provisionersdk.GraphError("%s", err.Error())
}
logTerraformEnvVars(sess)
modules := []*tfjson.StateModule{}
switch request.Source {
case proto.GraphSource_SOURCE_PLAN:
plan, err := e.parsePlan(ctx, killCtx, e.files.PlanFilePath())
if err != nil {
return provisionersdk.GraphError("parse plan for graph: %s", err)
}
modules = planModules(plan)
case proto.GraphSource_SOURCE_STATE:
tfState, err := e.state(ctx, killCtx)
if err != nil {
return provisionersdk.GraphError("load tfstate for graph: %s", err)
}
if tfState.Values != nil {
modules = []*tfjson.StateModule{tfState.Values.RootModule}
}
default:
return provisionersdk.GraphError("unknown graph source: %q", request.Source.String())
}
endStage := e.timings.startStage(database.ProvisionerJobTimingStageGraph)
rawGraph, err := e.graph(ctx, killCtx)
endStage(err)
if err != nil {
return provisionersdk.GraphError("generate graph: %s", err)
}
state, err := ConvertState(ctx, modules, rawGraph, e.server.logger)
if err != nil {
return provisionersdk.GraphError("convert state for graph: %s", err)
}
return &proto.GraphComplete{
Error: "",
Timings: e.timings.aggregate(),
Resources: state.Resources,
Parameters: state.Parameters,
ExternalAuthProviders: state.ExternalAuthProviders,
Presets: state.Presets,
HasAiTasks: state.HasAITasks,
AiTasks: state.AITasks,
HasExternalAgents: state.HasExternalAgents,
}
}
func (s *server) Apply( func (s *server) Apply(
sess *provisionersdk.Session, request *proto.ApplyRequest, canceledOrComplete <-chan struct{}, sess *provisionersdk.Session, request *proto.ApplyRequest, canceledOrComplete <-chan struct{},
) *proto.ApplyComplete { ) *proto.ApplyComplete {
@@ -194,7 +289,7 @@ func (s *server) Apply(
// e.g. bad template param values and cannot be deleted. This is just for // e.g. bad template param values and cannot be deleted. This is just for
// contingency, in the future we will try harder to prevent workspaces being // contingency, in the future we will try harder to prevent workspaces being
// broken this hard. // broken this hard.
if request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY && len(sess.Config.State) == 0 { if request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY && len(request.GetState()) == 0 {
sess.ProvisionLog(proto.LogLevel_INFO, "The terraform plan does not exist, there is nothing to do") sess.ProvisionLog(proto.LogLevel_INFO, "The terraform plan does not exist, there is nothing to do")
return &proto.ApplyComplete{} return &proto.ApplyComplete{}
} }
@@ -217,8 +312,9 @@ func (s *server) Apply(
// In this case, we return Complete with an explicit error message. // In this case, we return Complete with an explicit error message.
stateData, _ := os.ReadFile(statefilePath) stateData, _ := os.ReadFile(statefilePath)
return &proto.ApplyComplete{ return &proto.ApplyComplete{
State: stateData, State: stateData,
Error: errorMessage, Error: errorMessage,
Timings: e.timings.aggregate(),
} }
} }
resp.Timings = e.timings.aggregate() resp.Timings = e.timings.aggregate()
+168 -86
View File
@@ -13,6 +13,7 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
@@ -81,6 +82,27 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont
return ctx, api return ctx, api
} }
// sendInitAndGetResp will send the init request and wait for and return the InitComplete response.
func sendInitAndGetResp(t *testing.T, sess proto.DRPCProvisioner_SessionClient, archive []byte, onLog ...func(log string)) *proto.InitComplete {
t.Helper()
err := sendInit(sess, archive)
require.NoError(t, err)
for {
msg, err := sess.Recv()
require.NoError(t, err)
if logMsg, ok := msg.Type.(*proto.Response_Log); ok {
for _, do := range onLog {
do(logMsg.Log.Output)
}
continue
}
init := msg.GetInit()
require.NotNil(t, init)
return init
}
}
func configure(ctx context.Context, t *testing.T, client proto.DRPCProvisionerClient, config *proto.Config) proto.DRPCProvisioner_SessionClient { func configure(ctx context.Context, t *testing.T, client proto.DRPCProvisionerClient, config *proto.Config) proto.DRPCProvisioner_SessionClient {
t.Helper() t.Helper()
sess, err := client.Session(ctx) sess, err := client.Session(ctx)
@@ -107,6 +129,12 @@ func readProvisionLog(t *testing.T, response proto.DRPCProvisioner_SessionClient
return logBuf.String() return logBuf.String()
} }
func sendInit(sess proto.DRPCProvisioner_SessionClient, archive []byte) error {
return sess.Send(&proto.Request{Type: &proto.Request_Init{Init: &proto.InitRequest{
TemplateSourceArchive: archive,
}}})
}
func sendPlan(sess proto.DRPCProvisioner_SessionClient, transition proto.WorkspaceTransition) error { func sendPlan(sess proto.DRPCProvisioner_SessionClient, transition proto.WorkspaceTransition) error {
return sess.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{ return sess.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{
Metadata: &proto.Metadata{WorkspaceTransition: transition}, Metadata: &proto.Metadata{WorkspaceTransition: transition},
@@ -119,6 +147,12 @@ func sendApply(sess proto.DRPCProvisioner_SessionClient, transition proto.Worksp
}}}) }}})
} }
func sendGraph(sess proto.DRPCProvisioner_SessionClient, source proto.GraphSource) error {
return sess.Send(&proto.Request{Type: &proto.Request_Graph{Graph: &proto.GraphRequest{
Source: source,
}}})
}
// below we exec fake_cancel.sh, which causes the kernel to execute it, and if more than // below we exec fake_cancel.sh, which causes the kernel to execute it, and if more than
// one process tries to do this simultaneously, it can cause "text file busy" // one process tries to do this simultaneously, it can cause "text file busy"
// nolint: paralleltest // nolint: paralleltest
@@ -161,30 +195,46 @@ func TestProvision_Cancel(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
t.Logf("wrote fake terraform script to %s", binPath) t.Logf("wrote fake terraform script to %s", binPath)
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).
With(slog.F("source", "provisioner")).
Leveled(slog.LevelDebug)
ctx, api := setupProvisioner(t, &provisionerServeOptions{ ctx, api := setupProvisioner(t, &provisionerServeOptions{
binaryPath: binPath, binaryPath: binPath,
logger: &logger,
}) })
sess := configure(ctx, t, api, &proto.Config{ sess := configure(ctx, t, api, &proto.Config{})
TemplateSourceArchive: testutil.CreateTar(t, nil),
})
err = sendPlan(sess, proto.WorkspaceTransition_START) err = sendInit(sess, testutil.CreateTar(t, nil))
require.NoError(t, err) require.NoError(t, err)
var planOnce sync.Once
for _, line := range tt.startSequence { for _, line := range tt.startSequence {
LoopStart: LoopStart:
msg, err := sess.Recv() msg, err := sess.Recv()
require.NoError(t, err) require.NoError(t, err)
t.Log(msg.Type) t.Log(msg.Type)
if msg.GetInit() != nil && msg.GetInit().GetError() == "" {
planOnce.Do(func() {
t.Log("Sending terraform plan request")
// Send plan after init
err = sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err)
})
goto LoopStart
}
log := msg.GetLog() log := msg.GetLog()
if log == nil { if log == nil {
goto LoopStart goto LoopStart
} }
require.Equal(t, line, log.Output) require.Equal(t, line, log.Output)
} }
t.Log("Sending the cancel request")
err = sess.Send(&proto.Request{ err = sess.Send(&proto.Request{
Type: &proto.Request_Cancel{ Type: &proto.Request_Cancel{
Cancel: &proto.CancelRequest{}, Cancel: &proto.CancelRequest{},
@@ -199,10 +249,14 @@ func TestProvision_Cancel(t *testing.T) {
if log := msg.GetLog(); log != nil { if log := msg.GetLog(); log != nil {
gotLog = append(gotLog, log.Output) gotLog = append(gotLog, log.Output)
} } else if c := msg.GetPlan(); c != nil {
if c := msg.GetPlan(); c != nil {
require.Contains(t, c.Error, "exit status 1") require.Contains(t, c.Error, "exit status 1")
break break
} else if c := msg.GetInit(); c != nil {
require.Contains(t, c.Error, "exit status 1")
break
} else {
t.Fatalf("unexpected message: %v", msg)
} }
} }
require.Equal(t, tt.wantLog, gotLog) require.Equal(t, tt.wantLog, gotLog)
@@ -231,15 +285,14 @@ func TestProvision_CancelTimeout(t *testing.T) {
exitTimeout: time.Second, exitTimeout: time.Second,
}) })
sess := configure(ctx, t, api, &proto.Config{ sess := configure(ctx, t, api, &proto.Config{})
TemplateSourceArchive: testutil.CreateTar(t, nil), sendInitAndGetResp(t, sess, testutil.CreateTar(t, nil))
})
// provisioner requires plan before apply, so test cancel with plan. // provisioner requires plan before apply, so test cancel with plan.
err = sendPlan(sess, proto.WorkspaceTransition_START) err = sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err) require.NoError(t, err)
for _, line := range []string{"init", "plan_start"} { for _, line := range []string{"plan_start"} {
LoopStart: LoopStart:
msg, err := sess.Recv() msg, err := sess.Recv()
require.NoError(t, err) require.NoError(t, err)
@@ -316,11 +369,9 @@ func TestProvision_TextFileBusy(t *testing.T) {
logger: &logger, logger: &logger,
}) })
sess := configure(ctx, t, api, &proto.Config{ sess := configure(ctx, t, api, &proto.Config{})
TemplateSourceArchive: testutil.CreateTar(t, nil),
})
err = sendPlan(sess, proto.WorkspaceTransition_START) err = sendInit(sess, testutil.CreateTar(t, nil))
require.NoError(t, err) require.NoError(t, err)
found := false found := false
@@ -328,7 +379,7 @@ func TestProvision_TextFileBusy(t *testing.T) {
msg, err := sess.Recv() msg, err := sess.Recv()
require.NoError(t, err) require.NoError(t, err)
if c := msg.GetPlan(); c != nil { if c := msg.GetInit(); c != nil {
require.Contains(t, c.Error, "exit status 1") require.Contains(t, c.Error, "exit status 1")
found = true found = true
break break
@@ -347,11 +398,14 @@ func TestProvision(t *testing.T) {
Metadata *proto.Metadata Metadata *proto.Metadata
Request *proto.PlanRequest Request *proto.PlanRequest
// Response may be nil to not check the response. // Response may be nil to not check the response.
Response *proto.PlanComplete Response *proto.GraphComplete
InitResponse *proto.InitComplete
InitErrorContains string
InitExpectLogContains string
// If ErrorContains is not empty, PlanComplete should have an Error containing the given string // If ErrorContains is not empty, PlanComplete should have an Error containing the given string
ErrorContains string PlanErrorContains string
// If ExpectLogContains is not empty, then the logs should contain it. // If PlanExpectLogContains is not empty, then the logs should contain it.
ExpectLogContains string PlanExpectLogContains string
// If Apply is true, then send an Apply request and check we get the same Resources as in Response. // If Apply is true, then send an Apply request and check we get the same Resources as in Response.
Apply bool Apply bool
// Some tests may need to be skipped until the relevant provider version is released. // Some tests may need to be skipped until the relevant provider version is released.
@@ -365,8 +419,8 @@ func TestProvision(t *testing.T) {
"main.tf": `variable "A" { "main.tf": `variable "A" {
}`, }`,
}, },
ErrorContains: "terraform plan:", PlanErrorContains: "terraform plan:",
ExpectLogContains: "No value for required variable", PlanExpectLogContains: "No value for required variable",
}, },
{ {
Name: "missing-variable-dry-run", Name: "missing-variable-dry-run",
@@ -374,15 +428,15 @@ func TestProvision(t *testing.T) {
"main.tf": `variable "A" { "main.tf": `variable "A" {
}`, }`,
}, },
ErrorContains: "terraform plan:", PlanErrorContains: "terraform plan:",
ExpectLogContains: "No value for required variable", PlanExpectLogContains: "No value for required variable",
}, },
{ {
Name: "single-resource-dry-run", Name: "single-resource-dry-run",
Files: map[string]string{ Files: map[string]string{
"main.tf": `resource "null_resource" "A" {}`, "main.tf": `resource "null_resource" "A" {}`,
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "A", Name: "A",
Type: "null_resource", Type: "null_resource",
@@ -394,7 +448,7 @@ func TestProvision(t *testing.T) {
Files: map[string]string{ Files: map[string]string{
"main.tf": `resource "null_resource" "A" {}`, "main.tf": `resource "null_resource" "A" {}`,
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "A", Name: "A",
Type: "null_resource", Type: "null_resource",
@@ -415,7 +469,7 @@ func TestProvision(t *testing.T) {
} }
}`, }`,
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "A", Name: "A",
Type: "null_resource", Type: "null_resource",
@@ -428,18 +482,18 @@ func TestProvision(t *testing.T) {
Files: map[string]string{ Files: map[string]string{
"main.tf": `a`, "main.tf": `a`,
}, },
ErrorContains: "initialize terraform", InitErrorContains: "initialize terraform",
ExpectLogContains: "Argument or block definition required", InitExpectLogContains: "Argument or block definition required",
SkipCacheProviders: true, SkipCacheProviders: true,
}, },
{ {
Name: "bad-syntax-2", Name: "bad-syntax-2",
Files: map[string]string{ Files: map[string]string{
"main.tf": `;asdf;`, "main.tf": `;asdf;`,
}, },
ErrorContains: "initialize terraform", InitErrorContains: "initialize terraform",
ExpectLogContains: `The ";" character is not valid.`, InitExpectLogContains: `The ";" character is not valid.`,
SkipCacheProviders: true, SkipCacheProviders: true,
}, },
{ {
Name: "destroy-no-state", Name: "destroy-no-state",
@@ -449,7 +503,7 @@ func TestProvision(t *testing.T) {
Metadata: &proto.Metadata{ Metadata: &proto.Metadata{
WorkspaceTransition: proto.WorkspaceTransition_DESTROY, WorkspaceTransition: proto.WorkspaceTransition_DESTROY,
}, },
ExpectLogContains: "nothing to do", PlanExpectLogContains: "nothing to do",
}, },
{ {
Name: "rich-parameter-with-value", Name: "rich-parameter-with-value",
@@ -493,7 +547,7 @@ func TestProvision(t *testing.T) {
}, },
}, },
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: "Example", Name: "Example",
@@ -571,7 +625,7 @@ func TestProvision(t *testing.T) {
}, },
}, },
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Parameters: []*proto.RichParameter{ Parameters: []*proto.RichParameter{
{ {
Name: "Example", Name: "Example",
@@ -623,7 +677,7 @@ func TestProvision(t *testing.T) {
AccessToken: "some-value", AccessToken: "some-value",
}}, }},
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "null_resource", Type: "null_resource",
@@ -666,7 +720,7 @@ func TestProvision(t *testing.T) {
WorkspaceOwnerSshPrivateKey: "fake private key", WorkspaceOwnerSshPrivateKey: "fake private key",
}, },
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "null_resource", Type: "null_resource",
@@ -709,7 +763,7 @@ func TestProvision(t *testing.T) {
WorkspaceOwnerLoginType: "github", WorkspaceOwnerLoginType: "github",
}, },
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "null_resource", Type: "null_resource",
@@ -738,16 +792,7 @@ func TestProvision(t *testing.T) {
`, `,
}, },
Request: &proto.PlanRequest{}, Request: &proto.PlanRequest{},
Response: &proto.PlanComplete{ InitResponse: &proto.InitComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "null_resource",
ModulePath: "module.hello",
}, {
Name: "inner_example",
Type: "null_resource",
ModulePath: "module.hello.module.there",
}},
Modules: []*proto.Module{{ Modules: []*proto.Module{{
Key: "hello", Key: "hello",
Version: "", Version: "",
@@ -758,6 +803,17 @@ func TestProvision(t *testing.T) {
Source: "./inner_module", Source: "./inner_module",
}}, }},
}, },
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "null_resource",
ModulePath: "module.hello",
}, {
Name: "inner_example",
Type: "null_resource",
ModulePath: "module.hello.module.there",
}},
},
}, },
{ {
Name: "workspace-owner-rbac-roles", Name: "workspace-owner-rbac-roles",
@@ -792,7 +848,7 @@ func TestProvision(t *testing.T) {
WorkspaceOwnerRbacRoles: []*proto.Role{{Name: "member", OrgId: ""}}, WorkspaceOwnerRbacRoles: []*proto.Role{{Name: "member", OrgId: ""}},
}, },
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "null_resource", Type: "null_resource",
@@ -833,7 +889,7 @@ func TestProvision(t *testing.T) {
PrebuiltWorkspaceBuildStage: proto.PrebuiltWorkspaceBuildStage_CREATE, PrebuiltWorkspaceBuildStage: proto.PrebuiltWorkspaceBuildStage_CREATE,
}, },
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "null_resource", Type: "null_resource",
@@ -871,7 +927,7 @@ func TestProvision(t *testing.T) {
PrebuiltWorkspaceBuildStage: proto.PrebuiltWorkspaceBuildStage_CLAIM, PrebuiltWorkspaceBuildStage: proto.PrebuiltWorkspaceBuildStage_CLAIM,
}, },
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "null_resource", Type: "null_resource",
@@ -910,7 +966,7 @@ func TestProvision(t *testing.T) {
`, provider.TaskPromptParameterName), `, provider.TaskPromptParameterName),
}, },
Request: &proto.PlanRequest{}, Request: &proto.PlanRequest{},
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Name: "a", Name: "a",
@@ -962,7 +1018,7 @@ func TestProvision(t *testing.T) {
} }
`, `,
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "coder_external_agent", Type: "coder_external_agent",
@@ -987,7 +1043,7 @@ func TestProvision(t *testing.T) {
} }
`, `,
}, },
Response: &proto.PlanComplete{ Response: &proto.GraphComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Name: "my-task", Name: "my-task",
@@ -1004,6 +1060,14 @@ func TestProvision(t *testing.T) {
}, },
SkipCacheProviders: true, SkipCacheProviders: true,
}, },
{
Name: "malicious-tar",
Files: map[string]string{
// Non-local path outside the working directory.
"../../../etc/passwd": "content",
},
InitErrorContains: "refusing to extract to non-local path",
},
} }
// Remove unused cache dirs before running tests. // Remove unused cache dirs before running tests.
@@ -1043,9 +1107,18 @@ func TestProvision(t *testing.T) {
ctx, api := setupProvisioner(t, &provisionerServeOptions{ ctx, api := setupProvisioner(t, &provisionerServeOptions{
cliConfigPath: cliConfigPath, cliConfigPath: cliConfigPath,
}) })
sess := configure(ctx, t, api, &proto.Config{ sess := configure(ctx, t, api, &proto.Config{})
TemplateSourceArchive: testutil.CreateTar(t, testCase.Files), initLogGot := testCase.InitExpectLogContains == ""
initComplete := sendInitAndGetResp(t, sess, testutil.CreateTar(t, testCase.Files), func(log string) {
if strings.Contains(log, testCase.InitExpectLogContains) {
initLogGot = true
}
}) })
require.Truef(t, initLogGot, "did not get expected init log substring %q", testCase.InitExpectLogContains)
if testCase.InitErrorContains != "" {
require.Contains(t, initComplete.Error, testCase.InitErrorContains)
return
}
planRequest := &proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{ planRequest := &proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{
Metadata: testCase.Metadata, Metadata: testCase.Metadata,
@@ -1054,7 +1127,7 @@ func TestProvision(t *testing.T) {
planRequest = &proto.Request{Type: &proto.Request_Plan{Plan: testCase.Request}} planRequest = &proto.Request{Type: &proto.Request_Plan{Plan: testCase.Request}}
} }
gotExpectedLog := testCase.ExpectLogContains == "" gotExpectedLog := testCase.PlanExpectLogContains == ""
provision := func(req *proto.Request) *proto.Response { provision := func(req *proto.Request) *proto.Response {
err := sess.Send(req) err := sess.Send(req)
@@ -1063,7 +1136,7 @@ func TestProvision(t *testing.T) {
msg, err := sess.Recv() msg, err := sess.Recv()
require.NoError(t, err) require.NoError(t, err)
if msg.GetLog() != nil { if msg.GetLog() != nil {
if testCase.ExpectLogContains != "" && strings.Contains(msg.GetLog().Output, testCase.ExpectLogContains) { if testCase.PlanExpectLogContains != "" && strings.Contains(msg.GetLog().Output, testCase.PlanExpectLogContains) {
gotExpectedLog = true gotExpectedLog = true
} }
@@ -1078,35 +1151,43 @@ func TestProvision(t *testing.T) {
planComplete := resp.GetPlan() planComplete := resp.GetPlan()
require.NotNil(t, planComplete) require.NotNil(t, planComplete)
if testCase.ErrorContains != "" { if testCase.PlanErrorContains != "" {
require.Contains(t, planComplete.GetError(), testCase.ErrorContains) require.Contains(t, planComplete.GetError(), testCase.PlanErrorContains)
} }
graphCompleteResp := provision(&proto.Request{Type: &proto.Request_Graph{Graph: &proto.GraphRequest{
Source: proto.GraphSource_SOURCE_PLAN,
}}})
graphComplete := graphCompleteResp.GetGraph()
require.NotNil(t, graphCompleteResp)
if testCase.Response != nil { if testCase.Response != nil {
require.Equal(t, testCase.Response.Error, planComplete.Error) require.Equal(t, testCase.Response.Error, graphComplete.Error)
// Remove randomly generated data and sort by name. // Remove randomly generated data and sort by name.
normalizeResources(planComplete.Resources) normalizeResources(graphComplete.Resources)
resourcesGot, err := json.Marshal(planComplete.Resources) resourcesGot, err := json.Marshal(graphComplete.Resources)
require.NoError(t, err) require.NoError(t, err)
resourcesWant, err := json.Marshal(testCase.Response.Resources) resourcesWant, err := json.Marshal(testCase.Response.Resources)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, string(resourcesWant), string(resourcesGot)) require.Equal(t, string(resourcesWant), string(resourcesGot))
parametersGot, err := json.Marshal(planComplete.Parameters) parametersGot, err := json.Marshal(graphComplete.Parameters)
require.NoError(t, err) require.NoError(t, err)
parametersWant, err := json.Marshal(testCase.Response.Parameters) parametersWant, err := json.Marshal(testCase.Response.Parameters)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, string(parametersWant), string(parametersGot)) require.Equal(t, string(parametersWant), string(parametersGot))
modulesGot, err := json.Marshal(planComplete.Modules) modulesGot, err := json.Marshal(initComplete.Modules)
require.NoError(t, err) require.NoError(t, err)
modulesWant, err := json.Marshal(testCase.Response.Modules) if testCase.InitResponse != nil {
require.NoError(t, err) modulesWant, err := json.Marshal(testCase.InitResponse.Modules)
require.Equal(t, string(modulesWant), string(modulesGot)) require.NoError(t, err)
require.Equal(t, string(modulesWant), string(modulesGot))
}
require.Equal(t, planComplete.HasAiTasks, testCase.Response.HasAiTasks) require.Equal(t, graphComplete.HasAiTasks, testCase.Response.HasAiTasks)
require.Equal(t, planComplete.HasExternalAgents, testCase.Response.HasExternalAgents) require.Equal(t, graphComplete.HasExternalAgents, testCase.Response.HasExternalAgents)
} }
if testCase.Apply { if testCase.Apply {
@@ -1117,8 +1198,8 @@ func TestProvision(t *testing.T) {
require.NotNil(t, applyComplete) require.NotNil(t, applyComplete)
if testCase.Response != nil { if testCase.Response != nil {
normalizeResources(applyComplete.Resources) normalizeResources(graphComplete.Resources)
resourcesGot, err := json.Marshal(applyComplete.Resources) resourcesGot, err := json.Marshal(graphComplete.Resources)
require.NoError(t, err) require.NoError(t, err)
resourcesWant, err := json.Marshal(testCase.Response.Resources) resourcesWant, err := json.Marshal(testCase.Response.Resources)
require.NoError(t, err) require.NoError(t, err)
@@ -1127,7 +1208,7 @@ func TestProvision(t *testing.T) {
} }
if !gotExpectedLog { if !gotExpectedLog {
t.Fatalf("expected log string %q but never saw it", testCase.ExpectLogContains) t.Fatalf("expected log string %q but never saw it", testCase.PlanExpectLogContains)
} }
}) })
} }
@@ -1160,9 +1241,10 @@ func TestProvision_ExtraEnv(t *testing.T) {
t.Setenv("TF_SUPERSECRET", secretValue) t.Setenv("TF_SUPERSECRET", secretValue)
ctx, api := setupProvisioner(t, nil) ctx, api := setupProvisioner(t, nil)
sess := configure(ctx, t, api, &proto.Config{ sess := configure(ctx, t, api, &proto.Config{})
TemplateSourceArchive: testutil.CreateTar(t, map[string]string{"main.tf": `resource "null_resource" "A" {}`}),
}) resp := sendInitAndGetResp(t, sess, testutil.CreateTar(t, map[string]string{"main.tf": `resource "null_resource" "A" {}`}))
require.Empty(t, resp.Error)
err := sendPlan(sess, proto.WorkspaceTransition_START) err := sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err) require.NoError(t, err)
@@ -1210,9 +1292,10 @@ func TestProvision_SafeEnv(t *testing.T) {
` `
ctx, api := setupProvisioner(t, nil) ctx, api := setupProvisioner(t, nil)
sess := configure(ctx, t, api, &proto.Config{ sess := configure(ctx, t, api, &proto.Config{})
TemplateSourceArchive: testutil.CreateTar(t, map[string]string{"main.tf": echoResource}),
}) resp := sendInitAndGetResp(t, sess, testutil.CreateTar(t, map[string]string{"main.tf": echoResource}))
require.Empty(t, resp.Error)
err := sendPlan(sess, proto.WorkspaceTransition_START) err := sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err) require.NoError(t, err)
@@ -1232,15 +1315,14 @@ func TestProvision_MalformedModules(t *testing.T) {
t.Parallel() t.Parallel()
ctx, api := setupProvisioner(t, nil) ctx, api := setupProvisioner(t, nil)
sess := configure(ctx, t, api, &proto.Config{ sess := configure(ctx, t, api, &proto.Config{})
TemplateSourceArchive: testutil.CreateTar(t, map[string]string{
"main.tf": `module "hello" { source = "./module" }`,
"module/module.tf": `resource "null_`,
}),
})
err := sendPlan(sess, proto.WorkspaceTransition_START) err := sendInit(sess, testutil.CreateTar(t, map[string]string{
"main.tf": `module "hello" { source = "./module" }`,
"module/module.tf": `resource "null_`,
}))
require.NoError(t, err) require.NoError(t, err)
log := readProvisionLog(t, sess) log := readProvisionLog(t, sess)
require.Contains(t, log, "Invalid block definition") require.Contains(t, log, "Invalid block definition")
} }
-9
View File
@@ -281,12 +281,3 @@ func (e *timingSpan) toProto() *proto.Timing {
State: e.state, State: e.state,
} }
} }
func createGraphTimingsEvent(event timingKind) (time.Time, *timingSpan) {
return dbtime.Now(), &timingSpan{
kind: event,
action: "building terraform dependency graph",
provider: "terraform",
resource: "state file",
}
}
+57 -49
View File
@@ -4,6 +4,7 @@ package terraform_test
import ( import (
"context" "context"
"encoding/json"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -35,65 +36,66 @@ func TestTimingsFromProvision(t *testing.T) {
ctx, api := setupProvisioner(t, &provisionerServeOptions{ ctx, api := setupProvisioner(t, &provisionerServeOptions{
binaryPath: fakeBin, binaryPath: fakeBin,
}) })
sess := configure(ctx, t, api, &proto.Config{ sess := configure(ctx, t, api, &proto.Config{})
TemplateSourceArchive: testutil.CreateTar(t, nil),
})
ctx, cancel := context.WithTimeout(ctx, testutil.WaitLong) ctx, cancel := context.WithTimeout(ctx, testutil.WaitLong)
t.Cleanup(cancel) t.Cleanup(cancel)
var timings []*proto.Timing
handleResponse := func(t *testing.T, stage string) {
t.Helper()
for {
select {
case <-ctx.Done():
t.Fatal(ctx.Err())
default:
}
msg, err := sess.Recv()
require.NoError(t, err)
if log := msg.GetLog(); log != nil {
t.Logf("%s: %s: %s", stage, log.Level.String(), log.Output)
continue
}
switch {
case msg.GetInit() != nil:
timings = append(timings, msg.GetInit().GetTimings()...)
case msg.GetPlan() != nil:
timings = append(timings, msg.GetPlan().GetTimings()...)
case msg.GetApply() != nil:
timings = append(timings, msg.GetApply().GetTimings()...)
case msg.GetGraph() != nil:
timings = append(timings, msg.GetGraph().GetTimings()...)
}
break
}
}
// When: configured, our fake terraform will fake an init setup
err = sendInit(sess, testutil.CreateTar(t, nil))
require.NoError(t, err)
handleResponse(t, "init")
// When: a plan is executed in the provisioner, our fake terraform will be executed and will produce a // When: a plan is executed in the provisioner, our fake terraform will be executed and will produce a
// state file and some log content. // state file and some log content.
err = sendPlan(sess, proto.WorkspaceTransition_START) err = sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err) require.NoError(t, err)
var timings []*proto.Timing handleResponse(t, "plan")
for {
select {
case <-ctx.Done():
t.Fatal(ctx.Err())
default:
}
msg, err := sess.Recv()
require.NoError(t, err)
if log := msg.GetLog(); log != nil {
t.Logf("%s: %s: %s", "plan", log.Level.String(), log.Output)
}
if c := msg.GetPlan(); c != nil {
require.Empty(t, c.Error)
// Capture the timing information returned by the plan process.
timings = append(timings, c.GetTimings()...)
break
}
}
// When: the plan has completed, let's trigger an apply. // When: the plan has completed, let's trigger an apply.
err = sendApply(sess, proto.WorkspaceTransition_START) err = sendApply(sess, proto.WorkspaceTransition_START)
require.NoError(t, err) require.NoError(t, err)
for { handleResponse(t, "apply")
select {
case <-ctx.Done():
t.Fatal(ctx.Err())
default:
}
msg, err := sess.Recv() // When: the apply has completed, graph the results
require.NoError(t, err) err = sendGraph(sess, proto.GraphSource_SOURCE_STATE)
require.NoError(t, err)
if log := msg.GetLog(); log != nil { handleResponse(t, "graph")
t.Logf("%s: %s: %s", "apply", log.Level.String(), log.Output)
}
if c := msg.GetApply(); c != nil {
require.Empty(t, c.Error)
// Capture the timing information returned by the apply process.
timings = append(timings, c.GetTimings()...)
break
}
}
// Sort the timings stably to keep reduce flakiness. // Sort the timings stably to keep reduce flakiness.
terraform_internal.StableSortTimings(t, timings) terraform_internal.StableSortTimings(t, timings)
@@ -116,10 +118,19 @@ func TestTimingsFromProvision(t *testing.T) {
{"start":"2024-08-15T08:26:39.626722Z", "end":"2024-08-15T08:26:39.669954Z", "action":"create", "source":"docker", "resource":"docker_image.main", "stage":"apply", "state":"COMPLETED"} {"start":"2024-08-15T08:26:39.626722Z", "end":"2024-08-15T08:26:39.669954Z", "action":"create", "source":"docker", "resource":"docker_image.main", "stage":"apply", "state":"COMPLETED"}
{"start":"2024-08-15T08:26:39.627335Z", "end":"2024-08-15T08:26:39.660616Z", "action":"create", "source":"docker", "resource":"docker_volume.home_volume", "stage":"apply", "state":"COMPLETED"} {"start":"2024-08-15T08:26:39.627335Z", "end":"2024-08-15T08:26:39.660616Z", "action":"create", "source":"docker", "resource":"docker_volume.home_volume", "stage":"apply", "state":"COMPLETED"}
{"start":"2024-08-15T08:26:39.682223Z", "end":"2024-08-15T08:26:40.186482Z", "action":"create", "source":"docker", "resource":"docker_container.workspace[0]", "stage":"apply", "state":"COMPLETED"}`)) {"start":"2024-08-15T08:26:39.682223Z", "end":"2024-08-15T08:26:40.186482Z", "action":"create", "source":"docker", "resource":"docker_container.workspace[0]", "stage":"apply", "state":"COMPLETED"}`))
graphTimings := terraform_internal.ParseTimingLines(t, []byte(`{"start":"2000-01-01T01:01:01.123456Z", "end":"2000-01-01T01:01:01.123456Z", "action":"building terraform dependency graph", "source":"terraform", "resource":"state file", "stage":"graph", "state":"COMPLETED"}`)) // Graphing is omitted as it is captured by the stage timing, which uses now()
graphTiming := graphTimings[0]
require.Len(t, timings, len(initTimings)+len(planTimings)+len(applyTimings)+len(graphTimings)) totals := make(map[string]int)
for _, ti := range timings {
totals[ti.Stage]++
data, _ := json.Marshal(ti) // for debugging
t.Logf("Timings log (%s) :: %s", ti.Stage, string(data))
}
require.Equal(t, len(initTimings), totals["init"], "init")
require.Equal(t, len(planTimings), totals["plan"], "plan")
require.Equal(t, len(applyTimings), totals["apply"], "apply")
// Lastly total
require.Len(t, timings, len(initTimings)+len(planTimings)+len(applyTimings))
// init/graph timings are computed dynamically during provisioning whereas plan/apply come from the logs (fixtures) in // init/graph timings are computed dynamically during provisioning whereas plan/apply come from the logs (fixtures) in
// provisioner/terraform/testdata/timings-aggregation/fake-terraform.sh. // provisioner/terraform/testdata/timings-aggregation/fake-terraform.sh.
@@ -134,9 +145,6 @@ func TestTimingsFromProvision(t *testing.T) {
case string(database.ProvisionerJobTimingStageInit): case string(database.ProvisionerJobTimingStageInit):
require.True(t, terraform_internal.TimingsAreEqual(t, []*proto.Timing{initTimings[iCursor]}, []*proto.Timing{tim})) require.True(t, terraform_internal.TimingsAreEqual(t, []*proto.Timing{initTimings[iCursor]}, []*proto.Timing{tim}))
iCursor++ iCursor++
case string(database.ProvisionerJobTimingStageGraph):
tim.Start, tim.End = graphTiming.Start, graphTiming.End
require.True(t, terraform_internal.TimingsAreEqual(t, []*proto.Timing{graphTiming}, []*proto.Timing{tim}))
case string(database.ProvisionerJobTimingStagePlan): case string(database.ProvisionerJobTimingStagePlan):
require.True(t, terraform_internal.TimingsAreEqual(t, []*proto.Timing{planTimings[pCursor]}, []*proto.Timing{tim})) require.True(t, terraform_internal.TimingsAreEqual(t, []*proto.Timing{planTimings[pCursor]}, []*proto.Timing{tim}))
pCursor++ pCursor++
+103 -58
View File
@@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/hashicorp/yamux" "github.com/hashicorp/yamux"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/atomic" "go.uber.org/atomic"
@@ -131,6 +132,16 @@ func TestProvisionerd(t *testing.T) {
} }
return c return c
}, },
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
closerMutex.Lock()
defer closerMutex.Unlock()
err := closer.Close()
c := &sdkproto.InitComplete{}
if err != nil {
c.Error = err.Error()
}
return c
},
}), }),
}) })
closerMutex.Unlock() closerMutex.Unlock()
@@ -138,47 +149,6 @@ func TestProvisionerd(t *testing.T) {
require.NoError(t, closer.Close()) require.NoError(t, closer.Close())
}) })
t.Run("MaliciousTar", func(t *testing.T) {
// Ensures tars with "../../../etc/passwd" as the path
// are not allowed to run, and will fail the job.
t.Parallel()
done := make(chan struct{})
t.Cleanup(func() {
close(done)
})
var (
completeChan = make(chan struct{})
completeOnce sync.Once
acq = newAcquireOne(t, &proto.AcquiredJob{
JobId: "test",
Provisioner: "someprovisioner",
TemplateSourceArchive: testutil.CreateTar(t, map[string]string{
"../../../etc/passwd": "content",
}),
Type: &proto.AcquiredJob_TemplateImport_{
TemplateImport: &proto.AcquiredJob_TemplateImport{
Metadata: &sdkproto.Metadata{},
},
},
})
)
closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
return createProvisionerDaemonClient(t, done, provisionerDaemonTestServer{
acquireJobWithCancel: acq.acquireWithCancel,
updateJob: noopUpdateJob,
failJob: func(ctx context.Context, job *proto.FailedJob) (*proto.Empty, error) {
completeOnce.Do(func() { close(completeChan) })
return &proto.Empty{}, nil
},
}), nil
}, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{}),
})
require.Condition(t, closedWithin(completeChan, testutil.WaitMedium))
require.NoError(t, closer.Close())
})
// LargePayloads sends a 3mb tar file to the provisioner. The provisioner also // LargePayloads sends a 3mb tar file to the provisioner. The provisioner also
// returns large payload messages back. The limit should be 4mb, so all // returns large payload messages back. The limit should be 4mb, so all
// these messages should work. // these messages should work.
@@ -227,14 +197,16 @@ func TestProvisionerd(t *testing.T) {
Readme: make([]byte, largeSize), Readme: make([]byte, largeSize),
} }
}, },
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func( plan: func(
_ *provisionersdk.Session, _ *provisionersdk.Session,
_ *sdkproto.PlanRequest, _ *sdkproto.PlanRequest,
_ <-chan struct{}, _ <-chan struct{},
) *sdkproto.PlanComplete { ) *sdkproto.PlanComplete {
return &sdkproto.PlanComplete{ return &sdkproto.PlanComplete{
Resources: []*sdkproto.Resource{}, Plan: make([]byte, largeSize),
Plan: make([]byte, largeSize),
} }
}, },
apply: func( apply: func(
@@ -246,6 +218,11 @@ func TestProvisionerd(t *testing.T) {
State: make([]byte, largeSize), State: make([]byte, largeSize),
} }
}, },
graph: func(s *provisionersdk.Session, r *sdkproto.GraphRequest, canceledOrComplete <-chan struct{}) *sdkproto.GraphComplete {
return &sdkproto.GraphComplete{
Resources: []*sdkproto.Resource{},
}
},
}), }),
}) })
require.Condition(t, closedWithin(completeChan, testutil.WaitShort)) require.Condition(t, closedWithin(completeChan, testutil.WaitShort))
@@ -299,6 +276,9 @@ func TestProvisionerd(t *testing.T) {
<-cancelOrComplete <-cancelOrComplete
return &sdkproto.ParseComplete{} return &sdkproto.ParseComplete{}
}, },
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
}), }),
}) })
require.Condition(t, closedWithin(completeChan, testutil.WaitShort)) require.Condition(t, closedWithin(completeChan, testutil.WaitShort))
@@ -349,6 +329,7 @@ func TestProvisionerd(t *testing.T) {
}), nil }), nil
}, provisionerd.LocalProvisioners{ }, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: extractInit(t),
parse: func( parse: func(
s *provisionersdk.Session, s *provisionersdk.Session,
_ *sdkproto.ParseRequest, _ *sdkproto.ParseRequest,
@@ -366,9 +347,7 @@ func TestProvisionerd(t *testing.T) {
cancelOrComplete <-chan struct{}, cancelOrComplete <-chan struct{},
) *sdkproto.PlanComplete { ) *sdkproto.PlanComplete {
s.ProvisionLog(sdkproto.LogLevel_INFO, "hello") s.ProvisionLog(sdkproto.LogLevel_INFO, "hello")
return &sdkproto.PlanComplete{ return &sdkproto.PlanComplete{}
Resources: []*sdkproto.Resource{},
}
}, },
apply: func( apply: func(
_ *provisionersdk.Session, _ *provisionersdk.Session,
@@ -378,6 +357,11 @@ func TestProvisionerd(t *testing.T) {
t.Error("dry run should not apply") t.Error("dry run should not apply")
return &sdkproto.ApplyComplete{} return &sdkproto.ApplyComplete{}
}, },
graph: func(s *provisionersdk.Session, r *sdkproto.GraphRequest, canceledOrComplete <-chan struct{}) *sdkproto.GraphComplete {
return &sdkproto.GraphComplete{
Resources: []*sdkproto.Resource{},
}
},
}), }),
}) })
@@ -433,14 +417,15 @@ func TestProvisionerd(t *testing.T) {
}), nil }), nil
}, provisionerd.LocalProvisioners{ }, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func( plan: func(
_ *provisionersdk.Session, _ *provisionersdk.Session,
_ *sdkproto.PlanRequest, _ *sdkproto.PlanRequest,
_ <-chan struct{}, _ <-chan struct{},
) *sdkproto.PlanComplete { ) *sdkproto.PlanComplete {
return &sdkproto.PlanComplete{ return &sdkproto.PlanComplete{}
Resources: []*sdkproto.Resource{},
}
}, },
apply: func( apply: func(
_ *provisionersdk.Session, _ *provisionersdk.Session,
@@ -450,6 +435,11 @@ func TestProvisionerd(t *testing.T) {
t.Error("dry run should not apply") t.Error("dry run should not apply")
return &sdkproto.ApplyComplete{} return &sdkproto.ApplyComplete{}
}, },
graph: func(s *provisionersdk.Session, r *sdkproto.GraphRequest, canceledOrComplete <-chan struct{}) *sdkproto.GraphComplete {
return &sdkproto.GraphComplete{
Resources: []*sdkproto.Resource{},
}
},
}), }),
}) })
@@ -498,6 +488,9 @@ func TestProvisionerd(t *testing.T) {
}), nil }), nil
}, provisionerd.LocalProvisioners{ }, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func( plan: func(
s *provisionersdk.Session, s *provisionersdk.Session,
_ *sdkproto.PlanRequest, _ *sdkproto.PlanRequest,
@@ -513,6 +506,9 @@ func TestProvisionerd(t *testing.T) {
) *sdkproto.ApplyComplete { ) *sdkproto.ApplyComplete {
return &sdkproto.ApplyComplete{} return &sdkproto.ApplyComplete{}
}, },
graph: func(s *provisionersdk.Session, r *sdkproto.GraphRequest, canceledOrComplete <-chan struct{}) *sdkproto.GraphComplete {
return &sdkproto.GraphComplete{}
},
}), }),
}) })
require.Condition(t, closedWithin(acq.complete, testutil.WaitShort)) require.Condition(t, closedWithin(acq.complete, testutil.WaitShort))
@@ -570,6 +566,9 @@ func TestProvisionerd(t *testing.T) {
}), nil }), nil
}, provisionerd.LocalProvisioners{ }, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func( plan: func(
s *provisionersdk.Session, s *provisionersdk.Session,
_ *sdkproto.PlanRequest, _ *sdkproto.PlanRequest,
@@ -577,14 +576,7 @@ func TestProvisionerd(t *testing.T) {
) *sdkproto.PlanComplete { ) *sdkproto.PlanComplete {
s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow") s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow")
return &sdkproto.PlanComplete{ return &sdkproto.PlanComplete{
Resources: []*sdkproto.Resource{ DailyCost: 25,
{
DailyCost: 10,
},
{
DailyCost: 15,
},
},
} }
}, },
apply: func( apply: func(
@@ -593,7 +585,10 @@ func TestProvisionerd(t *testing.T) {
_ <-chan struct{}, _ <-chan struct{},
) *sdkproto.ApplyComplete { ) *sdkproto.ApplyComplete {
t.Error("should not apply when resources exceed quota") t.Error("should not apply when resources exceed quota")
return &sdkproto.ApplyComplete{ return &sdkproto.ApplyComplete{}
},
graph: func(s *provisionersdk.Session, r *sdkproto.GraphRequest, canceledOrComplete <-chan struct{}) *sdkproto.GraphComplete {
return &sdkproto.GraphComplete{
Resources: []*sdkproto.Resource{ Resources: []*sdkproto.Resource{
{ {
DailyCost: 10, DailyCost: 10,
@@ -646,6 +641,12 @@ func TestProvisionerd(t *testing.T) {
}), nil }), nil
}, provisionerd.LocalProvisioners{ }, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
graph: func(s *provisionersdk.Session, r *sdkproto.GraphRequest, canceledOrComplete <-chan struct{}) *sdkproto.GraphComplete {
return &sdkproto.GraphComplete{}
},
plan: func( plan: func(
s *provisionersdk.Session, s *provisionersdk.Session,
_ *sdkproto.PlanRequest, _ *sdkproto.PlanRequest,
@@ -756,6 +757,9 @@ func TestProvisionerd(t *testing.T) {
}), nil }), nil
}, provisionerd.LocalProvisioners{ }, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func( plan: func(
s *provisionersdk.Session, s *provisionersdk.Session,
_ *sdkproto.PlanRequest, _ *sdkproto.PlanRequest,
@@ -844,6 +848,9 @@ func TestProvisionerd(t *testing.T) {
}), nil }), nil
}, provisionerd.LocalProvisioners{ }, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func( plan: func(
s *provisionersdk.Session, s *provisionersdk.Session,
_ *sdkproto.PlanRequest, _ *sdkproto.PlanRequest,
@@ -938,6 +945,9 @@ func TestProvisionerd(t *testing.T) {
return client, nil return client, nil
}, provisionerd.LocalProvisioners{ }, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func( plan: func(
_ *provisionersdk.Session, _ *provisionersdk.Session,
_ *sdkproto.PlanRequest, _ *sdkproto.PlanRequest,
@@ -1031,6 +1041,9 @@ func TestProvisionerd(t *testing.T) {
return client, nil return client, nil
}, provisionerd.LocalProvisioners{ }, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func( plan: func(
_ *provisionersdk.Session, _ *provisionersdk.Session,
_ *sdkproto.PlanRequest, _ *sdkproto.PlanRequest,
@@ -1045,6 +1058,9 @@ func TestProvisionerd(t *testing.T) {
) *sdkproto.ApplyComplete { ) *sdkproto.ApplyComplete {
return &sdkproto.ApplyComplete{} return &sdkproto.ApplyComplete{}
}, },
graph: func(s *provisionersdk.Session, r *sdkproto.GraphRequest, canceledOrComplete <-chan struct{}) *sdkproto.GraphComplete {
return &sdkproto.GraphComplete{}
},
}), }),
}) })
require.Condition(t, closedWithin(completeChan, testutil.WaitShort)) require.Condition(t, closedWithin(completeChan, testutil.WaitShort))
@@ -1125,6 +1141,12 @@ func TestProvisionerd(t *testing.T) {
}), nil }), nil
}, provisionerd.LocalProvisioners{ }, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
graph: func(s *provisionersdk.Session, r *sdkproto.GraphRequest, canceledOrComplete <-chan struct{}) *sdkproto.GraphComplete {
return &sdkproto.GraphComplete{}
},
plan: func( plan: func(
s *provisionersdk.Session, s *provisionersdk.Session,
_ *sdkproto.PlanRequest, _ *sdkproto.PlanRequest,
@@ -1253,9 +1275,15 @@ func createProvisionerClient(t *testing.T, done <-chan struct{}, server provisio
} }
type provisionerTestServer struct { type provisionerTestServer struct {
init func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete
parse func(s *provisionersdk.Session, r *sdkproto.ParseRequest, canceledOrComplete <-chan struct{}) *sdkproto.ParseComplete parse func(s *provisionersdk.Session, r *sdkproto.ParseRequest, canceledOrComplete <-chan struct{}) *sdkproto.ParseComplete
plan func(s *provisionersdk.Session, r *sdkproto.PlanRequest, canceledOrComplete <-chan struct{}) *sdkproto.PlanComplete plan func(s *provisionersdk.Session, r *sdkproto.PlanRequest, canceledOrComplete <-chan struct{}) *sdkproto.PlanComplete
apply func(s *provisionersdk.Session, r *sdkproto.ApplyRequest, canceledOrComplete <-chan struct{}) *sdkproto.ApplyComplete apply func(s *provisionersdk.Session, r *sdkproto.ApplyRequest, canceledOrComplete <-chan struct{}) *sdkproto.ApplyComplete
graph func(s *provisionersdk.Session, r *sdkproto.GraphRequest, canceledOrComplete <-chan struct{}) *sdkproto.GraphComplete
}
func (p *provisionerTestServer) Init(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return p.init(s, r, canceledOrComplete)
} }
func (p *provisionerTestServer) Parse(s *provisionersdk.Session, r *sdkproto.ParseRequest, canceledOrComplete <-chan struct{}) *sdkproto.ParseComplete { func (p *provisionerTestServer) Parse(s *provisionersdk.Session, r *sdkproto.ParseRequest, canceledOrComplete <-chan struct{}) *sdkproto.ParseComplete {
@@ -1270,6 +1298,10 @@ func (p *provisionerTestServer) Apply(s *provisionersdk.Session, r *sdkproto.App
return p.apply(s, r, canceledOrComplete) return p.apply(s, r, canceledOrComplete)
} }
func (p *provisionerTestServer) Graph(s *provisionersdk.Session, r *sdkproto.GraphRequest, canceledOrComplete <-chan struct{}) *sdkproto.GraphComplete {
return p.graph(s, r, canceledOrComplete)
}
func (p *provisionerDaemonTestServer) UploadFile(stream proto.DRPCProvisionerDaemon_UploadFileStream) error { func (p *provisionerDaemonTestServer) UploadFile(stream proto.DRPCProvisionerDaemon_UploadFileStream) error {
return p.uploadFile(stream) return p.uploadFile(stream)
} }
@@ -1359,3 +1391,16 @@ func (a *acquireOne) acquireWithCancel(stream proto.DRPCProvisionerDaemon_Acquir
} }
return nil return nil
} }
func extractInit(t *testing.T) func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
logger := slogtest.Make(t, nil)
return func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
err := s.Files.ExtractArchive(s.Context(), logger, afero.NewOsFs(), r.TemplateSourceArchive)
if err != nil {
return &sdkproto.InitComplete{
Error: fmt.Sprintf("failed to extract template source archive: %v", err),
}
}
return &sdkproto.InitComplete{}
}
}
+64
View File
@@ -0,0 +1,64 @@
package runner
import (
"context"
"time"
"cdr.dev/slog"
"github.com/coder/coder/v2/provisionerd/proto"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
)
func (r *Runner) apply(ctx context.Context, stage string, req *sdkproto.ApplyRequest) (
*sdkproto.ApplyComplete, *proto.FailedJob,
) {
// use the notStopped so that if we attempt to gracefully cancel, the stream
// will still be available for us to send the cancel to the provisioner
err := r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Apply{Apply: req}})
if err != nil {
return nil, r.failedWorkspaceBuildf("start provision: %s", err)
}
nevermind := make(chan struct{})
defer close(nevermind)
go func() {
select {
case <-nevermind:
return
case <-r.notStopped.Done():
return
case <-r.notCanceled.Done():
_ = r.session.Send(&sdkproto.Request{
Type: &sdkproto.Request_Cancel{
Cancel: &sdkproto.CancelRequest{},
},
})
}
}()
for {
msg, err := r.session.Recv()
if err != nil {
return nil, r.failedWorkspaceBuildf("recv workspace provision: %s", err)
}
switch msgType := msg.Type.(type) {
case *sdkproto.Response_Log:
r.logProvisionerJobLog(context.Background(), msgType.Log.Level, "workspace provisioner job logged",
slog.F("level", msgType.Log.Level),
slog.F("output", msgType.Log.Output),
slog.F("workspace_build_id", r.job.GetWorkspaceBuild().WorkspaceBuildId),
)
r.queueLog(ctx, &proto.Log{
Source: proto.LogSource_PROVISIONER,
Level: msgType.Log.Level,
CreatedAt: time.Now().UnixMilli(),
Output: msgType.Log.Output,
Stage: stage,
})
case *sdkproto.Response_Apply:
return msgType.Apply, nil
default:
return nil, r.failedJobf("unexpected plan response type %T", msg.Type)
}
}
}
+64
View File
@@ -0,0 +1,64 @@
package runner
import (
"context"
"time"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/provisionerd/proto"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
)
func (r *Runner) graph(ctx context.Context, req *sdkproto.GraphRequest) (*sdkproto.GraphComplete, *proto.FailedJob) {
ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End()
err := r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Graph{Graph: req}})
if err != nil {
return nil, r.failedJobf("send graph request: %v", err)
}
nevermind := make(chan struct{})
defer close(nevermind)
go func() {
select {
case <-nevermind:
return
case <-r.notStopped.Done():
return
case <-r.notCanceled.Done():
_ = r.session.Send(&sdkproto.Request{
Type: &sdkproto.Request_Cancel{
Cancel: &sdkproto.CancelRequest{},
},
})
}
}()
for {
msg, err := r.session.Recv()
if err != nil {
return nil, r.failedJobf("receive graph response: %v", err)
}
switch msgType := msg.Type.(type) {
case *sdkproto.Response_Log:
r.logProvisionerJobLog(context.Background(), msgType.Log.Level, "terraform graphing",
slog.F("level", msgType.Log.Level),
slog.F("output", msgType.Log.Output),
)
r.queueLog(ctx, &proto.Log{
Source: proto.LogSource_PROVISIONER,
Level: msgType.Log.Level,
CreatedAt: time.Now().UnixMilli(),
Output: msgType.Log.Output,
Stage: "Graphing Infrastructure",
})
case *sdkproto.Response_Graph:
return msgType.Graph, nil
default:
return nil, r.failedJobf("unexpected graph response type %T", msg.Type)
}
}
}
+113
View File
@@ -0,0 +1,113 @@
package runner
import (
"bytes"
"context"
"time"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/provisionerd/proto"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
)
//nolint:revive
func (r *Runner) init(ctx context.Context, omitModules bool, templateArchive []byte) (*sdkproto.InitComplete, *proto.FailedJob) {
ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End()
err := r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Init{Init: &sdkproto.InitRequest{
TemplateSourceArchive: templateArchive,
OmitModuleFiles: omitModules,
}}})
if err != nil {
return nil, r.failedJobf("send init request: %v", err)
}
nevermind := make(chan struct{})
defer close(nevermind)
go func() {
select {
case <-nevermind:
return
case <-r.notStopped.Done():
return
case <-r.notCanceled.Done():
_ = r.session.Send(&sdkproto.Request{
Type: &sdkproto.Request_Cancel{
Cancel: &sdkproto.CancelRequest{},
},
})
}
}()
var moduleFilesUpload *sdkproto.DataBuilder
for {
msg, err := r.session.Recv()
if err != nil {
return nil, r.failedJobf("receive init response: %v", err)
}
switch msgType := msg.Type.(type) {
case *sdkproto.Response_Log:
r.logProvisionerJobLog(context.Background(), msgType.Log.Level, "terraform initialization",
slog.F("level", msgType.Log.Level),
slog.F("output", msgType.Log.Output),
)
r.queueLog(ctx, &proto.Log{
Source: proto.LogSource_PROVISIONER,
Level: msgType.Log.Level,
CreatedAt: time.Now().UnixMilli(),
Output: msgType.Log.Output,
Stage: "Initializing Terraform Directory",
})
case *sdkproto.Response_DataUpload:
if omitModules {
return nil, r.failedJobf("received unexpected module files data upload when omitModules is true")
}
c := msgType.DataUpload
if c.UploadType != sdkproto.DataUploadType_UPLOAD_TYPE_MODULE_FILES {
return nil, r.failedJobf("invalid data upload type: %q", c.UploadType)
}
if moduleFilesUpload != nil {
return nil, r.failedJobf("multiple module data uploads received, only expect 1")
}
moduleFilesUpload, err = sdkproto.NewDataBuilder(c)
if err != nil {
return nil, r.failedJobf("create data builder: %s", err.Error())
}
case *sdkproto.Response_ChunkPiece:
if omitModules {
return nil, r.failedJobf("received unexpected module files data upload when omitModules is true")
}
c := msgType.ChunkPiece
if moduleFilesUpload == nil {
return nil, r.failedJobf("received chunk piece before module files data upload")
}
_, err := moduleFilesUpload.Add(c)
if err != nil {
return nil, r.failedJobf("module files, add chunk piece: %s", err.Error())
}
case *sdkproto.Response_Init:
if moduleFilesUpload != nil {
// If files were uploaded in multiple chunks, put them back together.
moduleFilesData, err := moduleFilesUpload.Complete()
if err != nil {
return nil, r.failedJobf("complete module files data upload: %s", err.Error())
}
if !bytes.Equal(msgType.Init.ModuleFilesHash, moduleFilesUpload.Hash) {
return nil, r.failedJobf("module files hash mismatch, uploaded: %x, expected: %x", moduleFilesUpload.Hash, msgType.Init.ModuleFilesHash)
}
msgType.Init.ModuleFiles = moduleFilesData
}
return msgType.Init, nil
default:
return nil, r.failedJobf("unexpected init response type %T", msg.Type)
}
}
}
+64
View File
@@ -0,0 +1,64 @@
package runner
import (
"context"
"time"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/provisionerd/proto"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
)
func (r *Runner) plan(ctx context.Context, stage string, req *sdkproto.PlanRequest) (*sdkproto.PlanComplete, *proto.FailedJob) {
ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End()
err := r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Plan{Plan: req}})
if err != nil {
return nil, r.failedJobf("send plan request: %v", err)
}
nevermind := make(chan struct{})
defer close(nevermind)
go func() {
select {
case <-nevermind:
return
case <-r.notStopped.Done():
return
case <-r.notCanceled.Done():
_ = r.session.Send(&sdkproto.Request{
Type: &sdkproto.Request_Cancel{
Cancel: &sdkproto.CancelRequest{},
},
})
}
}()
for {
msg, err := r.session.Recv()
if err != nil {
return nil, r.failedJobf("receive plan response: %v", err)
}
switch msgType := msg.Type.(type) {
case *sdkproto.Response_Log:
r.logProvisionerJobLog(context.Background(), msgType.Log.Level, "terraform planning",
slog.F("level", msgType.Log.Level),
slog.F("output", msgType.Log.Output),
)
r.queueLog(ctx, &proto.Log{
Source: proto.LogSource_PROVISIONER,
Level: msgType.Log.Level,
CreatedAt: time.Now().UnixMilli(),
Output: msgType.Log.Output,
Stage: stage,
})
case *sdkproto.Response_Plan:
return msgType.Plan, nil
default:
return nil, r.failedJobf("unexpected plan response type %T", msg.Type)
}
}
}
-11
View File
@@ -1,11 +0,0 @@
package runner
import "github.com/coder/coder/v2/provisionersdk/proto"
func sumDailyCost(resources []*proto.Resource) int {
var sum int
for _, r := range resources {
sum += int(r.DailyCost)
}
return sum
}
+173 -245
View File
@@ -1,7 +1,6 @@
package runner package runner
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -515,7 +514,6 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
defer span.End() defer span.End()
failedJob := r.configure(&sdkproto.Config{ failedJob := r.configure(&sdkproto.Config{
TemplateSourceArchive: r.job.GetTemplateSourceArchive(),
TemplateId: strings2.EmptyToNil(r.job.GetTemplateImport().Metadata.TemplateId), TemplateId: strings2.EmptyToNil(r.job.GetTemplateImport().Metadata.TemplateId),
TemplateVersionId: strings2.EmptyToNil(r.job.GetTemplateImport().Metadata.TemplateVersionId), TemplateVersionId: strings2.EmptyToNil(r.job.GetTemplateImport().Metadata.TemplateVersionId),
ExpReuseTerraformWorkspace: ptr.Ref(false), ExpReuseTerraformWorkspace: ptr.Ref(false),
@@ -524,6 +522,18 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
return nil, failedJob return nil, failedJob
} }
// Initialize the Terraform working directory
initResp, failedInit := r.init(ctx, false, r.job.GetTemplateSourceArchive())
if failedInit != nil {
return nil, failedInit
}
if initResp == nil {
return nil, r.failedJobf("template import init returned nil response")
}
if initResp.Error != "" {
return nil, r.failedJobf("template import init error: %s", initResp.Error)
}
// Parse parameters and update the job with the parameter specs // Parse parameters and update the job with the parameter specs
r.queueLog(ctx, &proto.Log{ r.queueLog(ctx, &proto.Log{
Source: proto.LogSource_PROVISIONER_DAEMON, Source: proto.LogSource_PROVISIONER_DAEMON,
@@ -560,7 +570,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
CoderUrl: r.job.GetTemplateImport().Metadata.CoderUrl, CoderUrl: r.job.GetTemplateImport().Metadata.CoderUrl,
WorkspaceOwnerGroups: r.job.GetTemplateImport().Metadata.WorkspaceOwnerGroups, WorkspaceOwnerGroups: r.job.GetTemplateImport().Metadata.WorkspaceOwnerGroups,
WorkspaceTransition: sdkproto.WorkspaceTransition_START, WorkspaceTransition: sdkproto.WorkspaceTransition_START,
}, false) })
if err != nil { if err != nil {
return nil, r.failedJobf("template import provision for start: %s", err) return nil, r.failedJobf("template import provision for start: %s", err)
} }
@@ -576,8 +586,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
CoderUrl: r.job.GetTemplateImport().Metadata.CoderUrl, CoderUrl: r.job.GetTemplateImport().Metadata.CoderUrl,
WorkspaceOwnerGroups: r.job.GetTemplateImport().Metadata.WorkspaceOwnerGroups, WorkspaceOwnerGroups: r.job.GetTemplateImport().Metadata.WorkspaceOwnerGroups,
WorkspaceTransition: sdkproto.WorkspaceTransition_STOP, WorkspaceTransition: sdkproto.WorkspaceTransition_STOP,
}, true, // Modules downloaded on the start provision })
)
if err != nil { if err != nil {
return nil, r.failedJobf("template import provision for stop: %s", err) return nil, r.failedJobf("template import provision for stop: %s", err)
} }
@@ -597,12 +606,13 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
RichParameters: startProvision.Parameters, RichParameters: startProvision.Parameters,
ExternalAuthProvidersNames: externalAuthProviderNames, ExternalAuthProvidersNames: externalAuthProviderNames,
ExternalAuthProviders: startProvision.ExternalAuthProviders, ExternalAuthProviders: startProvision.ExternalAuthProviders,
StartModules: startProvision.Modules, // TODO: These are defined as different, but can they be?
StopModules: stopProvision.Modules, // Terraform downloads modules regardless of `count`, so this should be the same
Presets: startProvision.Presets, StartModules: initResp.Modules,
Plan: startProvision.Plan, StopModules: initResp.Modules,
// ModuleFiles are not on the stopProvision. So grab from the startProvision. Presets: startProvision.Presets,
ModuleFiles: startProvision.ModuleFiles, Plan: startProvision.Plan,
ModuleFiles: initResp.ModuleFiles,
// ModuleFileHash will be populated if the file is uploaded async // ModuleFileHash will be populated if the file is uploaded async
ModuleFilesHash: []byte{}, ModuleFilesHash: []byte{},
HasAiTasks: startProvision.HasAITasks, HasAiTasks: startProvision.HasAITasks,
@@ -666,10 +676,8 @@ type templateImportProvision struct {
Resources []*sdkproto.Resource Resources []*sdkproto.Resource
Parameters []*sdkproto.RichParameter Parameters []*sdkproto.RichParameter
ExternalAuthProviders []*sdkproto.ExternalAuthProviderResource ExternalAuthProviders []*sdkproto.ExternalAuthProviderResource
Modules []*sdkproto.Module
Presets []*sdkproto.Preset Presets []*sdkproto.Preset
Plan json.RawMessage Plan json.RawMessage
ModuleFiles []byte
HasAITasks bool HasAITasks bool
HasExternalAgents bool HasExternalAgents bool
} }
@@ -677,8 +685,8 @@ type templateImportProvision struct {
// Performs a dry-run provision when importing a template. // Performs a dry-run provision when importing a template.
// This is used to detect resources that would be provisioned for a workspace in various states. // This is used to detect resources that would be provisioned for a workspace in various states.
// It doesn't define values for rich parameters as they're unknown during template import. // It doesn't define values for rich parameters as they're unknown during template import.
func (r *Runner) runTemplateImportProvision(ctx context.Context, variableValues []*sdkproto.VariableValue, metadata *sdkproto.Metadata, omitModules bool) (*templateImportProvision, error) { func (r *Runner) runTemplateImportProvision(ctx context.Context, variableValues []*sdkproto.VariableValue, metadata *sdkproto.Metadata) (*templateImportProvision, error) {
return r.runTemplateImportProvisionWithRichParameters(ctx, variableValues, nil, metadata, omitModules) return r.runTemplateImportProvisionWithRichParameters(ctx, variableValues, nil, metadata)
} }
// Performs a dry-run provision with provided rich parameters. // Performs a dry-run provision with provided rich parameters.
@@ -688,7 +696,6 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(
variableValues []*sdkproto.VariableValue, variableValues []*sdkproto.VariableValue,
richParameterValues []*sdkproto.RichParameterValue, richParameterValues []*sdkproto.RichParameterValue,
metadata *sdkproto.Metadata, metadata *sdkproto.Metadata,
omitModules bool,
) (*templateImportProvision, error) { ) (*templateImportProvision, error) {
ctx, span := r.startTrace(ctx, tracing.FuncName()) ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End() defer span.End()
@@ -700,126 +707,48 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(
case sdkproto.WorkspaceTransition_STOP: case sdkproto.WorkspaceTransition_STOP:
stage = "Detecting ephemeral resources" stage = "Detecting ephemeral resources"
} }
// use the notStopped so that if we attempt to gracefully cancel, the stream will still be available for us
// to send the cancel to the provisioner planComplete, failed := r.plan(ctx, stage, &sdkproto.PlanRequest{
err := r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Plan{Plan: &sdkproto.PlanRequest{ Metadata: metadata,
Metadata: metadata, RichParameterValues: richParameterValues,
RichParameterValues: richParameterValues,
// Template import has no previous values
PreviousParameterValues: make([]*sdkproto.RichParameterValue, 0),
VariableValues: variableValues, VariableValues: variableValues,
OmitModuleFiles: omitModules, ExternalAuthProviders: nil,
}}}) PreviousParameterValues: nil,
if err != nil { State: nil,
return nil, xerrors.Errorf("start provision: %w", err) })
if failed != nil {
return nil, xerrors.Errorf("plan during template import provision: %w", failed)
} }
nevermind := make(chan struct{}) if planComplete == nil {
defer close(nevermind) return nil, xerrors.New("plan during template import provision returned nil response")
go func() {
select {
case <-nevermind:
return
case <-r.notStopped.Done():
return
case <-r.notCanceled.Done():
_ = r.session.Send(&sdkproto.Request{
Type: &sdkproto.Request_Cancel{
Cancel: &sdkproto.CancelRequest{},
},
})
}
}()
var moduleFilesUpload *sdkproto.DataBuilder
for {
msg, err := r.session.Recv()
if err != nil {
return nil, xerrors.Errorf("recv import provision: %w", err)
}
switch msgType := msg.Type.(type) {
case *sdkproto.Response_Log:
r.logProvisionerJobLog(context.Background(), msgType.Log.Level, "template import provision job logged",
slog.F("level", msgType.Log.Level),
slog.F("output", msgType.Log.Output),
)
r.queueLog(ctx, &proto.Log{
Source: proto.LogSource_PROVISIONER,
Level: msgType.Log.Level,
CreatedAt: time.Now().UnixMilli(),
Output: msgType.Log.Output,
Stage: stage,
})
case *sdkproto.Response_DataUpload:
c := msgType.DataUpload
if c.UploadType != sdkproto.DataUploadType_UPLOAD_TYPE_MODULE_FILES {
return nil, xerrors.Errorf("invalid data upload type: %q", c.UploadType)
}
if moduleFilesUpload != nil {
return nil, xerrors.New("multiple module data uploads received, only expect 1")
}
moduleFilesUpload, err = sdkproto.NewDataBuilder(c)
if err != nil {
return nil, xerrors.Errorf("create data builder: %w", err)
}
case *sdkproto.Response_ChunkPiece:
c := msgType.ChunkPiece
if moduleFilesUpload == nil {
return nil, xerrors.New("received chunk piece before module files data upload")
}
_, err := moduleFilesUpload.Add(c)
if err != nil {
return nil, xerrors.Errorf("module files, add chunk piece: %w", err)
}
case *sdkproto.Response_Plan:
c := msgType.Plan
if c.Error != "" {
r.logger.Info(context.Background(), "dry-run provision failure",
slog.F("error", c.Error),
)
return nil, xerrors.New(c.Error)
}
if moduleFilesUpload != nil && len(c.ModuleFiles) > 0 {
return nil, xerrors.New("module files were uploaded and module files were returned in the plan response. Only one of these should be set")
}
r.logger.Info(context.Background(), "parse dry-run provision successful",
slog.F("resource_count", len(c.Resources)),
slog.F("resources", resourceNames(c.Resources)),
)
moduleFilesData := c.ModuleFiles
if moduleFilesUpload != nil {
uploadData, err := moduleFilesUpload.Complete()
if err != nil {
return nil, xerrors.Errorf("module files, complete upload: %w", err)
}
moduleFilesData = uploadData
if !bytes.Equal(c.ModuleFilesHash, moduleFilesUpload.Hash) {
return nil, xerrors.Errorf("module files hash mismatch, uploaded: %x, expected: %x", moduleFilesUpload.Hash, c.ModuleFilesHash)
}
}
return &templateImportProvision{
Resources: c.Resources,
Parameters: c.Parameters,
ExternalAuthProviders: c.ExternalAuthProviders,
Modules: c.Modules,
Presets: c.Presets,
Plan: c.Plan,
ModuleFiles: moduleFilesData,
HasAITasks: c.HasAiTasks,
HasExternalAgents: c.HasExternalAgents,
}, nil
default:
return nil, xerrors.Errorf("invalid message type %q received from provisioner",
reflect.TypeOf(msg.Type).String())
}
} }
if planComplete.Error != "" {
return nil, xerrors.Errorf("plan during template import provision error: %s", planComplete.Error)
}
graphComplete, failed := r.graph(ctx, &sdkproto.GraphRequest{
Metadata: metadata,
Source: sdkproto.GraphSource_SOURCE_PLAN,
})
if failed != nil {
return nil, xerrors.Errorf("graph during template import provision: %w", failed)
}
if graphComplete == nil {
return nil, xerrors.New("graph during template import provision returned nil response")
}
if graphComplete.Error != "" {
return nil, xerrors.Errorf("graph during template import provision error: %s", graphComplete.Error)
}
return &templateImportProvision{
Resources: graphComplete.Resources,
Parameters: graphComplete.Parameters,
ExternalAuthProviders: graphComplete.ExternalAuthProviders,
Presets: graphComplete.Presets,
Plan: planComplete.Plan,
HasAITasks: graphComplete.HasAiTasks,
HasExternalAgents: graphComplete.HasExternalAgents,
}, nil
} }
func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob) { func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob) {
@@ -854,19 +783,28 @@ func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *p
metadata.WorkspaceOwnerId = id.String() metadata.WorkspaceOwnerId = id.String()
} }
failedJob := r.configure(&sdkproto.Config{ failedJob := r.configure(&sdkproto.Config{})
TemplateSourceArchive: r.job.GetTemplateSourceArchive(),
})
if failedJob != nil { if failedJob != nil {
return nil, failedJob return nil, failedJob
} }
// Initialize the Terraform working directory
initResp, failedJob := r.init(ctx, false, r.job.GetTemplateSourceArchive())
if failedJob != nil {
return nil, failedJob
}
if initResp == nil {
return nil, r.failedJobf("template dry-run init returned nil response")
}
if initResp.Error != "" {
return nil, r.failedJobf("template dry-run init error: %s", initResp.Error)
}
// Run the template import provision task since it's already a dry run. // Run the template import provision task since it's already a dry run.
provision, err := r.runTemplateImportProvisionWithRichParameters(ctx, provision, err := r.runTemplateImportProvisionWithRichParameters(ctx,
r.job.GetTemplateDryRun().GetVariableValues(), r.job.GetTemplateDryRun().GetVariableValues(),
r.job.GetTemplateDryRun().GetRichParameterValues(), r.job.GetTemplateDryRun().GetRichParameterValues(),
metadata, metadata,
false,
) )
if err != nil { if err != nil {
return nil, r.failedJobf("run dry-run provision job: %s", err) return nil, r.failedJobf("run dry-run provision job: %s", err)
@@ -877,73 +815,14 @@ func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *p
Type: &proto.CompletedJob_TemplateDryRun_{ Type: &proto.CompletedJob_TemplateDryRun_{
TemplateDryRun: &proto.CompletedJob_TemplateDryRun{ TemplateDryRun: &proto.CompletedJob_TemplateDryRun{
Resources: provision.Resources, Resources: provision.Resources,
Modules: provision.Modules, Modules: initResp.Modules,
}, },
}, },
}, nil }, nil
} }
func (r *Runner) buildWorkspace(ctx context.Context, stage string, req *sdkproto.Request) ( func (r *Runner) commitQuota(ctx context.Context, cost int32) *proto.FailedJob {
*sdkproto.Response, *proto.FailedJob,
) {
// use the notStopped so that if we attempt to gracefully cancel, the stream
// will still be available for us to send the cancel to the provisioner
err := r.session.Send(req)
if err != nil {
return nil, r.failedWorkspaceBuildf("start provision: %s", err)
}
nevermind := make(chan struct{})
defer close(nevermind)
go func() {
select {
case <-nevermind:
return
case <-r.notStopped.Done():
return
case <-r.notCanceled.Done():
_ = r.session.Send(&sdkproto.Request{
Type: &sdkproto.Request_Cancel{
Cancel: &sdkproto.CancelRequest{},
},
})
}
}()
for {
msg, err := r.session.Recv()
if err != nil {
return nil, r.failedWorkspaceBuildf("recv workspace provision: %s", err)
}
switch msgType := msg.Type.(type) {
case *sdkproto.Response_Log:
r.logProvisionerJobLog(context.Background(), msgType.Log.Level, "workspace provisioner job logged",
slog.F("level", msgType.Log.Level),
slog.F("output", msgType.Log.Output),
slog.F("workspace_build_id", r.job.GetWorkspaceBuild().WorkspaceBuildId),
)
r.queueLog(ctx, &proto.Log{
Source: proto.LogSource_PROVISIONER,
Level: msgType.Log.Level,
CreatedAt: time.Now().UnixMilli(),
Output: msgType.Log.Output,
Stage: stage,
})
case *sdkproto.Response_DataUpload:
continue // Only for template imports
case *sdkproto.Response_ChunkPiece:
continue // Only for template imports
default:
// Stop looping!
return msg, nil
}
}
}
func (r *Runner) commitQuota(ctx context.Context, resources []*sdkproto.Resource) *proto.FailedJob {
cost := sumDailyCost(resources)
r.logger.Debug(ctx, "committing quota", r.logger.Debug(ctx, "committing quota",
slog.F("resources", resourceNames(resources)),
slog.F("cost", cost), slog.F("cost", cost),
) )
if cost == 0 { if cost == 0 {
@@ -953,9 +832,8 @@ func (r *Runner) commitQuota(ctx context.Context, resources []*sdkproto.Resource
const stage = "Commit quota" const stage = "Commit quota"
resp, err := r.quotaCommitter.CommitQuota(ctx, &proto.CommitQuotaRequest{ resp, err := r.quotaCommitter.CommitQuota(ctx, &proto.CommitQuotaRequest{
JobId: r.job.JobId, JobId: r.job.JobId,
// #nosec G115 - Safe conversion as cost is expected to be within int32 range for provisioning costs DailyCost: cost,
DailyCost: int32(cost),
}) })
if err != nil { if err != nil {
r.queueLog(ctx, &proto.Log{ r.queueLog(ctx, &proto.Log{
@@ -1014,8 +892,6 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
} }
failedJob := r.configure(&sdkproto.Config{ failedJob := r.configure(&sdkproto.Config{
TemplateSourceArchive: r.job.GetTemplateSourceArchive(),
State: r.job.GetWorkspaceBuild().State,
ProvisionerLogLevel: r.job.GetWorkspaceBuild().LogLevel, ProvisionerLogLevel: r.job.GetWorkspaceBuild().LogLevel,
TemplateId: strings2.EmptyToNil(r.job.GetWorkspaceBuild().Metadata.TemplateId), TemplateId: strings2.EmptyToNil(r.job.GetWorkspaceBuild().Metadata.TemplateId),
TemplateVersionId: strings2.EmptyToNil(r.job.GetWorkspaceBuild().Metadata.TemplateVersionId), TemplateVersionId: strings2.EmptyToNil(r.job.GetWorkspaceBuild().Metadata.TemplateVersionId),
@@ -1025,25 +901,53 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
return nil, failedJob return nil, failedJob
} }
resp, failed := r.buildWorkspace(ctx, "Planning infrastructure", &sdkproto.Request{ // timings collects all timings from each phase of the build
Type: &sdkproto.Request_Plan{ timings := make([]*sdkproto.Timing, 0)
Plan: &sdkproto.PlanRequest{
OmitModuleFiles: true, // Only useful for template imports // Initialize the Terraform working directory
Metadata: r.job.GetWorkspaceBuild().Metadata, initComplete, failedJob := r.init(ctx, true, r.job.GetTemplateSourceArchive())
RichParameterValues: r.job.GetWorkspaceBuild().RichParameterValues, if failedJob != nil {
PreviousParameterValues: r.job.GetWorkspaceBuild().PreviousParameterValues, return nil, failedJob
VariableValues: r.job.GetWorkspaceBuild().VariableValues, }
ExternalAuthProviders: r.job.GetWorkspaceBuild().ExternalAuthProviders, if initComplete == nil {
return nil, r.failedWorkspaceBuildf("invalid message type received from provisioner during init")
}
// Collect init timings
timings = append(timings, initComplete.Timings...)
if initComplete.Error != "" {
r.logger.Warn(context.Background(), "init request failed",
slog.F("error", initComplete.Error),
)
return nil, &proto.FailedJob{
JobId: r.job.JobId,
Error: initComplete.Error,
Type: &proto.FailedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{
State: r.job.GetWorkspaceBuild().State,
Timings: timings,
},
}, },
}, }
}
// Run `terraform plan`
planComplete, failed := r.plan(ctx, "Planning Infrastructure", &sdkproto.PlanRequest{
Metadata: r.job.GetWorkspaceBuild().Metadata,
RichParameterValues: r.job.GetWorkspaceBuild().RichParameterValues,
VariableValues: r.job.GetWorkspaceBuild().VariableValues,
ExternalAuthProviders: r.job.GetWorkspaceBuild().ExternalAuthProviders,
PreviousParameterValues: r.job.GetWorkspaceBuild().PreviousParameterValues,
State: r.job.GetWorkspaceBuild().State,
}) })
if failed != nil { if failed != nil {
return nil, failed return nil, failed
} }
planComplete := resp.GetPlan()
if planComplete == nil { if planComplete == nil {
return nil, r.failedWorkspaceBuildf("invalid message type %T received from provisioner", resp.Type) return nil, r.failedWorkspaceBuildf("invalid message type received from provisioner during plan")
} }
// Collect plan timings
timings = append(timings, planComplete.Timings...)
if planComplete.Error != "" { if planComplete.Error != "" {
r.logger.Warn(context.Background(), "plan request failed", r.logger.Warn(context.Background(), "plan request failed",
slog.F("error", planComplete.Error), slog.F("error", planComplete.Error),
@@ -1053,27 +957,28 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
JobId: r.job.JobId, JobId: r.job.JobId,
Error: planComplete.Error, Error: planComplete.Error,
Type: &proto.FailedJob_WorkspaceBuild_{ Type: &proto.FailedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{}, WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{
Timings: timings,
},
}, },
} }
} }
if len(planComplete.AiTasks) > 1 {
return nil, r.failedWorkspaceBuildf("only one 'coder_ai_task' resource can be provisioned per template") if planComplete.AiTaskCount > 1 {
return nil, r.failedWorkspaceBuildf("only one 'coder_ai_task' resource can be provisioned per template, found %d", planComplete.AiTaskCount)
} }
r.logger.Info(context.Background(), "plan request successful", r.logger.Info(context.Background(), "plan request successful")
slog.F("resource_count", len(planComplete.Resources)),
slog.F("resources", resourceNames(planComplete.Resources)),
)
r.flushQueuedLogs(ctx) r.flushQueuedLogs(ctx)
if commitQuota { if commitQuota {
failed = r.commitQuota(ctx, planComplete.Resources) failed = r.commitQuota(ctx, planComplete.GetDailyCost())
r.flushQueuedLogs(ctx) r.flushQueuedLogs(ctx)
if failed != nil { if failed != nil {
return nil, failed return nil, failed
} }
} }
// Run Terraform Apply
r.queueLog(ctx, &proto.Log{ r.queueLog(ctx, &proto.Log{
Source: proto.LogSource_PROVISIONER_DAEMON, Source: proto.LogSource_PROVISIONER_DAEMON,
Level: sdkproto.LogLevel_INFO, Level: sdkproto.LogLevel_INFO,
@@ -1081,24 +986,17 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
CreatedAt: time.Now().UnixMilli(), CreatedAt: time.Now().UnixMilli(),
}) })
resp, failed = r.buildWorkspace(ctx, applyStage, &sdkproto.Request{ applyComplete, failed := r.apply(ctx, applyStage, &sdkproto.ApplyRequest{
Type: &sdkproto.Request_Apply{ Metadata: r.job.GetWorkspaceBuild().Metadata,
Apply: &sdkproto.ApplyRequest{
Metadata: r.job.GetWorkspaceBuild().Metadata,
},
},
}) })
if failed != nil { if failed != nil {
return nil, failed return nil, failed
} }
applyComplete := resp.GetApply()
if applyComplete == nil { if applyComplete == nil {
return nil, r.failedWorkspaceBuildf("invalid message type %T received from provisioner", resp.Type) return nil, r.failedWorkspaceBuildf("invalid message type received from provisioner during apply")
} }
// Collect apply timings
// Prepend the plan timings (since they occurred first). timings = append(timings, applyComplete.Timings...)
applyComplete.Timings = append(planComplete.Timings, applyComplete.Timings...)
if applyComplete.Error != "" { if applyComplete.Error != "" {
r.logger.Warn(context.Background(), "apply failed; updating state", r.logger.Warn(context.Background(), "apply failed; updating state",
slog.F("error", applyComplete.Error), slog.F("error", applyComplete.Error),
@@ -1111,15 +1009,46 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
Type: &proto.FailedJob_WorkspaceBuild_{ Type: &proto.FailedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{ WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{
State: applyComplete.State, State: applyComplete.State,
Timings: applyComplete.Timings, Timings: timings,
},
},
}
}
// Run Terraform Graph
graphComplete, failed := r.graph(ctx, &sdkproto.GraphRequest{
Metadata: r.job.GetWorkspaceBuild().Metadata,
Source: sdkproto.GraphSource_SOURCE_STATE,
})
if failed != nil {
return nil, failed
}
if graphComplete == nil {
return nil, r.failedWorkspaceBuildf("invalid message type received from provisioner during graph")
}
// Collect graph timings
timings = append(timings, graphComplete.Timings...)
if graphComplete.Error != "" {
r.logger.Warn(context.Background(), "graph request failed",
slog.F("error", planComplete.Error),
)
return nil, &proto.FailedJob{
JobId: r.job.JobId,
Error: graphComplete.Error,
Type: &proto.FailedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{
// Graph does not change the state, so return the state returned from apply.
State: applyComplete.State,
Timings: timings,
}, },
}, },
} }
} }
r.logger.Info(context.Background(), "apply successful", r.logger.Info(context.Background(), "apply successful",
slog.F("resource_count", len(applyComplete.Resources)), slog.F("resource_count", len(graphComplete.Resources)),
slog.F("resources", resourceNames(applyComplete.Resources)), slog.F("resources", resourceNames(graphComplete.Resources)),
slog.F("state_len", len(applyComplete.State)), slog.F("state_len", len(applyComplete.State)),
) )
r.flushQueuedLogs(ctx) r.flushQueuedLogs(ctx)
@@ -1129,15 +1058,14 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
Type: &proto.CompletedJob_WorkspaceBuild_{ Type: &proto.CompletedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{ WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{
State: applyComplete.State, State: applyComplete.State,
Resources: applyComplete.Resources, Resources: graphComplete.Resources,
Timings: applyComplete.Timings, Timings: timings,
// Modules are created on disk by `terraform init`, and that is only // Modules files are omitted for workspace builds, but the modules.json metadata
// called by `plan`. `apply` does not modify them, so we can use the // is available from init to return.
// modules from the plan response. Modules: initComplete.Modules,
Modules: planComplete.Modules,
// Resource replacements are discovered at plan time, only. // Resource replacements are discovered at plan time, only.
ResourceReplacements: planComplete.ResourceReplacements, ResourceReplacements: planComplete.ResourceReplacements,
AiTasks: applyComplete.AiTasks, AiTasks: graphComplete.AiTasks,
}, },
}, },
}, nil }, nil
+8
View File
@@ -10,6 +10,10 @@ func ParseErrorf(format string, args ...any) *proto.ParseComplete {
return &proto.ParseComplete{Error: fmt.Sprintf(format, args...)} return &proto.ParseComplete{Error: fmt.Sprintf(format, args...)}
} }
func InitErrorf(format string, args ...any) *proto.InitComplete {
return &proto.InitComplete{Error: fmt.Sprintf(format, args...)}
}
func PlanErrorf(format string, args ...any) *proto.PlanComplete { func PlanErrorf(format string, args ...any) *proto.PlanComplete {
return &proto.PlanComplete{Error: fmt.Sprintf(format, args...)} return &proto.PlanComplete{Error: fmt.Sprintf(format, args...)}
} }
@@ -17,3 +21,7 @@ func PlanErrorf(format string, args ...any) *proto.PlanComplete {
func ApplyErrorf(format string, args ...any) *proto.ApplyComplete { func ApplyErrorf(format string, args ...any) *proto.ApplyComplete {
return &proto.ApplyComplete{Error: fmt.Sprintf(format, args...)} return &proto.ApplyComplete{Error: fmt.Sprintf(format, args...)}
} }
func GraphError(format string, args ...any) *proto.GraphComplete {
return &proto.GraphComplete{Error: fmt.Sprintf(format, args...)}
}
+962 -575
View File
File diff suppressed because it is too large Load Diff
+91 -49
View File
@@ -369,16 +369,12 @@ message Metadata {
// Config represents execution configuration shared by all subsequent requests in the Session // Config represents execution configuration shared by all subsequent requests in the Session
message Config { message Config {
// template_source_archive is a tar of the template source files string provisioner_log_level = 1;
bytes template_source_archive = 1;
// state is the provisioner state (if any)
bytes state = 2;
string provisioner_log_level = 3;
// Template imports can omit template id // Template imports can omit template id
optional string template_id = 4; optional string template_id = 2;
// Dry runs omit version id // Dry runs omit version id
optional string template_version_id = 5; optional string template_version_id = 3;
optional bool exp_reuse_terraform_workspace = 6; // Whether to reuse existing terraform workspaces if they exist. optional bool exp_reuse_terraform_workspace = 4; // Whether to reuse existing terraform workspaces if they exist.
} }
// ParseRequest consumes source-code to produce inputs. // ParseRequest consumes source-code to produce inputs.
@@ -393,6 +389,25 @@ message ParseComplete {
map<string, string> workspace_tags = 4; map<string, string> workspace_tags = 4;
} }
message InitRequest {
// template_source_archive is a tar of the template source files
bytes template_source_archive = 1;
// If true, the provisioner can safely assume the caller does not need the
// module files downloaded by the `terraform init` command.
// Ideally this boolean would be flipped in its truthy value, however since
// this is costly, the zero value omitting the module files is preferred.
bool omit_module_files = 3;
}
message InitComplete {
string error = 1;
repeated Timing timings = 2;
repeated Module modules = 3;
bytes module_files = 4;
bytes module_files_hash = 5;
}
// PlanRequest asks the provisioner to plan what resources & parameters it will create // PlanRequest asks the provisioner to plan what resources & parameters it will create
message PlanRequest { message PlanRequest {
Metadata metadata = 1; Metadata metadata = 1;
@@ -401,52 +416,61 @@ message PlanRequest {
repeated ExternalAuthProvider external_auth_providers = 4; repeated ExternalAuthProvider external_auth_providers = 4;
repeated RichParameterValue previous_parameter_values = 5; repeated RichParameterValue previous_parameter_values = 5;
// If true, the provisioner can safely assume the caller does not need the // state is the provisioner state (if any)
// module files downloaded by the `terraform init` command. bytes state = 6;
// Ideally this boolean would be flipped in its truthy value, however for
// backwards compatibility reasons, the zero value should be the previous
// behavior of downloading the module files.
bool omit_module_files = 6;
} }
// PlanComplete indicates a request to plan completed. // PlanComplete indicates a request to plan completed.
message PlanComplete { message PlanComplete {
string error = 1; string error = 1;
repeated Resource resources = 2; repeated Timing timings = 2;
repeated RichParameter parameters = 3; bytes plan = 3;
repeated ExternalAuthProviderResource external_auth_providers = 4; int32 dailyCost = 4;
repeated Timing timings = 6; repeated ResourceReplacement resource_replacements = 5;
repeated Module modules = 7; int32 ai_task_count = 6;
repeated Preset presets = 8;
bytes plan = 9;
repeated ResourceReplacement resource_replacements = 10;
bytes module_files = 11;
bytes module_files_hash = 12;
// Whether a template has any `coder_ai_task` resources defined, even if not planned for creation.
// During a template import, a plan is run which may not yield in any `coder_ai_task` resources, but nonetheless we
// still need to know that such resources are defined.
//
// See `hasAITaskResources` in provisioner/terraform/resources.go for more details.
bool has_ai_tasks = 13;
repeated provisioner.AITask ai_tasks = 14;
bool has_external_agents = 15;
} }
// ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response // ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response
// in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session. // in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session.
message ApplyRequest { message ApplyRequest {
Metadata metadata = 1; Metadata metadata = 1;
// state is the provisioner state (if any)
bytes state = 6;
} }
// ApplyComplete indicates a request to apply completed. // ApplyComplete indicates a request to apply completed.
message ApplyComplete { message ApplyComplete {
bytes state = 1; bytes state = 1;
string error = 2; string error = 2;
repeated Timing timings = 3;
}
enum GraphSource {
SOURCE_UNKNOWN = 0;
SOURCE_PLAN = 1;
SOURCE_STATE = 2;
}
message GraphRequest {
Metadata metadata = 1;
GraphSource source = 2;
}
message GraphComplete {
string error = 1;
repeated Timing timings = 2;
repeated Resource resources = 3; repeated Resource resources = 3;
repeated RichParameter parameters = 4; repeated RichParameter parameters = 4;
repeated ExternalAuthProviderResource external_auth_providers = 5; repeated ExternalAuthProviderResource external_auth_providers = 5;
repeated Timing timings = 6; repeated Preset presets = 6;
repeated provisioner.AITask ai_tasks = 7; // Whether a template has any `coder_ai_task` resources defined, even if not planned for creation.
// During a template import, a plan is run which may not yield in any `coder_ai_task` resources, but nonetheless we
// still need to know that such resources are defined.
//
// See `hasAITaskResources` in provisioner/terraform/resources.go for more details.
bool has_ai_tasks = 7;
repeated provisioner.AITask ai_tasks = 8;
bool has_external_agents = 9;
} }
message Timing { message Timing {
@@ -472,9 +496,11 @@ message Request {
oneof type { oneof type {
Config config = 1; Config config = 1;
ParseRequest parse = 2; ParseRequest parse = 2;
PlanRequest plan = 3; InitRequest init = 3;
ApplyRequest apply = 4; PlanRequest plan = 4;
CancelRequest cancel = 5; ApplyRequest apply = 5;
GraphRequest graph = 6;
CancelRequest cancel = 7;
} }
} }
@@ -482,10 +508,12 @@ message Response {
oneof type { oneof type {
Log log = 1; Log log = 1;
ParseComplete parse = 2; ParseComplete parse = 2;
PlanComplete plan = 3; InitComplete init = 3;
ApplyComplete apply = 4; PlanComplete plan = 4;
DataUpload data_upload = 5; ApplyComplete apply = 5;
ChunkPiece chunk_piece = 6; GraphComplete graph = 6;
DataUpload data_upload = 7;
ChunkPiece chunk_piece = 8;
} }
} }
@@ -519,14 +547,28 @@ message ChunkPiece {
service Provisioner { service Provisioner {
// Session represents provisioning a single template import or workspace. The daemon always sends Config followed // Session represents provisioning a single template import or workspace. The daemon always sends Config followed
// by one of the requests (ParseRequest, PlanRequest, ApplyRequest). The provisioner should respond with a stream // by one of the requests (InitRequest, ParseRequest, PlanRequest, ApplyRequest, GraphRequest). The provisioner
// of zero or more Logs, followed by the corresponding complete message (ParseComplete, PlanComplete, // should respond with a stream of zero or more Logs, followed by the corresponding complete message
// ApplyComplete). The daemon may then send a new request. A request to apply MUST be preceded by a request plan, // (InitComplete, ParseComplete, PlanComplete, ApplyComplete, GraphComplete).
// and the provisioner should store the plan data on the Session after a successful plan, so that the daemon may // The daemon may then send a new request.
// request an apply. If the daemon closes the Session without an apply, the plan data may be safely discarded.
// //
// The daemon may send a CancelRequest, asynchronously to ask the provisioner to cancel the previous ParseRequest, // A request to Parse or Plan MUST be preceded by a request init. The provisioner should store the init data on
// PlanRequest, or ApplyRequest. The provisioner MUST reply with a complete message corresponding to the request // the session after a successful init. If the daemon closes the session, the init data may be safely discarded.
// that was canceled. If the provisioner has already completed the request, it may ignore the CancelRequest. //
// A request to apply MUST be preceded by a request plan, and the provisioner should store the plan data on the
// Session after a successful plan, so that the daemon may request an apply. If the daemon closes
// the Session without an apply, the plan data may be safely discarded.
//
// A request to graph MUST be preceded by a plan or an apply.
//
// The order of requests is then one of the following:
// 1. Init -> Parse
// 2. Init -> Plan -> Graph
// 3. Init -> Plan -> Apply -> Graph
//
// The daemon may send a CancelRequest, asynchronously to ask the provisioner to cancel the previous InitRequest,
// ParseRequest, PlanRequest, ApplyRequest, or GraphRequest. The provisioner MUST reply with a complete message
// corresponding to the request that was canceled. If the provisioner has already completed the request,
// it may ignore the CancelRequest.
rpc Session(stream Request) returns (stream Response); rpc Session(stream Request) returns (stream Response);
} }
+2
View File
@@ -35,9 +35,11 @@ type ServeOptions struct {
} }
type Server interface { type Server interface {
Init(s *Session, r *proto.InitRequest, canceledOrComplete <-chan struct{}) *proto.InitComplete
Parse(s *Session, r *proto.ParseRequest, canceledOrComplete <-chan struct{}) *proto.ParseComplete Parse(s *Session, r *proto.ParseRequest, canceledOrComplete <-chan struct{}) *proto.ParseComplete
Plan(s *Session, r *proto.PlanRequest, canceledOrComplete <-chan struct{}) *proto.PlanComplete Plan(s *Session, r *proto.PlanRequest, canceledOrComplete <-chan struct{}) *proto.PlanComplete
Apply(s *Session, r *proto.ApplyRequest, canceledOrComplete <-chan struct{}) *proto.ApplyComplete Apply(s *Session, r *proto.ApplyRequest, canceledOrComplete <-chan struct{}) *proto.ApplyComplete
Graph(s *Session, r *proto.GraphRequest, canceledOrComplete <-chan struct{}) *proto.GraphComplete
} }
// Serve starts a dRPC connection for the provisioner and transport provided. // Serve starts a dRPC connection for the provisioner and transport provided.
+20
View File
@@ -44,6 +44,11 @@ func TestProvisionerSDK(t *testing.T) {
err = s.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}}) err = s.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}})
require.NoError(t, err) require.NoError(t, err)
err = s.Send(&proto.Request{Type: &proto.Request_Init{Init: &proto.InitRequest{}}})
require.NoError(t, err)
_, err = s.Recv()
require.NoError(t, err)
err = s.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}}) err = s.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}})
require.NoError(t, err) require.NoError(t, err)
msg, err := s.Recv() msg, err := s.Recv()
@@ -102,6 +107,11 @@ func TestProvisionerSDK(t *testing.T) {
err = s.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}}) err = s.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}})
require.NoError(t, err) require.NoError(t, err)
err = s.Send(&proto.Request{Type: &proto.Request_Init{Init: &proto.InitRequest{}}})
require.NoError(t, err)
_, err = s.Recv()
require.NoError(t, err)
err = s.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}}) err = s.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}})
require.NoError(t, err) require.NoError(t, err)
msg, err := s.Recv() msg, err := s.Recv()
@@ -135,8 +145,18 @@ func TestProvisionerSDK(t *testing.T) {
}) })
} }
var _ provisionersdk.Server = unimplementedServer{}
type unimplementedServer struct{} type unimplementedServer struct{}
func (unimplementedServer) Init(s *provisionersdk.Session, r *proto.InitRequest, canceledOrComplete <-chan struct{}) *proto.InitComplete {
return &proto.InitComplete{}
}
func (unimplementedServer) Graph(s *provisionersdk.Session, r *proto.GraphRequest, canceledOrComplete <-chan struct{}) *proto.GraphComplete {
return &proto.GraphComplete{Error: "unimplemented"}
}
func (unimplementedServer) Parse(_ *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete { func (unimplementedServer) Parse(_ *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete {
return &proto.ParseComplete{Error: "unimplemented"} return &proto.ParseComplete{Error: "unimplemented"}
} }
+102 -41
View File
@@ -66,10 +66,6 @@ func (p *protoServer) Session(stream proto.DRPCProvisioner_SessionStream) error
return xerrors.Errorf("unable to clean stale sessions %q: %w", s.Files, err) return xerrors.Errorf("unable to clean stale sessions %q: %w", s.Files, err)
} }
err = s.Files.ExtractArchive(s.Context(), s.Logger, afero.NewOsFs(), s.Config)
if err != nil {
return xerrors.Errorf("extract archive: %w", err)
}
return s.handleRequests() return s.handleRequests()
} }
@@ -110,6 +106,10 @@ func (s *Session) handleRequests() error {
} }
resp := &proto.Response{} resp := &proto.Response{}
if parse := req.GetParse(); parse != nil { if parse := req.GetParse(); parse != nil {
if !s.initialized {
// Files must be initialized before parsing.
return xerrors.New("cannot parse before successful init")
}
r := &request[*proto.ParseRequest, *proto.ParseComplete]{ r := &request[*proto.ParseRequest, *proto.ParseComplete]{
req: parse, req: parse,
session: s, session: s,
@@ -129,48 +129,28 @@ func (s *Session) handleRequests() error {
} }
resp.Type = &proto.Response_Parse{Parse: complete} resp.Type = &proto.Response_Parse{Parse: complete}
} }
if plan := req.GetPlan(); plan != nil { if init := req.GetInit(); init != nil {
r := &request[*proto.PlanRequest, *proto.PlanComplete]{ if s.initialized {
req: plan, return xerrors.New("cannot init more than once per session")
session: s,
serverFn: s.server.Plan,
cancels: requests,
} }
complete, err := r.do() initResp, err := s.handleInitRequest(init, requests)
if err != nil { if err != nil {
return err return err
} }
resp.Type = &proto.Response_Plan{Plan: complete} resp.Type = &proto.Response_Init{Init: initResp}
}
if protobuf.Size(resp) > drpcsdk.MaxMessageSize { if plan := req.GetPlan(); plan != nil {
// It is likely the modules that is pushing the message size over the limit. if !s.initialized {
// Send the modules over a stream of messages instead. return xerrors.New("cannot plan before successful init")
s.Logger.Info(s.Context(), "plan response too large, sending modules as stream",
slog.F("size_bytes", len(complete.ModuleFiles)),
)
dataUp, chunks := proto.BytesToDataUpload(proto.DataUploadType_UPLOAD_TYPE_MODULE_FILES, complete.ModuleFiles)
complete.ModuleFiles = nil // sent over the stream
complete.ModuleFilesHash = dataUp.DataHash
resp.Type = &proto.Response_Plan{Plan: complete}
err := s.stream.Send(&proto.Response{Type: &proto.Response_DataUpload{DataUpload: dataUp}})
if err != nil {
complete.Error = fmt.Sprintf("send data upload: %s", err.Error())
} else {
for i, chunk := range chunks {
err := s.stream.Send(&proto.Response{Type: &proto.Response_ChunkPiece{ChunkPiece: chunk}})
if err != nil {
complete.Error = fmt.Sprintf("send data piece upload %d/%d: %s", i, dataUp.Chunks, err.Error())
break
}
}
}
} }
planResp, err := s.handlePlanRequest(plan, requests)
if complete.Error == "" { if err != nil {
return err
}
if planResp.Error == "" {
planned = true planned = true
} }
resp.Type = &proto.Response_Plan{Plan: planResp}
} }
if apply := req.GetApply(); apply != nil { if apply := req.GetApply(); apply != nil {
if !planned { if !planned {
@@ -188,6 +168,23 @@ func (s *Session) handleRequests() error {
} }
resp.Type = &proto.Response_Apply{Apply: complete} resp.Type = &proto.Response_Apply{Apply: complete}
} }
if graph := req.GetGraph(); graph != nil {
if !s.initialized {
return xerrors.New("cannot graph before successful init")
}
r := &request[*proto.GraphRequest, *proto.GraphComplete]{
req: graph,
session: s,
serverFn: s.server.Graph,
cancels: requests,
}
complete, err := r.do()
if err != nil {
return err
}
resp.Type = &proto.Response_Graph{Graph: complete}
}
err := s.stream.Send(resp) err := s.stream.Send(resp)
if err != nil { if err != nil {
return xerrors.Errorf("send response: %w", err) return xerrors.Errorf("send response: %w", err)
@@ -196,11 +193,75 @@ func (s *Session) handleRequests() error {
return nil return nil
} }
func (s *Session) handleInitRequest(init *proto.InitRequest, requests <-chan *proto.Request) (*proto.InitComplete, error) {
r := &request[*proto.InitRequest, *proto.InitComplete]{
req: init,
session: s,
serverFn: s.server.Init,
cancels: requests,
}
complete, err := r.do()
if err != nil {
return nil, err
}
if complete.Error != "" {
return complete, nil
}
// If the size of the complete message is too large, we need to stream the module files separately.
if protobuf.Size(&proto.Response{Type: &proto.Response_Init{Init: complete}}) > drpcsdk.MaxMessageSize {
// It is likely the modules that is pushing the message size over the limit.
// Send the modules over a stream of messages instead.
s.Logger.Info(s.Context(), "plan response too large, sending modules as stream",
slog.F("size_bytes", len(complete.ModuleFiles)),
)
dataUp, chunks := proto.BytesToDataUpload(proto.DataUploadType_UPLOAD_TYPE_MODULE_FILES, complete.ModuleFiles)
complete.ModuleFiles = nil // sent over the stream
complete.ModuleFilesHash = dataUp.DataHash
err := s.stream.Send(&proto.Response{Type: &proto.Response_DataUpload{DataUpload: dataUp}})
if err != nil {
complete.Error = fmt.Sprintf("send data upload: %s", err.Error())
} else {
for i, chunk := range chunks {
err := s.stream.Send(&proto.Response{Type: &proto.Response_ChunkPiece{ChunkPiece: chunk}})
if err != nil {
complete.Error = fmt.Sprintf("send data piece upload %d/%d: %s", i, dataUp.Chunks, err.Error())
break
}
}
}
}
s.initialized = true
return complete, nil
}
func (s *Session) handlePlanRequest(plan *proto.PlanRequest, requests <-chan *proto.Request) (*proto.PlanComplete, error) {
r := &request[*proto.PlanRequest, *proto.PlanComplete]{
req: plan,
session: s,
serverFn: s.server.Plan,
cancels: requests,
}
complete, err := r.do()
if err != nil {
return nil, err
}
return complete, nil
}
type Session struct { type Session struct {
Logger slog.Logger Logger slog.Logger
Files tfpath.Layouter Files tfpath.Layouter
Config *proto.Config Config *proto.Config
// initialized indicates if an init was run.
// Required for plan/apply.
initialized bool
server Server server Server
stream proto.DRPCProvisioner_SessionStream stream proto.DRPCProvisioner_SessionStream
logLevel int32 logLevel int32
@@ -226,11 +287,11 @@ func (s *Session) ProvisionLog(level proto.LogLevel, output string) {
} }
type pRequest interface { type pRequest interface {
*proto.ParseRequest | *proto.PlanRequest | *proto.ApplyRequest *proto.ParseRequest | *proto.InitRequest | *proto.PlanRequest | *proto.ApplyRequest | *proto.GraphRequest
} }
type pComplete interface { type pComplete interface {
*proto.ParseComplete | *proto.PlanComplete | *proto.ApplyComplete *proto.ParseComplete | *proto.InitComplete | *proto.PlanComplete | *proto.ApplyComplete | *proto.GraphComplete
} }
// request processes a single request call to the Server and returns its complete result, while also processing cancel // request processes a single request call to the Server and returns its complete result, while also processing cancel
+4 -9
View File
@@ -16,7 +16,6 @@ import (
"golang.org/x/xerrors" "golang.org/x/xerrors"
"cdr.dev/slog" "cdr.dev/slog"
"github.com/coder/coder/v2/provisionersdk/proto"
) )
type Layouter interface { type Layouter interface {
@@ -28,7 +27,7 @@ type Layouter interface {
TerraformMetadataDir() string TerraformMetadataDir() string
ModulesDirectory() string ModulesDirectory() string
ModulesFilePath() string ModulesFilePath() string
ExtractArchive(ctx context.Context, logger slog.Logger, fs afero.Fs, cfg *proto.Config) error ExtractArchive(ctx context.Context, logger slog.Logger, fs afero.Fs, templateSourceArchive []byte) error
Cleanup(ctx context.Context, logger slog.Logger, fs afero.Fs) Cleanup(ctx context.Context, logger slog.Logger, fs afero.Fs)
CleanStaleSessions(ctx context.Context, logger slog.Logger, fs afero.Fs, now time.Time) error CleanStaleSessions(ctx context.Context, logger slog.Logger, fs afero.Fs, now time.Time) error
} }
@@ -93,9 +92,9 @@ func (l Layout) ModulesFilePath() string {
return filepath.Join(l.ModulesDirectory(), "modules.json") return filepath.Join(l.ModulesDirectory(), "modules.json")
} }
func (l Layout) ExtractArchive(ctx context.Context, logger slog.Logger, fs afero.Fs, cfg *proto.Config) error { func (l Layout) ExtractArchive(ctx context.Context, logger slog.Logger, fs afero.Fs, templateSourceArchive []byte) error {
logger.Info(ctx, "unpacking template source archive", logger.Info(ctx, "unpacking template source archive",
slog.F("size_bytes", len(cfg.TemplateSourceArchive)), slog.F("size_bytes", len(templateSourceArchive)),
) )
err := fs.MkdirAll(l.WorkDirectory(), 0o700) err := fs.MkdirAll(l.WorkDirectory(), 0o700)
@@ -103,11 +102,7 @@ func (l Layout) ExtractArchive(ctx context.Context, logger slog.Logger, fs afero
return xerrors.Errorf("create work directory %q: %w", l.WorkDirectory(), err) return xerrors.Errorf("create work directory %q: %w", l.WorkDirectory(), err)
} }
// TODO: Pass in cfg.TemplateSourceArchive, not the full config. reader := tar.NewReader(bytes.NewBuffer(templateSourceArchive))
// niling out the config field is a bit hacky.
reader := tar.NewReader(bytes.NewBuffer(cfg.TemplateSourceArchive))
// for safety, nil out the reference on Config, since the reader now owns it.
cfg.TemplateSourceArchive = nil
for { for {
header, err := reader.Next() header, err := reader.Next()
if err != nil { if err != nil {
+3 -5
View File
@@ -140,9 +140,9 @@ func (td Layout) Cleanup(ctx context.Context, logger slog.Logger, fs afero.Fs) {
slog.F("path", path), slog.Error(err)) slog.F("path", path), slog.Error(err))
} }
func (td Layout) ExtractArchive(ctx context.Context, logger slog.Logger, fs afero.Fs, cfg *proto.Config) error { func (td Layout) ExtractArchive(ctx context.Context, logger slog.Logger, fs afero.Fs, archive []byte) error {
logger.Info(ctx, "unpacking template source archive", logger.Info(ctx, "unpacking template source archive",
slog.F("size_bytes", len(cfg.TemplateSourceArchive)), slog.F("size_bytes", len(archive)),
) )
err := fs.MkdirAll(td.WorkDirectory(), 0o700) err := fs.MkdirAll(td.WorkDirectory(), 0o700)
@@ -163,9 +163,7 @@ func (td Layout) ExtractArchive(ctx context.Context, logger slog.Logger, fs afer
return xerrors.Errorf("select terraform workspace: %w", err) return xerrors.Errorf("select terraform workspace: %w", err)
} }
reader := tar.NewReader(bytes.NewBuffer(cfg.TemplateSourceArchive)) reader := tar.NewReader(bytes.NewBuffer(archive))
// for safety, nil out the reference on Config, since the reader now owns it.
cfg.TemplateSourceArchive = nil
for { for {
header, err := reader.Next() header, err := reader.Next()
if err != nil { if err != nil {
+3 -3
View File
@@ -230,9 +230,9 @@ func setupRunnerTest(t *testing.T) (client *codersdk.Client, agentID uuid.UUID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
+3 -3
View File
@@ -43,10 +43,10 @@ func TestRun(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Name: "example", Name: "example",
+42 -44
View File
@@ -60,27 +60,11 @@ func Test_Runner(t *testing.T) {
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: testParameters, Parameters: testParameters,
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "hello from logs",
},
},
},
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Name: "example", Name: "example",
@@ -101,6 +85,21 @@ func Test_Runner(t *testing.T) {
}, },
}, },
}, },
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "hello from logs",
},
},
},
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{},
},
},
},
}) })
version = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) version = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -209,10 +208,10 @@ func Test_Runner(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: testParameters, Parameters: testParameters,
}, },
}, },
@@ -341,27 +340,11 @@ func Test_Runner(t *testing.T) {
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: testParameters, Parameters: testParameters,
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "hello from logs",
},
},
},
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Name: "example", Name: "example",
@@ -382,6 +365,21 @@ func Test_Runner(t *testing.T) {
}, },
}, },
}, },
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "hello from logs",
},
},
},
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{},
},
},
},
}) })
version = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) version = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -484,10 +482,10 @@ func Test_Runner(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Graph{
Plan: &proto.PlanComplete{ Graph: &proto.GraphComplete{
Parameters: testParameters, Parameters: testParameters,
}, },
}, },
+3 -3
View File
@@ -257,9 +257,9 @@ func setupRunnerTest(t *testing.T) (client *codersdk.Client, agentID uuid.UUID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
+12 -3
View File
@@ -58,7 +58,14 @@ func Test_Runner(t *testing.T) {
}, },
{ {
Type: &proto.Response_Apply{ Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{ Apply: &proto.ApplyComplete{},
},
},
},
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Name: "example1", Name: "example1",
@@ -245,8 +252,10 @@ func Test_Runner(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
ProvisionApply: []*proto.Response{ ProvisionApply: []*proto.Response{
{ {
Type: &proto.Response_Apply{ Type: &proto.Response_Apply{
+6 -6
View File
@@ -49,9 +49,9 @@ func TestRun(t *testing.T) {
version = coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{ version = coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
@@ -168,9 +168,9 @@ func TestRun(t *testing.T) {
version = coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{ version = coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{ ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{ Resources: []*proto.Resource{{
Name: "example", Name: "example",
Type: "aws_instance", Type: "aws_instance",
+3 -3
View File
@@ -39,10 +39,10 @@ func TestRun(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete, ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{ ProvisionGraph: []*proto.Response{
{ {
Type: &proto.Response_Apply{ Type: &proto.Response_Graph{
Apply: &proto.ApplyComplete{ Graph: &proto.GraphComplete{
Resources: []*proto.Resource{ Resources: []*proto.Resource{
{ {
Name: "example", Name: "example",
+110 -49
View File
@@ -32,6 +32,8 @@ import {
type ApplyComplete, type ApplyComplete,
AppSharingLevel, AppSharingLevel,
type ExternalAuthProviderResource, type ExternalAuthProviderResource,
type GraphComplete,
type InitComplete,
type ParseComplete, type ParseComplete,
type PlanComplete, type PlanComplete,
type Resource, type Resource,
@@ -540,12 +542,14 @@ type RecursivePartial<T> = {
}; };
interface EchoProvisionerResponses { interface EchoProvisionerResponses {
init?: RecursivePartial<Response>[];
// parse is for observing any Terraform variables // parse is for observing any Terraform variables
parse?: RecursivePartial<Response>[]; parse?: RecursivePartial<Response>[];
// plan occurs when the template is imported // plan occurs when the template is imported
plan?: RecursivePartial<Response>[]; plan?: RecursivePartial<Response>[];
// apply occurs when the workspace is built // apply occurs when the workspace is built
apply?: RecursivePartial<Response>[]; apply?: RecursivePartial<Response>[];
graph?: RecursivePartial<Response>[];
// extraFiles allows the bundling of terraform files in echo provisioner tars // extraFiles allows the bundling of terraform files in echo provisioner tars
// in order to support dynamic parameters // in order to support dynamic parameters
extraFiles?: Map<string, string>; extraFiles?: Map<string, string>;
@@ -560,6 +564,40 @@ const emptyPlan = new TextEncoder().encode("{}");
const createTemplateVersionTar = async ( const createTemplateVersionTar = async (
responses: EchoProvisionerResponses = {}, responses: EchoProvisionerResponses = {},
): Promise<Buffer> => { ): Promise<Buffer> => {
if (responses.graph) {
if (!responses.apply) {
responses.apply = responses.graph.map((response) => {
if (response.log) {
return response;
}
return {
apply: {
error: response.graph?.error ?? "",
},
};
});
}
if (!responses.plan) {
responses.plan = responses.graph.map((response) => {
if (response.log) {
return response;
}
return {
plan: {
error: response.graph?.error ?? "",
},
};
});
}
}
if (!responses.init) {
responses.init = [
{
init: {},
},
];
}
if (!responses.parse) { if (!responses.parse) {
responses.parse = [ responses.parse = [
{ {
@@ -575,25 +613,18 @@ const createTemplateVersionTar = async (
]; ];
} }
if (!responses.plan) { if (!responses.plan) {
responses.plan = responses.apply.map((response) => { responses.plan = [
if (response.log) { {
return response; plan: {},
} },
return { ];
plan: { }
error: response.apply?.error ?? "", if (!responses.graph) {
resources: response.apply?.resources ?? [], responses.graph = [
parameters: response.apply?.parameters ?? [], {
externalAuthProviders: response.apply?.externalAuthProviders ?? [], graph: {},
timings: response.apply?.timings ?? [], },
presets: [], ];
resourceReplacements: [],
plan: emptyPlan,
moduleFiles: new Uint8Array(),
moduleFilesHash: new Uint8Array(),
},
};
});
} }
const tar = new TarWriter(); const tar = new TarWriter();
@@ -617,6 +648,33 @@ const createTemplateVersionTar = async (
Response.encode(response as Response).finish(), Response.encode(response as Response).finish(),
); );
}); });
responses.init.forEach((response, index) => {
response.init = {
error: "",
timings: [],
modules: [],
moduleFiles: new Uint8Array(),
moduleFilesHash: new Uint8Array(),
...response.init,
} as InitComplete;
tar.addFile(
`${index}.init.protobuf`,
Response.encode(response as Response).finish(),
);
});
responses.plan.forEach((response, index) => {
response.plan = {
error: "",
timings: [],
plan: emptyPlan,
resourceReplacements: [],
...response.plan,
} as PlanComplete;
tar.addFile(
`${index}.plan.protobuf`,
Response.encode(response as Response).finish(),
);
});
const fillResource = (resource: RecursivePartial<Resource>) => { const fillResource = (resource: RecursivePartial<Resource>) => {
if (resource.agents) { if (resource.agents) {
@@ -701,40 +759,31 @@ const createTemplateVersionTar = async (
response.apply = { response.apply = {
error: "", error: "",
state: new Uint8Array(), state: new Uint8Array(),
resources: [],
parameters: [],
externalAuthProviders: [],
timings: [], timings: [],
aiTasks: [],
...response.apply, ...response.apply,
} as ApplyComplete; } as ApplyComplete;
response.apply.resources = response.apply.resources?.map(fillResource);
tar.addFile( tar.addFile(
`${index}.apply.protobuf`, `${index}.apply.protobuf`,
Response.encode(response as Response).finish(), Response.encode(response as Response).finish(),
); );
}); });
responses.plan.forEach((response, index) => { responses.graph.forEach((response, index) => {
response.plan = { response.graph = {
error: "", error: "",
resources: [], resources: [],
parameters: [], parameters: [],
externalAuthProviders: [], externalAuthProviders: [],
timings: [], timings: [],
modules: [],
presets: [], presets: [],
resourceReplacements: [], resourceReplacements: [],
plan: emptyPlan,
moduleFiles: new Uint8Array(),
moduleFilesHash: new Uint8Array(),
aiTasks: [], aiTasks: [],
...response.plan, ...response.graph,
} as PlanComplete; } as GraphComplete;
response.plan.resources = response.plan.resources?.map(fillResource); response.graph.resources = response.graph.resources?.map(fillResource);
tar.addFile( tar.addFile(
`${index}.plan.protobuf`, `${index}.graph.protobuf`,
Response.encode(response as Response).finish(), Response.encode(response as Response).finish(),
); );
}); });
@@ -889,16 +938,20 @@ ${options}}
parse: {}, parse: {},
}, },
], ],
plan: [ init: [
{ {
plan: { init: {},
parameters: richParameters,
},
}, },
], ],
apply: [ plan: [
{ {
apply: { plan: {},
},
],
graph: [
{
graph: {
parameters: richParameters,
resources: [ resources: [
{ {
name: "example", name: "example",
@@ -907,6 +960,11 @@ ${options}}
}, },
}, },
], ],
apply: [
{
apply: {},
},
],
extraFiles: new Map([["main.tf", tf]]), extraFiles: new Map([["main.tf", tf]]),
}; };
}; };
@@ -915,21 +973,19 @@ export const echoResponsesWithExternalAuth = (
providers: ExternalAuthProviderResource[], providers: ExternalAuthProviderResource[],
): EchoProvisionerResponses => { ): EchoProvisionerResponses => {
return { return {
init: [
{
init: {},
},
],
parse: [ parse: [
{ {
parse: {}, parse: {},
}, },
], ],
plan: [ graph: [
{ {
plan: { graph: {
externalAuthProviders: providers,
},
},
],
apply: [
{
apply: {
externalAuthProviders: providers, externalAuthProviders: providers,
resources: [ resources: [
{ {
@@ -939,6 +995,11 @@ export const echoResponsesWithExternalAuth = (
}, },
}, },
], ],
apply: [
{
apply: {},
},
],
}; };
}; };
+194 -99
View File
@@ -69,6 +69,13 @@ export enum PrebuiltWorkspaceBuildStage {
UNRECOGNIZED = -1, UNRECOGNIZED = -1,
} }
export enum GraphSource {
SOURCE_UNKNOWN = 0,
SOURCE_PLAN = 1,
SOURCE_STATE = 2,
UNRECOGNIZED = -1,
}
export enum TimingState { export enum TimingState {
STARTED = 0, STARTED = 0,
COMPLETED = 1, COMPLETED = 1,
@@ -410,10 +417,6 @@ export interface Metadata {
/** Config represents execution configuration shared by all subsequent requests in the Session */ /** Config represents execution configuration shared by all subsequent requests in the Session */
export interface Config { export interface Config {
/** template_source_archive is a tar of the template source files */
templateSourceArchive: Uint8Array;
/** state is the provisioner state (if any) */
state: Uint8Array;
provisionerLogLevel: string; provisionerLogLevel: string;
/** Template imports can omit template id */ /** Template imports can omit template id */
templateId?: templateId?:
@@ -444,6 +447,26 @@ export interface ParseComplete_WorkspaceTagsEntry {
value: string; value: string;
} }
export interface InitRequest {
/** template_source_archive is a tar of the template source files */
templateSourceArchive: Uint8Array;
/**
* If true, the provisioner can safely assume the caller does not need the
* module files downloaded by the `terraform init` command.
* Ideally this boolean would be flipped in its truthy value, however since
* this is costly, the zero value omitting the module files is preferred.
*/
omitModuleFiles: boolean;
}
export interface InitComplete {
error: string;
timings: Timing[];
modules: Module[];
moduleFiles: Uint8Array;
moduleFilesHash: Uint8Array;
}
/** PlanRequest asks the provisioner to plan what resources & parameters it will create */ /** PlanRequest asks the provisioner to plan what resources & parameters it will create */
export interface PlanRequest { export interface PlanRequest {
metadata: Metadata | undefined; metadata: Metadata | undefined;
@@ -451,29 +474,51 @@ export interface PlanRequest {
variableValues: VariableValue[]; variableValues: VariableValue[];
externalAuthProviders: ExternalAuthProvider[]; externalAuthProviders: ExternalAuthProvider[];
previousParameterValues: RichParameterValue[]; previousParameterValues: RichParameterValue[];
/** /** state is the provisioner state (if any) */
* If true, the provisioner can safely assume the caller does not need the state: Uint8Array;
* module files downloaded by the `terraform init` command.
* Ideally this boolean would be flipped in its truthy value, however for
* backwards compatibility reasons, the zero value should be the previous
* behavior of downloading the module files.
*/
omitModuleFiles: boolean;
} }
/** PlanComplete indicates a request to plan completed. */ /** PlanComplete indicates a request to plan completed. */
export interface PlanComplete { export interface PlanComplete {
error: string; error: string;
timings: Timing[];
plan: Uint8Array;
dailyCost: number;
resourceReplacements: ResourceReplacement[];
aiTaskCount: number;
}
/**
* ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response
* in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session.
*/
export interface ApplyRequest {
metadata:
| Metadata
| undefined;
/** state is the provisioner state (if any) */
state: Uint8Array;
}
/** ApplyComplete indicates a request to apply completed. */
export interface ApplyComplete {
state: Uint8Array;
error: string;
timings: Timing[];
}
export interface GraphRequest {
metadata: Metadata | undefined;
source: GraphSource;
}
export interface GraphComplete {
error: string;
timings: Timing[];
resources: Resource[]; resources: Resource[];
parameters: RichParameter[]; parameters: RichParameter[];
externalAuthProviders: ExternalAuthProviderResource[]; externalAuthProviders: ExternalAuthProviderResource[];
timings: Timing[];
modules: Module[];
presets: Preset[]; presets: Preset[];
plan: Uint8Array;
resourceReplacements: ResourceReplacement[];
moduleFiles: Uint8Array;
moduleFilesHash: Uint8Array;
/** /**
* Whether a template has any `coder_ai_task` resources defined, even if not planned for creation. * Whether a template has any `coder_ai_task` resources defined, even if not planned for creation.
* During a template import, a plan is run which may not yield in any `coder_ai_task` resources, but nonetheless we * During a template import, a plan is run which may not yield in any `coder_ai_task` resources, but nonetheless we
@@ -486,25 +531,6 @@ export interface PlanComplete {
hasExternalAgents: boolean; hasExternalAgents: boolean;
} }
/**
* ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response
* in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session.
*/
export interface ApplyRequest {
metadata: Metadata | undefined;
}
/** ApplyComplete indicates a request to apply completed. */
export interface ApplyComplete {
state: Uint8Array;
error: string;
resources: Resource[];
parameters: RichParameter[];
externalAuthProviders: ExternalAuthProviderResource[];
timings: Timing[];
aiTasks: AITask[];
}
export interface Timing { export interface Timing {
start: Date | undefined; start: Date | undefined;
end: Date | undefined; end: Date | undefined;
@@ -522,16 +548,20 @@ export interface CancelRequest {
export interface Request { export interface Request {
config?: Config | undefined; config?: Config | undefined;
parse?: ParseRequest | undefined; parse?: ParseRequest | undefined;
init?: InitRequest | undefined;
plan?: PlanRequest | undefined; plan?: PlanRequest | undefined;
apply?: ApplyRequest | undefined; apply?: ApplyRequest | undefined;
graph?: GraphRequest | undefined;
cancel?: CancelRequest | undefined; cancel?: CancelRequest | undefined;
} }
export interface Response { export interface Response {
log?: Log | undefined; log?: Log | undefined;
parse?: ParseComplete | undefined; parse?: ParseComplete | undefined;
init?: InitComplete | undefined;
plan?: PlanComplete | undefined; plan?: PlanComplete | undefined;
apply?: ApplyComplete | undefined; apply?: ApplyComplete | undefined;
graph?: GraphComplete | undefined;
dataUpload?: DataUpload | undefined; dataUpload?: DataUpload | undefined;
chunkPiece?: ChunkPiece | undefined; chunkPiece?: ChunkPiece | undefined;
} }
@@ -1318,23 +1348,17 @@ export const Metadata = {
export const Config = { export const Config = {
encode(message: Config, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: Config, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.templateSourceArchive.length !== 0) {
writer.uint32(10).bytes(message.templateSourceArchive);
}
if (message.state.length !== 0) {
writer.uint32(18).bytes(message.state);
}
if (message.provisionerLogLevel !== "") { if (message.provisionerLogLevel !== "") {
writer.uint32(26).string(message.provisionerLogLevel); writer.uint32(10).string(message.provisionerLogLevel);
} }
if (message.templateId !== undefined) { if (message.templateId !== undefined) {
writer.uint32(34).string(message.templateId); writer.uint32(18).string(message.templateId);
} }
if (message.templateVersionId !== undefined) { if (message.templateVersionId !== undefined) {
writer.uint32(42).string(message.templateVersionId); writer.uint32(26).string(message.templateVersionId);
} }
if (message.expReuseTerraformWorkspace !== undefined) { if (message.expReuseTerraformWorkspace !== undefined) {
writer.uint32(48).bool(message.expReuseTerraformWorkspace); writer.uint32(32).bool(message.expReuseTerraformWorkspace);
} }
return writer; return writer;
}, },
@@ -1376,6 +1400,39 @@ export const ParseComplete_WorkspaceTagsEntry = {
}, },
}; };
export const InitRequest = {
encode(message: InitRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.templateSourceArchive.length !== 0) {
writer.uint32(10).bytes(message.templateSourceArchive);
}
if (message.omitModuleFiles !== false) {
writer.uint32(24).bool(message.omitModuleFiles);
}
return writer;
},
};
export const InitComplete = {
encode(message: InitComplete, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.error !== "") {
writer.uint32(10).string(message.error);
}
for (const v of message.timings) {
Timing.encode(v!, writer.uint32(18).fork()).ldelim();
}
for (const v of message.modules) {
Module.encode(v!, writer.uint32(26).fork()).ldelim();
}
if (message.moduleFiles.length !== 0) {
writer.uint32(34).bytes(message.moduleFiles);
}
if (message.moduleFilesHash.length !== 0) {
writer.uint32(42).bytes(message.moduleFilesHash);
}
return writer;
},
};
export const PlanRequest = { export const PlanRequest = {
encode(message: PlanRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: PlanRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.metadata !== undefined) { if (message.metadata !== undefined) {
@@ -1393,8 +1450,8 @@ export const PlanRequest = {
for (const v of message.previousParameterValues) { for (const v of message.previousParameterValues) {
RichParameterValue.encode(v!, writer.uint32(42).fork()).ldelim(); RichParameterValue.encode(v!, writer.uint32(42).fork()).ldelim();
} }
if (message.omitModuleFiles !== false) { if (message.state.length !== 0) {
writer.uint32(48).bool(message.omitModuleFiles); writer.uint32(50).bytes(message.state);
} }
return writer; return writer;
}, },
@@ -1405,44 +1462,20 @@ export const PlanComplete = {
if (message.error !== "") { if (message.error !== "") {
writer.uint32(10).string(message.error); writer.uint32(10).string(message.error);
} }
for (const v of message.resources) {
Resource.encode(v!, writer.uint32(18).fork()).ldelim();
}
for (const v of message.parameters) {
RichParameter.encode(v!, writer.uint32(26).fork()).ldelim();
}
for (const v of message.externalAuthProviders) {
ExternalAuthProviderResource.encode(v!, writer.uint32(34).fork()).ldelim();
}
for (const v of message.timings) { for (const v of message.timings) {
Timing.encode(v!, writer.uint32(50).fork()).ldelim(); Timing.encode(v!, writer.uint32(18).fork()).ldelim();
}
for (const v of message.modules) {
Module.encode(v!, writer.uint32(58).fork()).ldelim();
}
for (const v of message.presets) {
Preset.encode(v!, writer.uint32(66).fork()).ldelim();
} }
if (message.plan.length !== 0) { if (message.plan.length !== 0) {
writer.uint32(74).bytes(message.plan); writer.uint32(26).bytes(message.plan);
}
if (message.dailyCost !== 0) {
writer.uint32(32).int32(message.dailyCost);
} }
for (const v of message.resourceReplacements) { for (const v of message.resourceReplacements) {
ResourceReplacement.encode(v!, writer.uint32(82).fork()).ldelim(); ResourceReplacement.encode(v!, writer.uint32(42).fork()).ldelim();
} }
if (message.moduleFiles.length !== 0) { if (message.aiTaskCount !== 0) {
writer.uint32(90).bytes(message.moduleFiles); writer.uint32(48).int32(message.aiTaskCount);
}
if (message.moduleFilesHash.length !== 0) {
writer.uint32(98).bytes(message.moduleFilesHash);
}
if (message.hasAiTasks !== false) {
writer.uint32(104).bool(message.hasAiTasks);
}
for (const v of message.aiTasks) {
AITask.encode(v!, writer.uint32(114).fork()).ldelim();
}
if (message.hasExternalAgents !== false) {
writer.uint32(120).bool(message.hasExternalAgents);
} }
return writer; return writer;
}, },
@@ -1453,6 +1486,9 @@ export const ApplyRequest = {
if (message.metadata !== undefined) { if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim(); Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim();
} }
if (message.state.length !== 0) {
writer.uint32(50).bytes(message.state);
}
return writer; return writer;
}, },
}; };
@@ -1465,6 +1501,33 @@ export const ApplyComplete = {
if (message.error !== "") { if (message.error !== "") {
writer.uint32(18).string(message.error); writer.uint32(18).string(message.error);
} }
for (const v of message.timings) {
Timing.encode(v!, writer.uint32(26).fork()).ldelim();
}
return writer;
},
};
export const GraphRequest = {
encode(message: GraphRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim();
}
if (message.source !== 0) {
writer.uint32(16).int32(message.source);
}
return writer;
},
};
export const GraphComplete = {
encode(message: GraphComplete, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.error !== "") {
writer.uint32(10).string(message.error);
}
for (const v of message.timings) {
Timing.encode(v!, writer.uint32(18).fork()).ldelim();
}
for (const v of message.resources) { for (const v of message.resources) {
Resource.encode(v!, writer.uint32(26).fork()).ldelim(); Resource.encode(v!, writer.uint32(26).fork()).ldelim();
} }
@@ -1474,11 +1537,17 @@ export const ApplyComplete = {
for (const v of message.externalAuthProviders) { for (const v of message.externalAuthProviders) {
ExternalAuthProviderResource.encode(v!, writer.uint32(42).fork()).ldelim(); ExternalAuthProviderResource.encode(v!, writer.uint32(42).fork()).ldelim();
} }
for (const v of message.timings) { for (const v of message.presets) {
Timing.encode(v!, writer.uint32(50).fork()).ldelim(); Preset.encode(v!, writer.uint32(50).fork()).ldelim();
}
if (message.hasAiTasks !== false) {
writer.uint32(56).bool(message.hasAiTasks);
} }
for (const v of message.aiTasks) { for (const v of message.aiTasks) {
AITask.encode(v!, writer.uint32(58).fork()).ldelim(); AITask.encode(v!, writer.uint32(66).fork()).ldelim();
}
if (message.hasExternalAgents !== false) {
writer.uint32(72).bool(message.hasExternalAgents);
} }
return writer; return writer;
}, },
@@ -1525,14 +1594,20 @@ export const Request = {
if (message.parse !== undefined) { if (message.parse !== undefined) {
ParseRequest.encode(message.parse, writer.uint32(18).fork()).ldelim(); ParseRequest.encode(message.parse, writer.uint32(18).fork()).ldelim();
} }
if (message.init !== undefined) {
InitRequest.encode(message.init, writer.uint32(26).fork()).ldelim();
}
if (message.plan !== undefined) { if (message.plan !== undefined) {
PlanRequest.encode(message.plan, writer.uint32(26).fork()).ldelim(); PlanRequest.encode(message.plan, writer.uint32(34).fork()).ldelim();
} }
if (message.apply !== undefined) { if (message.apply !== undefined) {
ApplyRequest.encode(message.apply, writer.uint32(34).fork()).ldelim(); ApplyRequest.encode(message.apply, writer.uint32(42).fork()).ldelim();
}
if (message.graph !== undefined) {
GraphRequest.encode(message.graph, writer.uint32(50).fork()).ldelim();
} }
if (message.cancel !== undefined) { if (message.cancel !== undefined) {
CancelRequest.encode(message.cancel, writer.uint32(42).fork()).ldelim(); CancelRequest.encode(message.cancel, writer.uint32(58).fork()).ldelim();
} }
return writer; return writer;
}, },
@@ -1546,17 +1621,23 @@ export const Response = {
if (message.parse !== undefined) { if (message.parse !== undefined) {
ParseComplete.encode(message.parse, writer.uint32(18).fork()).ldelim(); ParseComplete.encode(message.parse, writer.uint32(18).fork()).ldelim();
} }
if (message.init !== undefined) {
InitComplete.encode(message.init, writer.uint32(26).fork()).ldelim();
}
if (message.plan !== undefined) { if (message.plan !== undefined) {
PlanComplete.encode(message.plan, writer.uint32(26).fork()).ldelim(); PlanComplete.encode(message.plan, writer.uint32(34).fork()).ldelim();
} }
if (message.apply !== undefined) { if (message.apply !== undefined) {
ApplyComplete.encode(message.apply, writer.uint32(34).fork()).ldelim(); ApplyComplete.encode(message.apply, writer.uint32(42).fork()).ldelim();
}
if (message.graph !== undefined) {
GraphComplete.encode(message.graph, writer.uint32(50).fork()).ldelim();
} }
if (message.dataUpload !== undefined) { if (message.dataUpload !== undefined) {
DataUpload.encode(message.dataUpload, writer.uint32(42).fork()).ldelim(); DataUpload.encode(message.dataUpload, writer.uint32(58).fork()).ldelim();
} }
if (message.chunkPiece !== undefined) { if (message.chunkPiece !== undefined) {
ChunkPiece.encode(message.chunkPiece, writer.uint32(50).fork()).ldelim(); ChunkPiece.encode(message.chunkPiece, writer.uint32(66).fork()).ldelim();
} }
return writer; return writer;
}, },
@@ -1598,15 +1679,29 @@ export const ChunkPiece = {
export interface Provisioner { export interface Provisioner {
/** /**
* Session represents provisioning a single template import or workspace. The daemon always sends Config followed * Session represents provisioning a single template import or workspace. The daemon always sends Config followed
* by one of the requests (ParseRequest, PlanRequest, ApplyRequest). The provisioner should respond with a stream * by one of the requests (InitRequest, ParseRequest, PlanRequest, ApplyRequest, GraphRequest). The provisioner
* of zero or more Logs, followed by the corresponding complete message (ParseComplete, PlanComplete, * should respond with a stream of zero or more Logs, followed by the corresponding complete message
* ApplyComplete). The daemon may then send a new request. A request to apply MUST be preceded by a request plan, * (InitComplete, ParseComplete, PlanComplete, ApplyComplete, GraphComplete).
* and the provisioner should store the plan data on the Session after a successful plan, so that the daemon may * The daemon may then send a new request.
* request an apply. If the daemon closes the Session without an apply, the plan data may be safely discarded.
* *
* The daemon may send a CancelRequest, asynchronously to ask the provisioner to cancel the previous ParseRequest, * A request to Parse or Plan MUST be preceded by a request init. The provisioner should store the init data on
* PlanRequest, or ApplyRequest. The provisioner MUST reply with a complete message corresponding to the request * the session after a successful init. If the daemon closes the session, the init data may be safely discarded.
* that was canceled. If the provisioner has already completed the request, it may ignore the CancelRequest. *
* A request to apply MUST be preceded by a request plan, and the provisioner should store the plan data on the
* Session after a successful plan, so that the daemon may request an apply. If the daemon closes
* the Session without an apply, the plan data may be safely discarded.
*
* A request to graph MUST be preceded by a plan or an apply.
*
* The order of requests is then one of the following:
* 1. Init -> Parse
* 2. Init -> Plan -> Graph
* 3. Init -> Plan -> Apply -> Graph
*
* The daemon may send a CancelRequest, asynchronously to ask the provisioner to cancel the previous InitRequest,
* ParseRequest, PlanRequest, ApplyRequest, or GraphRequest. The provisioner MUST reply with a complete message
* corresponding to the request that was canceled. If the provisioner has already completed the request,
* it may ignore the CancelRequest.
*/ */
Session(request: Observable<Request>): Observable<Response>; Session(request: Observable<Request>): Observable<Response>;
} }
+2 -2
View File
@@ -32,9 +32,9 @@ test("app", async ({ context, page }) => {
} }
const appName = "test-app"; const appName = "test-app";
const template = await createTemplate(page, { const template = await createTemplate(page, {
apply: [ graph: [
{ {
apply: { graph: {
resources: [ resources: [
{ {
agents: [ agents: [
+2 -2
View File
@@ -25,9 +25,9 @@ test.skip(`ssh with agent ${agentVersion}`, async ({ page }) => {
const token = randomUUID(); const token = randomUUID();
const template = await createTemplate(page, { const template = await createTemplate(page, {
apply: [ graph: [
{ {
apply: { graph: {
resources: [ resources: [
{ {
agents: [ agents: [
+2 -2
View File
@@ -23,9 +23,9 @@ test.beforeEach(async ({ page }) => {
test(`ssh with client ${clientVersion}`, async ({ page }) => { test(`ssh with client ${clientVersion}`, async ({ page }) => {
const token = randomUUID(); const token = randomUUID();
const template = await createTemplate(page, { const template = await createTemplate(page, {
apply: [ graph: [
{ {
apply: { graph: {
resources: [ resources: [
{ {
agents: [ agents: [
+2 -2
View File
@@ -18,9 +18,9 @@ test.beforeEach(async ({ page }) => {
test("web terminal", async ({ context, page }) => { test("web terminal", async ({ context, page }) => {
const token = randomUUID(); const token = randomUUID();
const template = await createTemplate(page, { const template = await createTemplate(page, {
apply: [ graph: [
{ {
apply: { graph: {
resources: [ resources: [
{ {
agents: [ agents: [
@@ -32,7 +32,7 @@ test.beforeEach(async ({ page }) => {
test("create workspace", async ({ page }) => { test("create workspace", async ({ page }) => {
await login(page, users.templateAdmin); await login(page, users.templateAdmin);
const template = await createTemplate(page, { const template = await createTemplate(page, {
apply: [{ apply: { resources: [{ name: "example" }] } }], graph: [{ graph: { resources: [{ name: "example" }] } }],
}); });
await login(page, users.member); await login(page, users.member);
@@ -246,16 +246,6 @@ export const provisioningStages: Stage[] = [
"Compare state of desired vs actual resources and compute changes to be made.", "Compare state of desired vs actual resources and compute changes to be made.",
}, },
}, },
{
name: "graph",
label: "graph",
section: "provisioning",
tooltip: {
heading: "Terraform graph",
description:
"List all resources in plan, used to update coderd database.",
},
},
{ {
name: "apply", name: "apply",
label: "apply", label: "apply",
@@ -266,6 +256,16 @@ export const provisioningStages: Stage[] = [
"Execute Terraform plan to create/modify/delete resources into desired states.", "Execute Terraform plan to create/modify/delete resources into desired states.",
}, },
}, },
{
name: "graph",
label: "graph",
section: "provisioning",
tooltip: {
heading: "Terraform graph",
description:
"List all resources in plan, used to update coderd database.",
},
},
]; ];
export const agentStages = (section: string): Stage[] => { export const agentStages = (section: string): Stage[] => {