opentofu/internal/builtin/provisioners/remote-exec/resource_provisioner.go
Martin Atkins e81162c4e1 Move provisioners/ to internal/provisioners/
This is part of a general effort to move all of Terraform's non-library
package surface under internal in order to reinforce that these are for
internal use within Terraform only.

If you were previously importing packages under this prefix into an
external codebase, you could pin to an earlier release tag as an interim
solution until you've make a plan to achieve the same functionality some
other way.
2021-05-17 14:09:07 -07:00

271 lines
6.2 KiB
Go

package remoteexec
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/communicator"
"github.com/hashicorp/terraform/internal/communicator/remote"
"github.com/hashicorp/terraform/internal/provisioners"
"github.com/mitchellh/go-linereader"
"github.com/zclconf/go-cty/cty"
)
func New() provisioners.Interface {
ctx, cancel := context.WithCancel(context.Background())
return &provisioner{
ctx: ctx,
cancel: cancel,
}
}
type provisioner struct {
// We store a context here tied to the lifetime of the provisioner.
// This allows the Stop method to cancel any in-flight requests.
ctx context.Context
cancel context.CancelFunc
}
func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"inline": {
Type: cty.List(cty.String),
Optional: true,
},
"script": {
Type: cty.String,
Optional: true,
},
"scripts": {
Type: cty.List(cty.String),
Optional: true,
},
},
}
resp.Provisioner = schema
return resp
}
func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
cfg, err := p.GetSchema().Provisioner.CoerceValue(req.Config)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
inline := cfg.GetAttr("inline")
script := cfg.GetAttr("script")
scripts := cfg.GetAttr("scripts")
set := 0
if !inline.IsNull() {
set++
}
if !script.IsNull() {
set++
}
if !scripts.IsNull() {
set++
}
if set != 1 {
resp.Diagnostics = resp.Diagnostics.Append(errors.New(
`only one of "inline", "script", or "scripts" must be set`))
}
return resp
}
func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
if req.Connection.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing connection configuration for provisioner"))
return resp
}
comm, err := communicator.New(req.Connection)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
// Collect the scripts
scripts, err := collectScripts(req.Config)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
for _, s := range scripts {
defer s.Close()
}
// Copy and execute each script
if err := runScripts(p.ctx, req.UIOutput, comm, scripts); err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
return resp
}
func (p *provisioner) Stop() error {
p.cancel()
return nil
}
func (p *provisioner) Close() error {
return nil
}
// generateScripts takes the configuration and creates a script from each inline config
func generateScripts(inline cty.Value) ([]string, error) {
var lines []string
for _, l := range inline.AsValueSlice() {
if l.IsNull() {
return nil, errors.New("invalid null string in 'scripts'")
}
s := l.AsString()
if s == "" {
return nil, errors.New("invalid empty string in 'scripts'")
}
lines = append(lines, s)
}
lines = append(lines, "")
return []string{strings.Join(lines, "\n")}, nil
}
// collectScripts is used to collect all the scripts we need
// to execute in preparation for copying them.
func collectScripts(v cty.Value) ([]io.ReadCloser, error) {
// Check if inline
if inline := v.GetAttr("inline"); !inline.IsNull() {
scripts, err := generateScripts(inline)
if err != nil {
return nil, err
}
var r []io.ReadCloser
for _, script := range scripts {
r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script))))
}
return r, nil
}
// Collect scripts
var scripts []string
if script := v.GetAttr("script"); !script.IsNull() {
s := script.AsString()
if s == "" {
return nil, errors.New("invalid empty string in 'script'")
}
scripts = append(scripts, s)
}
if scriptList := v.GetAttr("scripts"); !scriptList.IsNull() {
for _, script := range scriptList.AsValueSlice() {
if script.IsNull() {
return nil, errors.New("invalid null string in 'script'")
}
s := script.AsString()
if s == "" {
return nil, errors.New("invalid empty string in 'script'")
}
scripts = append(scripts, s)
}
}
// Open all the scripts
var fhs []io.ReadCloser
for _, s := range scripts {
fh, err := os.Open(s)
if err != nil {
for _, fh := range fhs {
fh.Close()
}
return nil, fmt.Errorf("Failed to open script '%s': %v", s, err)
}
fhs = append(fhs, fh)
}
// Done, return the file handles
return fhs, nil
}
// runScripts is used to copy and execute a set of scripts
func runScripts(ctx context.Context, o provisioners.UIOutput, comm communicator.Communicator, scripts []io.ReadCloser) error {
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel()
// Wait and retry until we establish the connection
err := communicator.Retry(retryCtx, func() error {
return comm.Connect(o)
})
if err != nil {
return err
}
// Wait for the context to end and then disconnect
go func() {
<-ctx.Done()
comm.Disconnect()
}()
for _, script := range scripts {
var cmd *remote.Cmd
outR, outW := io.Pipe()
errR, errW := io.Pipe()
defer outW.Close()
defer errW.Close()
go copyUIOutput(o, outR)
go copyUIOutput(o, errR)
remotePath := comm.ScriptPath()
if err := comm.UploadScript(remotePath, script); err != nil {
return fmt.Errorf("Failed to upload script: %v", err)
}
cmd = &remote.Cmd{
Command: remotePath,
Stdout: outW,
Stderr: errW,
}
if err := comm.Start(cmd); err != nil {
return fmt.Errorf("Error starting script: %v", err)
}
if err := cmd.Wait(); err != nil {
return err
}
// Upload a blank follow up file in the same path to prevent residual
// script contents from remaining on remote machine
empty := bytes.NewReader([]byte(""))
if err := comm.Upload(remotePath, empty); err != nil {
// This feature is best-effort.
log.Printf("[WARN] Failed to upload empty follow up script: %v", err)
}
}
return nil
}
func copyUIOutput(o provisioners.UIOutput, r io.Reader) {
lr := linereader.New(r)
for line := range lr.Ch {
o.Output(line)
}
}