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 {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: parameters,
Presets: presets,
},
@@ -1573,11 +1575,13 @@ func TestCreateValidateRichParameters(t *testing.T) {
func TestCreateWithGitAuth(t *testing.T) {
t.Parallel()
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
ExternalAuthProviders: []*proto.ExternalAuthProviderResource{{Id: "github"}},
},
},
+3 -3
View File
@@ -306,10 +306,10 @@ func TestRestartWithParameters(t *testing.T) {
echoResponses := func() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: immutableParameterName,
+5 -5
View File
@@ -155,7 +155,7 @@ func TestSSH(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
@@ -305,7 +305,7 @@ func TestSSH(t *testing.T) {
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
}
version := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, echoResponses)
@@ -326,7 +326,7 @@ func TestSSH(t *testing.T) {
echoResponses2 := &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken2),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken2),
}
version = coderdtest.UpdateTemplateVersion(t, ownerClient, owner.OrganizationID, echoResponses2, template.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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
+12 -10
View File
@@ -36,10 +36,10 @@ const (
func mutableParamsResponse() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: mutableParameterName,
@@ -59,10 +59,10 @@ func mutableParamsResponse() *echo.Responses {
func immutableParamsResponse() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: immutableParameterName,
@@ -83,11 +83,13 @@ func TestStart(t *testing.T) {
echoResponses := func() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: ephemeralParameterName,
+4 -12
View File
@@ -285,19 +285,10 @@ func createAITaskTemplate(t *testing.T, client *codersdk.Client, orgID uuid.UUID
taskAppID := uuid.New()
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
HasAiTasks: true,
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Name: "example",
@@ -321,6 +312,7 @@ func createAITaskTemplate(t *testing.T, client *codersdk.Client, orgID uuid.UUID
},
},
},
HasAiTasks: true,
AiTasks: []*proto.AITask{
{
AppId: taskAppID.String(),
+3 -3
View File
@@ -282,10 +282,10 @@ func TestTemplatePresets(t *testing.T) {
func templateWithPresets(presets []*proto.Preset) *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Presets: presets,
},
},
+3 -24
View File
@@ -1306,31 +1306,10 @@ func createEchoResponsesWithTemplateVariables(templateVariables []*proto.Templat
func completeWithAgent() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
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{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Type: "compute",
+1
View File
@@ -71,6 +71,7 @@ func TestTemplateVersionsArchive(t *testing.T) {
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyFailed,
ProvisionPlan: echo.PlanFailed,
ProvisionInit: echo.InitComplete,
}, func(request *codersdk.CreateTemplateVersionRequest) {
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(agentToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(agentToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+19 -27
View File
@@ -61,19 +61,11 @@ func TestTasks(t *testing.T) {
taskAppID := uuid.New()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
HasAiTasks: true,
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{
{
Name: "example",
@@ -951,8 +943,8 @@ func TestTasksCreate(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true,
}}},
},
@@ -995,8 +987,8 @@ func TestTasksCreate(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true,
}}},
@@ -1097,8 +1089,8 @@ func TestTasksCreate(t *testing.T) {
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true,
}}},
},
@@ -1218,8 +1210,8 @@ func TestTasksCreate(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true,
}}},
},
@@ -1275,8 +1267,8 @@ func TestTasksCreate(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true,
}}},
},
@@ -1309,8 +1301,8 @@ func TestTasksCreate(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true,
}}},
},
@@ -1359,8 +1351,8 @@ func TestTasksCreate(t *testing.T) {
version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true,
}}},
},
@@ -1371,8 +1363,8 @@ func TestTasksCreate(t *testing.T) {
version2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true,
}}},
},
+3 -30
View File
@@ -476,37 +476,10 @@ func TestAuditLogsFilter(t *testing.T) {
func completeWithAgentAndApp() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
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{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
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
res = &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: "new",
@@ -1105,8 +1105,10 @@ func TestExecutorFailedWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyFailed,
ProvisionGraph: echo.GraphComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds())
@@ -1644,10 +1646,10 @@ func mustProvisionWorkspaceWithParameters(t *testing.T, client *codersdk.Client,
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: richParameters,
},
},
@@ -1774,17 +1776,10 @@ func TestExecutorTaskWorkspace(t *testing.T) {
taskAppID := uuid.New()
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{HasAiTasks: true},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Agents: []*proto.Agent{
@@ -1804,6 +1799,7 @@ func TestExecutorTaskWorkspace(t *testing.T) {
},
},
},
HasAiTasks: true,
AiTasks: []*proto.AITask{
{
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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.ProvisionInit = []*proto.Response{{
Type: &proto.Response_Init{
Init: &proto.InitComplete{
ModuleFiles: args.ModulesArchive,
},
},
}}
files.ProvisionPlan = []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Plan: args.Plan,
ModuleFiles: args.ModulesArchive,
Parameters: args.StaticParams,
Plan: args.Plan,
},
},
}}
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgentAndAPIKeyScope(authToken, tt.apiKeyScope),
ProvisionGraph: echo.ProvisionGraphWithAgentAndAPIKeyScope(authToken, tt.apiKeyScope),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
project := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgentAndAPIKeyScope(authToken, tt.apiKeyScope),
ProvisionGraph: echo.ProvisionGraphWithAgentAndAPIKeyScope(authToken, tt.apiKeyScope),
})
project := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
@@ -641,22 +641,16 @@ func TestTemplateInsights_Golden(t *testing.T) {
// Create the template version and template.
version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: parameters,
Resources: resources,
},
},
},
},
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: resources,
},
},
}},
})
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: resources,
},
},
@@ -536,9 +536,9 @@ func TestAgents(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Equal(t, -1, template.ActiveUserCount)
+21 -20
View File
@@ -760,7 +760,7 @@ func TestPatchCancelTemplateVersion(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Log{
Log: &proto.Log{},
},
@@ -793,7 +793,7 @@ func TestPatchCancelTemplateVersion(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Log{
Log: &proto.Log{},
},
@@ -857,9 +857,9 @@ func TestTemplateVersionsExternalAuth(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
ExternalAuthProviders: []*proto.ExternalAuthProviderResource{{Id: "github", Optional: true}},
},
},
@@ -912,9 +912,9 @@ func TestTemplateVersionResources(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -953,7 +953,7 @@ func TestTemplateVersionLogs(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
@@ -961,8 +961,8 @@ func TestTemplateVersionLogs(t *testing.T) {
},
},
}, {
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -1211,15 +1211,15 @@ func TestTemplateVersionDryRun(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Log{
Log: &proto.Log{},
},
},
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{resource},
},
},
@@ -2060,10 +2060,10 @@ func TestTemplateVersionParameters_Order(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: firstParameterName,
@@ -2133,6 +2133,7 @@ func TestTemplateArchiveVersions(t *testing.T) {
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanFailed,
ProvisionApply: echo.ApplyFailed,
ProvisionInit: echo.InitComplete,
}, func(req *codersdk.CreateTemplateVersionRequest) {
req.TemplateID = template.ID
})
@@ -2228,10 +2229,10 @@ func TestTemplateVersionHasExternalAgent(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitMedium)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Name: "example",
+4 -4
View File
@@ -495,7 +495,7 @@ func TestWorkspaceAgentConnectRPC(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
+42 -22
View File
@@ -556,13 +556,16 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
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{{
Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
ProvisionPlan: echo.PlanComplete,
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, 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})
owner := coderdtest.CreateFirstUser(t, client)
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{{
Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
ProvisionPlan: echo.PlanComplete,
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, 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})
user := coderdtest.CreateFirstUser(t, client)
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{{
Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
ProvisionPlan: echo.PlanComplete,
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, 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})
user := coderdtest.CreateFirstUser(t, client)
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{{
Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
ProvisionPlan: echo.PlanComplete,
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, 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)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "first_resource",
Type: "example",
@@ -1032,7 +1044,7 @@ func TestWorkspaceBuildLogs(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
@@ -1040,8 +1052,8 @@ func TestWorkspaceBuildLogs(t *testing.T) {
},
},
}, {
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -1208,9 +1220,9 @@ func TestWorkspaceDeleteSuspendedUser(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Error: "",
Resources: nil,
Parameters: nil,
@@ -1488,10 +1500,18 @@ func TestPostWorkspaceBuild(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
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)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
version = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
@@ -1642,9 +1662,9 @@ func TestPostWorkspaceBuild(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Presets: []*proto.Preset{
{
Name: "autodetected",
+9 -9
View File
@@ -26,9 +26,9 @@ func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
@@ -70,9 +70,9 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
@@ -151,9 +151,9 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
+67 -65
View File
@@ -213,9 +213,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -254,9 +254,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -299,9 +299,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -357,9 +357,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -735,9 +735,9 @@ func TestWorkspace(t *testing.T) {
authz := coderdtest.AssertRBAC(t, api, client)
// Create a plan response with the specified presets and parameters
planResponse := &proto.Response{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
graphResponse := &proto.Response{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Presets: tc.presets,
Parameters: tc.templateVersionParameters,
},
@@ -746,7 +746,7 @@ func TestWorkspace(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{planResponse},
ProvisionGraph: []*proto.Response{graphResponse},
ProvisionApply: echo.ApplyComplete,
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -2269,7 +2269,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -2420,7 +2420,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -2525,10 +2525,10 @@ func TestWorkspaceFilterManual(t *testing.T) {
makeParameters := func(extra ...*proto.RichParameter) *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: append([]*proto.RichParameter{
{Name: paramOneName, 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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -3467,8 +3467,10 @@ func TestWorkspaceWatcher(t *testing.T) {
// Add a new version that will fail.
badVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
@@ -3536,9 +3538,9 @@ func TestWorkspaceResource(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "beta",
Type: "example",
@@ -3604,9 +3606,9 @@ func TestWorkspaceResource(t *testing.T) {
}
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -3679,9 +3681,9 @@ func TestWorkspaceResource(t *testing.T) {
}
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -3723,9 +3725,9 @@ func TestWorkspaceResource(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -3803,10 +3805,10 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: firstParameterName,
@@ -3907,10 +3909,10 @@ func TestWorkspaceWithMultiSelectFailure(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: "param",
@@ -3986,10 +3988,10 @@ func TestWorkspaceWithOptionalRichParameters(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: firstParameterName,
@@ -4077,10 +4079,10 @@ func TestWorkspaceWithEphemeralRichParameters(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: firstParameterName,
@@ -4879,8 +4881,8 @@ func TestWorkspaceListTasks(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true,
}}},
},
@@ -4949,9 +4951,9 @@ func TestWorkspaceAppUpsertRestart(t *testing.T) {
// Create template version with workspace app
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "test-resource",
Type: "example",
@@ -5023,9 +5025,9 @@ func TestMultipleAITasksDisallowed(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
HasAiTasks: true,
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) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Presets: presets,
},
},
+2 -2
View File
@@ -1015,8 +1015,8 @@ func TestTools(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
Parameters: []*proto.RichParameter{{Name: "AI Prompt", Type: "string"}},
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 {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: parameters,
Presets: presets,
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{
{
Type: "compute",
+6 -48
View File
@@ -24,10 +24,10 @@ import (
func completeWithExternalAgent() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
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 {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
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{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Type: "compute",
+7 -7
View File
@@ -660,21 +660,21 @@ func TestManagedAgentLimit(t *testing.T) {
// build.
appID := uuid.NewString()
echoRes := &echo.Responses{
Parse: echo.ParseComplete,
Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Plan: []byte("{}"),
ModuleFiles: []byte{},
HasAiTasks: true,
Plan: []byte("{}"),
},
},
},
},
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionApply: echo.ApplyComplete,
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
project := coderdtest.CreateTemplate(t, client, org.ID, 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 {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
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()
data, err := echo.Tar(&echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*sdkproto.Response{{
Type: &sdkproto.Response_Plan{
Plan: &sdkproto.PlanComplete{
Resources: []*sdkproto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Id: uuid.NewString(),
Name: "example",
}},
}},
},
},
}},
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken, func(g *sdkproto.GraphComplete) {
g.Resources = []*sdkproto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Id: uuid.NewString(),
Name: "example",
}},
}}
}),
})
require.NoError(t, err)
//nolint:gocritic // Not testing file upload in this test.
@@ -446,9 +441,9 @@ func TestProvisionerDaemonServe(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*sdkproto.Response{{
Type: &sdkproto.Response_Apply{
Apply: &sdkproto.ApplyComplete{
ProvisionGraph: []*sdkproto.Response{{
Type: &sdkproto.Response_Graph{
Graph: &sdkproto.GraphComplete{
Resources: []*sdkproto.Resource{{
Name: "example",
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
@@ -155,8 +155,8 @@ func TestTemplates(t *testing.T) {
},
},
}, {
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -2161,10 +2161,10 @@ func TestInvalidateTemplatePrebuilds(t *testing.T) {
})
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{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Presets: presets,
Parameters: templateVersionParameters,
},
@@ -2174,8 +2174,8 @@ func TestInvalidateTemplatePrebuilds(t *testing.T) {
version1 := coderdtest.CreateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{buildPlanResponse(presetWithParameters1, presetWithParameters2)},
ProvisionApply: echo.ApplyComplete,
ProvisionGraph: []*proto.Response{buildGraphResponse(presetWithParameters1, presetWithParameters2)},
})
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, 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...
version2 := coderdtest.UpdateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{buildPlanResponse(presetWithParameters2, presetWithParameters3)},
ProvisionGraph: []*proto.Response{buildGraphResponse(presetWithParameters2, presetWithParameters3)},
ProvisionApply: echo.ApplyComplete,
}, template.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version2.ID)
@@ -2239,10 +2239,10 @@ func TestInvalidateTemplatePrebuilds_RegularUser(t *testing.T) {
// Given
version1 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Presets: []*proto.Preset{presetWithParameters1},
Parameters: templateVersionParameters,
},
+13 -25
View File
@@ -134,10 +134,10 @@ func TestReinitializeAgent(t *testing.T) {
agentToken := uuid.UUID{3}
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Presets: []*proto.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{
{
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)
@@ -273,9 +261,9 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
+2 -2
View File
@@ -633,7 +633,7 @@ func TestIssueSignedAppToken(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -756,7 +756,7 @@ func TestReconnectingPTYSignedToken(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+73 -31
View File
@@ -121,9 +121,16 @@ func TestWorkspaceQuota(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
DailyCost: 1,
},
},
}},
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -216,14 +223,17 @@ func TestWorkspaceQuota(t *testing.T) {
verifyQuota(ctx, t, client, user.OrganizationID.String(), 0, 4)
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{
proto.WorkspaceTransition_START: planWithCost(2),
proto.WorkspaceTransition_STOP: planWithCost(1),
},
ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_START: applyWithCost(2),
proto.WorkspaceTransition_STOP: applyWithCost(1),
ProvisionGraphMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_START: graphWithCost(2),
proto.WorkspaceTransition_STOP: graphWithCost(1),
},
})
@@ -422,10 +432,19 @@ func TestWorkspaceQuota(t *testing.T) {
// Create a template with a workspace that costs 1 credit
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: []*proto.Response{{
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{{
Name: "example",
Type: "aws_instance",
@@ -458,10 +477,19 @@ func TestWorkspaceQuota(t *testing.T) {
// Test with a template that has zero cost - should pass
versionZeroCost := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: []*proto.Response{{
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{{
Name: "example",
Type: "aws_instance",
@@ -542,10 +570,19 @@ func TestWorkspaceQuota(t *testing.T) {
// Create templates for both organizations
authToken := uuid.NewString()
version1 := coderdtest.CreateTemplateVersion(t, owner, first.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: []*proto.Response{{
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{{
Name: "example",
Type: "aws_instance",
@@ -566,10 +603,19 @@ func TestWorkspaceQuota(t *testing.T) {
template1 := coderdtest.CreateTemplate(t, owner, first.OrganizationID, version1.ID)
version2 := coderdtest.CreateTemplateVersion(t, owner, second.ID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Parse: echo.ParseComplete,
ProvisionInit: echo.InitComplete,
ProvisionPlan: []*proto.Response{{
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{{
Name: "example",
Type: "aws_instance",
@@ -1156,20 +1202,16 @@ func planWithCost(cost int32) []*proto.Response {
return []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
DailyCost: cost,
}},
DailyCost: cost,
},
},
}}
}
func applyWithCost(cost int32) []*proto.Response {
func graphWithCost(cost int32) []*proto.Response {
return []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
+27 -20
View File
@@ -629,6 +629,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyFailed,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds())
@@ -680,6 +682,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyFailed,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
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,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -1397,6 +1403,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyFailed,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
}, func(ctvr *codersdk.CreateTemplateVersionRequest) {
ctvr.TemplateID = template.ID
})
@@ -2579,21 +2587,11 @@ func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Resp
return r
}
applyResponse := func(withAgent bool) *proto.Response {
graphResponse := func(withAgent bool) *proto.Response {
return &proto.Response{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{resource(withAgent)},
},
},
}
}
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Presets: []*proto.Preset{{
Name: "preset-test",
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{
proto.WorkspaceTransition_START: {applyResponse(true)},
proto.WorkspaceTransition_STOP: {applyResponse(false)},
ProvisionGraphMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_START: {graphResponse(true)},
proto.WorkspaceTransition_STOP: {graphResponse(false)},
},
}
}
@@ -2612,10 +2619,10 @@ func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Resp
func templateWithFailedResponseAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Presets: []*proto.Preset{
{
Name: "preset-test",
@@ -74,10 +74,14 @@ func TestRemoteConnector_Mainline(t *testing.T) {
c := resp.Client
s, err := c.Session(ctx)
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,
}}})
require.NoError(t, err)
_, err = s.Recv()
require.NoError(t, err)
err = s.Send(&sdkproto.Request{Type: &sdkproto.Request_Parse{Parse: &sdkproto.ParseRequest{}}})
require.NoError(t, err)
r, err := s.Recv()
+2 -2
View File
@@ -173,7 +173,7 @@ func TestDERP(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -411,7 +411,7 @@ func TestDERPEndToEnd(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
+246 -47
View File
@@ -12,6 +12,7 @@ import (
"text/template"
"github.com/google/uuid"
"github.com/spf13/afero"
"golang.org/x/xerrors"
protobuf "google.golang.org/protobuf/proto"
@@ -21,12 +22,12 @@ import (
"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.
func ProvisionApplyWithAgentAndAPIKeyScope(authToken string, apiKeyScope string) []*proto.Response {
func ProvisionGraphWithAgentAndAPIKeyScope(authToken string, apiKeyScope string) []*proto.Response {
return []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example_with_scope",
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.
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{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: "example",
Auth: &proto.Agent_Token{
Token: authToken,
},
}},
}},
},
Type: &proto.Response_Graph{
Graph: gc,
},
}}
}
@@ -73,12 +79,19 @@ var (
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 = []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Plan: []byte("{}"),
ModuleFiles: []byte{},
Plan: []byte("{}"),
},
},
}}
@@ -88,7 +101,19 @@ var (
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 = []*proto.Response{{
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.
@@ -174,6 +206,59 @@ func (*echo) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan
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.
func (*echo) Plan(sess *provisionersdk.Session, req *proto.PlanRequest, canceledOrComplete <-chan struct{}) *proto.PlanComplete {
responses, err := readResponses(
@@ -228,19 +313,73 @@ func (*echo) Shutdown(_ context.Context, _ *proto.Empty) (*proto.Empty, error) {
type Responses struct {
Parse []*proto.Response
// ProvisionApply and ProvisionPlan are used to mock ALL responses of
// Apply and Plan, regardless of transition.
ProvisionApply []*proto.Response
// Used to mock ALL responses regardless of transition.
ProvisionInit []*proto.Response
ProvisionPlan []*proto.Response
ProvisionApply []*proto.Response
ProvisionGraph []*proto.Response
// ProvisionApplyMap and ProvisionPlanMap are used to mock specific
// transition responses. They are prioritized over the generic responses.
ProvisionApplyMap map[proto.WorkspaceTransition][]*proto.Response
// Used to mock specific transition responses. They are prioritized over the generic responses.
ProvisionPlanMap map[proto.WorkspaceTransition][]*proto.Response
ProvisionApplyMap map[proto.WorkspaceTransition][]*proto.Response
ProvisionGraphMap map[proto.WorkspaceTransition][]*proto.Response
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.
func Tar(responses *Responses) ([]byte, error) {
logger := slog.Make()
@@ -255,31 +394,56 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
if responses == nil {
responses = &Responses{
Parse: ParseComplete,
ProvisionApply: ApplyComplete,
ProvisionInit: InitComplete,
ProvisionPlan: PlanComplete,
ProvisionApply: ApplyComplete,
ProvisionGraph: GraphComplete,
ProvisionApplyMap: nil,
ProvisionPlanMap: 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 {
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 {
responses.ProvisionPlan = append(responses.ProvisionPlan, resp)
continue
}
responses.ProvisionPlan = append(responses.ProvisionPlan, &proto.Response{
Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
Error: resp.GetApply().GetError(),
Resources: resp.GetApply().GetResources(),
Parameters: resp.GetApply().GetParameters(),
ExternalAuthProviders: resp.GetApply().GetExternalAuthProviders(),
Plan: []byte("{}"),
ModuleFiles: []byte{},
}},
})
if g := resp.GetGraph(); g != nil {
dailycost := int32(0)
for _, r := range g.GetResources() {
dailycost += r.DailyCost
}
responses.ProvisionPlan = []*proto.Response{{
Type: &proto.Response_Plan{
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 {
plan := resp.GetPlan()
@@ -315,6 +479,13 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
if err != nil {
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))
return nil
@@ -325,6 +496,12 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
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 {
err := writeProto(fmt.Sprintf("%d.apply.protobuf", index), response)
if err != nil {
@@ -337,6 +514,12 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
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 i, rs := range m {
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{}
for name, content := range responses.ExtraFiles {
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
// parsed these, even in the echo provisioner.
var mainTF bytes.Buffer
for _, respPlan := range responses.ProvisionPlan {
plan := respPlan.GetPlan()
for _, respPlan := range responses.ProvisionGraph {
plan := respPlan.GetGraph()
if plan == nil {
continue
}
@@ -440,6 +631,11 @@ terraform {
if err != nil {
return nil, err
}
if err := responses.Valid(); err != nil {
return nil, xerrors.Errorf("responses invalid: %w", err)
}
return buffer.Bytes(), nil
}
@@ -508,13 +704,14 @@ data "coder_parameter" "{{ .Name }}" {
func WithResources(resources []*proto.Resource) *Responses {
return &Responses{
Parse: ParseComplete,
ProvisionApply: []*proto.Response{{Type: &proto.Response_Apply{Apply: &proto.ApplyComplete{
Parse: ParseComplete,
ProvisionInit: InitComplete,
ProvisionApply: []*proto.Response{{Type: &proto.Response_Apply{Apply: &proto.ApplyComplete{}}}},
ProvisionGraph: []*proto.Response{{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
Resources: resources,
}}}},
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 {
return &Responses{
Parse: ParseComplete,
ProvisionInit: InitComplete,
ProvisionApply: ApplyComplete,
ProvisionPlan: PlanComplete,
ProvisionGraph: GraphComplete,
ExtraFiles: extraFiles,
}
}
+93 -72
View File
@@ -56,7 +56,8 @@ func TestEcho(t *testing.T) {
},
}
data, err := echo.Tar(&echo.Responses{
Parse: responses,
Parse: responses,
ProvisionInit: echo.InitComplete,
})
require.NoError(t, err)
client, err := api.Session(ctx)
@@ -65,13 +66,19 @@ func TestEcho(t *testing.T) {
err := client.Close()
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,
}}})
require.NoError(t, err)
_, err = client.Recv()
require.NoError(t, err)
err = client.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}})
require.NoError(t, err)
log, err := client.Recv()
require.NoError(t, err)
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)
defer cancel()
planResponses := []*proto.Response{
graphResponses := []*proto.Response{
{
Type: &proto.Response_Log{
Log: &proto.Log{
@@ -95,27 +102,8 @@ func TestEcho(t *testing.T) {
},
},
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
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{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "resource",
}},
@@ -123,9 +111,12 @@ func TestEcho(t *testing.T) {
},
},
}
data, err := echo.Tar(&echo.Responses{
ProvisionPlan: planResponses,
ProvisionApply: applyResponses,
ProvisionGraph: graphResponses,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionInit: echo.InitComplete,
})
require.NoError(t, err)
client, err := api.Session(ctx)
@@ -134,30 +125,38 @@ func TestEcho(t *testing.T) {
err := client.Close()
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,
}}})
require.NoError(t, err)
_, err = client.Recv()
require.NoError(t, err)
err = client.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}})
require.NoError(t, err)
log, err := client.Recv()
_, err = client.Recv()
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{}}})
require.NoError(t, err)
log, err = client.Recv()
_, err = client.Recv()
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.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) {
@@ -165,13 +164,11 @@ func TestEcho(t *testing.T) {
// Stop responses should be returned when the workspace is being stopped.
data, err := echo.Tar(&echo.Responses{
ProvisionApply: applyCompleteResource("DEFAULT"),
ProvisionPlan: planCompleteResource("DEFAULT"),
ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_STOP: planCompleteResource("STOP"),
},
ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_STOP: applyCompleteResource("STOP"),
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionGraph: graphCompleteResource("DEFAULT"),
ProvisionGraphMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_STOP: graphCompleteResource("STOP"),
},
})
require.NoError(t, err)
@@ -182,10 +179,15 @@ func TestEcho(t *testing.T) {
err := client.Close()
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,
}}})
require.NoError(t, err)
_, err = client.Recv()
require.NoError(t, err)
// Do stop.
err = client.Send(&proto.Request{
@@ -199,17 +201,32 @@ func TestEcho(t *testing.T) {
})
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()
require.NoError(t, err)
require.Equal(t,
"STOP",
complete.GetPlan().Resources[0].Name,
complete.GetGraph().Resources[0].Name,
)
// Do start.
err = client.Send(&proto.Request{
Type: &proto.Request_Plan{
Plan: &proto.PlanRequest{
Type: &proto.Request_Graph{
Graph: &proto.GraphRequest{
Metadata: &proto.Metadata{
WorkspaceTransition: proto.WorkspaceTransition_START,
},
@@ -222,7 +239,7 @@ func TestEcho(t *testing.T) {
require.NoError(t, err)
require.Equal(t,
"DEFAULT",
complete.GetPlan().Resources[0].Name,
complete.GetGraph().Resources[0].Name,
)
})
@@ -246,8 +263,8 @@ func TestEcho(t *testing.T) {
},
},
}, {
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "resource",
}},
@@ -256,7 +273,9 @@ func TestEcho(t *testing.T) {
}}
data, err := echo.Tar(&echo.Responses{
ProvisionPlan: echo.PlanComplete,
ProvisionApply: responses,
ProvisionApply: echo.ApplyComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: responses,
})
require.NoError(t, err)
client, err := api.Session(ctx)
@@ -266,11 +285,17 @@ func TestEcho(t *testing.T) {
require.NoError(t, err)
}()
err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{
TemplateSourceArchive: data,
ProvisionerLogLevel: "debug",
ProvisionerLogLevel: "debug",
}}})
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
err = client.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}})
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{}}})
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()
require.NoError(t, err)
// Skip responses[0] as it's trace level
require.Equal(t, responses[1].GetLog().Output, log.GetLog().Output)
complete, err = client.Recv()
require.NoError(t, err)
require.Equal(t, responses[2].GetApply().Resources[0].Name,
complete.GetApply().Resources[0].Name)
require.Equal(t, responses[2].GetGraph().Resources[0].Name,
complete.GetGraph().Resources[0].Name)
})
}
func planCompleteResource(name string) []*proto.Response {
func graphCompleteResource(name string) []*proto.Response {
return []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Resources: []*proto.Resource{{
Name: name,
}},
},
},
}}
}
func applyCompleteResource(name string) []*proto.Response {
return []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: name,
}},
+16 -107
View File
@@ -23,7 +23,6 @@ import (
"cdr.dev/slog"
"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/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 {
content, err := os.ReadFile(path)
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 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)
}
// Capture the duration of the call to `terraform graph`.
graphTimings := newTimingAggregator(database.ProvisionerJobTimingStageGraph)
graphTimings.ingest(createGraphTimingsEvent(timingGraphStart))
state, plan, err := e.planResources(ctx, killCtx, planfilePath)
plan, err := e.parsePlan(ctx, killCtx, planfilePath)
if err != nil {
graphTimings.ingest(createGraphTimingsEvent(timingGraphErrored))
return nil, xerrors.Errorf("plan resources: %w", err)
return nil, xerrors.Errorf("show terraform plan file: %w", err)
}
planJSON, err := json.Marshal(plan)
if err != nil {
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
// the point of prebuilding if the expensive resource is replaced once claimed!
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{
Parameters: state.Parameters,
Resources: state.Resources,
ExternalAuthProviders: state.ExternalAuthProviders,
Timings: graphTimings.aggregate(),
Presets: state.Presets,
Plan: planJSON,
ResourceReplacements: resReps,
ModuleFiles: moduleFiles,
HasAiTasks: state.HasAITasks,
AiTasks: state.AITasks,
HasExternalAgents: state.HasExternalAgents,
Plan: planJSON,
DailyCost: state.DailyCost,
ResourceReplacements: resReps,
AiTaskCount: state.AITaskCount,
}
return msg, nil
@@ -418,42 +397,6 @@ func onlyDataResources(sm tfjson.StateModule) tfjson.StateModule {
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.
func (e *executor) parsePlan(ctx, killCtx context.Context, planfilePath string) (*tfjson.Plan, error) {
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?
// "-plan=" + e.files.PlanFilePath(),
}
if ver.GreaterThanOrEqual(version170) {
args = append(args, "-type=plan")
}
var out strings.Builder
cmd := exec.CommandContext(killCtx, e.binaryPath, args...) // #nosec
cmd.Stdout = &out
@@ -602,11 +547,6 @@ func (e *executor) apply(
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()
stateContent, err := os.ReadFile(statefilePath)
if err != nil {
@@ -614,41 +554,10 @@ func (e *executor) apply(
}
return &proto.ApplyComplete{
Parameters: state.Parameters,
Resources: state.Resources,
ExternalAuthProviders: state.ExternalAuthProviders,
State: stateContent,
AiTasks: state.AITasks,
State: stateContent,
}, 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.
func (e *executor) state(ctx, killCtx context.Context) (*tfjson.State, error) {
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
+45 -27
View File
@@ -4,6 +4,8 @@ package terraform_test
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
@@ -15,14 +17,19 @@ import (
func TestParse(t *testing.T) {
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 {
Name string
Files map[string]string
Response *proto.ParseComplete
// If ErrorContains is not empty, then the ParseComplete should have an Error containing the given string
ErrorContains string
Name string
Files map[string]string
Response *proto.ParseComplete
ParseErrorContains string
}{
{
Name: "single-variable",
@@ -63,6 +70,7 @@ func TestParse(t *testing.T) {
"main.tf": `variable "A" {
validation {
condition = var.A == "value"
error_message = "A must be 'value'"
}
}`,
},
@@ -80,7 +88,7 @@ func TestParse(t *testing.T) {
Files: map[string]string{
"main.tf": "a;sd;ajsd;lajsd;lasjdf;a",
},
ErrorContains: `The ";" character is not valid.`,
ParseErrorContains: `The ";" character is not valid.`,
},
{
Name: "multiple-variables",
@@ -205,6 +213,8 @@ func TestParse(t *testing.T) {
{
Name: "workspace-tags",
Files: map[string]string{
`main.tf`: `
`,
"parameters.tf": `data "coder_parameter" "os_selector" {
name = "os_selector"
display_name = "Operating System"
@@ -266,7 +276,6 @@ func TestParse(t *testing.T) {
Name: "workspace-tags-in-a-single-file",
Files: map[string]string{
"main.tf": `
data "coder_parameter" "os_selector" {
name = "os_selector"
display_name = "Operating System"
@@ -330,7 +339,6 @@ func TestParse(t *testing.T) {
Name: "workspace-tags-duplicate-tag",
Files: map[string]string{
"main.tf": `
data "coder_workspace_tags" "custom_workspace_tags" {
tags = {
"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",
Files: map[string]string{
"main.tf": `
data "coder_workspace_tags" "custom_workspace_tags" {
tags {
cluster = "developers"
debug = "yes"
cache = "no-cache"
data "coder_workspace_tags" "custom_workspace_tags" {
tags {
cluster = "developers"
debug = "yes"
cache = "no-cache"
}
}
}
`,
},
ErrorContains: `"tags" attribute is required by coder_workspace_tags`,
ParseErrorContains: `"tags" attribute is required by coder_workspace_tags`,
},
{
Name: "empty-main",
@@ -379,27 +386,38 @@ func TestParse(t *testing.T) {
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
session := configure(ctx, t, api, &proto.Config{
TemplateSourceArchive: testutil.CreateTar(t, testCase.Files),
})
session := configure(ctx, t, api, &proto.Config{})
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)
for {
msg, err := session.Recv()
require.NoError(t, err)
if testCase.ErrorContains != "" {
require.Contains(t, msg.GetParse().GetError(), testCase.ErrorContains)
break
}
// Ignore logs in this test
if msg.GetLog() != nil {
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!
want, err := json.Marshal(testCase.Response)
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"
"time"
tfjson "github.com/hashicorp/terraform-json"
"github.com/spf13/afero"
"golang.org/x/xerrors"
@@ -67,51 +68,34 @@ func (s *server) setupContexts(parent context.Context, canceledOrComplete <-chan
return ctx, cancel, killCtx, kill
}
func (s *server) Plan(
sess *provisionersdk.Session, request *proto.PlanRequest, canceledOrComplete <-chan struct{},
) *proto.PlanComplete {
func (s *server) Init(
sess *provisionersdk.Session, request *proto.InitRequest, canceledOrComplete <-chan struct{},
) *proto.InitComplete {
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)
e := s.executor(sess.Files, database.ProvisionerJobTimingStageInit)
if err := e.checkMinVersion(ctx); err != nil {
return provisionersdk.PlanErrorf("%s", err.Error())
return provisionersdk.InitErrorf("%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(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)
// TODO: These logs should probably be streamed back to the provisioner runner.
err := sess.Files.ExtractArchive(ctx, s.logger, afero.NewOsFs(), request.GetTemplateSourceArchive())
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")
// The JSON output of `terraform init` doesn't include discrete fields for capturing timings of each plugin,
// so we capture the whole init process.
initTimings := newTimingAggregator(database.ProvisionerJobTimingStageInit)
endStage := initTimings.startStage(database.ProvisionerJobTimingStageInit)
err = CleanStaleTerraformPlugins(sess.Context(), s.cachePath, afero.NewOsFs(), time.Now(), s.logger)
if err != nil {
return provisionersdk.InitErrorf("unable to clean stale Terraform plugins: %s", err)
}
s.logger.Debug(ctx, "running terraform initialization")
endStage := e.timings.startStage(database.ProvisionerJobTimingStageInit)
err = e.init(ctx, killCtx, sess)
endStage(err)
if err != nil {
@@ -137,7 +121,7 @@ func (s *server) Plan(
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)
@@ -147,8 +131,61 @@ func (s *server) Plan(
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")
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)
if err != nil {
return provisionersdk.PlanErrorf("setup env: %s", err)
@@ -160,20 +197,78 @@ func (s *server) Plan(
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)
endPlanStage(err)
endStage(err)
if err != nil {
return provisionersdk.PlanErrorf("%s", err.Error())
}
// Prepend init timings since they occur prior to plan timings.
// Order is irrelevant; this is merely indicative.
resp.Timings = append(resp.Timings, append(initTimings.aggregate(), e.timings.aggregate()...)...)
resp.Modules = modules
resp.Timings = e.timings.aggregate()
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(
sess *provisionersdk.Session, request *proto.ApplyRequest, canceledOrComplete <-chan struct{},
) *proto.ApplyComplete {
@@ -194,7 +289,7 @@ func (s *server) Apply(
// 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 {
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")
return &proto.ApplyComplete{}
}
@@ -217,8 +312,9 @@ func (s *server) Apply(
// In this case, we return Complete with an explicit error message.
stateData, _ := os.ReadFile(statefilePath)
return &proto.ApplyComplete{
State: stateData,
Error: errorMessage,
State: stateData,
Error: errorMessage,
Timings: e.timings.aggregate(),
}
}
resp.Timings = e.timings.aggregate()
+168 -86
View File
@@ -13,6 +13,7 @@ import (
"path/filepath"
"sort"
"strings"
"sync"
"testing"
"time"
@@ -81,6 +82,27 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont
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 {
t.Helper()
sess, err := client.Session(ctx)
@@ -107,6 +129,12 @@ func readProvisionLog(t *testing.T, response proto.DRPCProvisioner_SessionClient
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 {
return sess.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{
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
// one process tries to do this simultaneously, it can cause "text file busy"
// nolint: paralleltest
@@ -161,30 +195,46 @@ func TestProvision_Cancel(t *testing.T) {
require.NoError(t, err)
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{
binaryPath: binPath,
logger: &logger,
})
sess := configure(ctx, t, api, &proto.Config{
TemplateSourceArchive: testutil.CreateTar(t, nil),
})
sess := configure(ctx, t, api, &proto.Config{})
err = sendPlan(sess, proto.WorkspaceTransition_START)
err = sendInit(sess, testutil.CreateTar(t, nil))
require.NoError(t, err)
var planOnce sync.Once
for _, line := range tt.startSequence {
LoopStart:
msg, err := sess.Recv()
require.NoError(t, err)
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()
if log == nil {
goto LoopStart
}
require.Equal(t, line, log.Output)
}
t.Log("Sending the cancel request")
err = sess.Send(&proto.Request{
Type: &proto.Request_Cancel{
Cancel: &proto.CancelRequest{},
@@ -199,10 +249,14 @@ func TestProvision_Cancel(t *testing.T) {
if log := msg.GetLog(); log != nil {
gotLog = append(gotLog, log.Output)
}
if c := msg.GetPlan(); c != nil {
} else if c := msg.GetPlan(); c != nil {
require.Contains(t, c.Error, "exit status 1")
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)
@@ -231,15 +285,14 @@ func TestProvision_CancelTimeout(t *testing.T) {
exitTimeout: time.Second,
})
sess := configure(ctx, t, api, &proto.Config{
TemplateSourceArchive: testutil.CreateTar(t, nil),
})
sess := configure(ctx, t, api, &proto.Config{})
sendInitAndGetResp(t, sess, testutil.CreateTar(t, nil))
// provisioner requires plan before apply, so test cancel with plan.
err = sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err)
for _, line := range []string{"init", "plan_start"} {
for _, line := range []string{"plan_start"} {
LoopStart:
msg, err := sess.Recv()
require.NoError(t, err)
@@ -316,11 +369,9 @@ func TestProvision_TextFileBusy(t *testing.T) {
logger: &logger,
})
sess := configure(ctx, t, api, &proto.Config{
TemplateSourceArchive: testutil.CreateTar(t, nil),
})
sess := configure(ctx, t, api, &proto.Config{})
err = sendPlan(sess, proto.WorkspaceTransition_START)
err = sendInit(sess, testutil.CreateTar(t, nil))
require.NoError(t, err)
found := false
@@ -328,7 +379,7 @@ func TestProvision_TextFileBusy(t *testing.T) {
msg, err := sess.Recv()
require.NoError(t, err)
if c := msg.GetPlan(); c != nil {
if c := msg.GetInit(); c != nil {
require.Contains(t, c.Error, "exit status 1")
found = true
break
@@ -347,11 +398,14 @@ func TestProvision(t *testing.T) {
Metadata *proto.Metadata
Request *proto.PlanRequest
// 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
ErrorContains string
// If ExpectLogContains is not empty, then the logs should contain it.
ExpectLogContains string
PlanErrorContains string
// If PlanExpectLogContains is not empty, then the logs should contain it.
PlanExpectLogContains string
// If Apply is true, then send an Apply request and check we get the same Resources as in Response.
Apply bool
// 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" {
}`,
},
ErrorContains: "terraform plan:",
ExpectLogContains: "No value for required variable",
PlanErrorContains: "terraform plan:",
PlanExpectLogContains: "No value for required variable",
},
{
Name: "missing-variable-dry-run",
@@ -374,15 +428,15 @@ func TestProvision(t *testing.T) {
"main.tf": `variable "A" {
}`,
},
ErrorContains: "terraform plan:",
ExpectLogContains: "No value for required variable",
PlanErrorContains: "terraform plan:",
PlanExpectLogContains: "No value for required variable",
},
{
Name: "single-resource-dry-run",
Files: map[string]string{
"main.tf": `resource "null_resource" "A" {}`,
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "A",
Type: "null_resource",
@@ -394,7 +448,7 @@ func TestProvision(t *testing.T) {
Files: map[string]string{
"main.tf": `resource "null_resource" "A" {}`,
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "A",
Type: "null_resource",
@@ -415,7 +469,7 @@ func TestProvision(t *testing.T) {
}
}`,
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "A",
Type: "null_resource",
@@ -428,18 +482,18 @@ func TestProvision(t *testing.T) {
Files: map[string]string{
"main.tf": `a`,
},
ErrorContains: "initialize terraform",
ExpectLogContains: "Argument or block definition required",
SkipCacheProviders: true,
InitErrorContains: "initialize terraform",
InitExpectLogContains: "Argument or block definition required",
SkipCacheProviders: true,
},
{
Name: "bad-syntax-2",
Files: map[string]string{
"main.tf": `;asdf;`,
},
ErrorContains: "initialize terraform",
ExpectLogContains: `The ";" character is not valid.`,
SkipCacheProviders: true,
InitErrorContains: "initialize terraform",
InitExpectLogContains: `The ";" character is not valid.`,
SkipCacheProviders: true,
},
{
Name: "destroy-no-state",
@@ -449,7 +503,7 @@ func TestProvision(t *testing.T) {
Metadata: &proto.Metadata{
WorkspaceTransition: proto.WorkspaceTransition_DESTROY,
},
ExpectLogContains: "nothing to do",
PlanExpectLogContains: "nothing to do",
},
{
Name: "rich-parameter-with-value",
@@ -493,7 +547,7 @@ func TestProvision(t *testing.T) {
},
},
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: "Example",
@@ -571,7 +625,7 @@ func TestProvision(t *testing.T) {
},
},
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Parameters: []*proto.RichParameter{
{
Name: "Example",
@@ -623,7 +677,7 @@ func TestProvision(t *testing.T) {
AccessToken: "some-value",
}},
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "null_resource",
@@ -666,7 +720,7 @@ func TestProvision(t *testing.T) {
WorkspaceOwnerSshPrivateKey: "fake private key",
},
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "null_resource",
@@ -709,7 +763,7 @@ func TestProvision(t *testing.T) {
WorkspaceOwnerLoginType: "github",
},
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "null_resource",
@@ -738,16 +792,7 @@ func TestProvision(t *testing.T) {
`,
},
Request: &proto.PlanRequest{},
Response: &proto.PlanComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "null_resource",
ModulePath: "module.hello",
}, {
Name: "inner_example",
Type: "null_resource",
ModulePath: "module.hello.module.there",
}},
InitResponse: &proto.InitComplete{
Modules: []*proto.Module{{
Key: "hello",
Version: "",
@@ -758,6 +803,17 @@ func TestProvision(t *testing.T) {
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",
@@ -792,7 +848,7 @@ func TestProvision(t *testing.T) {
WorkspaceOwnerRbacRoles: []*proto.Role{{Name: "member", OrgId: ""}},
},
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "null_resource",
@@ -833,7 +889,7 @@ func TestProvision(t *testing.T) {
PrebuiltWorkspaceBuildStage: proto.PrebuiltWorkspaceBuildStage_CREATE,
},
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "null_resource",
@@ -871,7 +927,7 @@ func TestProvision(t *testing.T) {
PrebuiltWorkspaceBuildStage: proto.PrebuiltWorkspaceBuildStage_CLAIM,
},
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "null_resource",
@@ -910,7 +966,7 @@ func TestProvision(t *testing.T) {
`, provider.TaskPromptParameterName),
},
Request: &proto.PlanRequest{},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Name: "a",
@@ -962,7 +1018,7 @@ func TestProvision(t *testing.T) {
}
`,
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "coder_external_agent",
@@ -987,7 +1043,7 @@ func TestProvision(t *testing.T) {
}
`,
},
Response: &proto.PlanComplete{
Response: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Name: "my-task",
@@ -1004,6 +1060,14 @@ func TestProvision(t *testing.T) {
},
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.
@@ -1043,9 +1107,18 @@ func TestProvision(t *testing.T) {
ctx, api := setupProvisioner(t, &provisionerServeOptions{
cliConfigPath: cliConfigPath,
})
sess := configure(ctx, t, api, &proto.Config{
TemplateSourceArchive: testutil.CreateTar(t, testCase.Files),
sess := configure(ctx, t, api, &proto.Config{})
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{
Metadata: testCase.Metadata,
@@ -1054,7 +1127,7 @@ func TestProvision(t *testing.T) {
planRequest = &proto.Request{Type: &proto.Request_Plan{Plan: testCase.Request}}
}
gotExpectedLog := testCase.ExpectLogContains == ""
gotExpectedLog := testCase.PlanExpectLogContains == ""
provision := func(req *proto.Request) *proto.Response {
err := sess.Send(req)
@@ -1063,7 +1136,7 @@ func TestProvision(t *testing.T) {
msg, err := sess.Recv()
require.NoError(t, err)
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
}
@@ -1078,35 +1151,43 @@ func TestProvision(t *testing.T) {
planComplete := resp.GetPlan()
require.NotNil(t, planComplete)
if testCase.ErrorContains != "" {
require.Contains(t, planComplete.GetError(), testCase.ErrorContains)
if testCase.PlanErrorContains != "" {
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 {
require.Equal(t, testCase.Response.Error, planComplete.Error)
require.Equal(t, testCase.Response.Error, graphComplete.Error)
// Remove randomly generated data and sort by name.
normalizeResources(planComplete.Resources)
resourcesGot, err := json.Marshal(planComplete.Resources)
normalizeResources(graphComplete.Resources)
resourcesGot, err := json.Marshal(graphComplete.Resources)
require.NoError(t, err)
resourcesWant, err := json.Marshal(testCase.Response.Resources)
require.NoError(t, err)
require.Equal(t, string(resourcesWant), string(resourcesGot))
parametersGot, err := json.Marshal(planComplete.Parameters)
parametersGot, err := json.Marshal(graphComplete.Parameters)
require.NoError(t, err)
parametersWant, err := json.Marshal(testCase.Response.Parameters)
require.NoError(t, err)
require.Equal(t, string(parametersWant), string(parametersGot))
modulesGot, err := json.Marshal(planComplete.Modules)
modulesGot, err := json.Marshal(initComplete.Modules)
require.NoError(t, err)
modulesWant, err := json.Marshal(testCase.Response.Modules)
require.NoError(t, err)
require.Equal(t, string(modulesWant), string(modulesGot))
if testCase.InitResponse != nil {
modulesWant, err := json.Marshal(testCase.InitResponse.Modules)
require.NoError(t, err)
require.Equal(t, string(modulesWant), string(modulesGot))
}
require.Equal(t, planComplete.HasAiTasks, testCase.Response.HasAiTasks)
require.Equal(t, planComplete.HasExternalAgents, testCase.Response.HasExternalAgents)
require.Equal(t, graphComplete.HasAiTasks, testCase.Response.HasAiTasks)
require.Equal(t, graphComplete.HasExternalAgents, testCase.Response.HasExternalAgents)
}
if testCase.Apply {
@@ -1117,8 +1198,8 @@ func TestProvision(t *testing.T) {
require.NotNil(t, applyComplete)
if testCase.Response != nil {
normalizeResources(applyComplete.Resources)
resourcesGot, err := json.Marshal(applyComplete.Resources)
normalizeResources(graphComplete.Resources)
resourcesGot, err := json.Marshal(graphComplete.Resources)
require.NoError(t, err)
resourcesWant, err := json.Marshal(testCase.Response.Resources)
require.NoError(t, err)
@@ -1127,7 +1208,7 @@ func TestProvision(t *testing.T) {
}
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)
ctx, api := setupProvisioner(t, nil)
sess := configure(ctx, t, api, &proto.Config{
TemplateSourceArchive: testutil.CreateTar(t, map[string]string{"main.tf": `resource "null_resource" "A" {}`}),
})
sess := configure(ctx, t, api, &proto.Config{})
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)
require.NoError(t, err)
@@ -1210,9 +1292,10 @@ func TestProvision_SafeEnv(t *testing.T) {
`
ctx, api := setupProvisioner(t, nil)
sess := configure(ctx, t, api, &proto.Config{
TemplateSourceArchive: testutil.CreateTar(t, map[string]string{"main.tf": echoResource}),
})
sess := configure(ctx, t, api, &proto.Config{})
resp := sendInitAndGetResp(t, sess, testutil.CreateTar(t, map[string]string{"main.tf": echoResource}))
require.Empty(t, resp.Error)
err := sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err)
@@ -1232,15 +1315,14 @@ func TestProvision_MalformedModules(t *testing.T) {
t.Parallel()
ctx, api := setupProvisioner(t, nil)
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_`,
}),
})
sess := configure(ctx, t, api, &proto.Config{})
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)
log := readProvisionLog(t, sess)
require.Contains(t, log, "Invalid block definition")
}
-9
View File
@@ -281,12 +281,3 @@ func (e *timingSpan) toProto() *proto.Timing {
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 (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
@@ -35,65 +36,66 @@ func TestTimingsFromProvision(t *testing.T) {
ctx, api := setupProvisioner(t, &provisionerServeOptions{
binaryPath: fakeBin,
})
sess := configure(ctx, t, api, &proto.Config{
TemplateSourceArchive: testutil.CreateTar(t, nil),
})
sess := configure(ctx, t, api, &proto.Config{})
ctx, cancel := context.WithTimeout(ctx, testutil.WaitLong)
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
// state file and some log content.
err = sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err)
var timings []*proto.Timing
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
}
}
handleResponse(t, "plan")
// When: the plan has completed, let's trigger an apply.
err = sendApply(sess, proto.WorkspaceTransition_START)
require.NoError(t, err)
for {
select {
case <-ctx.Done():
t.Fatal(ctx.Err())
default:
}
handleResponse(t, "apply")
msg, err := sess.Recv()
require.NoError(t, err)
// When: the apply has completed, graph the results
err = sendGraph(sess, proto.GraphSource_SOURCE_STATE)
require.NoError(t, err)
if log := msg.GetLog(); log != nil {
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
}
}
handleResponse(t, "graph")
// Sort the timings stably to keep reduce flakiness.
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.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"}`))
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"}`))
graphTiming := graphTimings[0]
// Graphing is omitted as it is captured by the stage timing, which uses now()
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
// provisioner/terraform/testdata/timings-aggregation/fake-terraform.sh.
@@ -134,9 +145,6 @@ func TestTimingsFromProvision(t *testing.T) {
case string(database.ProvisionerJobTimingStageInit):
require.True(t, terraform_internal.TimingsAreEqual(t, []*proto.Timing{initTimings[iCursor]}, []*proto.Timing{tim}))
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):
require.True(t, terraform_internal.TimingsAreEqual(t, []*proto.Timing{planTimings[pCursor]}, []*proto.Timing{tim}))
pCursor++
+103 -58
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/hashicorp/yamux"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
@@ -131,6 +132,16 @@ func TestProvisionerd(t *testing.T) {
}
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()
@@ -138,47 +149,6 @@ func TestProvisionerd(t *testing.T) {
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
// returns large payload messages back. The limit should be 4mb, so all
// these messages should work.
@@ -227,14 +197,16 @@ func TestProvisionerd(t *testing.T) {
Readme: make([]byte, largeSize),
}
},
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func(
_ *provisionersdk.Session,
_ *sdkproto.PlanRequest,
_ <-chan struct{},
) *sdkproto.PlanComplete {
return &sdkproto.PlanComplete{
Resources: []*sdkproto.Resource{},
Plan: make([]byte, largeSize),
Plan: make([]byte, largeSize),
}
},
apply: func(
@@ -246,6 +218,11 @@ func TestProvisionerd(t *testing.T) {
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))
@@ -299,6 +276,9 @@ func TestProvisionerd(t *testing.T) {
<-cancelOrComplete
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))
@@ -349,6 +329,7 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: extractInit(t),
parse: func(
s *provisionersdk.Session,
_ *sdkproto.ParseRequest,
@@ -366,9 +347,7 @@ func TestProvisionerd(t *testing.T) {
cancelOrComplete <-chan struct{},
) *sdkproto.PlanComplete {
s.ProvisionLog(sdkproto.LogLevel_INFO, "hello")
return &sdkproto.PlanComplete{
Resources: []*sdkproto.Resource{},
}
return &sdkproto.PlanComplete{}
},
apply: func(
_ *provisionersdk.Session,
@@ -378,6 +357,11 @@ func TestProvisionerd(t *testing.T) {
t.Error("dry run should not apply")
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
}, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func(
_ *provisionersdk.Session,
_ *sdkproto.PlanRequest,
_ <-chan struct{},
) *sdkproto.PlanComplete {
return &sdkproto.PlanComplete{
Resources: []*sdkproto.Resource{},
}
return &sdkproto.PlanComplete{}
},
apply: func(
_ *provisionersdk.Session,
@@ -450,6 +435,11 @@ func TestProvisionerd(t *testing.T) {
t.Error("dry run should not apply")
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
}, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func(
s *provisionersdk.Session,
_ *sdkproto.PlanRequest,
@@ -513,6 +506,9 @@ func TestProvisionerd(t *testing.T) {
) *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))
@@ -570,6 +566,9 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func(
s *provisionersdk.Session,
_ *sdkproto.PlanRequest,
@@ -577,14 +576,7 @@ func TestProvisionerd(t *testing.T) {
) *sdkproto.PlanComplete {
s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow")
return &sdkproto.PlanComplete{
Resources: []*sdkproto.Resource{
{
DailyCost: 10,
},
{
DailyCost: 15,
},
},
DailyCost: 25,
}
},
apply: func(
@@ -593,7 +585,10 @@ func TestProvisionerd(t *testing.T) {
_ <-chan struct{},
) *sdkproto.ApplyComplete {
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{
{
DailyCost: 10,
@@ -646,6 +641,12 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.LocalProvisioners{
"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(
s *provisionersdk.Session,
_ *sdkproto.PlanRequest,
@@ -756,6 +757,9 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func(
s *provisionersdk.Session,
_ *sdkproto.PlanRequest,
@@ -844,6 +848,9 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func(
s *provisionersdk.Session,
_ *sdkproto.PlanRequest,
@@ -938,6 +945,9 @@ func TestProvisionerd(t *testing.T) {
return client, nil
}, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func(
_ *provisionersdk.Session,
_ *sdkproto.PlanRequest,
@@ -1031,6 +1041,9 @@ func TestProvisionerd(t *testing.T) {
return client, nil
}, provisionerd.LocalProvisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
init: func(s *provisionersdk.Session, r *sdkproto.InitRequest, canceledOrComplete <-chan struct{}) *sdkproto.InitComplete {
return &sdkproto.InitComplete{}
},
plan: func(
_ *provisionersdk.Session,
_ *sdkproto.PlanRequest,
@@ -1045,6 +1058,9 @@ func TestProvisionerd(t *testing.T) {
) *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))
@@ -1125,6 +1141,12 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.LocalProvisioners{
"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(
s *provisionersdk.Session,
_ *sdkproto.PlanRequest,
@@ -1253,9 +1275,15 @@ func createProvisionerClient(t *testing.T, done <-chan struct{}, server provisio
}
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
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
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 {
@@ -1270,6 +1298,10 @@ func (p *provisionerTestServer) Apply(s *provisionersdk.Session, r *sdkproto.App
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 {
return p.uploadFile(stream)
}
@@ -1359,3 +1391,16 @@ func (a *acquireOne) acquireWithCancel(stream proto.DRPCProvisionerDaemon_Acquir
}
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
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -515,7 +514,6 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
defer span.End()
failedJob := r.configure(&sdkproto.Config{
TemplateSourceArchive: r.job.GetTemplateSourceArchive(),
TemplateId: strings2.EmptyToNil(r.job.GetTemplateImport().Metadata.TemplateId),
TemplateVersionId: strings2.EmptyToNil(r.job.GetTemplateImport().Metadata.TemplateVersionId),
ExpReuseTerraformWorkspace: ptr.Ref(false),
@@ -524,6 +522,18 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
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
r.queueLog(ctx, &proto.Log{
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,
WorkspaceOwnerGroups: r.job.GetTemplateImport().Metadata.WorkspaceOwnerGroups,
WorkspaceTransition: sdkproto.WorkspaceTransition_START,
}, false)
})
if err != nil {
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,
WorkspaceOwnerGroups: r.job.GetTemplateImport().Metadata.WorkspaceOwnerGroups,
WorkspaceTransition: sdkproto.WorkspaceTransition_STOP,
}, true, // Modules downloaded on the start provision
)
})
if err != nil {
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,
ExternalAuthProvidersNames: externalAuthProviderNames,
ExternalAuthProviders: startProvision.ExternalAuthProviders,
StartModules: startProvision.Modules,
StopModules: stopProvision.Modules,
Presets: startProvision.Presets,
Plan: startProvision.Plan,
// ModuleFiles are not on the stopProvision. So grab from the startProvision.
ModuleFiles: startProvision.ModuleFiles,
// TODO: These are defined as different, but can they be?
// Terraform downloads modules regardless of `count`, so this should be the same
StartModules: initResp.Modules,
StopModules: initResp.Modules,
Presets: startProvision.Presets,
Plan: startProvision.Plan,
ModuleFiles: initResp.ModuleFiles,
// ModuleFileHash will be populated if the file is uploaded async
ModuleFilesHash: []byte{},
HasAiTasks: startProvision.HasAITasks,
@@ -666,10 +676,8 @@ type templateImportProvision struct {
Resources []*sdkproto.Resource
Parameters []*sdkproto.RichParameter
ExternalAuthProviders []*sdkproto.ExternalAuthProviderResource
Modules []*sdkproto.Module
Presets []*sdkproto.Preset
Plan json.RawMessage
ModuleFiles []byte
HasAITasks bool
HasExternalAgents bool
}
@@ -677,8 +685,8 @@ type templateImportProvision struct {
// 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.
// 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) {
return r.runTemplateImportProvisionWithRichParameters(ctx, variableValues, nil, metadata, omitModules)
func (r *Runner) runTemplateImportProvision(ctx context.Context, variableValues []*sdkproto.VariableValue, metadata *sdkproto.Metadata) (*templateImportProvision, error) {
return r.runTemplateImportProvisionWithRichParameters(ctx, variableValues, nil, metadata)
}
// Performs a dry-run provision with provided rich parameters.
@@ -688,7 +696,6 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(
variableValues []*sdkproto.VariableValue,
richParameterValues []*sdkproto.RichParameterValue,
metadata *sdkproto.Metadata,
omitModules bool,
) (*templateImportProvision, error) {
ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End()
@@ -700,126 +707,48 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(
case sdkproto.WorkspaceTransition_STOP:
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
err := r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Plan{Plan: &sdkproto.PlanRequest{
Metadata: metadata,
RichParameterValues: richParameterValues,
// Template import has no previous values
PreviousParameterValues: make([]*sdkproto.RichParameterValue, 0),
planComplete, failed := r.plan(ctx, stage, &sdkproto.PlanRequest{
Metadata: metadata,
RichParameterValues: richParameterValues,
VariableValues: variableValues,
OmitModuleFiles: omitModules,
}}})
if err != nil {
return nil, xerrors.Errorf("start provision: %w", err)
ExternalAuthProviders: nil,
PreviousParameterValues: nil,
State: nil,
})
if failed != nil {
return nil, xerrors.Errorf("plan during template import provision: %w", failed)
}
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, 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 == nil {
return nil, xerrors.New("plan during template import provision returned nil response")
}
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) {
@@ -854,19 +783,28 @@ func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *p
metadata.WorkspaceOwnerId = id.String()
}
failedJob := r.configure(&sdkproto.Config{
TemplateSourceArchive: r.job.GetTemplateSourceArchive(),
})
failedJob := r.configure(&sdkproto.Config{})
if failedJob != nil {
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.
provision, err := r.runTemplateImportProvisionWithRichParameters(ctx,
r.job.GetTemplateDryRun().GetVariableValues(),
r.job.GetTemplateDryRun().GetRichParameterValues(),
metadata,
false,
)
if err != nil {
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_{
TemplateDryRun: &proto.CompletedJob_TemplateDryRun{
Resources: provision.Resources,
Modules: provision.Modules,
Modules: initResp.Modules,
},
},
}, nil
}
func (r *Runner) buildWorkspace(ctx context.Context, stage string, req *sdkproto.Request) (
*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)
func (r *Runner) commitQuota(ctx context.Context, cost int32) *proto.FailedJob {
r.logger.Debug(ctx, "committing quota",
slog.F("resources", resourceNames(resources)),
slog.F("cost", cost),
)
if cost == 0 {
@@ -953,9 +832,8 @@ func (r *Runner) commitQuota(ctx context.Context, resources []*sdkproto.Resource
const stage = "Commit quota"
resp, err := r.quotaCommitter.CommitQuota(ctx, &proto.CommitQuotaRequest{
JobId: r.job.JobId,
// #nosec G115 - Safe conversion as cost is expected to be within int32 range for provisioning costs
DailyCost: int32(cost),
JobId: r.job.JobId,
DailyCost: cost,
})
if err != nil {
r.queueLog(ctx, &proto.Log{
@@ -1014,8 +892,6 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
}
failedJob := r.configure(&sdkproto.Config{
TemplateSourceArchive: r.job.GetTemplateSourceArchive(),
State: r.job.GetWorkspaceBuild().State,
ProvisionerLogLevel: r.job.GetWorkspaceBuild().LogLevel,
TemplateId: strings2.EmptyToNil(r.job.GetWorkspaceBuild().Metadata.TemplateId),
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
}
resp, failed := r.buildWorkspace(ctx, "Planning infrastructure", &sdkproto.Request{
Type: &sdkproto.Request_Plan{
Plan: &sdkproto.PlanRequest{
OmitModuleFiles: true, // Only useful for template imports
Metadata: r.job.GetWorkspaceBuild().Metadata,
RichParameterValues: r.job.GetWorkspaceBuild().RichParameterValues,
PreviousParameterValues: r.job.GetWorkspaceBuild().PreviousParameterValues,
VariableValues: r.job.GetWorkspaceBuild().VariableValues,
ExternalAuthProviders: r.job.GetWorkspaceBuild().ExternalAuthProviders,
// timings collects all timings from each phase of the build
timings := make([]*sdkproto.Timing, 0)
// Initialize the Terraform working directory
initComplete, failedJob := r.init(ctx, true, r.job.GetTemplateSourceArchive())
if failedJob != nil {
return nil, failedJob
}
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 {
return nil, failed
}
planComplete := resp.GetPlan()
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 != "" {
r.logger.Warn(context.Background(), "plan request failed",
slog.F("error", planComplete.Error),
@@ -1053,27 +957,28 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
JobId: r.job.JobId,
Error: planComplete.Error,
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",
slog.F("resource_count", len(planComplete.Resources)),
slog.F("resources", resourceNames(planComplete.Resources)),
)
r.logger.Info(context.Background(), "plan request successful")
r.flushQueuedLogs(ctx)
if commitQuota {
failed = r.commitQuota(ctx, planComplete.Resources)
failed = r.commitQuota(ctx, planComplete.GetDailyCost())
r.flushQueuedLogs(ctx)
if failed != nil {
return nil, failed
}
}
// Run Terraform Apply
r.queueLog(ctx, &proto.Log{
Source: proto.LogSource_PROVISIONER_DAEMON,
Level: sdkproto.LogLevel_INFO,
@@ -1081,24 +986,17 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
CreatedAt: time.Now().UnixMilli(),
})
resp, failed = r.buildWorkspace(ctx, applyStage, &sdkproto.Request{
Type: &sdkproto.Request_Apply{
Apply: &sdkproto.ApplyRequest{
Metadata: r.job.GetWorkspaceBuild().Metadata,
},
},
applyComplete, failed := r.apply(ctx, applyStage, &sdkproto.ApplyRequest{
Metadata: r.job.GetWorkspaceBuild().Metadata,
})
if failed != nil {
return nil, failed
}
applyComplete := resp.GetApply()
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")
}
// Prepend the plan timings (since they occurred first).
applyComplete.Timings = append(planComplete.Timings, applyComplete.Timings...)
// Collect apply timings
timings = append(timings, applyComplete.Timings...)
if applyComplete.Error != "" {
r.logger.Warn(context.Background(), "apply failed; updating state",
slog.F("error", applyComplete.Error),
@@ -1111,15 +1009,46 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
Type: &proto.FailedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{
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",
slog.F("resource_count", len(applyComplete.Resources)),
slog.F("resources", resourceNames(applyComplete.Resources)),
slog.F("resource_count", len(graphComplete.Resources)),
slog.F("resources", resourceNames(graphComplete.Resources)),
slog.F("state_len", len(applyComplete.State)),
)
r.flushQueuedLogs(ctx)
@@ -1129,15 +1058,14 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
Type: &proto.CompletedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{
State: applyComplete.State,
Resources: applyComplete.Resources,
Timings: applyComplete.Timings,
// Modules are created on disk by `terraform init`, and that is only
// called by `plan`. `apply` does not modify them, so we can use the
// modules from the plan response.
Modules: planComplete.Modules,
Resources: graphComplete.Resources,
Timings: timings,
// Modules files are omitted for workspace builds, but the modules.json metadata
// is available from init to return.
Modules: initComplete.Modules,
// Resource replacements are discovered at plan time, only.
ResourceReplacements: planComplete.ResourceReplacements,
AiTasks: applyComplete.AiTasks,
AiTasks: graphComplete.AiTasks,
},
},
}, nil
+8
View File
@@ -10,6 +10,10 @@ func ParseErrorf(format string, args ...any) *proto.ParseComplete {
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 {
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 {
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
message Config {
// template_source_archive is a tar of the template source files
bytes template_source_archive = 1;
// state is the provisioner state (if any)
bytes state = 2;
string provisioner_log_level = 3;
string provisioner_log_level = 1;
// Template imports can omit template id
optional string template_id = 4;
optional string template_id = 2;
// Dry runs omit version id
optional string template_version_id = 5;
optional bool exp_reuse_terraform_workspace = 6; // Whether to reuse existing terraform workspaces if they exist.
optional string template_version_id = 3;
optional bool exp_reuse_terraform_workspace = 4; // Whether to reuse existing terraform workspaces if they exist.
}
// ParseRequest consumes source-code to produce inputs.
@@ -393,6 +389,25 @@ message ParseComplete {
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
message PlanRequest {
Metadata metadata = 1;
@@ -401,52 +416,61 @@ message PlanRequest {
repeated ExternalAuthProvider external_auth_providers = 4;
repeated RichParameterValue previous_parameter_values = 5;
// 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 for
// backwards compatibility reasons, the zero value should be the previous
// behavior of downloading the module files.
bool omit_module_files = 6;
// state is the provisioner state (if any)
bytes state = 6;
}
// PlanComplete indicates a request to plan completed.
message PlanComplete {
string error = 1;
repeated Resource resources = 2;
repeated RichParameter parameters = 3;
repeated ExternalAuthProviderResource external_auth_providers = 4;
repeated Timing timings = 6;
repeated Module modules = 7;
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;
repeated Timing timings = 2;
bytes plan = 3;
int32 dailyCost = 4;
repeated ResourceReplacement resource_replacements = 5;
int32 ai_task_count = 6;
}
// 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.
message ApplyRequest {
Metadata metadata = 1;
// state is the provisioner state (if any)
bytes state = 6;
}
// ApplyComplete indicates a request to apply completed.
message ApplyComplete {
bytes state = 1;
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 RichParameter parameters = 4;
repeated ExternalAuthProviderResource external_auth_providers = 5;
repeated Timing timings = 6;
repeated provisioner.AITask ai_tasks = 7;
repeated Preset presets = 6;
// 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 {
@@ -472,9 +496,11 @@ message Request {
oneof type {
Config config = 1;
ParseRequest parse = 2;
PlanRequest plan = 3;
ApplyRequest apply = 4;
CancelRequest cancel = 5;
InitRequest init = 3;
PlanRequest plan = 4;
ApplyRequest apply = 5;
GraphRequest graph = 6;
CancelRequest cancel = 7;
}
}
@@ -482,10 +508,12 @@ message Response {
oneof type {
Log log = 1;
ParseComplete parse = 2;
PlanComplete plan = 3;
ApplyComplete apply = 4;
DataUpload data_upload = 5;
ChunkPiece chunk_piece = 6;
InitComplete init = 3;
PlanComplete plan = 4;
ApplyComplete apply = 5;
GraphComplete graph = 6;
DataUpload data_upload = 7;
ChunkPiece chunk_piece = 8;
}
}
@@ -519,14 +547,28 @@ message ChunkPiece {
service Provisioner {
// 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
// of zero or more Logs, followed by the corresponding complete message (ParseComplete, PlanComplete,
// ApplyComplete). The daemon may then send a new request. 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.
// by one of the requests (InitRequest, ParseRequest, PlanRequest, ApplyRequest, GraphRequest). The provisioner
// should respond with a stream of zero or more Logs, followed by the corresponding complete message
// (InitComplete, ParseComplete, PlanComplete, ApplyComplete, GraphComplete).
// The daemon may then send a new request.
//
// The daemon may send a CancelRequest, asynchronously to ask the provisioner to cancel the previous ParseRequest,
// PlanRequest, or ApplyRequest. 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.
// A request to Parse or Plan MUST be preceded by a request init. The provisioner should store the init data on
// the session after a successful init. If the daemon closes the session, the init data may be safely discarded.
//
// 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);
}
+2
View File
@@ -35,9 +35,11 @@ type ServeOptions struct {
}
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
Plan(s *Session, r *proto.PlanRequest, canceledOrComplete <-chan struct{}) *proto.PlanComplete
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.
+20
View File
@@ -44,6 +44,11 @@ func TestProvisionerSDK(t *testing.T) {
err = s.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}})
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{}}})
require.NoError(t, err)
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{}}})
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{}}})
require.NoError(t, err)
msg, err := s.Recv()
@@ -135,8 +145,18 @@ func TestProvisionerSDK(t *testing.T) {
})
}
var _ provisionersdk.Server = unimplementedServer{}
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 {
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)
}
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()
}
@@ -110,6 +106,10 @@ func (s *Session) handleRequests() error {
}
resp := &proto.Response{}
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]{
req: parse,
session: s,
@@ -129,48 +129,28 @@ func (s *Session) handleRequests() error {
}
resp.Type = &proto.Response_Parse{Parse: complete}
}
if plan := req.GetPlan(); plan != nil {
r := &request[*proto.PlanRequest, *proto.PlanComplete]{
req: plan,
session: s,
serverFn: s.server.Plan,
cancels: requests,
if init := req.GetInit(); init != nil {
if s.initialized {
return xerrors.New("cannot init more than once per session")
}
complete, err := r.do()
initResp, err := s.handleInitRequest(init, requests)
if err != nil {
return err
}
resp.Type = &proto.Response_Plan{Plan: complete}
if protobuf.Size(resp) > 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
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
}
}
}
resp.Type = &proto.Response_Init{Init: initResp}
}
if plan := req.GetPlan(); plan != nil {
if !s.initialized {
return xerrors.New("cannot plan before successful init")
}
if complete.Error == "" {
planResp, err := s.handlePlanRequest(plan, requests)
if err != nil {
return err
}
if planResp.Error == "" {
planned = true
}
resp.Type = &proto.Response_Plan{Plan: planResp}
}
if apply := req.GetApply(); apply != nil {
if !planned {
@@ -188,6 +168,23 @@ func (s *Session) handleRequests() error {
}
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)
if err != nil {
return xerrors.Errorf("send response: %w", err)
@@ -196,11 +193,75 @@ func (s *Session) handleRequests() error {
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 {
Logger slog.Logger
Files tfpath.Layouter
Config *proto.Config
// initialized indicates if an init was run.
// Required for plan/apply.
initialized bool
server Server
stream proto.DRPCProvisioner_SessionStream
logLevel int32
@@ -226,11 +287,11 @@ func (s *Session) ProvisionLog(level proto.LogLevel, output string) {
}
type pRequest interface {
*proto.ParseRequest | *proto.PlanRequest | *proto.ApplyRequest
*proto.ParseRequest | *proto.InitRequest | *proto.PlanRequest | *proto.ApplyRequest | *proto.GraphRequest
}
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
+4 -9
View File
@@ -16,7 +16,6 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/provisionersdk/proto"
)
type Layouter interface {
@@ -28,7 +27,7 @@ type Layouter interface {
TerraformMetadataDir() string
ModulesDirectory() 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)
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")
}
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",
slog.F("size_bytes", len(cfg.TemplateSourceArchive)),
slog.F("size_bytes", len(templateSourceArchive)),
)
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)
}
// TODO: Pass in cfg.TemplateSourceArchive, not the full config.
// 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
reader := tar.NewReader(bytes.NewBuffer(templateSourceArchive))
for {
header, err := reader.Next()
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))
}
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",
slog.F("size_bytes", len(cfg.TemplateSourceArchive)),
slog.F("size_bytes", len(archive)),
)
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)
}
reader := tar.NewReader(bytes.NewBuffer(cfg.TemplateSourceArchive))
// for safety, nil out the reference on Config, since the reader now owns it.
cfg.TemplateSourceArchive = nil
reader := tar.NewReader(bytes.NewBuffer(archive))
for {
header, err := reader.Next()
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Name: "example",
+42 -44
View File
@@ -60,27 +60,11 @@ func Test_Runner(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
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{
{
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)
@@ -209,10 +208,10 @@ func Test_Runner(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Parameters: testParameters,
},
},
@@ -341,27 +340,11 @@ func Test_Runner(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
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{
{
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)
@@ -484,10 +482,10 @@ func Test_Runner(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
+12 -3
View File
@@ -58,7 +58,14 @@ func Test_Runner(t *testing.T) {
},
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Apply: &proto.ApplyComplete{},
},
},
},
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Name: "example1",
@@ -245,8 +252,10 @@ func Test_Runner(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionInit: echo.InitComplete,
ProvisionGraph: echo.GraphComplete,
ProvisionApply: []*proto.Response{
{
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -168,9 +168,9 @@ func TestRun(t *testing.T) {
version = coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
ProvisionGraph: []*proto.Response{{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{{
Name: "example",
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{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: []*proto.Response{
ProvisionGraph: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Type: &proto.Response_Graph{
Graph: &proto.GraphComplete{
Resources: []*proto.Resource{
{
Name: "example",
+110 -49
View File
@@ -32,6 +32,8 @@ import {
type ApplyComplete,
AppSharingLevel,
type ExternalAuthProviderResource,
type GraphComplete,
type InitComplete,
type ParseComplete,
type PlanComplete,
type Resource,
@@ -540,12 +542,14 @@ type RecursivePartial<T> = {
};
interface EchoProvisionerResponses {
init?: RecursivePartial<Response>[];
// parse is for observing any Terraform variables
parse?: RecursivePartial<Response>[];
// plan occurs when the template is imported
plan?: RecursivePartial<Response>[];
// apply occurs when the workspace is built
apply?: RecursivePartial<Response>[];
graph?: RecursivePartial<Response>[];
// extraFiles allows the bundling of terraform files in echo provisioner tars
// in order to support dynamic parameters
extraFiles?: Map<string, string>;
@@ -560,6 +564,40 @@ const emptyPlan = new TextEncoder().encode("{}");
const createTemplateVersionTar = async (
responses: EchoProvisionerResponses = {},
): 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) {
responses.parse = [
{
@@ -575,25 +613,18 @@ const createTemplateVersionTar = async (
];
}
if (!responses.plan) {
responses.plan = responses.apply.map((response) => {
if (response.log) {
return response;
}
return {
plan: {
error: response.apply?.error ?? "",
resources: response.apply?.resources ?? [],
parameters: response.apply?.parameters ?? [],
externalAuthProviders: response.apply?.externalAuthProviders ?? [],
timings: response.apply?.timings ?? [],
presets: [],
resourceReplacements: [],
plan: emptyPlan,
moduleFiles: new Uint8Array(),
moduleFilesHash: new Uint8Array(),
},
};
});
responses.plan = [
{
plan: {},
},
];
}
if (!responses.graph) {
responses.graph = [
{
graph: {},
},
];
}
const tar = new TarWriter();
@@ -617,6 +648,33 @@ const createTemplateVersionTar = async (
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>) => {
if (resource.agents) {
@@ -701,40 +759,31 @@ const createTemplateVersionTar = async (
response.apply = {
error: "",
state: new Uint8Array(),
resources: [],
parameters: [],
externalAuthProviders: [],
timings: [],
aiTasks: [],
...response.apply,
} as ApplyComplete;
response.apply.resources = response.apply.resources?.map(fillResource);
tar.addFile(
`${index}.apply.protobuf`,
Response.encode(response as Response).finish(),
);
});
responses.plan.forEach((response, index) => {
response.plan = {
responses.graph.forEach((response, index) => {
response.graph = {
error: "",
resources: [],
parameters: [],
externalAuthProviders: [],
timings: [],
modules: [],
presets: [],
resourceReplacements: [],
plan: emptyPlan,
moduleFiles: new Uint8Array(),
moduleFilesHash: new Uint8Array(),
aiTasks: [],
...response.plan,
} as PlanComplete;
response.plan.resources = response.plan.resources?.map(fillResource);
...response.graph,
} as GraphComplete;
response.graph.resources = response.graph.resources?.map(fillResource);
tar.addFile(
`${index}.plan.protobuf`,
`${index}.graph.protobuf`,
Response.encode(response as Response).finish(),
);
});
@@ -889,16 +938,20 @@ ${options}}
parse: {},
},
],
plan: [
init: [
{
plan: {
parameters: richParameters,
},
init: {},
},
],
apply: [
plan: [
{
apply: {
plan: {},
},
],
graph: [
{
graph: {
parameters: richParameters,
resources: [
{
name: "example",
@@ -907,6 +960,11 @@ ${options}}
},
},
],
apply: [
{
apply: {},
},
],
extraFiles: new Map([["main.tf", tf]]),
};
};
@@ -915,21 +973,19 @@ export const echoResponsesWithExternalAuth = (
providers: ExternalAuthProviderResource[],
): EchoProvisionerResponses => {
return {
init: [
{
init: {},
},
],
parse: [
{
parse: {},
},
],
plan: [
graph: [
{
plan: {
externalAuthProviders: providers,
},
},
],
apply: [
{
apply: {
graph: {
externalAuthProviders: providers,
resources: [
{
@@ -939,6 +995,11 @@ export const echoResponsesWithExternalAuth = (
},
},
],
apply: [
{
apply: {},
},
],
};
};
+194 -99
View File
@@ -69,6 +69,13 @@ export enum PrebuiltWorkspaceBuildStage {
UNRECOGNIZED = -1,
}
export enum GraphSource {
SOURCE_UNKNOWN = 0,
SOURCE_PLAN = 1,
SOURCE_STATE = 2,
UNRECOGNIZED = -1,
}
export enum TimingState {
STARTED = 0,
COMPLETED = 1,
@@ -410,10 +417,6 @@ export interface Metadata {
/** Config represents execution configuration shared by all subsequent requests in the Session */
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;
/** Template imports can omit template id */
templateId?:
@@ -444,6 +447,26 @@ export interface ParseComplete_WorkspaceTagsEntry {
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 */
export interface PlanRequest {
metadata: Metadata | undefined;
@@ -451,29 +474,51 @@ export interface PlanRequest {
variableValues: VariableValue[];
externalAuthProviders: ExternalAuthProvider[];
previousParameterValues: RichParameterValue[];
/**
* 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 for
* backwards compatibility reasons, the zero value should be the previous
* behavior of downloading the module files.
*/
omitModuleFiles: boolean;
/** state is the provisioner state (if any) */
state: Uint8Array;
}
/** PlanComplete indicates a request to plan completed. */
export interface PlanComplete {
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[];
parameters: RichParameter[];
externalAuthProviders: ExternalAuthProviderResource[];
timings: Timing[];
modules: Module[];
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.
* 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;
}
/**
* 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 {
start: Date | undefined;
end: Date | undefined;
@@ -522,16 +548,20 @@ export interface CancelRequest {
export interface Request {
config?: Config | undefined;
parse?: ParseRequest | undefined;
init?: InitRequest | undefined;
plan?: PlanRequest | undefined;
apply?: ApplyRequest | undefined;
graph?: GraphRequest | undefined;
cancel?: CancelRequest | undefined;
}
export interface Response {
log?: Log | undefined;
parse?: ParseComplete | undefined;
init?: InitComplete | undefined;
plan?: PlanComplete | undefined;
apply?: ApplyComplete | undefined;
graph?: GraphComplete | undefined;
dataUpload?: DataUpload | undefined;
chunkPiece?: ChunkPiece | undefined;
}
@@ -1318,23 +1348,17 @@ export const Metadata = {
export const Config = {
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 !== "") {
writer.uint32(26).string(message.provisionerLogLevel);
writer.uint32(10).string(message.provisionerLogLevel);
}
if (message.templateId !== undefined) {
writer.uint32(34).string(message.templateId);
writer.uint32(18).string(message.templateId);
}
if (message.templateVersionId !== undefined) {
writer.uint32(42).string(message.templateVersionId);
writer.uint32(26).string(message.templateVersionId);
}
if (message.expReuseTerraformWorkspace !== undefined) {
writer.uint32(48).bool(message.expReuseTerraformWorkspace);
writer.uint32(32).bool(message.expReuseTerraformWorkspace);
}
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 = {
encode(message: PlanRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.metadata !== undefined) {
@@ -1393,8 +1450,8 @@ export const PlanRequest = {
for (const v of message.previousParameterValues) {
RichParameterValue.encode(v!, writer.uint32(42).fork()).ldelim();
}
if (message.omitModuleFiles !== false) {
writer.uint32(48).bool(message.omitModuleFiles);
if (message.state.length !== 0) {
writer.uint32(50).bytes(message.state);
}
return writer;
},
@@ -1405,44 +1462,20 @@ export const PlanComplete = {
if (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) {
Timing.encode(v!, writer.uint32(50).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();
Timing.encode(v!, writer.uint32(18).fork()).ldelim();
}
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) {
ResourceReplacement.encode(v!, writer.uint32(82).fork()).ldelim();
ResourceReplacement.encode(v!, writer.uint32(42).fork()).ldelim();
}
if (message.moduleFiles.length !== 0) {
writer.uint32(90).bytes(message.moduleFiles);
}
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);
if (message.aiTaskCount !== 0) {
writer.uint32(48).int32(message.aiTaskCount);
}
return writer;
},
@@ -1453,6 +1486,9 @@ export const ApplyRequest = {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim();
}
if (message.state.length !== 0) {
writer.uint32(50).bytes(message.state);
}
return writer;
},
};
@@ -1465,6 +1501,33 @@ export const ApplyComplete = {
if (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) {
Resource.encode(v!, writer.uint32(26).fork()).ldelim();
}
@@ -1474,11 +1537,17 @@ export const ApplyComplete = {
for (const v of message.externalAuthProviders) {
ExternalAuthProviderResource.encode(v!, writer.uint32(42).fork()).ldelim();
}
for (const v of message.timings) {
Timing.encode(v!, writer.uint32(50).fork()).ldelim();
for (const v of message.presets) {
Preset.encode(v!, writer.uint32(50).fork()).ldelim();
}
if (message.hasAiTasks !== false) {
writer.uint32(56).bool(message.hasAiTasks);
}
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;
},
@@ -1525,14 +1594,20 @@ export const Request = {
if (message.parse !== undefined) {
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) {
PlanRequest.encode(message.plan, writer.uint32(26).fork()).ldelim();
PlanRequest.encode(message.plan, writer.uint32(34).fork()).ldelim();
}
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) {
CancelRequest.encode(message.cancel, writer.uint32(42).fork()).ldelim();
CancelRequest.encode(message.cancel, writer.uint32(58).fork()).ldelim();
}
return writer;
},
@@ -1546,17 +1621,23 @@ export const Response = {
if (message.parse !== undefined) {
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) {
PlanComplete.encode(message.plan, writer.uint32(26).fork()).ldelim();
PlanComplete.encode(message.plan, writer.uint32(34).fork()).ldelim();
}
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) {
DataUpload.encode(message.dataUpload, writer.uint32(42).fork()).ldelim();
DataUpload.encode(message.dataUpload, writer.uint32(58).fork()).ldelim();
}
if (message.chunkPiece !== undefined) {
ChunkPiece.encode(message.chunkPiece, writer.uint32(50).fork()).ldelim();
ChunkPiece.encode(message.chunkPiece, writer.uint32(66).fork()).ldelim();
}
return writer;
},
@@ -1598,15 +1679,29 @@ export const ChunkPiece = {
export interface Provisioner {
/**
* 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
* of zero or more Logs, followed by the corresponding complete message (ParseComplete, PlanComplete,
* ApplyComplete). The daemon may then send a new request. 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.
* by one of the requests (InitRequest, ParseRequest, PlanRequest, ApplyRequest, GraphRequest). The provisioner
* should respond with a stream of zero or more Logs, followed by the corresponding complete message
* (InitComplete, ParseComplete, PlanComplete, ApplyComplete, GraphComplete).
* The daemon may then send a new request.
*
* The daemon may send a CancelRequest, asynchronously to ask the provisioner to cancel the previous ParseRequest,
* PlanRequest, or ApplyRequest. 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.
* A request to Parse or Plan MUST be preceded by a request init. The provisioner should store the init data on
* the session after a successful init. If the daemon closes the session, the init data may be safely discarded.
*
* 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>;
}
+2 -2
View File
@@ -32,9 +32,9 @@ test("app", async ({ context, page }) => {
}
const appName = "test-app";
const template = await createTemplate(page, {
apply: [
graph: [
{
apply: {
graph: {
resources: [
{
agents: [
+2 -2
View File
@@ -25,9 +25,9 @@ test.skip(`ssh with agent ${agentVersion}`, async ({ page }) => {
const token = randomUUID();
const template = await createTemplate(page, {
apply: [
graph: [
{
apply: {
graph: {
resources: [
{
agents: [
+2 -2
View File
@@ -23,9 +23,9 @@ test.beforeEach(async ({ page }) => {
test(`ssh with client ${clientVersion}`, async ({ page }) => {
const token = randomUUID();
const template = await createTemplate(page, {
apply: [
graph: [
{
apply: {
graph: {
resources: [
{
agents: [
+2 -2
View File
@@ -18,9 +18,9 @@ test.beforeEach(async ({ page }) => {
test("web terminal", async ({ context, page }) => {
const token = randomUUID();
const template = await createTemplate(page, {
apply: [
graph: [
{
apply: {
graph: {
resources: [
{
agents: [
@@ -32,7 +32,7 @@ test.beforeEach(async ({ page }) => {
test("create workspace", async ({ page }) => {
await login(page, users.templateAdmin);
const template = await createTemplate(page, {
apply: [{ apply: { resources: [{ name: "example" }] } }],
graph: [{ graph: { resources: [{ name: "example" }] } }],
});
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.",
},
},
{
name: "graph",
label: "graph",
section: "provisioning",
tooltip: {
heading: "Terraform graph",
description:
"List all resources in plan, used to update coderd database.",
},
},
{
name: "apply",
label: "apply",
@@ -266,6 +256,16 @@ export const provisioningStages: Stage[] = [
"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[] => {