github.com/magodo/terraform@v0.11.12-beta1/builtin/provisioners/local-exec/resource_provisioner.go (about) 1 package localexec 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "os" 8 "os/exec" 9 "runtime" 10 11 "github.com/armon/circbuf" 12 "github.com/hashicorp/terraform/helper/schema" 13 "github.com/hashicorp/terraform/terraform" 14 "github.com/mitchellh/go-linereader" 15 ) 16 17 const ( 18 // maxBufSize limits how much output we collect from a local 19 // invocation. This is to prevent TF memory usage from growing 20 // to an enormous amount due to a faulty process. 21 maxBufSize = 8 * 1024 22 ) 23 24 func Provisioner() terraform.ResourceProvisioner { 25 return &schema.Provisioner{ 26 Schema: map[string]*schema.Schema{ 27 "command": &schema.Schema{ 28 Type: schema.TypeString, 29 Required: true, 30 }, 31 "interpreter": &schema.Schema{ 32 Type: schema.TypeList, 33 Elem: &schema.Schema{Type: schema.TypeString}, 34 Optional: true, 35 }, 36 "working_dir": &schema.Schema{ 37 Type: schema.TypeString, 38 Optional: true, 39 }, 40 "environment": &schema.Schema{ 41 Type: schema.TypeMap, 42 Optional: true, 43 }, 44 }, 45 46 ApplyFunc: applyFn, 47 } 48 } 49 50 func applyFn(ctx context.Context) error { 51 data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) 52 o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) 53 54 command := data.Get("command").(string) 55 if command == "" { 56 return fmt.Errorf("local-exec provisioner command must be a non-empty string") 57 } 58 59 // Execute the command with env 60 environment := data.Get("environment").(map[string]interface{}) 61 62 var env []string 63 for k := range environment { 64 entry := fmt.Sprintf("%s=%s", k, environment[k].(string)) 65 env = append(env, entry) 66 } 67 68 // Execute the command using a shell 69 interpreter := data.Get("interpreter").([]interface{}) 70 71 var cmdargs []string 72 if len(interpreter) > 0 { 73 for _, i := range interpreter { 74 if arg, ok := i.(string); ok { 75 cmdargs = append(cmdargs, arg) 76 } 77 } 78 } else { 79 if runtime.GOOS == "windows" { 80 cmdargs = []string{"cmd", "/C"} 81 } else { 82 cmdargs = []string{"/bin/sh", "-c"} 83 } 84 } 85 cmdargs = append(cmdargs, command) 86 87 workingdir := data.Get("working_dir").(string) 88 89 // Setup the reader that will read the output from the command. 90 // We use an os.Pipe so that the *os.File can be passed directly to the 91 // process, and not rely on goroutines copying the data which may block. 92 // See golang.org/issue/18874 93 pr, pw, err := os.Pipe() 94 if err != nil { 95 return fmt.Errorf("failed to initialize pipe for output: %s", err) 96 } 97 98 var cmdEnv []string 99 cmdEnv = os.Environ() 100 cmdEnv = append(cmdEnv, env...) 101 102 // Setup the command 103 cmd := exec.CommandContext(ctx, cmdargs[0], cmdargs[1:]...) 104 cmd.Stderr = pw 105 cmd.Stdout = pw 106 // Dir specifies the working directory of the command. 107 // If Dir is the empty string (this is default), runs the command 108 // in the calling process's current directory. 109 cmd.Dir = workingdir 110 // Env specifies the environment of the command. 111 // By default will use the calling process's environment 112 cmd.Env = cmdEnv 113 114 output, _ := circbuf.NewBuffer(maxBufSize) 115 116 // Write everything we read from the pipe to the output buffer too 117 tee := io.TeeReader(pr, output) 118 119 // copy the teed output to the UI output 120 copyDoneCh := make(chan struct{}) 121 go copyOutput(o, tee, copyDoneCh) 122 123 // Output what we're about to run 124 o.Output(fmt.Sprintf("Executing: %q", cmdargs)) 125 126 // Start the command 127 err = cmd.Start() 128 if err == nil { 129 err = cmd.Wait() 130 } 131 132 // Close the write-end of the pipe so that the goroutine mirroring output 133 // ends properly. 134 pw.Close() 135 136 // Cancelling the command may block the pipe reader if the file descriptor 137 // was passed to a child process which hasn't closed it. In this case the 138 // copyOutput goroutine will just hang out until exit. 139 select { 140 case <-copyDoneCh: 141 case <-ctx.Done(): 142 } 143 144 if err != nil { 145 return fmt.Errorf("Error running command '%s': %v. Output: %s", 146 command, err, output.Bytes()) 147 } 148 149 return nil 150 } 151 152 func copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { 153 defer close(doneCh) 154 lr := linereader.New(r) 155 for line := range lr.Ch { 156 o.Output(line) 157 } 158 }