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