github.com/hugorut/terraform@v1.1.3/main.go (about) 1 package main 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "net" 8 "os" 9 "path/filepath" 10 "runtime" 11 "strings" 12 13 "github.com/hashicorp/go-plugin" 14 "github.com/hashicorp/terraform-svchost/disco" 15 "github.com/hugorut/terraform/src/addrs" 16 "github.com/hugorut/terraform/src/command/cliconfig" 17 "github.com/hugorut/terraform/src/command/format" 18 "github.com/hugorut/terraform/src/didyoumean" 19 "github.com/hugorut/terraform/src/httpclient" 20 "github.com/hugorut/terraform/src/logging" 21 "github.com/hugorut/terraform/src/terminal" 22 "github.com/hugorut/terraform/version" 23 "github.com/mattn/go-shellwords" 24 "github.com/mitchellh/cli" 25 "github.com/mitchellh/colorstring" 26 27 backendInit "github.com/hugorut/terraform/src/backend/init" 28 ) 29 30 const ( 31 // EnvCLI is the environment variable name to set additional CLI args. 32 EnvCLI = "TF_CLI_ARGS" 33 34 // The parent process will create a file to collect crash logs 35 envTmpLogPath = "TF_TEMP_LOG_PATH" 36 ) 37 38 // ui wraps the primary output cli.Ui, and redirects Warn calls to Output 39 // calls. This ensures that warnings are sent to stdout, and are properly 40 // serialized within the stdout stream. 41 type ui struct { 42 cli.Ui 43 } 44 45 func (u *ui) Warn(msg string) { 46 u.Ui.Output(msg) 47 } 48 49 func init() { 50 Ui = &ui{&cli.BasicUi{ 51 Writer: os.Stdout, 52 ErrorWriter: os.Stderr, 53 Reader: os.Stdin, 54 }} 55 } 56 57 func main() { 58 os.Exit(realMain()) 59 } 60 61 func realMain() int { 62 defer logging.PanicHandler() 63 64 var err error 65 66 tmpLogPath := os.Getenv(envTmpLogPath) 67 if tmpLogPath != "" { 68 f, err := os.OpenFile(tmpLogPath, os.O_RDWR|os.O_APPEND, 0666) 69 if err == nil { 70 defer f.Close() 71 72 log.Printf("[DEBUG] Adding temp file log sink: %s", f.Name()) 73 logging.RegisterSink(f) 74 } else { 75 log.Printf("[ERROR] Could not open temp log file: %v", err) 76 } 77 } 78 79 log.Printf( 80 "[INFO] Terraform version: %s %s", 81 Version, VersionPrerelease) 82 for _, depMod := range version.InterestingDependencies() { 83 log.Printf("[DEBUG] using %s %s", depMod.Path, depMod.Version) 84 } 85 log.Printf("[INFO] Go runtime version: %s", runtime.Version()) 86 log.Printf("[INFO] CLI args: %#v", os.Args) 87 88 streams, err := terminal.Init() 89 if err != nil { 90 Ui.Error(fmt.Sprintf("Failed to configure the terminal: %s", err)) 91 return 1 92 } 93 if streams.Stdout.IsTerminal() { 94 log.Printf("[TRACE] Stdout is a terminal of width %d", streams.Stdout.Columns()) 95 } else { 96 log.Printf("[TRACE] Stdout is not a terminal") 97 } 98 if streams.Stderr.IsTerminal() { 99 log.Printf("[TRACE] Stderr is a terminal of width %d", streams.Stderr.Columns()) 100 } else { 101 log.Printf("[TRACE] Stderr is not a terminal") 102 } 103 if streams.Stdin.IsTerminal() { 104 log.Printf("[TRACE] Stdin is a terminal") 105 } else { 106 log.Printf("[TRACE] Stdin is not a terminal") 107 } 108 109 // NOTE: We're intentionally calling LoadConfig _before_ handling a possible 110 // -chdir=... option on the command line, so that a possible relative 111 // path in the TERRAFORM_CONFIG_FILE environment variable (though probably 112 // ill-advised) will be resolved relative to the true working directory, 113 // not the overridden one. 114 config, diags := cliconfig.LoadConfig() 115 116 if len(diags) > 0 { 117 // Since we haven't instantiated a command.Meta yet, we need to do 118 // some things manually here and use some "safe" defaults for things 119 // that command.Meta could otherwise figure out in smarter ways. 120 Ui.Error("There are some problems with the CLI configuration:") 121 for _, diag := range diags { 122 earlyColor := &colorstring.Colorize{ 123 Colors: colorstring.DefaultColors, 124 Disable: true, // Disable color to be conservative until we know better 125 Reset: true, 126 } 127 // We don't currently have access to the source code cache for 128 // the parser used to load the CLI config, so we can't show 129 // source code snippets in early diagnostics. 130 Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78)) 131 } 132 if diags.HasErrors() { 133 Ui.Error("As a result of the above problems, Terraform may not behave as intended.\n\n") 134 // We continue to run anyway, since Terraform has reasonable defaults. 135 } 136 } 137 138 // Get any configured credentials from the config and initialize 139 // a service discovery object. The slightly awkward predeclaration of 140 // disco is required to allow us to pass untyped nil as the creds source 141 // when creating the source fails. Otherwise we pass a typed nil which 142 // breaks the nil checks in the disco object 143 var services *disco.Disco 144 credsSrc, err := credentialsSource(config) 145 if err == nil { 146 services = disco.NewWithCredentialsSource(credsSrc) 147 } else { 148 // Most commands don't actually need credentials, and most situations 149 // that would get us here would already have been reported by the config 150 // loading above, so we'll just log this one as an aid to debugging 151 // in the unlikely event that it _does_ arise. 152 log.Printf("[WARN] Cannot initialize remote host credentials manager: %s", err) 153 // passing (untyped) nil as the creds source is okay because the disco 154 // object checks that and just acts as though no credentials are present. 155 services = disco.NewWithCredentialsSource(nil) 156 } 157 services.SetUserAgent(httpclient.TerraformUserAgent(version.String())) 158 159 providerSrc, diags := providerSource(config.ProviderInstallation, services) 160 if len(diags) > 0 { 161 Ui.Error("There are some problems with the provider_installation configuration:") 162 for _, diag := range diags { 163 earlyColor := &colorstring.Colorize{ 164 Colors: colorstring.DefaultColors, 165 Disable: true, // Disable color to be conservative until we know better 166 Reset: true, 167 } 168 Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78)) 169 } 170 if diags.HasErrors() { 171 Ui.Error("As a result of the above problems, Terraform's provider installer may not behave as intended.\n\n") 172 // We continue to run anyway, because most commands don't do provider installation. 173 } 174 } 175 providerDevOverrides := providerDevOverrides(config.ProviderInstallation) 176 177 // The user can declare that certain providers are being managed on 178 // Terraform's behalf using this environment variable. This is used 179 // primarily by the SDK's acceptance testing framework. 180 unmanagedProviders, err := parseReattachProviders(os.Getenv("TF_REATTACH_PROVIDERS")) 181 if err != nil { 182 Ui.Error(err.Error()) 183 return 1 184 } 185 186 // Initialize the backends. 187 backendInit.Init(services) 188 189 // Get the command line args. 190 binName := filepath.Base(os.Args[0]) 191 args := os.Args[1:] 192 193 originalWd, err := os.Getwd() 194 if err != nil { 195 // It would be very strange to end up here 196 Ui.Error(fmt.Sprintf("Failed to determine current working directory: %s", err)) 197 return 1 198 } 199 200 // The arguments can begin with a -chdir option to ask Terraform to switch 201 // to a different working directory for the rest of its work. If that 202 // option is present then extractChdirOption returns a trimmed args with that option removed. 203 overrideWd, args, err := extractChdirOption(args) 204 if err != nil { 205 Ui.Error(fmt.Sprintf("Invalid -chdir option: %s", err)) 206 return 1 207 } 208 if overrideWd != "" { 209 err := os.Chdir(overrideWd) 210 if err != nil { 211 Ui.Error(fmt.Sprintf("Error handling -chdir option: %s", err)) 212 return 1 213 } 214 } 215 216 // In tests, Commands may already be set to provide mock commands 217 if Commands == nil { 218 // Commands get to hold on to the original working directory here, 219 // in case they need to refer back to it for any special reason, though 220 // they should primarily be working with the override working directory 221 // that we've now switched to above. 222 initCommands(originalWd, streams, config, services, providerSrc, providerDevOverrides, unmanagedProviders) 223 } 224 225 // Run checkpoint 226 go runCheckpoint(config) 227 228 // Make sure we clean up any managed plugins at the end of this 229 defer plugin.CleanupClients() 230 231 // Build the CLI so far, we do this so we can query the subcommand. 232 cliRunner := &cli.CLI{ 233 Args: args, 234 Commands: Commands, 235 HelpFunc: helpFunc, 236 HelpWriter: os.Stdout, 237 } 238 239 // Prefix the args with any args from the EnvCLI 240 args, err = mergeEnvArgs(EnvCLI, cliRunner.Subcommand(), args) 241 if err != nil { 242 Ui.Error(err.Error()) 243 return 1 244 } 245 246 // Prefix the args with any args from the EnvCLI targeting this command 247 suffix := strings.Replace(strings.Replace( 248 cliRunner.Subcommand(), "-", "_", -1), " ", "_", -1) 249 args, err = mergeEnvArgs( 250 fmt.Sprintf("%s_%s", EnvCLI, suffix), cliRunner.Subcommand(), args) 251 if err != nil { 252 Ui.Error(err.Error()) 253 return 1 254 } 255 256 // We shortcut "--version" and "-v" to just show the version 257 for _, arg := range args { 258 if arg == "-v" || arg == "-version" || arg == "--version" { 259 newArgs := make([]string, len(args)+1) 260 newArgs[0] = "version" 261 copy(newArgs[1:], args) 262 args = newArgs 263 break 264 } 265 } 266 267 // Rebuild the CLI with any modified args. 268 log.Printf("[INFO] CLI command args: %#v", args) 269 cliRunner = &cli.CLI{ 270 Name: binName, 271 Args: args, 272 Commands: Commands, 273 HelpFunc: helpFunc, 274 HelpWriter: os.Stdout, 275 276 Autocomplete: true, 277 AutocompleteInstall: "install-autocomplete", 278 AutocompleteUninstall: "uninstall-autocomplete", 279 } 280 281 // Before we continue we'll check whether the requested command is 282 // actually known. If not, we might be able to suggest an alternative 283 // if it seems like the user made a typo. 284 // (This bypasses the built-in help handling in cli.CLI for the situation 285 // where a command isn't found, because it's likely more helpful to 286 // mention what specifically went wrong, rather than just printing out 287 // a big block of usage information.) 288 289 // Check if this is being run via shell auto-complete, which uses the 290 // binary name as the first argument and won't be listed as a subcommand. 291 autoComplete := os.Getenv("COMP_LINE") != "" 292 293 if cmd := cliRunner.Subcommand(); cmd != "" && !autoComplete { 294 // Due to the design of cli.CLI, this special error message only works 295 // for typos of top-level commands. For a subcommand typo, like 296 // "terraform state posh", cmd would be "state" here and thus would 297 // be considered to exist, and it would print out its own usage message. 298 if _, exists := Commands[cmd]; !exists { 299 suggestions := make([]string, 0, len(Commands)) 300 for name := range Commands { 301 suggestions = append(suggestions, name) 302 } 303 suggestion := didyoumean.NameSuggestion(cmd, suggestions) 304 if suggestion != "" { 305 suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) 306 } 307 fmt.Fprintf(os.Stderr, "Terraform has no command named %q.%s\n\nTo see all of Terraform's top-level commands, run:\n terraform -help\n\n", cmd, suggestion) 308 return 1 309 } 310 } 311 312 exitCode, err := cliRunner.Run() 313 if err != nil { 314 Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error())) 315 return 1 316 } 317 318 // if we are exiting with a non-zero code, check if it was caused by any 319 // plugins crashing 320 if exitCode != 0 { 321 for _, panicLog := range logging.PluginPanics() { 322 Ui.Error(panicLog) 323 } 324 } 325 326 return exitCode 327 } 328 329 func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) { 330 v := os.Getenv(envName) 331 if v == "" { 332 return args, nil 333 } 334 335 log.Printf("[INFO] %s value: %q", envName, v) 336 extra, err := shellwords.Parse(v) 337 if err != nil { 338 return nil, fmt.Errorf( 339 "Error parsing extra CLI args from %s: %s", 340 envName, err) 341 } 342 343 // Find the command to look for in the args. If there is a space, 344 // we need to find the last part. 345 search := cmd 346 if idx := strings.LastIndex(search, " "); idx >= 0 { 347 search = cmd[idx+1:] 348 } 349 350 // Find the index to place the flags. We put them exactly 351 // after the first non-flag arg. 352 idx := -1 353 for i, v := range args { 354 if v == search { 355 idx = i 356 break 357 } 358 } 359 360 // idx points to the exact arg that isn't a flag. We increment 361 // by one so that all the copying below expects idx to be the 362 // insertion point. 363 idx++ 364 365 // Copy the args 366 newArgs := make([]string, len(args)+len(extra)) 367 copy(newArgs, args[:idx]) 368 copy(newArgs[idx:], extra) 369 copy(newArgs[len(extra)+idx:], args[idx:]) 370 return newArgs, nil 371 } 372 373 // parse information on reattaching to unmanaged providers out of a 374 // JSON-encoded environment variable. 375 func parseReattachProviders(in string) (map[addrs.Provider]*plugin.ReattachConfig, error) { 376 unmanagedProviders := map[addrs.Provider]*plugin.ReattachConfig{} 377 if in != "" { 378 type reattachConfig struct { 379 Protocol string 380 ProtocolVersion int 381 Addr struct { 382 Network string 383 String string 384 } 385 Pid int 386 Test bool 387 } 388 var m map[string]reattachConfig 389 err := json.Unmarshal([]byte(in), &m) 390 if err != nil { 391 return unmanagedProviders, fmt.Errorf("Invalid format for TF_REATTACH_PROVIDERS: %w", err) 392 } 393 for p, c := range m { 394 a, diags := addrs.ParseProviderSourceString(p) 395 if diags.HasErrors() { 396 return unmanagedProviders, fmt.Errorf("Error parsing %q as a provider address: %w", a, diags.Err()) 397 } 398 var addr net.Addr 399 switch c.Addr.Network { 400 case "unix": 401 addr, err = net.ResolveUnixAddr("unix", c.Addr.String) 402 if err != nil { 403 return unmanagedProviders, fmt.Errorf("Invalid unix socket path %q for %q: %w", c.Addr.String, p, err) 404 } 405 case "tcp": 406 addr, err = net.ResolveTCPAddr("tcp", c.Addr.String) 407 if err != nil { 408 return unmanagedProviders, fmt.Errorf("Invalid TCP address %q for %q: %w", c.Addr.String, p, err) 409 } 410 default: 411 return unmanagedProviders, fmt.Errorf("Unknown address type %q for %q", c.Addr.Network, p) 412 } 413 unmanagedProviders[a] = &plugin.ReattachConfig{ 414 Protocol: plugin.Protocol(c.Protocol), 415 ProtocolVersion: c.ProtocolVersion, 416 Pid: c.Pid, 417 Test: c.Test, 418 Addr: addr, 419 } 420 } 421 } 422 return unmanagedProviders, nil 423 } 424 425 func extractChdirOption(args []string) (string, []string, error) { 426 if len(args) == 0 { 427 return "", args, nil 428 } 429 430 const argName = "-chdir" 431 const argPrefix = argName + "=" 432 var argValue string 433 var argPos int 434 435 for i, arg := range args { 436 if !strings.HasPrefix(arg, "-") { 437 // Because the chdir option is a subcommand-agnostic one, we require 438 // it to appear before any subcommand argument, so if we find a 439 // non-option before we find -chdir then we are finished. 440 break 441 } 442 if arg == argName || arg == argPrefix { 443 return "", args, fmt.Errorf("must include an equals sign followed by a directory path, like -chdir=example") 444 } 445 if strings.HasPrefix(arg, argPrefix) { 446 argPos = i 447 argValue = arg[len(argPrefix):] 448 } 449 } 450 451 // When we fall out here, we'll have populated argValue with a non-empty 452 // string if the -chdir=... option was present and valid, or left it 453 // empty if it wasn't present. 454 if argValue == "" { 455 return "", args, nil 456 } 457 458 // If we did find the option then we'll need to produce a new args that 459 // doesn't include it anymore. 460 if argPos == 0 { 461 // Easy case: we can just slice off the front 462 return argValue, args[1:], nil 463 } 464 // Otherwise we need to construct a new array and copy to it. 465 newArgs := make([]string, len(args)-1) 466 copy(newArgs, args[:argPos]) 467 copy(newArgs[argPos:], args[argPos+1:]) 468 return argValue, newArgs, nil 469 }