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