├── .github
├── CODE_OF_CONDUCT.md
├── ISSUE_TEMPLATE.md
├── dependabot.yaml
├── dependabot.yml
└── workflows
│ ├── codecov.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .goreleaser.yml
├── LICENSE
├── Makefile
├── README.md
├── docs
├── data-sources
│ ├── team.md
│ └── user.md
├── index.md
└── resources
│ ├── dns.md
│ ├── domain.md
│ ├── env.md
│ ├── project.md
│ └── secret.md
├── examples
├── README.md
├── data-sources
│ └── data_user
│ │ └── data-source.tf
├── e2e
│ └── main.tf
├── provider
│ └── provider.tf
└── resources
│ ├── vercel_dns
│ └── resource.tf
│ ├── vercel_domain
│ └── resource.tf
│ ├── vercel_env
│ └── resource.tf
│ ├── vercel_project
│ └── resource.tf
│ └── vercel_secret
│ └── resource.tf
├── go.mod
├── go.sum
├── internal
└── provider
│ ├── data_source_team.go
│ ├── data_source_user.go
│ ├── data_source_user_test.go
│ ├── provider.go
│ ├── provider_test.go
│ ├── resource_dns.go
│ ├── resource_domain.go
│ ├── resource_domain_test.go
│ ├── resource_env.go
│ ├── resource_env_test.go
│ ├── resource_project.go
│ ├── resource_project_test.go
│ ├── resource_secret.go
│ └── resource_secret_test.go
├── main.go
├── pkg
├── util
│ ├── env.go
│ ├── util.go
│ └── util_test.go
└── vercel
│ ├── alias
│ └── alias.go
│ ├── client.go
│ ├── dns
│ └── dns.go
│ ├── domain
│ └── domain.go
│ ├── env
│ └── env.go
│ ├── httpApi
│ └── httpApi.go
│ ├── project
│ ├── project.go
│ └── types.go
│ ├── secret
│ └── secret.go
│ ├── team
│ └── team.go
│ └── user
│ └── user.go
└── tools
└── tools.go
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code.
4 |
5 | Please read the full text at https://www.hashicorp.com/community-guidelines
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Hi there,
2 |
3 | Thank you for opening an issue!
4 |
5 | ### Terraform Version
6 | Run `terraform -v` to show the version. We support terraform v0.13+
7 |
8 | ### Affected Resource(s)
9 | Please list the resources as a list, for example:
10 | - vercel_project
11 | - vercel_domain
12 |
13 | If this issue appears to affect multiple resources, it may be an issue with Terraform's core, so please mention this.
14 |
15 | ### Terraform Configuration Files
16 | ```hcl
17 | # Copy-paste your Terraform configurations here - for large Terraform configs,
18 | # please use a service like Dropbox and share a link to the ZIP file. For
19 | # security, you can also encrypt the files using our GPG public key.
20 | ```
21 |
22 | ### Debug Output
23 | Please provider a link to a GitHub Gist containing the complete debug output: https://www.terraform.io/docs/internals/debugging.html. Please do NOT paste the debug output in the issue; just paste a link to the Gist.
24 |
25 | ### Panic Output
26 | If Terraform produced a panic, please provide a link to a GitHub Gist containing the output of the `crash.log`.
27 |
28 | ### Expected Behavior
29 | What should have happened?
30 |
31 | ### Actual Behavior
32 | What actually happened?
33 |
34 | ### Steps to Reproduce
35 | Please list the steps required to reproduce the issue, for example:
36 | 1. `terraform apply`
37 |
38 | ### Important Factoids
39 | Are there anything atypical about your accounts that we should know? For example: Running in EC2 Classic? Custom version of OpenStack? Tight ACLs?
40 |
41 | ### References
42 | Are there any other GitHub issues (open or closed) or Pull Requests that should be linked here? For example:
43 | - GH-1234
44 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gomod"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | assignees:
8 | - chronark
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # See GitHub's docs for more information on this file:
2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates
3 | version: 2
4 | updates:
5 | # Maintain dependencies for GitHub Actions
6 | - package-ecosystem: "github-actions"
7 | directory: "/"
8 | schedule:
9 | # Check for updates to GitHub Actions every weekday
10 | interval: "daily"
11 |
12 | # Maintain dependencies for Go modules
13 | - package-ecosystem: "gomod"
14 | directory: "/"
15 | schedule:
16 | # Check for updates to Go modules every weekday
17 | interval: "daily"
18 |
--------------------------------------------------------------------------------
/.github/workflows/codecov.yml:
--------------------------------------------------------------------------------
1 | name: Report coverage
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 |
7 |
8 | jobs:
9 | collect-coverage:
10 | runs-on: ubuntu-latest
11 | steps:
12 |
13 | - name: Set up Go
14 | uses: actions/setup-go@v2
15 | with:
16 | go-version: 1.16
17 |
18 | - name: Check out code into the Go module directory
19 | uses: actions/checkout@v2
20 |
21 | - name: Get dependencies
22 | run: go mod download
23 |
24 | - name: Test
25 | run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
26 | env:
27 | TF_ACC: "1"
28 | VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
29 |
30 |
31 | - name: Run codecov coverage reporter
32 | run: bash <(curl -s https://codecov.io/bash)
33 | env:
34 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
35 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 | jobs:
7 | goreleaser:
8 | runs-on: ubuntu-latest
9 | steps:
10 | -
11 | name: Checkout
12 | uses: actions/checkout@v2
13 | -
14 | name: Unshallow
15 | run: git fetch --prune --unshallow
16 | -
17 | name: Set up Go
18 | uses: actions/setup-go@v2
19 | with:
20 | go-version: 1.16
21 | -
22 | name: Import GPG key
23 | id: import_gpg
24 | uses: paultyng/ghaction-import-gpg@v2.1.0
25 | env:
26 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
27 | PASSPHRASE: ${{ secrets.PASSPHRASE }}
28 | -
29 | name: Run GoReleaser
30 | uses: goreleaser/goreleaser-action@v2
31 | with:
32 | version: latest
33 | args: release --rm-dist
34 | env:
35 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This GitHub action runs your tests for each commit push and/or PR. Optionally
2 | # you can turn it on using a cron schedule for regular testing.
3 | #
4 | name: Tests
5 | on:
6 | pull_request:
7 | # For systems with an upstream API that could drift unexpectedly (like most SaaS systems, etc.),
8 | # we recommend testing at a regular interval not necessarily tied to code changes. This will
9 | # ensure you are alerted to something breaking due to an API change, even if the code did not
10 | # change.
11 | schedule:
12 | - cron: "0 23 * * *"
13 | jobs:
14 | # ensure the code builds...
15 | build:
16 | name: Build
17 | runs-on: ubuntu-latest
18 | timeout-minutes: 5
19 | steps:
20 | - name: Set up Go
21 | uses: actions/setup-go@v2.1.3
22 | with:
23 | go-version: "1.16"
24 | id: go
25 |
26 | - name: Check out code into the Go module directory
27 | uses: actions/checkout@v2.3.3
28 |
29 | - name: Get dependencies
30 | run: |
31 | go mod download
32 |
33 | - name: Build
34 | run: |
35 | go build -v .
36 |
37 | lint:
38 | name: Lint
39 | runs-on: ubuntu-latest
40 | timeout-minutes: 5
41 | steps:
42 | - name: Set up Go
43 | uses: actions/setup-go@v2.1.3
44 | with:
45 | go-version: "1.16"
46 |
47 | - name: Check out code into the Go module directory
48 | uses: actions/checkout@v2.3.3
49 |
50 | - name: golangci-lint
51 | uses: golangci/golangci-lint-action@v2
52 | with:
53 | version: latest
54 |
55 | # run acceptance tests in a matrix with Terraform core versions
56 | test:
57 | name: Acceptance test
58 | needs:
59 | - build
60 | - lint
61 | runs-on: ubuntu-latest
62 | timeout-minutes: 15
63 | strategy:
64 | max-parallel: 1
65 | fail-fast: false
66 | matrix:
67 | terraform: ["1.1.4"]
68 | steps:
69 | - name: Set up Go
70 | uses: actions/setup-go@v2.1.3
71 | with:
72 | go-version: 1.16
73 |
74 | - name: Check out code into the Go module directory
75 | uses: actions/checkout@v2.3.3
76 |
77 | - name: Get dependencies
78 | run: go mod download
79 |
80 | - name: Set up terraform
81 | uses: hashicorp/setup-terraform@v1
82 | with:
83 | terraform_version: ${{ matrix.terraform }}
84 | terraform_wrapper: false
85 |
86 | - name: TF acceptance tests
87 | timeout-minutes: 10
88 | env:
89 | TF_ACC: "1"
90 | VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
91 |
92 | run: go test -v ./...
93 |
94 | # Get a single check for the branch protection rules.
95 | tests_ok:
96 | name: Tests OK
97 | needs: test
98 | runs-on: ubuntu-latest
99 | steps:
100 | - run: echo Success!
101 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.dll
2 | *.exe
3 | .DS_Store
4 | example.tf
5 | terraform.tfplan
6 | terraform.tfstate
7 | bin/
8 | dist/
9 | modules-dev/
10 | website/.vagrant
11 | website/.bundle
12 | website/build
13 | website/node_modules
14 | .vagrant/
15 | *.backup
16 | ./*.tfstate
17 | .terraform/
18 | *.log
19 | *.bak
20 | *~
21 | .*.swp
22 | .idea
23 | *.iml
24 | *.test
25 | *.iml
26 |
27 | website/vendor
28 |
29 | # Test exclusions
30 | !command/test-fixtures/**/*.tfstate
31 | !command/test-fixtures/**/.terraform/
32 |
33 | # Keep windows files with windows line endings
34 | *.winfile eol=crlf
35 | .terraform.lock.hcl
36 | terraform.tfstate*
37 | vendor/
38 | .vscode/
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # Visit https://goreleaser.com for documentation on how to customize this
2 | # behavior.
3 | before:
4 | hooks:
5 | # this is just an example and not a requirement for provider building/publishing
6 | - go mod tidy
7 | - go generate
8 | builds:
9 | - env:
10 | # goreleaser does not work with CGO, it could also complicate
11 | # usage by users in CI/CD systems like Terraform Cloud where
12 | # they are unable to install libraries.
13 | - CGO_ENABLED=0
14 | mod_timestamp: '{{ .CommitTimestamp }}'
15 | flags:
16 | - -trimpath
17 | ldflags:
18 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}'
19 | goos:
20 | - freebsd
21 | - windows
22 | - linux
23 | - darwin
24 | goarch:
25 | - amd64
26 | - '386'
27 | - arm
28 | - arm64
29 | binary: '{{ .ProjectName }}_v{{ .Version }}'
30 | archives:
31 | - format: zip
32 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
33 | checksum:
34 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
35 | algorithm: sha256
36 | signs:
37 | - artifacts: checksum
38 | args:
39 | # if you are using this in a GitHub action or some other automated pipeline, you
40 | # need to pass the batch flag to indicate its not interactive.
41 | - "--batch"
42 | - "--local-user"
43 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key
44 | - "--output"
45 | - "${signature}"
46 | - "--detach-sign"
47 | - "${artifact}"
48 | release:
49 | # If you want to manually examine the release before its live, uncomment this line:
50 | # draft: true
51 | changelog:
52 | skip: false
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | 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,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | default: testacc
2 |
3 | # Run acceptance tests
4 | .PHONY: testacc
5 | testacc:
6 | TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 2m -race -covermode=atomic
7 |
8 | test:
9 | go test ./... -v
10 |
11 | build: fmt
12 | go build -o terraform-provider-vercel
13 | mkdir -p ~/.terraform.d/plugins/hashicorp.com/chronark/vercel/9000.1/linux_amd64
14 | mv terraform-provider-vercel ~/.terraform.d/plugins/hashicorp.com/chronark/vercel/9000.1/linux_amd64
15 |
16 |
17 | fmt:
18 |
19 | go generate -v ./...
20 | golangci-lint run -v
21 | go fmt ./...
22 | terraform fmt -recursive .
23 |
24 | rm-state:
25 | rm -rf examples/e2e/terraform.tfstate*
26 |
27 |
28 | init: build
29 | rm -rf examples/e2e/.terraform*
30 | terraform -chdir=examples/e2e init -upgrade
31 | e2e: init
32 | terraform -chdir=examples/e2e apply
33 |
34 |
35 | release:
36 | @go get github.com/caarlos0/svu
37 | @echo "Releasing $$(svu next)..."
38 |
39 | @git tag $$(svu next) && git push --tags
40 | @echo "Done"
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | # DEPRECATED, please use the [official provider by vercel](https://github.com/vercel/terraform-provider-vercel)
9 |
10 |
11 | # Terraform Provider for Vercel
12 |
13 | Configure vercel resources such as projects, deployments and secrets as code with terraform.
14 |
15 |
18 |
19 |
20 |
21 | ## Features
22 |
23 | This provider has not reached feauture parity with the vercel api yet. I am adding new features as I need them.
24 | Please create an issue if you requrie a certain feature, I will work on them asap.
25 |
26 | Available features can be found [here](https://registry.terraform.io/providers/chronark/vercel/latest/docs).
27 |
28 | ## Quickstart
29 |
30 | 1. Create a token [here](https://vercel.com/account/tokens)
31 | 2. Create a `vercel.tf` file with the following content.
32 | - Replace `` with the token from step 1. Alternatively you can set the `VERCEL_TOKEN` environment variable
33 | - Change the `git_repository` to whatever you want to deploy.
34 |
35 | ```tf
36 | terraform {
37 | required_providers {
38 | vercel = {
39 | source = "registry.terraform.io/chronark/vercel"
40 | version = ">=0.10.3"
41 | }
42 | }
43 | }
44 |
45 | provider "vercel" {
46 | token = ""
47 | }
48 |
49 | resource "vercel_project" "my_project" {
50 | name = "mercury-via-terraform"
51 | git_repository {
52 | type = "github"
53 | repo = "chronark/terraform-provider-vercel"
54 | }
55 | }
56 | ```
57 |
58 | 3. Run
59 | ```sh
60 | terraform init
61 | terraform apply
62 | ```
63 |
64 |
65 | 4. Check vercel's [dashboard](https://vercel.com/dashboard) to see your project.
66 | 5. Push to the default branch of your repository to create your first deployment.
67 |
68 | ## Documentation
69 |
70 | Documentation can be found [here](https://registry.terraform.io/providers/chronark/vercel/latest/docs)
71 |
72 | ## Development Requirements
73 |
74 | - [Terraform](https://www.terraform.io/downloads.html) >= 0.13.x
75 | - [Go](https://golang.org/doc/install) >= 1.15
76 |
--------------------------------------------------------------------------------
/docs/data-sources/team.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "vercel_team Data Source - terraform-provider-vercel"
4 | subcategory: ""
5 | description: |-
6 | Retrieves information related to an existing team. https://vercel.com/docs/api#endpoints/teams
7 | ---
8 |
9 | # vercel_team (Data Source)
10 |
11 | Retrieves information related to an existing team. https://vercel.com/docs/api#endpoints/teams
12 |
13 |
14 |
15 |
16 | ## Schema
17 |
18 | ### Required
19 |
20 | - **slug** (String)
21 |
22 | ### Read-Only
23 |
24 | - **avatar** (String)
25 | - **created** (Number)
26 | - **creator_id** (String)
27 | - **id** (String) The ID of this resource.
28 | - **name** (String)
29 |
30 |
31 |
--------------------------------------------------------------------------------
/docs/data-sources/user.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "vercel_user Data Source - terraform-provider-vercel"
4 | subcategory: ""
5 | description: |-
6 | Retrieves information related to the authenticated user. https://vercel.com/docs/api#endpoints/user/get-the-authenticated-user
7 | ---
8 |
9 | # vercel_user (Data Source)
10 |
11 | Retrieves information related to the authenticated user. https://vercel.com/docs/api#endpoints/user/get-the-authenticated-user
12 |
13 |
14 |
15 |
16 | ## Schema
17 |
18 | ### Read-Only
19 |
20 | - **avatar** (String)
21 | - **bio** (String)
22 | - **email** (String)
23 | - **id** (String) The ID of this resource.
24 | - **name** (String)
25 | - **platformversion** (Number)
26 | - **profiles** (List of Object) (see [below for nested schema](#nestedatt--profiles))
27 | - **username** (String)
28 | - **website** (String)
29 |
30 |
31 | ### Nested Schema for `profiles`
32 |
33 | Read-Only:
34 |
35 | - **link** (String)
36 | - **service** (String)
37 |
38 |
39 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "vercel Provider"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # vercel Provider
10 |
11 |
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | terraform {
17 | required_providers {
18 | vercel = {
19 | source = "hashicorp.com/chronark/vercel"
20 | version = "9000.1"
21 | }
22 | }
23 | }
24 |
25 | provider "vercel" {
26 | token = ""
27 | }
28 | ```
29 |
30 |
31 | ## Schema
32 |
33 | ### Optional
34 |
35 | - **token** (String, Sensitive)
36 |
--------------------------------------------------------------------------------
/docs/resources/dns.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "vercel_dns Resource - terraform-provider-vercel"
4 | subcategory: ""
5 | description: |-
6 | https://vercel.com/docs/api#endpoints/dns
7 | Currently this can only fetch the last 1000 records. Please create an issue if you require more.
8 | ---
9 |
10 | # vercel_dns (Resource)
11 |
12 | https://vercel.com/docs/api#endpoints/dns
13 | Currently this can only fetch the last 1000 records. Please create an issue if you require more.
14 |
15 | ## Example Usage
16 |
17 | ```terraform
18 | resource "vercel_domain" "chronark_com" {
19 | name = "chronark.com"
20 | }
21 |
22 |
23 | resource "vercel_dns" "www" {
24 | domain = vercel_domain.chronark_com.name
25 | type = "CNAME"
26 | value = "www.${vercel_domain.chronark_com.name}"
27 | name = "www"
28 | }
29 | ```
30 |
31 |
32 | ## Schema
33 |
34 | ### Required
35 |
36 | - **domain** (String) The domain for this DNS record
37 | - **name** (String) A subdomain name or an empty string for the root domain.
38 | - **type** (String) The type of record, it could be any valid DNS record.
39 | - **value** (String) The record value.
40 |
41 | ### Optional
42 |
43 | - **team_id** (String) By default, you can access resources contained within your own user account. To access resources owned by a team, you can pass in the team ID
44 | - **ttl** (Number) The TTL value. Must be a number between 60 and 2147483647. Default value is 60.
45 |
46 | ### Read-Only
47 |
48 | - **created** (Number) The date when the record was created.
49 | - **created_at** (Number) The date when the record was created in milliseconds since the UNIX epoch.
50 | - **creator** (String) The ID of the user who created the record or system if the record is an automatic record.
51 | - **id** (String) The unique identifier of the dns record.
52 | - **updated** (Number) The date when the record was updated.
53 | - **updated_at** (Number) The date when the record was updated in milliseconds since the UNIX epoch.
54 |
55 |
56 |
--------------------------------------------------------------------------------
/docs/resources/domain.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "vercel_domain Resource - terraform-provider-vercel"
4 | subcategory: ""
5 | description: |-
6 | Creates account wide domains to be used in your projects.
7 | ---
8 |
9 | # vercel_domain (Resource)
10 |
11 | Creates account wide domains to be used in your projects.
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | resource "vercel_domain" "google-com" {
17 | name = "google.com"
18 | }
19 | ```
20 |
21 |
22 | ## Schema
23 |
24 | ### Required
25 |
26 | - **name** (String) The name of the production domain.
27 |
28 | ### Optional
29 |
30 | - **team_id** (String) By default, you can access resources contained within your own user account. To access resources owned by a team, you can pass in the team ID
31 |
32 | ### Read-Only
33 |
34 | - **bought_at** (Number) If it was purchased through Vercel, the date when it was purchased.
35 | - **cdn_enabled** (Boolean) Whether the domain has the Vercel Edge Network enabled or not.
36 | - **created_at** (Number) The date when the domain was created in the registry.
37 | - **expires_at** (Number) The date at which the domain is set to expire. null if not bought with Vercel.
38 | - **id** (String) Unique id for this variable.
39 | - **intended_nameservers** (List of String) A list of the intended nameservers for the domain to point to Vercel DNS.
40 | - **nameservers** (List of String) A list of the current nameservers of the domain.
41 | - **ns_verified_at** (Number) The date at which the domain's nameservers were verified based on the intended set.
42 | - **service_type** (String) The type of service the domain is handled by. external if the DNS is externally handled, or zeit.world if handled with Vercel.
43 | - **transfer_started_at** (Number) If transferred into Vercel, The date when the domain transfer was initiated
44 | - **transferred_at** (Number) The date at which the domain was successfully transferred into Vercel. null if the transfer is still processing or was never transferred in.
45 | - **txt_verified_at** (Number) The date at which the domain's TXT DNS record was verified.
46 | - **verification_record** (String) The ID of the verification record in the registry.
47 | - **verified** (Boolean) If the domain has the ownership verified.
48 |
49 |
50 | ## Importing
51 |
52 | Domains can be imported from Vercel using the domain name itself, e.g:
53 |
54 | ```
55 | $ terraform import vercel_domain.domain example.com
56 | ```
57 |
58 | For team domains, prefix the domain name with the team slug followed by a slash (`/`), e.g:
59 |
60 | ```
61 | $ terraform import vercel_domain.domain my-team/example.com
62 | ```
63 |
64 | The team slug can be found in the URL for any team screen in Vercel's web interface.
65 |
66 | Slashes in the team slug may be URL-escaped (percent-encoded) if needed.
67 |
68 |
--------------------------------------------------------------------------------
/docs/resources/env.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "vercel_env Resource - terraform-provider-vercel"
4 | subcategory: ""
5 | description: |-
6 | https://vercel.com/docs/api#endpoints/projects/get-project-environment-variables
7 | ---
8 |
9 | # vercel_env (Resource)
10 |
11 | https://vercel.com/docs/api#endpoints/projects/get-project-environment-variables
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | resource "vercel_project" "my_project" {
17 | // ...
18 | }
19 |
20 | resource "vercel_env" "my_env" {
21 | project_id = vercel_project.my_project.id // or use a hardcoded value of an existing project
22 | type = "plain"
23 | key = "hello"
24 | value = "world"
25 | target = ["production", "preview", "development"]
26 | }
27 | ```
28 |
29 |
30 | ## Schema
31 |
32 | ### Required
33 |
34 | - **key** (String) The name of the environment variable.
35 | - **project_id** (String) The unique project identifier.
36 | - **target** (List of String) The target can be a list of `development`, `preview`, or `production`.
37 | - **type** (String) The type can be `plain`, `secret`, or `system`.
38 | - **value** (String) If the type is `plain`, a string representing the value of the environment variable. If the type is `secret`, the secret ID of the secret attached to the environment variable. If the type is `system`, the name of the System Environment Variable.
39 |
40 | ### Optional
41 |
42 | - **team_id** (String) By default, you can access resources contained within your own user account. To access resources owned by a team, you can pass in the team ID
43 |
44 | ### Read-Only
45 |
46 | - **created_at** (Number) A number containing the date when the variable was created in milliseconds.
47 | - **id** (String) Unique id for this variable.
48 | - **updated_at** (Number) A number containing the date when the variable was updated in milliseconds.
49 |
50 | ## Importing
51 |
52 | Use the project and variable names separated by a slash (`/`), e.g:
53 |
54 | ```
55 | $ terraform import vercel_env.env my-project/THE_VARIABLE
56 | ```
57 |
58 | For team projects, include the team slug as a prefix, e.g:
59 |
60 | ```
61 | $ terraform import vercel_project.team_app my-team/my-project/THE_VARIABLE
62 | ```
63 |
64 | The team slug can be found in the URL for any team screen in Vercel's web interface.
65 |
66 | Slashes in the project name or team slug may be URL-escaped (percent-encoded) if needed.
67 |
68 |
--------------------------------------------------------------------------------
/docs/resources/project.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "vercel_project Resource - terraform-provider-vercel"
4 | subcategory: ""
5 | description: |-
6 | https://vercel.com/docs/api#endpoints/projects
7 | ---
8 |
9 | # vercel_project (Resource)
10 |
11 | https://vercel.com/docs/api#endpoints/projects
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | resource "vercel_project" "my_project" {
17 | name = "mercury"
18 | git_repository {
19 | type = "github"
20 | repo = "chronark/terraform-provider-vercel"
21 | }
22 | }
23 | ```
24 |
25 |
26 | ## Schema
27 |
28 | ### Required
29 |
30 | - **git_repository** (Block List, Min: 1, Max: 1) The git repository that will be connected to the project. Any pushes to the specified connected git repository will be automatically deployed. (see [below for nested schema](#nestedblock--git_repository))
31 | - **name** (String) The name of the project.
32 |
33 | ### Optional
34 |
35 | - **alias** (List of String) A list of production domains for the project.
36 | - **build_command** (String) The build command for this project. When null is used this value will be automatically detected.
37 | - **dev_command** (String) The dev command for this project. When null is used this value will be automatically detected.
38 | - **domain** (Block List) Add a domain to the project by passing the project. (see [below for nested schema](#nestedblock--domain))
39 | - **framework** (String) The framework that is being used for this project. When null is used no framework is selected.
40 | - **install_command** (String) The install command for this project. When null is used this value will be automatically detected.
41 | - **node_version** (String) The Node.js Version for this project.
42 | - **output_directory** (String) The output directory of the project. When null is used this value will be automatically detected.
43 | - **public_source** (Boolean) Specifies whether the source code and logs of the deployments for this project should be public or not.
44 | - **root_directory** (String) The name of a directory or relative path to the source code of your project. When null is used it will default to the project root.
45 | - **serverless_function_region** (String) The region to deploy Serverless Functions in this project.
46 | - **team_id** (String) By default, you can access resources contained within your own user account. To access resources owned by a team, you can pass in the team ID
47 |
48 | ### Read-Only
49 |
50 | - **account_id** (String) The unique ID of the user or team the project belongs to.
51 | - **created_at** (Number) A number containing the date when the project was created in milliseconds.
52 | - **id** (String) Internal id of this project
53 | - **updated_at** (Number) A number containing the date when the project was updated in milliseconds.
54 |
55 |
56 | ### Nested Schema for `git_repository`
57 |
58 | Required:
59 |
60 | - **repo** (String) The name of the git repository. For example: `chronark/terraform-provider-vercel`
61 | - **type** (String) The git provider of the repository. Must be either `github`, `gitlab`, or `bitbucket`.
62 |
63 |
64 | ### Nested Schema for `domain`
65 |
66 | Required:
67 |
68 | - **name** (String) The name of the production domain.
69 |
70 | Optional:
71 |
72 | - **git_branch** (String) it branch for the domain to be auto assigned to. The Project's production branch is the default (null).
73 | - **redirect** (String) Target destination domain for redirect.
74 | - **redirect_status_code** (Number) The redirect status code (301, 302, 307, 308).
75 |
76 |
77 | ## Importing
78 |
79 | Project resources can be imported using the project name, e.g:
80 |
81 | ```
82 | $ terraform import vercel_project.app my-project
83 | ```
84 |
85 | For team projects, prefix the project name with the team slug followed by a slash (`/`), e.g:
86 |
87 | ```
88 | $ terraform import vercel_project.team_app my-team/my-project
89 | ```
90 |
91 | The team slug can be found in the URL for any team screen in Vercel's web interface.
92 |
93 | Slashes in the project name or team slug may be URL-escaped (percent-encoded) if needed.
94 |
--------------------------------------------------------------------------------
/docs/resources/secret.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "vercel_secret Resource - terraform-provider-vercel"
4 | subcategory: ""
5 | description: |-
6 | https://vercel.com/docs/api#endpoints/secrets
7 | ---
8 |
9 | # vercel_secret (Resource)
10 |
11 | https://vercel.com/docs/api#endpoints/secrets
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | // Create a new secret
17 | resource "vercel_secret" "my_secret" {
18 | name = "my_secret_name" // `@` will be prefixed automatically
19 | value = "super secret"
20 | }
21 |
22 | // Use the secret
23 | resource "vercel_env" "env" {
24 | type = "secret"
25 | key = "Hello"
26 | value = vercel_secret.my_secret.id
27 |
28 | // irrelevant values omitted
29 | }
30 | ```
31 |
32 |
33 | ## Schema
34 |
35 | ### Required
36 |
37 | - **name** (String) The name of the secret.
38 | - **value** (String, Sensitive) The value of the new secret.
39 |
40 | ### Optional
41 |
42 | - **team_id** (String) By default, you can access resources contained within your own user account. To access resources owned by a team, you can pass in the team ID
43 |
44 | ### Read-Only
45 |
46 | - **created_at** (Number) A number containing the date when the variable was created in milliseconds.
47 | - **id** (String) The unique identifier of the secret.
48 | - **user_id** (String) The unique identifier of the user who created the secret.
49 |
50 | ## Importing
51 |
52 | Use the Vercel secret ID:
53 |
54 | ```
55 | $ terraform import vercel_secret.thesecret sec_CTjwzonA7MCjQnDDP0yQbDc8
56 | ```
57 |
58 | Please note that after secrets are written, Vercel does not allow reading the value.
59 | Subsequent operations may overwrite or replace the resource because of that.
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI.
4 |
5 | The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or ar testable even if some parts are not relevant for the documentation.
6 |
7 | * **provider/provider.tf** example file for the provider index page
8 | * **data-sources//data-source.tf** example file for the named data source page
9 | * **resources//resource.tf** example file for the named data source page
10 |
--------------------------------------------------------------------------------
/examples/data-sources/data_user/data-source.tf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | data "vercel_user" "user" {}
--------------------------------------------------------------------------------
/examples/e2e/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | vercel = {
4 | source = "hashicorp.com/chronark/vercel"
5 | version = "9000.1"
6 | }
7 | }
8 | }
9 |
10 | provider "vercel" {
11 | }
12 |
13 |
14 |
15 | resource "vercel_project" "my_project" {
16 | name = "test"
17 | git_repository {
18 | type = "github"
19 | repo = "chronark/terraform-provider-vercel"
20 | }
21 |
22 | domain {
23 | name = "one.chronark.com"
24 | }
25 | domain {
26 | name = "two.chronark.com"
27 | }
28 |
29 |
30 | }
31 |
32 |
33 | resource "vercel_domain" "chronark_com" {
34 | name = "chronark.com"
35 | }
36 |
37 |
38 | resource "vercel_dns" "www" {
39 | domain = vercel_domain.chronark_com.name
40 | type = "CNAME"
41 | value = "www.${vercel_domain.chronark_com.name}"
42 | name = "www"
43 | }
--------------------------------------------------------------------------------
/examples/provider/provider.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | vercel = {
4 | source = "hashicorp.com/chronark/vercel"
5 | version = "9000.1"
6 | }
7 | }
8 | }
9 |
10 | provider "vercel" {
11 | token = ""
12 | }
13 |
--------------------------------------------------------------------------------
/examples/resources/vercel_dns/resource.tf:
--------------------------------------------------------------------------------
1 |
2 |
3 | resource "vercel_domain" "chronark_com" {
4 | name = "chronark.com"
5 | }
6 |
7 |
8 | resource "vercel_dns" "www" {
9 | domain = vercel_domain.chronark_com.name
10 | type = "CNAME"
11 | value = "www.${vercel_domain.chronark_com.name}"
12 | name = "www"
13 | }
--------------------------------------------------------------------------------
/examples/resources/vercel_domain/resource.tf:
--------------------------------------------------------------------------------
1 |
2 | resource "vercel_domain" "google-com" {
3 | name = "google.com"
4 | }
--------------------------------------------------------------------------------
/examples/resources/vercel_env/resource.tf:
--------------------------------------------------------------------------------
1 |
2 | resource "vercel_project" "my_project" {
3 | // ...
4 | }
5 |
6 | resource "vercel_env" "my_env" {
7 | project_id = vercel_project.my_project.id // or use a hardcoded value of an existing project
8 | type = "plain"
9 | key = "hello"
10 | value = "world"
11 | target = ["production", "preview", "development"]
12 | }
--------------------------------------------------------------------------------
/examples/resources/vercel_project/resource.tf:
--------------------------------------------------------------------------------
1 |
2 | resource "vercel_project" "my_project" {
3 | name = "mercury"
4 | git_repository {
5 | type = "github"
6 | repo = "chronark/terraform-provider-vercel"
7 | }
8 | }
--------------------------------------------------------------------------------
/examples/resources/vercel_secret/resource.tf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | // Create a new secret
6 | resource "vercel_secret" "my_secret" {
7 | name = "my_secret_name" // `@` will be prefixed automatically
8 | value = "super secret"
9 | }
10 |
11 | // Use the secret
12 | resource "vercel_env" "env" {
13 | type = "secret"
14 | key = "Hello"
15 | value = vercel_secret.my_secret.id
16 |
17 | // irrelevant values omitted
18 | }
19 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/chronark/terraform-provider-vercel
2 |
3 | go 1.15
4 |
5 | require (
6 | github.com/hashicorp/go-uuid v1.0.2
7 | github.com/hashicorp/terraform-plugin-docs v0.4.0
8 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.8.0
9 | github.com/matryer/is v1.4.0 // indirect
10 | github.com/stretchr/testify v1.7.0
11 | golang.org/x/tools v0.1.1-0.20210504170620-03ebc2c9fca8 // indirect
12 | gopkg.in/yaml.v2 v2.4.0 // indirect
13 | )
14 |
--------------------------------------------------------------------------------
/internal/provider/data_source_team.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | )
10 |
11 | func dataSourceTeam() *schema.Resource {
12 | return &schema.Resource{
13 | Description: "Retrieves information related to an existing team. https://vercel.com/docs/api#endpoints/teams",
14 | ReadContext: dataSourceTeamRead,
15 | Schema: map[string]*schema.Schema{
16 | "id": {
17 | Computed: true,
18 | Type: schema.TypeString,
19 | },
20 | "slug": {
21 | Required: true,
22 | Type: schema.TypeString,
23 | },
24 | "name": {
25 | Computed: true,
26 | Type: schema.TypeString,
27 | },
28 | "creator_id": {
29 | Computed: true,
30 | Type: schema.TypeString,
31 | },
32 | "avatar": {
33 | Computed: true,
34 | Type: schema.TypeString,
35 | },
36 | "created": {
37 | Computed: true,
38 | Type: schema.TypeInt,
39 | },
40 | },
41 | }
42 | }
43 |
44 | func dataSourceTeamRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
45 |
46 | client := meta.(*vercel.Client)
47 |
48 | slug := d.Get("slug").(string)
49 |
50 | team, err := client.Team.Read(slug)
51 | if err != nil {
52 | return diag.FromErr(err)
53 | }
54 | err = d.Set("name", team.Name)
55 | if err != nil {
56 | return diag.FromErr(err)
57 | }
58 |
59 | err = d.Set("creator_id", team.CreatorId)
60 | if err != nil {
61 | return diag.FromErr(err)
62 | }
63 |
64 | err = d.Set("avatar", team.Avatar)
65 | if err != nil {
66 | return diag.FromErr(err)
67 | }
68 |
69 | err = d.Set("created", team.Created.Unix())
70 | if err != nil {
71 | return diag.FromErr(err)
72 | }
73 |
74 | d.SetId(team.Id)
75 |
76 | return diag.Diagnostics{}
77 | }
78 |
--------------------------------------------------------------------------------
/internal/provider/data_source_user.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | )
10 |
11 | func dataSourceUser() *schema.Resource {
12 | return &schema.Resource{
13 | // This description is used by the documentation generator and the language server.
14 | Description: "Retrieves information related to the authenticated user. https://vercel.com/docs/api#endpoints/user/get-the-authenticated-user",
15 | ReadContext: dataSourceUserRead,
16 | Schema: map[string]*schema.Schema{
17 | "id": {
18 | Computed: true,
19 | Type: schema.TypeString,
20 | },
21 | "email": {
22 | Computed: true,
23 | Type: schema.TypeString,
24 | },
25 | "name": {
26 | Computed: true,
27 | Type: schema.TypeString,
28 | },
29 | "username": {
30 | Computed: true,
31 | Type: schema.TypeString,
32 | },
33 | "avatar": {
34 | Computed: true,
35 | Type: schema.TypeString,
36 | },
37 | "platformversion": {
38 | Computed: true,
39 | Type: schema.TypeInt,
40 | },
41 |
42 | "bio": {
43 | Computed: true,
44 | Type: schema.TypeString,
45 | },
46 | "website": {
47 | Computed: true,
48 | Type: schema.TypeString,
49 | },
50 | "profiles": {
51 | Computed: true,
52 | Type: schema.TypeList,
53 | Elem: &schema.Resource{
54 | Schema: map[string]*schema.Schema{
55 | "service": {
56 | Computed: true,
57 | Type: schema.TypeString,
58 | },
59 | "link": {
60 | Computed: true,
61 | Type: schema.TypeString,
62 | },
63 | },
64 | },
65 | },
66 | },
67 | }
68 | }
69 |
70 | func dataSourceUserRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
71 |
72 | client := meta.(*vercel.Client)
73 |
74 | user, err := client.User.Read()
75 | if err != nil {
76 | return diag.FromErr(err)
77 | }
78 | err = d.Set("email", user.Email)
79 | if err != nil {
80 | return diag.FromErr(err)
81 | }
82 |
83 | err = d.Set("name", user.Name)
84 | if err != nil {
85 | return diag.FromErr(err)
86 | }
87 |
88 | err = d.Set("username", user.Username)
89 | if err != nil {
90 | return diag.FromErr(err)
91 | }
92 |
93 | err = d.Set("avatar", user.Avatar)
94 | if err != nil {
95 | return diag.FromErr(err)
96 | }
97 | err = d.Set("platformversion", user.PlatformVersion)
98 | if err != nil {
99 | return diag.FromErr(err)
100 | }
101 |
102 | err = d.Set("bio", user.Bio)
103 | if err != nil {
104 | return diag.FromErr(err)
105 | }
106 |
107 | err = d.Set("website", user.Website)
108 | if err != nil {
109 | return diag.FromErr(err)
110 | }
111 |
112 | err = d.Set("profiles", user.Profiles)
113 | if err != nil {
114 | return diag.FromErr(err)
115 | }
116 |
117 | d.SetId(user.UID)
118 |
119 | return diag.Diagnostics{}
120 | }
121 |
--------------------------------------------------------------------------------
/internal/provider/data_source_user_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
5 | "testing"
6 | )
7 |
8 | func TestAccDataSourceUser(t *testing.T) {
9 | resource.UnitTest(t, resource.TestCase{
10 | PreCheck: func() { testAccPreCheck(t) },
11 | ProviderFactories: providerFactories,
12 | Steps: []resource.TestStep{
13 | {
14 | Config: testAccDataSourceUser,
15 | Check: resource.ComposeTestCheckFunc(
16 | resource.TestCheckResourceAttr("data.vercel_user.me", "username", "chronark"),
17 | ),
18 | },
19 | },
20 | })
21 | }
22 |
23 | const testAccDataSourceUser = `data "vercel_user" "me" {}`
24 |
--------------------------------------------------------------------------------
/internal/provider/provider.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10 | )
11 |
12 | func init() {
13 | // Set descriptions to support markdown syntax, this will be used in document generation
14 | // and the language server.
15 | schema.DescriptionKind = schema.StringMarkdown
16 |
17 | // Customize the content of descriptions when output. For example you can add defaults on
18 | // to the exported descriptions if present.
19 | // schema.SchemaDescriptionBuilder = func(s *schema.Schema) string {
20 | // desc := s.Description
21 | // if s.Default != nil {
22 | // desc += fmt.Sprintf(" Defaults to `%v`.", s.Default)
23 | // }
24 | // return strings.TrimSpace(desc)
25 | // }
26 | }
27 |
28 | func New(version string) func() *schema.Provider {
29 | return func() *schema.Provider {
30 | p := &schema.Provider{
31 | Schema: map[string]*schema.Schema{
32 | "token": {
33 | Type: schema.TypeString,
34 | Required: true,
35 | Sensitive: true,
36 | DefaultFunc: schema.EnvDefaultFunc("VERCEL_TOKEN", nil),
37 | },
38 | },
39 | DataSourcesMap: map[string]*schema.Resource{
40 | "vercel_user": dataSourceUser(),
41 | "vercel_team": dataSourceTeam(),
42 | },
43 | ResourcesMap: map[string]*schema.Resource{
44 | "vercel_env": resourceEnv(),
45 | "vercel_project": resourceProject(),
46 | "vercel_secret": resourceSecret(),
47 | "vercel_domain": resourceDomain(),
48 | "vercel_dns": resourceDNS(),
49 | },
50 | }
51 |
52 | p.ConfigureContextFunc = configure(version, p)
53 |
54 | return p
55 | }
56 | }
57 |
58 | func configure(version string, p *schema.Provider) func(context.Context, *schema.ResourceData) (interface{}, diag.Diagnostics) {
59 | return func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
60 | token := d.Get("token").(string)
61 |
62 | if token == "" {
63 | return nil, diag.FromErr(fmt.Errorf("vercel token is not set, set manually or via `VERCEL_TOKEN` "))
64 | }
65 |
66 | client := vercel.New(token)
67 |
68 | return client, diag.Diagnostics{}
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/internal/provider/provider_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "github.com/stretchr/testify/require"
5 | "os"
6 | "testing"
7 |
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | )
10 |
11 | var providerFactories = map[string]func() (*schema.Provider, error){
12 | "vercel": func() (*schema.Provider, error) {
13 | return New("dev")(), nil
14 | },
15 | }
16 |
17 | func TestProvider(t *testing.T) {
18 | if err := New("dev")().InternalValidate(); err != nil {
19 | t.Fatalf("Unable to create new provider: %s", err)
20 | }
21 | }
22 |
23 | func testAccPreCheck(t *testing.T) {
24 | require.NotEmpty(t, os.Getenv("VERCEL_TOKEN"))
25 | }
26 |
--------------------------------------------------------------------------------
/internal/provider/resource_dns.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
6 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/dns"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | )
10 |
11 | func resourceDNS() *schema.Resource {
12 | return &schema.Resource{
13 | Description: "https://vercel.com/docs/api#endpoints/dns\nCurrently this can only fetch the last 1000 records. Please create an issue if you require more.",
14 |
15 | CreateContext: resourceDNSCreate,
16 | ReadContext: resourceDNSRead,
17 | DeleteContext: resourceDNSDelete,
18 |
19 | Schema: map[string]*schema.Schema{
20 | "domain": {
21 | Description: "The domain for this DNS record",
22 | Type: schema.TypeString,
23 | Required: true,
24 | ForceNew: true,
25 | },
26 | "id": {
27 | Description: "The unique identifier of the dns record.",
28 | Type: schema.TypeString,
29 | Computed: true,
30 | },
31 | "team_id": {
32 | Description: "By default, you can access resources contained within your own user account. To access resources owned by a team, you can pass in the team ID",
33 | Type: schema.TypeString,
34 | Optional: true,
35 | ForceNew: true,
36 | Default: "",
37 | },
38 | "type": {
39 | Description: "The type of record, it could be any valid DNS record.",
40 | Type: schema.TypeString,
41 | Required: true,
42 | ForceNew: true,
43 | },
44 | "name": {
45 | Description: "A subdomain name or an empty string for the root domain.",
46 | Type: schema.TypeString,
47 | Required: true,
48 | ForceNew: true,
49 | },
50 |
51 | "value": {
52 | Description: "The record value.",
53 | Type: schema.TypeString,
54 | Required: true,
55 | ForceNew: true,
56 | },
57 | "ttl": {
58 | Description: "The TTL value. Must be a number between 60 and 2147483647. Default value is 60.",
59 | Type: schema.TypeInt,
60 | Optional: true,
61 | Default: 60,
62 | ForceNew: true,
63 | },
64 | "creator": {
65 | Description: "The ID of the user who created the record or system if the record is an automatic record.",
66 | Type: schema.TypeString,
67 | Computed: true,
68 | },
69 | "created": {
70 | Description: "The date when the record was created.",
71 | Type: schema.TypeInt,
72 | Computed: true,
73 | },
74 | "updated": {
75 | Description: "The date when the record was updated.",
76 | Type: schema.TypeInt,
77 | Computed: true,
78 | },
79 | "created_at": {
80 | Description: "The date when the record was created in milliseconds since the UNIX epoch.",
81 | Type: schema.TypeInt,
82 | Computed: true,
83 | },
84 | "updated_at": {
85 | Description: "The date when the record was updated in milliseconds since the UNIX epoch.",
86 | Type: schema.TypeInt,
87 | Computed: true,
88 | },
89 | },
90 | }
91 | }
92 |
93 | func resourceDNSCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
94 |
95 | client := meta.(*vercel.Client)
96 |
97 | payload := dns.CreateRecord{
98 | Name: d.Get("name").(string),
99 | Type: d.Get("type").(string),
100 | Value: d.Get("value").(string),
101 | TTL: d.Get("ttl").(int),
102 | }
103 | domain := d.Get("domain").(string)
104 | teamId := d.Get("team_id").(string)
105 | dnsId, err := client.DNS.Create(domain, payload, teamId)
106 | if err != nil {
107 | return diag.FromErr(err)
108 | }
109 |
110 | d.SetId(dnsId)
111 |
112 | return resourceDNSRead(ctx, d, meta)
113 | }
114 |
115 | func resourceDNSRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
116 | client := meta.(*vercel.Client)
117 |
118 | record, err := client.DNS.Read(d.Get("domain").(string), d.Id(), d.Get("team_id").(string))
119 | if err != nil {
120 | return diag.FromErr(err)
121 | }
122 |
123 | err = d.Set("name", record.Name)
124 | if err != nil {
125 | return diag.FromErr(err)
126 | }
127 | err = d.Set("type", record.Type)
128 | if err != nil {
129 | return diag.FromErr(err)
130 | }
131 | err = d.Set("value", record.Value)
132 | if err != nil {
133 | return diag.FromErr(err)
134 | }
135 | err = d.Set("creator", record.Creator)
136 | if err != nil {
137 | return diag.FromErr(err)
138 | }
139 | err = d.Set("created", record.Created)
140 | if err != nil {
141 | return diag.FromErr(err)
142 | }
143 | err = d.Set("updated", record.Updated)
144 | if err != nil {
145 | return diag.FromErr(err)
146 | }
147 | err = d.Set("created_at", record.CreatedAt)
148 | if err != nil {
149 | return diag.FromErr(err)
150 | }
151 | err = d.Set("updated_at", record.UpdatedAt)
152 | if err != nil {
153 | return diag.FromErr(err)
154 | }
155 |
156 | return diag.Diagnostics{}
157 | }
158 |
159 | func resourceDNSDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
160 |
161 | client := meta.(*vercel.Client)
162 |
163 | err := client.DNS.Delete(d.Get("domain").(string), d.Id(), d.Get("team_id").(string))
164 | if err != nil {
165 | return diag.FromErr(err)
166 | }
167 | d.SetId("")
168 | return diag.Diagnostics{}
169 | }
170 |
--------------------------------------------------------------------------------
/internal/provider/resource_domain.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | )
10 |
11 | func resourceDomain() *schema.Resource {
12 | return &schema.Resource{
13 | Description: "Creates account wide domains to be used in your projects.",
14 |
15 | CreateContext: resourceDomainCreate,
16 | ReadContext: resourceDomainRead,
17 | DeleteContext: resourceDomainDelete,
18 |
19 | Importer: &schema.ResourceImporter{
20 | StateContext: resourceDomainImportState,
21 | },
22 |
23 | Schema: map[string]*schema.Schema{
24 | "team_id": {
25 | Description: "By default, you can access resources contained within your own user account. To access resources owned by a team, you can pass in the team ID",
26 | Type: schema.TypeString,
27 | Optional: true,
28 | ForceNew: true,
29 | Default: "",
30 | },
31 | "name": {
32 | Description: "The name of the production domain.",
33 | Type: schema.TypeString,
34 | ForceNew: true,
35 | Required: true,
36 | },
37 | "id": {
38 | Description: "Unique id for this variable.",
39 | Type: schema.TypeString,
40 | Computed: true,
41 | },
42 | "service_type": {
43 | Description: "The type of service the domain is handled by. external if the DNS is externally handled, or zeit.world if handled with Vercel.",
44 | Type: schema.TypeString,
45 | Computed: true,
46 | },
47 |
48 | "ns_verified_at": {
49 | Description: "The date at which the domain's nameservers were verified based on the intended set.",
50 | Type: schema.TypeInt,
51 | Computed: true,
52 | },
53 |
54 | "txt_verified_at": {
55 | Description: "The date at which the domain's TXT DNS record was verified.",
56 | Type: schema.TypeInt,
57 | Computed: true,
58 | },
59 |
60 | "cdn_enabled": {
61 | Description: "Whether the domain has the Vercel Edge Network enabled or not.",
62 | Type: schema.TypeBool,
63 | Computed: true,
64 | },
65 |
66 | "created_at": {
67 | Description: "The date when the domain was created in the registry.",
68 | Type: schema.TypeInt,
69 | Computed: true,
70 | },
71 |
72 | "expires_at": {
73 | Description: "The date at which the domain is set to expire. null if not bought with Vercel.",
74 | Type: schema.TypeInt,
75 | Computed: true,
76 | },
77 |
78 | "bought_at": {
79 | Description: "If it was purchased through Vercel, the date when it was purchased.",
80 | Type: schema.TypeInt,
81 | Computed: true,
82 | },
83 |
84 | "transfer_started_at": {
85 | Description: "If transferred into Vercel, The date when the domain transfer was initiated",
86 | Type: schema.TypeInt,
87 | Computed: true,
88 | },
89 |
90 | "transferred_at": {
91 | Description: "The date at which the domain was successfully transferred into Vercel. null if the transfer is still processing or was never transferred in.",
92 | Type: schema.TypeInt,
93 | Computed: true,
94 | },
95 |
96 | "verification_record": {
97 | Description: "The ID of the verification record in the registry.",
98 | Type: schema.TypeString,
99 | Computed: true,
100 | },
101 |
102 | "verified": {
103 | Description: "If the domain has the ownership verified.",
104 | Type: schema.TypeBool,
105 | Computed: true,
106 | },
107 |
108 | "nameservers": {
109 | Description: "A list of the current nameservers of the domain.",
110 | Type: schema.TypeList,
111 | Computed: true,
112 | Elem: &schema.Schema{
113 | Type: schema.TypeString,
114 | },
115 | },
116 |
117 | "intended_nameservers": {
118 | Description: "A list of the intended nameservers for the domain to point to Vercel DNS.",
119 | Type: schema.TypeList,
120 | Computed: true,
121 | Elem: &schema.Schema{
122 | Type: schema.TypeString,
123 | },
124 | },
125 | },
126 | }
127 | }
128 |
129 | func resourceDomainImportState(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
130 | parts, err := partsFromID(d.Id())
131 | if err != nil {
132 | return []*schema.ResourceData{}, err
133 | }
134 | domainName := parts[0]
135 | if len(parts) > 1 {
136 | client := meta.(*vercel.Client)
137 |
138 | teamSlug := parts[0]
139 | team, err := client.Team.Read(teamSlug)
140 | if err != nil {
141 | return []*schema.ResourceData{}, err
142 | }
143 |
144 | err = d.Set("team_id", team.Id)
145 | if err != nil {
146 | return []*schema.ResourceData{}, err
147 | }
148 |
149 | domainName = parts[1]
150 | }
151 | err = d.Set("name", domainName)
152 | if err != nil {
153 | return []*schema.ResourceData{}, err
154 | }
155 | return []*schema.ResourceData{d}, nil
156 | }
157 |
158 | func resourceDomainCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
159 |
160 | client := meta.(*vercel.Client)
161 |
162 | id, err := client.Domain.Create(d.Get("name").(string), d.Get("team_id").(string))
163 | if err != nil {
164 | return diag.FromErr(err)
165 | }
166 |
167 | d.SetId(id)
168 |
169 | return resourceDomainRead(ctx, d, meta)
170 | }
171 |
172 | func resourceDomainRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
173 | client := meta.(*vercel.Client)
174 |
175 | domain, err := client.Domain.Read(d.Get("name").(string), d.Get("team_id").(string))
176 | if err != nil {
177 | return diag.FromErr(err)
178 | }
179 | d.SetId(domain.ID)
180 | err = d.Set("service_type", domain.ServiceType)
181 | if err != nil {
182 | return diag.FromErr(err)
183 | }
184 | err = d.Set("ns_verified_at", domain.NsVerifiedAt)
185 | if err != nil {
186 | return diag.FromErr(err)
187 | }
188 | err = d.Set("txt_verified_at", domain.TxtVerifiedAt)
189 | if err != nil {
190 | return diag.FromErr(err)
191 | }
192 | err = d.Set("cdn_enabled", domain.CdnEnabled)
193 | if err != nil {
194 | return diag.FromErr(err)
195 | }
196 | err = d.Set("created_at", domain.CreatedAt)
197 | if err != nil {
198 | return diag.FromErr(err)
199 | }
200 | err = d.Set("expires_at", domain.ExpiresAt)
201 | if err != nil {
202 | return diag.FromErr(err)
203 | }
204 |
205 | err = d.Set("bought_at", domain.BoughtAt)
206 | if err != nil {
207 | return diag.FromErr(err)
208 | }
209 | err = d.Set("transfer_started_at", domain.TransferredAt)
210 | if err != nil {
211 | return diag.FromErr(err)
212 | }
213 | err = d.Set("transferred_at", domain.TransferredAt)
214 | if err != nil {
215 | return diag.FromErr(err)
216 | }
217 | err = d.Set("verification_record", domain.VerificationRecord)
218 | if err != nil {
219 | return diag.FromErr(err)
220 | }
221 | err = d.Set("verified", domain.Verified)
222 | if err != nil {
223 | return diag.FromErr(err)
224 | }
225 | err = d.Set("nameservers", domain.Nameservers)
226 | if err != nil {
227 | return diag.FromErr(err)
228 | }
229 | err = d.Set("intended_nameservers", domain.IntendedNameservers)
230 | if err != nil {
231 | return diag.FromErr(err)
232 | }
233 | return diag.Diagnostics{}
234 | }
235 |
236 | func resourceDomainDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
237 |
238 | client := meta.(*vercel.Client)
239 |
240 | domainName := d.Get("name").(string)
241 |
242 | err := client.Domain.Delete(domainName, d.Get("team_id").(string))
243 | if err != nil {
244 | return diag.FromErr(err)
245 | }
246 | d.SetId("")
247 | return diag.Diagnostics{}
248 | }
249 |
--------------------------------------------------------------------------------
/internal/provider/resource_domain_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "testing"
8 |
9 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
10 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/domain"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
12 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
13 | )
14 |
15 | func TestAccVercelDomain(t *testing.T) {
16 |
17 | domainName := "acceptancetestdomainone.com"
18 | updatedDomainName := "acceptancetestdomaintwo.com"
19 | var (
20 |
21 | // Holds the domain fetched from vercel when we create it at the beginning
22 | actualDomainAfterCreation domain.Domain
23 |
24 | // Renaming or changing a variable should not result in the recreation of the domain, so we expect to have the same id.
25 | actualDomainAfterUpdate domain.Domain
26 | )
27 | resource.Test(t, resource.TestCase{
28 | PreCheck: func() { testAccPreCheck(t) },
29 | ProviderFactories: providerFactories,
30 | CheckDestroy: testAccCheckVercelDomainDestroy(domainName),
31 | Steps: []resource.TestStep{
32 | {
33 | Config: testAccCheckVercelDomainConfig(domainName),
34 | Check: resource.ComposeTestCheckFunc(
35 | testAccCheckDomainStateHasValues(
36 | "vercel_domain.new", domain.Domain{Name: domainName},
37 | ),
38 | testAccCheckVercelDomainExists("vercel_domain.new", &actualDomainAfterCreation),
39 | testAccCheckActualDomainHasValues(&actualDomainAfterCreation, &domain.Domain{Name: domainName}),
40 | ),
41 | },
42 | {
43 | Config: testAccCheckVercelDomainConfig(updatedDomainName),
44 | Check: resource.ComposeTestCheckFunc(
45 | testAccCheckVercelDomainExists("vercel_domain.new", &actualDomainAfterUpdate),
46 | testAccCheckDomainStateHasValues(
47 | "vercel_domain.new", domain.Domain{Name: updatedDomainName},
48 | ),
49 | testAccCheckActualDomainHasValues(&actualDomainAfterUpdate, &domain.Domain{Name: updatedDomainName}),
50 | testAccCheckDomainWasRecreated(&actualDomainAfterCreation, &actualDomainAfterUpdate),
51 | ),
52 | },
53 | },
54 | })
55 | }
56 |
57 | func TestAccVercelDomain_import(t *testing.T) {
58 |
59 | domainName := "acceptancetestdomainone.com"
60 | var (
61 | actualDomain domain.Domain
62 | )
63 | resource.Test(t, resource.TestCase{
64 | PreCheck: func() { testAccPreCheck(t) },
65 | ProviderFactories: providerFactories,
66 | CheckDestroy: testAccCheckVercelDomainDestroy(domainName),
67 | Steps: []resource.TestStep{
68 | {
69 | Config: testAccCheckVercelDomainConfig(domainName),
70 | Check: resource.ComposeTestCheckFunc(
71 | testAccCheckDomainStateHasValues(
72 | "vercel_domain.new", domain.Domain{Name: domainName},
73 | ),
74 | testAccCheckVercelDomainExists("vercel_domain.new", &actualDomain),
75 | testAccVercelDomainImport(&actualDomain),
76 | ),
77 | },
78 | },
79 | })
80 | }
81 |
82 | // Combines multiple `resource.TestCheckResourceAttr` calls
83 | func testAccCheckDomainStateHasValues(name string, want domain.Domain) resource.TestCheckFunc {
84 | return func(s *terraform.State) error {
85 | tests := []resource.TestCheckFunc{
86 |
87 | resource.TestCheckResourceAttr(
88 | name, "name", want.Name),
89 | }
90 |
91 | for _, test := range tests {
92 | err := test(s)
93 | if err != nil {
94 | return err
95 | }
96 | }
97 | return nil
98 | }
99 | }
100 |
101 | func testAccCheckDomainWasRecreated(s1, s2 *domain.Domain) resource.TestCheckFunc {
102 | return func(s *terraform.State) error {
103 | if s1.ID == s2.ID {
104 | return fmt.Errorf("Expected different IDs but they are the same.")
105 | }
106 | return nil
107 | }
108 | }
109 |
110 | func testAccCheckActualDomainHasValues(actual *domain.Domain, want *domain.Domain) resource.TestCheckFunc {
111 | return func(s *terraform.State) error {
112 | if actual.Name != want.Name {
113 | return fmt.Errorf("name does not match, expected: %s, got: %s", want.Name, actual.Name)
114 | }
115 | if actual.ID == "" {
116 | return fmt.Errorf("ID is empty")
117 | }
118 |
119 | return nil
120 | }
121 | }
122 |
123 | // Test whether the domain was destroyed properly and finishes the job if necessary
124 | func testAccCheckVercelDomainDestroy(name string) resource.TestCheckFunc {
125 | return func(s *terraform.State) error {
126 | client := vercel.New(os.Getenv("VERCEL_TOKEN"))
127 |
128 | for _, rs := range s.RootModule().Resources {
129 | if rs.Type != name {
130 | continue
131 | }
132 |
133 | domain, err := client.Domain.Read(rs.Primary.ID, "")
134 | if err == nil {
135 | message := "Domain was not deleted from vercel during terraform destroy."
136 | deleteErr := client.Domain.Delete(domain.Name, "")
137 | if deleteErr != nil {
138 | return fmt.Errorf(message+" Automated removal did not succeed. Please manually remove @%s. Error: %w", domain.Name, err)
139 | }
140 | return fmt.Errorf(message + " It was removed now.")
141 | }
142 |
143 | }
144 | return nil
145 | }
146 |
147 | }
148 | func testAccCheckVercelDomainConfig(name string) string {
149 | return fmt.Sprintf(`
150 | resource "vercel_domain" "new" {
151 | name = "%s"
152 | }
153 | `, name)
154 | }
155 |
156 | func testAccCheckVercelDomainExists(n string, actual *domain.Domain) resource.TestCheckFunc {
157 | return func(s *terraform.State) error {
158 | rs, ok := s.RootModule().Resources[n]
159 |
160 | if !ok {
161 | return fmt.Errorf("Not found: %s in %+v", n, s.RootModule().Resources)
162 | }
163 |
164 | if rs.Primary.ID == "" {
165 | return fmt.Errorf("No domain set")
166 | }
167 |
168 | domain, err := vercel.New(os.Getenv("VERCEL_TOKEN")).Domain.Read(rs.Primary.Attributes["name"], "")
169 | if err != nil {
170 | return err
171 | }
172 | *actual = domain
173 | return nil
174 | }
175 | }
176 |
177 | func testAccVercelDomainImport(source *domain.Domain) resource.TestCheckFunc {
178 | return func(s *terraform.State) error {
179 | data := resourceDomain().Data(nil)
180 | data.SetId(source.Name)
181 | ds, err := resourceDomain().Importer.StateContext(context.Background(), data, vercel.New(os.Getenv("VERCEL_TOKEN")))
182 | if err != nil {
183 | return err
184 | }
185 | if len(ds) != 1 {
186 | return fmt.Errorf("Expected 1 instance state from importer function. Got %d", len(ds))
187 | }
188 |
189 | if ds[0].Get("name") != source.Name {
190 | return fmt.Errorf("Imported domain name. Expected '%s'. Actual '%s'.", source.Name, ds[0].Get("name").(string))
191 | }
192 | return nil
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/internal/provider/resource_env.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
8 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/env"
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11 | )
12 |
13 | func resourceEnv() *schema.Resource {
14 | return &schema.Resource{
15 | Description: "https://vercel.com/docs/api#endpoints/projects/get-project-environment-variables",
16 |
17 | CreateContext: resourceEnvCreate,
18 | ReadContext: resourceEnvRead,
19 | UpdateContext: resourceEnvUpdate,
20 | DeleteContext: resourceEnvDelete,
21 |
22 | Importer: &schema.ResourceImporter{
23 | StateContext: resourceEnvImportState,
24 | },
25 |
26 | Schema: map[string]*schema.Schema{
27 | "project_id": {
28 | Description: "The unique project identifier.",
29 | Type: schema.TypeString,
30 | Required: true,
31 | },
32 | "team_id": {
33 | Description: "By default, you can access resources contained within your own user account. To access resources owned by a team, you can pass in the team ID",
34 | Type: schema.TypeString,
35 | Optional: true,
36 | ForceNew: true,
37 | Default: "",
38 | },
39 | "type": {
40 | Description: "The type can be `plain`, `secret`, or `system`.",
41 | Type: schema.TypeString,
42 | Required: true,
43 | },
44 | "id": {
45 | Description: "Unique id for this variable.",
46 | Type: schema.TypeString,
47 | Computed: true,
48 | },
49 | "key": {
50 | Description: "The name of the environment variable.",
51 | Type: schema.TypeString,
52 | Required: true,
53 | },
54 | "value": {
55 | Description: "If the type is `plain`, a string representing the value of the environment variable. If the type is `secret`, the secret ID of the secret attached to the environment variable. If the type is `system`, the name of the System Environment Variable.",
56 | Type: schema.TypeString,
57 | Required: true,
58 | },
59 | "target": {
60 | Description: "The target can be a list of `development`, `preview`, or `production`.",
61 | Type: schema.TypeList,
62 | Required: true,
63 | MaxItems: 3,
64 | Elem: &schema.Schema{
65 | Type: schema.TypeString,
66 | },
67 | },
68 | "created_at": {
69 | Description: "A number containing the date when the variable was created in milliseconds.",
70 | Type: schema.TypeInt,
71 | Computed: true,
72 | },
73 | "updated_at": {
74 | Description: "A number containing the date when the variable was updated in milliseconds.",
75 | Type: schema.TypeInt,
76 | Computed: true,
77 | },
78 | },
79 | }
80 | }
81 |
82 | func resourceEnvImportState(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
83 | parts, err := partsFromID(d.Id())
84 | if err != nil {
85 | return []*schema.ResourceData{}, err
86 | }
87 | if len(parts) < 2 {
88 | return []*schema.ResourceData{}, fmt.Errorf("Invalid import ID (use project_id/VARIABLE_NAME")
89 | }
90 |
91 | projectID, key := parts[0], parts[1]
92 |
93 | client := meta.(*vercel.Client)
94 |
95 | teamID := ""
96 | if len(parts) > 2 {
97 | var teamSlug string
98 | teamSlug, projectID = projectID, key
99 | key = parts[2]
100 |
101 | team, err := client.Team.Read(teamSlug)
102 | if err != nil {
103 | return []*schema.ResourceData{}, err
104 | }
105 | teamID = team.Id
106 | }
107 |
108 | // Read project ID from project name
109 | project, err := client.Project.Read(projectID, teamID)
110 | if err != nil {
111 | return []*schema.ResourceData{}, err
112 | }
113 | projectID = project.ID
114 |
115 | allEnvVariables, err := client.Env.Read(projectID, teamID)
116 | if err != nil {
117 | return []*schema.ResourceData{}, err
118 | }
119 |
120 | // Filter the current variable out of all existing ones
121 | for _, envVar := range allEnvVariables {
122 | if envVar.Key == key {
123 | d.SetId(envVar.ID)
124 | err = d.Set("project_id", projectID)
125 | if err != nil {
126 | return []*schema.ResourceData{}, err
127 | }
128 | return []*schema.ResourceData{d}, nil
129 | }
130 | }
131 |
132 | return []*schema.ResourceData{d}, fmt.Errorf("No '%s' environment variable in project", key)
133 | }
134 |
135 | func toCreateOrUpdateEnv(d *schema.ResourceData) env.CreateOrUpdateEnv {
136 |
137 | // Casting each target because go does not allow typecasting from interface{} to []string
138 | targetList := d.Get("target").([]interface{})
139 | target := make([]string, len(targetList))
140 | for i := 0; i < len(target); i++ {
141 | target[i] = targetList[i].(string)
142 | }
143 |
144 | return env.CreateOrUpdateEnv{
145 | Type: d.Get("type").(string),
146 | Key: d.Get("key").(string),
147 | Value: d.Get("value").(string),
148 | Target: target,
149 | }
150 | }
151 |
152 | func resourceEnvCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
153 |
154 | client := meta.(*vercel.Client)
155 |
156 | payload := toCreateOrUpdateEnv(d)
157 |
158 | envID, err := client.Env.Create(d.Get("project_id").(string), payload, d.Get("team_id").(string))
159 | if err != nil {
160 | return diag.FromErr(err)
161 | }
162 |
163 | d.SetId(envID)
164 |
165 | return resourceEnvRead(ctx, d, meta)
166 | }
167 |
168 | func resourceEnvRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
169 | client := meta.(*vercel.Client)
170 |
171 | id := d.Id()
172 | allEnvVariables, err := client.Env.Read(d.Get("project_id").(string), d.Get("team_id").(string))
173 | if err != nil {
174 | return diag.FromErr(err)
175 | }
176 |
177 | // Filter the current variable out of all existing ones
178 | var currentVar env.Env
179 | for _, envVar := range allEnvVariables {
180 | if envVar.ID == id {
181 | currentVar = envVar
182 | break
183 | }
184 | }
185 |
186 | err = d.Set("type", currentVar.Type)
187 | if err != nil {
188 | return diag.FromErr(err)
189 | }
190 | err = d.Set("key", currentVar.Key)
191 | if err != nil {
192 | return diag.FromErr(err)
193 | }
194 | err = d.Set("value", currentVar.Value)
195 | if err != nil {
196 | return diag.FromErr(err)
197 | }
198 | err = d.Set("target", currentVar.Target)
199 | if err != nil {
200 | return diag.FromErr(err)
201 | }
202 | err = d.Set("updated_at", currentVar.UpdatedAt)
203 | if err != nil {
204 | return diag.FromErr(err)
205 | }
206 | err = d.Set("created_at", currentVar.CreatedAt)
207 | if err != nil {
208 | return diag.FromErr(err)
209 | }
210 |
211 | return diag.Diagnostics{}
212 | }
213 |
214 | func resourceEnvUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
215 |
216 | client := meta.(*vercel.Client)
217 |
218 | // Vercel expects an object with all 4 keys, so there's not point in checking for individual changes.
219 | if d.HasChanges("type", "key", "value", "taget") {
220 |
221 | projectID := d.Get("project_id").(string)
222 | envID := d.Id()
223 | payload := toCreateOrUpdateEnv(d)
224 |
225 | err := client.Env.Update(projectID, envID, payload, d.Get("team_id").(string))
226 | if err != nil {
227 | return diag.FromErr(err)
228 | }
229 | }
230 | return resourceEnvRead(ctx, d, meta)
231 | }
232 |
233 | func resourceEnvDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
234 |
235 | client := meta.(*vercel.Client)
236 |
237 | projectID := d.Get("project_id").(string)
238 | envID := d.Get("id").(string)
239 |
240 | err := client.Env.Delete(projectID, envID, d.Get("team_id").(string))
241 | if err != nil {
242 | return diag.FromErr(err)
243 | }
244 | d.SetId("")
245 | return diag.Diagnostics{}
246 | }
247 |
--------------------------------------------------------------------------------
/internal/provider/resource_env_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "testing"
8 |
9 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
10 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/env"
11 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/project"
12 | "github.com/hashicorp/go-uuid"
13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
14 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
15 | )
16 |
17 | func TestAccVercelEnv_import(t *testing.T) {
18 |
19 | projectName, _ := uuid.GenerateUUID()
20 | var (
21 | actualProject project.Project
22 | actualEnv env.Env
23 | )
24 | resource.Test(t, resource.TestCase{
25 | PreCheck: func() { testAccPreCheck(t) },
26 | ProviderFactories: providerFactories,
27 | CheckDestroy: testAccCheckVercelProjectDestroy(projectName),
28 | Steps: []resource.TestStep{
29 | {
30 | Config: testAccCheckVercelEnvConfig(projectName),
31 | Check: resource.ComposeTestCheckFunc(
32 | testAccCheckProjectStateHasValues(
33 | "vercel_project.new", project.Project{Name: projectName},
34 | ),
35 | testAccCheckVercelProjectExists("vercel_project.new", &actualProject),
36 | testAccCheckVercelEnvExists("vercel_env.new", &actualEnv),
37 | testAccVercelEnvImport(&actualProject, &actualEnv),
38 | ),
39 | },
40 | },
41 | })
42 | }
43 |
44 | func testAccCheckVercelEnvConfig(name string) string {
45 | return fmt.Sprintf(`
46 | resource "vercel_project" "new" {
47 | name = "%s"
48 | git_repository {
49 | type = "github"
50 | repo = "chronark/terraform-provider-vercel"
51 | }
52 | }
53 |
54 | resource "vercel_env" "new" {
55 | key = "FOO"
56 | value = "BAR"
57 |
58 | project_id = vercel_project.new.id
59 | target = [ "production" ]
60 | type = "plain"
61 | }
62 | `, name)
63 | }
64 |
65 | func testAccCheckVercelEnvExists(n string, actual *env.Env) resource.TestCheckFunc {
66 | return func(s *terraform.State) error {
67 | rs, ok := s.RootModule().Resources[n]
68 |
69 | if !ok {
70 | return fmt.Errorf("Not found: %s in %+v", n, s.RootModule().Resources)
71 | }
72 |
73 | if rs.Primary.ID == "" {
74 | return fmt.Errorf("No project set")
75 | }
76 |
77 | projectID := rs.Primary.Attributes["project_id"]
78 | teamID := rs.Primary.Attributes["team_id"]
79 |
80 | envs, err := vercel.New(os.Getenv("VERCEL_TOKEN")).Env.Read(projectID, teamID)
81 | if err != nil {
82 | return err
83 | }
84 | for _, env := range envs {
85 | if env.ID == rs.Primary.ID {
86 | *actual = env
87 | return nil
88 | }
89 | }
90 |
91 | return fmt.Errorf("Could not find env '%s' in project '%s'", rs.Primary.ID, projectID)
92 | }
93 | }
94 |
95 | func testAccVercelEnvImport(srcProject *project.Project, srcEnv *env.Env) resource.TestCheckFunc {
96 | return func(s *terraform.State) error {
97 | data := resourceEnv().Data(nil)
98 | data.SetId(srcProject.Name + "/" + srcEnv.Key)
99 | ds, err := resourceEnv().Importer.StateContext(context.Background(), data, vercel.New(os.Getenv("VERCEL_TOKEN")))
100 | if err != nil {
101 | return err
102 | }
103 | if len(ds) != 1 {
104 | return fmt.Errorf("Expected 1 instance state from importer function. Got %d", len(ds))
105 | }
106 |
107 | if ds[0].Id() != srcEnv.ID {
108 | return fmt.Errorf("Imported env ID. Expected '%s'. Actual '%s'.", srcEnv.ID, ds[0].Id())
109 | }
110 | if ds[0].Get("project_id") != srcProject.ID {
111 | return fmt.Errorf("Imported env project ID. Expected '%s'. Actual '%s'.", srcProject.ID, ds[0].Get("project_id").(string))
112 | }
113 | return nil
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/internal/provider/resource_project.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/url"
7 | "strings"
8 |
9 | "github.com/chronark/terraform-provider-vercel/pkg/util"
10 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
11 | projectApi "github.com/chronark/terraform-provider-vercel/pkg/vercel/project"
12 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
14 | )
15 |
16 | func resourceProject() *schema.Resource {
17 | return &schema.Resource{
18 | Description: "https://vercel.com/docs/api#endpoints/projects",
19 |
20 | CreateContext: resourceProjectCreate,
21 | ReadContext: resourceProjectRead,
22 | UpdateContext: resourceProjectUpdate,
23 | DeleteContext: resourceProjectDelete,
24 |
25 | Importer: &schema.ResourceImporter{
26 | StateContext: resourceProjectImportState,
27 | },
28 |
29 | Schema: map[string]*schema.Schema{
30 | "id": {
31 | Description: "Internal id of this project",
32 | Type: schema.TypeString,
33 | Computed: true,
34 | },
35 | "name": {
36 | Description: "The name of the project.",
37 | Type: schema.TypeString,
38 | Required: true,
39 | },
40 | "team_id": {
41 | Description: "By default, you can access resources contained within your own user account. To access resources owned by a team, you can pass in the team ID",
42 | Type: schema.TypeString,
43 | Optional: true,
44 | ForceNew: true,
45 | Default: "",
46 | },
47 | "git_repository": {
48 | Description: "The git repository that will be connected to the project. Any pushes to the specified connected git repository will be automatically deployed.",
49 | Optional: true,
50 | ForceNew: true,
51 | Type: schema.TypeList,
52 | MaxItems: 1,
53 | Elem: &schema.Resource{
54 | Schema: map[string]*schema.Schema{
55 | "type": {
56 | Description: "The git provider of the repository. Must be either `github`, `gitlab`, or `bitbucket`.",
57 | Type: schema.TypeString,
58 | Required: true,
59 | },
60 | "repo": {
61 | Description: "The name of the git repository. For example: `chronark/terraform-provider-vercel`",
62 | Type: schema.TypeString,
63 | Required: true,
64 | },
65 | },
66 | },
67 | },
68 | "domain": {
69 | Description: "Add a domain to the project by passing the project.",
70 | Optional: true,
71 | Type: schema.TypeList,
72 | Elem: &schema.Resource{
73 | Schema: map[string]*schema.Schema{
74 | "name": {
75 | Description: "The name of the production domain.",
76 | Type: schema.TypeString,
77 | Required: true,
78 | },
79 | "redirect": {
80 | Description: "Target destination domain for redirect.",
81 | Type: schema.TypeString,
82 | Optional: true,
83 | }, "redirect_status_code": {
84 | Description: "The redirect status code (301, 302, 307, 308).",
85 | Type: schema.TypeInt,
86 | Optional: true,
87 | }, "git_branch": {
88 | Description: "it branch for the domain to be auto assigned to. The Project's production branch is the default (null).",
89 | Type: schema.TypeString,
90 | Optional: true,
91 | },
92 | },
93 | },
94 | },
95 | "account_id": {
96 | Description: "The unique ID of the user or team the project belongs to.",
97 | Type: schema.TypeString,
98 | Computed: true,
99 | },
100 | "created_at": {
101 | Description: "A number containing the date when the project was created in milliseconds.",
102 | Type: schema.TypeInt,
103 | Computed: true,
104 | },
105 | "updated_at": {
106 | Description: "A number containing the date when the project was updated in milliseconds.",
107 | Type: schema.TypeInt,
108 | Computed: true,
109 | },
110 | "framework": {
111 | Description: "The framework that is being used for this project. When null is used no framework is selected.",
112 | Type: schema.TypeString,
113 | Optional: true,
114 | },
115 | "public_source": {
116 | Description: " Specifies whether the source code and logs of the deployments for this project should be public or not.",
117 | Type: schema.TypeBool,
118 | Optional: true,
119 | },
120 |
121 | "install_command": {
122 | Description: "The install command for this project. When null is used this value will be automatically detected.",
123 | Type: schema.TypeString,
124 | Optional: true,
125 | },
126 | "build_command": {
127 | Description: "The build command for this project. When null is used this value will be automatically detected.",
128 | Type: schema.TypeString,
129 | Optional: true,
130 | },
131 | "dev_command": {
132 | Description: "The dev command for this project. When null is used this value will be automatically detected.",
133 | Type: schema.TypeString,
134 | Optional: true,
135 | },
136 | "output_directory": {
137 | Description: "The output directory of the project. When null is used this value will be automatically detected.",
138 | Type: schema.TypeString,
139 | Optional: true,
140 | Default: nil,
141 | },
142 | "serverless_function_region": {
143 | Description: "The region to deploy Serverless Functions in this project.",
144 | Type: schema.TypeString,
145 | Optional: true,
146 | Computed: true,
147 | },
148 | "root_directory": {
149 | Description: "The name of a directory or relative path to the source code of your project. When null is used it will default to the project root.",
150 | Type: schema.TypeString,
151 | Optional: true,
152 | Default: nil,
153 | },
154 | "node_version": {
155 | Description: "The Node.js Version for this project.",
156 | Type: schema.TypeString,
157 | Optional: true,
158 | Computed: true,
159 | },
160 | "alias": {
161 | Description: "A list of production domains for the project.",
162 | Type: schema.TypeList,
163 | Computed: true,
164 | Optional: true,
165 | Elem: &schema.Schema{
166 | Type: schema.TypeString,
167 | },
168 | },
169 | },
170 | }
171 | }
172 |
173 | func partsFromID(id string) ([]string, error) {
174 | parts := strings.Split(id, "/")
175 | results := make([]string, len(parts))
176 | for i, part := range parts {
177 | result, err := url.QueryUnescape(part)
178 | if err != nil {
179 | return results, err
180 | }
181 |
182 | results[i] = result
183 | }
184 |
185 | return results, nil
186 | }
187 |
188 | func resourceProjectImportState(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
189 | parts, err := partsFromID(d.Id())
190 | if err != nil {
191 | return []*schema.ResourceData{}, err
192 | }
193 | projectID := parts[0]
194 | if len(parts) > 1 {
195 | teamSlug := parts[0]
196 | client := meta.(*vercel.Client)
197 | team, err := client.Team.Read(teamSlug)
198 | if err != nil {
199 | return []*schema.ResourceData{}, err
200 | }
201 |
202 | err = d.Set("team_id", team.Id)
203 | if err != nil {
204 | return []*schema.ResourceData{}, err
205 | }
206 |
207 | projectID = parts[1]
208 | }
209 | d.SetId(projectID)
210 | return []*schema.ResourceData{d}, nil
211 | }
212 |
213 | func resourceProjectCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
214 |
215 | client := meta.(*vercel.Client)
216 |
217 | var project projectApi.CreateProject
218 | _, repoSet := d.GetOk("git_repository")
219 | if repoSet {
220 | // Terraform does not have nested objects with different types yet, so I am using a `TypeList`
221 | // Here we have to typecast to list first and then take the first item and cast again.
222 | repo := d.Get("git_repository").([]interface{})[0].(map[string]interface{})
223 | project = projectApi.CreateProject{
224 | Name: d.Get("name").(string),
225 | GitRepository: &struct {
226 | Type string `json:"type"`
227 | Repo string `json:"repo"`
228 | }{
229 | Type: repo["type"].(string),
230 | Repo: repo["repo"].(string),
231 | },
232 | }
233 | } else {
234 | project = projectApi.CreateProject{
235 | Name: d.Get("name").(string),
236 | }
237 | }
238 |
239 | framework, frameworkSet := d.GetOk("framework")
240 | if frameworkSet {
241 | project.Framework = framework.(string)
242 | }
243 | publicSource, publicSourceSet := d.GetOk("public_source")
244 | if publicSourceSet {
245 | project.PublicSource = publicSource.(bool)
246 | }
247 | installCommand, installCommandSet := d.GetOk("install_command")
248 | if installCommandSet {
249 | project.InstallCommand = installCommand.(string)
250 | }
251 | buildCommand, buildCommandSet := d.GetOk("build_command")
252 | if buildCommandSet {
253 | project.BuildCommand = buildCommand.(string)
254 | }
255 | devCommand, devCommandSet := d.GetOk("dev_command")
256 | if devCommandSet {
257 | project.DevCommand = devCommand.(string)
258 | }
259 | outputDirectory, outputDirectorySet := d.GetOk("output_directory")
260 | if outputDirectorySet {
261 | project.OutputDirectory = outputDirectory.(string)
262 | }
263 |
264 | serverlessFunctionRegion, serverlessFunctionRegionSet := d.GetOk("serverless_function_region")
265 | if serverlessFunctionRegionSet {
266 | project.ServerlessFunctionRegion = serverlessFunctionRegion.(string)
267 | }
268 | rootDirectory, rootDirectorySet := d.GetOk("root_directory")
269 | if rootDirectorySet {
270 | project.RootDirectory = rootDirectory.(string)
271 | }
272 | nodeVersion, nodeVersionSet := d.GetOk("node_version")
273 | if nodeVersionSet {
274 | project.NodeVersion = nodeVersion.(string)
275 |
276 | }
277 |
278 | id, err := client.Project.Create(project, d.Get("team_id").(string))
279 | if err != nil {
280 | return diag.FromErr(err)
281 | }
282 | // repo := d.Get("git_repository").([]interface{})[0].(map[string]interface{})
283 |
284 | _, domainSet := d.GetOk("domain")
285 | if domainSet {
286 |
287 | rawDomain := d.Get("domain").([]interface{})[0].(map[string]interface{})
288 | domain := projectApi.Domain{
289 | Name: rawDomain["name"].(string),
290 | // Redirect: rawDomain["redirect"].(string),
291 | // RedirectStatusCode: rawDomain["redirect_status_code"].(int),
292 | }
293 |
294 | err = client.Project.AddDomain(id, domain, d.Get("team_id").(string))
295 | if err != nil {
296 | return diag.FromErr(err)
297 | }
298 | }
299 |
300 | d.SetId(id)
301 |
302 | return resourceProjectRead(ctx, d, meta)
303 | }
304 |
305 | func resourceProjectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
306 | client := meta.(*vercel.Client)
307 |
308 | id := d.Id()
309 |
310 | project, err := client.Project.Read(id, d.Get("team_id").(string))
311 | if err != nil {
312 | return diag.FromErr(err)
313 | }
314 |
315 | d.SetId(project.ID)
316 |
317 | err = d.Set("name", project.Name)
318 | if err != nil {
319 | return diag.FromErr(err)
320 | }
321 | err = d.Set("account_id", project.AccountID)
322 | if err != nil {
323 | return diag.FromErr(err)
324 | }
325 | err = d.Set("created_at", project.CreatedAt)
326 | if err != nil {
327 | return diag.FromErr(err)
328 | }
329 | err = d.Set("updated_at", project.UpdatedAt)
330 | if err != nil {
331 | return diag.FromErr(err)
332 | }
333 |
334 | err = d.Set("framework", project.Framework)
335 | if err != nil {
336 | return diag.FromErr(err)
337 | }
338 | err = d.Set("public_source", project.PublicSource)
339 | if err != nil {
340 | return diag.FromErr(err)
341 | }
342 |
343 | err = d.Set("install_command", project.InstallCommand)
344 | if err != nil {
345 | return diag.FromErr(err)
346 | }
347 | err = d.Set("build_command", project.BuildCommand)
348 | if err != nil {
349 | return diag.FromErr(err)
350 | }
351 | err = d.Set("dev_command", project.DevCommand)
352 | if err != nil {
353 | return diag.FromErr(err)
354 | }
355 | err = d.Set("output_directory", project.OutputDirectory)
356 | if err != nil {
357 | return diag.FromErr(err)
358 | }
359 | err = d.Set("serverless_function_region", project.ServerlessFunctionRegion)
360 | if err != nil {
361 | return diag.FromErr(err)
362 | }
363 | err = d.Set("root_directory", project.RootDirectory)
364 | if err != nil {
365 | return diag.FromErr(err)
366 | }
367 | err = d.Set("node_version", project.NodeVersion)
368 | if err != nil {
369 | return diag.FromErr(err)
370 | }
371 |
372 | aliases := make([]string, 0)
373 | for i := 0; i < len(project.Alias); i++ {
374 | aliases = append(aliases, project.Alias[i].Domain)
375 | }
376 | err = d.Set("alias", aliases)
377 | if err != nil {
378 | return diag.FromErr(err)
379 | }
380 |
381 | gitRepository := make([]map[string]interface{}, 1)
382 | gitRepository[0] = map[string]interface{}{
383 | "type": project.Link.Type,
384 | }
385 | switch project.Link.Type {
386 | case "gitlab":
387 | gitRepository[0]["repo"] = fmt.Sprintf("%s/%s", project.Link.ProjectNamespace, project.Link.ProjectName)
388 | case "github":
389 | gitRepository[0]["repo"] = fmt.Sprintf("%s/%s", project.Link.Org, project.Link.Repo)
390 | case "bitbucket":
391 | gitRepository[0]["repo"] = fmt.Sprintf("%s/%s", project.Link.Owner, project.Link.Slug)
392 | default:
393 | return diag.Diagnostics{}
394 | }
395 |
396 | err = d.Set("git_repository", gitRepository)
397 | if err != nil {
398 | return diag.FromErr(err)
399 | }
400 |
401 | return diag.Diagnostics{}
402 | }
403 |
404 | func resourceProjectUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
405 |
406 | client := meta.(*vercel.Client)
407 | var update projectApi.UpdateProject
408 |
409 | if d.HasChange("name") {
410 | update.Name = d.Get("name").(string)
411 | }
412 | if d.HasChange("framework") {
413 | update.Framework = d.Get("framework").(string)
414 | }
415 | if d.HasChange("public_source") {
416 | update.PublicSource = d.Get("public_source").(bool)
417 | }
418 | if d.HasChange("install_command") {
419 | update.InstallCommand = d.Get("install_command").(string)
420 | }
421 | if d.HasChange("build_command") {
422 | update.BuildCommand = d.Get("build_command").(string)
423 | }
424 | if d.HasChange("dev_command") {
425 | update.DevCommand = d.Get("dev_command").(string)
426 | }
427 | if d.HasChange("output_directory") {
428 | update.OutputDirectory = d.Get("output_directory").(string)
429 | }
430 | if d.HasChange("serverless_function_region") {
431 | update.ServerlessFunctionRegion = d.Get("serverless_function_region").(string)
432 | }
433 | if d.HasChange("root_directory") {
434 | update.RootDirectory = d.Get("root_directory").(string)
435 | }
436 | if d.HasChange("node_version") {
437 | update.NodeVersion = d.Get("node_version").(string)
438 | }
439 |
440 | err := client.Project.Update(d.Id(), update, d.Get("team_id").(string))
441 | if err != nil {
442 | return diag.FromErr(err)
443 | }
444 |
445 | if d.HasChange("domain") {
446 | rawOldDomains, rawNewDomains := d.GetChange("domain")
447 |
448 | var (
449 | oldDomains []string
450 | newDomains []string
451 | )
452 |
453 | for _, d := range rawNewDomains.([]interface{}) {
454 | newDomains = append(newDomains, d.(map[string]interface{})["name"].(string))
455 | }
456 |
457 | for _, d := range rawOldDomains.([]interface{}) {
458 | oldDomains = append(oldDomains, d.(map[string]interface{})["name"].(string))
459 | }
460 |
461 | toAdd := util.Difference(newDomains, oldDomains)
462 | toRemove := util.Difference(oldDomains, newDomains)
463 |
464 | for _, dom := range toAdd {
465 | err := client.Project.AddDomain(d.Id(), projectApi.Domain{Name: dom}, d.Get("team_id").(string))
466 | if err != nil {
467 | return diag.FromErr(err)
468 | }
469 | }
470 |
471 | for _, dom := range toRemove {
472 | err := client.Project.RemoveDomain(d.Id(), dom, d.Get("team_id").(string))
473 | if err != nil {
474 | return diag.FromErr(err)
475 | }
476 | }
477 | }
478 |
479 | return resourceProjectRead(ctx, d, meta)
480 | }
481 |
482 | func resourceProjectDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
483 |
484 | client := meta.(*vercel.Client)
485 | err := client.Project.Delete(d.Id(), d.Get("team_id").(string))
486 | if err != nil {
487 | return diag.FromErr(err)
488 | }
489 | return diag.Diagnostics{}
490 | }
491 |
--------------------------------------------------------------------------------
/internal/provider/resource_project_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "testing"
8 |
9 | "github.com/chronark/terraform-provider-vercel/pkg/util"
10 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
11 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/project"
12 | "github.com/hashicorp/go-uuid"
13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
14 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
15 | )
16 |
17 | var (
18 | // The domain to add to the project. The TLD should be authorized in your test Vercel account.
19 | domainAlias = util.GetEnv("DOMAIN_ALIAS", "domain-alias.chronark.com")
20 |
21 | // The repository to test project creation with. Your Vercel account should have access to this.
22 | repository = util.GetEnv("REPOSITORY", "chronark/terraform-provider-vercel")
23 | )
24 |
25 | func TestAccVercelProject(t *testing.T) {
26 |
27 | projectName, _ := uuid.GenerateUUID()
28 | updatedProjectName, _ := uuid.GenerateUUID()
29 | var (
30 |
31 | // Holds the project fetched from vercel when we create it at the beginning
32 | actualProjectAfterCreation project.Project
33 |
34 | // Renaming or changing a variable should not result in the recreation of the project, so we expect to have the same id.
35 | actualProjectAfterUpdate project.Project
36 |
37 | // Used everywhere else
38 | actualProject project.Project
39 | )
40 | resource.Test(t, resource.TestCase{
41 | PreCheck: func() { testAccPreCheck(t) },
42 | ProviderFactories: providerFactories,
43 | CheckDestroy: testAccCheckVercelProjectDestroy(projectName),
44 | Steps: []resource.TestStep{
45 | {
46 | Config: testAccCheckVercelProjectConfig(projectName),
47 | Check: resource.ComposeTestCheckFunc(
48 | testAccCheckProjectStateHasValues(
49 | "vercel_project.new", project.Project{Name: projectName},
50 | ),
51 | testAccCheckVercelProjectExists("vercel_project.new", &actualProjectAfterCreation),
52 | testAccCheckActualProjectHasValues(&actualProjectAfterCreation, &project.Project{Name: projectName}),
53 | ),
54 | },
55 | {
56 | Config: testAccCheckVercelProjectConfig(updatedProjectName),
57 | Check: resource.ComposeTestCheckFunc(
58 | testAccCheckVercelProjectExists("vercel_project.new", &actualProjectAfterUpdate),
59 | testAccCheckProjectStateHasValues(
60 | "vercel_project.new", project.Project{Name: updatedProjectName},
61 | ),
62 | testAccCheckActualProjectHasValues(&actualProjectAfterUpdate, &project.Project{Name: updatedProjectName}),
63 | testAccCheckProjectWasNotRecreated(&actualProjectAfterCreation, &actualProjectAfterUpdate),
64 | ),
65 | },
66 | {
67 | Config: testAccCheckVercelProjectConfigWithDomain(updatedProjectName),
68 | Check: resource.ComposeTestCheckFunc(
69 | testAccCheckVercelProjectExists("vercel_project.new", &actualProjectAfterUpdate),
70 | testAccCheckProjectStateHasDomain("vercel_project.new"),
71 | testAccCheckActualProjectHasDomain(&actualProjectAfterUpdate),
72 | ),
73 | },
74 | {
75 | Config: testAccCheckVercelProjectConfigWithOverridenCommands(projectName),
76 | Check: resource.ComposeTestCheckFunc(
77 | testAccCheckVercelProjectExists("vercel_project.new", &actualProject),
78 | testAccCheckProjectStateHasValues(
79 | "vercel_project.new", project.Project{
80 | Name: projectName,
81 | InstallCommand: "echo install",
82 | BuildCommand: "echo build",
83 | DevCommand: "echo dev",
84 | OutputDirectory: "out",
85 | },
86 | ),
87 | testAccCheckActualProjectHasValues(&actualProject, &project.Project{
88 | Name: projectName,
89 | InstallCommand: "echo install",
90 | BuildCommand: "echo build",
91 | DevCommand: "echo dev",
92 | OutputDirectory: "out",
93 | },
94 | ),
95 | ),
96 | },
97 | },
98 | })
99 | }
100 |
101 | func TestAccVercelProject_import(t *testing.T) {
102 |
103 | projectName, _ := uuid.GenerateUUID()
104 | var (
105 | // Used everywhere else
106 | actualProject project.Project
107 | )
108 | resource.Test(t, resource.TestCase{
109 | PreCheck: func() { testAccPreCheck(t) },
110 | ProviderFactories: providerFactories,
111 | CheckDestroy: testAccCheckVercelProjectDestroy(projectName),
112 | Steps: []resource.TestStep{
113 | {
114 | Config: testAccCheckVercelProjectConfig(projectName),
115 | Check: resource.ComposeTestCheckFunc(
116 | testAccCheckProjectStateHasValues(
117 | "vercel_project.new", project.Project{Name: projectName},
118 | ),
119 | testAccCheckVercelProjectExists("vercel_project.new", &actualProject),
120 | testAccVercelProjectImport(&actualProject),
121 | ),
122 | },
123 | },
124 | })
125 | }
126 |
127 | // Combines multiple `resource.TestCheckResourceAttr` calls
128 | func testAccCheckProjectStateHasValues(name string, want project.Project) resource.TestCheckFunc {
129 | return func(s *terraform.State) error {
130 | tests := []resource.TestCheckFunc{
131 |
132 | resource.TestCheckResourceAttr(
133 | name, "install_command", want.InstallCommand),
134 | resource.TestCheckResourceAttr(
135 | name, "build_command", want.BuildCommand),
136 | resource.TestCheckResourceAttr(
137 | name, "dev_command", want.DevCommand),
138 | resource.TestCheckResourceAttr(
139 | name, "output_directory", want.OutputDirectory),
140 | }
141 |
142 | for _, test := range tests {
143 | err := test(s)
144 | if err != nil {
145 | return err
146 | }
147 | }
148 | return nil
149 | }
150 | }
151 |
152 | func testAccCheckProjectStateHasDomain(n string) resource.TestCheckFunc {
153 | return func(s *terraform.State) error {
154 | rs := s.RootModule().Resources[n]
155 |
156 | actual := rs.Primary.Attributes["alias.1"]
157 | want := domainAlias
158 |
159 | if actual != want {
160 | return fmt.Errorf("domain alias does not match, expected: %s, got: %s", want, actual)
161 | }
162 |
163 | return nil
164 | }
165 | }
166 |
167 | // Chaning the name or value of a project should not result in a recreation meaning the UID assigned by vercel
168 | // should not have changed.
169 | func testAccCheckProjectWasNotRecreated(s1, s2 *project.Project) resource.TestCheckFunc {
170 | return func(s *terraform.State) error {
171 | if s1.ID != s2.ID {
172 | return fmt.Errorf("Expected same IDs but they are not the same.")
173 | }
174 | return nil
175 | }
176 | }
177 |
178 | func testAccCheckActualProjectHasValues(actual *project.Project, want *project.Project) resource.TestCheckFunc {
179 | return func(s *terraform.State) error {
180 | if actual.Name != want.Name {
181 | return fmt.Errorf("name does not match, expected: %s, got: %s", want.Name, actual.Name)
182 | }
183 | if actual.ID == "" {
184 | return fmt.Errorf("ID is empty")
185 | }
186 |
187 | if actual.InstallCommand != want.InstallCommand {
188 | return fmt.Errorf("install_command does not match: expected: %s, got: %s", want.InstallCommand, actual.InstallCommand)
189 | }
190 | if actual.BuildCommand != want.BuildCommand {
191 | return fmt.Errorf("build_command does not match: expected: %s, got: %s", want.BuildCommand, actual.BuildCommand)
192 | }
193 | if actual.DevCommand != want.DevCommand {
194 | return fmt.Errorf("dev_command does not match: expected: %s, got: %s", want.DevCommand, actual.DevCommand)
195 | }
196 | if actual.OutputDirectory != want.OutputDirectory {
197 | return fmt.Errorf("output_directory does not match: expected: %s, got: %s", want.OutputDirectory, actual.OutputDirectory)
198 | }
199 |
200 | return nil
201 | }
202 | }
203 |
204 | func testAccCheckActualProjectHasDomain(actual *project.Project) resource.TestCheckFunc {
205 | return func(s *terraform.State) error {
206 | want := domainAlias
207 |
208 | if actual.Alias[1].Domain != want {
209 | return fmt.Errorf("name does not match, expected: %s, got: %s", want, actual.Alias[1].Domain)
210 | }
211 |
212 | return nil
213 | }
214 | }
215 |
216 | // Test whether the project was destroyed properly and finishes the job if necessary
217 | func testAccCheckVercelProjectDestroy(name string) resource.TestCheckFunc {
218 | return func(s *terraform.State) error {
219 | client := vercel.New(os.Getenv("VERCEL_TOKEN"))
220 |
221 | for _, rs := range s.RootModule().Resources {
222 | if rs.Type != name {
223 | continue
224 | }
225 |
226 | project, err := client.Project.Read(rs.Primary.ID, "")
227 | if err == nil {
228 | message := "Project was not deleted from vercel during terraform destroy."
229 | deleteErr := client.Project.Delete(project.Name, "")
230 | if deleteErr != nil {
231 | return fmt.Errorf(message+" Automated removal did not succeed. Please manually remove @%s. Error: %w", project.Name, err)
232 | }
233 | return fmt.Errorf(message + " It was removed now.")
234 | }
235 |
236 | }
237 | return nil
238 | }
239 |
240 | }
241 |
242 | func testAccCheckVercelProjectConfig(name string) string {
243 | return fmt.Sprintf(`
244 | resource "vercel_project" "new" {
245 | name = "%s"
246 | git_repository {
247 | type = "github"
248 | repo = "%s"
249 | }
250 | }
251 | `, name, repository)
252 | }
253 |
254 | func testAccCheckVercelProjectConfigWithDomain(name string) string {
255 | return fmt.Sprintf(`
256 | resource "vercel_project" "new" {
257 | name = "%s"
258 |
259 | git_repository {
260 | type = "github"
261 | repo = "%s"
262 | }
263 |
264 | domain {
265 | git_branch = "main"
266 | name = "%s"
267 | }
268 | }
269 | `, name, repository, domainAlias)
270 | }
271 |
272 | func testAccCheckVercelProjectConfigWithOverridenCommands(name string) string {
273 | return fmt.Sprintf(`
274 | resource "vercel_project" "new" {
275 | name = "%s"
276 | git_repository {
277 | type = "github"
278 | repo = "%s"
279 | }
280 | install_command = "echo install"
281 | build_command = "echo build"
282 | dev_command = "echo dev"
283 | output_directory = "out"
284 | }
285 | `, name, repository)
286 | }
287 |
288 | func testAccCheckVercelProjectExists(n string, actual *project.Project) resource.TestCheckFunc {
289 | return func(s *terraform.State) error {
290 | rs, ok := s.RootModule().Resources[n]
291 |
292 | if !ok {
293 | return fmt.Errorf("Not found: %s in %+v", n, s.RootModule().Resources)
294 | }
295 |
296 | if rs.Primary.ID == "" {
297 | return fmt.Errorf("No project set")
298 | }
299 |
300 | project, err := vercel.New(os.Getenv("VERCEL_TOKEN")).Project.Read(rs.Primary.ID, "")
301 | if err != nil {
302 | return err
303 | }
304 | *actual = project
305 | return nil
306 | }
307 | }
308 |
309 | func testAccVercelProjectImport(source *project.Project) resource.TestCheckFunc {
310 | return func(s *terraform.State) error {
311 | data := resourceProject().Data(nil)
312 | data.SetId(source.Name)
313 | ds, err := resourceProject().Importer.StateContext(context.Background(), data, vercel.New(os.Getenv("VERCEL_TOKEN")))
314 | if err != nil {
315 | return err
316 | }
317 | if len(ds) != 1 {
318 | return fmt.Errorf("Expected 1 instance state from importer function. Got %d", len(ds))
319 | }
320 |
321 | if ds[0].Id() != source.Name {
322 | return fmt.Errorf("Imported project ID. Expected '%s'. Actual '%s'.", source.Name, ds[0].Id())
323 | }
324 | return nil
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/internal/provider/resource_secret.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
7 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/secret"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10 | )
11 |
12 | func resourceSecret() *schema.Resource {
13 | return &schema.Resource{
14 | Description: "https://vercel.com/docs/api#endpoints/secrets",
15 |
16 | CreateContext: resourceSecretCreate,
17 | ReadContext: resourceSecretRead,
18 | DeleteContext: resourceSecretDelete,
19 |
20 | Importer: &schema.ResourceImporter{
21 | StateContext: schema.ImportStatePassthroughContext,
22 | },
23 |
24 | Schema: map[string]*schema.Schema{
25 | "id": {
26 | Description: "The unique identifier of the secret.",
27 | Type: schema.TypeString,
28 | Computed: true,
29 | },
30 | "team_id": {
31 | Description: "By default, you can access resources contained within your own user account. To access resources owned by a team, you can pass in the team ID",
32 | Type: schema.TypeString,
33 | Optional: true,
34 | ForceNew: true,
35 | Default: "",
36 | },
37 | "name": {
38 | Description: "The name of the secret.",
39 | Type: schema.TypeString,
40 | Required: true,
41 | ForceNew: true,
42 | },
43 | "value": {
44 | Description: "The value of the new secret.",
45 | Type: schema.TypeString,
46 | Required: true,
47 | Sensitive: true,
48 | ForceNew: true,
49 | },
50 |
51 | "user_id": {
52 | Description: "The unique identifier of the user who created the secret.",
53 | Type: schema.TypeString,
54 | Computed: true,
55 | },
56 | "created_at": {
57 | Description: "A number containing the date when the variable was created in milliseconds.",
58 | Type: schema.TypeInt,
59 | Computed: true,
60 | },
61 | },
62 | }
63 | }
64 |
65 | func resourceSecretCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
66 |
67 | client := meta.(*vercel.Client)
68 |
69 | payload := secret.CreateSecret{
70 | Name: d.Get("name").(string),
71 | Value: d.Get("value").(string),
72 | }
73 |
74 | secretID, err := client.Secret.Create(payload)
75 | if err != nil {
76 | return diag.FromErr(err)
77 | }
78 |
79 | d.SetId(secretID)
80 |
81 | return resourceSecretRead(ctx, d, meta)
82 | }
83 |
84 | func resourceSecretRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
85 | client := meta.(*vercel.Client)
86 |
87 | id := d.Id()
88 |
89 | secret, err := client.Secret.Read(id, d.Get("team_id").(string))
90 | if err != nil {
91 | return diag.FromErr(err)
92 | }
93 |
94 | err = d.Set("name", secret.Name)
95 | if err != nil {
96 | return diag.FromErr(err)
97 | }
98 | err = d.Set("team_id", secret.TeamID)
99 | if err != nil {
100 | return diag.FromErr(err)
101 | }
102 | err = d.Set("user_id", secret.UserID)
103 | if err != nil {
104 | return diag.FromErr(err)
105 | }
106 | err = d.Set("created_at", secret.CreatedAt)
107 | if err != nil {
108 | return diag.FromErr(err)
109 | }
110 |
111 | return diag.Diagnostics{}
112 | }
113 |
114 | func resourceSecretDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
115 |
116 | client := meta.(*vercel.Client)
117 |
118 | err := client.Secret.Delete(d.Get("name").(string), d.Get("team_id").(string))
119 | if err != nil {
120 | return diag.FromErr(err)
121 | }
122 | d.SetId("")
123 | return diag.Diagnostics{}
124 | }
125 |
--------------------------------------------------------------------------------
/internal/provider/resource_secret_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "testing"
8 |
9 | "github.com/chronark/terraform-provider-vercel/pkg/vercel"
10 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/secret"
11 | "github.com/hashicorp/go-uuid"
12 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
13 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
14 | )
15 |
16 | func TestAccVercelSecret(t *testing.T) {
17 |
18 | secretName, _ := uuid.GenerateUUID()
19 | secretValue, _ := uuid.GenerateUUID()
20 | updatedSecretValue, _ := uuid.GenerateUUID()
21 | var (
22 |
23 | // Holds the secret fetched from vercel when we create it at the beginning
24 | actualSecretAfterCreation secret.Secret
25 |
26 | // Renaming or chaning a variable results in the recreation of the secret, so we expect this value to have a different id.
27 | actualSecretAfterUpdate secret.Secret
28 | )
29 | resource.Test(t, resource.TestCase{
30 | PreCheck: func() { testAccPreCheck(t) },
31 | ProviderFactories: providerFactories,
32 | CheckDestroy: testAccCheckVercelSecretDestroy(secretName),
33 | Steps: []resource.TestStep{
34 | {
35 | Config: testAccCheckVercelSecretConfig(secretName, secretValue),
36 | Check: resource.ComposeTestCheckFunc(
37 | testAccCheckSecretStateHasValues(
38 | "vercel_secret.new", secret.CreateSecret{Name: secretName, Value: secretValue},
39 | ),
40 | testAccCheckVercelSecretExists("vercel_secret.new", &actualSecretAfterCreation),
41 | testAccCheckActualSecretHasValues(&actualSecretAfterCreation, &secret.Secret{Name: secretName}),
42 | ),
43 | },
44 | {
45 | Config: testAccCheckVercelSecretConfig(secretName, updatedSecretValue),
46 | Check: resource.ComposeTestCheckFunc(
47 | testAccCheckVercelSecretExists("vercel_secret.new", &actualSecretAfterUpdate),
48 | testAccCheckSecretStateHasValues(
49 | "vercel_secret.new", secret.CreateSecret{Name: secretName, Value: updatedSecretValue},
50 | ),
51 | testAccCheckActualSecretHasValues(&actualSecretAfterUpdate, &secret.Secret{Name: secretName}),
52 | testAccCheckSecretWasRecreated(&actualSecretAfterCreation, &actualSecretAfterUpdate),
53 | ),
54 | },
55 | },
56 | })
57 | }
58 |
59 | func TestAccVercelSecret_import(t *testing.T) {
60 |
61 | secretName, _ := uuid.GenerateUUID()
62 | secretValue, _ := uuid.GenerateUUID()
63 | var (
64 | // Holds the secret fetched from vercel when we create it at the beginning
65 | actualSecretAfterCreation secret.Secret
66 | )
67 | resource.Test(t, resource.TestCase{
68 | PreCheck: func() { testAccPreCheck(t) },
69 | ProviderFactories: providerFactories,
70 | CheckDestroy: testAccCheckVercelSecretDestroy(secretName),
71 | Steps: []resource.TestStep{
72 | {
73 | Config: testAccCheckVercelSecretConfig(secretName, secretValue),
74 | Check: resource.ComposeTestCheckFunc(
75 | testAccCheckVercelSecretExists("vercel_secret.new", &actualSecretAfterCreation),
76 | testAccVercelSecretImport(&actualSecretAfterCreation),
77 | ),
78 | },
79 | },
80 | })
81 | }
82 |
83 | // Combines multiple `resource.TestCheckResourceAttr` calls
84 | func testAccCheckSecretStateHasValues(name string, want secret.CreateSecret) resource.TestCheckFunc {
85 | return func(s *terraform.State) error {
86 | tests := []resource.TestCheckFunc{
87 | resource.TestCheckResourceAttr(
88 | name, "name", want.Name),
89 | resource.TestCheckResourceAttr(
90 | name, "value", want.Value),
91 | }
92 |
93 | for _, test := range tests {
94 | err := test(s)
95 | if err != nil {
96 | return err
97 | }
98 | }
99 | return nil
100 | }
101 | }
102 |
103 | // Chaning the name or value of a secret results in a recreation meaning the UID assigned by vercel
104 | // should have changed.
105 | func testAccCheckSecretWasRecreated(s1, s2 *secret.Secret) resource.TestCheckFunc {
106 | return func(s *terraform.State) error {
107 | if s1.UID == s2.UID {
108 | return fmt.Errorf("Expected different UIDs but they are the same.")
109 | }
110 | return nil
111 | }
112 | }
113 |
114 | func testAccCheckActualSecretHasValues(actual *secret.Secret, want *secret.Secret) resource.TestCheckFunc {
115 | return func(s *terraform.State) error {
116 | if actual.Name != want.Name {
117 | return fmt.Errorf("name is not correct, expected: %s, got: %s", want.Name, actual.Name)
118 | }
119 | if actual.UID == "" {
120 | return fmt.Errorf("UID is empty")
121 | }
122 |
123 | return nil
124 | }
125 | }
126 |
127 | // Test whether the secret was destroyed properly and finishes the job if necessary
128 | func testAccCheckVercelSecretDestroy(name string) resource.TestCheckFunc {
129 | return func(s *terraform.State) error {
130 | client := vercel.New(os.Getenv("VERCEL_TOKEN"))
131 |
132 | for _, rs := range s.RootModule().Resources {
133 | if rs.Type != name {
134 | continue
135 | }
136 |
137 | secret, err := client.Secret.Read(rs.Primary.ID, "")
138 | if err == nil {
139 | message := "Secret was not deleted from vercel during terraform destroy."
140 | deleteErr := client.Secret.Delete(secret.Name, "")
141 | if deleteErr != nil {
142 | return fmt.Errorf(message+" Automated removal did not succeed. Please manually remove @%s. Error: %w", secret.Name, err)
143 | }
144 | return fmt.Errorf(message + " It was removed now.")
145 | }
146 |
147 | }
148 | return nil
149 | }
150 |
151 | }
152 | func testAccCheckVercelSecretConfig(name string, value string) string {
153 | return fmt.Sprintf(`
154 | resource "vercel_secret" "new" {
155 | name = "%s"
156 | value = "%s"
157 | }
158 | `, name, value)
159 | }
160 |
161 | func testAccCheckVercelSecretExists(n string, actual *secret.Secret) resource.TestCheckFunc {
162 | return func(s *terraform.State) error {
163 | rs, ok := s.RootModule().Resources[n]
164 |
165 | if !ok {
166 | return fmt.Errorf("Not found: %s in %+v", n, s.RootModule().Resources)
167 | }
168 |
169 | if rs.Primary.ID == "" {
170 | return fmt.Errorf("No secret set")
171 | }
172 |
173 | secret, err := vercel.New(os.Getenv("VERCEL_TOKEN")).Secret.Read(rs.Primary.ID, "")
174 | if err != nil {
175 | return err
176 | }
177 | *actual = secret
178 | return nil
179 | }
180 | }
181 |
182 | func testAccVercelSecretImport(source *secret.Secret) resource.TestCheckFunc {
183 | return func(s *terraform.State) error {
184 | data := resourceSecret().Data(nil)
185 | data.SetId(source.UID)
186 | ds, err := resourceSecret().Importer.StateContext(context.Background(), data, nil)
187 | if err != nil {
188 | return err
189 | }
190 | if len(ds) != 1 {
191 | return fmt.Errorf("Expected 1 instance state from importer function. Got %d", len(ds))
192 | }
193 |
194 | if ds[0].Id() != source.UID {
195 | return fmt.Errorf("Imported secret ID. Expected '%s'. Actual '%s'.", source.UID, ds[0].Id())
196 | }
197 | return nil
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "log"
7 |
8 | "github.com/chronark/terraform-provider-vercel/internal/provider"
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
10 | )
11 |
12 | // Run "go generate" to format example terraform files and generate the docs for the registry/website
13 |
14 | // If you do not have terraform installed, you can remove the formatting command, but its suggested to
15 | // ensure the documentation is formatted properly.
16 | //go:generate terraform fmt -recursive ./examples/
17 |
18 | // Run the docs generation tool, check its repository for more information on how it works and how docs
19 | // can be customized.
20 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs
21 |
22 | var (
23 | // these will be set by the goreleaser configuration
24 | // to appropriate values for the compiled binary
25 | version string = "dev"
26 |
27 | // goreleaser can also pass the specific commit if you want
28 | // commit string = ""
29 | )
30 |
31 | func main() {
32 | var debugMode bool
33 |
34 | flag.BoolVar(&debugMode, "debug", false, "set to true to run the provider with support for debuggers like delve")
35 | flag.Parse()
36 |
37 | opts := &plugin.ServeOpts{ProviderFunc: provider.New(version)}
38 |
39 | if debugMode {
40 | err := plugin.Debug(context.Background(), "registry.terraform.io/chronark/vercel", opts)
41 | if err != nil {
42 | log.Fatal(err.Error())
43 | }
44 | return
45 | }
46 |
47 | plugin.Serve(opts)
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/util/env.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import "os"
4 |
5 | func GetEnv(key string, fallback string) string {
6 | value := os.Getenv(key)
7 | if len(value) == 0 {
8 | return fallback
9 | }
10 | return value
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | // Difference returns a slice containing all of the elements of 'a' that aren't present in 'b'.
4 | //
5 | // See https://siongui.github.io/2018/03/14/go-set-difference-of-two-arrays/
6 | func Difference(a, b []string) (diff []string) {
7 | m := make(map[string]bool)
8 |
9 | for _, item := range b {
10 | m[item] = true
11 | }
12 |
13 | for _, item := range a {
14 | if _, ok := m[item]; !ok {
15 | diff = append(diff, item)
16 | }
17 | }
18 |
19 | return diff
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/util/util_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestDifference(t *testing.T) {
9 | tests := []struct {
10 | input1 []string
11 | input2 []string
12 | expected []string
13 | }{
14 | {[]string{"a", "b", "c"}, []string{"a", "b", "d"}, []string{"c"}},
15 | {[]string{"a", "b", "d"}, []string{"a", "b", "c"}, []string{"d"}},
16 | {[]string{"a", "b", "c"}, []string{"d", "e", "f"}, []string{"a", "b", "c"}},
17 | {[]string{"a", "b", "c"}, []string{}, []string{"a", "b", "c"}},
18 | }
19 |
20 | for _, test := range tests {
21 | actual := Difference(test.input1, test.input2)
22 |
23 | if !reflect.DeepEqual(test.expected, actual) {
24 | t.Errorf("expected: %v, received: %v", test.expected, actual)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/vercel/alias/alias.go:
--------------------------------------------------------------------------------
1 | package alias
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net/http"
8 |
9 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/httpApi"
10 | )
11 |
12 | type CreateOrUpdateAlias struct {
13 | Domain string `json:"domain"`
14 | Redirect string `json:"redirect"`
15 | }
16 |
17 | type Alias struct {
18 | CreatedAt int64 `json:"createdAt"`
19 | Domain string `json:"domain"`
20 | Target string `json:"target"`
21 | ConfiguredBy string `json:"configuredBy"`
22 | ConfiguredChangedAt int64 `json:"configuredChangedAt"`
23 | }
24 |
25 | type Handler struct {
26 | Api httpApi.API
27 | }
28 |
29 | func (h *Handler) Create(projectId string, alias CreateOrUpdateAlias, teamId string) error {
30 | url := fmt.Sprintf("/v1/projects/%s/alias", projectId)
31 | if teamId != "" {
32 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
33 | }
34 | res, err := h.Api.Request(http.MethodPost, url, alias)
35 | if err != nil {
36 | return err
37 | }
38 | defer res.Body.Close()
39 |
40 | var createdAliases []Alias
41 | err = json.NewDecoder(res.Body).Decode(&createdAliases)
42 | if err != nil {
43 | return err
44 | }
45 | log.Printf("%+v\n\n", createdAliases)
46 |
47 | return nil
48 | }
49 |
50 | func (h *Handler) Update(projectId string, alias CreateOrUpdateAlias, teamId string) error {
51 | url := fmt.Sprintf("/v1/projects/%s/alias", projectId)
52 | if teamId != "" {
53 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
54 | }
55 |
56 | res, err := h.Api.Request("PATCH", url, alias)
57 | if err != nil {
58 | return fmt.Errorf("Unable to update env: %w", err)
59 | }
60 | defer res.Body.Close()
61 | return nil
62 | }
63 | func (h *Handler) Delete(projectId, envKey string, teamId string) error {
64 | url := fmt.Sprintf("/v1/projects/%s/alias?domain", projectId)
65 | if teamId != "" {
66 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
67 | }
68 | res, err := h.Api.Request("DELETE", url, nil)
69 | if err != nil {
70 | return fmt.Errorf("Unable to delete domain: %w", err)
71 | }
72 | defer res.Body.Close()
73 | return nil
74 | }
75 |
--------------------------------------------------------------------------------
/pkg/vercel/client.go:
--------------------------------------------------------------------------------
1 | package vercel
2 |
3 | import (
4 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/env"
5 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/httpApi"
6 |
7 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/alias"
8 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/dns"
9 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/domain"
10 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/project"
11 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/secret"
12 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/team"
13 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/user"
14 | )
15 |
16 | type Client struct {
17 | Project *project.ProjectHandler
18 | User *user.UserHandler
19 | Env *env.Handler
20 | Secret *secret.Handler
21 | Team *team.Handler
22 | Alias *alias.Handler
23 | Domain *domain.Handler
24 | DNS *dns.Handler
25 | }
26 |
27 | func New(token string) *Client {
28 | api := httpApi.New(token)
29 |
30 | return &Client{
31 | Project: &project.ProjectHandler{
32 | Api: api,
33 | },
34 | User: &user.UserHandler{
35 | Api: api,
36 | },
37 | Env: &env.Handler{Api: api},
38 | Secret: &secret.Handler{Api: api},
39 | Team: &team.Handler{Api: api},
40 | Alias: &alias.Handler{Api: api},
41 | Domain: &domain.Handler{Api: api},
42 | DNS: &dns.Handler{Api: api},
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/vercel/dns/dns.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/httpApi"
9 | )
10 |
11 | type CreateRecord struct {
12 | // The type of record, it could be any valid DNS record.
13 | // See https://vercel.com/docs/api#endpoints/dns for details
14 | Type string `json:"type"`
15 |
16 | // A subdomain name or an empty string for the root domain.
17 | Name string `json:"name"`
18 |
19 | // The record value.
20 | Value string `json:"value"`
21 |
22 | // The TTL value. Must be a number between 60 and 2147483647. Default value is 60.
23 | TTL int `json:"ttl"`
24 | }
25 |
26 | type Record struct {
27 | // The unique ID of the DNS record. Always prepended with rec_.
28 | Id string `json:"id"`
29 |
30 | // The type of record, it could be any valid DNS record.
31 | // See https://vercel.com/docs/api#endpoints/dns for details
32 | Type string `json:"type"`
33 |
34 | // A subdomain name or an empty string for the root domain.
35 | Name string `json:"name"`
36 |
37 | // The record value.
38 | Value string `json:"value"`
39 |
40 | // The ID of the user who created the record or system if the record is an automatic record.
41 | Creator string `json:"creator"`
42 |
43 | // The date when the record was created.
44 | Created int `json:"created"`
45 |
46 | // The date when the record was updated.
47 | Updated int `json:"updated"`
48 |
49 | // The date when the record was created in milliseconds since the UNIX epoch.
50 | CreatedAt int `json:"createdAt"`
51 |
52 | // The date when the record was updated in milliseconds since the UNIX epoch.
53 | UpdatedAt int `json:"updatedAt"`
54 | }
55 |
56 | type Handler struct {
57 | Api httpApi.API
58 | }
59 |
60 | func (h *Handler) Create(domain string, record CreateRecord, teamId string) (string, error) {
61 | url := fmt.Sprintf("/v2/domains/%s/records", domain)
62 | if teamId != "" {
63 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
64 | }
65 | res, err := h.Api.Request(http.MethodPost, url, record)
66 | if err != nil {
67 | return "", err
68 | }
69 | defer res.Body.Close()
70 |
71 | type CreateResponse struct {
72 | UID string `json:"uid"`
73 | }
74 |
75 | var createResponse CreateResponse
76 |
77 | err = json.NewDecoder(res.Body).Decode(&createResponse)
78 | if err != nil {
79 | return "", err
80 | }
81 |
82 | return createResponse.UID, nil
83 | }
84 |
85 | func (h *Handler) Read(domain, recordId, teamId string) (Record, error) {
86 | url := fmt.Sprintf("/v2/domains/%s/records?limit=1000", domain)
87 | if teamId != "" {
88 | url = fmt.Sprintf("%s&teamId=%s", url, teamId)
89 | }
90 | res, err := h.Api.Request("GET", url, nil)
91 | if err != nil {
92 | return Record{}, fmt.Errorf("Unable to fetch dns records: %w", err)
93 | }
94 | defer res.Body.Close()
95 |
96 | type Response struct {
97 | Records []Record `json:"records"`
98 | }
99 | var response Response
100 | err = json.NewDecoder(res.Body).Decode(&response)
101 | if err != nil {
102 | return Record{}, err
103 | }
104 |
105 | for _, record := range response.Records {
106 | if record.Id == recordId {
107 | return record, nil
108 | }
109 | }
110 | return Record{}, fmt.Errorf("Record with id %s was not found", recordId)
111 |
112 | }
113 |
114 | func (h *Handler) Delete(domain, recordId string, teamId string) error {
115 | url := fmt.Sprintf("/v2/domains/%s/records/%s", domain, recordId)
116 | if teamId != "" {
117 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
118 | }
119 | res, err := h.Api.Request("DELETE", url, nil)
120 | if err != nil {
121 | return fmt.Errorf("Unable to delete dns record: %w", err)
122 | }
123 | defer res.Body.Close()
124 | return nil
125 | }
126 |
--------------------------------------------------------------------------------
/pkg/vercel/domain/domain.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/httpApi"
7 | "net/http"
8 | )
9 |
10 | type CreateDomain struct {
11 | Name string `json:"name"`
12 | }
13 | type Domain struct {
14 | ID string `json:"id"`
15 | Name string `json:"name"`
16 | ServiceType string `json:"serviceType"`
17 | NsVerifiedAt int64 `json:"nsVerifiedAt"`
18 | TxtVerifiedAt int64 `json:"txtVerifiedAt"`
19 | CdnEnabled bool `json:"cdnEnabled"`
20 | CreatedAt int64 `json:"createdAt"`
21 | ExpiresAt int64 `json:"expiresAt"`
22 | BoughtAt int64 `json:"boughtAt"`
23 | TransferredAt int64 `json:"transferredAt"`
24 | VerificationRecord string `json:"verificationRecord"`
25 | Verified bool `json:"verified"`
26 | Nameservers []string `json:"nameservers"`
27 | IntendedNameservers []string `json:"intendedNameservers"`
28 | Creator struct {
29 | ID string `json:"id"`
30 | Username string `json:"username"`
31 | Email string `json:"email"`
32 | } `json:"creator"`
33 | }
34 |
35 | type Handler struct {
36 | Api httpApi.API
37 | }
38 |
39 | func (h *Handler) Create(name string, teamId string) (string, error) {
40 | url := "/v5/domains"
41 | if teamId != "" {
42 | url = fmt.Sprintf("%s?teamId=%s", url, teamId)
43 | }
44 | res, err := h.Api.Request(http.MethodPost, url, CreateDomain{Name: name})
45 | if err != nil {
46 | return "", err
47 | }
48 | defer res.Body.Close()
49 |
50 | var createdDomain Domain
51 | err = json.NewDecoder(res.Body).Decode(&createdDomain)
52 | if err != nil {
53 | return "", nil
54 | }
55 |
56 | return createdDomain.ID, nil
57 | }
58 |
59 | // Read returns metadata about a domain
60 | func (h *Handler) Read(domainName string, teamId string) (domain Domain, err error) {
61 | url := fmt.Sprintf("/v5/domains/%s", domainName)
62 | if teamId != "" {
63 | url = fmt.Sprintf("%s?teamId=%s", url, teamId)
64 | }
65 |
66 | res, err := h.Api.Request("GET", url, nil)
67 | if err != nil {
68 | return Domain{}, fmt.Errorf("Unable to fetch domain from vercel: %w", err)
69 | }
70 | defer res.Body.Close()
71 |
72 | type GetDomainResponse struct {
73 | Domain Domain `json:"domain"`
74 | }
75 | getDomainResponse := GetDomainResponse{}
76 | err = json.NewDecoder(res.Body).Decode(&getDomainResponse)
77 | if err != nil {
78 | return Domain{}, fmt.Errorf("Unable to unmarshal domain response: %w", err)
79 | }
80 |
81 | return getDomainResponse.Domain, nil
82 | }
83 |
84 | func (h *Handler) Delete(domainName string, teamId string) error {
85 | url := fmt.Sprintf("/v5/domains/%s", domainName)
86 | if teamId != "" {
87 | url = fmt.Sprintf("%s?teamId=%s", url, teamId)
88 | }
89 | res, err := h.Api.Request("DELETE", url, nil)
90 | if err != nil {
91 | return fmt.Errorf("Unable to delete domain: %w", err)
92 | }
93 | defer res.Body.Close()
94 | return nil
95 | }
96 |
--------------------------------------------------------------------------------
/pkg/vercel/env/env.go:
--------------------------------------------------------------------------------
1 | package env
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net/http"
8 |
9 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/httpApi"
10 | )
11 |
12 | type CreateOrUpdateEnv struct {
13 | // The type can be `plain`, `secret`, or `system`.
14 | Type string `json:"type"`
15 |
16 | // The name of the environment variable.
17 | Key string `json:"key"`
18 |
19 | // If the type is `plain`, a string representing the value of the environment variable.
20 | // If the type is `secret`, the secret ID of the secret attached to the environment variable.
21 | // If the type is `system`, the name of the System Environment Variable.
22 | Value string `json:"value"`
23 |
24 | // The target can be a list of `development`, `preview`, or `production`.
25 | Target []string `json:"target"`
26 | }
27 |
28 | type Env struct {
29 | Type string `json:"type"`
30 | ID string `json:"id"`
31 | Key string `json:"key"`
32 | Value string `json:"value"`
33 | Target []string `json:"target"`
34 | ConfigurationID interface{} `json:"configurationId"`
35 | UpdatedAt int64 `json:"updatedAt"`
36 | CreatedAt int64 `json:"createdAt"`
37 | }
38 |
39 | type Handler struct {
40 | Api httpApi.API
41 | }
42 |
43 | func (h *Handler) Create(projectID string, env CreateOrUpdateEnv, teamId string) (string, error) {
44 | url := fmt.Sprintf("/v6/projects/%s/env", projectID)
45 | if teamId != "" {
46 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
47 | }
48 | res, err := h.Api.Request(http.MethodPost, url, env)
49 | if err != nil {
50 | return "", err
51 | }
52 | defer res.Body.Close()
53 |
54 | var createdEnv Env
55 | err = json.NewDecoder(res.Body).Decode(&createdEnv)
56 | if err != nil {
57 | return "", nil
58 | }
59 | log.Printf("%+v\n\n", createdEnv)
60 |
61 | return createdEnv.ID, nil
62 | }
63 |
64 | // Read returns environment variables associated with a project
65 | func (h *Handler) Read(projectID string, teamId string) (envs []Env, err error) {
66 | url := fmt.Sprintf("/v6/projects/%s/env", projectID)
67 | if teamId != "" {
68 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
69 | }
70 | res, err := h.Api.Request("GET", url, nil)
71 | if err != nil {
72 | return []Env{}, fmt.Errorf("Unable to fetch environment variables from vercel: %w", err)
73 | }
74 | defer res.Body.Close()
75 |
76 | // EnvResponse is only a subset of available data but all we care about
77 | // See https://vercel.com/docs/api#endpoints/projects/get-project-environment-variables
78 | type EnvResponse struct {
79 | Envs []Env `json:"envs"`
80 | }
81 |
82 | var envResponse EnvResponse
83 |
84 | err = json.NewDecoder(res.Body).Decode(&envResponse)
85 | if err != nil {
86 | return []Env{}, fmt.Errorf("Unable to unmarshal environment variables response: %w", err)
87 | }
88 | log.Printf("%+v\n\n", envResponse)
89 | return envResponse.Envs, nil
90 | }
91 | func (h *Handler) Update(projectID string, envID string, env CreateOrUpdateEnv, teamId string) error {
92 | url := fmt.Sprintf("/v6/projects/%s/env/%s", projectID, envID)
93 | if teamId != "" {
94 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
95 | }
96 |
97 | res, err := h.Api.Request("PATCH", url, env)
98 | if err != nil {
99 | return fmt.Errorf("Unable to update env: %w", err)
100 | }
101 | defer res.Body.Close()
102 | return nil
103 | }
104 | func (h *Handler) Delete(projectID, envID string, teamId string) error {
105 | url := fmt.Sprintf("/v8/projects/%s/env/%s", projectID, envID)
106 | if teamId != "" {
107 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
108 | }
109 | res, err := h.Api.Request("DELETE", url, nil)
110 | if err != nil {
111 | return fmt.Errorf("Unable to delete env: %w", err)
112 | }
113 | defer res.Body.Close()
114 | return nil
115 | }
116 |
--------------------------------------------------------------------------------
/pkg/vercel/httpApi/httpApi.go:
--------------------------------------------------------------------------------
1 | package httpApi
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | )
10 |
11 | type API interface {
12 | Request(method string, path string, body interface{}) (*http.Response, error)
13 | }
14 |
15 | type Api struct {
16 | url string
17 | httpClient *http.Client
18 | userAgent string
19 | token string
20 | }
21 |
22 | func New(token string) API {
23 | return &Api{
24 | url: "https://api.vercel.com",
25 | httpClient: &http.Client{},
26 | userAgent: "chronark/terraform-provider-vercel",
27 | token: token,
28 | }
29 | }
30 |
31 | // https://vercel.com/docs/api#api-basics/errors
32 | type VercelError struct {
33 | Error struct {
34 | Code string `json:"code"`
35 | Message string `json:"message"`
36 | } `json:"error"`
37 | }
38 |
39 | func (c *Api) setHeaders(req *http.Request) {
40 | req.Header.Set("User-Agent", c.userAgent)
41 | req.Header.Set("Content-Type", "application/json")
42 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
43 | }
44 |
45 | func (c *Api) do(req *http.Request) (*http.Response, error) {
46 | c.setHeaders(req)
47 | res, err := c.httpClient.Do(req)
48 | if err != nil {
49 | return nil, fmt.Errorf("Unable to perform request: %w", err)
50 | }
51 | if res.StatusCode < 200 || res.StatusCode >= 300 {
52 | defer res.Body.Close()
53 |
54 | var x map[string]interface{}
55 | _ = json.NewDecoder(res.Body).Decode(&x)
56 |
57 | // var vercelError VercelError
58 | // err = json.NewDecoder(res.Body).Decode(&vercelError)
59 | // if err != nil {
60 | // return nil, fmt.Errorf("Error during http request: Unable to extract error: %w", err)
61 |
62 | // }
63 | return nil, fmt.Errorf("Error during http request: %+v", x)
64 | }
65 | return res, nil
66 | }
67 |
68 | func (c *Api) Request(method string, path string, body interface{}) (*http.Response, error) {
69 | var payload io.Reader = nil
70 | if body != nil {
71 | b, err := json.Marshal(body)
72 | if err != nil {
73 | return nil, err
74 | }
75 | payload = bytes.NewBuffer(b)
76 | }
77 |
78 | req, err := http.NewRequest(method, fmt.Sprintf("%s%s", c.url, path), payload)
79 | if err != nil {
80 | return nil, err
81 | }
82 | res, err := c.do(req)
83 |
84 | if err != nil {
85 | return &http.Response{}, fmt.Errorf("Unable to request resource: [%s] %s with payload {%+v}: %w", method, path, payload, err)
86 | }
87 | return res, nil
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/vercel/project/project.go:
--------------------------------------------------------------------------------
1 | package project
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/httpApi"
7 | )
8 |
9 | type ProjectHandler struct {
10 | Api httpApi.API
11 | }
12 |
13 | func (p *ProjectHandler) Create(project CreateProject, teamId string) (string, error) {
14 | url := "/v6/projects"
15 | if teamId != "" {
16 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
17 | }
18 |
19 | res, err := p.Api.Request("POST", url, project)
20 | if err != nil {
21 | return "", err
22 | }
23 | defer res.Body.Close()
24 |
25 | var createdProject Project
26 | err = json.NewDecoder(res.Body).Decode(&createdProject)
27 | if err != nil {
28 | return "", nil
29 | }
30 |
31 | return createdProject.ID, nil
32 | }
33 | func (p *ProjectHandler) Read(id string, teamId string) (project Project, err error) {
34 | url := fmt.Sprintf("/v1/projects/%s", id)
35 | if teamId != "" {
36 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
37 | }
38 |
39 | res, err := p.Api.Request("GET", url, nil)
40 | if err != nil {
41 | return Project{}, fmt.Errorf("Unable to fetch project from vercel: %w", err)
42 | }
43 | defer res.Body.Close()
44 |
45 | err = json.NewDecoder(res.Body).Decode(&project)
46 | if err != nil {
47 | return Project{}, fmt.Errorf("Unable to unmarshal project: %w", err)
48 | }
49 | return project, nil
50 | }
51 | func (p *ProjectHandler) Update(id string, project UpdateProject, teamId string) error {
52 | url := fmt.Sprintf("/v2/projects/%s", id)
53 | if teamId != "" {
54 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
55 | }
56 |
57 | res, err := p.Api.Request("PATCH", url, project)
58 | if err != nil {
59 | return fmt.Errorf("Unable to update project: %w", err)
60 | }
61 | defer res.Body.Close()
62 | return nil
63 | }
64 | func (p *ProjectHandler) Delete(id string, teamId string) error {
65 | url := fmt.Sprintf("/v1/projects/%s", id)
66 | if teamId != "" {
67 | url = fmt.Sprintf("%s?teamId=%s", url, teamId)
68 | }
69 |
70 | res, err := p.Api.Request("DELETE", url, nil)
71 | if err != nil {
72 | return fmt.Errorf("Unable to delete project: %w", err)
73 | }
74 | defer res.Body.Close()
75 | return nil
76 | }
77 |
78 | func (p *ProjectHandler) AddDomain(projectId string, domain Domain, teamId string) error {
79 | url := fmt.Sprintf("/v8/projects/%s/domains", projectId)
80 | if teamId != "" {
81 | url = fmt.Sprintf("%s?teamId=%s", url, teamId)
82 | }
83 | res, err := p.Api.Request("POST", url, domain)
84 | if err != nil {
85 | return err
86 | }
87 | defer res.Body.Close()
88 |
89 | if res.StatusCode != 200 {
90 | return fmt.Errorf("Unable to add domain")
91 | }
92 |
93 | return nil
94 | }
95 |
96 | func (p *ProjectHandler) RemoveDomain(projectId string, domain string, teamId string) error {
97 | url := fmt.Sprintf("/v8/projects/%s/domains/%s", projectId, domain)
98 | if teamId != "" {
99 | url = fmt.Sprintf("%s?teamId=%s", url, teamId)
100 | }
101 | res, err := p.Api.Request("DELETE", url, domain)
102 | if err != nil {
103 | return err
104 | }
105 | defer res.Body.Close()
106 |
107 | if res.StatusCode != 200 {
108 | return fmt.Errorf("Unable to add domain")
109 | }
110 |
111 | return nil
112 | }
113 |
--------------------------------------------------------------------------------
/pkg/vercel/project/types.go:
--------------------------------------------------------------------------------
1 | package project
2 |
3 | import "github.com/chronark/terraform-provider-vercel/pkg/vercel/env"
4 |
5 | type Domain struct {
6 | Name string `json:"name"`
7 | // Redirect string `json:"redirect"`
8 | // RedirectStatusCode int `json:"redirectStatusCode"`
9 | // GitBranch string `json:"gitBranch"`
10 | }
11 |
12 | // Project houses all the information vercel offers about a project via their api
13 | type Project struct {
14 | AccountID string `json:"accountId"`
15 | Alias []struct {
16 | ConfiguredBy string `json:"configuredBy"`
17 | ConfiguredChangedAt int64 `json:"configuredChangedAt"`
18 | CreatedAt int64 `json:"createdAt"`
19 | Deployment struct {
20 | Alias []string `json:"alias"`
21 | AliasAssigned int64 `json:"aliasAssigned"`
22 | Builds []interface{} `json:"builds"`
23 | CreatedAt int64 `json:"createdAt"`
24 | CreatedIn string `json:"createdIn"`
25 | Creator struct {
26 | UID string `json:"uid"`
27 | Email string `json:"email"`
28 | Username string `json:"username"`
29 | GithubLogin string `json:"githubLogin"`
30 | } `json:"creator"`
31 | DeploymentHostname string `json:"deploymentHostname"`
32 | Forced bool `json:"forced"`
33 | ID string `json:"id"`
34 | Meta struct {
35 | GithubCommitRef string `json:"githubCommitRef"`
36 | GithubRepo string `json:"githubRepo"`
37 | GithubOrg string `json:"githubOrg"`
38 | GithubCommitSha string `json:"githubCommitSha"`
39 | GithubRepoID string `json:"githubRepoId"`
40 | GithubCommitMessage string `json:"githubCommitMessage"`
41 | GithubCommitAuthorLogin string `json:"githubCommitAuthorLogin"`
42 | GithubDeployment string `json:"githubDeployment"`
43 | GithubCommitOrg string `json:"githubCommitOrg"`
44 | GithubCommitAuthorName string `json:"githubCommitAuthorName"`
45 | GithubCommitRepo string `json:"githubCommitRepo"`
46 | GithubCommitRepoID string `json:"githubCommitRepoId"`
47 | } `json:"meta"`
48 | Name string `json:"name"`
49 | Plan string `json:"plan"`
50 | Private bool `json:"private"`
51 | ReadyState string `json:"readyState"`
52 | Target string `json:"target"`
53 | TeamID string `json:"teamId"`
54 | Type string `json:"type"`
55 | URL string `json:"url"`
56 | UserID string `json:"userId"`
57 | WithCache bool `json:"withCache"`
58 | } `json:"deployment"`
59 | Domain string `json:"domain"`
60 | Environment string `json:"environment"`
61 | Target string `json:"target"`
62 | } `json:"alias"`
63 | Analytics struct {
64 | ID string `json:"id"`
65 | EnabledAt int64 `json:"enabledAt"`
66 | DisabledAt int64 `json:"disabledAt"`
67 | CanceledAt int64 `json:"canceledAt"`
68 | } `json:"analytics"`
69 | AutoExposeSystemEnvs bool `json:"autoExposeSystemEnvs"`
70 | BuildCommand string `json:"buildCommand"`
71 | CreatedAt int64 `json:"createdAt"`
72 | DevCommand string `json:"devCommand"`
73 | DirectoryListing bool `json:"directoryListing"`
74 | Env []env.Env `json:"env"`
75 | Framework string `json:"framework"`
76 | ID string `json:"id"`
77 | InstallCommand string `json:"installCommand"`
78 | Name string `json:"name"`
79 | NodeVersion string `json:"nodeVersion"`
80 | OutputDirectory string `json:"outputDirectory"`
81 | PublicSource bool `json:"publicSource"`
82 | RootDirectory string `json:"rootDirectory"`
83 | ServerlessFunctionRegion string `json:"serverlessFunctionRegion"`
84 | SourceFilesOutsideRootDirectory bool `json:"sourceFilesOutsideRootDirectory"`
85 | UpdatedAt int64 `json:"updatedAt"`
86 | Link struct {
87 | Type string `json:"type"`
88 | Repo string `json:"repo"`
89 | RepoID int `json:"repoId"`
90 | Org string `json:"org"`
91 | GitCredentialID string `json:"gitCredentialId"`
92 | CreatedAt int64 `json:"createdAt"`
93 | UpdatedAt int64 `json:"updatedAt"`
94 | Sourceless bool `json:"sourceless"`
95 | ProductionBranch string `json:"productionBranch"`
96 | DeployHooks []interface{} `json:"deployHooks"`
97 | ProjectName string `json:"projectName"`
98 | ProjectNamespace string `json:"projectNamespace"`
99 | Owner string `json:"owner"`
100 | Slug string `json:"slug"`
101 | } `json:"link"`
102 | LatestDeployments []struct {
103 | Alias []string `json:"alias"`
104 | AliasAssigned int64 `json:"aliasAssigned"`
105 | Builds []interface{} `json:"builds"`
106 | CreatedAt int64 `json:"createdAt"`
107 | CreatedIn string `json:"createdIn"`
108 | Creator struct {
109 | UID string `json:"uid"`
110 | Email string `json:"email"`
111 | Username string `json:"username"`
112 | GithubLogin string `json:"githubLogin"`
113 | } `json:"creator"`
114 | DeploymentHostname string `json:"deploymentHostname"`
115 | Forced bool `json:"forced"`
116 | ID string `json:"id"`
117 | Meta struct {
118 | GithubCommitRef string `json:"githubCommitRef"`
119 | GithubRepo string `json:"githubRepo"`
120 | GithubOrg string `json:"githubOrg"`
121 | GithubCommitSha string `json:"githubCommitSha"`
122 | GithubCommitAuthorLogin string `json:"githubCommitAuthorLogin"`
123 | GithubCommitMessage string `json:"githubCommitMessage"`
124 | GithubRepoID string `json:"githubRepoId"`
125 | GithubDeployment string `json:"githubDeployment"`
126 | GithubCommitOrg string `json:"githubCommitOrg"`
127 | GithubCommitAuthorName string `json:"githubCommitAuthorName"`
128 | GithubCommitRepo string `json:"githubCommitRepo"`
129 | GithubCommitRepoID string `json:"githubCommitRepoId"`
130 | } `json:"meta"`
131 | Name string `json:"name"`
132 | Plan string `json:"plan"`
133 | Private bool `json:"private"`
134 | ReadyState string `json:"readyState"`
135 | Target interface{} `json:"target"`
136 | TeamID string `json:"teamId"`
137 | Type string `json:"type"`
138 | URL string `json:"url"`
139 | UserID string `json:"userId"`
140 | WithCache bool `json:"withCache"`
141 | } `json:"latestDeployments"`
142 | Targets struct {
143 | Production struct {
144 | Alias []string `json:"alias"`
145 | AliasAssigned int64 `json:"aliasAssigned"`
146 | Builds []interface{} `json:"builds"`
147 | CreatedAt int64 `json:"createdAt"`
148 | CreatedIn string `json:"createdIn"`
149 | Creator struct {
150 | UID string `json:"uid"`
151 | Email string `json:"email"`
152 | Username string `json:"username"`
153 | GithubLogin string `json:"githubLogin"`
154 | } `json:"creator"`
155 | DeploymentHostname string `json:"deploymentHostname"`
156 | Forced bool `json:"forced"`
157 | ID string `json:"id"`
158 | Meta struct {
159 | GithubCommitRef string `json:"githubCommitRef"`
160 | GithubRepo string `json:"githubRepo"`
161 | GithubOrg string `json:"githubOrg"`
162 | GithubCommitSha string `json:"githubCommitSha"`
163 | GithubRepoID string `json:"githubRepoId"`
164 | GithubCommitMessage string `json:"githubCommitMessage"`
165 | GithubCommitAuthorLogin string `json:"githubCommitAuthorLogin"`
166 | GithubDeployment string `json:"githubDeployment"`
167 | GithubCommitOrg string `json:"githubCommitOrg"`
168 | GithubCommitAuthorName string `json:"githubCommitAuthorName"`
169 | GithubCommitRepo string `json:"githubCommitRepo"`
170 | GithubCommitRepoID string `json:"githubCommitRepoId"`
171 | } `json:"meta"`
172 | Name string `json:"name"`
173 | Plan string `json:"plan"`
174 | Private bool `json:"private"`
175 | ReadyState string `json:"readyState"`
176 | Target string `json:"target"`
177 | TeamID string `json:"teamId"`
178 | Type string `json:"type"`
179 | URL string `json:"url"`
180 | UserID string `json:"userId"`
181 | WithCache bool `json:"withCache"`
182 | } `json:"production"`
183 | } `json:"targets"`
184 | }
185 |
186 | // CreateProject has all the fields the user can set when creating a new project
187 | type CreateProject struct {
188 | Name string `json:"name"`
189 | GitRepository *struct {
190 | Type string `json:"type"`
191 | Repo string `json:"repo"`
192 | } `json:"gitRepository,omitempty"`
193 | UpdateProject
194 | }
195 |
196 | // UpdateProject has all the values a user can update without recreating a project
197 | // https://vercel.com/docs/api#endpoints/projects/update-a-single-project
198 | type UpdateProject struct {
199 | // The framework that is being used for this project. When null is used no framework is selected.
200 | Framework string `json:"framework,omitempty"`
201 |
202 | // Specifies whether the source code and logs of the deployments for this project should be public or not.
203 | PublicSource bool `json:"publicSource,omitempty"`
204 |
205 | // The install command for this project. When null is used this value will be automatically detected.
206 | InstallCommand string `json:"installCommand,omitempty"`
207 |
208 | // The build command for this project. When null is used this value will be automatically detected.
209 | BuildCommand string `json:"buildCommand,omitempty"`
210 |
211 | // The dev command for this project. When null is used this value will be automatically detected.
212 | DevCommand string `json:"devCommand,omitempty"`
213 |
214 | // The output directory of the project. When null is used this value will be automatically detected.
215 | OutputDirectory string `json:"outputDirectory,omitempty"`
216 |
217 | // The region to deploy Serverless Functions in this project.
218 | ServerlessFunctionRegion string `json:"serverlessFunctionRegion,omitempty"`
219 |
220 | // The name of a directory or relative path to the source code of your project. When null is used it will default to the project root.
221 | RootDirectory string `json:"rootDirectory,omitempty"`
222 |
223 | // A new name for this project.
224 | Name string `json:"name,omitempty"`
225 |
226 | // The Node.js Version for this project.
227 | NodeVersion string `json:"nodeVersion,omitempty"`
228 | }
229 |
--------------------------------------------------------------------------------
/pkg/vercel/secret/secret.go:
--------------------------------------------------------------------------------
1 | package secret
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/httpApi"
7 | "time"
8 | )
9 |
10 | type Secret struct {
11 | UID string `json:"uid"`
12 | Name string `json:"name"`
13 | TeamID string `json:"teamId"`
14 | UserID string `json:"userId"`
15 | Created time.Time `json:"created"`
16 | CreatedAt int64 `json:"createdAt"`
17 | }
18 |
19 | type CreateSecret struct {
20 | Name string `json:"name"`
21 | Value string `json:"value"`
22 | }
23 |
24 | type Handler struct {
25 | Api httpApi.API
26 | }
27 |
28 | func (h *Handler) Create(secret CreateSecret) (string, error) {
29 | res, err := h.Api.Request("POST", "/v2/now/secrets", secret)
30 | if err != nil {
31 | return "", err
32 | }
33 | defer res.Body.Close()
34 |
35 | var createdSecret Secret
36 | err = json.NewDecoder(res.Body).Decode(&createdSecret)
37 | if err != nil {
38 | return "", nil
39 | }
40 |
41 | return createdSecret.UID, nil
42 | }
43 |
44 | // Read returns environment variables associated with a project
45 | func (h *Handler) Read(secretID, teamId string) (secret Secret, err error) {
46 | url := fmt.Sprintf("/v3/now/secrets/%s", secretID)
47 | if teamId != "" {
48 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
49 | }
50 |
51 | res, err := h.Api.Request("GET", url, nil)
52 | if err != nil {
53 | return Secret{}, fmt.Errorf("Unable to fetch secret from vercel: %w", err)
54 | }
55 | defer res.Body.Close()
56 |
57 | err = json.NewDecoder(res.Body).Decode(&secret)
58 | if err != nil {
59 | return Secret{}, fmt.Errorf("Unable to unmarshal environment variables response: %w", err)
60 | }
61 | return secret, nil
62 | }
63 | func (h *Handler) Update(oldName, newName, teamId string) error {
64 | url := fmt.Sprintf("/v2/now/secrets/%s", oldName)
65 | if teamId != "" {
66 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
67 | }
68 | payload := struct {
69 | Name string `json:"name"`
70 | }{
71 | Name: newName,
72 | }
73 |
74 | res, err := h.Api.Request("PATCH", url, payload)
75 | if err != nil {
76 | return fmt.Errorf("Unable to update secret: %w", err)
77 | }
78 | defer res.Body.Close()
79 | return nil
80 | }
81 | func (h *Handler) Delete(secretName, teamId string) error {
82 | url := fmt.Sprintf("/v2/now/secrets/%s", secretName)
83 | if teamId != "" {
84 | url = fmt.Sprintf("%s/?teamId=%s", url, teamId)
85 | }
86 | res, err := h.Api.Request("DELETE", url, nil)
87 | if err != nil {
88 | return fmt.Errorf("Unable to delete secret: %w", err)
89 | }
90 | defer res.Body.Close()
91 | return nil
92 | }
93 |
--------------------------------------------------------------------------------
/pkg/vercel/team/team.go:
--------------------------------------------------------------------------------
1 | package team
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/httpApi"
9 | )
10 |
11 | type Team struct {
12 | Id string `json:"id"`
13 | Slug string `json:"slug"`
14 | Name string `json:"name"`
15 | CreatorId string `json:"creatorId"`
16 | Created time.Time `json:"created"`
17 | Avatar string `json:"avatar"`
18 | }
19 |
20 | type Handler struct {
21 | Api httpApi.API
22 | }
23 |
24 | func (h *Handler) Read(slug string) (user Team, err error) {
25 | res, err := h.Api.Request("GET", fmt.Sprintf("/v1/teams/?slug=%s", slug), nil)
26 | if err != nil {
27 | return Team{}, fmt.Errorf("Unable to fetch team from vercel: %w", err)
28 | }
29 | defer res.Body.Close()
30 |
31 | var team Team
32 | err = json.NewDecoder(res.Body).Decode(&team)
33 | if err != nil {
34 | return Team{}, fmt.Errorf("Unable to unmarshal team: %w", err)
35 | }
36 | return team, nil
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/vercel/user/user.go:
--------------------------------------------------------------------------------
1 | package user
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/chronark/terraform-provider-vercel/pkg/vercel/httpApi"
7 | )
8 |
9 | type User struct {
10 | UID string `json:"uid"`
11 | Email string `json:"email"`
12 | Name string `json:"name"`
13 | Username string `json:"username"`
14 | Avatar string `json:"avatar"`
15 | PlatformVersion int `json:"platformVersion"`
16 | Billing struct {
17 | Plan string `json:"plan"`
18 | Period interface{} `json:"period"`
19 | Trial interface{} `json:"trial"`
20 | Cancelation interface{} `json:"cancelation"`
21 | Addons interface{} `json:"addons"`
22 | } `json:"billing"`
23 | Bio string `json:"bio"`
24 | Website string `json:"website"`
25 | Profiles []struct {
26 | Service string `json:"service"`
27 | Link string `json:"link"`
28 | } `json:"profiles"`
29 | }
30 |
31 | type UserHandler struct {
32 | Api httpApi.API
33 | }
34 |
35 | func (p *UserHandler) Read() (user User, err error) {
36 | res, err := p.Api.Request("GET", "/www/user", nil)
37 | if err != nil {
38 | return User{}, fmt.Errorf("Unable to fetch user from vercel: %w", err)
39 | }
40 | defer res.Body.Close()
41 |
42 | type UserResponse struct {
43 | User User `json:"user"`
44 | }
45 |
46 | var userResponse UserResponse
47 | err = json.NewDecoder(res.Body).Decode(&userResponse)
48 | if err != nil {
49 | return User{}, fmt.Errorf("Unable to unmarshal project: %w", err)
50 | }
51 | return userResponse.User, nil
52 | }
53 |
--------------------------------------------------------------------------------
/tools/tools.go:
--------------------------------------------------------------------------------
1 | // +build tools
2 |
3 | package tools
4 |
5 | import (
6 | // document generation
7 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs"
8 | )
9 |
--------------------------------------------------------------------------------