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