github.com/tarrant/terraform@v0.3.8-0.20150402012457-f68c9eee638e/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, false) 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 ctx.SetVariable(k, v) 136 } 137 138 // Ask for input 139 if err := ctx.Input(c.InputMode()); err != nil { 140 c.Ui.Error(fmt.Sprintf( 141 "Error while asking for variable input:\n\n%s", err)) 142 return 1 143 } 144 145 // Build the archiving options, which includes everything it can 146 // by default according to VCS rules but forcing the data directory. 147 archiveOpts := &archive.ArchiveOpts{ 148 VCS: archiveVCS, 149 Extra: map[string]string{ 150 DefaultDataDir: c.DataDir(), 151 }, 152 } 153 if !moduleUpload { 154 // If we're not uploading modules, then exclude the modules dir. 155 archiveOpts.Exclude = append( 156 archiveOpts.Exclude, 157 filepath.Join(c.DataDir(), "modules")) 158 } 159 160 archiveR, err := archive.CreateArchive(configPath, archiveOpts) 161 if err != nil { 162 c.Ui.Error(fmt.Sprintf( 163 "An error has occurred while archiving the module for uploading:\n"+ 164 "%s", err)) 165 return 1 166 } 167 168 // Upsert! 169 opts := &pushUpsertOptions{ 170 Name: name, 171 Archive: archiveR, 172 Variables: ctx.Variables(), 173 } 174 vsn, err := c.client.Upsert(opts) 175 if err != nil { 176 c.Ui.Error(fmt.Sprintf( 177 "An error occurred while uploading the module:\n\n%s", err)) 178 return 1 179 } 180 181 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 182 "[reset][bold][green]Configuration %q uploaded! (v%d)", 183 name, vsn))) 184 return 0 185 } 186 187 func (c *PushCommand) Help() string { 188 helpText := ` 189 Usage: terraform push [options] [DIR] 190 191 Upload this Terraform module to an Atlas server for remote 192 infrastructure management. 193 194 Options: 195 196 -atlas-address=<url> An alternate address to an Atlas instance. Defaults 197 to https://atlas.hashicorp.com 198 199 -upload-modules=true If true (default), then the modules are locked at 200 their current checkout and uploaded completely. This 201 prevents Atlas from running "terraform get". 202 203 -name=<name> Name of the configuration in Atlas. This can also 204 be set in the configuration itself. Format is 205 typically: "username/name". 206 207 -token=<token> Access token to use to upload. If blank or unspecified, 208 the ATLAS_TOKEN environmental variable will be used. 209 210 -vcs=true If true (default), push will upload only files 211 comitted to your VCS, if detected. 212 213 ` 214 return strings.TrimSpace(helpText) 215 } 216 217 func (c *PushCommand) Synopsis() string { 218 return "Upload this Terraform module to Atlas to run" 219 } 220 221 // pushClient is implementd internally to control where pushes go. This is 222 // either to Atlas or a mock for testing. 223 type pushClient interface { 224 Get(string) (map[string]string, error) 225 Upsert(*pushUpsertOptions) (int, error) 226 } 227 228 type pushUpsertOptions struct { 229 Name string 230 Archive *archive.Archive 231 Variables map[string]string 232 } 233 234 type atlasPushClient struct { 235 Client *atlas.Client 236 } 237 238 func (c *atlasPushClient) Get(name string) (map[string]string, error) { 239 user, name, err := atlas.ParseSlug(name) 240 if err != nil { 241 return nil, err 242 } 243 244 version, err := c.Client.TerraformConfigLatest(user, name) 245 if err != nil { 246 return nil, err 247 } 248 249 var variables map[string]string 250 if version != nil { 251 variables = version.Variables 252 } 253 254 return variables, nil 255 } 256 257 func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) { 258 user, name, err := atlas.ParseSlug(opts.Name) 259 if err != nil { 260 return 0, err 261 } 262 263 data := &atlas.TerraformConfigVersion{ 264 Variables: opts.Variables, 265 } 266 267 version, err := c.Client.CreateTerraformConfigVersion( 268 user, name, data, opts.Archive, opts.Archive.Size) 269 if err != nil { 270 return 0, err 271 } 272 273 return version, nil 274 } 275 276 type mockPushClient struct { 277 File string 278 279 GetCalled bool 280 GetName string 281 GetResult map[string]string 282 GetError error 283 284 UpsertCalled bool 285 UpsertOptions *pushUpsertOptions 286 UpsertVersion int 287 UpsertError error 288 } 289 290 func (c *mockPushClient) Get(name string) (map[string]string, error) { 291 c.GetCalled = true 292 c.GetName = name 293 return c.GetResult, c.GetError 294 } 295 296 func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) { 297 f, err := os.Create(c.File) 298 if err != nil { 299 return 0, err 300 } 301 defer f.Close() 302 303 data := opts.Archive 304 size := opts.Archive.Size 305 if _, err := io.CopyN(f, data, size); err != nil { 306 return 0, err 307 } 308 309 c.UpsertCalled = true 310 c.UpsertOptions = opts 311 return c.UpsertVersion, c.UpsertError 312 }