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