feat: Add templates to create working release (#422)

* Add templates

* Move API structs to codersdk

* Back to green tests!

* It all works, but now with tea! 🧋

* It works!

* Add cancellation to provisionerd

* Tests pass!

* Add deletion of workspaces and projects

* Fix agent lock

* Add clog

* Fix linting errors

* Remove unused CLI tests

* Rename daemon to start

* Fix leaking command

* Fix promptui test

* Update agent connection frequency

* Skip login tests on Windows

* Increase tunnel connect timeout

* Fix templater

* Lower test requirements

* Fix embed

* Disable promptui tests for Windows

* Fix write newline

* Fix PTY write newline

* Fix CloseReader

* Fix compilation on Windows

* Fix linting error

* Remove bubbletea

* Cleanup readwriter

* Use embedded templates instead of serving over API

* Move templates to examples

* Improve workspace create flow

* Fix Windows build

* Fix tests

* Fix linting errors

* Fix untar with extracting max size

* Fix newline char
This commit is contained in:
Kyle Carberry
2022-03-22 13:17:50 -06:00
committed by GitHub
parent 2818b3ce6d
commit c451f4e685
138 changed files with 7314 additions and 2331 deletions
+6 -1
View File
@@ -73,7 +73,12 @@ func (*echo) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_Pa
}
// Provision reads requests from the provided directory to stream responses.
func (*echo) Provision(request *proto.Provision_Request, stream proto.DRPCProvisioner_ProvisionStream) error {
func (*echo) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
msg, err := stream.Recv()
if err != nil {
return err
}
request := msg.GetStart()
for index := 0; ; index++ {
extension := ".protobuf"
if request.DryRun {
+8 -2
View File
@@ -92,8 +92,14 @@ func TestEcho(t *testing.T) {
Provision: responses,
})
require.NoError(t, err)
client, err := api.Provision(ctx, &proto.Provision_Request{
Directory: unpackTar(t, data),
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, data),
},
},
})
require.NoError(t, err)
log, err := client.Recv()
+10 -4
View File
@@ -45,6 +45,7 @@ func convertVariableToParameter(variable *tfconfig.Variable) (*proto.ParameterSc
Name: variable.Name,
Description: variable.Description,
RedisplayValue: !variable.Sensitive,
AllowOverrideSource: !variable.Sensitive,
ValidationValueType: variable.Type,
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
@@ -52,13 +53,18 @@ func convertVariableToParameter(variable *tfconfig.Variable) (*proto.ParameterSc
}
if variable.Default != nil {
defaultData, err := json.Marshal(variable.Default)
if err != nil {
return nil, xerrors.Errorf("parse variable %q default: %w", variable.Name, err)
defaultData, valid := variable.Default.(string)
if !valid {
defaultDataRaw, err := json.Marshal(variable.Default)
if err != nil {
return nil, xerrors.Errorf("parse variable %q default: %w", variable.Name, err)
}
defaultData = string(defaultDataRaw)
}
schema.DefaultSource = &proto.ParameterSource{
Scheme: proto.ParameterSource_DATA,
Value: string(defaultData),
Value: defaultData,
}
}
+9 -6
View File
@@ -52,9 +52,10 @@ func TestParse(t *testing.T) {
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "A",
RedisplayValue: true,
Description: "Testing!",
Name: "A",
RedisplayValue: true,
AllowOverrideSource: true,
Description: "Testing!",
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
@@ -73,11 +74,12 @@ func TestParse(t *testing.T) {
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "A",
RedisplayValue: true,
Name: "A",
RedisplayValue: true,
AllowOverrideSource: true,
DefaultSource: &proto.ParameterSource{
Scheme: proto.ParameterSource_DATA,
Value: "\"wow\"",
Value: "wow",
},
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
@@ -103,6 +105,7 @@ func TestParse(t *testing.T) {
RedisplayValue: true,
ValidationCondition: `var.A == "value"`,
ValidationTypeSystem: proto.ParameterSchema_HCL,
AllowOverrideSource: true,
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
+163 -181
View File
@@ -17,27 +17,54 @@ import (
"github.com/mitchellh/mapstructure"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/provisionersdk"
"github.com/coder/coder/provisionersdk/proto"
)
// Provision executes `terraform apply`.
func (t *terraform) Provision(request *proto.Provision_Request, stream proto.DRPCProvisioner_ProvisionStream) error {
ctx := stream.Context()
statefilePath := filepath.Join(request.Directory, "terraform.tfstate")
if len(request.State) > 0 {
err := os.WriteFile(statefilePath, request.State, 0600)
func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
shutdown, shutdownFunc := context.WithCancel(stream.Context())
defer shutdownFunc()
request, err := stream.Recv()
if err != nil {
return err
}
if request.GetCancel() != nil {
return nil
}
// We expect the first message is start!
if request.GetStart() == nil {
return nil
}
go func() {
for {
request, err := stream.Recv()
if err != nil {
return
}
if request.GetCancel() == nil {
// This is only to process cancels!
continue
}
shutdownFunc()
return
}
}()
start := request.GetStart()
statefilePath := filepath.Join(start.Directory, "terraform.tfstate")
if len(start.State) > 0 {
err := os.WriteFile(statefilePath, start.State, 0600)
if err != nil {
return xerrors.Errorf("write statefile %q: %w", statefilePath, err)
}
}
terraform, err := tfexec.NewTerraform(request.Directory, t.binaryPath)
terraform, err := tfexec.NewTerraform(start.Directory, t.binaryPath)
if err != nil {
return xerrors.Errorf("create new terraform executor: %w", err)
}
version, _, err := terraform.Version(ctx, false)
version, _, err := terraform.Version(shutdown, false)
if err != nil {
return xerrors.Errorf("get terraform version: %w", err)
}
@@ -62,48 +89,39 @@ func (t *terraform) Provision(request *proto.Provision_Request, stream proto.DRP
}
}()
terraform.SetStdout(writer)
t.logger.Debug(ctx, "running initialization")
err = terraform.Init(ctx)
t.logger.Debug(shutdown, "running initialization")
err = terraform.Init(shutdown)
if err != nil {
return xerrors.Errorf("initialize terraform: %w", err)
}
t.logger.Debug(ctx, "ran initialization")
t.logger.Debug(shutdown, "ran initialization")
_ = reader.Close()
terraform.SetStdout(io.Discard)
if request.DryRun {
return t.runTerraformPlan(ctx, terraform, request, stream)
env := os.Environ()
env = append(env,
"CODER_URL="+start.Metadata.CoderUrl,
"CODER_WORKSPACE_TRANSITION="+strings.ToLower(start.Metadata.WorkspaceTransition.String()),
)
for key, value := range provisionersdk.AgentScriptEnv() {
env = append(env, key+"="+value)
}
return t.runTerraformApply(ctx, terraform, request, stream, statefilePath)
}
func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, request *proto.Provision_Request, stream proto.DRPCProvisioner_ProvisionStream) error {
env := map[string]string{}
for _, envEntry := range os.Environ() {
parts := strings.SplitN(envEntry, "=", 2)
env[parts[0]] = parts[1]
}
env["CODER_URL"] = request.Metadata.CoderUrl
env["CODER_WORKSPACE_TRANSITION"] = strings.ToLower(request.Metadata.WorkspaceTransition.String())
planfilePath := filepath.Join(request.Directory, "terraform.tfplan")
options := []tfexec.PlanOption{tfexec.JSON(true), tfexec.Out(planfilePath)}
for _, param := range request.ParameterValues {
vars := []string{}
for _, param := range start.ParameterValues {
switch param.DestinationScheme {
case proto.ParameterDestination_ENVIRONMENT_VARIABLE:
env[param.Name] = param.Value
env = append(env, fmt.Sprintf("%s=%s", param.Name, param.Value))
case proto.ParameterDestination_PROVISIONER_VARIABLE:
options = append(options, tfexec.Var(fmt.Sprintf("%s=%s", param.Name, param.Value)))
vars = append(vars, fmt.Sprintf("%s=%s", param.Name, param.Value))
default:
return xerrors.Errorf("unsupported parameter type %q for %q", param.DestinationScheme, param.Name)
}
}
err := terraform.SetEnv(env)
if err != nil {
return xerrors.Errorf("apply environment variables: %w", err)
}
reader, writer := io.Pipe()
closeChan := make(chan struct{})
reader, writer = io.Pipe()
defer reader.Close()
defer writer.Close()
closeChan := make(chan struct{})
go func() {
defer close(closeChan)
decoder := json.NewDecoder(reader)
@@ -148,21 +166,88 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
}
}()
terraform.SetStdout(writer)
t.logger.Debug(ctx, "running plan")
_, err = terraform.Plan(ctx, options...)
if err != nil {
return xerrors.Errorf("plan terraform: %w", err)
planfilePath := filepath.Join(start.Directory, "terraform.tfplan")
var args []string
if start.DryRun {
args = []string{
"plan",
"-no-color",
"-input=false",
"-json",
"-refresh=true",
"-out=" + planfilePath,
}
} else {
args = []string{
"apply",
"-no-color",
"-auto-approve",
"-input=false",
"-json",
"-refresh=true",
}
}
t.logger.Debug(ctx, "ran plan")
plan, err := terraform.ShowPlanFile(ctx, planfilePath)
if start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY {
args = append(args, "-destroy")
}
for _, variable := range vars {
args = append(args, "-var", variable)
}
// #nosec
cmd := exec.CommandContext(stream.Context(), t.binaryPath, args...)
go func() {
select {
case <-stream.Context().Done():
return
case <-shutdown.Done():
_ = cmd.Process.Signal(os.Kill)
}
}()
cmd.Stdout = writer
cmd.Env = env
cmd.Dir = terraform.WorkingDir()
err = cmd.Run()
if err != nil {
return xerrors.Errorf("show terraform plan file: %w", err)
if start.DryRun {
return xerrors.Errorf("plan terraform: %w", err)
}
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.
statefileContent, err := os.ReadFile(statefilePath)
if err != nil {
return xerrors.Errorf("read file %q: %w", statefilePath, err)
}
return stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
State: statefileContent,
Error: errorMessage,
},
},
})
}
_ = reader.Close()
<-closeChan
var resp *proto.Provision_Response
if start.DryRun {
resp, err = parseTerraformPlan(stream.Context(), terraform, planfilePath)
} else {
resp, err = parseTerraformApply(stream.Context(), terraform, statefilePath)
}
if err != nil {
return err
}
return stream.Send(resp)
}
func parseTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, planfilePath string) (*proto.Provision_Response, error) {
plan, err := terraform.ShowPlanFile(ctx, planfilePath)
if err != nil {
return nil, xerrors.Errorf("show terraform plan file: %w", err)
}
// Maps resource dependencies to expression references.
// This is *required* for a plan, because "DependsOn"
// does not propagate.
@@ -200,14 +285,14 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
if envRaw, has := resource.Expressions["env"]; has {
env, ok := envRaw.ConstantValue.(map[string]string)
if !ok {
return xerrors.Errorf("unexpected type %q for env map", reflect.TypeOf(envRaw.ConstantValue).String())
return nil, xerrors.Errorf("unexpected type %q for env map", reflect.TypeOf(envRaw.ConstantValue).String())
}
agent.Env = env
}
if startupScriptRaw, has := resource.Expressions["startup_script"]; has {
startupScript, ok := startupScriptRaw.ConstantValue.(string)
if !ok {
return xerrors.Errorf("unexpected type %q for startup script", reflect.TypeOf(startupScriptRaw.ConstantValue).String())
return nil, xerrors.Errorf("unexpected type %q for startup script", reflect.TypeOf(startupScriptRaw.ConstantValue).String())
}
agent.StartupScript = startupScript
}
@@ -218,7 +303,7 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
if has {
authTypeValue, valid := authType.ConstantValue.(string)
if !valid {
return xerrors.Errorf("unexpected type %q for auth type", reflect.TypeOf(authType.ConstantValue))
return nil, xerrors.Errorf("unexpected type %q for auth type", reflect.TypeOf(authType.ConstantValue))
}
switch authTypeValue {
case "google-instance-identity":
@@ -229,7 +314,7 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
},
}
default:
return xerrors.Errorf("unknown auth type: %q", authTypeValue)
return nil, xerrors.Errorf("unknown auth type: %q", authTypeValue)
}
}
}
@@ -273,107 +358,23 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
})
}
return stream.Send(&proto.Provision_Response{
return &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: resources,
},
},
})
}, nil
}
func (t *terraform) runTerraformApply(ctx context.Context, terraform *tfexec.Terraform, request *proto.Provision_Request, stream proto.DRPCProvisioner_ProvisionStream, statefilePath string) error {
env := os.Environ()
env = append(env,
"CODER_URL="+request.Metadata.CoderUrl,
"CODER_WORKSPACE_TRANSITION="+strings.ToLower(request.Metadata.WorkspaceTransition.String()),
)
vars := []string{}
for _, param := range request.ParameterValues {
switch param.DestinationScheme {
case proto.ParameterDestination_ENVIRONMENT_VARIABLE:
env = append(env, fmt.Sprintf("%s=%s", param.Name, param.Value))
case proto.ParameterDestination_PROVISIONER_VARIABLE:
vars = append(vars, fmt.Sprintf("%s=%s", param.Name, param.Value))
default:
return xerrors.Errorf("unsupported parameter type %q for %q", param.DestinationScheme, param.Name)
}
}
reader, writer := io.Pipe()
defer reader.Close()
defer writer.Close()
go func() {
decoder := json.NewDecoder(reader)
for {
var log terraformProvisionLog
err := decoder.Decode(&log)
if err != nil {
return
}
logLevel, err := convertTerraformLogLevel(log.Level)
if err != nil {
// Not a big deal, but we should handle this at some point!
continue
}
_ = stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: logLevel,
Output: log.Message,
},
},
})
if log.Diagnostic == nil {
continue
}
// If the diagnostic is provided, let's provide a bit more info!
logLevel, err = convertTerraformLogLevel(log.Diagnostic.Severity)
if err != nil {
continue
}
_ = stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: logLevel,
Output: log.Diagnostic.Detail,
},
},
})
}
}()
t.logger.Debug(ctx, "running apply", slog.F("vars", len(vars)), slog.F("env", len(env)))
err := runApplyCommand(ctx, t.shutdownCtx, request.Metadata.WorkspaceTransition, terraform.ExecPath(), terraform.WorkingDir(), writer, env, vars)
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.
statefileContent, err := os.ReadFile(statefilePath)
if err != nil {
return xerrors.Errorf("read file %q: %w", statefilePath, err)
}
return stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
State: statefileContent,
Error: errorMessage,
},
},
})
}
t.logger.Debug(ctx, "ran apply")
func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, statefilePath string) (*proto.Provision_Response, error) {
statefileContent, err := os.ReadFile(statefilePath)
if err != nil {
return xerrors.Errorf("read file %q: %w", statefilePath, err)
return nil, xerrors.Errorf("read file %q: %w", statefilePath, err)
}
state, err := terraform.ShowStateFile(ctx, statefilePath)
if err != nil {
return xerrors.Errorf("show state file %q: %w", statefilePath, err)
return nil, xerrors.Errorf("show state file %q: %w", statefilePath, err)
}
resources := make([]*proto.Resource, 0)
if state.Values != nil {
@@ -398,7 +399,7 @@ func (t *terraform) runTerraformApply(ctx context.Context, terraform *tfexec.Ter
var attrs agentAttributes
err = mapstructure.Decode(resource.AttributeValues, &attrs)
if err != nil {
return xerrors.Errorf("decode agent attributes: %w", err)
return nil, xerrors.Errorf("decode agent attributes: %w", err)
}
agent := &proto.Agent{
Id: attrs.ID,
@@ -418,7 +419,7 @@ func (t *terraform) runTerraformApply(ctx context.Context, terraform *tfexec.Ter
},
}
default:
return xerrors.Errorf("unknown auth type: %q", auth.Type)
return nil, xerrors.Errorf("unknown auth type: %q", auth.Type)
}
}
resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".")
@@ -439,14 +440,27 @@ func (t *terraform) runTerraformApply(ctx context.Context, terraform *tfexec.Ter
break
}
}
// Associate resources where the agent depends on it.
for agentKey, dependsOn := range agentDepends {
for _, depend := range dependsOn {
if depend != strings.Join([]string{resource.Type, resource.Name}, ".") {
continue
if agent == nil {
// Associate resources where the agent depends on it.
for agentKey, dependsOn := range agentDepends {
for _, depend := range dependsOn {
if depend != strings.Join([]string{resource.Type, resource.Name}, ".") {
continue
}
agent = agents[agentKey]
break
}
}
}
if agent != nil {
if agent.GetGoogleInstanceIdentity() != nil {
// Make sure the instance has an instance ID!
_, exists := resource.AttributeValues["instance_id"]
if !exists {
// This was a mistake!
agent = nil
}
agent = agents[agentKey]
break
}
}
@@ -458,46 +472,14 @@ func (t *terraform) runTerraformApply(ctx context.Context, terraform *tfexec.Ter
}
}
return stream.Send(&proto.Provision_Response{
return &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
State: statefileContent,
Resources: resources,
},
},
})
}
// This couldn't use terraform-exec, because it doesn't support cancellation, and there didn't appear
// to be a straight-forward way to add it.
func runApplyCommand(ctx, shutdownCtx context.Context, transition proto.WorkspaceTransition, bin, dir string, stdout io.Writer, env, vars []string) error {
args := []string{
"apply",
"-no-color",
"-auto-approve",
"-input=false",
"-json",
"-refresh=true",
}
if transition == proto.WorkspaceTransition_DESTROY {
args = append(args, "-destroy")
}
for _, variable := range vars {
args = append(args, "-var", variable)
}
cmd := exec.CommandContext(ctx, bin, args...)
go func() {
select {
case <-ctx.Done():
return
case <-shutdownCtx.Done():
_ = cmd.Process.Signal(os.Kill)
}
}()
cmd.Stdout = stdout
cmd.Env = env
cmd.Dir = dir
return cmd.Run()
}, nil
}
type terraformProvisionLog struct {
+55 -32
View File
@@ -69,11 +69,15 @@ provider "coder" {
}`,
},
Request: &proto.Provision_Request{
ParameterValues: []*proto.ParameterValue{{
DestinationScheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
Name: "A",
Value: "example",
}},
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
ParameterValues: []*proto.ParameterValue{{
DestinationScheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
Name: "A",
Value: "example",
}},
},
},
},
Response: &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
@@ -114,7 +118,11 @@ provider "coder" {
"main.tf": `resource "null_resource" "A" {}`,
},
Request: &proto.Provision_Request{
DryRun: true,
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
DryRun: true,
},
},
},
Response: &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
@@ -156,8 +164,12 @@ provider "coder" {
}`,
},
Request: &proto.Provision_Request{
Metadata: &proto.Provision_Metadata{
CoderUrl: "https://example.com",
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
Metadata: &proto.Provision_Metadata{
CoderUrl: "https://example.com",
},
},
},
},
Response: &proto.Provision_Response{
@@ -191,8 +203,12 @@ provider "coder" {
resource "null_resource" "A" {}`,
},
Request: &proto.Provision_Request{
Metadata: &proto.Provision_Metadata{
CoderUrl: "https://example.com",
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
Metadata: &proto.Provision_Metadata{
CoderUrl: "https://example.com",
},
},
},
},
Response: &proto.Provision_Response{
@@ -201,13 +217,6 @@ provider "coder" {
Resources: []*proto.Resource{{
Name: "A",
Type: "null_resource",
Agent: &proto.Agent{
Auth: &proto.Agent_GoogleInstanceIdentity{
GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{
InstanceId: "an-instance",
},
},
},
}},
},
},
@@ -224,9 +233,13 @@ provider "coder" {
}`,
},
Request: &proto.Provision_Request{
DryRun: true,
Metadata: &proto.Provision_Metadata{
CoderUrl: "https://example.com",
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
DryRun: true,
Metadata: &proto.Provision_Metadata{
CoderUrl: "https://example.com",
},
},
},
},
Response: &proto.Provision_Response{
@@ -258,9 +271,13 @@ provider "coder" {
}`,
},
Request: &proto.Provision_Request{
DryRun: true,
Metadata: &proto.Provision_Metadata{
CoderUrl: "https://example.com",
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
DryRun: true,
Metadata: &proto.Provision_Metadata{
CoderUrl: "https://example.com",
},
},
},
},
Response: &proto.Provision_Response{
@@ -292,18 +309,24 @@ provider "coder" {
}
request := &proto.Provision_Request{
Directory: directory,
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
Directory: directory,
},
},
}
if testCase.Request != nil {
request.ParameterValues = testCase.Request.ParameterValues
request.State = testCase.Request.State
request.DryRun = testCase.Request.DryRun
request.Metadata = testCase.Request.Metadata
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 request.Metadata == nil {
request.Metadata = &proto.Provision_Metadata{}
if request.GetStart().Metadata == nil {
request.GetStart().Metadata = &proto.Provision_Metadata{}
}
response, err := api.Provision(ctx, request)
response, err := api.Provision(ctx)
require.NoError(t, err)
err = response.Send(request)
require.NoError(t, err)
for {
msg, err := response.Recv()
@@ -322,7 +345,7 @@ provider "coder" {
}
require.NoError(t, err)
if !request.DryRun {
if !request.GetStart().DryRun {
require.Greater(t, len(msg.GetComplete().State), 0)
}
+17 -17
View File
@@ -10,7 +10,9 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/provisionersdk"
"github.com/coder/coder/provisionersdk/proto"
"github.com/hashicorp/hc-install/product"
"github.com/hashicorp/hc-install/releases"
)
var (
@@ -40,29 +42,27 @@ func Serve(ctx context.Context, options *ServeOptions) error {
if options.BinaryPath == "" {
binaryPath, err := exec.LookPath("terraform")
if err != nil {
return xerrors.Errorf("terraform binary not found: %w", err)
installer := &releases.ExactVersion{
Product: product.Terraform,
Version: version.Must(version.NewVersion("1.1.7")),
}
execPath, err := installer.Install(ctx)
if err != nil {
return xerrors.Errorf("install terraform: %w", err)
}
options.BinaryPath = execPath
} else {
options.BinaryPath = binaryPath
}
options.BinaryPath = binaryPath
}
shutdownCtx, shutdownCancel := context.WithCancel(ctx)
return provisionersdk.Serve(ctx, &terraform{
binaryPath: options.BinaryPath,
logger: options.Logger,
shutdownCtx: shutdownCtx,
shutdownCancel: shutdownCancel,
binaryPath: options.BinaryPath,
logger: options.Logger,
}, options.ServeOptions)
}
type terraform struct {
binaryPath string
logger slog.Logger
shutdownCtx context.Context
shutdownCancel context.CancelFunc
}
// Shutdown signals to begin graceful shutdown of any running operations.
func (t *terraform) Shutdown(_ context.Context, _ *proto.Empty) (*proto.Empty, error) {
t.shutdownCancel()
return &proto.Empty{}, nil
}