github.com/ojongerius/terraform@v0.7.1-0.20160811111335-97fcd5f4cc90/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 // Build the archiving options, which includes everything it can 195 // by default according to VCS rules but forcing the data directory. 196 archiveOpts := &archive.ArchiveOpts{ 197 VCS: archiveVCS, 198 Extra: map[string]string{ 199 DefaultDataDir: c.DataDir(), 200 }, 201 } 202 if !moduleUpload { 203 // If we're not uploading modules, then exclude the modules dir. 204 archiveOpts.Exclude = append( 205 archiveOpts.Exclude, 206 filepath.Join(c.DataDir(), "modules")) 207 } 208 209 archiveR, err := archive.CreateArchive(configPath, archiveOpts) 210 if err != nil { 211 c.Ui.Error(fmt.Sprintf( 212 "An error has occurred while archiving the module for uploading:\n"+ 213 "%s", err)) 214 return 1 215 } 216 217 // List of the vars we're uploading to display to the user. 218 // We always upload all vars from atlas, but only report them if they are overwritten. 219 var setVars []string 220 221 // variables to upload 222 var uploadVars []atlas.TFVar 223 224 // first add all the variables we want to send which have been serialized 225 // from the local context. 226 for _, sv := range serializedVars { 227 _, inOverwrite := overwriteMap[sv.Key] 228 _, inAtlas := atlasVars[sv.Key] 229 230 // We have a variable that's not in atlas, so always send it. 231 if !inAtlas { 232 uploadVars = append(uploadVars, sv) 233 setVars = append(setVars, sv.Key) 234 } 235 236 // We're overwriting an atlas variable. 237 // We also want to check that we 238 // don't send the dummy sentry value back to atlas. This could happen 239 // if it's specified as an overwrite on the cli, but we didn't set a 240 // new value. 241 if inAtlas && inOverwrite && sv.Value != atlasVarSentry { 242 uploadVars = append(uploadVars, sv) 243 setVars = append(setVars, sv.Key) 244 245 // remove this value from the atlas vars, because we're going to 246 // send back the remainder regardless. 247 delete(atlasVars, sv.Key) 248 } 249 } 250 251 // now send back all the existing atlas vars, inserting any overwrites from the cli. 252 for k, av := range atlasVars { 253 if v, ok := cliVars[k]; ok { 254 av.Value = v 255 setVars = append(setVars, k) 256 } 257 uploadVars = append(uploadVars, av) 258 } 259 260 sort.Strings(setVars) 261 if len(setVars) > 0 { 262 c.Ui.Output( 263 "The following variables will be set or overwritten within Atlas from\n" + 264 "their local values. All other variables are already set within Atlas.\n" + 265 "If you want to modify the value of a variable, use the Atlas web\n" + 266 "interface or set it locally and use the -overwrite flag.\n\n") 267 for _, v := range setVars { 268 c.Ui.Output(fmt.Sprintf(" * %s", v)) 269 } 270 271 // Newline 272 c.Ui.Output("") 273 } 274 275 // Upsert! 276 opts := &pushUpsertOptions{ 277 Name: name, 278 Archive: archiveR, 279 Variables: ctx.Variables(), 280 TFVars: uploadVars, 281 } 282 283 c.Ui.Output("Uploading Terraform configuration...") 284 vsn, err := c.client.Upsert(opts) 285 if err != nil { 286 c.Ui.Error(fmt.Sprintf( 287 "An error occurred while uploading the module:\n\n%s", err)) 288 return 1 289 } 290 291 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 292 "[reset][bold][green]Configuration %q uploaded! (v%d)", 293 name, vsn))) 294 return 0 295 } 296 297 func (c *PushCommand) Help() string { 298 helpText := ` 299 Usage: terraform push [options] [DIR] 300 301 Upload this Terraform module to an Atlas server for remote 302 infrastructure management. 303 304 Options: 305 306 -atlas-address=<url> An alternate address to an Atlas instance. Defaults 307 to https://atlas.hashicorp.com 308 309 -upload-modules=true If true (default), then the modules are locked at 310 their current checkout and uploaded completely. This 311 prevents Atlas from running "terraform get". 312 313 -name=<name> Name of the configuration in Atlas. This can also 314 be set in the configuration itself. Format is 315 typically: "username/name". 316 317 -token=<token> Access token to use to upload. If blank or unspecified, 318 the ATLAS_TOKEN environmental variable will be used. 319 320 -overwrite=foo Variable keys that should overwrite values in Atlas. 321 Otherwise, variables already set in Atlas will overwrite 322 local values. This flag can be repeated. 323 324 -var 'foo=bar' Set a variable in the Terraform configuration. This 325 flag can be set multiple times. 326 327 -var-file=foo Set variables in the Terraform configuration from 328 a file. If "terraform.tfvars" is present, it will be 329 automatically loaded if this flag is not specified. 330 331 -vcs=true If true (default), push will upload only files 332 committed to your VCS, if detected. 333 334 -no-color If specified, output won't contain any color. 335 336 ` 337 return strings.TrimSpace(helpText) 338 } 339 340 func sortedKeys(m map[string]interface{}) []string { 341 var keys []string 342 for k := range m { 343 keys = append(keys, k) 344 } 345 sort.Strings(keys) 346 return keys 347 } 348 349 // build the set of TFVars for push 350 func tfVars(vars map[string]interface{}) ([]atlas.TFVar, error) { 351 var tfVars []atlas.TFVar 352 var err error 353 354 RANGE: 355 for _, k := range sortedKeys(vars) { 356 v := vars[k] 357 358 var hcl []byte 359 tfv := atlas.TFVar{Key: k} 360 361 switch v := v.(type) { 362 case string: 363 tfv.Value = v 364 365 default: 366 // everything that's not a string is now HCL encoded 367 hcl, err = encodeHCL(v) 368 if err != nil { 369 break RANGE 370 } 371 372 tfv.Value = string(hcl) 373 tfv.IsHCL = true 374 } 375 376 tfVars = append(tfVars, tfv) 377 } 378 379 return tfVars, err 380 } 381 382 func (c *PushCommand) Synopsis() string { 383 return "Upload this Terraform module to Atlas to run" 384 } 385 386 // pushClient is implemented internally to control where pushes go. This is 387 // either to Atlas or a mock for testing. We still return a map to make it 388 // easier to check for variable existence when filtering the overrides. 389 type pushClient interface { 390 Get(string) (map[string]atlas.TFVar, error) 391 Upsert(*pushUpsertOptions) (int, error) 392 } 393 394 type pushUpsertOptions struct { 395 Name string 396 Archive *archive.Archive 397 Variables map[string]interface{} 398 TFVars []atlas.TFVar 399 } 400 401 type atlasPushClient struct { 402 Client *atlas.Client 403 } 404 405 func (c *atlasPushClient) Get(name string) (map[string]atlas.TFVar, error) { 406 user, name, err := atlas.ParseSlug(name) 407 if err != nil { 408 return nil, err 409 } 410 411 version, err := c.Client.TerraformConfigLatest(user, name) 412 if err != nil { 413 return nil, err 414 } 415 416 variables := make(map[string]atlas.TFVar) 417 418 if version == nil { 419 return variables, nil 420 } 421 422 // Variables is superseded by TFVars 423 if version.TFVars == nil { 424 for k, v := range version.Variables { 425 variables[k] = atlas.TFVar{Key: k, Value: v} 426 } 427 } else { 428 for _, v := range version.TFVars { 429 variables[v.Key] = v 430 } 431 } 432 433 return variables, nil 434 } 435 436 func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) { 437 user, name, err := atlas.ParseSlug(opts.Name) 438 if err != nil { 439 return 0, err 440 } 441 442 data := &atlas.TerraformConfigVersion{ 443 TFVars: opts.TFVars, 444 } 445 446 version, err := c.Client.CreateTerraformConfigVersion( 447 user, name, data, opts.Archive, opts.Archive.Size) 448 if err != nil { 449 return 0, err 450 } 451 452 return version, nil 453 } 454 455 type mockPushClient struct { 456 File string 457 458 GetCalled bool 459 GetName string 460 GetResult map[string]atlas.TFVar 461 GetError error 462 463 UpsertCalled bool 464 UpsertOptions *pushUpsertOptions 465 UpsertVersion int 466 UpsertError error 467 } 468 469 func (c *mockPushClient) Get(name string) (map[string]atlas.TFVar, error) { 470 c.GetCalled = true 471 c.GetName = name 472 return c.GetResult, c.GetError 473 } 474 475 func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) { 476 f, err := os.Create(c.File) 477 if err != nil { 478 return 0, err 479 } 480 defer f.Close() 481 482 data := opts.Archive 483 size := opts.Archive.Size 484 if _, err := io.CopyN(f, data, size); err != nil { 485 return 0, err 486 } 487 488 c.UpsertCalled = true 489 c.UpsertOptions = opts 490 return c.UpsertVersion, c.UpsertError 491 }