github.com/hobbeswalsh/terraform@v0.3.7-0.20150619183303-ad17cf55a0fa/command/push.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/hashicorp/atlas-go/archive" 11 "github.com/hashicorp/atlas-go/v1" 12 ) 13 14 type PushCommand struct { 15 Meta 16 17 // client is the client to use for the actual push operations. 18 // If this isn't set, then the Atlas client is used. This should 19 // really only be set for testing reasons (and is hence not exported). 20 client pushClient 21 } 22 23 func (c *PushCommand) Run(args []string) int { 24 var atlasAddress, atlasToken string 25 var archiveVCS, moduleUpload bool 26 var name string 27 args = c.Meta.process(args, true) 28 cmdFlags := c.Meta.flagSet("push") 29 cmdFlags.StringVar(&atlasAddress, "atlas-address", "", "") 30 cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") 31 cmdFlags.StringVar(&atlasToken, "token", "", "") 32 cmdFlags.BoolVar(&moduleUpload, "upload-modules", true, "") 33 cmdFlags.StringVar(&name, "name", "", "") 34 cmdFlags.BoolVar(&archiveVCS, "vcs", true, "") 35 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 36 if err := cmdFlags.Parse(args); err != nil { 37 return 1 38 } 39 40 // The pwd is used for the configuration path if one is not given 41 pwd, err := os.Getwd() 42 if err != nil { 43 c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) 44 return 1 45 } 46 47 // Get the path to the configuration depending on the args. 48 var configPath string 49 args = cmdFlags.Args() 50 if len(args) > 1 { 51 c.Ui.Error("The apply command expects at most one argument.") 52 cmdFlags.Usage() 53 return 1 54 } else if len(args) == 1 { 55 configPath = args[0] 56 } else { 57 configPath = pwd 58 } 59 60 // Verify the state is remote, we can't push without a remote state 61 s, err := c.State() 62 if err != nil { 63 c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err)) 64 return 1 65 } 66 if !s.State().IsRemote() { 67 c.Ui.Error( 68 "Remote state is not enabled. For Atlas to run Terraform\n" + 69 "for you, remote state must be used and configured. Remote\n" + 70 "state via any backend is accepted, not just Atlas. To\n" + 71 "configure remote state, use the `terraform remote config`\n" + 72 "command.") 73 return 1 74 } 75 76 // Build the context based on the arguments given 77 ctx, planned, err := c.Context(contextOpts{ 78 Path: configPath, 79 StatePath: c.Meta.statePath, 80 }) 81 if err != nil { 82 c.Ui.Error(err.Error()) 83 return 1 84 } 85 if planned { 86 c.Ui.Error( 87 "A plan file cannot be given as the path to the configuration.\n" + 88 "A path to a module (directory with configuration) must be given.") 89 return 1 90 } 91 92 // Get the configuration 93 config := ctx.Module().Config() 94 if name == "" { 95 if config.Atlas == nil || config.Atlas.Name == "" { 96 c.Ui.Error( 97 "The name of this Terraform configuration in Atlas must be\n" + 98 "specified within your configuration or the command-line. To\n" + 99 "set it on the command-line, use the `-name` parameter.") 100 return 1 101 } 102 name = config.Atlas.Name 103 } 104 105 // Initialize the client if it isn't given. 106 if c.client == nil { 107 // Make sure to nil out our client so our token isn't sitting around 108 defer func() { c.client = nil }() 109 110 // Initialize it to the default client, we set custom settings later 111 client := atlas.DefaultClient() 112 if atlasAddress != "" { 113 client, err = atlas.NewClient(atlasAddress) 114 if err != nil { 115 c.Ui.Error(fmt.Sprintf("Error initializing Atlas client: %s", err)) 116 return 1 117 } 118 } 119 120 if atlasToken != "" { 121 client.Token = atlasToken 122 } 123 124 c.client = &atlasPushClient{Client: client} 125 } 126 127 // Get the variables we might already have 128 vars, err := c.client.Get(name) 129 if err != nil { 130 c.Ui.Error(fmt.Sprintf( 131 "Error looking up previously pushed configuration: %s", err)) 132 return 1 133 } 134 for k, v := range vars { 135 // Local variables override remote ones 136 if _, exists := ctx.Variables()[k]; exists { 137 continue 138 } 139 ctx.SetVariable(k, v) 140 } 141 142 // Ask for input 143 if err := ctx.Input(c.InputMode()); err != nil { 144 c.Ui.Error(fmt.Sprintf( 145 "Error while asking for variable input:\n\n%s", err)) 146 return 1 147 } 148 149 // Build the archiving options, which includes everything it can 150 // by default according to VCS rules but forcing the data directory. 151 archiveOpts := &archive.ArchiveOpts{ 152 VCS: archiveVCS, 153 Extra: map[string]string{ 154 DefaultDataDir: c.DataDir(), 155 }, 156 } 157 if !moduleUpload { 158 // If we're not uploading modules, then exclude the modules dir. 159 archiveOpts.Exclude = append( 160 archiveOpts.Exclude, 161 filepath.Join(c.DataDir(), "modules")) 162 } 163 164 archiveR, err := archive.CreateArchive(configPath, archiveOpts) 165 if err != nil { 166 c.Ui.Error(fmt.Sprintf( 167 "An error has occurred while archiving the module for uploading:\n"+ 168 "%s", err)) 169 return 1 170 } 171 172 // Upsert! 173 opts := &pushUpsertOptions{ 174 Name: name, 175 Archive: archiveR, 176 Variables: ctx.Variables(), 177 } 178 vsn, err := c.client.Upsert(opts) 179 if err != nil { 180 c.Ui.Error(fmt.Sprintf( 181 "An error occurred while uploading the module:\n\n%s", err)) 182 return 1 183 } 184 185 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 186 "[reset][bold][green]Configuration %q uploaded! (v%d)", 187 name, vsn))) 188 return 0 189 } 190 191 func (c *PushCommand) Help() string { 192 helpText := ` 193 Usage: terraform push [options] [DIR] 194 195 Upload this Terraform module to an Atlas server for remote 196 infrastructure management. 197 198 Options: 199 200 -atlas-address=<url> An alternate address to an Atlas instance. Defaults 201 to https://atlas.hashicorp.com 202 203 -upload-modules=true If true (default), then the modules are locked at 204 their current checkout and uploaded completely. This 205 prevents Atlas from running "terraform get". 206 207 -name=<name> Name of the configuration in Atlas. This can also 208 be set in the configuration itself. Format is 209 typically: "username/name". 210 211 -token=<token> Access token to use to upload. If blank or unspecified, 212 the ATLAS_TOKEN environmental variable will be used. 213 214 -var 'foo=bar' Set a variable in the Terraform configuration. This 215 flag can be set multiple times. 216 217 -var-file=foo Set variables in the Terraform configuration from 218 a file. If "terraform.tfvars" is present, it will be 219 automatically loaded if this flag is not specified. 220 221 -vcs=true If true (default), push will upload only files 222 comitted to your VCS, if detected. 223 224 ` 225 return strings.TrimSpace(helpText) 226 } 227 228 func (c *PushCommand) Synopsis() string { 229 return "Upload this Terraform module to Atlas to run" 230 } 231 232 // pushClient is implementd internally to control where pushes go. This is 233 // either to Atlas or a mock for testing. 234 type pushClient interface { 235 Get(string) (map[string]string, error) 236 Upsert(*pushUpsertOptions) (int, error) 237 } 238 239 type pushUpsertOptions struct { 240 Name string 241 Archive *archive.Archive 242 Variables map[string]string 243 } 244 245 type atlasPushClient struct { 246 Client *atlas.Client 247 } 248 249 func (c *atlasPushClient) Get(name string) (map[string]string, error) { 250 user, name, err := atlas.ParseSlug(name) 251 if err != nil { 252 return nil, err 253 } 254 255 version, err := c.Client.TerraformConfigLatest(user, name) 256 if err != nil { 257 return nil, err 258 } 259 260 var variables map[string]string 261 if version != nil { 262 variables = version.Variables 263 } 264 265 return variables, nil 266 } 267 268 func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) { 269 user, name, err := atlas.ParseSlug(opts.Name) 270 if err != nil { 271 return 0, err 272 } 273 274 data := &atlas.TerraformConfigVersion{ 275 Variables: opts.Variables, 276 } 277 278 version, err := c.Client.CreateTerraformConfigVersion( 279 user, name, data, opts.Archive, opts.Archive.Size) 280 if err != nil { 281 return 0, err 282 } 283 284 return version, nil 285 } 286 287 type mockPushClient struct { 288 File string 289 290 GetCalled bool 291 GetName string 292 GetResult map[string]string 293 GetError error 294 295 UpsertCalled bool 296 UpsertOptions *pushUpsertOptions 297 UpsertVersion int 298 UpsertError error 299 } 300 301 func (c *mockPushClient) Get(name string) (map[string]string, error) { 302 c.GetCalled = true 303 c.GetName = name 304 return c.GetResult, c.GetError 305 } 306 307 func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) { 308 f, err := os.Create(c.File) 309 if err != nil { 310 return 0, err 311 } 312 defer f.Close() 313 314 data := opts.Archive 315 size := opts.Archive.Size 316 if _, err := io.CopyN(f, data, size); err != nil { 317 return 0, err 318 } 319 320 c.UpsertCalled = true 321 c.UpsertOptions = opts 322 return c.UpsertVersion, c.UpsertError 323 }