github.com/paultyng/terraform@v0.6.11-0.20180227224804-66ff8f8bed40/command/push.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "path/filepath" 8 "sort" 9 "strings" 10 11 "github.com/hashicorp/atlas-go/archive" 12 "github.com/hashicorp/atlas-go/v1" 13 "github.com/hashicorp/terraform/backend" 14 "github.com/hashicorp/terraform/config" 15 "github.com/hashicorp/terraform/version" 16 ) 17 18 type PushCommand struct { 19 Meta 20 21 // client is the client to use for the actual push operations. 22 // If this isn't set, then the Atlas client is used. This should 23 // really only be set for testing reasons (and is hence not exported). 24 client pushClient 25 } 26 27 func (c *PushCommand) Run(args []string) int { 28 var atlasAddress, atlasToken string 29 var archiveVCS, moduleUpload bool 30 var name string 31 var overwrite []string 32 args, err := c.Meta.process(args, true) 33 if err != nil { 34 return 1 35 } 36 cmdFlags := c.Meta.flagSet("push") 37 cmdFlags.StringVar(&atlasAddress, "atlas-address", "", "") 38 cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") 39 cmdFlags.StringVar(&atlasToken, "token", "", "") 40 cmdFlags.BoolVar(&moduleUpload, "upload-modules", true, "") 41 cmdFlags.StringVar(&name, "name", "", "") 42 cmdFlags.BoolVar(&archiveVCS, "vcs", true, "") 43 cmdFlags.Var((*FlagStringSlice)(&overwrite), "overwrite", "") 44 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 45 if err := cmdFlags.Parse(args); err != nil { 46 return 1 47 } 48 49 // Make a map of the set values 50 overwriteMap := make(map[string]struct{}, len(overwrite)) 51 for _, v := range overwrite { 52 overwriteMap[v] = struct{}{} 53 } 54 55 // This is a map of variables specifically from the CLI that we want to overwrite. 56 // We need this because there is a chance that the user is trying to modify 57 // a variable we don't see in our context, but which exists in this Terraform 58 // Enterprise workspace. 59 cliVars := make(map[string]string) 60 for k, v := range c.variables { 61 if _, ok := overwriteMap[k]; ok { 62 if val, ok := v.(string); ok { 63 cliVars[k] = val 64 } else { 65 c.Ui.Error(fmt.Sprintf("Error reading value for variable: %s", k)) 66 return 1 67 } 68 } 69 } 70 71 // Get the path to the configuration depending on the args. 72 configPath, err := ModulePath(cmdFlags.Args()) 73 if err != nil { 74 c.Ui.Error(err.Error()) 75 return 1 76 } 77 78 // Check if the path is a plan 79 plan, err := c.Plan(configPath) 80 if err != nil { 81 c.Ui.Error(err.Error()) 82 return 1 83 } 84 if plan != nil { 85 c.Ui.Error( 86 "A plan file cannot be given as the path to the configuration.\n" + 87 "A path to a module (directory with configuration) must be given.") 88 return 1 89 } 90 91 // Load the module 92 mod, diags := c.Module(configPath) 93 if diags.HasErrors() { 94 c.showDiagnostics(diags) 95 return 1 96 } 97 if mod == nil { 98 c.Ui.Error(fmt.Sprintf( 99 "No configuration files found in the directory: %s\n\n"+ 100 "This command requires configuration to run.", 101 configPath)) 102 return 1 103 } 104 105 var conf *config.Config 106 if mod != nil { 107 conf = mod.Config() 108 } 109 110 // Load the backend 111 b, err := c.Backend(&BackendOpts{ 112 Config: conf, 113 }) 114 if err != nil { 115 c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) 116 return 1 117 } 118 119 // We require a non-local backend 120 if c.IsLocalBackend(b) { 121 c.Ui.Error( 122 "A remote backend is not enabled. For Atlas to run Terraform\n" + 123 "for you, remote state must be used and configured. Remote \n" + 124 "state via any backend is accepted, not just Atlas. To configure\n" + 125 "a backend, please see the documentation at the URL below:\n\n" + 126 "https://www.terraform.io/docs/state/remote.html") 127 return 1 128 } 129 130 // We require a local backend 131 local, ok := b.(backend.Local) 132 if !ok { 133 c.Ui.Error(ErrUnsupportedLocalOp) 134 return 1 135 } 136 137 // Build the operation 138 opReq := c.Operation() 139 opReq.Module = mod 140 opReq.Plan = plan 141 142 // Get the context 143 ctx, _, err := local.Context(opReq) 144 if err != nil { 145 c.Ui.Error(err.Error()) 146 return 1 147 } 148 149 // Get the configuration 150 config := ctx.Module().Config() 151 if name == "" { 152 if config.Atlas == nil || config.Atlas.Name == "" { 153 c.Ui.Error( 154 "The name of this Terraform configuration in Atlas must be\n" + 155 "specified within your configuration or the command-line. To\n" + 156 "set it on the command-line, use the `-name` parameter.") 157 return 1 158 } 159 name = config.Atlas.Name 160 } 161 162 // Initialize the client if it isn't given. 163 if c.client == nil { 164 // Make sure to nil out our client so our token isn't sitting around 165 defer func() { c.client = nil }() 166 167 // Initialize it to the default client, we set custom settings later 168 client := atlas.DefaultClient() 169 if atlasAddress != "" { 170 client, err = atlas.NewClient(atlasAddress) 171 if err != nil { 172 c.Ui.Error(fmt.Sprintf("Error initializing Atlas client: %s", err)) 173 return 1 174 } 175 } 176 177 client.DefaultHeader.Set(version.Header, version.Version) 178 179 if atlasToken != "" { 180 client.Token = atlasToken 181 } 182 183 c.client = &atlasPushClient{Client: client} 184 } 185 186 // Get the variables we already have in atlas 187 atlasVars, err := c.client.Get(name) 188 if err != nil { 189 c.Ui.Error(fmt.Sprintf( 190 "Error looking up previously pushed configuration: %s", err)) 191 return 1 192 } 193 194 // Set remote variables in the context if we don't have a value here. These 195 // don't have to be correct, it just prevents the Input walk from prompting 196 // the user for input. 197 ctxVars := ctx.Variables() 198 atlasVarSentry := "ATLAS_78AC153CA649EAA44815DAD6CBD4816D" 199 for k, _ := range atlasVars { 200 if _, ok := ctxVars[k]; !ok { 201 ctx.SetVariable(k, atlasVarSentry) 202 } 203 } 204 205 // Ask for input 206 if err := ctx.Input(c.InputMode()); err != nil { 207 c.Ui.Error(fmt.Sprintf( 208 "Error while asking for variable input:\n\n%s", err)) 209 return 1 210 } 211 212 // Now that we've gone through the input walk, we can be sure we have all 213 // the variables we're going to get. 214 // We are going to keep these separate from the atlas variables until 215 // upload, so we can notify the user which local variables we're sending. 216 serializedVars, err := tfVars(ctx.Variables()) 217 if err != nil { 218 c.Ui.Error(fmt.Sprintf( 219 "An error has occurred while serializing the variables for uploading:\n"+ 220 "%s", err)) 221 return 1 222 } 223 224 // Get the absolute path for our data directory, since the Extra field 225 // value below needs to be absolute. 226 dataDirAbs, err := filepath.Abs(c.DataDir()) 227 if err != nil { 228 c.Ui.Error(fmt.Sprintf( 229 "Error while expanding the data directory %q: %s", c.DataDir(), err)) 230 return 1 231 } 232 233 // Build the archiving options, which includes everything it can 234 // by default according to VCS rules but forcing the data directory. 235 archiveOpts := &archive.ArchiveOpts{ 236 VCS: archiveVCS, 237 Extra: map[string]string{ 238 DefaultDataDir: archive.ExtraEntryDir, 239 }, 240 } 241 242 // Always store the state file in here so we can find state 243 statePathKey := fmt.Sprintf("%s/%s", DefaultDataDir, DefaultStateFilename) 244 archiveOpts.Extra[statePathKey] = filepath.Join(dataDirAbs, DefaultStateFilename) 245 if moduleUpload { 246 // If we're uploading modules, explicitly add that directory if exists. 247 moduleKey := fmt.Sprintf("%s/%s", DefaultDataDir, "modules") 248 moduleDir := filepath.Join(dataDirAbs, "modules") 249 _, err := os.Stat(moduleDir) 250 if err == nil { 251 archiveOpts.Extra[moduleKey] = filepath.Join(dataDirAbs, "modules") 252 } 253 if err != nil && !os.IsNotExist(err) { 254 c.Ui.Error(fmt.Sprintf( 255 "Error checking for module dir %q: %s", moduleDir, err)) 256 return 1 257 } 258 } else { 259 // If we're not uploading modules, explicitly exclude add that 260 archiveOpts.Exclude = append( 261 archiveOpts.Exclude, 262 filepath.Join(c.DataDir(), "modules")) 263 } 264 265 archiveR, err := archive.CreateArchive(configPath, archiveOpts) 266 if err != nil { 267 c.Ui.Error(fmt.Sprintf( 268 "An error has occurred while archiving the module for uploading:\n"+ 269 "%s", err)) 270 return 1 271 } 272 273 // List of the vars we're uploading to display to the user. 274 // We always upload all vars from atlas, but only report them if they are overwritten. 275 var setVars []string 276 277 // variables to upload 278 var uploadVars []atlas.TFVar 279 280 // first add all the variables we want to send which have been serialized 281 // from the local context. 282 for _, sv := range serializedVars { 283 _, inOverwrite := overwriteMap[sv.Key] 284 _, inAtlas := atlasVars[sv.Key] 285 286 // We have a variable that's not in atlas, so always send it. 287 if !inAtlas { 288 uploadVars = append(uploadVars, sv) 289 setVars = append(setVars, sv.Key) 290 } 291 292 // We're overwriting an atlas variable. 293 // We also want to check that we 294 // don't send the dummy sentry value back to atlas. This could happen 295 // if it's specified as an overwrite on the cli, but we didn't set a 296 // new value. 297 if inAtlas && inOverwrite && sv.Value != atlasVarSentry { 298 uploadVars = append(uploadVars, sv) 299 setVars = append(setVars, sv.Key) 300 301 // remove this value from the atlas vars, because we're going to 302 // send back the remainder regardless. 303 delete(atlasVars, sv.Key) 304 } 305 } 306 307 // now send back all the existing atlas vars, inserting any overwrites from the cli. 308 for k, av := range atlasVars { 309 if v, ok := cliVars[k]; ok { 310 av.Value = v 311 setVars = append(setVars, k) 312 } 313 uploadVars = append(uploadVars, av) 314 } 315 316 sort.Strings(setVars) 317 if len(setVars) > 0 { 318 c.Ui.Output( 319 "The following variables will be set or overwritten within Atlas from\n" + 320 "their local values. All other variables are already set within Atlas.\n" + 321 "If you want to modify the value of a variable, use the Atlas web\n" + 322 "interface or set it locally and use the -overwrite flag.\n\n") 323 for _, v := range setVars { 324 c.Ui.Output(fmt.Sprintf(" * %s", v)) 325 } 326 327 // Newline 328 c.Ui.Output("") 329 } 330 331 // Upsert! 332 opts := &pushUpsertOptions{ 333 Name: name, 334 Archive: archiveR, 335 Variables: ctx.Variables(), 336 TFVars: uploadVars, 337 } 338 339 c.Ui.Output("Uploading Terraform configuration...") 340 vsn, err := c.client.Upsert(opts) 341 if err != nil { 342 c.Ui.Error(fmt.Sprintf( 343 "An error occurred while uploading the module:\n\n%s", err)) 344 return 1 345 } 346 347 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 348 "[reset][bold][green]Configuration %q uploaded! (v%d)", 349 name, vsn))) 350 351 c.showDiagnostics(diags) 352 if diags.HasErrors() { 353 return 1 354 } 355 356 return 0 357 } 358 359 func (c *PushCommand) Help() string { 360 helpText := ` 361 Usage: terraform push [options] [DIR] 362 363 Upload this Terraform module to an Atlas server for remote 364 infrastructure management. 365 366 Options: 367 368 -atlas-address=<url> An alternate address to an Atlas instance. Defaults 369 to https://atlas.hashicorp.com 370 371 -upload-modules=true If true (default), then the modules are locked at 372 their current checkout and uploaded completely. This 373 prevents Atlas from running "terraform get". 374 375 -name=<name> Name of the configuration in Atlas. This can also 376 be set in the configuration itself. Format is 377 typically: "username/name". 378 379 -token=<token> Access token to use to upload. If blank or unspecified, 380 the ATLAS_TOKEN environmental variable will be used. 381 382 -overwrite=foo Variable keys that should overwrite values in Atlas. 383 Otherwise, variables already set in Atlas will overwrite 384 local values. This flag can be repeated. 385 386 -var 'foo=bar' Set a variable in the Terraform configuration. This 387 flag can be set multiple times. 388 389 -var-file=foo Set variables in the Terraform configuration from 390 a file. If "terraform.tfvars" or any ".auto.tfvars" 391 files are present, they will be automatically loaded. 392 393 -vcs=true If true (default), push will upload only files 394 committed to your VCS, if detected. 395 396 -no-color If specified, output won't contain any color. 397 398 ` 399 return strings.TrimSpace(helpText) 400 } 401 402 func sortedKeys(m map[string]interface{}) []string { 403 var keys []string 404 for k := range m { 405 keys = append(keys, k) 406 } 407 sort.Strings(keys) 408 return keys 409 } 410 411 // build the set of TFVars for push 412 func tfVars(vars map[string]interface{}) ([]atlas.TFVar, error) { 413 var tfVars []atlas.TFVar 414 var err error 415 416 RANGE: 417 for _, k := range sortedKeys(vars) { 418 v := vars[k] 419 420 var hcl []byte 421 tfv := atlas.TFVar{Key: k} 422 423 switch v := v.(type) { 424 case string: 425 tfv.Value = v 426 427 default: 428 // everything that's not a string is now HCL encoded 429 hcl, err = encodeHCL(v) 430 if err != nil { 431 break RANGE 432 } 433 434 tfv.Value = string(hcl) 435 tfv.IsHCL = true 436 } 437 438 tfVars = append(tfVars, tfv) 439 } 440 441 return tfVars, err 442 } 443 444 func (c *PushCommand) Synopsis() string { 445 return "Upload this Terraform module to Atlas to run" 446 } 447 448 // pushClient is implemented internally to control where pushes go. This is 449 // either to Atlas or a mock for testing. We still return a map to make it 450 // easier to check for variable existence when filtering the overrides. 451 type pushClient interface { 452 Get(string) (map[string]atlas.TFVar, error) 453 Upsert(*pushUpsertOptions) (int, error) 454 } 455 456 type pushUpsertOptions struct { 457 Name string 458 Archive *archive.Archive 459 Variables map[string]interface{} 460 TFVars []atlas.TFVar 461 } 462 463 type atlasPushClient struct { 464 Client *atlas.Client 465 } 466 467 func (c *atlasPushClient) Get(name string) (map[string]atlas.TFVar, error) { 468 user, name, err := atlas.ParseSlug(name) 469 if err != nil { 470 return nil, err 471 } 472 473 version, err := c.Client.TerraformConfigLatest(user, name) 474 if err != nil { 475 return nil, err 476 } 477 478 variables := make(map[string]atlas.TFVar) 479 480 if version == nil { 481 return variables, nil 482 } 483 484 // Variables is superseded by TFVars 485 if version.TFVars == nil { 486 for k, v := range version.Variables { 487 variables[k] = atlas.TFVar{Key: k, Value: v} 488 } 489 } else { 490 for _, v := range version.TFVars { 491 variables[v.Key] = v 492 } 493 } 494 495 return variables, nil 496 } 497 498 func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) { 499 user, name, err := atlas.ParseSlug(opts.Name) 500 if err != nil { 501 return 0, err 502 } 503 504 data := &atlas.TerraformConfigVersion{ 505 TFVars: opts.TFVars, 506 } 507 508 version, err := c.Client.CreateTerraformConfigVersion( 509 user, name, data, opts.Archive, opts.Archive.Size) 510 if err != nil { 511 return 0, err 512 } 513 514 return version, nil 515 } 516 517 type mockPushClient struct { 518 File string 519 520 GetCalled bool 521 GetName string 522 GetResult map[string]atlas.TFVar 523 GetError error 524 525 UpsertCalled bool 526 UpsertOptions *pushUpsertOptions 527 UpsertVersion int 528 UpsertError error 529 } 530 531 func (c *mockPushClient) Get(name string) (map[string]atlas.TFVar, error) { 532 c.GetCalled = true 533 c.GetName = name 534 return c.GetResult, c.GetError 535 } 536 537 func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) { 538 f, err := os.Create(c.File) 539 if err != nil { 540 return 0, err 541 } 542 defer f.Close() 543 544 data := opts.Archive 545 size := opts.Archive.Size 546 if _, err := io.CopyN(f, data, size); err != nil { 547 return 0, err 548 } 549 550 c.UpsertCalled = true 551 c.UpsertOptions = opts 552 return c.UpsertVersion, c.UpsertError 553 }