github.com/magodo/terraform@v0.11.12-beta1/main.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "log" 8 "os" 9 "path/filepath" 10 "runtime" 11 "strings" 12 "sync" 13 14 "github.com/hashicorp/go-plugin" 15 backendInit "github.com/hashicorp/terraform/backend/init" 16 "github.com/hashicorp/terraform/command/format" 17 "github.com/hashicorp/terraform/helper/logging" 18 "github.com/hashicorp/terraform/svchost/disco" 19 "github.com/hashicorp/terraform/terraform" 20 "github.com/mattn/go-colorable" 21 "github.com/mattn/go-shellwords" 22 "github.com/mitchellh/cli" 23 "github.com/mitchellh/colorstring" 24 "github.com/mitchellh/panicwrap" 25 "github.com/mitchellh/prefixedio" 26 ) 27 28 const ( 29 // EnvCLI is the environment variable name to set additional CLI args. 30 EnvCLI = "TF_CLI_ARGS" 31 ) 32 33 func main() { 34 // Override global prefix set by go-dynect during init() 35 log.SetPrefix("") 36 os.Exit(realMain()) 37 } 38 39 func realMain() int { 40 var wrapConfig panicwrap.WrapConfig 41 42 // don't re-exec terraform as a child process for easier debugging 43 if os.Getenv("TF_FORK") == "0" { 44 return wrappedMain() 45 } 46 47 if !panicwrap.Wrapped(&wrapConfig) { 48 // Determine where logs should go in general (requested by the user) 49 logWriter, err := logging.LogOutput() 50 if err != nil { 51 fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err) 52 return 1 53 } 54 55 // We always send logs to a temporary file that we use in case 56 // there is a panic. Otherwise, we delete it. 57 logTempFile, err := ioutil.TempFile("", "terraform-log") 58 if err != nil { 59 fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err) 60 return 1 61 } 62 defer os.Remove(logTempFile.Name()) 63 defer logTempFile.Close() 64 65 // Setup the prefixed readers that send data properly to 66 // stdout/stderr. 67 doneCh := make(chan struct{}) 68 outR, outW := io.Pipe() 69 go copyOutput(outR, doneCh) 70 71 // Create the configuration for panicwrap and wrap our executable 72 wrapConfig.Handler = panicHandler(logTempFile) 73 wrapConfig.Writer = io.MultiWriter(logTempFile, logWriter) 74 wrapConfig.Stdout = outW 75 wrapConfig.IgnoreSignals = ignoreSignals 76 wrapConfig.ForwardSignals = forwardSignals 77 exitStatus, err := panicwrap.Wrap(&wrapConfig) 78 if err != nil { 79 fmt.Fprintf(os.Stderr, "Couldn't start Terraform: %s", err) 80 return 1 81 } 82 83 // If >= 0, we're the parent, so just exit 84 if exitStatus >= 0 { 85 // Close the stdout writer so that our copy process can finish 86 outW.Close() 87 88 // Wait for the output copying to finish 89 <-doneCh 90 91 return exitStatus 92 } 93 94 // We're the child, so just close the tempfile we made in order to 95 // save file handles since the tempfile is only used by the parent. 96 logTempFile.Close() 97 } 98 99 // Call the real main 100 return wrappedMain() 101 } 102 103 func init() { 104 Ui = &cli.PrefixedUi{ 105 AskPrefix: OutputPrefix, 106 OutputPrefix: OutputPrefix, 107 InfoPrefix: OutputPrefix, 108 ErrorPrefix: ErrorPrefix, 109 Ui: &cli.BasicUi{Writer: os.Stdout}, 110 } 111 } 112 113 func wrappedMain() int { 114 var err error 115 116 // We always need to close the DebugInfo before we exit. 117 defer terraform.CloseDebugInfo() 118 119 log.SetOutput(os.Stderr) 120 log.Printf( 121 "[INFO] Terraform version: %s %s %s", 122 Version, VersionPrerelease, GitCommit) 123 log.Printf("[INFO] Go runtime version: %s", runtime.Version()) 124 log.Printf("[INFO] CLI args: %#v", os.Args) 125 126 config, diags := LoadConfig() 127 if len(diags) > 0 { 128 // Since we haven't instantiated a command.Meta yet, we need to do 129 // some things manually here and use some "safe" defaults for things 130 // that command.Meta could otherwise figure out in smarter ways. 131 Ui.Error("There are some problems with the CLI configuration:") 132 for _, diag := range diags { 133 earlyColor := &colorstring.Colorize{ 134 Colors: colorstring.DefaultColors, 135 Disable: true, // Disable color to be conservative until we know better 136 Reset: true, 137 } 138 Ui.Error(format.Diagnostic(diag, earlyColor, 78)) 139 } 140 if diags.HasErrors() { 141 Ui.Error("As a result of the above problems, Terraform may not behave as intended.\n\n") 142 // We continue to run anyway, since Terraform has reasonable defaults. 143 } 144 } 145 146 // Get any configured credentials from the config and initialize 147 // a service discovery object. 148 credsSrc := credentialsSource(config) 149 services := disco.NewWithCredentialsSource(credsSrc) 150 151 // Initialize the backends. 152 backendInit.Init(services) 153 154 // In tests, Commands may already be set to provide mock commands 155 if Commands == nil { 156 initCommands(config, services) 157 } 158 159 // Run checkpoint 160 go runCheckpoint(config) 161 162 // Make sure we clean up any managed plugins at the end of this 163 defer plugin.CleanupClients() 164 165 // Get the command line args. 166 binName := filepath.Base(os.Args[0]) 167 args := os.Args[1:] 168 169 // Build the CLI so far, we do this so we can query the subcommand. 170 cliRunner := &cli.CLI{ 171 Args: args, 172 Commands: Commands, 173 HelpFunc: helpFunc, 174 HelpWriter: os.Stdout, 175 } 176 177 // Prefix the args with any args from the EnvCLI 178 args, err = mergeEnvArgs(EnvCLI, cliRunner.Subcommand(), args) 179 if err != nil { 180 Ui.Error(err.Error()) 181 return 1 182 } 183 184 // Prefix the args with any args from the EnvCLI targeting this command 185 suffix := strings.Replace(strings.Replace( 186 cliRunner.Subcommand(), "-", "_", -1), " ", "_", -1) 187 args, err = mergeEnvArgs( 188 fmt.Sprintf("%s_%s", EnvCLI, suffix), cliRunner.Subcommand(), args) 189 if err != nil { 190 Ui.Error(err.Error()) 191 return 1 192 } 193 194 // We shortcut "--version" and "-v" to just show the version 195 for _, arg := range args { 196 if arg == "-v" || arg == "-version" || arg == "--version" { 197 newArgs := make([]string, len(args)+1) 198 newArgs[0] = "version" 199 copy(newArgs[1:], args) 200 args = newArgs 201 break 202 } 203 } 204 205 // Rebuild the CLI with any modified args. 206 log.Printf("[INFO] CLI command args: %#v", args) 207 cliRunner = &cli.CLI{ 208 Name: binName, 209 Args: args, 210 Commands: Commands, 211 HelpFunc: helpFunc, 212 HelpWriter: os.Stdout, 213 214 Autocomplete: true, 215 AutocompleteInstall: "install-autocomplete", 216 AutocompleteUninstall: "uninstall-autocomplete", 217 } 218 219 // Pass in the overriding plugin paths from config 220 PluginOverrides.Providers = config.Providers 221 PluginOverrides.Provisioners = config.Provisioners 222 223 exitCode, err := cliRunner.Run() 224 if err != nil { 225 Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error())) 226 return 1 227 } 228 229 return exitCode 230 } 231 232 func cliConfigFile() (string, error) { 233 mustExist := true 234 configFilePath := os.Getenv("TERRAFORM_CONFIG") 235 if configFilePath == "" { 236 var err error 237 configFilePath, err = ConfigFile() 238 mustExist = false 239 240 if err != nil { 241 log.Printf( 242 "[ERROR] Error detecting default CLI config file path: %s", 243 err) 244 } 245 } 246 247 log.Printf("[DEBUG] Attempting to open CLI config file: %s", configFilePath) 248 f, err := os.Open(configFilePath) 249 if err == nil { 250 f.Close() 251 return configFilePath, nil 252 } 253 254 if mustExist || !os.IsNotExist(err) { 255 return "", err 256 } 257 258 log.Println("[DEBUG] File doesn't exist, but doesn't need to. Ignoring.") 259 return "", nil 260 } 261 262 // copyOutput uses output prefixes to determine whether data on stdout 263 // should go to stdout or stderr. This is due to panicwrap using stderr 264 // as the log and error channel. 265 func copyOutput(r io.Reader, doneCh chan<- struct{}) { 266 defer close(doneCh) 267 268 pr, err := prefixedio.NewReader(r) 269 if err != nil { 270 panic(err) 271 } 272 273 stderrR, err := pr.Prefix(ErrorPrefix) 274 if err != nil { 275 panic(err) 276 } 277 stdoutR, err := pr.Prefix(OutputPrefix) 278 if err != nil { 279 panic(err) 280 } 281 defaultR, err := pr.Prefix("") 282 if err != nil { 283 panic(err) 284 } 285 286 var stdout io.Writer = os.Stdout 287 var stderr io.Writer = os.Stderr 288 289 if runtime.GOOS == "windows" { 290 stdout = colorable.NewColorableStdout() 291 stderr = colorable.NewColorableStderr() 292 293 // colorable is not concurrency-safe when stdout and stderr are the 294 // same console, so we need to add some synchronization to ensure that 295 // we can't be concurrently writing to both stderr and stdout at 296 // once, or else we get intermingled writes that create gibberish 297 // in the console. 298 wrapped := synchronizedWriters(stdout, stderr) 299 stdout = wrapped[0] 300 stderr = wrapped[1] 301 } 302 303 var wg sync.WaitGroup 304 wg.Add(3) 305 go func() { 306 defer wg.Done() 307 io.Copy(stderr, stderrR) 308 }() 309 go func() { 310 defer wg.Done() 311 io.Copy(stdout, stdoutR) 312 }() 313 go func() { 314 defer wg.Done() 315 io.Copy(stdout, defaultR) 316 }() 317 318 wg.Wait() 319 } 320 321 func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) { 322 v := os.Getenv(envName) 323 if v == "" { 324 return args, nil 325 } 326 327 log.Printf("[INFO] %s value: %q", envName, v) 328 extra, err := shellwords.Parse(v) 329 if err != nil { 330 return nil, fmt.Errorf( 331 "Error parsing extra CLI args from %s: %s", 332 envName, err) 333 } 334 335 // Find the command to look for in the args. If there is a space, 336 // we need to find the last part. 337 search := cmd 338 if idx := strings.LastIndex(search, " "); idx >= 0 { 339 search = cmd[idx+1:] 340 } 341 342 // Find the index to place the flags. We put them exactly 343 // after the first non-flag arg. 344 idx := -1 345 for i, v := range args { 346 if v == search { 347 idx = i 348 break 349 } 350 } 351 352 // idx points to the exact arg that isn't a flag. We increment 353 // by one so that all the copying below expects idx to be the 354 // insertion point. 355 idx++ 356 357 // Copy the args 358 newArgs := make([]string, len(args)+len(extra)) 359 copy(newArgs, args[:idx]) 360 copy(newArgs[idx:], extra) 361 copy(newArgs[len(extra)+idx:], args[idx:]) 362 return newArgs, nil 363 }