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