mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user