├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── archive
├── archive.go
├── archive_test.go
├── test-fixtures
│ ├── archive-dir-mode
│ │ └── file.txt
│ ├── archive-file-compressed
│ │ └── file.tar.gz
│ ├── archive-file
│ │ └── foo.txt
│ ├── archive-flat
│ │ ├── baz.txt
│ │ └── foo.txt
│ ├── archive-git
│ │ ├── DOTgit
│ │ │ ├── COMMIT_EDITMSG
│ │ │ ├── HEAD
│ │ │ ├── config
│ │ │ ├── description
│ │ │ ├── hooks
│ │ │ │ ├── applypatch-msg.sample
│ │ │ │ ├── commit-msg.sample
│ │ │ │ ├── post-update.sample
│ │ │ │ ├── pre-applypatch.sample
│ │ │ │ ├── pre-commit.sample
│ │ │ │ ├── pre-push.sample
│ │ │ │ ├── pre-rebase.sample
│ │ │ │ ├── prepare-commit-msg.sample
│ │ │ │ └── update.sample
│ │ │ ├── index
│ │ │ ├── info
│ │ │ │ └── exclude
│ │ │ ├── logs
│ │ │ │ ├── HEAD
│ │ │ │ └── refs
│ │ │ │ │ └── heads
│ │ │ │ │ └── master
│ │ │ ├── objects
│ │ │ │ ├── 25
│ │ │ │ │ └── 7cc5642cb1a054f08cc83f2d943e56fd3ebe99
│ │ │ │ ├── 57
│ │ │ │ │ └── 16ca5987cbf97d6bb54920bea6adde242d87e6
│ │ │ │ ├── 75
│ │ │ │ │ └── 25d17cbbb56f3253a20903ffddc07c6c935c76
│ │ │ │ ├── 7e
│ │ │ │ │ └── 49ea5550b356e32b63c044201f5f7da1e0925f
│ │ │ │ └── 7f
│ │ │ │ │ └── 7402c7d2a6e71ca3db3e236099771b160b8ad1
│ │ │ └── refs
│ │ │ │ └── heads
│ │ │ │ └── master
│ │ ├── bar.txt
│ │ ├── foo.txt
│ │ ├── subdir
│ │ │ └── hello.txt
│ │ └── untracked.txt
│ ├── archive-hg
│ │ ├── .hg
│ │ │ ├── 00changelog.i
│ │ │ ├── cache
│ │ │ │ └── branch2-served
│ │ │ ├── dirstate
│ │ │ ├── last-message.txt
│ │ │ ├── requires
│ │ │ ├── store
│ │ │ │ ├── 00changelog.i
│ │ │ │ ├── 00manifest.i
│ │ │ │ ├── data
│ │ │ │ │ ├── bar.txt.i
│ │ │ │ │ ├── foo.txt.i
│ │ │ │ │ └── subdir
│ │ │ │ │ │ └── hello.txt.i
│ │ │ │ ├── fncache
│ │ │ │ ├── phaseroots
│ │ │ │ ├── undo
│ │ │ │ └── undo.phaseroots
│ │ │ ├── undo.bookmarks
│ │ │ ├── undo.branch
│ │ │ ├── undo.desc
│ │ │ └── undo.dirstate
│ │ ├── bar.txt
│ │ ├── foo.txt
│ │ └── subdir
│ │ │ └── hello.txt
│ ├── archive-subdir-splat
│ │ ├── bar.txt
│ │ └── build
│ │ │ ├── darwin-amd64
│ │ │ └── build.txt
│ │ │ └── linux-amd64
│ │ │ └── build.txt
│ ├── archive-subdir
│ │ ├── bar.txt
│ │ ├── foo.txt
│ │ └── subdir
│ │ │ └── hello.txt
│ ├── archive-symlink-file
│ │ ├── link
│ │ │ ├── deeper
│ │ │ │ ├── adeeperlink
│ │ │ │ ├── linklink
│ │ │ │ └── linklinklink
│ │ │ └── link
│ │ └── real
│ │ │ └── foo.txt
│ └── archive-symlink
│ │ ├── link
│ │ └── link
│ │ └── real
│ │ └── foo.txt
├── vcs.go
└── vcs_test.go
└── v1
├── application.go
├── application_test.go
├── artifact.go
├── artifact_test.go
├── atlas_test.go
├── authentication.go
├── authentication_test.go
├── build_config.go
├── build_config_test.go
├── client.go
├── client_test.go
├── terraform.go
├── terraform_test.go
├── util.go
└── util_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Go ###
2 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
3 | *.o
4 | *.a
5 | *.so
6 |
7 | # Folders
8 | _obj
9 | _test
10 |
11 | # Architecture specific extensions/prefixes
12 | *.[568vq]
13 | [568vq].out
14 |
15 | *.cgo1.go
16 | *.cgo2.c
17 | _cgo_defun.c
18 | _cgo_gotypes.go
19 | _cgo_export.*
20 |
21 | _testmain.go
22 |
23 | *.exe
24 | *.test
25 | *.prof
26 |
27 | /bin/
28 | /build/
29 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: go
4 |
5 | go:
6 | - 1.7
7 | - 1.8
8 |
9 | branches:
10 | only:
11 | - master
12 |
13 | script: make deps test
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 HashiCorp, Inc.
2 |
3 | Mozilla Public License, version 2.0
4 |
5 | 1. Definitions
6 |
7 | 1.1. “Contributor”
8 |
9 | means each individual or legal entity that creates, contributes to the
10 | creation of, or owns Covered Software.
11 |
12 | 1.2. “Contributor Version”
13 |
14 | means the combination of the Contributions of others (if any) used by a
15 | Contributor and that particular Contributor’s Contribution.
16 |
17 | 1.3. “Contribution”
18 |
19 | means Covered Software of a particular Contributor.
20 |
21 | 1.4. “Covered Software”
22 |
23 | means Source Code Form to which the initial Contributor has attached the
24 | notice in Exhibit A, the Executable Form of such Source Code Form, and
25 | Modifications of such Source Code Form, in each case including portions
26 | thereof.
27 |
28 | 1.5. “Incompatible With Secondary Licenses”
29 | means
30 |
31 | a. that the initial Contributor has attached the notice described in
32 | Exhibit B to the Covered Software; or
33 |
34 | b. that the Covered Software was made available under the terms of version
35 | 1.1 or earlier of the License, but not also under the terms of a
36 | Secondary License.
37 |
38 | 1.6. “Executable Form”
39 |
40 | means any form of the work other than Source Code Form.
41 |
42 | 1.7. “Larger Work”
43 |
44 | means a work that combines Covered Software with other material, in a separate
45 | file or files, that is not Covered Software.
46 |
47 | 1.8. “License”
48 |
49 | means this document.
50 |
51 | 1.9. “Licensable”
52 |
53 | means having the right to grant, to the maximum extent possible, whether at the
54 | time of the initial grant or subsequently, any and all of the rights conveyed by
55 | this License.
56 |
57 | 1.10. “Modifications”
58 |
59 | means any of the following:
60 |
61 | a. any file in Source Code Form that results from an addition to, deletion
62 | from, or modification of the contents of Covered Software; or
63 |
64 | b. any new file in Source Code Form that contains any Covered Software.
65 |
66 | 1.11. “Patent Claims” of a Contributor
67 |
68 | means any patent claim(s), including without limitation, method, process,
69 | and apparatus claims, in any patent Licensable by such Contributor that
70 | would be infringed, but for the grant of the License, by the making,
71 | using, selling, offering for sale, having made, import, or transfer of
72 | either its Contributions or its Contributor Version.
73 |
74 | 1.12. “Secondary License”
75 |
76 | means either the GNU General Public License, Version 2.0, the GNU Lesser
77 | General Public License, Version 2.1, the GNU Affero General Public
78 | License, Version 3.0, or any later versions of those licenses.
79 |
80 | 1.13. “Source Code Form”
81 |
82 | means the form of the work preferred for making modifications.
83 |
84 | 1.14. “You” (or “Your”)
85 |
86 | means an individual or a legal entity exercising rights under this
87 | License. For legal entities, “You” includes any entity that controls, is
88 | controlled by, or is under common control with You. For purposes of this
89 | definition, “control” means (a) the power, direct or indirect, to cause
90 | the direction or management of such entity, whether by contract or
91 | otherwise, or (b) ownership of more than fifty percent (50%) of the
92 | outstanding shares or beneficial ownership of such entity.
93 |
94 |
95 | 2. License Grants and Conditions
96 |
97 | 2.1. Grants
98 |
99 | Each Contributor hereby grants You a world-wide, royalty-free,
100 | non-exclusive license:
101 |
102 | a. under intellectual property rights (other than patent or trademark)
103 | Licensable by such Contributor to use, reproduce, make available,
104 | modify, display, perform, distribute, and otherwise exploit its
105 | Contributions, either on an unmodified basis, with Modifications, or as
106 | part of a Larger Work; and
107 |
108 | b. under Patent Claims of such Contributor to make, use, sell, offer for
109 | sale, have made, import, and otherwise transfer either its Contributions
110 | or its Contributor Version.
111 |
112 | 2.2. Effective Date
113 |
114 | The licenses granted in Section 2.1 with respect to any Contribution become
115 | effective for each Contribution on the date the Contributor first distributes
116 | such Contribution.
117 |
118 | 2.3. Limitations on Grant Scope
119 |
120 | The licenses granted in this Section 2 are the only rights granted under this
121 | License. No additional rights or licenses will be implied from the distribution
122 | or licensing of Covered Software under this License. Notwithstanding Section
123 | 2.1(b) above, no patent license is granted by a Contributor:
124 |
125 | a. for any code that a Contributor has removed from Covered Software; or
126 |
127 | b. for infringements caused by: (i) Your and any other third party’s
128 | modifications of Covered Software, or (ii) the combination of its
129 | Contributions with other software (except as part of its Contributor
130 | Version); or
131 |
132 | c. under Patent Claims infringed by Covered Software in the absence of its
133 | Contributions.
134 |
135 | This License does not grant any rights in the trademarks, service marks, or
136 | logos of any Contributor (except as may be necessary to comply with the
137 | notice requirements in Section 3.4).
138 |
139 | 2.4. Subsequent Licenses
140 |
141 | No Contributor makes additional grants as a result of Your choice to
142 | distribute the Covered Software under a subsequent version of this License
143 | (see Section 10.2) or under the terms of a Secondary License (if permitted
144 | under the terms of Section 3.3).
145 |
146 | 2.5. Representation
147 |
148 | Each Contributor represents that the Contributor believes its Contributions
149 | are its original creation(s) or it has sufficient rights to grant the
150 | rights to its Contributions conveyed by this License.
151 |
152 | 2.6. Fair Use
153 |
154 | This License is not intended to limit any rights You have under applicable
155 | copyright doctrines of fair use, fair dealing, or other equivalents.
156 |
157 | 2.7. Conditions
158 |
159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
160 | Section 2.1.
161 |
162 |
163 | 3. Responsibilities
164 |
165 | 3.1. Distribution of Source Form
166 |
167 | All distribution of Covered Software in Source Code Form, including any
168 | Modifications that You create or to which You contribute, must be under the
169 | terms of this License. You must inform recipients that the Source Code Form
170 | of the Covered Software is governed by the terms of this License, and how
171 | they can obtain a copy of this License. You may not attempt to alter or
172 | restrict the recipients’ rights in the Source Code Form.
173 |
174 | 3.2. Distribution of Executable Form
175 |
176 | If You distribute Covered Software in Executable Form then:
177 |
178 | a. such Covered Software must also be made available in Source Code Form,
179 | as described in Section 3.1, and You must inform recipients of the
180 | Executable Form how they can obtain a copy of such Source Code Form by
181 | reasonable means in a timely manner, at a charge no more than the cost
182 | of distribution to the recipient; and
183 |
184 | b. You may distribute such Executable Form under the terms of this License,
185 | or sublicense it under different terms, provided that the license for
186 | the Executable Form does not attempt to limit or alter the recipients’
187 | rights in the Source Code Form under this License.
188 |
189 | 3.3. Distribution of a Larger Work
190 |
191 | You may create and distribute a Larger Work under terms of Your choice,
192 | provided that You also comply with the requirements of this License for the
193 | Covered Software. If the Larger Work is a combination of Covered Software
194 | with a work governed by one or more Secondary Licenses, and the Covered
195 | Software is not Incompatible With Secondary Licenses, this License permits
196 | You to additionally distribute such Covered Software under the terms of
197 | such Secondary License(s), so that the recipient of the Larger Work may, at
198 | their option, further distribute the Covered Software under the terms of
199 | either this License or such Secondary License(s).
200 |
201 | 3.4. Notices
202 |
203 | You may not remove or alter the substance of any license notices (including
204 | copyright notices, patent notices, disclaimers of warranty, or limitations
205 | of liability) contained within the Source Code Form of the Covered
206 | Software, except that You may alter any license notices to the extent
207 | required to remedy known factual inaccuracies.
208 |
209 | 3.5. Application of Additional Terms
210 |
211 | You may choose to offer, and to charge a fee for, warranty, support,
212 | indemnity or liability obligations to one or more recipients of Covered
213 | Software. However, You may do so only on Your own behalf, and not on behalf
214 | of any Contributor. You must make it absolutely clear that any such
215 | warranty, support, indemnity, or liability obligation is offered by You
216 | alone, and You hereby agree to indemnify every Contributor for any
217 | liability incurred by such Contributor as a result of warranty, support,
218 | indemnity or liability terms You offer. You may include additional
219 | disclaimers of warranty and limitations of liability specific to any
220 | jurisdiction.
221 |
222 | 4. Inability to Comply Due to Statute or Regulation
223 |
224 | If it is impossible for You to comply with any of the terms of this License
225 | with respect to some or all of the Covered Software due to statute, judicial
226 | order, or regulation then You must: (a) comply with the terms of this License
227 | to the maximum extent possible; and (b) describe the limitations and the code
228 | they affect. Such description must be placed in a text file included with all
229 | distributions of the Covered Software under this License. Except to the
230 | extent prohibited by statute or regulation, such description must be
231 | sufficiently detailed for a recipient of ordinary skill to be able to
232 | understand it.
233 |
234 | 5. Termination
235 |
236 | 5.1. The rights granted under this License will terminate automatically if You
237 | fail to comply with any of its terms. However, if You become compliant,
238 | then the rights granted under this License from a particular Contributor
239 | are reinstated (a) provisionally, unless and until such Contributor
240 | explicitly and finally terminates Your grants, and (b) on an ongoing basis,
241 | if such Contributor fails to notify You of the non-compliance by some
242 | reasonable means prior to 60 days after You have come back into compliance.
243 | Moreover, Your grants from a particular Contributor are reinstated on an
244 | ongoing basis if such Contributor notifies You of the non-compliance by
245 | some reasonable means, this is the first time You have received notice of
246 | non-compliance with this License from such Contributor, and You become
247 | compliant prior to 30 days after Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions, counter-claims,
251 | and cross-claims) alleging that a Contributor Version directly or
252 | indirectly infringes any patent, then the rights granted to You by any and
253 | all Contributors for the Covered Software under Section 2.1 of this License
254 | shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
257 | license agreements (excluding distributors and resellers) which have been
258 | validly granted by You or Your distributors under this License prior to
259 | termination shall survive termination.
260 |
261 | 6. Disclaimer of Warranty
262 |
263 | Covered Software is provided under this License on an “as is” basis, without
264 | warranty of any kind, either expressed, implied, or statutory, including,
265 | without limitation, warranties that the Covered Software is free of defects,
266 | merchantable, fit for a particular purpose or non-infringing. The entire
267 | risk as to the quality and performance of the Covered Software is with You.
268 | Should any Covered Software prove defective in any respect, You (not any
269 | Contributor) assume the cost of any necessary servicing, repair, or
270 | correction. This disclaimer of warranty constitutes an essential part of this
271 | License. No use of any Covered Software is authorized under this License
272 | except under this disclaimer.
273 |
274 | 7. Limitation of Liability
275 |
276 | Under no circumstances and under no legal theory, whether tort (including
277 | negligence), contract, or otherwise, shall any Contributor, or anyone who
278 | distributes Covered Software as permitted above, be liable to You for any
279 | direct, indirect, special, incidental, or consequential damages of any
280 | character including, without limitation, damages for lost profits, loss of
281 | goodwill, work stoppage, computer failure or malfunction, or any and all
282 | other commercial damages or losses, even if such party shall have been
283 | informed of the possibility of such damages. This limitation of liability
284 | shall not apply to liability for death or personal injury resulting from such
285 | party’s negligence to the extent applicable law prohibits such limitation.
286 | Some jurisdictions do not allow the exclusion or limitation of incidental or
287 | consequential damages, so this exclusion and limitation may not apply to You.
288 |
289 | 8. Litigation
290 |
291 | Any litigation relating to this License may be brought only in the courts of
292 | a jurisdiction where the defendant maintains its principal place of business
293 | and such litigation shall be governed by laws of that jurisdiction, without
294 | reference to its conflict-of-law provisions. Nothing in this Section shall
295 | prevent a party’s ability to bring cross-claims or counter-claims.
296 |
297 | 9. Miscellaneous
298 |
299 | This License represents the complete agreement concerning the subject matter
300 | hereof. If any provision of this License is held to be unenforceable, such
301 | provision shall be reformed only to the extent necessary to make it
302 | enforceable. Any law or regulation which provides that the language of a
303 | contract shall be construed against the drafter shall not be used to construe
304 | this License against a Contributor.
305 |
306 |
307 | 10. Versions of the License
308 |
309 | 10.1. New Versions
310 |
311 | Mozilla Foundation is the license steward. Except as provided in Section
312 | 10.3, no one other than the license steward has the right to modify or
313 | publish new versions of this License. Each version will be given a
314 | distinguishing version number.
315 |
316 | 10.2. Effect of New Versions
317 |
318 | You may distribute the Covered Software under the terms of the version of
319 | the License under which You originally received the Covered Software, or
320 | under the terms of any subsequent version published by the license
321 | steward.
322 |
323 | 10.3. Modified Versions
324 |
325 | If you create software not governed by this License, and you want to
326 | create a new license for such software, you may create and use a modified
327 | version of this License if you rename the license and remove any
328 | references to the name of the license steward (except to note that such
329 | modified license differs from this License).
330 |
331 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
332 | If You choose to distribute Source Code Form that is Incompatible With
333 | Secondary Licenses under the terms of this version of the License, the
334 | notice described in Exhibit B of this License must be attached.
335 |
336 | Exhibit A - Source Code Form License Notice
337 |
338 | This Source Code Form is subject to the
339 | terms of the Mozilla Public License, v.
340 | 2.0. If a copy of the MPL was not
341 | distributed with this file, You can
342 | obtain one at
343 | http://mozilla.org/MPL/2.0/.
344 |
345 | If it is not possible or desirable to put the notice in a particular file, then
346 | You may include the notice in a location (such as a LICENSE file in a relevant
347 | directory) where a recipient would be likely to look for such a notice.
348 |
349 | You may add additional accurate notices of copyright ownership.
350 |
351 | Exhibit B - “Incompatible With Secondary Licenses” Notice
352 |
353 | This Source Code Form is “Incompatible
354 | With Secondary Licenses”, as defined by
355 | the Mozilla Public License, v. 2.0.
356 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TEST?=./...
2 | DEPS = $(shell go list -f '{{range .TestImports}}{{.}} {{end}}' ./...)
3 |
4 | all: deps build
5 |
6 | deps:
7 | go get -d -v ./...
8 | echo $(DEPS) | xargs -n1 go get -d
9 |
10 | build:
11 | @mkdir -p bin/
12 | go build -o bin/atlas-go ./v1
13 |
14 | test:
15 | go test $(TEST) $(TESTARGS) -timeout=10s -parallel=4
16 | go vet $(TEST)
17 | go test $(TEST) -race
18 |
19 | .PHONY: all deps build test
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Atlas Go
2 | ========
3 | [][release]
4 | [][travis]
5 | [][godocs]
6 |
7 | [release]: https://github.com/hashicorp/atlas-go/releases
8 | [travis]: http://travis-ci.org/hashicorp/atlas-go
9 | [godocs]: http://godoc.org/github.com/hashicorp/atlas-go
10 |
11 | Atlas Go is the official Go client for [HashiCorp's Atlas][Atlas] service.
12 |
13 | Usage
14 | -----
15 | ### Authenticating with username and password
16 | Atlas Go can automatically generate an API authentication token given a username
17 | and password. For example:
18 |
19 | ```go
20 | client := atlas.DefaultClient()
21 | token, err := client.Login("username", "password")
22 | if err != nil {
23 | panic(err)
24 | }
25 | ```
26 |
27 | The `Login` function returns an API token that can be used to sign requests.
28 | This function also sets the `Token` parameter on the Atlas Client, so future
29 | requests are signed with this access token.
30 |
31 | **If you have two-factor authentication enabled, you must manually generate an
32 | access token on the Atlas website.**
33 |
34 | ### Usage with on-premise Atlas
35 | Atlas Go supports on-premise Atlas installs, but you must specify the URL of the
36 | Atlas server in the client:
37 |
38 | ```go
39 | client, err := atlas.NewClient("https://url.to.your.atlas.server")
40 | if err != nil {
41 | panic(err)
42 | }
43 | ```
44 |
45 | Example
46 | -------
47 | The following example generates a new access token for a user named "sethvargo",
48 | generates a new Application named "frontend", and uploads the contents of a path
49 | to said application with some user-supplied metadata:
50 |
51 | ```go
52 | client := atlas.DefaultClient()
53 | token, err := client.Login("sethvargo", "b@c0n")
54 | if err != nil {
55 | log.Fatalf("err logging in: %s", err)
56 | }
57 |
58 | app, err := client.CreateApp("sethvargo", "frontend")
59 | if err != nil {
60 | log.Fatalf("err creating app: %s", err)
61 | }
62 |
63 | metadata := map[string]interface{
64 | "developed-on": runtime.GOOS,
65 | }
66 |
67 | data, size := functionThatReturnsAnIOReaderAndSize()
68 | version, err := client.UploadApp(app, metadata, data, size)
69 | if err != nil {
70 | log.Fatalf("err uploading app: %s", err)
71 | }
72 |
73 | // version is the unique version of the application that was just uploaded
74 | version
75 | ```
76 |
77 |
78 | FAQ
79 | ---
80 | **Q: Can I specify my token via an environment variable?**
81 | A: All of HashiCorp's products support the `ATLAS_TOKEN` environment variable.
82 | You can set this value in your shell profile or securely in your environment and
83 | it will be used.
84 |
85 | **Q: How can I authenticate if I have two-factor authentication enabled?**
86 | A: If you have two-factor authentication enabled, you must generate an access
87 | token via the Atlas website and pass it to the client initialization. The Atlas
88 | Go client does not support generating access tokens from two-factor
89 | authentication enabled accounts via the command line.
90 |
91 | **Q: Why do I need to specify the "user" for an Application, Build Configuration,
92 | and Runtime?**
93 | A: Since you can be a collaborator on different projects, we wanted to have
94 | absolute clarity around which artifact you are currently interacting with.
95 |
96 |
97 | Contributing
98 | ------------
99 | To hack on Atlas Go, you will need a modern [Go][] environment. To compile the `atlas-go` binary and run the test suite, simply execute:
100 |
101 | ```shell
102 | $ make
103 | ```
104 |
105 | This will compile the `atlas-go` binary into `bin/atlas-go` and run the test suite.
106 |
107 | If you just want to run the tests:
108 |
109 | ```shell
110 | $ make test
111 | ```
112 |
113 | Or to run a specific test in the suite:
114 |
115 | ```shell
116 | go test ./... -run SomeTestFunction_name
117 | ```
118 |
119 | Submit Pull Requests and Issues to the [Atlas Go project on GitHub][Atlas Go].
120 |
121 | [Atlas]: https://atlas.hashicorp.com "HashiCorp's Atlas"
122 | [Atlas Go]: https://github.com/hashicorp/atlas-go "Atlas Go on GitHub"
123 | [Go]: http://golang.org "Go the language"
124 |
--------------------------------------------------------------------------------
/archive/archive.go:
--------------------------------------------------------------------------------
1 | // archive is package that helps create archives in a format that
2 | // Atlas expects with its various upload endpoints.
3 | package archive
4 |
5 | import (
6 | "archive/tar"
7 | "bufio"
8 | "compress/gzip"
9 | "fmt"
10 | "io"
11 | "io/ioutil"
12 | "log"
13 | "os"
14 | "path/filepath"
15 | "strings"
16 | )
17 |
18 | // Archive is the resulting archive. The archive data is generally streamed
19 | // so the io.ReadCloser can be used to backpressure the archive progress
20 | // and avoid memory pressure.
21 | type Archive struct {
22 | io.ReadCloser
23 |
24 | Size int64
25 | Metadata map[string]string
26 | }
27 |
28 | // ArchiveOpts are the options for defining how the archive will be built.
29 | type ArchiveOpts struct {
30 | // Exclude and Include are filters of files to include/exclude in
31 | // the archive when creating it from a directory. These filters should
32 | // be relative to the packaging directory and should be basic glob
33 | // patterns.
34 | Exclude []string
35 | Include []string
36 |
37 | // Extra is a mapping of extra files to include within the archive. The
38 | // key should be the path within the archive and the value should be
39 | // an absolute path to the file to put into the archive. These extra
40 | // files will override any other files in the archive.
41 | Extra map[string]string
42 |
43 | // VCS, if true, will detect and use a VCS system to determine what
44 | // files to include the archive.
45 | VCS bool
46 | }
47 |
48 | // IsSet says whether any options were set.
49 | func (o *ArchiveOpts) IsSet() bool {
50 | return len(o.Exclude) > 0 || len(o.Include) > 0 || o.VCS
51 | }
52 |
53 | // Constants related to setting special values for Extra in ArchiveOpts.
54 | const (
55 | // ExtraEntryDir just creates the Extra key as a directory entry.
56 | ExtraEntryDir = ""
57 | )
58 |
59 | // CreateArchive takes the given path and ArchiveOpts and archives it.
60 | //
61 | // The archive will be fully completed and put into a temporary file.
62 | // This must be done to retrieve the content length of the archive which
63 | // is needed for almost all operations involving archives with Atlas. Because
64 | // of this, sufficient disk space will be required to buffer the archive.
65 | func CreateArchive(path string, opts *ArchiveOpts) (*Archive, error) {
66 | log.Printf("[INFO] creating archive from %s", path)
67 |
68 | // Dereference any symlinks and determine the real path and info
69 | fi, err := os.Lstat(path)
70 | if err != nil {
71 | return nil, err
72 | }
73 | if fi.Mode()&os.ModeSymlink != 0 {
74 | path, fi, err = readLinkFull(path, fi)
75 | if err != nil {
76 | return nil, err
77 | }
78 | }
79 |
80 | // Windows
81 | path = filepath.ToSlash(path)
82 |
83 | // Direct file paths cannot have archive options
84 | if !fi.IsDir() && opts.IsSet() {
85 | return nil, fmt.Errorf(
86 | "options such as exclude, include, and VCS can't be set when " +
87 | "the path is a file.")
88 | }
89 |
90 | if fi.IsDir() {
91 | return archiveDir(path, opts)
92 | } else {
93 | return archiveFile(path)
94 | }
95 | }
96 |
97 | func archiveFile(path string) (*Archive, error) {
98 | f, err := os.Open(path)
99 | if err != nil {
100 | return nil, err
101 | }
102 |
103 | if _, err := gzip.NewReader(f); err == nil {
104 | // Reset the read offset for future reading
105 | if _, err := f.Seek(0, 0); err != nil {
106 | f.Close()
107 | return nil, err
108 | }
109 |
110 | // Get the file info for the size
111 | fi, err := f.Stat()
112 | if err != nil {
113 | f.Close()
114 | return nil, err
115 | }
116 |
117 | // This is a gzip file, let it through.
118 | return &Archive{ReadCloser: f, Size: fi.Size()}, nil
119 | }
120 |
121 | // Close the file, no use for it anymore
122 | f.Close()
123 |
124 | // We have a single file that is not gzipped. Compress it.
125 | path, err = filepath.Abs(path)
126 | if err != nil {
127 | return nil, err
128 | }
129 |
130 | // Act like we're compressing a directory, but only include this one
131 | // file.
132 | return archiveDir(filepath.Dir(path), &ArchiveOpts{
133 | Include: []string{filepath.Base(path)},
134 | })
135 | }
136 |
137 | func archiveDir(root string, opts *ArchiveOpts) (*Archive, error) {
138 |
139 | var vcsInclude []string
140 | var metadata map[string]string
141 | if opts.VCS {
142 | var err error
143 |
144 | if err = vcsPreflight(root); err != nil {
145 | return nil, err
146 | }
147 |
148 | vcsInclude, err = vcsFiles(root)
149 | if err != nil {
150 | return nil, err
151 | }
152 |
153 | metadata, err = vcsMetadata(root)
154 | if err != nil {
155 | return nil, err
156 | }
157 | }
158 |
159 | // Make sure the root path is absolute
160 | root, err := filepath.Abs(root)
161 | if err != nil {
162 | return nil, err
163 | }
164 |
165 | // Create the temporary file that we'll send the archive data to.
166 | archiveF, err := ioutil.TempFile("", "atlas-archive")
167 | if err != nil {
168 | return nil, err
169 | }
170 |
171 | // Create the wrapper for the result which will automatically
172 | // remove the temporary file on close.
173 | archiveWrapper := &readCloseRemover{F: archiveF}
174 |
175 | // Buffer the writer so that we can push as much data to disk at
176 | // a time as possible. 4M should be good.
177 | bufW := bufio.NewWriterSize(archiveF, 4096*1024)
178 |
179 | // Gzip compress all the output data
180 | gzipW := gzip.NewWriter(bufW)
181 |
182 | // Tar the file contents
183 | tarW := tar.NewWriter(gzipW)
184 |
185 | // First, walk the path and do the normal files
186 | werr := filepath.Walk(root, copyDirWalkFn(
187 | tarW, root, "", opts, vcsInclude))
188 | if werr == nil {
189 | // If that succeeded, handle the extra files
190 | werr = copyExtras(tarW, opts.Extra)
191 | }
192 |
193 | // Attempt to close all the things. If we get an error on the way
194 | // and we haven't had an error yet, then record that as the critical
195 | // error. But we still try to close everything.
196 |
197 | // Close the tar writer
198 | if err := tarW.Close(); err != nil && werr == nil {
199 | werr = err
200 | }
201 |
202 | // Close the gzip writer
203 | if err := gzipW.Close(); err != nil && werr == nil {
204 | werr = err
205 | }
206 |
207 | // Flush the buffer
208 | if err := bufW.Flush(); err != nil && werr == nil {
209 | werr = err
210 | }
211 |
212 | // If we had an error, then close the file (removing it) and
213 | // return the error.
214 | if werr != nil {
215 | archiveWrapper.Close()
216 | return nil, werr
217 | }
218 |
219 | // Seek to the beginning
220 | if _, err := archiveWrapper.F.Seek(0, 0); err != nil {
221 | archiveWrapper.Close()
222 | return nil, err
223 | }
224 |
225 | // Get the file information so we can get the size
226 | fi, err := archiveWrapper.F.Stat()
227 | if err != nil {
228 | archiveWrapper.Close()
229 | return nil, err
230 | }
231 |
232 | return &Archive{
233 | ReadCloser: archiveWrapper,
234 | Size: fi.Size(),
235 | Metadata: metadata,
236 | }, nil
237 | }
238 |
239 | func copyDirWalkFn(
240 | tarW *tar.Writer, root string, prefix string,
241 | opts *ArchiveOpts, vcsInclude []string) filepath.WalkFunc {
242 |
243 | errFunc := func(err error) filepath.WalkFunc {
244 | return func(string, os.FileInfo, error) error {
245 | return err
246 | }
247 | }
248 |
249 | // Windows
250 | root = filepath.ToSlash(root)
251 |
252 | var includeMap map[string]struct{}
253 |
254 | // If we have an include/exclude pattern set, then setup the lookup
255 | // table to determine what we want to include.
256 | if opts != nil && len(opts.Include) > 0 {
257 | includeMap = make(map[string]struct{})
258 | for _, pattern := range opts.Include {
259 | matches, err := filepath.Glob(filepath.Join(root, pattern))
260 | if err != nil {
261 | return errFunc(fmt.Errorf(
262 | "error checking include glob '%s': %s",
263 | pattern, err))
264 | }
265 |
266 | for _, path := range matches {
267 | // Windows
268 | path = filepath.ToSlash(path)
269 | subpath, err := filepath.Rel(root, path)
270 | subpath = filepath.ToSlash(subpath)
271 |
272 | if err != nil {
273 | return errFunc(err)
274 | }
275 |
276 | for {
277 | includeMap[subpath] = struct{}{}
278 | subpath = filepath.Dir(subpath)
279 | if subpath == "." {
280 | break
281 | }
282 | }
283 | }
284 | }
285 | }
286 |
287 | return func(path string, info os.FileInfo, err error) error {
288 | path = filepath.ToSlash(path)
289 |
290 | if err != nil {
291 | return err
292 | }
293 |
294 | // Get the relative path from the path since it contains the root
295 | // plus the path.
296 | subpath, err := filepath.Rel(root, path)
297 | if err != nil {
298 | return err
299 | }
300 | if subpath == "." {
301 | return nil
302 | }
303 | if prefix != "" {
304 | subpath = filepath.Join(prefix, subpath)
305 | }
306 | // Windows
307 | subpath = filepath.ToSlash(subpath)
308 |
309 | // If we have a list of VCS files, check that first
310 | skip := false
311 | if len(vcsInclude) > 0 {
312 | skip = true
313 | for _, f := range vcsInclude {
314 | if f == subpath {
315 | skip = false
316 | break
317 | }
318 |
319 | if info.IsDir() && strings.HasPrefix(f, subpath+"/") {
320 | skip = false
321 | break
322 | }
323 | }
324 | }
325 |
326 | // If include is present, we only include what is listed
327 | if len(includeMap) > 0 {
328 | if _, ok := includeMap[subpath]; !ok {
329 | skip = true
330 | }
331 | }
332 |
333 | // If exclude, it is one last gate to excluding files
334 | if opts != nil {
335 | for _, exclude := range opts.Exclude {
336 | match, err := filepath.Match(exclude, subpath)
337 | if err != nil {
338 | return err
339 | }
340 | if match {
341 | skip = true
342 | break
343 | }
344 | }
345 | }
346 |
347 | // If we have to skip this file, then skip it, properly skipping
348 | // children if we're a directory.
349 | if skip {
350 | if info.IsDir() {
351 | return filepath.SkipDir
352 | }
353 |
354 | return nil
355 | }
356 |
357 | // If this is a symlink, then we need to get the symlink target
358 | // rather than the symlink itself.
359 | if info.Mode()&os.ModeSymlink != 0 {
360 | target, info, err := readLinkFull(path, info)
361 | if err != nil {
362 | return err
363 | }
364 |
365 | // Copy the concrete entry for this path. This will either
366 | // be the file itself or just a directory entry.
367 | if err := copyConcreteEntry(tarW, subpath, target, info); err != nil {
368 | return err
369 | }
370 |
371 | if info.IsDir() {
372 | return filepath.Walk(target, copyDirWalkFn(
373 | tarW, target, subpath, opts, vcsInclude))
374 | }
375 | // return now so that we don't try to copy twice
376 | return nil
377 | }
378 |
379 | return copyConcreteEntry(tarW, subpath, path, info)
380 | }
381 | }
382 |
383 | func copyConcreteEntry(
384 | tarW *tar.Writer, entry string,
385 | path string, info os.FileInfo) error {
386 | // Windows
387 | path = filepath.ToSlash(path)
388 |
389 | // Build the file header for the tar entry
390 | header, err := tar.FileInfoHeader(info, path)
391 | if err != nil {
392 | return fmt.Errorf(
393 | "failed creating archive header: %s", path)
394 | }
395 |
396 | // Modify the header to properly be the full entry name
397 | header.Name = entry
398 | if info.IsDir() {
399 | header.Name += "/"
400 | }
401 |
402 | // Write the header first to the archive.
403 | if err := tarW.WriteHeader(header); err != nil {
404 | return fmt.Errorf(
405 | "failed writing archive header: %s", path)
406 | }
407 |
408 | // If it is a directory, then we're done (no body to write)
409 | if info.IsDir() {
410 | return nil
411 | }
412 |
413 | // Open the real file to write the data
414 | f, err := os.Open(path)
415 | if err != nil {
416 | return fmt.Errorf(
417 | "failed opening file '%s' to write compressed archive.", path)
418 | }
419 | defer f.Close()
420 |
421 | if _, err = io.Copy(tarW, f); err != nil {
422 | return fmt.Errorf(
423 | "failed copying file to archive: %s, %s", path, err)
424 | }
425 |
426 | return nil
427 | }
428 |
429 | func copyExtras(w *tar.Writer, extra map[string]string) error {
430 | var tmpDir string
431 | defer func() {
432 | if tmpDir != "" {
433 | os.RemoveAll(tmpDir)
434 | }
435 | }()
436 |
437 | for entry, path := range extra {
438 | // If the path is empty, then we set it to a generic empty directory
439 | if path == "" {
440 | // If tmpDir is still empty, then we create an empty dir
441 | if tmpDir == "" {
442 | td, err := ioutil.TempDir("", "archive")
443 | if err != nil {
444 | return err
445 | }
446 |
447 | tmpDir = td
448 | }
449 |
450 | path = tmpDir
451 | }
452 |
453 | info, err := os.Stat(path)
454 | if err != nil {
455 | return err
456 | }
457 |
458 | // No matter what, write the entry. If this is a directory,
459 | // it'll just write the directory header.
460 | if err := copyConcreteEntry(w, entry, path, info); err != nil {
461 | return err
462 | }
463 |
464 | // If this is a directory, then we walk the internal contents
465 | // and copy those as well.
466 | if info.IsDir() {
467 | err := filepath.Walk(path, copyDirWalkFn(
468 | w, path, entry, nil, nil))
469 | if err != nil {
470 | return err
471 | }
472 | }
473 | }
474 |
475 | return nil
476 | }
477 |
478 | func readLinkFull(path string, info os.FileInfo) (string, os.FileInfo, error) {
479 | target, err := filepath.EvalSymlinks(path)
480 | if err != nil {
481 | return "", nil, err
482 | }
483 |
484 | target, err = filepath.Abs(target)
485 | if err != nil {
486 | return "", nil, err
487 | }
488 |
489 | fi, err := os.Lstat(target)
490 | if err != nil {
491 | return "", nil, err
492 | }
493 |
494 | return target, fi, nil
495 | }
496 |
497 | // readCloseRemover is an io.ReadCloser implementation that will remove
498 | // the file on Close(). We use this to clean up our temporary file for
499 | // the archive.
500 | type readCloseRemover struct {
501 | F *os.File
502 | }
503 |
504 | func (r *readCloseRemover) Read(p []byte) (int, error) {
505 | return r.F.Read(p)
506 | }
507 |
508 | func (r *readCloseRemover) Close() error {
509 | // First close the file
510 | err := r.F.Close()
511 |
512 | // Next make sure to remove it, or at least try, regardless of error
513 | // above.
514 | os.Remove(r.F.Name())
515 |
516 | return err
517 | }
518 |
--------------------------------------------------------------------------------
/archive/archive_test.go:
--------------------------------------------------------------------------------
1 | package archive
2 |
3 | import (
4 | "archive/tar"
5 | "bytes"
6 | "compress/gzip"
7 | "io"
8 | "io/ioutil"
9 | "os"
10 | "os/exec"
11 | "path/filepath"
12 | "reflect"
13 | "runtime"
14 | "sort"
15 | "testing"
16 | )
17 |
18 | const fixturesDir = "./test-fixtures"
19 |
20 | var testHasGit bool
21 | var testHasHg bool
22 |
23 | func init() {
24 | if _, err := exec.LookPath("git"); err == nil {
25 | testHasGit = true
26 | }
27 |
28 | if _, err := exec.LookPath("hg"); err == nil {
29 | testHasHg = true
30 | }
31 | }
32 |
33 | func TestArchiveOptsIsSet(t *testing.T) {
34 | cases := []struct {
35 | Opts *ArchiveOpts
36 | Set bool
37 | }{
38 | {
39 | &ArchiveOpts{},
40 | false,
41 | },
42 | {
43 | &ArchiveOpts{VCS: true},
44 | true,
45 | },
46 | {
47 | &ArchiveOpts{Exclude: make([]string, 0, 0)},
48 | false,
49 | },
50 | {
51 | &ArchiveOpts{Exclude: []string{"foo"}},
52 | true,
53 | },
54 | {
55 | &ArchiveOpts{Include: make([]string, 0, 0)},
56 | false,
57 | },
58 | {
59 | &ArchiveOpts{Include: []string{"foo"}},
60 | true,
61 | },
62 | }
63 |
64 | for i, tc := range cases {
65 | if tc.Opts.IsSet() != tc.Set {
66 | t.Fatalf("%d: expected %#v", i, tc.Set)
67 | }
68 | }
69 | }
70 |
71 | func TestArchive_file(t *testing.T) {
72 | path := filepath.Join(testFixture("archive-file"), "foo.txt")
73 | r, err := CreateArchive(path, new(ArchiveOpts))
74 | if err != nil {
75 | t.Fatalf("err: %s", err)
76 | }
77 |
78 | expected := []string{
79 | "foo.txt",
80 | }
81 |
82 | entries := testArchive(t, r, false)
83 | if !reflect.DeepEqual(entries, expected) {
84 | t.Fatalf("bad: %#v", entries)
85 | }
86 | }
87 |
88 | func TestArchive_fileCompressed(t *testing.T) {
89 | path := filepath.Join(testFixture("archive-file-compressed"), "file.tar.gz")
90 | r, err := CreateArchive(path, new(ArchiveOpts))
91 | if err != nil {
92 | t.Fatalf("err: %s", err)
93 | }
94 |
95 | expected := []string{
96 | "./foo.txt",
97 | }
98 |
99 | entries := testArchive(t, r, false)
100 | if !reflect.DeepEqual(entries, expected) {
101 | t.Fatalf("bad: %#v", entries)
102 | }
103 | }
104 |
105 | func TestArchive_fileNoExist(t *testing.T) {
106 | tf := tempFile(t)
107 | if err := os.Remove(tf); err != nil {
108 | t.Fatalf("err: %s", err)
109 | }
110 |
111 | r, err := CreateArchive(tf, nil)
112 | if err == nil {
113 | t.Fatal("err should not be nil")
114 | }
115 | if r != nil {
116 | t.Fatal("should be nil")
117 | }
118 | }
119 |
120 | func TestArchive_fileWithOpts(t *testing.T) {
121 | r, err := CreateArchive(tempFile(t), &ArchiveOpts{VCS: true})
122 | if err == nil {
123 | t.Fatal("err should not be nil")
124 | }
125 | if r != nil {
126 | t.Fatal("should be nil")
127 | }
128 | }
129 |
130 | func TestArchive_dirExtra(t *testing.T) {
131 | opts := &ArchiveOpts{
132 | Extra: map[string]string{
133 | "hello.txt": filepath.Join(
134 | testFixture("archive-subdir"), "subdir", "hello.txt"),
135 | },
136 | }
137 |
138 | r, err := CreateArchive(testFixture("archive-flat"), opts)
139 | if err != nil {
140 | t.Fatalf("err: %s", err)
141 | }
142 |
143 | expected := []string{
144 | "baz.txt",
145 | "foo.txt",
146 | "hello.txt",
147 | }
148 |
149 | entries := testArchive(t, r, false)
150 | if !reflect.DeepEqual(entries, expected) {
151 | t.Fatalf("bad: %#v", entries)
152 | }
153 | }
154 |
155 | func TestArchive_dirExtraDir(t *testing.T) {
156 | opts := &ArchiveOpts{
157 | Extra: map[string]string{
158 | "foo": filepath.Join(testFixture("archive-subdir"), "subdir"),
159 | },
160 | }
161 |
162 | r, err := CreateArchive(testFixture("archive-flat"), opts)
163 | if err != nil {
164 | t.Fatalf("err: %s", err)
165 | }
166 |
167 | expected := []string{
168 | "baz.txt",
169 | "foo.txt",
170 | "foo/",
171 | "foo/hello.txt",
172 | }
173 |
174 | entries := testArchive(t, r, false)
175 | if !reflect.DeepEqual(entries, expected) {
176 | t.Fatalf("bad: %#v", entries)
177 | }
178 | }
179 |
180 | func TestArchive_dirExtraDirHeader(t *testing.T) {
181 | opts := &ArchiveOpts{
182 | Extra: map[string]string{
183 | "foo": ExtraEntryDir,
184 | },
185 | }
186 |
187 | r, err := CreateArchive(testFixture("archive-flat"), opts)
188 | if err != nil {
189 | t.Fatalf("err: %s", err)
190 | }
191 |
192 | expected := []string{
193 | "baz.txt",
194 | "foo.txt",
195 | "foo/",
196 | }
197 |
198 | entries := testArchive(t, r, false)
199 | if !reflect.DeepEqual(entries, expected) {
200 | t.Fatalf("bad: %#v", entries)
201 | }
202 | }
203 |
204 | func TestArchive_dirMode(t *testing.T) {
205 | if runtime.GOOS == "windows" {
206 | t.Skip("modes don't work on Windows")
207 | }
208 |
209 | opts := &ArchiveOpts{}
210 |
211 | r, err := CreateArchive(testFixture("archive-dir-mode"), opts)
212 | if err != nil {
213 | t.Fatalf("err: %s", err)
214 | }
215 |
216 | expected := []string{
217 | "file.txt-exec",
218 | }
219 |
220 | entries := testArchive(t, r, true)
221 | if !reflect.DeepEqual(entries, expected) {
222 | t.Fatalf("bad: %#v", entries)
223 | }
224 | }
225 | func TestArchive_dirSymlink(t *testing.T) {
226 | if runtime.GOOS == "windows" {
227 | t.Skip("git symlinks don't work on Windows")
228 | }
229 |
230 | path := filepath.Join(testFixture("archive-symlink"), "link", "link")
231 | r, err := CreateArchive(path, new(ArchiveOpts))
232 | if err != nil {
233 | t.Fatalf("err: %s", err)
234 | }
235 |
236 | expected := []string{
237 | "foo.txt",
238 | }
239 |
240 | entries := testArchive(t, r, false)
241 | if !reflect.DeepEqual(entries, expected) {
242 | t.Fatalf("bad: %#v", entries)
243 | }
244 | }
245 |
246 | func TestArchive_dirWithSymlink(t *testing.T) {
247 | if runtime.GOOS == "windows" {
248 | t.Skip("git symlinks don't work on Windows")
249 | }
250 |
251 | path := filepath.Join(testFixture("archive-symlink"), "link")
252 | r, err := CreateArchive(path, new(ArchiveOpts))
253 | if err != nil {
254 | t.Fatalf("err: %s", err)
255 | }
256 |
257 | expected := []string{
258 | "link/",
259 | "link/foo.txt",
260 | }
261 |
262 | entries := testArchive(t, r, false)
263 | if !reflect.DeepEqual(entries, expected) {
264 | t.Fatalf("bad: %#v", entries)
265 | }
266 | }
267 |
268 | func TestArchive_dirWithSymlinkToFile(t *testing.T) {
269 | if runtime.GOOS == "windows" {
270 | t.Skip("git symlinks don't work on Windows")
271 | }
272 |
273 | path := filepath.Join(testFixture("archive-symlink-file"), "link")
274 | r, err := CreateArchive(path, new(ArchiveOpts))
275 | if err != nil {
276 | t.Fatalf("err: %s", err)
277 | }
278 |
279 | expected := []string{
280 | "deeper/",
281 | "deeper/adeeperlink",
282 | "deeper/linklink",
283 | "deeper/linklinklink",
284 | "link",
285 | }
286 |
287 | entries := testArchive(t, r, false)
288 | if !reflect.DeepEqual(entries, expected) {
289 | t.Fatalf("bad: %#v", entries)
290 | }
291 | }
292 |
293 | func TestArchive_dirNoVCS(t *testing.T) {
294 | r, err := CreateArchive(testFixture("archive-flat"), new(ArchiveOpts))
295 | if err != nil {
296 | t.Fatalf("err: %s", err)
297 | }
298 |
299 | expected := []string{
300 | "baz.txt",
301 | "foo.txt",
302 | }
303 |
304 | entries := testArchive(t, r, false)
305 | if !reflect.DeepEqual(entries, expected) {
306 | t.Fatalf("bad: %#v", entries)
307 | }
308 | }
309 |
310 | func TestArchive_dirSubdirsNoVCS(t *testing.T) {
311 | r, err := CreateArchive(testFixture("archive-subdir"), new(ArchiveOpts))
312 | if err != nil {
313 | t.Fatalf("err: %s", err)
314 | }
315 |
316 | expected := []string{
317 | "bar.txt",
318 | "foo.txt",
319 | "subdir/",
320 | "subdir/hello.txt",
321 | }
322 |
323 | entries := testArchive(t, r, false)
324 | if !reflect.DeepEqual(entries, expected) {
325 | t.Fatalf("bad: %#v", entries)
326 | }
327 | }
328 |
329 | func TestArchive_dirExclude(t *testing.T) {
330 | opts := &ArchiveOpts{
331 | Exclude: []string{"subdir", "subdir/*"},
332 | }
333 |
334 | r, err := CreateArchive(testFixture("archive-subdir"), opts)
335 | if err != nil {
336 | t.Fatalf("err: %s", err)
337 | }
338 |
339 | expected := []string{
340 | "bar.txt",
341 | "foo.txt",
342 | }
343 |
344 | entries := testArchive(t, r, false)
345 | if !reflect.DeepEqual(entries, expected) {
346 | t.Fatalf("bad: %#v", entries)
347 | }
348 | }
349 |
350 | func TestArchive_dirInclude(t *testing.T) {
351 | opts := &ArchiveOpts{
352 | Include: []string{"bar.txt"},
353 | }
354 |
355 | r, err := CreateArchive(testFixture("archive-subdir"), opts)
356 | if err != nil {
357 | t.Fatalf("err: %s", err)
358 | }
359 |
360 | expected := []string{
361 | "bar.txt",
362 | }
363 |
364 | entries := testArchive(t, r, false)
365 | if !reflect.DeepEqual(entries, expected) {
366 | t.Fatalf("bad: %#v", entries)
367 | }
368 | }
369 |
370 | func TestArchive_dirIncludeStar(t *testing.T) {
371 | opts := &ArchiveOpts{
372 | Include: []string{"build/**/*"},
373 | }
374 |
375 | r, err := CreateArchive(testFixture("archive-subdir-splat"), opts)
376 | if err != nil {
377 | t.Fatalf("err: %s", err)
378 | }
379 |
380 | expected := []string{
381 | "build/",
382 | "build/darwin-amd64/",
383 | "build/darwin-amd64/build.txt",
384 | "build/linux-amd64/",
385 | "build/linux-amd64/build.txt",
386 | }
387 |
388 | entries := testArchive(t, r, false)
389 | if !reflect.DeepEqual(entries, expected) {
390 | t.Fatalf("bad: %#v", entries)
391 | }
392 | }
393 |
394 | func TestArchive_git(t *testing.T) {
395 | if !testHasGit {
396 | t.Log("git not found, skipping")
397 | t.Skip()
398 | }
399 |
400 | // Git doesn't allow nested ".git" directories so we do some hackiness
401 | // here to get around that...
402 | testDir := testFixture("archive-git")
403 | oldName := filepath.ToSlash(filepath.Join(testDir, "DOTgit"))
404 | newName := filepath.ToSlash(filepath.Join(testDir, ".git"))
405 | os.Remove(newName)
406 | if err := os.Rename(oldName, newName); err != nil {
407 | t.Fatalf("err: %s", err)
408 | }
409 | defer os.Rename(newName, oldName)
410 |
411 | // testDir with VCS set to true
412 | r, err := CreateArchive(testDir, &ArchiveOpts{VCS: true})
413 | if err != nil {
414 | t.Fatalf("err: %s", err)
415 | }
416 |
417 | expected := []string{
418 | "bar.txt",
419 | "foo.txt",
420 | "subdir/",
421 | "subdir/hello.txt",
422 | }
423 |
424 | entries := testArchive(t, r, false)
425 | if !reflect.DeepEqual(entries, expected) {
426 | t.Fatalf("bad: %#v", entries)
427 | }
428 |
429 | // Test that metadata was added
430 | if r.Metadata == nil {
431 | t.Fatal("expected archive metadata to be set")
432 | }
433 |
434 | expectedMetadata := map[string]string{
435 | "branch": "master",
436 | "commit": "7525d17cbbb56f3253a20903ffddc07c6c935c76",
437 | "remote.origin": "https://github.com/hashicorp/origin.git",
438 | "remote.upstream": "https://github.com/hashicorp/upstream.git",
439 | }
440 |
441 | if !reflect.DeepEqual(r.Metadata, expectedMetadata) {
442 | t.Fatalf("expected %+v to be %+v", r.Metadata, expectedMetadata)
443 | }
444 | }
445 |
446 | func TestArchive_gitSubdir(t *testing.T) {
447 | if !testHasGit {
448 | t.Log("git not found, skipping")
449 | t.Skip()
450 | }
451 |
452 | // Git doesn't allow nested ".git" directories so we do some hackiness
453 | // here to get around that...
454 | testDir := testFixture("archive-git")
455 | oldName := filepath.ToSlash(filepath.Join(testDir, "DOTgit"))
456 | newName := filepath.ToSlash(filepath.Join(testDir, ".git"))
457 | os.Remove(newName)
458 | if err := os.Rename(oldName, newName); err != nil {
459 | t.Fatalf("err: %s", err)
460 | }
461 | defer os.Rename(newName, oldName)
462 |
463 | // testDir with VCS set to true
464 | r, err := CreateArchive(filepath.Join(testDir, "subdir"), &ArchiveOpts{VCS: true})
465 | if err != nil {
466 | t.Fatalf("err: %s", err)
467 | }
468 |
469 | expected := []string{
470 | "hello.txt",
471 | }
472 |
473 | entries := testArchive(t, r, false)
474 | if !reflect.DeepEqual(entries, expected) {
475 | t.Fatalf("bad: %#v", entries)
476 | }
477 | }
478 |
479 | func TestArchive_hg(t *testing.T) {
480 | if !testHasHg {
481 | t.Log("hg not found, skipping")
482 | t.Skip()
483 | }
484 |
485 | // testDir with VCS set to true
486 | testDir := testFixture("archive-hg")
487 | r, err := CreateArchive(testDir, &ArchiveOpts{VCS: true})
488 | if err != nil {
489 | t.Fatalf("err: %s", err)
490 | }
491 |
492 | expected := []string{
493 | "bar.txt",
494 | "foo.txt",
495 | "subdir/",
496 | "subdir/hello.txt",
497 | }
498 |
499 | entries := testArchive(t, r, false)
500 | if !reflect.DeepEqual(entries, expected) {
501 | t.Fatalf("\n-- Expected --\n%#v\n-- Found --\n%#v", expected, entries)
502 | }
503 | }
504 |
505 | func TestArchive_hgSubdir(t *testing.T) {
506 | if !testHasHg {
507 | t.Log("hg not found, skipping")
508 | t.Skip()
509 | }
510 |
511 | // testDir with VCS set to true
512 | testDir := filepath.Join(testFixture("archive-hg"), "subdir")
513 | r, err := CreateArchive(testDir, &ArchiveOpts{VCS: true})
514 | if err != nil {
515 | t.Fatalf("err: %s", err)
516 | }
517 |
518 | expected := []string{
519 | "hello.txt",
520 | }
521 |
522 | entries := testArchive(t, r, false)
523 | if !reflect.DeepEqual(entries, expected) {
524 | t.Fatalf("\n-- Expected --\n%#v\n-- Found --\n%#v", expected, entries)
525 | }
526 | }
527 |
528 | func TestReadCloseRemover(t *testing.T) {
529 | f, err := ioutil.TempFile("", "atlas-go")
530 | if err != nil {
531 | t.Fatalf("err: %s", err)
532 | }
533 |
534 | r := &readCloseRemover{F: f}
535 | if err := r.Close(); err != nil {
536 | t.Fatalf("err: %s", err)
537 | }
538 |
539 | if _, err := os.Stat(f.Name()); err == nil {
540 | t.Fatal("file should not exist anymore")
541 | }
542 | }
543 |
544 | func testArchive(t *testing.T, r *Archive, detailed bool) []string {
545 | // Finish the archiving process in-memory
546 | var buf bytes.Buffer
547 | n, err := io.Copy(&buf, r)
548 | if err != nil {
549 | t.Fatalf("err: %s", err)
550 | }
551 | if n != r.Size {
552 | t.Fatalf("bad size: %d (expected: %d)", n, r.Size)
553 | }
554 |
555 | gzipR, err := gzip.NewReader(&buf)
556 | if err != nil {
557 | t.Fatalf("err: %s", err)
558 | }
559 | tarR := tar.NewReader(gzipR)
560 |
561 | // Read all the entries
562 | result := make([]string, 0, 5)
563 | for {
564 | hdr, err := tarR.Next()
565 | if err == io.EOF {
566 | break
567 | }
568 | if err != nil {
569 | t.Fatalf("err: %s", err)
570 | }
571 |
572 | text := hdr.Name
573 | if detailed {
574 | // Check if the file is executable. We use these stub names
575 | // to compensate for umask differences in test environments
576 | // and limitations in using "git clone".
577 | if hdr.FileInfo().Mode()&0111 != 0 {
578 | text = hdr.Name + "-exec"
579 | } else {
580 | text = hdr.Name + "-reg"
581 | }
582 | }
583 |
584 | result = append(result, text)
585 | }
586 |
587 | sort.Strings(result)
588 | return result
589 | }
590 |
591 | func tempFile(t *testing.T) string {
592 | tf, err := ioutil.TempFile("", "test")
593 | if err != nil {
594 | t.Fatalf("err: %s", err)
595 | }
596 | defer tf.Close()
597 |
598 | return tf.Name()
599 | }
600 |
601 | func testFixture(n string) string {
602 | return filepath.Join(fixturesDir, n)
603 | }
604 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-dir-mode/file.txt:
--------------------------------------------------------------------------------
1 | I should be mode 0777
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-file-compressed/file.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-file-compressed/file.tar.gz
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-file/foo.txt:
--------------------------------------------------------------------------------
1 | foo
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-flat/baz.txt:
--------------------------------------------------------------------------------
1 | baz
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-flat/foo.txt:
--------------------------------------------------------------------------------
1 | foo
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/COMMIT_EDITMSG:
--------------------------------------------------------------------------------
1 | Those files tho
2 | # Please enter the commit message for your changes. Lines starting
3 | # with '#' will be ignored, and an empty message aborts the commit.
4 | # On branch master
5 | #
6 | # Initial commit
7 | #
8 | # Changes to be committed:
9 | # new file: bar.txt
10 | # new file: foo.txt
11 | # new file: subdir/hello.txt
12 | #
13 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/HEAD:
--------------------------------------------------------------------------------
1 | ref: refs/heads/master
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/config:
--------------------------------------------------------------------------------
1 | [core]
2 | repositoryformatversion = 0
3 | filemode = true
4 | bare = false
5 | logallrefupdates = true
6 | ignorecase = true
7 | precomposeunicode = true
8 | [remote "origin"]
9 | url = https://github.com/hashicorp/origin.git
10 | fetch = +refs/heads/*:refs/remotes/origin/*
11 | [remote "upstream"]
12 | url = https://github.com/hashicorp/upstream.git
13 | fetch = +refs/heads/*:refs/remotes/upstream/*
14 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/description:
--------------------------------------------------------------------------------
1 | Unnamed repository; edit this file 'description' to name the repository.
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/hooks/applypatch-msg.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # An example hook script to check the commit log message taken by
4 | # applypatch from an e-mail message.
5 | #
6 | # The hook should exit with non-zero status after issuing an
7 | # appropriate message if it wants to stop the commit. The hook is
8 | # allowed to edit the commit message file.
9 | #
10 | # To enable this hook, rename this file to "applypatch-msg".
11 |
12 | . git-sh-setup
13 | test -x "$GIT_DIR/hooks/commit-msg" &&
14 | exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
15 | :
16 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/hooks/commit-msg.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # An example hook script to check the commit log message.
4 | # Called by "git commit" with one argument, the name of the file
5 | # that has the commit message. The hook should exit with non-zero
6 | # status after issuing an appropriate message if it wants to stop the
7 | # commit. The hook is allowed to edit the commit message file.
8 | #
9 | # To enable this hook, rename this file to "commit-msg".
10 |
11 | # Uncomment the below to add a Signed-off-by line to the message.
12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg
13 | # hook is more suited to it.
14 | #
15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
17 |
18 | # This example catches duplicate Signed-off-by lines.
19 |
20 | test "" = "$(grep '^Signed-off-by: ' "$1" |
21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
22 | echo >&2 Duplicate Signed-off-by lines.
23 | exit 1
24 | }
25 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/hooks/post-update.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # An example hook script to prepare a packed repository for use over
4 | # dumb transports.
5 | #
6 | # To enable this hook, rename this file to "post-update".
7 |
8 | exec git update-server-info
9 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/hooks/pre-applypatch.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # An example hook script to verify what is about to be committed
4 | # by applypatch from an e-mail message.
5 | #
6 | # The hook should exit with non-zero status after issuing an
7 | # appropriate message if it wants to stop the commit.
8 | #
9 | # To enable this hook, rename this file to "pre-applypatch".
10 |
11 | . git-sh-setup
12 | test -x "$GIT_DIR/hooks/pre-commit" &&
13 | exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
14 | :
15 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/hooks/pre-commit.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # An example hook script to verify what is about to be committed.
4 | # Called by "git commit" with no arguments. The hook should
5 | # exit with non-zero status after issuing an appropriate message if
6 | # it wants to stop the commit.
7 | #
8 | # To enable this hook, rename this file to "pre-commit".
9 |
10 | if git rev-parse --verify HEAD >/dev/null 2>&1
11 | then
12 | against=HEAD
13 | else
14 | # Initial commit: diff against an empty tree object
15 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
16 | fi
17 |
18 | # If you want to allow non-ASCII filenames set this variable to true.
19 | allownonascii=$(git config --bool hooks.allownonascii)
20 |
21 | # Redirect output to stderr.
22 | exec 1>&2
23 |
24 | # Cross platform projects tend to avoid non-ASCII filenames; prevent
25 | # them from being added to the repository. We exploit the fact that the
26 | # printable range starts at the space character and ends with tilde.
27 | if [ "$allownonascii" != "true" ] &&
28 | # Note that the use of brackets around a tr range is ok here, (it's
29 | # even required, for portability to Solaris 10's /usr/bin/tr), since
30 | # the square bracket bytes happen to fall in the designated range.
31 | test $(git diff --cached --name-only --diff-filter=A -z $against |
32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
33 | then
34 | cat <<\EOF
35 | Error: Attempt to add a non-ASCII file name.
36 |
37 | This can cause problems if you want to work with people on other platforms.
38 |
39 | To be portable it is advisable to rename the file.
40 |
41 | If you know what you are doing you can disable this check using:
42 |
43 | git config hooks.allownonascii true
44 | EOF
45 | exit 1
46 | fi
47 |
48 | # If there are whitespace errors, print the offending file names and fail.
49 | exec git diff-index --check --cached $against --
50 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/hooks/pre-push.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # An example hook script to verify what is about to be pushed. Called by "git
4 | # push" after it has checked the remote status, but before anything has been
5 | # pushed. If this script exits with a non-zero status nothing will be pushed.
6 | #
7 | # This hook is called with the following parameters:
8 | #
9 | # $1 -- Name of the remote to which the push is being done
10 | # $2 -- URL to which the push is being done
11 | #
12 | # If pushing without using a named remote those arguments will be equal.
13 | #
14 | # Information about the commits which are being pushed is supplied as lines to
15 | # the standard input in the form:
16 | #
17 | #
18 | #
19 | # This sample shows how to prevent push of commits where the log message starts
20 | # with "WIP" (work in progress).
21 |
22 | remote="$1"
23 | url="$2"
24 |
25 | z40=0000000000000000000000000000000000000000
26 |
27 | IFS=' '
28 | while read local_ref local_sha remote_ref remote_sha
29 | do
30 | if [ "$local_sha" = $z40 ]
31 | then
32 | # Handle delete
33 | :
34 | else
35 | if [ "$remote_sha" = $z40 ]
36 | then
37 | # New branch, examine all commits
38 | range="$local_sha"
39 | else
40 | # Update to existing branch, examine new commits
41 | range="$remote_sha..$local_sha"
42 | fi
43 |
44 | # Check for WIP commit
45 | commit=`git rev-list -n 1 --grep '^WIP' "$range"`
46 | if [ -n "$commit" ]
47 | then
48 | echo "Found WIP commit in $local_ref, not pushing"
49 | exit 1
50 | fi
51 | fi
52 | done
53 |
54 | exit 0
55 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/hooks/pre-rebase.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # Copyright (c) 2006, 2008 Junio C Hamano
4 | #
5 | # The "pre-rebase" hook is run just before "git rebase" starts doing
6 | # its job, and can prevent the command from running by exiting with
7 | # non-zero status.
8 | #
9 | # The hook is called with the following parameters:
10 | #
11 | # $1 -- the upstream the series was forked from.
12 | # $2 -- the branch being rebased (or empty when rebasing the current branch).
13 | #
14 | # This sample shows how to prevent topic branches that are already
15 | # merged to 'next' branch from getting rebased, because allowing it
16 | # would result in rebasing already published history.
17 |
18 | publish=next
19 | basebranch="$1"
20 | if test "$#" = 2
21 | then
22 | topic="refs/heads/$2"
23 | else
24 | topic=`git symbolic-ref HEAD` ||
25 | exit 0 ;# we do not interrupt rebasing detached HEAD
26 | fi
27 |
28 | case "$topic" in
29 | refs/heads/??/*)
30 | ;;
31 | *)
32 | exit 0 ;# we do not interrupt others.
33 | ;;
34 | esac
35 |
36 | # Now we are dealing with a topic branch being rebased
37 | # on top of master. Is it OK to rebase it?
38 |
39 | # Does the topic really exist?
40 | git show-ref -q "$topic" || {
41 | echo >&2 "No such branch $topic"
42 | exit 1
43 | }
44 |
45 | # Is topic fully merged to master?
46 | not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
47 | if test -z "$not_in_master"
48 | then
49 | echo >&2 "$topic is fully merged to master; better remove it."
50 | exit 1 ;# we could allow it, but there is no point.
51 | fi
52 |
53 | # Is topic ever merged to next? If so you should not be rebasing it.
54 | only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
55 | only_next_2=`git rev-list ^master ${publish} | sort`
56 | if test "$only_next_1" = "$only_next_2"
57 | then
58 | not_in_topic=`git rev-list "^$topic" master`
59 | if test -z "$not_in_topic"
60 | then
61 | echo >&2 "$topic is already up-to-date with master"
62 | exit 1 ;# we could allow it, but there is no point.
63 | else
64 | exit 0
65 | fi
66 | else
67 | not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
68 | /usr/bin/perl -e '
69 | my $topic = $ARGV[0];
70 | my $msg = "* $topic has commits already merged to public branch:\n";
71 | my (%not_in_next) = map {
72 | /^([0-9a-f]+) /;
73 | ($1 => 1);
74 | } split(/\n/, $ARGV[1]);
75 | for my $elem (map {
76 | /^([0-9a-f]+) (.*)$/;
77 | [$1 => $2];
78 | } split(/\n/, $ARGV[2])) {
79 | if (!exists $not_in_next{$elem->[0]}) {
80 | if ($msg) {
81 | print STDERR $msg;
82 | undef $msg;
83 | }
84 | print STDERR " $elem->[1]\n";
85 | }
86 | }
87 | ' "$topic" "$not_in_next" "$not_in_master"
88 | exit 1
89 | fi
90 |
91 | exit 0
92 |
93 | ################################################################
94 |
95 | This sample hook safeguards topic branches that have been
96 | published from being rewound.
97 |
98 | The workflow assumed here is:
99 |
100 | * Once a topic branch forks from "master", "master" is never
101 | merged into it again (either directly or indirectly).
102 |
103 | * Once a topic branch is fully cooked and merged into "master",
104 | it is deleted. If you need to build on top of it to correct
105 | earlier mistakes, a new topic branch is created by forking at
106 | the tip of the "master". This is not strictly necessary, but
107 | it makes it easier to keep your history simple.
108 |
109 | * Whenever you need to test or publish your changes to topic
110 | branches, merge them into "next" branch.
111 |
112 | The script, being an example, hardcodes the publish branch name
113 | to be "next", but it is trivial to make it configurable via
114 | $GIT_DIR/config mechanism.
115 |
116 | With this workflow, you would want to know:
117 |
118 | (1) ... if a topic branch has ever been merged to "next". Young
119 | topic branches can have stupid mistakes you would rather
120 | clean up before publishing, and things that have not been
121 | merged into other branches can be easily rebased without
122 | affecting other people. But once it is published, you would
123 | not want to rewind it.
124 |
125 | (2) ... if a topic branch has been fully merged to "master".
126 | Then you can delete it. More importantly, you should not
127 | build on top of it -- other people may already want to
128 | change things related to the topic as patches against your
129 | "master", so if you need further changes, it is better to
130 | fork the topic (perhaps with the same name) afresh from the
131 | tip of "master".
132 |
133 | Let's look at this example:
134 |
135 | o---o---o---o---o---o---o---o---o---o "next"
136 | / / / /
137 | / a---a---b A / /
138 | / / / /
139 | / / c---c---c---c B /
140 | / / / \ /
141 | / / / b---b C \ /
142 | / / / / \ /
143 | ---o---o---o---o---o---o---o---o---o---o---o "master"
144 |
145 |
146 | A, B and C are topic branches.
147 |
148 | * A has one fix since it was merged up to "next".
149 |
150 | * B has finished. It has been fully merged up to "master" and "next",
151 | and is ready to be deleted.
152 |
153 | * C has not merged to "next" at all.
154 |
155 | We would want to allow C to be rebased, refuse A, and encourage
156 | B to be deleted.
157 |
158 | To compute (1):
159 |
160 | git rev-list ^master ^topic next
161 | git rev-list ^master next
162 |
163 | if these match, topic has not merged in next at all.
164 |
165 | To compute (2):
166 |
167 | git rev-list master..topic
168 |
169 | if this is empty, it is fully merged to "master".
170 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/hooks/prepare-commit-msg.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # An example hook script to prepare the commit log message.
4 | # Called by "git commit" with the name of the file that has the
5 | # commit message, followed by the description of the commit
6 | # message's source. The hook's purpose is to edit the commit
7 | # message file. If the hook fails with a non-zero status,
8 | # the commit is aborted.
9 | #
10 | # To enable this hook, rename this file to "prepare-commit-msg".
11 |
12 | # This hook includes three examples. The first comments out the
13 | # "Conflicts:" part of a merge commit.
14 | #
15 | # The second includes the output of "git diff --name-status -r"
16 | # into the message, just before the "git status" output. It is
17 | # commented because it doesn't cope with --amend or with squashed
18 | # commits.
19 | #
20 | # The third example adds a Signed-off-by line to the message, that can
21 | # still be edited. This is rarely a good idea.
22 |
23 | case "$2,$3" in
24 | merge,)
25 | /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;;
26 |
27 | # ,|template,)
28 | # /usr/bin/perl -i.bak -pe '
29 | # print "\n" . `git diff --cached --name-status -r`
30 | # if /^#/ && $first++ == 0' "$1" ;;
31 |
32 | *) ;;
33 | esac
34 |
35 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
36 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
37 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/hooks/update.sample:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # An example hook script to blocks unannotated tags from entering.
4 | # Called by "git receive-pack" with arguments: refname sha1-old sha1-new
5 | #
6 | # To enable this hook, rename this file to "update".
7 | #
8 | # Config
9 | # ------
10 | # hooks.allowunannotated
11 | # This boolean sets whether unannotated tags will be allowed into the
12 | # repository. By default they won't be.
13 | # hooks.allowdeletetag
14 | # This boolean sets whether deleting tags will be allowed in the
15 | # repository. By default they won't be.
16 | # hooks.allowmodifytag
17 | # This boolean sets whether a tag may be modified after creation. By default
18 | # it won't be.
19 | # hooks.allowdeletebranch
20 | # This boolean sets whether deleting branches will be allowed in the
21 | # repository. By default they won't be.
22 | # hooks.denycreatebranch
23 | # This boolean sets whether remotely creating branches will be denied
24 | # in the repository. By default this is allowed.
25 | #
26 |
27 | # --- Command line
28 | refname="$1"
29 | oldrev="$2"
30 | newrev="$3"
31 |
32 | # --- Safety check
33 | if [ -z "$GIT_DIR" ]; then
34 | echo "Don't run this script from the command line." >&2
35 | echo " (if you want, you could supply GIT_DIR then run" >&2
36 | echo " $0 [ )" >&2
37 | exit 1
38 | fi
39 |
40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
41 | echo "usage: $0 ][ " >&2
42 | exit 1
43 | fi
44 |
45 | # --- Config
46 | allowunannotated=$(git config --bool hooks.allowunannotated)
47 | allowdeletebranch=$(git config --bool hooks.allowdeletebranch)
48 | denycreatebranch=$(git config --bool hooks.denycreatebranch)
49 | allowdeletetag=$(git config --bool hooks.allowdeletetag)
50 | allowmodifytag=$(git config --bool hooks.allowmodifytag)
51 |
52 | # check for no description
53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description")
54 | case "$projectdesc" in
55 | "Unnamed repository"* | "")
56 | echo "*** Project description file hasn't been set" >&2
57 | exit 1
58 | ;;
59 | esac
60 |
61 | # --- Check types
62 | # if $newrev is 0000...0000, it's a commit to delete a ref.
63 | zero="0000000000000000000000000000000000000000"
64 | if [ "$newrev" = "$zero" ]; then
65 | newrev_type=delete
66 | else
67 | newrev_type=$(git cat-file -t $newrev)
68 | fi
69 |
70 | case "$refname","$newrev_type" in
71 | refs/tags/*,commit)
72 | # un-annotated tag
73 | short_refname=${refname##refs/tags/}
74 | if [ "$allowunannotated" != "true" ]; then
75 | echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
77 | exit 1
78 | fi
79 | ;;
80 | refs/tags/*,delete)
81 | # delete tag
82 | if [ "$allowdeletetag" != "true" ]; then
83 | echo "*** Deleting a tag is not allowed in this repository" >&2
84 | exit 1
85 | fi
86 | ;;
87 | refs/tags/*,tag)
88 | # annotated tag
89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
90 | then
91 | echo "*** Tag '$refname' already exists." >&2
92 | echo "*** Modifying a tag is not allowed in this repository." >&2
93 | exit 1
94 | fi
95 | ;;
96 | refs/heads/*,commit)
97 | # branch
98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
99 | echo "*** Creating a branch is not allowed in this repository" >&2
100 | exit 1
101 | fi
102 | ;;
103 | refs/heads/*,delete)
104 | # delete branch
105 | if [ "$allowdeletebranch" != "true" ]; then
106 | echo "*** Deleting a branch is not allowed in this repository" >&2
107 | exit 1
108 | fi
109 | ;;
110 | refs/remotes/*,commit)
111 | # tracking branch
112 | ;;
113 | refs/remotes/*,delete)
114 | # delete tracking branch
115 | if [ "$allowdeletebranch" != "true" ]; then
116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2
117 | exit 1
118 | fi
119 | ;;
120 | *)
121 | # Anything else (is there anything else?)
122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
123 | exit 1
124 | ;;
125 | esac
126 |
127 | # --- Finished
128 | exit 0
129 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/index:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-git/DOTgit/index
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/info/exclude:
--------------------------------------------------------------------------------
1 | # git ls-files --others --exclude-from=.git/info/exclude
2 | # Lines that start with '#' are comments.
3 | # For a project mostly in C, the following would be a good set of
4 | # exclude patterns (uncomment them if you want to use them):
5 | # *.[oa]
6 | # *~
7 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/logs/HEAD:
--------------------------------------------------------------------------------
1 | 0000000000000000000000000000000000000000 7525d17cbbb56f3253a20903ffddc07c6c935c76 Mitchell Hashimoto 1414446684 -0700 commit (initial): Those files tho
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/logs/refs/heads/master:
--------------------------------------------------------------------------------
1 | 0000000000000000000000000000000000000000 7525d17cbbb56f3253a20903ffddc07c6c935c76 Mitchell Hashimoto 1414446684 -0700 commit (initial): Those files tho
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/objects/25/7cc5642cb1a054f08cc83f2d943e56fd3ebe99:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-git/DOTgit/objects/25/7cc5642cb1a054f08cc83f2d943e56fd3ebe99
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/objects/57/16ca5987cbf97d6bb54920bea6adde242d87e6:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-git/DOTgit/objects/57/16ca5987cbf97d6bb54920bea6adde242d87e6
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/objects/75/25d17cbbb56f3253a20903ffddc07c6c935c76:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-git/DOTgit/objects/75/25d17cbbb56f3253a20903ffddc07c6c935c76
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/objects/7e/49ea5550b356e32b63c044201f5f7da1e0925f:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-git/DOTgit/objects/7e/49ea5550b356e32b63c044201f5f7da1e0925f
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/objects/7f/7402c7d2a6e71ca3db3e236099771b160b8ad1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-git/DOTgit/objects/7f/7402c7d2a6e71ca3db3e236099771b160b8ad1
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/DOTgit/refs/heads/master:
--------------------------------------------------------------------------------
1 | 7525d17cbbb56f3253a20903ffddc07c6c935c76
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/bar.txt:
--------------------------------------------------------------------------------
1 | bar
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/foo.txt:
--------------------------------------------------------------------------------
1 | foo
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/subdir/hello.txt:
--------------------------------------------------------------------------------
1 | foo
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-git/untracked.txt:
--------------------------------------------------------------------------------
1 | nope
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/00changelog.i:
--------------------------------------------------------------------------------
1 | dummy changelog to prevent using the old repo layout
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/cache/branch2-served:
--------------------------------------------------------------------------------
1 | 2e4c00191f239e489dca961dbd6fca8fe0d93e2e 0
2 | 2e4c00191f239e489dca961dbd6fca8fe0d93e2e o default
3 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/dirstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-hg/.hg/dirstate
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/last-message.txt:
--------------------------------------------------------------------------------
1 | Tubes
2 |
3 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/requires:
--------------------------------------------------------------------------------
1 | dotencode
2 | fncache
3 | revlogv1
4 | store
5 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/store/00changelog.i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-hg/.hg/store/00changelog.i
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/store/00manifest.i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-hg/.hg/store/00manifest.i
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/store/data/bar.txt.i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-hg/.hg/store/data/bar.txt.i
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/store/data/foo.txt.i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-hg/.hg/store/data/foo.txt.i
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/store/data/subdir/hello.txt.i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-hg/.hg/store/data/subdir/hello.txt.i
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/store/fncache:
--------------------------------------------------------------------------------
1 | data/bar.txt.i
2 | data/foo.txt.i
3 | data/subdir/hello.txt.i
4 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/store/phaseroots:
--------------------------------------------------------------------------------
1 | 1 2e4c00191f239e489dca961dbd6fca8fe0d93e2e
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/store/undo:
--------------------------------------------------------------------------------
1 | data/bar.txt.i 0
2 | data/foo.txt.i 0
3 | data/subdir/hello.txt.i 0
4 | 00manifest.i 0
5 | 00changelog.i 0
6 | fncache 0
7 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/store/undo.phaseroots:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-hg/.hg/store/undo.phaseroots
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/undo.bookmarks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-hg/.hg/undo.bookmarks
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/undo.branch:
--------------------------------------------------------------------------------
1 | default
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/undo.desc:
--------------------------------------------------------------------------------
1 | 0
2 | commit
3 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/.hg/undo.dirstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/atlas-go/46e9b3e5e4f9f53d31ccddeebfed9acf05641861/archive/test-fixtures/archive-hg/.hg/undo.dirstate
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/bar.txt:
--------------------------------------------------------------------------------
1 | bar
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/foo.txt:
--------------------------------------------------------------------------------
1 | foo
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-hg/subdir/hello.txt:
--------------------------------------------------------------------------------
1 | hello
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-subdir-splat/bar.txt:
--------------------------------------------------------------------------------
1 | bar
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-subdir-splat/build/darwin-amd64/build.txt:
--------------------------------------------------------------------------------
1 | build.txt
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-subdir-splat/build/linux-amd64/build.txt:
--------------------------------------------------------------------------------
1 | linux-amd64
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-subdir/bar.txt:
--------------------------------------------------------------------------------
1 | bar
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-subdir/foo.txt:
--------------------------------------------------------------------------------
1 | foo
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-subdir/subdir/hello.txt:
--------------------------------------------------------------------------------
1 | hello
2 |
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-symlink-file/link/deeper/adeeperlink:
--------------------------------------------------------------------------------
1 | ../../real/foo.txt
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-symlink-file/link/deeper/linklink:
--------------------------------------------------------------------------------
1 | adeeperlink
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-symlink-file/link/deeper/linklinklink:
--------------------------------------------------------------------------------
1 | linklink
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-symlink-file/link/link:
--------------------------------------------------------------------------------
1 | ../real/foo.txt
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-symlink-file/real/foo.txt:
--------------------------------------------------------------------------------
1 | tasty foo
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-symlink/link/link:
--------------------------------------------------------------------------------
1 | ../real
--------------------------------------------------------------------------------
/archive/test-fixtures/archive-symlink/real/foo.txt:
--------------------------------------------------------------------------------
1 | tasty foo
--------------------------------------------------------------------------------
/archive/vcs.go:
--------------------------------------------------------------------------------
1 | package archive
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "log"
8 | "os"
9 | "os/exec"
10 | "path/filepath"
11 | "strings"
12 |
13 | version "github.com/hashicorp/go-version"
14 | )
15 |
16 | // VCS is a struct that explains how to get the file list for a given
17 | // VCS.
18 | type VCS struct {
19 | Name string
20 |
21 | // Detect is a list of files/folders that if they exist, signal that
22 | // this VCS is the VCS in use.
23 | Detect []string
24 |
25 | // Files returns the files that are under version control for the
26 | // given path.
27 | Files VCSFilesFunc
28 |
29 | // Metadata returns arbitrary metadata about the underlying VCS for the
30 | // given path.
31 | Metadata VCSMetadataFunc
32 |
33 | // Preflight is a function to run before looking for VCS files.
34 | Preflight VCSPreflightFunc
35 | }
36 |
37 | // VCSList is the list of VCS we recognize.
38 | var VCSList = []*VCS{
39 | &VCS{
40 | Name: "git",
41 | Detect: []string{".git/"},
42 | Preflight: gitPreflight,
43 | Files: vcsFilesCmd("git", "ls-files"),
44 | Metadata: gitMetadata,
45 | },
46 | &VCS{
47 | Name: "hg",
48 | Detect: []string{".hg/"},
49 | Files: vcsTrimCmd(vcsFilesCmd("hg", "locate", "-f", "--include", ".")),
50 | },
51 | &VCS{
52 | Name: "svn",
53 | Detect: []string{".svn/"},
54 | Files: vcsFilesCmd("svn", "ls"),
55 | },
56 | }
57 |
58 | // VCSFilesFunc is the callback invoked to return the files in the VCS.
59 | //
60 | // The return value should be paths relative to the given path.
61 | type VCSFilesFunc func(string) ([]string, error)
62 |
63 | // VCSMetadataFunc is the callback invoked to get arbitrary information about
64 | // the current VCS.
65 | //
66 | // The return value should be a map of key-value pairs.
67 | type VCSMetadataFunc func(string) (map[string]string, error)
68 |
69 | // VCSPreflightFunc is a function that runs before VCS detection to be
70 | // configured by the user. It may be used to check if pre-requisites (like the
71 | // actual VCS) are installed or that a program is at the correct version. If an
72 | // error is returned, the VCS will not be processed and the error will be
73 | // returned up the stack.
74 | //
75 | // The given argument is the path where the VCS is running.
76 | type VCSPreflightFunc func(string) error
77 |
78 | // vcsDetect detects the VCS that is used for path.
79 | func vcsDetect(path string) (*VCS, error) {
80 | dir := path
81 | for {
82 | for _, v := range VCSList {
83 | for _, f := range v.Detect {
84 | check := filepath.Join(dir, f)
85 | if _, err := os.Stat(check); err == nil {
86 | return v, nil
87 | }
88 | }
89 | }
90 | lastDir := dir
91 | dir = filepath.Dir(dir)
92 | if dir == lastDir {
93 | break
94 | }
95 | }
96 |
97 | return nil, fmt.Errorf("no VCS found for path: %s", path)
98 | }
99 |
100 | // vcsPreflight returns the metadata for the VCS directory path.
101 | func vcsPreflight(path string) error {
102 | vcs, err := vcsDetect(path)
103 | if err != nil {
104 | return fmt.Errorf("error detecting VCS: %s", err)
105 | }
106 |
107 | if vcs.Preflight != nil {
108 | return vcs.Preflight(path)
109 | }
110 |
111 | return nil
112 | }
113 |
114 | // vcsFiles returns the files for the VCS directory path.
115 | func vcsFiles(path string) ([]string, error) {
116 | vcs, err := vcsDetect(path)
117 | if err != nil {
118 | return nil, fmt.Errorf("error detecting VCS: %s", err)
119 | }
120 |
121 | if vcs.Files != nil {
122 | return vcs.Files(path)
123 | }
124 |
125 | return nil, nil
126 | }
127 |
128 | // vcsFilesCmd creates a Files-compatible function that reads the files
129 | // by executing the command in the repository path and returning each
130 | // line in stdout.
131 | func vcsFilesCmd(args ...string) VCSFilesFunc {
132 | return func(path string) ([]string, error) {
133 | var stderr, stdout bytes.Buffer
134 |
135 | cmd := exec.Command(args[0], args[1:]...)
136 | cmd.Dir = path
137 | cmd.Stdout = &stdout
138 | cmd.Stderr = &stderr
139 | if err := cmd.Run(); err != nil {
140 | return nil, fmt.Errorf(
141 | "error executing %s: %s",
142 | strings.Join(args, " "),
143 | err)
144 | }
145 |
146 | // Read each line of output as a path
147 | result := make([]string, 0, 100)
148 | scanner := bufio.NewScanner(&stdout)
149 | for scanner.Scan() {
150 | result = append(result, scanner.Text())
151 | }
152 |
153 | // Always use *nix-style paths (for Windows)
154 | for idx, value := range result {
155 | result[idx] = filepath.ToSlash(value)
156 | }
157 |
158 | return result, nil
159 | }
160 | }
161 |
162 | // vcsTrimCmd trims the prefix from the paths returned by another VCSFilesFunc.
163 | // This should be used to wrap another function if the return value is known
164 | // to have full paths rather than relative paths
165 | func vcsTrimCmd(f VCSFilesFunc) VCSFilesFunc {
166 | return func(path string) ([]string, error) {
167 | absPath, err := filepath.Abs(path)
168 | if err != nil {
169 | return nil, fmt.Errorf(
170 | "error expanding VCS path: %s", err)
171 | }
172 |
173 | // Now that we have the root path, get the inner files
174 | fs, err := f(path)
175 | if err != nil {
176 | return nil, err
177 | }
178 |
179 | // Trim the root path from the files
180 | result := make([]string, 0, len(fs))
181 | for _, f := range fs {
182 | if !strings.HasPrefix(f, absPath) {
183 | continue
184 | }
185 |
186 | f, err = filepath.Rel(absPath, f)
187 | if err != nil {
188 | return nil, fmt.Errorf(
189 | "error determining path: %s", err)
190 | }
191 |
192 | result = append(result, f)
193 | }
194 |
195 | return result, nil
196 | }
197 | }
198 |
199 | // vcsMetadata returns the metadata for the VCS directory path.
200 | func vcsMetadata(path string) (map[string]string, error) {
201 | vcs, err := vcsDetect(path)
202 | if err != nil {
203 | return nil, fmt.Errorf("error detecting VCS: %s", err)
204 | }
205 |
206 | if vcs.Metadata != nil {
207 | return vcs.Metadata(path)
208 | }
209 |
210 | return nil, nil
211 | }
212 |
213 | const ignorableDetachedHeadError = "HEAD is not a symbolic ref"
214 |
215 | // gitBranch gets and returns the current git branch for the Git repository
216 | // at the given path. It is assumed that the VCS is git.
217 | func gitBranch(path string) (string, error) {
218 | var stderr, stdout bytes.Buffer
219 |
220 | cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD")
221 | cmd.Dir = path
222 | cmd.Stdout = &stdout
223 | cmd.Stderr = &stderr
224 | if err := cmd.Run(); err != nil {
225 | if strings.Contains(stderr.String(), ignorableDetachedHeadError) {
226 | return "", nil
227 | } else {
228 | return "",
229 | fmt.Errorf("error getting git branch: %s\nstdout: %s\nstderr: %s",
230 | err, stdout.String(), stderr.String())
231 | }
232 | }
233 |
234 | branch := strings.TrimSpace(stdout.String())
235 |
236 | return branch, nil
237 | }
238 |
239 | // gitCommit gets the SHA of the latest commit for the Git repository at the
240 | // given path. It is assumed that the VCS is git.
241 | func gitCommit(path string) (string, error) {
242 | var stderr, stdout bytes.Buffer
243 |
244 | cmd := exec.Command("git", "log", "-n1", "--pretty=format:%H")
245 | cmd.Dir = path
246 | cmd.Stdout = &stdout
247 | cmd.Stderr = &stderr
248 | if err := cmd.Run(); err != nil {
249 | return "", fmt.Errorf("error getting git commit: %s\nstdout: %s\nstderr: %s",
250 | err, stdout.String(), stderr.String())
251 | }
252 |
253 | commit := strings.TrimSpace(stdout.String())
254 |
255 | return commit, nil
256 | }
257 |
258 | // gitRemotes gets and returns a map of all remotes for the Git repository. The
259 | // map key is the name of the remote of the format "remote.NAME" and the value
260 | // is the endpoint for the remote. It is assumed that the VCS is git.
261 | func gitRemotes(path string) (map[string]string, error) {
262 | var stderr, stdout bytes.Buffer
263 |
264 | cmd := exec.Command("git", "remote", "-v")
265 | cmd.Dir = path
266 | cmd.Stdout = &stdout
267 | cmd.Stderr = &stderr
268 | if err := cmd.Run(); err != nil {
269 | return nil, fmt.Errorf("error getting git remotes: %s\nstdout: %s\nstderr: %s",
270 | err, stdout.String(), stderr.String())
271 | }
272 |
273 | // Read each line of output as a remote
274 | result := make(map[string]string)
275 | scanner := bufio.NewScanner(&stdout)
276 | for scanner.Scan() {
277 | line := scanner.Text()
278 | split := strings.Split(line, "\t")
279 |
280 | if len(split) < 2 {
281 | return nil, fmt.Errorf("invalid response from git remote: %s", stdout.String())
282 | }
283 |
284 | remote := fmt.Sprintf("remote.%s", strings.TrimSpace(split[0]))
285 | if _, ok := result[remote]; !ok {
286 | // https://github.com/foo/bar.git (fetch) #=> https://github.com/foo/bar.git
287 | urlSplit := strings.Split(split[1], " ")
288 | result[remote] = strings.TrimSpace(urlSplit[0])
289 | }
290 | }
291 |
292 | return result, nil
293 | }
294 |
295 | // gitPreflight is the pre-flight command that runs for Git-based VCSs
296 | func gitPreflight(path string) error {
297 | var stderr, stdout bytes.Buffer
298 |
299 | cmd := exec.Command("git", "--version")
300 | cmd.Dir = path
301 | cmd.Stdout = &stdout
302 | cmd.Stderr = &stderr
303 | if err := cmd.Run(); err != nil {
304 | return fmt.Errorf("error getting git version: %s\nstdout: %s\nstderr: %s",
305 | err, stdout.String(), stderr.String())
306 | }
307 |
308 | // Check if the output is valid
309 | output := strings.Split(strings.TrimSpace(stdout.String()), " ")
310 | if len(output) < 1 {
311 | log.Printf("[WARN] could not extract version output from Git")
312 | return nil
313 | }
314 |
315 | // Parse the version
316 | gitv, err := version.NewVersion(output[len(output)-1])
317 | if err != nil {
318 | log.Printf("[WARN] could not parse version output from Git")
319 | return nil
320 | }
321 |
322 | constraint, err := version.NewConstraint("> 1.8")
323 | if err != nil {
324 | log.Printf("[WARN] could not create version constraint to check")
325 | return nil
326 | }
327 | if !constraint.Check(gitv) {
328 | return fmt.Errorf("git version (%s) is too old, please upgrade", gitv.String())
329 | }
330 |
331 | return nil
332 | }
333 |
334 | // gitMetadata is the function to parse and return Git metadata
335 | func gitMetadata(path string) (map[string]string, error) {
336 | // Future-self note: Git is NOT threadsafe, so we cannot run these
337 | // operations in go routines or else you're going to have a really really
338 | // bad day and Panda.State == "Sad" :(
339 |
340 | branch, err := gitBranch(path)
341 | if err != nil {
342 | return nil, err
343 | }
344 |
345 | commit, err := gitCommit(path)
346 | if err != nil {
347 | return nil, err
348 | }
349 |
350 | remotes, err := gitRemotes(path)
351 | if err != nil {
352 | return nil, err
353 | }
354 |
355 | // Make the return result (we already know the size)
356 | result := make(map[string]string, 2+len(remotes))
357 |
358 | result["branch"] = branch
359 | result["commit"] = commit
360 | for remote, value := range remotes {
361 | result[remote] = value
362 | }
363 |
364 | return result, nil
365 | }
366 |
--------------------------------------------------------------------------------
/archive/vcs_test.go:
--------------------------------------------------------------------------------
1 | package archive
2 |
3 | import (
4 | "bytes"
5 | "os"
6 | "os/exec"
7 | "path/filepath"
8 | "reflect"
9 | "runtime"
10 | "testing"
11 | )
12 |
13 | func setupGitFixtures(t *testing.T) (string, func()) {
14 | testDir := testFixture("archive-git")
15 | oldName := filepath.Join(testDir, "DOTgit")
16 | newName := filepath.Join(testDir, ".git")
17 |
18 | cleanup := func() {
19 | os.Rename(newName, oldName)
20 | // Windows leaves an empty folder lying around afterward
21 | if runtime.GOOS == "windows" {
22 | os.Remove(newName)
23 | }
24 | }
25 |
26 | // We call this BEFORE and after each setup for tests that make lower-level
27 | // calls like runCommand
28 | cleanup()
29 |
30 | if err := os.Rename(oldName, newName); err != nil {
31 | t.Fatal(err)
32 | }
33 |
34 | return testDir, cleanup
35 | }
36 |
37 | func TestVCSPreflight(t *testing.T) {
38 | if !testHasGit {
39 | t.Skip("git not found")
40 | }
41 |
42 | testDir, cleanup := setupGitFixtures(t)
43 | defer cleanup()
44 |
45 | if err := vcsPreflight(testDir); err != nil {
46 | t.Fatal(err)
47 | }
48 | }
49 |
50 | func TestGitBranch(t *testing.T) {
51 | if !testHasGit {
52 | t.Skip("git not found")
53 | }
54 |
55 | testDir, cleanup := setupGitFixtures(t)
56 | defer cleanup()
57 |
58 | branch, err := gitBranch(testDir)
59 | if err != nil {
60 | t.Fatal(err)
61 | }
62 |
63 | expected := "master"
64 | if branch != expected {
65 | t.Fatalf("expected %q to be %q", branch, expected)
66 | }
67 | }
68 |
69 | func TestGitBranch_detached(t *testing.T) {
70 | if !testHasGit {
71 | t.Skip("git not found")
72 | }
73 |
74 | testDir := testFixture("archive-git")
75 | oldName := filepath.Join(testDir, "DOTgit")
76 | newName := filepath.Join(testDir, ".git")
77 | pwd, err := os.Getwd()
78 | if err != nil {
79 | t.Fatalf("err: %#v", err)
80 | }
81 |
82 | // Copy and then remove the .git dir instead of moving and replacing like
83 | // other tests, since the checkout below is going to write to the reflog and
84 | // the index
85 | runCommand(t, pwd, "cp", "-r", oldName, newName)
86 | defer runCommand(t, pwd, "rm", "-rf", newName)
87 |
88 | runCommand(t, testDir, "git", "checkout", "--detach")
89 |
90 | branch, err := gitBranch(testDir)
91 | if err != nil {
92 | t.Fatal(err)
93 | }
94 |
95 | if branch != "" {
96 | t.Fatalf("expected branch to be empty, but it was: %s", branch)
97 | }
98 | }
99 |
100 | func TestGitCommit(t *testing.T) {
101 | if !testHasGit {
102 | t.Skip("git not found")
103 | }
104 |
105 | testDir, cleanup := setupGitFixtures(t)
106 | defer cleanup()
107 |
108 | commit, err := gitCommit(testDir)
109 | if err != nil {
110 | t.Fatal(err)
111 | }
112 |
113 | expected := "7525d17cbbb56f3253a20903ffddc07c6c935c76"
114 | if commit != expected {
115 | t.Fatalf("expected %q to be %q", commit, expected)
116 | }
117 | }
118 |
119 | func TestGitRemotes(t *testing.T) {
120 | if !testHasGit {
121 | t.Skip("git not found")
122 | }
123 |
124 | testDir, cleanup := setupGitFixtures(t)
125 | defer cleanup()
126 |
127 | remotes, err := gitRemotes(testDir)
128 | if err != nil {
129 | t.Fatal(err)
130 | }
131 |
132 | expected := map[string]string{
133 | "remote.origin": "https://github.com/hashicorp/origin.git",
134 | "remote.upstream": "https://github.com/hashicorp/upstream.git",
135 | }
136 |
137 | if !reflect.DeepEqual(remotes, expected) {
138 | t.Fatalf("expected %+v to be %+v", remotes, expected)
139 | }
140 | }
141 |
142 | func TestVCSMetadata_git(t *testing.T) {
143 | if !testHasGit {
144 | t.Skip("git not found")
145 | }
146 |
147 | testDir, cleanup := setupGitFixtures(t)
148 | defer cleanup()
149 |
150 | metadata, err := vcsMetadata(testDir)
151 | if err != nil {
152 | t.Fatal(err)
153 | }
154 |
155 | expected := map[string]string{
156 | "branch": "master",
157 | "commit": "7525d17cbbb56f3253a20903ffddc07c6c935c76",
158 | "remote.origin": "https://github.com/hashicorp/origin.git",
159 | "remote.upstream": "https://github.com/hashicorp/upstream.git",
160 | }
161 |
162 | if !reflect.DeepEqual(metadata, expected) {
163 | t.Fatalf("expected %+v to be %+v", metadata, expected)
164 | }
165 | }
166 |
167 | func TestVCSMetadata_git_detached(t *testing.T) {
168 | if !testHasGit {
169 | t.Skip("git not found")
170 | }
171 |
172 | testDir := testFixture("archive-git")
173 | oldName := filepath.Join(testDir, "DOTgit")
174 | newName := filepath.Join(testDir, ".git")
175 | pwd, err := os.Getwd()
176 | if err != nil {
177 | t.Fatalf("err: %#v", err)
178 | }
179 |
180 | // Copy and then remove the .git dir instead of moving and replacing like
181 | // other tests, since the checkout below is going to write to the reflog and
182 | // the index
183 | runCommand(t, pwd, "cp", "-r", oldName, newName)
184 | defer runCommand(t, pwd, "rm", "-rf", newName)
185 |
186 | runCommand(t, testDir, "git", "checkout", "--detach")
187 |
188 | metadata, err := vcsMetadata(testDir)
189 | if err != nil {
190 | t.Fatal(err)
191 | }
192 |
193 | expected := map[string]string{
194 | "branch": "",
195 | "commit": "7525d17cbbb56f3253a20903ffddc07c6c935c76",
196 | "remote.origin": "https://github.com/hashicorp/origin.git",
197 | "remote.upstream": "https://github.com/hashicorp/upstream.git",
198 | }
199 |
200 | if !reflect.DeepEqual(metadata, expected) {
201 | t.Fatalf("expected %+v to be %+v", metadata, expected)
202 | }
203 | }
204 |
205 | func TestVCSPathDetect_git(t *testing.T) {
206 | testDir, cleanup := setupGitFixtures(t)
207 | defer cleanup()
208 |
209 | vcs, err := vcsDetect(testDir)
210 | if err != nil {
211 | t.Errorf("VCS detection failed")
212 | }
213 |
214 | if vcs.Name != "git" {
215 | t.Errorf("Expected to find git; found %s", vcs.Name)
216 | }
217 | }
218 |
219 | func TestVCSPathDetect_git_failure(t *testing.T) {
220 | _, err := vcsDetect(testFixture("archive-flat"))
221 | // We expect to get an error because there is no git repo here
222 | if err == nil {
223 | t.Errorf("VCS detection failed")
224 | }
225 | }
226 |
227 | func TestVCSPathDetect_hg(t *testing.T) {
228 | vcs, err := vcsDetect(testFixture("archive-hg"))
229 | if err != nil {
230 | t.Errorf("VCS detection failed")
231 | }
232 |
233 | if vcs.Name != "hg" {
234 | t.Errorf("Expected to find hg; found %s", vcs.Name)
235 | }
236 | }
237 |
238 | func TestVCSPathDetect_hg_absolute(t *testing.T) {
239 | abspath, err := filepath.Abs(testFixture("archive-hg"))
240 | vcs, err := vcsDetect(abspath)
241 | if err != nil {
242 | t.Errorf("VCS detection failed")
243 | }
244 |
245 | if vcs.Name != "hg" {
246 | t.Errorf("Expected to find hg; found %s", vcs.Name)
247 | }
248 | }
249 |
250 | func runCommand(t *testing.T, path, command string, args ...string) {
251 | var stderr, stdout bytes.Buffer
252 | cmd := exec.Command(command, args...)
253 | cmd.Dir = path
254 | cmd.Stdout = &stdout
255 | cmd.Stderr = &stderr
256 | if err := cmd.Run(); err != nil {
257 | t.Fatalf("error running command: %s\nstdout: %s\nstderr: %s",
258 | err, stdout.String(), stderr.String())
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/v1/application.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "log"
9 | )
10 |
11 | // appWrapper is the API wrapper since the server wraps the resulting object.
12 | type appWrapper struct {
13 | Application *App `json:"application"`
14 | }
15 |
16 | // App represents a single instance of an application on the Atlas server.
17 | type App struct {
18 | // User is the namespace (username or organization) under which the
19 | // Atlas application resides
20 | User string `json:"username"`
21 |
22 | // Name is the name of the application
23 | Name string `json:"name"`
24 | }
25 |
26 | // Slug returns the slug format for this App (User/Name)
27 | func (a *App) Slug() string {
28 | return fmt.Sprintf("%s/%s", a.User, a.Name)
29 | }
30 |
31 | // App gets the App by the given user space and name. In the event the App is
32 | // not found (404), or for any other non-200 responses, an error is returned.
33 | func (c *Client) App(user, name string) (*App, error) {
34 | log.Printf("[INFO] getting application %s/%s", user, name)
35 |
36 | endpoint := fmt.Sprintf("/api/v1/vagrant/applications/%s/%s", user, name)
37 | request, err := c.Request("GET", endpoint, nil)
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | response, err := checkResp(c.HTTPClient.Do(request))
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | var app App
48 | if err := decodeJSON(response, &app); err != nil {
49 | return nil, err
50 | }
51 |
52 | return &app, nil
53 | }
54 |
55 | // CreateApp creates a new App under the given user with the given name. If the
56 | // App is created successfully, it is returned. If the server returns any
57 | // errors, an error is returned.
58 | func (c *Client) CreateApp(user, name string) (*App, error) {
59 | log.Printf("[INFO] creating application %s/%s", user, name)
60 |
61 | body, err := json.Marshal(&appWrapper{&App{
62 | User: user,
63 | Name: name,
64 | }})
65 | if err != nil {
66 | return nil, err
67 | }
68 |
69 | endpoint := "/api/v1/vagrant/applications"
70 | request, err := c.Request("POST", endpoint, &RequestOptions{
71 | Body: bytes.NewReader(body),
72 | Headers: map[string]string{
73 | "Content-Type": "application/json",
74 | },
75 | })
76 | if err != nil {
77 | return nil, err
78 | }
79 |
80 | response, err := checkResp(c.HTTPClient.Do(request))
81 | if err != nil {
82 | return nil, err
83 | }
84 |
85 | var app App
86 | if err := decodeJSON(response, &app); err != nil {
87 | return nil, err
88 | }
89 |
90 | return &app, nil
91 | }
92 |
93 | // appVersion represents a specific version of an App in Atlas. It is actually
94 | // an upload container/wrapper.
95 | type appVersion struct {
96 | UploadPath string `json:"upload_path"`
97 | Token string `json:"token"`
98 | Version uint64 `json:"version"`
99 | }
100 |
101 | // appMetadataWrapper is a wrapper around a map the prefixes the json key with
102 | // "metadata" when marshalled to format requests to the API properly.
103 | type appMetadataWrapper struct {
104 | Metadata map[string]interface{} `json:"metadata,omitempty"`
105 | }
106 |
107 | // UploadApp creates and uploads a new version for the App. If the server does not
108 | // find the application, an error is returned. If the server does not accept the
109 | // data, an error is returned.
110 | //
111 | // It is the responsibility of the caller to create a properly-formed data
112 | // object; this method blindly passes along the contents of the io.Reader.
113 | func (c *Client) UploadApp(app *App, metadata map[string]interface{},
114 | data io.Reader, size int64) (uint64, error) {
115 |
116 | log.Printf("[INFO] uploading application %s (%d bytes) with metadata %q",
117 | app.Slug(), size, metadata)
118 |
119 | endpoint := fmt.Sprintf("/api/v1/vagrant/applications/%s/%s/versions",
120 | app.User, app.Name)
121 |
122 | // If metadata was given, setup the RequestOptions to pass in the metadata
123 | // with the request.
124 | var ro *RequestOptions
125 | if metadata != nil {
126 | // wrap the struct into the correct JSON format
127 | wrapper := struct {
128 | Application *appMetadataWrapper `json:"application"`
129 | }{
130 | &appMetadataWrapper{metadata},
131 | }
132 | m, err := json.Marshal(wrapper)
133 | if err != nil {
134 | return 0, err
135 | }
136 |
137 | // Create the request options.
138 | ro = &RequestOptions{
139 | Body: bytes.NewReader(m),
140 | BodyLength: int64(len(m)),
141 | }
142 | }
143 |
144 | request, err := c.Request("POST", endpoint, ro)
145 | if err != nil {
146 | return 0, err
147 | }
148 |
149 | response, err := checkResp(c.HTTPClient.Do(request))
150 | if err != nil {
151 | return 0, err
152 | }
153 |
154 | var av appVersion
155 | if err := decodeJSON(response, &av); err != nil {
156 | return 0, err
157 | }
158 |
159 | if err := c.putFile(av.UploadPath, data, size); err != nil {
160 | return 0, err
161 | }
162 |
163 | return av.Version, nil
164 | }
165 |
--------------------------------------------------------------------------------
/v1/application_test.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestSlug_returnsSlug(t *testing.T) {
9 | app := &App{
10 | User: "hashicorp",
11 | Name: "project",
12 | }
13 |
14 | expected := "hashicorp/project"
15 | if app.Slug() != expected {
16 | t.Fatalf("expected %q to be %q", app.Slug(), expected)
17 | }
18 | }
19 |
20 | func TestApp_fetchesApp(t *testing.T) {
21 | server := newTestAtlasServer(t)
22 | defer server.Stop()
23 |
24 | client, err := NewClient(server.URL.String())
25 | if err != nil {
26 | t.Fatal(err)
27 | }
28 |
29 | app, err := client.App("hashicorp", "existing")
30 | if err != nil {
31 | t.Fatal(err)
32 | }
33 |
34 | if app.User != "hashicorp" {
35 | t.Errorf("expected %q to be %q", app.User, "hashicorp")
36 | }
37 |
38 | if app.Name != "existing" {
39 | t.Errorf("expected %q to be %q", app.Name, "existing")
40 | }
41 | }
42 |
43 | func TestApp_returnsErrorNoApp(t *testing.T) {
44 | server := newTestAtlasServer(t)
45 | defer server.Stop()
46 |
47 | client, err := NewClient(server.URL.String())
48 | if err != nil {
49 | t.Fatal(err)
50 | }
51 |
52 | _, err = client.App("hashicorp", "newproject")
53 | if err == nil {
54 | t.Fatal("expected error, but nothing was returned")
55 | }
56 | }
57 |
58 | func TestCreateApp_createsAndReturnsApp(t *testing.T) {
59 | server := newTestAtlasServer(t)
60 | defer server.Stop()
61 |
62 | client, err := NewClient(server.URL.String())
63 | if err != nil {
64 | t.Fatal(err)
65 | }
66 |
67 | app, err := client.CreateApp("hashicorp", "newproject")
68 | if err != nil {
69 | t.Fatal(err)
70 | }
71 |
72 | if app.User != "hashicorp" {
73 | t.Errorf("expected %q to be %q", app.User, "hashicorp")
74 | }
75 |
76 | if app.Name != "newproject" {
77 | t.Errorf("expected %q to be %q", app.Name, "newproject")
78 | }
79 | }
80 |
81 | func TestCreateApp_returnsErrorExistingApp(t *testing.T) {
82 | server := newTestAtlasServer(t)
83 | defer server.Stop()
84 |
85 | client, err := NewClient(server.URL.String())
86 | if err != nil {
87 | t.Fatal(err)
88 | }
89 |
90 | _, err = client.CreateApp("hashicorp", "existing")
91 | if err == nil {
92 | t.Fatal("expected error, but nothing was returned")
93 | }
94 | }
95 |
96 | func TestUploadApp_createsAndReturnsVersion(t *testing.T) {
97 | server := newTestAtlasServer(t)
98 | defer server.Stop()
99 |
100 | client, err := NewClient(server.URL.String())
101 | if err != nil {
102 | t.Fatal(err)
103 | }
104 |
105 | app := &App{
106 | User: "hashicorp",
107 | Name: "existing",
108 | }
109 | metadata := map[string]interface{}{"testing": true}
110 | data := new(bytes.Buffer)
111 | version, err := client.UploadApp(app, metadata, data, int64(data.Len()))
112 | if err != nil {
113 | t.Fatal(err)
114 | }
115 | if version != 125 {
116 | t.Fatalf("bad: %#v", version)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/v1/artifact.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net/url"
10 | )
11 |
12 | // Artifact represents a single instance of an artifact.
13 | type Artifact struct {
14 | // User and name are self-explanatory. Tag is the combination
15 | // of both into "username/name"
16 | User string `json:"username"`
17 | Name string `json:"name"`
18 | Tag string `json:",omitempty"`
19 | }
20 |
21 | // ArtifactVersion represents a single version of an artifact.
22 | type ArtifactVersion struct {
23 | User string `json:"username"`
24 | Name string `json:"name"`
25 | Tag string `json:",omitempty"`
26 | Type string `json:"artifact_type"`
27 | ID string `json:"id"`
28 | Version int `json:"version"`
29 | Metadata map[string]string `json:"metadata"`
30 | File bool `json:"file"`
31 | Slug string `json:"slug"`
32 |
33 | UploadPath string `json:"upload_path"`
34 | UploadToken string `json:"upload_token"`
35 | }
36 |
37 | // ArtifactSearchOpts are the options used to search for an artifact.
38 | type ArtifactSearchOpts struct {
39 | User string
40 | Name string
41 | Type string
42 |
43 | Build string
44 | Version string
45 | Metadata map[string]string
46 | }
47 |
48 | // UploadArtifactOpts are the options used to upload an artifact.
49 | type UploadArtifactOpts struct {
50 | User string
51 | Name string
52 | Type string
53 | ID string
54 | File io.Reader
55 | FileSize int64
56 | Metadata map[string]string
57 | BuildID int
58 | CompileID int
59 | }
60 |
61 | // MarshalJSON converts the UploadArtifactOpts into a JSON struct.
62 | func (o *UploadArtifactOpts) MarshalJSON() ([]byte, error) {
63 | return json.Marshal(map[string]interface{}{
64 | "artifact_version": map[string]interface{}{
65 | "id": o.ID,
66 | "file": o.File != nil,
67 | "metadata": o.Metadata,
68 | "build_id": o.BuildID,
69 | "compile_id": o.CompileID,
70 | },
71 | })
72 | }
73 |
74 | // This is the value that should be used for metadata in ArtifactSearchOpts
75 | // if you don't care what the value is.
76 | const MetadataAnyValue = "943febbf-589f-401b-8f25-58f6d8786848"
77 |
78 | // Artifact finds the Atlas artifact by the given name and returns it. Any
79 | // errors that occur are returned, including ErrAuth and ErrNotFound special
80 | // exceptions which the user may want to handle separately.
81 | func (c *Client) Artifact(user, name string) (*Artifact, error) {
82 | endpoint := fmt.Sprintf("/api/v1/artifacts/%s/%s", user, name)
83 | request, err := c.Request("GET", endpoint, nil)
84 | if err != nil {
85 | return nil, err
86 | }
87 |
88 | response, err := checkResp(c.HTTPClient.Do(request))
89 | if err != nil {
90 | return nil, err
91 | }
92 |
93 | var aw artifactWrapper
94 | if err := decodeJSON(response, &aw); err != nil {
95 | return nil, err
96 | }
97 |
98 | return aw.Artifact, nil
99 | }
100 |
101 | // ArtifactSearch searches Atlas for the given ArtifactSearchOpts and returns
102 | // a slice of ArtifactVersions.
103 | func (c *Client) ArtifactSearch(opts *ArtifactSearchOpts) ([]*ArtifactVersion, error) {
104 | log.Printf("[INFO] searching artifacts: %#v", opts)
105 |
106 | params := make(map[string]string)
107 | if opts.Version != "" {
108 | params["version"] = opts.Version
109 | }
110 | if opts.Build != "" {
111 | params["build"] = opts.Build
112 | }
113 |
114 | i := 1
115 | for k, v := range opts.Metadata {
116 | prefix := fmt.Sprintf("metadata.%d.", i)
117 | params[prefix+"key"] = k
118 | if v != MetadataAnyValue {
119 | params[prefix+"value"] = v
120 | }
121 |
122 | i++
123 | }
124 |
125 | endpoint := fmt.Sprintf("/api/v1/artifacts/%s/%s/%s/search",
126 | opts.User, opts.Name, opts.Type)
127 | request, err := c.Request("GET", endpoint, &RequestOptions{
128 | Params: params,
129 | })
130 | if err != nil {
131 | return nil, err
132 | }
133 |
134 | response, err := checkResp(c.HTTPClient.Do(request))
135 | if err != nil {
136 | return nil, err
137 | }
138 |
139 | var w artifactSearchWrapper
140 | if err := decodeJSON(response, &w); err != nil {
141 | return nil, err
142 | }
143 |
144 | return w.Versions, nil
145 | }
146 |
147 | // CreateArtifact creates and returns a new Artifact in Atlas. Any errors that
148 | // occurr are returned.
149 | func (c *Client) CreateArtifact(user, name string) (*Artifact, error) {
150 | log.Printf("[INFO] creating artifact: %s/%s", user, name)
151 | body, err := json.Marshal(&artifactWrapper{&Artifact{
152 | User: user,
153 | Name: name,
154 | }})
155 | if err != nil {
156 | return nil, err
157 | }
158 |
159 | endpoint := "/api/v1/artifacts"
160 | request, err := c.Request("POST", endpoint, &RequestOptions{
161 | Body: bytes.NewReader(body),
162 | Headers: map[string]string{
163 | "Content-Type": "application/json",
164 | },
165 | })
166 | if err != nil {
167 | return nil, err
168 | }
169 |
170 | response, err := checkResp(c.HTTPClient.Do(request))
171 | if err != nil {
172 | return nil, err
173 | }
174 |
175 | var aw artifactWrapper
176 | if err := decodeJSON(response, &aw); err != nil {
177 | return nil, err
178 | }
179 |
180 | return aw.Artifact, nil
181 | }
182 |
183 | // ArtifactFileURL is a helper method for getting the URL for an ArtifactVersion
184 | // from the Client.
185 | func (c *Client) ArtifactFileURL(av *ArtifactVersion) (*url.URL, error) {
186 | if !av.File {
187 | return nil, nil
188 | }
189 |
190 | u := *c.URL
191 | u.Path = fmt.Sprintf("/api/v1/artifacts/%s/%s/%s/%d/file",
192 | av.User, av.Name, av.Type, av.Version)
193 | return &u, nil
194 | }
195 |
196 | // UploadArtifact streams the upload of a file on disk using the given
197 | // UploadArtifactOpts. Any errors that occur are returned.
198 | func (c *Client) UploadArtifact(opts *UploadArtifactOpts) (*ArtifactVersion, error) {
199 | log.Printf("[INFO] uploading artifact: %s/%s (%s)", opts.User, opts.Name, opts.Type)
200 |
201 | endpoint := fmt.Sprintf("/api/v1/artifacts/%s/%s/%s",
202 | opts.User, opts.Name, opts.Type)
203 |
204 | body, err := json.Marshal(opts)
205 | if err != nil {
206 | return nil, err
207 | }
208 |
209 | request, err := c.Request("POST", endpoint, &RequestOptions{
210 | Body: bytes.NewReader(body),
211 | Headers: map[string]string{
212 | "Content-Type": "application/json",
213 | },
214 | })
215 | if err != nil {
216 | return nil, err
217 | }
218 |
219 | response, err := checkResp(c.HTTPClient.Do(request))
220 | if err != nil {
221 | return nil, err
222 | }
223 |
224 | var av ArtifactVersion
225 | if err := decodeJSON(response, &av); err != nil {
226 | return nil, err
227 | }
228 |
229 | if opts.File != nil {
230 | if err := c.putFile(av.UploadPath, opts.File, opts.FileSize); err != nil {
231 | return nil, err
232 | }
233 | }
234 |
235 | return &av, nil
236 | }
237 |
238 | type artifactWrapper struct {
239 | Artifact *Artifact `json:"artifact"`
240 | }
241 |
242 | type artifactSearchWrapper struct {
243 | Versions []*ArtifactVersion
244 | }
245 |
246 | type artifactVersionWrapper struct {
247 | Version *ArtifactVersion
248 | }
249 |
--------------------------------------------------------------------------------
/v1/artifact_test.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestArtifact_fetchesArtifact(t *testing.T) {
9 | server := newTestAtlasServer(t)
10 | defer server.Stop()
11 |
12 | client, err := NewClient(server.URL.String())
13 | if err != nil {
14 | t.Fatal(err)
15 | }
16 |
17 | art, err := client.Artifact("hashicorp", "existing")
18 | if err != nil {
19 | t.Fatal(err)
20 | }
21 |
22 | if art.User != "hashicorp" {
23 | t.Errorf("expected %q to be %q", art.User, "hashicorp")
24 | }
25 |
26 | if art.Name != "existing" {
27 | t.Errorf("expected %q to be %q", art.Name, "existing")
28 | }
29 | }
30 |
31 | func TestArtifact_returnsErrorNoArtifact(t *testing.T) {
32 | server := newTestAtlasServer(t)
33 | defer server.Stop()
34 |
35 | client, err := NewClient(server.URL.String())
36 | if err != nil {
37 | t.Fatal(err)
38 | }
39 |
40 | _, err = client.App("hashicorp", "newproject")
41 | if err == nil {
42 | t.Fatal("expected error, but nothing was returned")
43 | }
44 | }
45 |
46 | func TestArtifactSearch_fetches(t *testing.T) {
47 | server := newTestAtlasServer(t)
48 | defer server.Stop()
49 |
50 | client, err := NewClient(server.URL.String())
51 | if err != nil {
52 | t.Fatal(err)
53 | }
54 |
55 | vs, err := client.ArtifactSearch(&ArtifactSearchOpts{
56 | User: "hashicorp",
57 | Name: "existing1",
58 | Type: "amazon-ami",
59 | })
60 | if err != nil {
61 | t.Fatal(err)
62 | }
63 |
64 | if len(vs) != 1 {
65 | t.Fatalf("bad: %#v", vs)
66 | }
67 | }
68 |
69 | func TestArtifactSearch_metadata(t *testing.T) {
70 | server := newTestAtlasServer(t)
71 | defer server.Stop()
72 |
73 | client, err := NewClient(server.URL.String())
74 | if err != nil {
75 | t.Fatal(err)
76 | }
77 |
78 | vs, err := client.ArtifactSearch(&ArtifactSearchOpts{
79 | User: "hashicorp",
80 | Name: "existing2",
81 | Type: "amazon-ami",
82 | Metadata: map[string]string{
83 | "foo": "bar",
84 | "bar": MetadataAnyValue,
85 | },
86 | })
87 | if err != nil {
88 | t.Fatal(err)
89 | }
90 |
91 | if len(vs) != 1 {
92 | t.Fatalf("bad: %#v", vs)
93 | }
94 | }
95 |
96 | func TestArtifactFileURL(t *testing.T) {
97 | server := newTestAtlasServer(t)
98 | defer server.Stop()
99 |
100 | client, err := NewClient(server.URL.String())
101 | if err != nil {
102 | t.Fatal(err)
103 | }
104 |
105 | v := &ArtifactVersion{
106 | User: "foo",
107 | Name: "bar",
108 | Version: 1,
109 | Type: "vagrant-box",
110 | File: true,
111 | }
112 |
113 | u, err := client.ArtifactFileURL(v)
114 | if err != nil {
115 | t.Fatal(err)
116 | }
117 |
118 | expected := *server.URL
119 | expected.Path = "/api/v1/artifacts/foo/bar/vagrant-box/1/file"
120 | if u.String() != expected.String() {
121 | t.Fatalf("unexpected: %s\n\nexpected: %s", u, expected.String())
122 | }
123 | }
124 |
125 | func TestArtifactFileURL_nil(t *testing.T) {
126 | server := newTestAtlasServer(t)
127 | defer server.Stop()
128 |
129 | client, err := NewClient(server.URL.String())
130 | if err != nil {
131 | t.Fatal(err)
132 | }
133 |
134 | v := &ArtifactVersion{
135 | User: "foo",
136 | Name: "bar",
137 | Type: "vagrant-box",
138 | }
139 |
140 | u, err := client.ArtifactFileURL(v)
141 | if err != nil {
142 | t.Fatal(err)
143 | }
144 | if u != nil {
145 | t.Fatal("should be nil")
146 | }
147 | }
148 |
149 | func TestUploadArtifact(t *testing.T) {
150 | server := newTestAtlasServer(t)
151 | defer server.Stop()
152 |
153 | client, err := NewClient(server.URL.String())
154 | if err != nil {
155 | t.Fatal(err)
156 | }
157 |
158 | data := new(bytes.Buffer)
159 | _, err = client.UploadArtifact(&UploadArtifactOpts{
160 | User: "hashicorp",
161 | Name: "existing",
162 | Type: "amazon-ami",
163 | File: data,
164 | })
165 | if err != nil {
166 | t.Fatal(err)
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/v1/atlas_test.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "net"
10 | "net/http"
11 | "net/url"
12 | "path"
13 | "reflect"
14 | "strconv"
15 | "strings"
16 | "testing"
17 | )
18 |
19 | type atlasServer struct {
20 | URL *url.URL
21 |
22 | t *testing.T
23 | ln net.Listener
24 | server *http.Server
25 | }
26 |
27 | type clientTestResp struct {
28 | RawPath string
29 | Host string
30 | Header http.Header
31 | Body string
32 | }
33 |
34 | func newTestAtlasServer(t *testing.T) *atlasServer {
35 | hs := &atlasServer{t: t}
36 |
37 | ln, err := net.Listen("tcp", ":0")
38 | if err != nil {
39 | t.Fatal(err)
40 | }
41 | hs.ln = ln
42 |
43 | hs.URL = &url.URL{
44 | Scheme: "http",
45 | Host: ln.Addr().String(),
46 | }
47 |
48 | mux := http.NewServeMux()
49 | hs.setupRoutes(mux)
50 |
51 | // TODO: this should be using httptest.Server
52 | server := &http.Server{}
53 | server.Handler = mux
54 | hs.server = server
55 | go server.Serve(ln)
56 |
57 | return hs
58 | }
59 |
60 | func (hs *atlasServer) Stop() {
61 | hs.ln.Close()
62 | }
63 |
64 | func (hs *atlasServer) setupRoutes(mux *http.ServeMux) {
65 | mux.HandleFunc("/_json", hs.jsonHandler)
66 | mux.HandleFunc("/_rails-error", hs.railsHandler)
67 | mux.HandleFunc("/_status/", hs.statusHandler)
68 |
69 | mux.HandleFunc("/_binstore/", hs.binstoreHandler)
70 |
71 | mux.HandleFunc("/api/v1/authenticate", hs.authenticationHandler)
72 | mux.HandleFunc("/api/v1/token", hs.tokenHandler)
73 |
74 | mux.HandleFunc("/api/v1/artifacts/hashicorp/existing", hs.vagrantArtifactExistingHandler)
75 | mux.HandleFunc("/api/v1/artifacts/hashicorp/existing/amazon-ami", hs.vagrantArtifactUploadHandler)
76 | mux.HandleFunc("/api/v1/artifacts/hashicorp/existing1/amazon-ami/search", hs.vagrantArtifactSearchHandler1)
77 | mux.HandleFunc("/api/v1/artifacts/hashicorp/existing2/amazon-ami/search", hs.vagrantArtifactSearchHandler2)
78 |
79 | mux.HandleFunc("/api/v1/vagrant/applications", hs.vagrantCreateAppHandler)
80 | mux.HandleFunc("/api/v1/vagrant/applications/", hs.vagrantCreateAppsHandler)
81 | mux.HandleFunc("/api/v1/vagrant/applications/hashicorp/existing", hs.vagrantAppExistingHandler)
82 | mux.HandleFunc("/api/v1/vagrant/applications/hashicorp/existing/versions", hs.vagrantUploadAppHandler)
83 |
84 | mux.HandleFunc("/api/v1/packer/build-configurations", hs.vagrantBCCreateHandler)
85 | mux.HandleFunc("/api/v1/packer/build-configurations/hashicorp/existing", hs.vagrantBCExistingHandler)
86 | mux.HandleFunc("/api/v1/packer/build-configurations/hashicorp/existing/versions", hs.vagrantBCCreateVersionHandler)
87 |
88 | mux.HandleFunc("/api/v1/terraform/configurations/hashicorp/existing/versions/latest", hs.tfConfigLatest)
89 | mux.HandleFunc("/api/v1/terraform/configurations/hashicorp/existing/versions", hs.tfConfigUpload)
90 |
91 | // add an endpoint for testing arbitrary requests
92 | mux.HandleFunc("/_test", hs.testHandler)
93 | }
94 |
95 | // testHandler echos the data sent from the client in a json object
96 | func (hs *atlasServer) testHandler(w http.ResponseWriter, r *http.Request) {
97 |
98 | req := &clientTestResp{
99 | RawPath: r.URL.RawPath,
100 | Host: r.Host,
101 | Header: r.Header,
102 | }
103 |
104 | body, err := ioutil.ReadAll(r.Body)
105 | if err != nil {
106 | // log this, since an error should fail the test anyway
107 | hs.t.Log("error reading body:", err)
108 | }
109 |
110 | req.Body = string(body)
111 |
112 | js, _ := json.Marshal(req)
113 | if err != nil {
114 | hs.t.Log("error marshaling req:", err)
115 | }
116 |
117 | w.Write(js)
118 | }
119 |
120 | func (hs *atlasServer) statusHandler(w http.ResponseWriter, r *http.Request) {
121 | slice := strings.Split(r.URL.Path, "/")
122 | codeStr := slice[len(slice)-1]
123 |
124 | code, err := strconv.ParseInt(codeStr, 10, 32)
125 | if err != nil {
126 | hs.t.Fatal(err)
127 | }
128 |
129 | w.WriteHeader(int(code))
130 | }
131 |
132 | func (hs *atlasServer) railsHandler(w http.ResponseWriter, r *http.Request) {
133 | w.WriteHeader(422)
134 | w.Header().Set("Content-Type", "application/json")
135 | fmt.Fprintf(w, `{"errors": ["this is an error", "this is another error"]}`)
136 | }
137 |
138 | func (hs *atlasServer) jsonHandler(w http.ResponseWriter, r *http.Request) {
139 | w.Header().Set("Content-Type", "application/json")
140 | fmt.Fprintf(w, `{"ok": true}`)
141 | }
142 |
143 | func (hs *atlasServer) authenticationHandler(w http.ResponseWriter, r *http.Request) {
144 | if err := r.ParseForm(); err != nil {
145 | hs.t.Fatal(err)
146 | }
147 |
148 | login, password := r.Form["user[login]"][0], r.Form["user[password]"][0]
149 |
150 | if login == "sethloves" && password == "bacon" {
151 | w.WriteHeader(http.StatusOK)
152 | fmt.Fprintf(w, `
153 | {
154 | "token": "pX4AQ5vO7T-xJrxsnvlB0cfeF-tGUX-A-280LPxoryhDAbwmox7PKinMgA1F6R3BKaT"
155 | }
156 | `)
157 | } else {
158 | w.WriteHeader(http.StatusUnauthorized)
159 | }
160 | }
161 |
162 | func (hs *atlasServer) tokenHandler(w http.ResponseWriter, r *http.Request) {
163 | if r.Method != "GET" {
164 | w.WriteHeader(http.StatusMethodNotAllowed)
165 | return
166 | }
167 |
168 | token := r.Header.Get(atlasTokenHeader)
169 | if token == "a.atlasv1.b" {
170 | w.WriteHeader(http.StatusOK)
171 | } else {
172 | w.WriteHeader(http.StatusUnauthorized)
173 | }
174 | }
175 |
176 | func (hs *atlasServer) tfConfigLatest(w http.ResponseWriter, r *http.Request) {
177 | if r.Method != "GET" {
178 | w.WriteHeader(http.StatusMethodNotAllowed)
179 | return
180 | }
181 |
182 | fmt.Fprintf(w, `
183 | {
184 | "version": {
185 | "version": 5,
186 | "metadata": { "foo": "bar" },
187 | "variables": { "foo": "bar" }
188 | }
189 | }
190 | `)
191 | }
192 |
193 | func (hs *atlasServer) tfConfigUpload(w http.ResponseWriter, r *http.Request) {
194 | if r.Method != "POST" {
195 | w.WriteHeader(http.StatusMethodNotAllowed)
196 | return
197 | }
198 |
199 | var buf bytes.Buffer
200 | if _, err := io.Copy(&buf, r.Body); err != nil {
201 | w.WriteHeader(http.StatusMethodNotAllowed)
202 | return
203 | }
204 |
205 | if buf.Len() == 0 {
206 | w.WriteHeader(http.StatusConflict)
207 | return
208 | }
209 |
210 | uploadPath := hs.URL.String() + "/_binstore/"
211 |
212 | w.WriteHeader(http.StatusOK)
213 | fmt.Fprintf(w, `
214 | {
215 | "version": 5,
216 | "upload_path": "%s"
217 | }
218 | `, uploadPath)
219 | }
220 |
221 | func (hs *atlasServer) vagrantArtifactExistingHandler(w http.ResponseWriter, r *http.Request) {
222 | if r.Method != "GET" {
223 | w.WriteHeader(http.StatusMethodNotAllowed)
224 | return
225 | }
226 |
227 | fmt.Fprintf(w, `
228 | {
229 | "artifact": {
230 | "username": "hashicorp",
231 | "name": "existing",
232 | "tag": "hashicorp/existing"
233 | }
234 | }
235 | `)
236 | }
237 |
238 | func (hs *atlasServer) vagrantArtifactSearchHandler1(w http.ResponseWriter, r *http.Request) {
239 | if r.Method != "GET" {
240 | w.WriteHeader(http.StatusMethodNotAllowed)
241 | return
242 | }
243 |
244 | fmt.Fprintf(w, `
245 | {
246 | "versions": [{
247 | "username": "hashicorp",
248 | "name": "existing",
249 | "tag": "hashicorp/existing"
250 | }]
251 | }
252 | `)
253 | }
254 |
255 | func (hs *atlasServer) vagrantArtifactSearchHandler2(w http.ResponseWriter, r *http.Request) {
256 | if r.Method != "GET" {
257 | w.WriteHeader(http.StatusMethodNotAllowed)
258 | return
259 | }
260 |
261 | if err := r.ParseForm(); err != nil {
262 | w.WriteHeader(http.StatusMethodNotAllowed)
263 | return
264 | }
265 |
266 | if r.Form.Get("metadata.1.key") == "" {
267 | w.WriteHeader(http.StatusMethodNotAllowed)
268 | return
269 | }
270 | if r.Form.Get("metadata.2.key") == "" {
271 | w.WriteHeader(http.StatusMethodNotAllowed)
272 | return
273 | }
274 |
275 | fmt.Fprintf(w, `
276 | {
277 | "versions": [{
278 | "username": "hashicorp",
279 | "name": "existing",
280 | "tag": "hashicorp/existing"
281 | }]
282 | }
283 | `)
284 | }
285 |
286 | func (hs *atlasServer) vagrantArtifactUploadHandler(w http.ResponseWriter, r *http.Request) {
287 | if r.Method != "POST" {
288 | w.WriteHeader(http.StatusMethodNotAllowed)
289 | return
290 | }
291 |
292 | var buf bytes.Buffer
293 | if _, err := io.Copy(&buf, r.Body); err != nil {
294 | w.WriteHeader(http.StatusMethodNotAllowed)
295 | return
296 | }
297 |
298 | if buf.Len() == 0 {
299 | w.WriteHeader(http.StatusConflict)
300 | return
301 | }
302 |
303 | uploadPath := hs.URL.String() + "/_binstore/"
304 |
305 | w.WriteHeader(http.StatusOK)
306 | fmt.Fprintf(w, `
307 | {
308 | "upload_path": "%s"
309 | }
310 | `, uploadPath)
311 | }
312 |
313 | func (hs *atlasServer) vagrantAppExistingHandler(w http.ResponseWriter, r *http.Request) {
314 | if r.Method != "GET" {
315 | w.WriteHeader(http.StatusMethodNotAllowed)
316 | return
317 | }
318 |
319 | fmt.Fprintf(w, `
320 | {
321 | "username": "hashicorp",
322 | "name": "existing",
323 | "tag": "hashicorp/existing",
324 | "private": true
325 | }
326 | `)
327 | }
328 |
329 | func (hs *atlasServer) vagrantBCCreateHandler(w http.ResponseWriter, r *http.Request) {
330 | if r.Method != "POST" {
331 | w.WriteHeader(http.StatusMethodNotAllowed)
332 | return
333 | }
334 |
335 | var wrapper bcWrapper
336 | dec := json.NewDecoder(r.Body)
337 | if err := dec.Decode(&wrapper); err != nil && err != io.EOF {
338 | hs.t.Fatal(err)
339 | }
340 | bc := wrapper.BuildConfig
341 |
342 | if bc.User != "hashicorp" {
343 | w.WriteHeader(http.StatusConflict)
344 | return
345 | }
346 |
347 | w.WriteHeader(http.StatusOK)
348 | fmt.Fprintf(w, `
349 | {
350 | "username":"hashicorp",
351 | "name":"new",
352 | "tag":"hashicorp/new",
353 | "private":true
354 | }
355 | `)
356 | }
357 |
358 | func (hs *atlasServer) vagrantBCCreateVersionHandler(w http.ResponseWriter, r *http.Request) {
359 | if r.Method != "POST" {
360 | w.WriteHeader(http.StatusMethodNotAllowed)
361 | return
362 | }
363 |
364 | var wrapper bcCreateWrapper
365 | dec := json.NewDecoder(r.Body)
366 | if err := dec.Decode(&wrapper); err != nil && err != io.EOF {
367 | hs.t.Fatal(err)
368 | }
369 | builds := wrapper.Version.Builds
370 |
371 | if len(builds) == 0 {
372 | w.WriteHeader(http.StatusConflict)
373 | return
374 | }
375 |
376 | expected := map[string]interface{}{"testing": true}
377 | if !reflect.DeepEqual(wrapper.Version.Metadata, expected) {
378 | hs.t.Fatalf("expected %q to be %q", wrapper.Version.Metadata, expected)
379 | }
380 |
381 | uploadPath := hs.URL.String() + "/_binstore/"
382 |
383 | w.WriteHeader(http.StatusOK)
384 | fmt.Fprintf(w, `
385 | {
386 | "upload_path": "%s"
387 | }
388 | `, uploadPath)
389 | }
390 |
391 | func (hs *atlasServer) vagrantBCExistingHandler(w http.ResponseWriter, r *http.Request) {
392 | if r.Method != "GET" {
393 | w.WriteHeader(http.StatusMethodNotAllowed)
394 | return
395 | }
396 |
397 | fmt.Fprintf(w, `
398 | {
399 | "username": "hashicorp",
400 | "name": "existing"
401 | }
402 | `)
403 | }
404 |
405 | func (hs *atlasServer) vagrantCreateAppHandler(w http.ResponseWriter, r *http.Request) {
406 | if r.Method != "POST" {
407 | w.WriteHeader(http.StatusMethodNotAllowed)
408 | return
409 | }
410 |
411 | var aw appWrapper
412 | dec := json.NewDecoder(r.Body)
413 | if err := dec.Decode(&aw); err != nil && err != io.EOF {
414 | hs.t.Fatal(err)
415 | }
416 | app := aw.Application
417 |
418 | if app.User == "hashicorp" && app.Name == "existing" {
419 | w.WriteHeader(http.StatusConflict)
420 | } else {
421 | body, err := json.Marshal(app)
422 | if err != nil {
423 | hs.t.Fatal(err)
424 | }
425 |
426 | w.WriteHeader(http.StatusOK)
427 | fmt.Fprintf(w, string(body))
428 | }
429 | }
430 |
431 | func (hs *atlasServer) vagrantCreateAppsHandler(w http.ResponseWriter, r *http.Request) {
432 | if r.Method != "GET" {
433 | w.WriteHeader(http.StatusMethodNotAllowed)
434 | return
435 | }
436 |
437 | split := strings.Split(r.RequestURI, "/")
438 | parts := split[len(split)-2:]
439 | user, name := parts[0], parts[1]
440 |
441 | if user == "hashicorp" && name == "existing" {
442 | body, err := json.Marshal(&App{
443 | User: "hashicorp",
444 | Name: "existing",
445 | })
446 | if err != nil {
447 | hs.t.Fatal(err)
448 | }
449 |
450 | w.WriteHeader(http.StatusOK)
451 | fmt.Fprintf(w, string(body))
452 | } else {
453 | w.WriteHeader(http.StatusNotFound)
454 | }
455 | }
456 |
457 | func (hs *atlasServer) vagrantUploadAppHandler(w http.ResponseWriter, r *http.Request) {
458 | u := *hs.URL
459 | u.Path = path.Join(u.Path, "_binstore/630e42d9-2364-2412-4121-18266770468e")
460 |
461 | var buf bytes.Buffer
462 | if _, err := io.Copy(&buf, r.Body); err != nil {
463 | hs.t.Fatal(err)
464 | }
465 | expected := `{"application":{"metadata":{"testing":true}}}`
466 | if buf.String() != expected {
467 | hs.t.Fatalf("expected metadata to be %q, but was %q", expected, buf.String())
468 | }
469 |
470 | body, err := json.Marshal(&appVersion{
471 | UploadPath: u.String(),
472 | Token: "630e42d9-2364-2412-4121-18266770468e",
473 | Version: 125,
474 | })
475 | if err != nil {
476 | hs.t.Fatal(err)
477 | }
478 |
479 | w.WriteHeader(http.StatusOK)
480 | fmt.Fprintf(w, string(body))
481 | }
482 |
483 | func (hs *atlasServer) binstoreHandler(w http.ResponseWriter, r *http.Request) {
484 | if r.Method != "PUT" {
485 | w.WriteHeader(http.StatusMethodNotAllowed)
486 | return
487 | }
488 |
489 | w.WriteHeader(http.StatusOK)
490 | }
491 |
--------------------------------------------------------------------------------
/v1/authentication.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/url"
7 | "strings"
8 | )
9 |
10 | // Login accepts a username and password as string arguments. Both username and
11 | // password must be non-nil, non-empty values. Atlas does not permit
12 | // passwordless authentication.
13 | //
14 | // If authentication is unsuccessful, an error is returned with the body of the
15 | // error containing the server's response.
16 | //
17 | // If authentication is successful, this method sets the Token value on the
18 | // Client and returns the Token as a string.
19 | func (c *Client) Login(username, password string) (string, error) {
20 | log.Printf("[INFO] logging in user %s", username)
21 |
22 | if len(username) == 0 {
23 | return "", fmt.Errorf("client: missing username")
24 | }
25 |
26 | if len(password) == 0 {
27 | return "", fmt.Errorf("client: missing password")
28 | }
29 |
30 | // Make a request
31 | request, err := c.Request("POST", "/api/v1/authenticate", &RequestOptions{
32 | Body: strings.NewReader(url.Values{
33 | "user[login]": []string{username},
34 | "user[password]": []string{password},
35 | "user[description]": []string{"Created by the Atlas Go Client"},
36 | }.Encode()),
37 | Headers: map[string]string{
38 | "Content-Type": "application/x-www-form-urlencoded",
39 | },
40 | })
41 | if err != nil {
42 | return "", err
43 | }
44 |
45 | // Make the request
46 | response, err := checkResp(c.HTTPClient.Do(request))
47 | if err != nil {
48 | return "", err
49 | }
50 |
51 | // Decode the body
52 | var tResponse struct{ Token string }
53 | if err := decodeJSON(response, &tResponse); err != nil {
54 | return "", nil
55 | }
56 |
57 | // Set the token
58 | log.Printf("[DEBUG] setting atlas token (%s)", maskString(tResponse.Token))
59 | c.Token = tResponse.Token
60 |
61 | // Return the token
62 | return c.Token, nil
63 | }
64 |
65 | // Verify verifies that authentication and communication with Atlas
66 | // is properly functioning.
67 | func (c *Client) Verify() error {
68 | log.Printf("[INFO] verifying authentication")
69 |
70 | request, err := c.Request("GET", "/api/v1/authenticate", nil)
71 | if err != nil {
72 | return err
73 | }
74 |
75 | _, err = checkResp(c.HTTPClient.Do(request))
76 | return err
77 | }
78 |
79 | // maskString masks all but the first few characters of a string for display
80 | // output. This is useful for tokens so we can display them to the user without
81 | // showing the full output.
82 | func maskString(s string) string {
83 | if len(s) <= 3 {
84 | return "*** (masked)"
85 | }
86 |
87 | return s[0:3] + "*** (masked)"
88 | }
89 |
--------------------------------------------------------------------------------
/v1/authentication_test.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import "testing"
4 |
5 | func TestMaskString_emptyString(t *testing.T) {
6 | result := maskString("")
7 | expected := "*** (masked)"
8 |
9 | if result != expected {
10 | t.Errorf("expected %s to be %s", result, expected)
11 | }
12 | }
13 |
14 | func TestMaskString_threeString(t *testing.T) {
15 | result := maskString("123")
16 | expected := "*** (masked)"
17 |
18 | if result != expected {
19 | t.Errorf("expected %s to be %s", result, expected)
20 | }
21 | }
22 |
23 | func TestMaskString_longerString(t *testing.T) {
24 | result := maskString("ABCD1234")
25 | expected := "ABC*** (masked)"
26 |
27 | if result != expected {
28 | t.Errorf("expected %s to be %s", result, expected)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/v1/build_config.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "log"
9 | )
10 |
11 | // bcWrapper is the API wrapper since the server wraps the resulting object.
12 | type bcWrapper struct {
13 | BuildConfig *BuildConfig `json:"build_configuration"`
14 | }
15 |
16 | // Atlas expects a list of key/value vars
17 | type BuildVar struct {
18 | Key string `json:"key"`
19 | Value string `json:"value"`
20 | Sensitive bool `json:"sensitive"`
21 | }
22 | type BuildVars []BuildVar
23 |
24 | // BuildConfig represents a Packer build configuration.
25 | type BuildConfig struct {
26 | // User is the namespace under which the build config lives
27 | User string `json:"username"`
28 |
29 | // Name is the actual name of the build config, unique in the scope
30 | // of the username.
31 | Name string `json:"name"`
32 | }
33 |
34 | // Slug returns the slug format for this BuildConfig (User/Name)
35 | func (b *BuildConfig) Slug() string {
36 | return fmt.Sprintf("%s/%s", b.User, b.Name)
37 | }
38 |
39 | // BuildConfigVersion represents a single uploaded (or uploadable) version
40 | // of a build configuration.
41 | type BuildConfigVersion struct {
42 | // The fields below are the username/name combo to uniquely identify
43 | // a build config.
44 | User string `json:"username"`
45 | Name string `json:"name"`
46 |
47 | // Builds is the list of builds that this version supports.
48 | Builds []BuildConfigBuild
49 | }
50 |
51 | // Slug returns the slug format for this BuildConfigVersion (User/Name)
52 | func (bv *BuildConfigVersion) Slug() string {
53 | return fmt.Sprintf("%s/%s", bv.User, bv.Name)
54 | }
55 |
56 | // BuildConfigBuild is a single build that is present in an uploaded
57 | // build configuration.
58 | type BuildConfigBuild struct {
59 | // Name is a unique name for this build
60 | Name string `json:"name"`
61 |
62 | // Type is the type of builder that this build needs to run on,
63 | // such as "amazon-ebs" or "qemu".
64 | Type string `json:"type"`
65 |
66 | // Artifact is true if this build results in one or more artifacts
67 | // being sent to Atlas
68 | Artifact bool `json:"artifact"`
69 | }
70 |
71 | // BuildConfig gets a single build configuration by user and name.
72 | func (c *Client) BuildConfig(user, name string) (*BuildConfig, error) {
73 | log.Printf("[INFO] getting build configuration %s/%s", user, name)
74 |
75 | endpoint := fmt.Sprintf("/api/v1/packer/build-configurations/%s/%s", user, name)
76 | request, err := c.Request("GET", endpoint, nil)
77 | if err != nil {
78 | return nil, err
79 | }
80 |
81 | response, err := checkResp(c.HTTPClient.Do(request))
82 | if err != nil {
83 | return nil, err
84 | }
85 |
86 | var bc BuildConfig
87 | if err := decodeJSON(response, &bc); err != nil {
88 | return nil, err
89 | }
90 |
91 | return &bc, nil
92 | }
93 |
94 | // CreateBuildConfig creates a new build configuration.
95 | func (c *Client) CreateBuildConfig(user, name string) (*BuildConfig, error) {
96 | log.Printf("[INFO] creating build configuration %s/%s", user, name)
97 |
98 | endpoint := "/api/v1/packer/build-configurations"
99 | body, err := json.Marshal(&bcWrapper{
100 | BuildConfig: &BuildConfig{
101 | User: user,
102 | Name: name,
103 | },
104 | })
105 | if err != nil {
106 | return nil, err
107 | }
108 |
109 | request, err := c.Request("POST", endpoint, &RequestOptions{
110 | Body: bytes.NewReader(body),
111 | Headers: map[string]string{
112 | "Content-Type": "application/json",
113 | },
114 | })
115 | if err != nil {
116 | return nil, err
117 | }
118 |
119 | response, err := checkResp(c.HTTPClient.Do(request))
120 | if err != nil {
121 | return nil, err
122 | }
123 |
124 | var bc BuildConfig
125 | if err := decodeJSON(response, &bc); err != nil {
126 | return nil, err
127 | }
128 |
129 | return &bc, nil
130 | }
131 |
132 | // UploadBuildConfigVersion creates a single build configuration version
133 | // and uploads the template associated with it.
134 | //
135 | // Actual API: "Create Build Config Version"
136 | func (c *Client) UploadBuildConfigVersion(v *BuildConfigVersion, metadata map[string]interface{},
137 | vars BuildVars, data io.Reader, size int64) error {
138 |
139 | log.Printf("[INFO] uploading build configuration version %s (%d bytes), with metadata %q",
140 | v.Slug(), size, metadata)
141 |
142 | endpoint := fmt.Sprintf("/api/v1/packer/build-configurations/%s/%s/versions",
143 | v.User, v.Name)
144 |
145 | var bodyData bcCreateWrapper
146 | bodyData.Version.Builds = v.Builds
147 | bodyData.Version.Metadata = metadata
148 | bodyData.Version.Vars = vars
149 | body, err := json.Marshal(bodyData)
150 | if err != nil {
151 | return err
152 | }
153 |
154 | request, err := c.Request("POST", endpoint, &RequestOptions{
155 | Body: bytes.NewReader(body),
156 | Headers: map[string]string{
157 | "Content-Type": "application/json",
158 | },
159 | })
160 | if err != nil {
161 | return err
162 | }
163 |
164 | response, err := checkResp(c.HTTPClient.Do(request))
165 | if err != nil {
166 | return err
167 | }
168 |
169 | var bv bcCreate
170 | if err := decodeJSON(response, &bv); err != nil {
171 | return err
172 | }
173 |
174 | if err := c.putFile(bv.UploadPath, data, size); err != nil {
175 | return err
176 | }
177 |
178 | return nil
179 | }
180 |
181 | // bcCreate is the struct returned when creating a build configuration.
182 | type bcCreate struct {
183 | UploadPath string `json:"upload_path"`
184 | }
185 |
186 | // bcCreateWrapper is the wrapper for creating a build config.
187 | type bcCreateWrapper struct {
188 | Version struct {
189 | Metadata map[string]interface{} `json:"metadata,omitempty"`
190 | Builds []BuildConfigBuild `json:"builds"`
191 | Vars BuildVars `json:"packer_vars,omitempty"`
192 | } `json:"version"`
193 | }
194 |
--------------------------------------------------------------------------------
/v1/build_config_test.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "bytes"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | func TestBuildConfig_slug(t *testing.T) {
10 | bc := &BuildConfig{User: "sethvargo", Name: "bacon"}
11 | expected := "sethvargo/bacon"
12 | if bc.Slug() != expected {
13 | t.Errorf("expected %q to be %q", bc.Slug(), expected)
14 | }
15 | }
16 |
17 | func TestBuildConfigVersion_slug(t *testing.T) {
18 | bc := &BuildConfigVersion{User: "sethvargo", Name: "bacon"}
19 | expected := "sethvargo/bacon"
20 | if bc.Slug() != expected {
21 | t.Errorf("expected %q to be %q", bc.Slug(), expected)
22 | }
23 | }
24 |
25 | func TestBuildConfig_fetches(t *testing.T) {
26 | server := newTestAtlasServer(t)
27 | defer server.Stop()
28 |
29 | client, err := NewClient(server.URL.String())
30 | if err != nil {
31 | t.Fatal(err)
32 | }
33 |
34 | actual, err := client.BuildConfig("hashicorp", "existing")
35 | if err != nil {
36 | t.Fatal(err)
37 | }
38 |
39 | expected := &BuildConfig{
40 | User: "hashicorp",
41 | Name: "existing",
42 | }
43 |
44 | if !reflect.DeepEqual(actual, expected) {
45 | t.Fatalf("%#v", actual)
46 | }
47 | }
48 |
49 | func TestCreateBuildConfig(t *testing.T) {
50 | server := newTestAtlasServer(t)
51 | defer server.Stop()
52 |
53 | client, err := NewClient(server.URL.String())
54 | if err != nil {
55 | t.Fatal(err)
56 | }
57 |
58 | user, name := "hashicorp", "new"
59 | bc, err := client.CreateBuildConfig(user, name)
60 | if err != nil {
61 | t.Fatal(err)
62 | }
63 |
64 | if bc.User != user {
65 | t.Errorf("expected %q to be %q", bc.User, user)
66 | }
67 |
68 | if bc.Name != name {
69 | t.Errorf("expected %q to be %q", bc.Name, name)
70 | }
71 | }
72 |
73 | func TestUploadBuildConfigVersion(t *testing.T) {
74 | server := newTestAtlasServer(t)
75 | defer server.Stop()
76 |
77 | client, err := NewClient(server.URL.String())
78 | if err != nil {
79 | t.Fatal(err)
80 | }
81 |
82 | bc := &BuildConfigVersion{
83 | User: "hashicorp",
84 | Name: "existing",
85 | Builds: []BuildConfigBuild{
86 | BuildConfigBuild{Name: "foo", Type: "ami"},
87 | },
88 | }
89 | metadata := map[string]interface{}{"testing": true}
90 | vars := BuildVars{BuildVar{Key: "one", Value: "two"}}
91 | data := new(bytes.Buffer)
92 | err = client.UploadBuildConfigVersion(bc, metadata, vars, data, int64(data.Len()))
93 | if err != nil {
94 | t.Fatal(err)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/v1/client.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "bytes"
5 | "crypto/tls"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "log"
10 | "net/http"
11 | "net/url"
12 | "os"
13 | "path"
14 | "runtime"
15 | "strings"
16 |
17 | "github.com/hashicorp/go-cleanhttp"
18 | "github.com/hashicorp/go-rootcerts"
19 | )
20 |
21 | const (
22 | // atlasDefaultEndpoint is the default base URL for connecting to Atlas.
23 | atlasDefaultEndpoint = "https://atlas.hashicorp.com"
24 |
25 | // atlasEndpointEnvVar is the environment variable that overrrides the
26 | // default Atlas address.
27 | atlasEndpointEnvVar = "ATLAS_ADDRESS"
28 |
29 | // atlasCAFileEnvVar is the environment variable that causes the client to
30 | // load trusted certs from a file
31 | atlasCAFileEnvVar = "ATLAS_CAFILE"
32 |
33 | // atlasCAPathEnvVar is the environment variable that causes the client to
34 | // load trusted certs from a directory
35 | atlasCAPathEnvVar = "ATLAS_CAPATH"
36 |
37 | // atlasTLSNoVerifyEnvVar disables TLS verification, similar to curl -k
38 | // This defaults to false (verify) and will change to true (skip
39 | // verification) with any non-empty value
40 | atlasTLSNoVerifyEnvVar = "ATLAS_TLS_NOVERIFY"
41 |
42 | // atlasTokenHeader is the header key used for authenticating with Atlas
43 | atlasTokenHeader = "X-Atlas-Token"
44 | )
45 |
46 | var projectURL = "https://github.com/hashicorp/atlas-go"
47 | var userAgent = fmt.Sprintf("AtlasGo/1.0 (+%s; %s)",
48 | projectURL, runtime.Version())
49 |
50 | // ErrAuth is the error returned if a 401 is returned by an API request.
51 | var ErrAuth = fmt.Errorf("authentication failed")
52 |
53 | // ErrNotFound is the error returned if a 404 is returned by an API request.
54 | var ErrNotFound = fmt.Errorf("resource not found")
55 |
56 | // RailsError represents an error that was returned from the Rails server.
57 | type RailsError struct {
58 | Errors []string `json:"errors"`
59 | }
60 |
61 | // Error collects all of the errors in the RailsError and returns a comma-
62 | // separated list of the errors that were returned from the server.
63 | func (re *RailsError) Error() string {
64 | return strings.Join(re.Errors, ", ")
65 | }
66 |
67 | // Client represents a single connection to a Atlas API endpoint.
68 | type Client struct {
69 | // URL is the full endpoint address to the Atlas server including the
70 | // protocol, port, and path.
71 | URL *url.URL
72 |
73 | // Token is the Atlas authentication token
74 | Token string
75 |
76 | // HTTPClient is the underlying http client with which to make requests.
77 | HTTPClient *http.Client
78 |
79 | // DefaultHeaders is a set of headers that will be added to every request.
80 | // This minimally includes the atlas user-agent string.
81 | DefaultHeader http.Header
82 | }
83 |
84 | // DefaultClient returns a client that connects to the Atlas API.
85 | func DefaultClient() *Client {
86 | atlasEndpoint := os.Getenv(atlasEndpointEnvVar)
87 | if atlasEndpoint == "" {
88 | atlasEndpoint = atlasDefaultEndpoint
89 | }
90 |
91 | client, err := NewClient(atlasEndpoint)
92 | if err != nil {
93 | panic(err)
94 | }
95 |
96 | return client
97 | }
98 |
99 | // NewClient creates a new Atlas Client from the given URL (as a string). If
100 | // the URL cannot be parsed, an error is returned. The HTTPClient is set to
101 | // an empty http.Client, but this can be changed programmatically by setting
102 | // client.HTTPClient. The user can also programmatically set the URL as a
103 | // *url.URL.
104 | func NewClient(urlString string) (*Client, error) {
105 | if len(urlString) == 0 {
106 | return nil, fmt.Errorf("client: missing url")
107 | }
108 |
109 | parsedURL, err := url.Parse(urlString)
110 | if err != nil {
111 | return nil, err
112 | }
113 |
114 | token := os.Getenv("ATLAS_TOKEN")
115 | if token != "" {
116 | log.Printf("[DEBUG] using ATLAS_TOKEN (%s)", maskString(token))
117 | }
118 |
119 | client := &Client{
120 | URL: parsedURL,
121 | Token: token,
122 | DefaultHeader: make(http.Header),
123 | }
124 |
125 | client.DefaultHeader.Set("User-Agent", userAgent)
126 |
127 | if err := client.init(); err != nil {
128 | return nil, err
129 | }
130 |
131 | return client, nil
132 | }
133 |
134 | // init() sets defaults on the client.
135 | func (c *Client) init() error {
136 | c.HTTPClient = cleanhttp.DefaultClient()
137 | tlsConfig := &tls.Config{}
138 | if os.Getenv(atlasTLSNoVerifyEnvVar) != "" {
139 | tlsConfig.InsecureSkipVerify = true
140 | }
141 | err := rootcerts.ConfigureTLS(tlsConfig, &rootcerts.Config{
142 | CAFile: os.Getenv(atlasCAFileEnvVar),
143 | CAPath: os.Getenv(atlasCAPathEnvVar),
144 | })
145 | if err != nil {
146 | return err
147 | }
148 | t := cleanhttp.DefaultTransport()
149 | t.TLSClientConfig = tlsConfig
150 | c.HTTPClient.Transport = t
151 | return nil
152 | }
153 |
154 | // RequestOptions is the list of options to pass to the request.
155 | type RequestOptions struct {
156 | // Params is a map of key-value pairs that will be added to the Request.
157 | Params map[string]string
158 |
159 | // Headers is a map of key-value pairs that will be added to the Request.
160 | Headers map[string]string
161 |
162 | // Body is an io.Reader object that will be streamed or uploaded with the
163 | // Request. BodyLength is the final size of the Body.
164 | Body io.Reader
165 | BodyLength int64
166 | }
167 |
168 | // Request creates a new HTTP request using the given verb and sub path.
169 | func (c *Client) Request(verb, spath string, ro *RequestOptions) (*http.Request, error) {
170 | log.Printf("[INFO] request: %s %s", verb, spath)
171 |
172 | // Ensure we have a RequestOptions struct (passing nil is an acceptable)
173 | if ro == nil {
174 | ro = new(RequestOptions)
175 | }
176 |
177 | // Create a new URL with the appended path
178 | u := *c.URL
179 | u.Path = path.Join(c.URL.Path, spath)
180 |
181 | // Add the token and other params
182 | if c.Token != "" {
183 | log.Printf("[DEBUG] request: appending token (%s)", maskString(c.Token))
184 | if ro.Headers == nil {
185 | ro.Headers = make(map[string]string)
186 | }
187 |
188 | ro.Headers[atlasTokenHeader] = c.Token
189 | }
190 |
191 | return c.rawRequest(verb, &u, ro)
192 | }
193 |
194 | func (c *Client) putFile(rawURL string, r io.Reader, size int64) error {
195 | log.Printf("[INFO] putting file: %s", rawURL)
196 |
197 | url, err := url.Parse(rawURL)
198 | if err != nil {
199 | return err
200 | }
201 |
202 | request, err := c.rawRequest("PUT", url, &RequestOptions{
203 | Body: r,
204 | BodyLength: size,
205 | })
206 | if err != nil {
207 | return err
208 | }
209 |
210 | if _, err := checkResp(c.HTTPClient.Do(request)); err != nil {
211 | return err
212 | }
213 |
214 | return nil
215 | }
216 |
217 | // rawRequest accepts a verb, URL, and RequestOptions struct and returns the
218 | // constructed http.Request and any errors that occurred
219 | func (c *Client) rawRequest(verb string, u *url.URL, ro *RequestOptions) (*http.Request, error) {
220 | if verb == "" {
221 | return nil, fmt.Errorf("client: missing verb")
222 | }
223 |
224 | if u == nil {
225 | return nil, fmt.Errorf("client: missing URL.url")
226 | }
227 |
228 | if ro == nil {
229 | return nil, fmt.Errorf("client: missing RequestOptions")
230 | }
231 |
232 | // Add the token and other params
233 | var params = make(url.Values)
234 | for k, v := range ro.Params {
235 | params.Add(k, v)
236 | }
237 | u.RawQuery = params.Encode()
238 |
239 | // Create the request object
240 | request, err := http.NewRequest(verb, u.String(), ro.Body)
241 | if err != nil {
242 | return nil, err
243 | }
244 |
245 | // set our default headers first
246 | for k, v := range c.DefaultHeader {
247 | request.Header[k] = v
248 | }
249 |
250 | // Add any request headers (auth will be here if set)
251 | for k, v := range ro.Headers {
252 | request.Header.Add(k, v)
253 | }
254 |
255 | // Add content-length if we have it
256 | if ro.BodyLength > 0 {
257 | request.ContentLength = ro.BodyLength
258 | }
259 |
260 | log.Printf("[DEBUG] raw request: %#v", request)
261 |
262 | return request, nil
263 | }
264 |
265 | // checkResp wraps http.Client.Do() and verifies that the request was
266 | // successful. A non-200 request returns an error formatted to included any
267 | // validation problems or otherwise.
268 | func checkResp(resp *http.Response, err error) (*http.Response, error) {
269 | // If the err is already there, there was an error higher up the chain, so
270 | // just return that
271 | if err != nil {
272 | return resp, err
273 | }
274 |
275 | log.Printf("[INFO] response: %d (%s)", resp.StatusCode, resp.Status)
276 | var buf bytes.Buffer
277 | if _, err := io.Copy(&buf, resp.Body); err != nil {
278 | log.Printf("[ERR] response: error copying response body")
279 | } else {
280 | log.Printf("[DEBUG] response: %s", buf.String())
281 |
282 | // We are going to reset the response body, so we need to close the old
283 | // one or else it will leak.
284 | resp.Body.Close()
285 | resp.Body = &bytesReadCloser{&buf}
286 | }
287 |
288 | switch resp.StatusCode {
289 | case 200:
290 | return resp, nil
291 | case 201:
292 | return resp, nil
293 | case 202:
294 | return resp, nil
295 | case 204:
296 | return resp, nil
297 | case 400:
298 | return nil, parseErr(resp)
299 | case 401:
300 | return nil, ErrAuth
301 | case 404:
302 | return nil, ErrNotFound
303 | case 422:
304 | return nil, parseErr(resp)
305 | default:
306 | return nil, fmt.Errorf("client: %s", resp.Status)
307 | }
308 | }
309 |
310 | // parseErr is used to take an error JSON response and return a single string
311 | // for use in error messages.
312 | func parseErr(r *http.Response) error {
313 | re := &RailsError{}
314 |
315 | if err := decodeJSON(r, &re); err != nil {
316 | return fmt.Errorf("error decoding JSON body: %s", err)
317 | }
318 |
319 | return re
320 | }
321 |
322 | // decodeJSON is used to JSON decode a body into an interface.
323 | func decodeJSON(resp *http.Response, out interface{}) error {
324 | defer resp.Body.Close()
325 | dec := json.NewDecoder(resp.Body)
326 | return dec.Decode(out)
327 | }
328 |
329 | // bytesReadCloser is a simple wrapper around a bytes buffer that implements
330 | // Close as a noop.
331 | type bytesReadCloser struct {
332 | *bytes.Buffer
333 | }
334 |
335 | func (nrc *bytesReadCloser) Close() error {
336 | // we don't actually have to do anything here, since the buffer is just some
337 | // data in memory and the error is initialized to no-error
338 | return nil
339 | }
340 |
--------------------------------------------------------------------------------
/v1/client_test.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 | "os"
7 | "reflect"
8 | "strings"
9 | "testing"
10 | )
11 |
12 | func TestDefaultClient_url(t *testing.T) {
13 | client := DefaultClient()
14 |
15 | if client.URL.String() != atlasDefaultEndpoint {
16 | t.Fatalf("expected %q to be %q", client.URL.String(), atlasDefaultEndpoint)
17 | }
18 | }
19 |
20 | func TestDefaultClient_urlFromEnvVar(t *testing.T) {
21 | defer os.Setenv(atlasEndpointEnvVar, os.Getenv(atlasEndpointEnvVar))
22 | otherEndpoint := "http://127.0.0.1:1234"
23 |
24 | err := os.Setenv(atlasEndpointEnvVar, otherEndpoint)
25 | if err != nil {
26 | t.Fatal(err)
27 | }
28 |
29 | client := DefaultClient()
30 |
31 | if client.URL.String() != otherEndpoint {
32 | t.Fatalf("expected %q to be %q", client.URL.String(), otherEndpoint)
33 | }
34 | }
35 |
36 | func TestNewClient_badURL(t *testing.T) {
37 | _, err := NewClient("")
38 | if err == nil {
39 | t.Fatal("expected error, but nothing was returned")
40 | }
41 |
42 | expected := "client: missing url"
43 | if !strings.Contains(err.Error(), expected) {
44 | t.Fatalf("expected %q to contain %q", err.Error(), expected)
45 | }
46 | }
47 |
48 | func TestNewClient_parsesURL(t *testing.T) {
49 | client, err := NewClient("https://example.com/foo/bar")
50 | if err != nil {
51 | t.Fatal(err)
52 | }
53 |
54 | expected := &url.URL{
55 | Scheme: "https",
56 | Host: "example.com",
57 | Path: "/foo/bar",
58 | }
59 | if !reflect.DeepEqual(client.URL, expected) {
60 | t.Fatalf("expected %q to equal %q", client.URL, expected)
61 | }
62 | }
63 |
64 | func TestNewClient_TLSVerify(t *testing.T) {
65 | client, err := NewClient("https://example.com/foo/bar")
66 |
67 | if err != nil {
68 | t.Fatal(err)
69 | }
70 | if client.HTTPClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify != false {
71 | t.Fatal("Expected InsecureSkipVerify to be false")
72 | }
73 | }
74 |
75 | func TestNewClient_TLSNoVerify(t *testing.T) {
76 | os.Setenv("ATLAS_TLS_NOVERIFY", "1")
77 | client, err := NewClient("https://example.com/foo/bar")
78 |
79 | if err != nil {
80 | t.Fatal(err)
81 | }
82 | if client.HTTPClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify != true {
83 | t.Fatal("Expected InsecureSkipVerify to be true when ATLAS_TLS_NOVERIFY is set")
84 | }
85 | os.Setenv("ATLAS_TLS_NOVERIFY", "")
86 | }
87 |
88 | func TestNewClient_setsDefaultHTTPClient(t *testing.T) {
89 | _, err := NewClient("https://example.com/foo/bar")
90 | if err != nil {
91 | t.Fatal(err)
92 | }
93 | }
94 |
95 | func TestLogin_missingUsername(t *testing.T) {
96 | client, err := NewClient("https://example.com/foo/bar")
97 | if err != nil {
98 | t.Fatal(err)
99 | }
100 |
101 | _, err = client.Login("", "")
102 | if err == nil {
103 | t.Fatal("expected error, but nothing was returned")
104 | }
105 |
106 | expected := "client: missing username"
107 | if !strings.Contains(err.Error(), expected) {
108 | t.Fatalf("expected %q to contain %q", err.Error(), expected)
109 | }
110 | }
111 |
112 | func TestLogin_missingPassword(t *testing.T) {
113 | client, err := NewClient("https://example.com/foo/bar")
114 | if err != nil {
115 | t.Fatal(err)
116 | }
117 |
118 | _, err = client.Login("username", "")
119 | if err == nil {
120 | t.Fatal("expected error, but nothing was returned")
121 | }
122 |
123 | expected := "client: missing password"
124 | if !strings.Contains(err.Error(), expected) {
125 | t.Fatalf("expected %q to contain %q", err.Error(), expected)
126 | }
127 | }
128 |
129 | func TestLogin_serverErrorMessage(t *testing.T) {
130 | server := newTestAtlasServer(t)
131 | defer server.Stop()
132 |
133 | client, err := NewClient(server.URL.String())
134 | if err != nil {
135 | t.Fatal(err)
136 | }
137 |
138 | _, err = client.Login("username", "password")
139 | if err == nil {
140 | t.Fatal("expected error, but nothing was returned")
141 | }
142 |
143 | if err != ErrAuth {
144 | t.Fatalf("bad: %s", err)
145 | }
146 | }
147 |
148 | func TestLogin_success(t *testing.T) {
149 | server := newTestAtlasServer(t)
150 | defer server.Stop()
151 |
152 | client, err := NewClient(server.URL.String())
153 | if err != nil {
154 | t.Fatal(err)
155 | }
156 |
157 | token, err := client.Login("sethloves", "bacon")
158 | if err != nil {
159 | t.Fatal(err)
160 | }
161 |
162 | if client.Token == "" {
163 | t.Fatal("expected client token to be set")
164 | }
165 |
166 | if token == "" {
167 | t.Fatal("expected token to be returned")
168 | }
169 | }
170 |
171 | func TestRequest_tokenAuth(t *testing.T) {
172 | server := newTestAtlasServer(t)
173 | defer server.Stop()
174 |
175 | client, err := NewClient(server.URL.String())
176 | if err != nil {
177 | t.Fatal(err)
178 | }
179 | client.Token = "a.atlasv1.b"
180 |
181 | request, err := client.Request("GET", "/api/v1/token", nil)
182 | if err != nil {
183 | t.Fatal(err)
184 | }
185 |
186 | _, err = checkResp(client.HTTPClient.Do(request))
187 | if err != nil {
188 | t.Fatal(err)
189 | }
190 | }
191 |
192 | func TestRequest_getsData(t *testing.T) {
193 | server := newTestAtlasServer(t)
194 | defer server.Stop()
195 |
196 | client, err := NewClient(server.URL.String())
197 | if err != nil {
198 | t.Fatal(err)
199 | }
200 |
201 | request, err := client.Request("GET", "/_status/200", nil)
202 | if err != nil {
203 | t.Fatal(err)
204 | }
205 |
206 | if _, err := checkResp(client.HTTPClient.Do(request)); err != nil {
207 | t.Fatal(err)
208 | }
209 | }
210 |
211 | func TestRequest_railsError(t *testing.T) {
212 | server := newTestAtlasServer(t)
213 | defer server.Stop()
214 |
215 | client, err := NewClient(server.URL.String())
216 | if err != nil {
217 | t.Fatal(err)
218 | }
219 |
220 | request, err := client.Request("GET", "/_rails-error", nil)
221 | if err != nil {
222 | t.Fatal(err)
223 | }
224 |
225 | _, err = checkResp(client.HTTPClient.Do(request))
226 | if err == nil {
227 | t.Fatal("expected error, but nothing was returned")
228 | }
229 |
230 | expected := &RailsError{
231 | Errors: []string{
232 | "this is an error",
233 | "this is another error",
234 | },
235 | }
236 |
237 | if !reflect.DeepEqual(err, expected) {
238 | t.Fatalf("expected %+v to be %+v", err, expected)
239 | }
240 | }
241 |
242 | func TestRequest_notFoundError(t *testing.T) {
243 | server := newTestAtlasServer(t)
244 | defer server.Stop()
245 |
246 | client, err := NewClient(server.URL.String())
247 | if err != nil {
248 | t.Fatal(err)
249 | }
250 |
251 | request, err := client.Request("GET", "/_status/404", nil)
252 | if err != nil {
253 | t.Fatal(err)
254 | }
255 |
256 | _, err = checkResp(client.HTTPClient.Do(request))
257 | if err == nil {
258 | t.Fatal("expected error, but nothing was returned")
259 | }
260 |
261 | if err != ErrNotFound {
262 | t.Fatalf("bad error: %#v", err)
263 | }
264 | }
265 |
266 | func TestRequestJSON_decodesData(t *testing.T) {
267 | server := newTestAtlasServer(t)
268 | defer server.Stop()
269 |
270 | client, err := NewClient(server.URL.String())
271 | if err != nil {
272 | t.Fatal(err)
273 | }
274 |
275 | request, err := client.Request("GET", "/_json", nil)
276 | if err != nil {
277 | t.Fatal(err)
278 | }
279 |
280 | response, err := checkResp(client.HTTPClient.Do(request))
281 | if err != nil {
282 | t.Fatal(err)
283 | }
284 |
285 | var decoded struct{ Ok bool }
286 | if err := decodeJSON(response, &decoded); err != nil {
287 | t.Fatal(err)
288 | }
289 |
290 | if !decoded.Ok {
291 | t.Fatal("expected decoded response to be Ok, but was not")
292 | }
293 | }
294 |
295 | // check that our DefaultHeader works correctly, along with it providing
296 | // User-Agent
297 | func TestClient_defaultHeaders(t *testing.T) {
298 | server := newTestAtlasServer(t)
299 | defer server.Stop()
300 |
301 | client, err := NewClient(server.URL.String())
302 | if err != nil {
303 | t.Fatal(err)
304 | }
305 |
306 | testHeader := "Atlas-Test"
307 | testHeaderVal := "default header test"
308 | client.DefaultHeader.Set(testHeader, testHeaderVal)
309 |
310 | request, err := client.Request("GET", "/_test", nil)
311 | if err != nil {
312 | t.Fatal(err)
313 | }
314 |
315 | response, err := checkResp(client.HTTPClient.Do(request))
316 | if err != nil {
317 | t.Fatal(err)
318 | }
319 |
320 | decoded := &clientTestResp{}
321 | if err := decodeJSON(response, &decoded); err != nil {
322 | t.Fatal(err)
323 | }
324 |
325 | // Make sure User-Agent is set correctly
326 | if decoded.Header.Get("User-Agent") != userAgent {
327 | t.Fatal("User-Agent reported as", decoded.Header.Get("User-Agent"))
328 | }
329 |
330 | // look for our test header
331 | if decoded.Header.Get(testHeader) != testHeaderVal {
332 | t.Fatalf("DefaultHeader %q reported as %q", testHeader, testHeaderVal)
333 | }
334 | }
335 |
--------------------------------------------------------------------------------
/v1/terraform.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "log"
9 | )
10 |
11 | // TerraformConfigVersion represents a single uploaded version of a
12 | // Terraform configuration.
13 | type TerraformConfigVersion struct {
14 | Version int
15 | Remotes []string `json:"remotes"`
16 | Metadata map[string]string `json:"metadata"`
17 | Variables map[string]string `json:"variables,omitempty"`
18 | TFVars []TFVar `json:"tf_vars"`
19 | }
20 |
21 | // TFVar is used to serialize a single Terraform variable sent by the
22 | // manager as a collection of Variables in a Job payload.
23 | type TFVar struct {
24 | Key string `json:"key"`
25 | Value string `json:"value"`
26 | IsHCL bool `json:"hcl"`
27 | }
28 |
29 | // TerraformConfigLatest returns the latest Terraform configuration version.
30 | func (c *Client) TerraformConfigLatest(user, name string) (*TerraformConfigVersion, error) {
31 | log.Printf("[INFO] getting terraform configuration %s/%s", user, name)
32 |
33 | endpoint := fmt.Sprintf("/api/v1/terraform/configurations/%s/%s/versions/latest", user, name)
34 | request, err := c.Request("GET", endpoint, nil)
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | response, err := checkResp(c.HTTPClient.Do(request))
40 | if err == ErrNotFound {
41 | return nil, nil
42 | }
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | var wrapper tfConfigVersionWrapper
48 | if err := decodeJSON(response, &wrapper); err != nil {
49 | return nil, err
50 | }
51 |
52 | return wrapper.Version, nil
53 | }
54 |
55 | // CreateTerraformConfigVersion creatse a new Terraform configuration
56 | // versions and uploads a slug with it.
57 | func (c *Client) CreateTerraformConfigVersion(
58 | user string, name string,
59 | version *TerraformConfigVersion,
60 | data io.Reader, size int64) (int, error) {
61 | log.Printf("[INFO] creating terraform configuration %s/%s", user, name)
62 |
63 | endpoint := fmt.Sprintf(
64 | "/api/v1/terraform/configurations/%s/%s/versions", user, name)
65 | body, err := json.Marshal(&tfConfigVersionWrapper{
66 | Version: version,
67 | })
68 | if err != nil {
69 | return 0, err
70 | }
71 |
72 | request, err := c.Request("POST", endpoint, &RequestOptions{
73 | Body: bytes.NewReader(body),
74 | Headers: map[string]string{
75 | "Content-Type": "application/json",
76 | },
77 | })
78 | if err != nil {
79 | return 0, err
80 | }
81 |
82 | response, err := checkResp(c.HTTPClient.Do(request))
83 | if err != nil {
84 | return 0, err
85 | }
86 |
87 | var result tfConfigVersionCreate
88 | if err := decodeJSON(response, &result); err != nil {
89 | return 0, err
90 | }
91 |
92 | if err := c.putFile(result.UploadPath, data, size); err != nil {
93 | return 0, err
94 | }
95 |
96 | return result.Version, nil
97 | }
98 |
99 | type tfConfigVersionCreate struct {
100 | UploadPath string `json:"upload_path"`
101 | Version int
102 | }
103 |
104 | type tfConfigVersionWrapper struct {
105 | Version *TerraformConfigVersion `json:"version"`
106 | }
107 |
--------------------------------------------------------------------------------
/v1/terraform_test.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "bytes"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | func TestTerraformConfigLatest(t *testing.T) {
10 | server := newTestAtlasServer(t)
11 | defer server.Stop()
12 |
13 | client, err := NewClient(server.URL.String())
14 | if err != nil {
15 | t.Fatal(err)
16 | }
17 |
18 | actual, err := client.TerraformConfigLatest("hashicorp", "existing")
19 | if err != nil {
20 | t.Fatal(err)
21 | }
22 |
23 | expected := &TerraformConfigVersion{
24 | Version: 5,
25 | Metadata: map[string]string{"foo": "bar"},
26 | Variables: map[string]string{"foo": "bar"},
27 | }
28 |
29 | if !reflect.DeepEqual(actual, expected) {
30 | t.Fatalf("%#v", actual)
31 | }
32 | }
33 |
34 | func TestCreateTerraformConfigVersion(t *testing.T) {
35 | server := newTestAtlasServer(t)
36 | defer server.Stop()
37 |
38 | client, err := NewClient(server.URL.String())
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 |
43 | v := &TerraformConfigVersion{
44 | Version: 5,
45 | Metadata: map[string]string{"foo": "bar"},
46 | }
47 |
48 | data := new(bytes.Buffer)
49 | vsn, err := client.CreateTerraformConfigVersion(
50 | "hashicorp", "existing", v, data, int64(data.Len()))
51 | if err != nil {
52 | t.Fatal(err)
53 | }
54 | if vsn != 5 {
55 | t.Fatalf("bad: %v", vsn)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/v1/util.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | // ParseSlug parses a slug of the format (x/y) into the x and y components. It
9 | // accepts a string of the format "x/y" ("user/name" for example). If an empty
10 | // string is given, an error is returned. If the given string is not a valid
11 | // slug format, an error is returned.
12 | func ParseSlug(slug string) (string, string, error) {
13 | if slug == "" {
14 | return "", "", fmt.Errorf("missing slug")
15 | }
16 |
17 | parts := strings.Split(slug, "/")
18 | if len(parts) != 2 {
19 | return "", "", fmt.Errorf("malformed slug %q", slug)
20 | }
21 | return parts[0], parts[1], nil
22 | }
23 |
--------------------------------------------------------------------------------
/v1/util_test.go:
--------------------------------------------------------------------------------
1 | package atlas
2 |
3 | import (
4 | "strings"
5 | "testing"
6 | )
7 |
8 | func TestParseSlug_emptyString(t *testing.T) {
9 | _, _, err := ParseSlug("")
10 | if err == nil {
11 | t.Fatal("expected error, but nothing was returned")
12 | }
13 |
14 | expected := "missing slug"
15 | if !strings.Contains(err.Error(), expected) {
16 | t.Fatalf("expected %q to contain %q", err.Error(), expected)
17 | }
18 | }
19 |
20 | func TestParseSlug_noSlashes(t *testing.T) {
21 | _, _, err := ParseSlug("bacon")
22 | if err == nil {
23 | t.Fatal("expected error, but nothing was returned")
24 | }
25 |
26 | expected := "malformed slug"
27 | if !strings.Contains(err.Error(), expected) {
28 | t.Fatalf("expected %q to contain %q", err.Error(), expected)
29 | }
30 | }
31 |
32 | func TestParseSlug_multipleSlashes(t *testing.T) {
33 | _, _, err := ParseSlug("bacon/is/delicious/but/this/is/not/valid")
34 | if err == nil {
35 | t.Fatal("expected error, but nothing was returned")
36 | }
37 |
38 | expected := "malformed slug"
39 | if !strings.Contains(err.Error(), expected) {
40 | t.Fatalf("expected %q to contain %q", err.Error(), expected)
41 | }
42 | }
43 |
44 | func TestParseSlug_goodString(t *testing.T) {
45 | user, name, err := ParseSlug("hashicorp/project")
46 | if err != nil {
47 | t.Fatal(err)
48 | }
49 |
50 | if user != "hashicorp" {
51 | t.Fatalf("expected %q to be %q", user, "hashicorp")
52 | }
53 |
54 | if name != "project" {
55 | t.Fatalf("expected %q to be %q", name, "project")
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
]