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