mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Refactor Provisioner to distinguish Plan and Apply (#5036)
This commit is contained in:
+25
-17
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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_")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user