Refactor Provisioner to distinguish Plan and Apply (#5036)

This commit is contained in:
Ammar Bandukwala
2022-11-11 16:45:58 -06:00
committed by GitHub
parent 71601f4971
commit 95fb59696e
37 changed files with 942 additions and 648 deletions
+25 -17
View File
@@ -116,13 +116,19 @@ func (e *echo) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
if err != nil {
return err
}
request := msg.GetStart()
if request == nil {
// A cancel could occur here!
var config *proto.Provision_Config
switch {
case msg.GetPlan() != nil:
config = msg.GetPlan().GetConfig()
case msg.GetApply() != nil:
config = msg.GetApply().GetConfig()
default:
// Probably a cancel
return nil
}
for _, param := range request.ParameterValues {
for _, param := range msg.GetPlan().GetParameterValues() {
if param.Name == ParameterExecKey {
toks := strings.Split(param.Value, "=")
if len(toks) < 2 {
@@ -139,11 +145,13 @@ func (e *echo) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
}
for index := 0; ; index++ {
extension := ".protobuf"
if request.DryRun {
extension = ".dry.protobuf"
var extension string
if msg.GetPlan() != nil {
extension = ".plan.protobuf"
} else {
extension = ".apply.protobuf"
}
path := filepath.Join(request.Directory, fmt.Sprintf("%d.provision"+extension, index))
path := filepath.Join(config.Directory, fmt.Sprintf("%d.provision"+extension, index))
_, err := e.filesystem.Stat(path)
if err != nil {
if index == 0 {
@@ -175,9 +183,9 @@ func (*echo) Shutdown(_ context.Context, _ *proto.Empty) (*proto.Empty, error) {
}
type Responses struct {
Parse []*proto.Parse_Response
Provision []*proto.Provision_Response
ProvisionDryRun []*proto.Provision_Response
Parse []*proto.Parse_Response
ProvisionApply []*proto.Provision_Response
ProvisionPlan []*proto.Provision_Response
}
// Tar returns a tar archive of responses to provisioner operations.
@@ -185,8 +193,8 @@ func Tar(responses *Responses) ([]byte, error) {
if responses == nil {
responses = &Responses{ParseComplete, ProvisionComplete, ProvisionComplete}
}
if responses.ProvisionDryRun == nil {
responses.ProvisionDryRun = responses.Provision
if responses.ProvisionPlan == nil {
responses.ProvisionPlan = responses.ProvisionApply
}
var buffer bytes.Buffer
@@ -208,13 +216,13 @@ func Tar(responses *Responses) ([]byte, error) {
return nil, err
}
}
for index, response := range responses.Provision {
for index, response := range responses.ProvisionApply {
data, err := protobuf.Marshal(response)
if err != nil {
return nil, err
}
err = writer.WriteHeader(&tar.Header{
Name: fmt.Sprintf("%d.provision.protobuf", index),
Name: fmt.Sprintf("%d.provision.apply.protobuf", index),
Size: int64(len(data)),
})
if err != nil {
@@ -225,13 +233,13 @@ func Tar(responses *Responses) ([]byte, error) {
return nil, err
}
}
for index, response := range responses.ProvisionDryRun {
for index, response := range responses.ProvisionPlan {
data, err := protobuf.Marshal(response)
if err != nil {
return nil, err
}
err = writer.WriteHeader(&tar.Header{
Name: fmt.Sprintf("%d.provision.dry.protobuf", index),
Name: fmt.Sprintf("%d.provision.plan.protobuf", index),
Size: int64(len(data)),
})
if err != nil {
+6 -4
View File
@@ -92,15 +92,17 @@ func TestEcho(t *testing.T) {
},
}}
data, err := echo.Tar(&echo.Responses{
Provision: responses,
ProvisionApply: responses,
})
require.NoError(t, err)
client, err := api.Provision(ctx)
require.NoError(t, err)
err = client.Send(&proto.Provision_Request{
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
Directory: unpackTar(t, fs, data),
Type: &proto.Provision_Request_Plan{
Plan: &proto.Provision_Plan{
Config: &proto.Provision_Config{
Directory: unpackTar(t, fs, data),
},
},
},
})
+24 -9
View File
@@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -76,6 +77,10 @@ func (e executor) execWriteOutput(ctx, killCtx context.Context, args, env []stri
// #nosec
cmd := exec.CommandContext(killCtx, e.binaryPath, args...)
cmd.Dir = e.workdir
if env == nil {
// We don't want to passthrough host env when unset.
env = []string{}
}
cmd.Env = env
// We want logs to be written in the correct order, so we wrap all logging
@@ -238,10 +243,15 @@ func (e executor) plan(ctx, killCtx context.Context, env, vars []string, logr lo
if err != nil {
return nil, err
}
planFileByt, err := os.ReadFile(planfilePath)
if err != nil {
return nil, err
}
return &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: resources,
Plan: planFileByt,
},
},
}, nil
@@ -292,21 +302,26 @@ func (e executor) graph(ctx, killCtx context.Context) (string, error) {
}
// revive:disable-next-line:flag-parameter
func (e executor) apply(ctx, killCtx context.Context, env, vars []string, logr logSink, destroy bool,
func (e executor) apply(
ctx, killCtx context.Context, plan []byte, env []string, logr logSink,
) (*proto.Provision_Response, error) {
planFile, err := ioutil.TempFile("", "coder-terrafrom-plan")
if err != nil {
return nil, xerrors.Errorf("create plan file: %w", err)
}
_, err = planFile.Write(plan)
if err != nil {
return nil, xerrors.Errorf("write plan file: %w", err)
}
defer os.Remove(planFile.Name())
args := []string{
"apply",
"-no-color",
"-auto-approve",
"-input=false",
"-json",
"-refresh=true",
}
if destroy {
args = append(args, "-destroy")
}
for _, variable := range vars {
args = append(args, "-var", variable)
planFile.Name(),
}
outWriter, doneOut := provisionLogWriter(logr)
@@ -318,7 +333,7 @@ func (e executor) apply(ctx, killCtx context.Context, env, vars []string, logr l
<-doneErr
}()
err := e.execWriteOutput(ctx, killCtx, args, env, outWriter, errWriter)
err = e.execWriteOutput(ctx, killCtx, args, env, outWriter, errWriter)
if err != nil {
return nil, xerrors.Errorf("terraform apply: %w", err)
}
+50 -34
View File
@@ -23,9 +23,21 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
if request.GetCancel() != nil {
return nil
}
// We expect the first message is start!
if request.GetStart() == nil {
var (
applyRequest = request.GetApply()
planRequest = request.GetPlan()
)
var (
config *proto.Provision_Config
)
if applyRequest == nil && planRequest == nil {
return nil
} else if applyRequest != nil {
config = applyRequest.Config
} else if planRequest != nil {
config = planRequest.Config
}
// Create a context for graceful cancellation bound to the stream
@@ -73,17 +85,16 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
logger: s.logger.Named("execution_logs"),
stream: stream,
}
start := request.GetStart()
e := s.executor(start.Directory)
e := s.executor(config.Directory)
if err = e.checkMinVersion(ctx); err != nil {
return err
}
logTerraformEnvVars(sink)
statefilePath := filepath.Join(start.Directory, "terraform.tfstate")
if len(start.State) > 0 {
err = os.WriteFile(statefilePath, start.State, 0o600)
statefilePath := filepath.Join(config.Directory, "terraform.tfstate")
if len(config.State) > 0 {
err = os.WriteFile(statefilePath, config.State, 0o600)
if err != nil {
return xerrors.Errorf("write statefile %q: %w", statefilePath, err)
}
@@ -94,7 +105,7 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
// 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 start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY && len(start.State) == 0 {
if config.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY && len(config.State) == 0 {
_ = stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
@@ -127,24 +138,23 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
}
s.logger.Debug(ctx, "ran initialization")
env, err := provisionEnv(start)
if err != nil {
return err
}
vars, err := provisionVars(start)
env, err := provisionEnv(config, request.GetPlan().GetParameterValues())
if err != nil {
return err
}
var resp *proto.Provision_Response
if start.DryRun {
resp, err = e.plan(ctx, killCtx, env, vars, sink,
start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY)
} else {
resp, err = e.apply(ctx, killCtx, env, vars, sink,
start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY)
}
if err != nil {
if start.DryRun {
if planRequest != nil {
vars, err := planVars(planRequest)
if err != nil {
return err
}
resp, err = e.plan(
ctx, killCtx, env, vars, sink,
config.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY,
)
if err != nil {
if ctx.Err() != nil {
return stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
@@ -156,6 +166,13 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
}
return xerrors.Errorf("plan terraform: %w", err)
}
return stream.Send(resp)
}
// Must be apply
resp, err = e.apply(
ctx, killCtx, applyRequest.Plan, env, sink,
)
if err != nil {
errorMessage := err.Error()
// Terraform can fail and apply and still need to store it's state.
// In this case, we return Complete with an explicit error message.
@@ -169,13 +186,12 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
},
})
}
return stream.Send(resp)
}
func provisionVars(start *proto.Provision_Start) ([]string, error) {
func planVars(plan *proto.Provision_Plan) ([]string, error) {
vars := []string{}
for _, param := range start.ParameterValues {
for _, param := range plan.ParameterValues {
switch param.DestinationScheme {
case proto.ParameterDestination_ENVIRONMENT_VARIABLE:
continue
@@ -188,21 +204,21 @@ func provisionVars(start *proto.Provision_Start) ([]string, error) {
return vars, nil
}
func provisionEnv(start *proto.Provision_Start) ([]string, error) {
func provisionEnv(config *proto.Provision_Config, params []*proto.ParameterValue) ([]string, error) {
env := safeEnviron()
env = append(env,
"CODER_AGENT_URL="+start.Metadata.CoderUrl,
"CODER_WORKSPACE_TRANSITION="+strings.ToLower(start.Metadata.WorkspaceTransition.String()),
"CODER_WORKSPACE_NAME="+start.Metadata.WorkspaceName,
"CODER_WORKSPACE_OWNER="+start.Metadata.WorkspaceOwner,
"CODER_WORKSPACE_OWNER_EMAIL="+start.Metadata.WorkspaceOwnerEmail,
"CODER_WORKSPACE_ID="+start.Metadata.WorkspaceId,
"CODER_WORKSPACE_OWNER_ID="+start.Metadata.WorkspaceOwnerId,
"CODER_AGENT_URL="+config.Metadata.CoderUrl,
"CODER_WORKSPACE_TRANSITION="+strings.ToLower(config.Metadata.WorkspaceTransition.String()),
"CODER_WORKSPACE_NAME="+config.Metadata.WorkspaceName,
"CODER_WORKSPACE_OWNER="+config.Metadata.WorkspaceOwner,
"CODER_WORKSPACE_OWNER_EMAIL="+config.Metadata.WorkspaceOwnerEmail,
"CODER_WORKSPACE_ID="+config.Metadata.WorkspaceId,
"CODER_WORKSPACE_OWNER_ID="+config.Metadata.WorkspaceOwnerId,
)
for key, value := range provisionersdk.AgentScriptEnv() {
env = append(env, key+"="+value)
}
for _, param := range start.ParameterValues {
for _, param := range params {
switch param.DestinationScheme {
case proto.ParameterDestination_ENVIRONMENT_VARIABLE:
env = append(env, fmt.Sprintf("%s=%s", param.Name, param.Value))
+179 -149
View File
@@ -63,6 +63,30 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont
return ctx, api
}
func readProvisionLog(t *testing.T, response proto.DRPCProvisioner_ProvisionClient) (
string,
*proto.Provision_Complete,
) {
var (
logBuf strings.Builder
c *proto.Provision_Complete
)
for {
msg, err := response.Recv()
require.NoError(t, err)
if log := msg.GetLog(); log != nil {
t.Log(log.Level.String(), log.Output)
logBuf.WriteString(log.Output)
}
if c = msg.GetComplete(); c != nil {
require.Empty(t, c.Error)
break
}
}
return logBuf.String(), c
}
func TestProvision_Cancel(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
@@ -113,16 +137,12 @@ func TestProvision_Cancel(t *testing.T) {
response, err := api.Provision(ctx)
require.NoError(t, err)
err = response.Send(&proto.Provision_Request{
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
Directory: dir,
DryRun: false,
ParameterValues: []*proto.ParameterValue{{
DestinationScheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
Name: "A",
Value: "example",
}},
Metadata: &proto.Provision_Metadata{},
Type: &proto.Provision_Request_Apply{
Apply: &proto.Provision_Apply{
Config: &proto.Provision_Config{
Directory: dir,
Metadata: &proto.Provision_Metadata{},
},
},
},
})
@@ -175,7 +195,7 @@ func TestProvision(t *testing.T) {
testCases := []struct {
Name string
Files map[string]string
Request *proto.Provision_Request
Request *proto.Provision_Plan
// Response may be nil to not check the response.
Response *proto.Provision_Response
// If ErrorContains is not empty, then response.Recv() should return an
@@ -183,7 +203,7 @@ func TestProvision(t *testing.T) {
ErrorContains string
// If ExpectLogContains is not empty, then the logs should contain it.
ExpectLogContains string
DryRun bool
Apply bool
}{
{
Name: "single-variable",
@@ -192,36 +212,26 @@ func TestProvision(t *testing.T) {
description = "Testing!"
}`,
},
Request: &proto.Provision_Request{
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
ParameterValues: []*proto.ParameterValue{{
DestinationScheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
Name: "A",
Value: "example",
}},
},
},
Request: &proto.Provision_Plan{
ParameterValues: []*proto.ParameterValue{{
DestinationScheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
Name: "A",
Value: "example",
}},
},
Response: &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
},
Apply: true,
},
{
Name: "missing-variable",
Files: map[string]string{
"main.tf": `variable "A" {
}`,
},
Response: &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Error: "terraform apply: exit status 1",
},
},
},
}`},
ErrorContains: "terraform plan:",
ExpectLogContains: "No value for required variable",
},
{
@@ -232,7 +242,6 @@ func TestProvision(t *testing.T) {
},
ErrorContains: "terraform plan:",
ExpectLogContains: "No value for required variable",
DryRun: true,
},
{
Name: "single-resource-dry-run",
@@ -249,7 +258,6 @@ func TestProvision(t *testing.T) {
},
},
},
DryRun: true,
},
{
Name: "single-resource",
@@ -266,6 +274,7 @@ func TestProvision(t *testing.T) {
},
},
},
Apply: true,
},
{
Name: "bad-syntax-1",
@@ -288,13 +297,10 @@ func TestProvision(t *testing.T) {
Files: map[string]string{
"main.tf": `resource "null_resource" "A" {}`,
},
Request: &proto.Provision_Request{
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
State: nil,
Metadata: &proto.Provision_Metadata{
WorkspaceTransition: proto.WorkspaceTransition_DESTROY,
},
Request: &proto.Provision_Plan{
Config: &proto.Provision_Config{
Metadata: &proto.Provision_Metadata{
WorkspaceTransition: proto.WorkspaceTransition_DESTROY,
},
},
},
@@ -305,16 +311,12 @@ func TestProvision(t *testing.T) {
Files: map[string]string{
"main.tf": "",
},
Request: &proto.Provision_Request{
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
ParameterValues: []*proto.ParameterValue{
{
DestinationScheme: 88,
Name: "UNSUPPORTED",
Value: "sadface",
},
},
Request: &proto.Provision_Plan{
ParameterValues: []*proto.ParameterValue{
{
DestinationScheme: 88,
Name: "UNSUPPORTED",
Value: "sadface",
},
},
},
@@ -333,79 +335,107 @@ func TestProvision(t *testing.T) {
require.NoError(t, err)
}
request := &proto.Provision_Request{
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
Directory: directory,
DryRun: testCase.DryRun,
planRequest := &proto.Provision_Request{
Type: &proto.Provision_Request_Plan{
Plan: &proto.Provision_Plan{
Config: &proto.Provision_Config{
Directory: directory,
},
},
},
}
if testCase.Request != nil {
request.GetStart().ParameterValues = testCase.Request.GetStart().ParameterValues
request.GetStart().State = testCase.Request.GetStart().State
request.GetStart().DryRun = testCase.Request.GetStart().DryRun
request.GetStart().Metadata = testCase.Request.GetStart().Metadata
if planRequest.GetPlan().GetConfig() == nil {
planRequest.GetPlan().Config = &proto.Provision_Config{}
}
planRequest.GetPlan().ParameterValues = testCase.Request.ParameterValues
if testCase.Request.Config != nil {
planRequest.GetPlan().Config.State = testCase.Request.Config.State
planRequest.GetPlan().Config.Metadata = testCase.Request.Config.Metadata
}
}
if request.GetStart().Metadata == nil {
request.GetStart().Metadata = &proto.Provision_Metadata{}
if planRequest.GetPlan().Config.Metadata == nil {
planRequest.GetPlan().Config.Metadata = &proto.Provision_Metadata{}
}
response, err := api.Provision(ctx)
require.NoError(t, err)
err = response.Send(request)
require.NoError(t, err)
var (
gotExpectedLog = testCase.ExpectLogContains == ""
)
gotExpectedLog := testCase.ExpectLogContains == ""
for {
msg, err := response.Recv()
if msg != nil && msg.GetLog() != nil {
if testCase.ExpectLogContains != "" && strings.Contains(msg.GetLog().Output, testCase.ExpectLogContains) {
gotExpectedLog = true
provision := func(req *proto.Provision_Request) *proto.Provision_Complete {
response, err := api.Provision(ctx)
require.NoError(t, err)
err = response.Send(req)
require.NoError(t, err)
var complete *proto.Provision_Complete
for {
msg, err := response.Recv()
if msg != nil && msg.GetLog() != nil {
if testCase.ExpectLogContains != "" && strings.Contains(msg.GetLog().Output, testCase.ExpectLogContains) {
gotExpectedLog = true
}
t.Logf("log: [%s] %s", msg.GetLog().Level, msg.GetLog().Output)
continue
}
if testCase.ErrorContains != "" {
require.ErrorContains(t, err, testCase.ErrorContains)
break
}
require.NoError(t, err)
if complete = msg.GetComplete(); complete == nil {
continue
}
t.Logf("log: [%s] %s", msg.GetLog().Level, msg.GetLog().Output)
continue
}
if testCase.ErrorContains != "" {
require.ErrorContains(t, err, testCase.ErrorContains)
require.NoError(t, err)
// Remove randomly generated data.
for _, resource := range msg.GetComplete().Resources {
sort.Slice(resource.Agents, func(i, j int) bool {
return resource.Agents[i].Name < resource.Agents[j].Name
})
for _, agent := range resource.Agents {
agent.Id = ""
if agent.GetToken() == "" {
continue
}
agent.Auth = &proto.Agent_Token{}
}
}
if testCase.Response != nil {
resourcesGot, err := json.Marshal(msg.GetComplete().Resources)
require.NoError(t, err)
resourcesWant, err := json.Marshal(testCase.Response.GetComplete().Resources)
require.NoError(t, err)
require.Equal(t, testCase.Response.GetComplete().Error, msg.GetComplete().Error)
require.Equal(t, string(resourcesWant), string(resourcesGot))
}
break
}
require.NoError(t, err)
if msg.GetComplete() == nil {
continue
}
return complete
}
require.NoError(t, err)
planComplete := provision(planRequest)
// Remove randomly generated data.
for _, resource := range msg.GetComplete().Resources {
sort.Slice(resource.Agents, func(i, j int) bool {
return resource.Agents[i].Name < resource.Agents[j].Name
})
for _, agent := range resource.Agents {
agent.Id = ""
if agent.GetToken() == "" {
continue
}
agent.Auth = &proto.Agent_Token{}
}
}
if testCase.Response != nil {
resourcesGot, err := json.Marshal(msg.GetComplete().Resources)
require.NoError(t, err)
resourcesWant, err := json.Marshal(testCase.Response.GetComplete().Resources)
require.NoError(t, err)
require.Equal(t, testCase.Response.GetComplete().Error, msg.GetComplete().Error)
require.Equal(t, string(resourcesWant), string(resourcesGot))
}
break
if testCase.Apply {
require.NotNil(t, planComplete.Plan)
provision(&proto.Provision_Request{
Type: &proto.Provision_Request_Apply{
Apply: &proto.Provision_Apply{
Config: planRequest.GetPlan().GetConfig(),
Plan: planComplete.Plan,
},
},
})
}
if !gotExpectedLog {
@@ -430,11 +460,13 @@ func TestProvision_ExtraEnv(t *testing.T) {
require.NoError(t, err)
request := &proto.Provision_Request{
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
Directory: directory,
Metadata: &proto.Provision_Metadata{
WorkspaceTransition: proto.WorkspaceTransition_START,
Type: &proto.Provision_Request_Plan{
Plan: &proto.Provision_Plan{
Config: &proto.Provision_Config{
Directory: directory,
Metadata: &proto.Provision_Metadata{
WorkspaceTransition: proto.WorkspaceTransition_START,
},
},
},
},
@@ -493,45 +525,43 @@ func TestProvision_SafeEnv(t *testing.T) {
err := os.WriteFile(path, []byte(echoResource), 0o600)
require.NoError(t, err)
request := &proto.Provision_Request{
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
Directory: directory,
Metadata: &proto.Provision_Metadata{
WorkspaceTransition: proto.WorkspaceTransition_START,
response, err := api.Provision(ctx)
require.NoError(t, err)
err = response.Send(&proto.Provision_Request{
Type: &proto.Provision_Request_Plan{
Plan: &proto.Provision_Plan{
Config: &proto.Provision_Config{
Directory: directory,
Metadata: &proto.Provision_Metadata{
WorkspaceTransition: proto.WorkspaceTransition_START,
},
},
},
},
}
response, err := api.Provision(ctx)
})
require.NoError(t, err)
err = response.Send(request)
require.NoError(t, err)
var (
foundUserEnv = false
// Some CODER_ environment variables used by our Terraform provider
// must make it through.
foundCoderEnv = false
)
for {
msg, err := response.Recv()
require.NoError(t, err)
if log := msg.GetLog(); log != nil {
t.Log(log.Level.String(), log.Output)
if strings.Contains(log.Output, passedValue) {
foundUserEnv = true
}
if strings.Contains(log.Output, "CODER_") {
foundCoderEnv = true
}
require.NotContains(t, log.Output, secretValue)
}
if c := msg.GetComplete(); c != nil {
require.Empty(t, c.Error)
break
}
}
require.True(t, foundUserEnv)
require.True(t, foundCoderEnv)
_, complete := readProvisionLog(t, response)
response, err = api.Provision(ctx)
require.NoError(t, err)
err = response.Send(&proto.Provision_Request{
Type: &proto.Provision_Request_Apply{
Apply: &proto.Provision_Apply{
Config: &proto.Provision_Config{
Directory: directory,
Metadata: &proto.Provision_Metadata{
WorkspaceTransition: proto.WorkspaceTransition_START,
},
},
Plan: complete.GetPlan(),
},
},
})
require.NoError(t, err)
log, _ := readProvisionLog(t, response)
require.Contains(t, log, passedValue)
require.NotContains(t, log, secretValue)
require.Contains(t, log, "CODER_")
}