github.com/abhinavdahiya/terraform@v0.11.12-beta1/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 defer func() { 150 err := opReq.StateLocker.Unlock(nil) 151 if err != nil { 152 c.Ui.Error(err.Error()) 153 } 154 }() 155 156 // Get the configuration 157 config := ctx.Module().Config() 158 if name == "" { 159 if config.Atlas == nil || config.Atlas.Name == "" { 160 c.Ui.Error( 161 "The name of this Terraform configuration in Atlas must be\n" + 162 "specified within your configuration or the command-line. To\n" + 163 "set it on the command-line, use the `-name` parameter.") 164 return 1 165 } 166 name = config.Atlas.Name 167 } 168 169 // Initialize the client if it isn't given. 170 if c.client == nil { 171 // Make sure to nil out our client so our token isn't sitting around 172 defer func() { c.client = nil }() 173 174 // Initialize it to the default client, we set custom settings later 175 client := atlas.DefaultClient() 176 if atlasAddress != "" { 177 client, err = atlas.NewClient(atlasAddress) 178 if err != nil { 179 c.Ui.Error(fmt.Sprintf("Error initializing Atlas client: %s", err)) 180 return 1 181 } 182 } 183 184 client.DefaultHeader.Set(version.Header, version.Version) 185 186 if atlasToken != "" { 187 client.Token = atlasToken 188 } 189 190 c.client = &atlasPushClient{Client: client} 191 } 192 193 // Get the variables we already have in atlas 194 atlasVars, err := c.client.Get(name) 195 if err != nil { 196 c.Ui.Error(fmt.Sprintf( 197 "Error looking up previously pushed configuration: %s", err)) 198 return 1 199 } 200 201 // Set remote variables in the context if we don't have a value here. These 202 // don't have to be correct, it just prevents the Input walk from prompting 203 // the user for input. 204 ctxVars := ctx.Variables() 205 atlasVarSentry := "ATLAS_78AC153CA649EAA44815DAD6CBD4816D" 206 for k, _ := range atlasVars { 207 if _, ok := ctxVars[k]; !ok { 208 ctx.SetVariable(k, atlasVarSentry) 209 } 210 } 211 212 // Ask for input 213 if err := ctx.Input(c.InputMode()); err != nil { 214 c.Ui.Error(fmt.Sprintf( 215 "Error while asking for variable input:\n\n%s", err)) 216 return 1 217 } 218 219 // Now that we've gone through the input walk, we can be sure we have all 220 // the variables we're going to get. 221 // We are going to keep these separate from the atlas variables until 222 // upload, so we can notify the user which local variables we're sending. 223 serializedVars, err := tfVars(ctx.Variables()) 224 if err != nil { 225 c.Ui.Error(fmt.Sprintf( 226 "An error has occurred while serializing the variables for uploading:\n"+ 227 "%s", err)) 228 return 1 229 } 230 231 // Get the absolute path for our data directory, since the Extra field 232 // value below needs to be absolute. 233 dataDirAbs, err := filepath.Abs(c.DataDir()) 234 if err != nil { 235 c.Ui.Error(fmt.Sprintf( 236 "Error while expanding the data directory %q: %s", c.DataDir(), err)) 237 return 1 238 } 239 240 // Build the archiving options, which includes everything it can 241 // by default according to VCS rules but forcing the data directory. 242 archiveOpts := &archive.ArchiveOpts{ 243 VCS: archiveVCS, 244 Extra: map[string]string{ 245 DefaultDataDir: archive.ExtraEntryDir, 246 }, 247 } 248 249 // Always store the state file in here so we can find state 250 statePathKey := fmt.Sprintf("%s/%s", DefaultDataDir, DefaultStateFilename) 251 archiveOpts.Extra[statePathKey] = filepath.Join(dataDirAbs, DefaultStateFilename) 252 if moduleUpload { 253 // If we're uploading modules, explicitly add that directory if exists. 254 moduleKey := fmt.Sprintf("%s/%s", DefaultDataDir, "modules") 255 moduleDir := filepath.Join(dataDirAbs, "modules") 256 _, err := os.Stat(moduleDir) 257 if err == nil { 258 archiveOpts.Extra[moduleKey] = filepath.Join(dataDirAbs, "modules") 259 } 260 if err != nil && !os.IsNotExist(err) { 261 c.Ui.Error(fmt.Sprintf( 262 "Error checking for module dir %q: %s", moduleDir, err)) 263 return 1 264 } 265 } else { 266 // If we're not uploading modules, explicitly exclude add that 267 archiveOpts.Exclude = append( 268 archiveOpts.Exclude, 269 filepath.Join(c.DataDir(), "modules")) 270 } 271 272 archiveR, err := archive.CreateArchive(configPath, archiveOpts) 273 if err != nil { 274 c.Ui.Error(fmt.Sprintf( 275 "An error has occurred while archiving the module for uploading:\n"+ 276 "%s", err)) 277 return 1 278 } 279 280 // List of the vars we're uploading to display to the user. 281 // We always upload all vars from atlas, but only report them if they are overwritten. 282 var setVars []string 283 284 // variables to upload 285 var uploadVars []atlas.TFVar 286 287 // first add all the variables we want to send which have been serialized 288 // from the local context. 289 for _, sv := range serializedVars { 290 _, inOverwrite := overwriteMap[sv.Key] 291 _, inAtlas := atlasVars[sv.Key] 292 293 // We have a variable that's not in atlas, so always send it. 294 if !inAtlas { 295 uploadVars = append(uploadVars, sv) 296 setVars = append(setVars, sv.Key) 297 } 298 299 // We're overwriting an atlas variable. 300 // We also want to check that we 301 // don't send the dummy sentry value back to atlas. This could happen 302 // if it's specified as an overwrite on the cli, but we didn't set a 303 // new value. 304 if inAtlas && inOverwrite && sv.Value != atlasVarSentry { 305 uploadVars = append(uploadVars, sv) 306 setVars = append(setVars, sv.Key) 307 308 // remove this value from the atlas vars, because we're going to 309 // send back the remainder regardless. 310 delete(atlasVars, sv.Key) 311 } 312 } 313 314 // now send back all the existing atlas vars, inserting any overwrites from the cli. 315 for k, av := range atlasVars { 316 if v, ok := cliVars[k]; ok { 317 av.Value = v 318 setVars = append(setVars, k) 319 } 320 uploadVars = append(uploadVars, av) 321 } 322 323 sort.Strings(setVars) 324 if len(setVars) > 0 { 325 c.Ui.Output( 326 "The following variables will be set or overwritten within Atlas from\n" + 327 "their local values. All other variables are already set within Atlas.\n" + 328 "If you want to modify the value of a variable, use the Atlas web\n" + 329 "interface or set it locally and use the -overwrite flag.\n\n") 330 for _, v := range setVars { 331 c.Ui.Output(fmt.Sprintf(" * %s", v)) 332 } 333 334 // Newline 335 c.Ui.Output("") 336 } 337 338 // Upsert! 339 opts := &pushUpsertOptions{ 340 Name: name, 341 Archive: archiveR, 342 Variables: ctx.Variables(), 343 TFVars: uploadVars, 344 } 345 346 c.Ui.Output("Uploading Terraform configuration...") 347 vsn, err := c.client.Upsert(opts) 348 if err != nil { 349 c.Ui.Error(fmt.Sprintf( 350 "An error occurred while uploading the module:\n\n%s", err)) 351 return 1 352 } 353 354 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 355 "[reset][bold][green]Configuration %q uploaded! (v%d)", 356 name, vsn))) 357 358 c.showDiagnostics(diags) 359 if diags.HasErrors() { 360 return 1 361 } 362 363 return 0 364 } 365 366 func (c *PushCommand) Help() string { 367 helpText := ` 368 Usage: terraform push [options] [DIR] 369 370 Upload this Terraform module to an Atlas server for remote 371 infrastructure management. 372 373 Options: 374 375 -atlas-address=<url> An alternate address to an Atlas instance. Defaults 376 to https://atlas.hashicorp.com 377 378 -upload-modules=true If true (default), then the modules are locked at 379 their current checkout and uploaded completely. This 380 prevents Atlas from running "terraform get". 381 382 -name=<name> Name of the configuration in Atlas. This can also 383 be set in the configuration itself. Format is 384 typically: "username/name". 385 386 -token=<token> Access token to use to upload. If blank or unspecified, 387 the ATLAS_TOKEN environmental variable will be used. 388 389 -overwrite=foo Variable keys that should overwrite values in Atlas. 390 Otherwise, variables already set in Atlas will overwrite 391 local values. This flag can be repeated. 392 393 -var 'foo=bar' Set a variable in the Terraform configuration. This 394 flag can be set multiple times. 395 396 -var-file=foo Set variables in the Terraform configuration from 397 a file. If "terraform.tfvars" or any ".auto.tfvars" 398 files are present, they will be automatically loaded. 399 400 -vcs=true If true (default), push will upload only files 401 committed to your VCS, if detected. 402 403 -no-color If specified, output won't contain any color. 404 405 ` 406 return strings.TrimSpace(helpText) 407 } 408 409 func sortedKeys(m map[string]interface{}) []string { 410 var keys []string 411 for k := range m { 412 keys = append(keys, k) 413 } 414 sort.Strings(keys) 415 return keys 416 } 417 418 // build the set of TFVars for push 419 func tfVars(vars map[string]interface{}) ([]atlas.TFVar, error) { 420 var tfVars []atlas.TFVar 421 var err error 422 423 RANGE: 424 for _, k := range sortedKeys(vars) { 425 v := vars[k] 426 427 var hcl []byte 428 tfv := atlas.TFVar{Key: k} 429 430 switch v := v.(type) { 431 case string: 432 tfv.Value = v 433 434 default: 435 // everything that's not a string is now HCL encoded 436 hcl, err = encodeHCL(v) 437 if err != nil { 438 break RANGE 439 } 440 441 tfv.Value = string(hcl) 442 tfv.IsHCL = true 443 } 444 445 tfVars = append(tfVars, tfv) 446 } 447 448 return tfVars, err 449 } 450 451 func (c *PushCommand) Synopsis() string { 452 return "Upload this Terraform module to Atlas to run" 453 } 454 455 // pushClient is implemented internally to control where pushes go. This is 456 // either to Atlas or a mock for testing. We still return a map to make it 457 // easier to check for variable existence when filtering the overrides. 458 type pushClient interface { 459 Get(string) (map[string]atlas.TFVar, error) 460 Upsert(*pushUpsertOptions) (int, error) 461 } 462 463 type pushUpsertOptions struct { 464 Name string 465 Archive *archive.Archive 466 Variables map[string]interface{} 467 TFVars []atlas.TFVar 468 } 469 470 type atlasPushClient struct { 471 Client *atlas.Client 472 } 473 474 func (c *atlasPushClient) Get(name string) (map[string]atlas.TFVar, error) { 475 user, name, err := atlas.ParseSlug(name) 476 if err != nil { 477 return nil, err 478 } 479 480 version, err := c.Client.TerraformConfigLatest(user, name) 481 if err != nil { 482 return nil, err 483 } 484 485 variables := make(map[string]atlas.TFVar) 486 487 if version == nil { 488 return variables, nil 489 } 490 491 // Variables is superseded by TFVars 492 if version.TFVars == nil { 493 for k, v := range version.Variables { 494 variables[k] = atlas.TFVar{Key: k, Value: v} 495 } 496 } else { 497 for _, v := range version.TFVars { 498 variables[v.Key] = v 499 } 500 } 501 502 return variables, nil 503 } 504 505 func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) { 506 user, name, err := atlas.ParseSlug(opts.Name) 507 if err != nil { 508 return 0, err 509 } 510 511 data := &atlas.TerraformConfigVersion{ 512 TFVars: opts.TFVars, 513 } 514 515 version, err := c.Client.CreateTerraformConfigVersion( 516 user, name, data, opts.Archive, opts.Archive.Size) 517 if err != nil { 518 return 0, err 519 } 520 521 return version, nil 522 } 523 524 type mockPushClient struct { 525 File string 526 527 GetCalled bool 528 GetName string 529 GetResult map[string]atlas.TFVar 530 GetError error 531 532 UpsertCalled bool 533 UpsertOptions *pushUpsertOptions 534 UpsertVersion int 535 UpsertError error 536 } 537 538 func (c *mockPushClient) Get(name string) (map[string]atlas.TFVar, error) { 539 c.GetCalled = true 540 c.GetName = name 541 return c.GetResult, c.GetError 542 } 543 544 func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) { 545 f, err := os.Create(c.File) 546 if err != nil { 547 return 0, err 548 } 549 defer f.Close() 550 551 data := opts.Archive 552 size := opts.Archive.Size 553 if _, err := io.CopyN(f, data, size); err != nil { 554 return 0, err 555 } 556 557 c.UpsertCalled = true 558 c.UpsertOptions = opts 559 return c.UpsertVersion, c.UpsertError 560 }