Bundle new Vault plugin: Terraform secrets (#10931)
* Bundle Terraform secrets engine * update go.mod/sum * vendor update * add changelog entry * add secrets terraform
This commit is contained in:
parent
0017b78919
commit
2aff402279
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
secrets/terraform: New secret engine for managing Terraform Cloud API tokens
|
||||
```
|
|
@ -392,6 +392,7 @@ func TestPredict_Plugins(t *testing.T) {
|
|||
"redshift-database-plugin",
|
||||
"snowflake-database-plugin",
|
||||
"ssh",
|
||||
"terraform",
|
||||
"totp",
|
||||
"transform",
|
||||
"transit",
|
||||
|
|
1
go.mod
1
go.mod
|
@ -96,6 +96,7 @@ require (
|
|||
github.com/hashicorp/vault-plugin-secrets-kv v0.7.0
|
||||
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.2.0
|
||||
github.com/hashicorp/vault-plugin-secrets-openldap v0.1.6-0.20210201204049-4f0f91977798
|
||||
github.com/hashicorp/vault-plugin-secrets-terraform v0.1.0
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20201001211907-38d91b749c77
|
||||
github.com/hashicorp/vault/sdk v0.1.14-0.20210127185906-6b455835fa8c
|
||||
github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4
|
||||
|
|
13
go.sum
13
go.sum
|
@ -467,7 +467,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
|
@ -588,6 +587,7 @@ github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cR
|
|||
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
|
||||
github.com/hashicorp/go-raftchunking v0.6.3-0.20191002164813-7e9e8525653a h1:FmnBDwGwlTgugDGbVxwV8UavqSMACbGrUpfc98yFLR4=
|
||||
github.com/hashicorp/go-raftchunking v0.6.3-0.20191002164813-7e9e8525653a/go.mod h1:xbXnmKqX9/+RhPkJ4zrEx4738HacP72aaUPlT2RZ4sU=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.7 h1:8/CAEZt/+F7kR7GevNHulKkUjLht3CPmn7egmhieNKo=
|
||||
|
@ -596,11 +596,15 @@ github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa
|
|||
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-slug v0.4.1 h1:/jAo8dNuLgSImoLXaX7Od7QB4TfYCVPam+OpAt5bZqc=
|
||||
github.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-tfe v0.12.0 h1:teL523WPxwYzL5Gjc2QFxExndrMfWY4BXS2/olVpULM=
|
||||
github.com/hashicorp/go-tfe v0.12.0/go.mod h1:oT0AG5u/ROzWiw8JZFLDY6FLh6AZnJIG0Ahhvp10txg=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||
|
@ -683,6 +687,8 @@ github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.2.0 h1:uTtKxt5qfwTj6Pq
|
|||
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.2.0/go.mod h1:JOqn2mWJJbTp9NaC0CSCc3q5HQA99LfeSqgpC3YS+oA=
|
||||
github.com/hashicorp/vault-plugin-secrets-openldap v0.1.6-0.20210201204049-4f0f91977798 h1:G3S7rF/zHfQnYZglk+WvjzBuJyjQAnP0xdGL/4i3jzM=
|
||||
github.com/hashicorp/vault-plugin-secrets-openldap v0.1.6-0.20210201204049-4f0f91977798/go.mod h1:GiFI8Bxwx3+fn0A3SyVp9XdYQhm3cOgN8GzwKxyJ9So=
|
||||
github.com/hashicorp/vault-plugin-secrets-terraform v0.1.0 h1:g+r6TKJsD2aM0kUNWByuL4ffZTbZH/xO/sqDwTltOu0=
|
||||
github.com/hashicorp/vault-plugin-secrets-terraform v0.1.0/go.mod h1:7r/0t51X/ZtSRh/TjBk7gCm1CUMk50aqLAx811OsGQ8=
|
||||
github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyuqg7yuAWUg/jMZR1/0QTzTRdNR6Uw=
|
||||
github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
|
@ -836,7 +842,6 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
|
|||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.14.0 h1:/x0XQ6h+3U3nAyk1yx+bHPURrKa9sVVvYbuqZ7pIAtI=
|
||||
github.com/mitchellh/go-testing-interface v1.14.0/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
|
@ -984,7 +989,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac h1:jWKYCNlX4J5s8M0nHYkh7Y7c9gRVDEb3mq51j5J0F5M=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac/go.mod h1:hoLfEwdY11HjRfKFH6KqnPsfxlo3BP6bJehpDv8t6sQ=
|
||||
|
@ -1118,6 +1122,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
|||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d h1:Z4EH+5EffvBEhh37F0C0DnpklTMh00JOkjW5zK3ofBI=
|
||||
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go v3.0.83+incompatible/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go v3.0.171+incompatible h1:K3fcS92NS8cRntIdu8Uqy2ZSePvX73nNhOkKuPGJLXQ=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go v3.0.171+incompatible/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4=
|
||||
|
@ -1373,7 +1379,6 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
logicalKv "github.com/hashicorp/vault-plugin-secrets-kv"
|
||||
logicalMongoAtlas "github.com/hashicorp/vault-plugin-secrets-mongodbatlas"
|
||||
logicalOpenLDAP "github.com/hashicorp/vault-plugin-secrets-openldap"
|
||||
logicalTerraform "github.com/hashicorp/vault-plugin-secrets-terraform"
|
||||
credAppId "github.com/hashicorp/vault/builtin/credential/app-id"
|
||||
credAppRole "github.com/hashicorp/vault/builtin/credential/approle"
|
||||
credAws "github.com/hashicorp/vault/builtin/credential/aws"
|
||||
|
@ -130,6 +131,7 @@ func newRegistry() *registry {
|
|||
"postgresql": logicalPostgres.Factory, // Deprecated
|
||||
"rabbitmq": logicalRabbit.Factory,
|
||||
"ssh": logicalSsh.Factory,
|
||||
"terraform": logicalTerraform.Factory,
|
||||
"totp": logicalTotp.Factory,
|
||||
"transit": logicalTransit.Factory,
|
||||
},
|
||||
|
|
|
@ -65,6 +65,7 @@ vault secrets enable pki
|
|||
vault secrets enable postgresql
|
||||
vault secrets enable rabbitmq
|
||||
vault secrets enable ssh
|
||||
vault secrets enable terraform
|
||||
vault secrets enable totp
|
||||
vault secrets enable transit
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
|
@ -0,0 +1,70 @@
|
|||
# go-slug
|
||||
|
||||
[![Build Status](https://travis-ci.org/hashicorp/go-slug.svg?branch=master)](https://travis-ci.org/hashicorp/go-slug)
|
||||
[![GitHub license](https://img.shields.io/github/license/hashicorp/go-slug.svg)](https://github.com/hashicorp/go-slug/blob/master/LICENSE)
|
||||
[![GoDoc](https://godoc.org/github.com/hashicorp/go-slug?status.svg)](https://godoc.org/github.com/hashicorp/go-slug)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/hashicorp/go-slug)](https://goreportcard.com/report/github.com/hashicorp/go-slug)
|
||||
[![GitHub issues](https://img.shields.io/github/issues/hashicorp/go-slug.svg)](https://github.com/hashicorp/go-slug/issues)
|
||||
|
||||
Package `go-slug` offers functions for packing and unpacking Terraform Enterprise
|
||||
compatible slugs. Slugs are gzip compressed tar files containing Terraform configuration files.
|
||||
|
||||
## Installation
|
||||
|
||||
Installation can be done with a normal `go get`:
|
||||
|
||||
```
|
||||
go get -u github.com/hashicorp/go-slug
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For the complete usage of `go-slug`, see the full [package docs](https://godoc.org/github.com/hashicorp/go-slug).
|
||||
|
||||
## Example
|
||||
|
||||
Packing or unpacking a slug is pretty straight forward as shown in the
|
||||
following example:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
slug "github.com/hashicorp/go-slug"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// First create a buffer for storing the slug.
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
// Then call the Pack function with a directory path containing the
|
||||
// configuration files and an io.Writer to write the slug to.
|
||||
if _, err := slug.Pack("testdata/archive-dir", buf, false); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a directory to unpack the slug contents into.
|
||||
dst, err := ioutil.TempDir("", "slug")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dst)
|
||||
|
||||
// Unpacking a slug is done by calling the Unpack function with an
|
||||
// io.Reader to read the slug from and a directory path of an existing
|
||||
// directory to store the unpacked configuration files.
|
||||
if err := slug.Unpack(buf, dst); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Issues and Contributing
|
||||
|
||||
If you find an issue with this package, please report an issue. If you'd like,
|
||||
we welcome any contributions. Fork this repository and submit a pull request.
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/hashicorp/go-slug
|
||||
|
||||
go 1.12
|
|
@ -0,0 +1,303 @@
|
|||
package slug
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Meta provides detailed information about a slug.
|
||||
type Meta struct {
|
||||
// The list of files contained in the slug.
|
||||
Files []string
|
||||
|
||||
// Total size of the slug in bytes.
|
||||
Size int64
|
||||
}
|
||||
|
||||
// Pack creates a slug from a src directory, and writes the new slug
|
||||
// to w. Returns metadata about the slug and any errors.
|
||||
//
|
||||
// When dereference is set to true, symlinks with a target outside of
|
||||
// the src directory will be dereferenced. When dereference is set to
|
||||
// false symlinks with a target outside the src directory are omitted
|
||||
// from the slug.
|
||||
func Pack(src string, w io.Writer, dereference bool) (*Meta, error) {
|
||||
// Gzip compress all the output data.
|
||||
gzipW := gzip.NewWriter(w)
|
||||
|
||||
// Tar the file contents.
|
||||
tarW := tar.NewWriter(gzipW)
|
||||
|
||||
// Load the ignore rule configuration, which will use
|
||||
// defaults if no .terraformignore is configured
|
||||
ignoreRules := parseIgnoreFile(src)
|
||||
|
||||
// Track the metadata details as we go.
|
||||
meta := &Meta{}
|
||||
|
||||
// Walk the tree of files.
|
||||
err := filepath.Walk(src, packWalkFn(src, src, src, tarW, meta, dereference, ignoreRules))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Flush the tar writer.
|
||||
if err := tarW.Close(); err != nil {
|
||||
return nil, fmt.Errorf("Failed to close the tar archive: %v", err)
|
||||
}
|
||||
|
||||
// Flush the gzip writer.
|
||||
if err := gzipW.Close(); err != nil {
|
||||
return nil, fmt.Errorf("Failed to close the gzip writer: %v", err)
|
||||
}
|
||||
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func packWalkFn(root, src, dst string, tarW *tar.Writer, meta *Meta, dereference bool, ignoreRules []rule) filepath.WalkFunc {
|
||||
return func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the relative path from the current src directory.
|
||||
subpath, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get relative path for file %q: %v", path, err)
|
||||
}
|
||||
if subpath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
if m := matchIgnoreRule(subpath, ignoreRules); m {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Catch directories so we don't end up with empty directories,
|
||||
// the files are ignored correctly
|
||||
if info.IsDir() {
|
||||
if m := matchIgnoreRule(subpath+string(os.PathSeparator), ignoreRules); m {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Get the relative path from the initial root directory.
|
||||
subpath, err = filepath.Rel(root, strings.Replace(path, src, dst, 1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get relative path for file %q: %v", path, err)
|
||||
}
|
||||
if subpath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check the file type and if we need to write the body.
|
||||
keepFile, writeBody := checkFileMode(info.Mode())
|
||||
if !keepFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
fm := info.Mode()
|
||||
header := &tar.Header{
|
||||
Name: filepath.ToSlash(subpath),
|
||||
ModTime: info.ModTime(),
|
||||
Mode: int64(fm.Perm()),
|
||||
}
|
||||
|
||||
switch {
|
||||
case info.IsDir():
|
||||
header.Typeflag = tar.TypeDir
|
||||
header.Name += "/"
|
||||
|
||||
case fm.IsRegular():
|
||||
header.Typeflag = tar.TypeReg
|
||||
header.Size = info.Size()
|
||||
|
||||
case fm&os.ModeSymlink != 0:
|
||||
target, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get symbolic link destination for %q: %v", path, err)
|
||||
}
|
||||
|
||||
// If the target is within the current source, we
|
||||
// create the symlink using a relative path.
|
||||
if strings.Contains(target, src) {
|
||||
link, err := filepath.Rel(filepath.Dir(path), target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get relative path for symlink destination %q: %v", target, err)
|
||||
}
|
||||
|
||||
header.Typeflag = tar.TypeSymlink
|
||||
header.Linkname = filepath.ToSlash(link)
|
||||
|
||||
// Break out of the case as a symlink
|
||||
// doesn't need any additional config.
|
||||
break
|
||||
}
|
||||
|
||||
if !dereference {
|
||||
// Return early as the symlink has a target outside of the
|
||||
// src directory and we don't want to dereference symlinks.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the file info for the target.
|
||||
info, err = os.Lstat(target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get file info from file %q: %v", target, err)
|
||||
}
|
||||
|
||||
// If the target is a directory we can recurse into the target
|
||||
// directory by calling the packWalkFn with updated arguments.
|
||||
if info.IsDir() {
|
||||
return filepath.Walk(target, packWalkFn(root, target, path, tarW, meta, dereference, ignoreRules))
|
||||
}
|
||||
|
||||
// Dereference this symlink by updating the header with the target file
|
||||
// details and set writeBody to true so the body will be written.
|
||||
header.Typeflag = tar.TypeReg
|
||||
header.ModTime = info.ModTime()
|
||||
header.Mode = int64(info.Mode().Perm())
|
||||
header.Size = info.Size()
|
||||
writeBody = true
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Unexpected file mode %v", fm)
|
||||
}
|
||||
|
||||
// Write the header first to the archive.
|
||||
if err := tarW.WriteHeader(header); err != nil {
|
||||
return fmt.Errorf("Failed writing archive header for file %q: %v", path, err)
|
||||
}
|
||||
|
||||
// Account for the file in the list.
|
||||
meta.Files = append(meta.Files, header.Name)
|
||||
|
||||
// Skip writing file data for certain file types (above).
|
||||
if !writeBody {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed opening file %q for archiving: %v", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
size, err := io.Copy(tarW, f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed copying file %q to archive: %v", path, err)
|
||||
}
|
||||
|
||||
// Add the size we copied to the body.
|
||||
meta.Size += size
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Unpack is used to read and extract the contents of a slug to
|
||||
// the dst directory. Returns any errors.
|
||||
func Unpack(r io.Reader, dst string) error {
|
||||
// Decompress as we read.
|
||||
uncompressed, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to uncompress slug: %v", err)
|
||||
}
|
||||
|
||||
// Untar as we read.
|
||||
untar := tar.NewReader(uncompressed)
|
||||
|
||||
// Unpackage all the contents into the directory.
|
||||
for {
|
||||
header, err := untar.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to untar slug: %v", err)
|
||||
}
|
||||
|
||||
// Get rid of absolute paths.
|
||||
path := header.Name
|
||||
if path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
path = filepath.Join(dst, path)
|
||||
|
||||
// Make the directories to the path.
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("Failed to create directory %q: %v", dir, err)
|
||||
}
|
||||
|
||||
// If we have a symlink, just link it.
|
||||
if header.Typeflag == tar.TypeSymlink {
|
||||
if err := os.Symlink(header.Linkname, path); err != nil {
|
||||
return fmt.Errorf("Failed creating symlink %q => %q: %v",
|
||||
path, header.Linkname, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Only unpack regular files from this point on.
|
||||
if header.Typeflag == tar.TypeDir {
|
||||
continue
|
||||
} else if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA {
|
||||
return fmt.Errorf("Failed creating %q: unsupported type %c", path,
|
||||
header.Typeflag)
|
||||
}
|
||||
|
||||
// Open a handle to the destination.
|
||||
fh, err := os.Create(path)
|
||||
if err != nil {
|
||||
// This mimics tar's behavior wrt the tar file containing duplicate files
|
||||
// and it allowing later ones to clobber earlier ones even if the file
|
||||
// has perms that don't allow overwriting.
|
||||
if os.IsPermission(err) {
|
||||
os.Chmod(path, 0600)
|
||||
fh, err = os.Create(path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed creating file %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the contents.
|
||||
_, err = io.Copy(fh, untar)
|
||||
fh.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to copy slug file %q: %v", path, err)
|
||||
}
|
||||
|
||||
// Restore the file mode. We have to do this after writing the file,
|
||||
// since it is possible we have a read-only mode.
|
||||
mode := header.FileInfo().Mode()
|
||||
if err := os.Chmod(path, mode); err != nil {
|
||||
return fmt.Errorf("Failed setting permissions on %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkFileMode is used to examine an os.FileMode and determine if it should
|
||||
// be included in the archive, and if it has a data body which needs writing.
|
||||
func checkFileMode(m os.FileMode) (keep, body bool) {
|
||||
switch {
|
||||
case m.IsDir():
|
||||
return true, false
|
||||
|
||||
case m.IsRegular():
|
||||
return true, true
|
||||
|
||||
case m&os.ModeSymlink != 0:
|
||||
return true, false
|
||||
}
|
||||
|
||||
return false, false
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
package slug
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/scanner"
|
||||
)
|
||||
|
||||
func parseIgnoreFile(rootPath string) []rule {
|
||||
// Look for .terraformignore at our root path/src
|
||||
file, err := os.Open(filepath.Join(rootPath, ".terraformignore"))
|
||||
defer file.Close()
|
||||
|
||||
// If there's any kind of file error, punt and use the default ignore patterns
|
||||
if err != nil {
|
||||
// Only show the error debug if an error *other* than IsNotExist
|
||||
if !os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Error reading .terraformignore, default exclusions will apply: %v \n", err)
|
||||
}
|
||||
return defaultExclusions
|
||||
}
|
||||
return readRules(file)
|
||||
}
|
||||
|
||||
func readRules(input io.Reader) []rule {
|
||||
rules := defaultExclusions
|
||||
scanner := bufio.NewScanner(input)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
for scanner.Scan() {
|
||||
pattern := scanner.Text()
|
||||
// Ignore blank lines
|
||||
if len(pattern) == 0 {
|
||||
continue
|
||||
}
|
||||
// Trim spaces
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
// Ignore comments
|
||||
if pattern[0] == '#' {
|
||||
continue
|
||||
}
|
||||
// New rule structure
|
||||
rule := rule{}
|
||||
// Exclusions
|
||||
if pattern[0] == '!' {
|
||||
rule.excluded = true
|
||||
pattern = pattern[1:]
|
||||
}
|
||||
// If it is a directory, add ** so we catch descendants
|
||||
if pattern[len(pattern)-1] == os.PathSeparator {
|
||||
pattern = pattern + "**"
|
||||
}
|
||||
// If it starts with /, it is absolute
|
||||
if pattern[0] == os.PathSeparator {
|
||||
pattern = pattern[1:]
|
||||
} else {
|
||||
// Otherwise prepend **/
|
||||
pattern = "**" + string(os.PathSeparator) + pattern
|
||||
}
|
||||
rule.val = pattern
|
||||
rule.dirs = strings.Split(pattern, string(os.PathSeparator))
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading .terraformignore, default exclusions will apply: %v \n", err)
|
||||
return defaultExclusions
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
func matchIgnoreRule(path string, rules []rule) bool {
|
||||
matched := false
|
||||
path = filepath.FromSlash(path)
|
||||
for _, rule := range rules {
|
||||
match, _ := rule.match(path)
|
||||
|
||||
if match {
|
||||
matched = !rule.excluded
|
||||
}
|
||||
}
|
||||
|
||||
if matched {
|
||||
debug(true, path, "Skipping excluded path:", path)
|
||||
}
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
type rule struct {
|
||||
val string // the value of the rule itself
|
||||
excluded bool // ! is present, an exclusion rule
|
||||
dirs []string // directories of the rule
|
||||
regex *regexp.Regexp // regular expression to match for the rule
|
||||
}
|
||||
|
||||
func (r *rule) match(path string) (bool, error) {
|
||||
if r.regex == nil {
|
||||
if err := r.compile(); err != nil {
|
||||
return false, filepath.ErrBadPattern
|
||||
}
|
||||
}
|
||||
|
||||
b := r.regex.MatchString(path)
|
||||
debug(false, path, path, r.regex, b)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (r *rule) compile() error {
|
||||
regStr := "^"
|
||||
pattern := r.val
|
||||
// Go through the pattern and convert it to a regexp.
|
||||
// Use a scanner to support utf-8 chars.
|
||||
var scan scanner.Scanner
|
||||
scan.Init(strings.NewReader(pattern))
|
||||
|
||||
sl := string(os.PathSeparator)
|
||||
escSL := sl
|
||||
if sl == `\` {
|
||||
escSL += `\`
|
||||
}
|
||||
|
||||
for scan.Peek() != scanner.EOF {
|
||||
ch := scan.Next()
|
||||
if ch == '*' {
|
||||
if scan.Peek() == '*' {
|
||||
// is some flavor of "**"
|
||||
scan.Next()
|
||||
|
||||
// Treat **/ as ** so eat the "/"
|
||||
if string(scan.Peek()) == sl {
|
||||
scan.Next()
|
||||
}
|
||||
|
||||
if scan.Peek() == scanner.EOF {
|
||||
// is "**EOF" - to align with .gitignore just accept all
|
||||
regStr += ".*"
|
||||
} else {
|
||||
// is "**"
|
||||
// Note that this allows for any # of /'s (even 0) because
|
||||
// the .* will eat everything, even /'s
|
||||
regStr += "(.*" + escSL + ")?"
|
||||
}
|
||||
} else {
|
||||
// is "*" so map it to anything but "/"
|
||||
regStr += "[^" + escSL + "]*"
|
||||
}
|
||||
} else if ch == '?' {
|
||||
// "?" is any char except "/"
|
||||
regStr += "[^" + escSL + "]"
|
||||
} else if ch == '.' || ch == '$' {
|
||||
// Escape some regexp special chars that have no meaning
|
||||
// in golang's filepath.Match
|
||||
regStr += `\` + string(ch)
|
||||
} else if ch == '\\' {
|
||||
// escape next char. Note that a trailing \ in the pattern
|
||||
// will be left alone (but need to escape it)
|
||||
if sl == `\` {
|
||||
// On windows map "\" to "\\", meaning an escaped backslash,
|
||||
// and then just continue because filepath.Match on
|
||||
// Windows doesn't allow escaping at all
|
||||
regStr += escSL
|
||||
continue
|
||||
}
|
||||
if scan.Peek() != scanner.EOF {
|
||||
regStr += `\` + string(scan.Next())
|
||||
} else {
|
||||
regStr += `\`
|
||||
}
|
||||
} else {
|
||||
regStr += string(ch)
|
||||
}
|
||||
}
|
||||
|
||||
regStr += "$"
|
||||
re, err := regexp.Compile(regStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.regex = re
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Default rules as they would appear in .terraformignore:
|
||||
.git/
|
||||
.terraform/
|
||||
!.terraform/modules/
|
||||
*/
|
||||
|
||||
var defaultExclusions = []rule{
|
||||
{
|
||||
val: strings.Join([]string{"**", ".git", "**"}, string(os.PathSeparator)),
|
||||
excluded: false,
|
||||
},
|
||||
{
|
||||
val: strings.Join([]string{"**", ".terraform", "**"}, string(os.PathSeparator)),
|
||||
excluded: false,
|
||||
},
|
||||
{
|
||||
val: strings.Join([]string{"**", ".terraform", "modules", "**"}, string(os.PathSeparator)),
|
||||
excluded: true,
|
||||
},
|
||||
}
|
||||
|
||||
func debug(printAll bool, path string, message ...interface{}) {
|
||||
logLevel := os.Getenv("TF_IGNORE") == "trace"
|
||||
debugPath := os.Getenv("TF_IGNORE_DEBUG")
|
||||
isPath := debugPath != ""
|
||||
if isPath {
|
||||
isPath = strings.Contains(path, debugPath)
|
||||
}
|
||||
|
||||
if logLevel {
|
||||
if printAll || isPath {
|
||||
fmt.Println(message...)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
1.14.10
|
|
@ -0,0 +1,354 @@
|
|||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. “Contributor”
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. “Contributor Version”
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor’s Contribution.
|
||||
|
||||
1.3. “Contribution”
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. “Covered Software”
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. “Incompatible With Secondary Licenses”
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of version
|
||||
1.1 or earlier of the License, but not also under the terms of a
|
||||
Secondary License.
|
||||
|
||||
1.6. “Executable Form”
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. “Larger Work”
|
||||
|
||||
means a work that combines Covered Software with other material, in a separate
|
||||
file or files, that is not Covered Software.
|
||||
|
||||
1.8. “License”
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. “Licensable”
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether at the
|
||||
time of the initial grant or subsequently, any and all of the rights conveyed by
|
||||
this License.
|
||||
|
||||
1.10. “Modifications”
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to, deletion
|
||||
from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. “Patent Claims” of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method, process,
|
||||
and apparatus claims, in any patent Licensable by such Contributor that
|
||||
would be infringed, but for the grant of the License, by the making,
|
||||
using, selling, offering for sale, having made, import, or transfer of
|
||||
either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. “Secondary License”
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. “Source Code Form”
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. “You” (or “Your”)
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, “You” includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, “control” means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or as
|
||||
part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its Contributions
|
||||
or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution become
|
||||
effective for each Contribution on the date the Contributor first distributes
|
||||
such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under this
|
||||
License. No additional rights or licenses will be implied from the distribution
|
||||
or licensing of Covered Software under this License. Notwithstanding Section
|
||||
2.1(b) above, no patent license is granted by a Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party’s
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of its
|
||||
Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks, or
|
||||
logos of any Contributor (except as may be necessary to comply with the
|
||||
notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this License
|
||||
(see Section 10.2) or under the terms of a Secondary License (if permitted
|
||||
under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its Contributions
|
||||
are its original creation(s) or it has sufficient rights to grant the
|
||||
rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under applicable
|
||||
copyright doctrines of fair use, fair dealing, or other equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under the
|
||||
terms of this License. You must inform recipients that the Source Code Form
|
||||
of the Covered Software is governed by the terms of this License, and how
|
||||
they can obtain a copy of this License. You may not attempt to alter or
|
||||
restrict the recipients’ rights in the Source Code Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this License,
|
||||
or sublicense it under different terms, provided that the license for
|
||||
the Executable Form does not attempt to limit or alter the recipients’
|
||||
rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for the
|
||||
Covered Software. If the Larger Work is a combination of Covered Software
|
||||
with a work governed by one or more Secondary Licenses, and the Covered
|
||||
Software is not Incompatible With Secondary Licenses, this License permits
|
||||
You to additionally distribute such Covered Software under the terms of
|
||||
such Secondary License(s), so that the recipient of the Larger Work may, at
|
||||
their option, further distribute the Covered Software under the terms of
|
||||
either this License or such Secondary License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices (including
|
||||
copyright notices, patent notices, disclaimers of warranty, or limitations
|
||||
of liability) contained within the Source Code Form of the Covered
|
||||
Software, except that You may alter any license notices to the extent
|
||||
required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on behalf
|
||||
of any Contributor. You must make it absolutely clear that any such
|
||||
warranty, support, indemnity, or liability obligation is offered by You
|
||||
alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute, judicial
|
||||
order, or regulation then You must: (a) comply with the terms of this License
|
||||
to the maximum extent possible; and (b) describe the limitations and the code
|
||||
they affect. Such description must be placed in a text file included with all
|
||||
distributions of the Covered Software under this License. Except to the
|
||||
extent prohibited by statute or regulation, such description must be
|
||||
sufficiently detailed for a recipient of ordinary skill to be able to
|
||||
understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing basis,
|
||||
if such Contributor fails to notify You of the non-compliance by some
|
||||
reasonable means prior to 60 days after You have come back into compliance.
|
||||
Moreover, Your grants from a particular Contributor are reinstated on an
|
||||
ongoing basis if such Contributor notifies You of the non-compliance by
|
||||
some reasonable means, this is the first time You have received notice of
|
||||
non-compliance with this License from such Contributor, and You become
|
||||
compliant prior to 30 days after Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions, counter-claims,
|
||||
and cross-claims) alleging that a Contributor Version directly or
|
||||
indirectly infringes any patent, then the rights granted to You by any and
|
||||
all Contributors for the Covered Software under Section 2.1 of this License
|
||||
shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an “as is” basis, without
|
||||
warranty of any kind, either expressed, implied, or statutory, including,
|
||||
without limitation, warranties that the Covered Software is free of defects,
|
||||
merchantable, fit for a particular purpose or non-infringing. The entire
|
||||
risk as to the quality and performance of the Covered Software is with You.
|
||||
Should any Covered Software prove defective in any respect, You (not any
|
||||
Contributor) assume the cost of any necessary servicing, repair, or
|
||||
correction. This disclaimer of warranty constitutes an essential part of this
|
||||
License. No use of any Covered Software is authorized under this License
|
||||
except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from such
|
||||
party’s negligence to the extent applicable law prohibits such limitation.
|
||||
Some jurisdictions do not allow the exclusion or limitation of incidental or
|
||||
consequential damages, so this exclusion and limitation may not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts of
|
||||
a jurisdiction where the defendant maintains its principal place of business
|
||||
and such litigation shall be governed by laws of that jurisdiction, without
|
||||
reference to its conflict-of-law provisions. Nothing in this Section shall
|
||||
prevent a party’s ability to bring cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject matter
|
||||
hereof. If any provision of this License is held to be unenforceable, such
|
||||
provision shall be reformed only to the extent necessary to make it
|
||||
enforceable. Any law or regulation which provides that the language of a
|
||||
contract shall be construed against the drafter shall not be used to construe
|
||||
this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version of
|
||||
the License under which You originally received the Covered Software, or
|
||||
under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a modified
|
||||
version of this License if you rename the license and remove any
|
||||
references to the name of the license steward (except to note that such
|
||||
modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file, then
|
||||
You may include the notice in a location (such as a LICENSE file in a relevant
|
||||
directory) where a recipient would be likely to look for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - “Incompatible With Secondary Licenses” Notice
|
||||
|
||||
This Source Code Form is “Incompatible
|
||||
With Secondary Licenses”, as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
Terraform Cloud/Enterprise Go Client
|
||||
==============================
|
||||
|
||||
[![Build Status](https://circleci.com/gh/hashicorp/go-tfe.svg?style=shield)](https://circleci.com/gh/hashicorp/go-tfe)
|
||||
[![GitHub license](https://img.shields.io/github/license/hashicorp/go-tfe.svg)](https://github.com/hashicorp/go-tfe/blob/master/LICENSE)
|
||||
[![GoDoc](https://godoc.org/github.com/hashicorp/go-tfe?status.svg)](https://godoc.org/github.com/hashicorp/go-tfe)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/hashicorp/go-tfe)](https://goreportcard.com/report/github.com/hashicorp/go-tfe)
|
||||
[![GitHub issues](https://img.shields.io/github/issues/hashicorp/go-tfe.svg)](https://github.com/hashicorp/go-tfe/issues)
|
||||
|
||||
The official Go API client for [Terraform Cloud/Enterprise](https://www.hashicorp.com/products/terraform).
|
||||
|
||||
This client supports the [Terraform Cloud V2 API](https://www.terraform.io/docs/cloud/api/index.html).
|
||||
As Terraform Enterprise is a self-hosted distribution of Terraform Cloud, this
|
||||
client supports both Cloud and Enterprise use cases. In all package
|
||||
documentation and API, the platform will always be stated as 'Terraform
|
||||
Enterprise' - but a feature will be explicitly noted as only supported in one or
|
||||
the other, if applicable (rare).
|
||||
|
||||
Note this client is in beta and is subject to change (though it is generally
|
||||
quite stable). We will indicate any breaking changes by releasing new versions.
|
||||
Until the release of v1.0, any minor version changes will indicate possible
|
||||
breaking changes. Patch version changes will be used for both bugfixes and
|
||||
non-breaking changes.
|
||||
|
||||
## Installation
|
||||
|
||||
Installation can be done with a normal `go get`:
|
||||
|
||||
```
|
||||
go get -u github.com/hashicorp/go-tfe
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import tfe "github.com/hashicorp/go-tfe"
|
||||
```
|
||||
|
||||
Construct a new TFE client, then use the various endpoints on the client to
|
||||
access different parts of the Terraform Enterprise API. For example, to list
|
||||
all organizations:
|
||||
|
||||
```go
|
||||
config := &tfe.Config{
|
||||
Token: "insert-your-token-here",
|
||||
}
|
||||
|
||||
client, err := tfe.NewClient(config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
orgs, err := client.Organizations.List(context.Background(), tfe.OrganizationListOptions{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For complete usage of the API client, see the full [package docs](https://godoc.org/github.com/hashicorp/go-tfe).
|
||||
|
||||
## API Coverage
|
||||
|
||||
Most of the [Terraform Cloud/Enterprise V2 API](https://www.terraform.io/docs/cloud/api/index.html) is supported in this
|
||||
client. Currently, the separate [Admin API](https://www.terraform.io/docs/cloud/api/admin/index.html) - applicable only
|
||||
to Terraform Enterprise - is not.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
See the [examples directory](https://github.com/hashicorp/go-tfe/tree/master/examples).
|
||||
|
||||
## Running tests
|
||||
|
||||
See [TESTS.md](https://github.com/hashicorp/go-tfe/tree/master/TESTS.md).
|
||||
|
||||
## Issues and Contributing
|
||||
|
||||
If you find an issue with this package, please report an issue. If you'd like,
|
||||
we welcome any contributions. Fork this repository and submit a pull request.
|
||||
|
||||
## Releases
|
||||
|
||||
Documentation updates and test fixes that only touch test files don't require a release or tag. You can just merge these changes into master once they have been approved.
|
||||
|
||||
### Creating a release
|
||||
1. Merge your approved branch into master.
|
||||
1. [Create a new release in GitHub](https://help.github.com/en/github/administering-a-repository/creating-releases).
|
||||
- Click on "Releases" and then "Draft a new release"
|
||||
- Set the `tag version` to a new tag, using [Semantic Versioning](https://semver.org/) as a guideline.
|
||||
- Set the `target` as master.
|
||||
- Set the `Release title` to the tag you created, `vX.Y.Z`
|
||||
- Use the description section to describe why you're releasing and what changes you've made. You should include links to merged PRs
|
||||
- Consider using the following headers in the description of your release:
|
||||
- BREAKING CHANGES: Use this for any changes that aren't backwards compatible. Include details on how to handle these changes.
|
||||
- FEATURES: Use this for any large new features added,
|
||||
- ENHANCEMENTS: Use this for smaller new features added
|
||||
- BUG FIXES: Use this for any bugs that were fixed.
|
||||
- NOTES: Use this section if you need to include any additional notes on things like upgrading, upcoming deprecations, or any other information you might want to highlight.
|
||||
|
||||
Markdown example:
|
||||
|
||||
```markdown
|
||||
ENHANCEMENTS
|
||||
* Add description of new small feature (#3)[link-to-pull-request]
|
||||
|
||||
BUG FIXES
|
||||
* Fix description of a bug (#2)[link-to-pull-request]
|
||||
* Fix description of another bug (#1)[link-to-pull-request]
|
||||
```
|
||||
|
||||
- Don't attach any binaries. The zip and tar.gz assets are automatically created and attached after you publish your release.
|
||||
- Click "Publish release" to save and publish your release.
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
## Running tests
|
||||
|
||||
### 1. (Optional) Create repositories for policy sets and registry modules
|
||||
|
||||
If you are planning to run the full suite of tests or work on policy sets or registry modules, you'll need to set up repositories for them in GitHub.
|
||||
|
||||
Your policy set repository will need the following:
|
||||
1. A policy set stored in a subdirectory `policy-sets/foo`
|
||||
1. A branch other than master named `policies`
|
||||
|
||||
Your registry module repository will need to be a [valid module](https://www.terraform.io/docs/cloud/registry/publish.html#preparing-a-module-repository).
|
||||
It will need the following:
|
||||
1. To be named `terraform-<PROVIDER>-<NAME>`
|
||||
1. At least one valid SemVer tag in the format `x.y.z`
|
||||
[terraform-random-module](ttps://github.com/caseylang/terraform-random-module) is a good example repo.
|
||||
|
||||
### 2. Set up environment variables
|
||||
|
||||
##### Required:
|
||||
Tests are run against an actual backend so they require a valid backend address and token.
|
||||
1. `TFE_ADDRESS` - URL of a Terraform Cloud or Terraform Enterprise instance to be used for testing, including scheme. Example: `https://tfe.local`
|
||||
1. `TFE_TOKEN` - A [user API token](https://www.terraform.io/docs/cloud/users-teams-organizations/users.html#api-tokens) for the Terraform Cloud or Terraform Enterprise instance being used for testing.
|
||||
|
||||
##### Optional:
|
||||
1. `GITHUB_TOKEN` - [GitHub personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). Required for running any tests that use VCS (OAuth clients, policy sets, etc).
|
||||
1. `GITHUB_POLICY_SET_IDENTIFIER` - GitHub policy set repository identifier in the format `username/repository`. Required for running policy set tests.
|
||||
1. `GITHUB_REGISTRY_MODULE_IDENTIFIER` - GitHub registry module repository identifier in the format `username/repository`. Required for running registry module tests.
|
||||
|
||||
You can set your environment variables up however you prefer. The following are instructions for setting up environment variables using [envchain](https://github.com/sorah/envchain).
|
||||
1. Make sure you have envchain installed. [Instructions for this can be found in the envchain README](https://github.com/sorah/envchain#installation).
|
||||
1. Pick a namespace for storing your environment variables. I suggest `go-tfe` or something similar.
|
||||
1. For each environment variable you need to set, run the following command:
|
||||
```sh
|
||||
envchain --set YOUR_NAMESPACE_HERE ENVIRONMENT_VARIABLE_HERE
|
||||
```
|
||||
**OR**
|
||||
|
||||
Set all of the environment variables at once with the following command:
|
||||
```sh
|
||||
envchain --set YOUR_NAMESPACE_HERE TFE_ADDRESS TFE_TOKEN GITHUB_TOKEN GITHUB_POLICY_SET_IDENTIFIER
|
||||
```
|
||||
|
||||
### 3. Make sure run queue settings are correct
|
||||
|
||||
In order for the tests relating to queuing and capacity to pass, FRQ (fair run queuing) should be
|
||||
enabled with a limit of 2 concurrent runs per organization on the Terraform Cloud or Terraform Enterprise instance you are using for testing.
|
||||
|
||||
### 4. Run the tests
|
||||
|
||||
#### Running all the tests
|
||||
As running the all of the tests takes about ~20 minutes, make sure to add a timeout to your
|
||||
command (as the default timeout is 10m).
|
||||
|
||||
##### With envchain:
|
||||
```sh
|
||||
$ envchain YOUR_NAMESPACE_HERE go test ./... -timeout=30m
|
||||
```
|
||||
|
||||
##### Without envchain:
|
||||
```sh
|
||||
$ go test ./... -timeout=30m
|
||||
```
|
||||
#### Running specific tests
|
||||
|
||||
The commands below use notification configurations as an example.
|
||||
|
||||
##### With envchain:
|
||||
```sh
|
||||
$ envchain YOUR_NAMESPACE_HERE go test -run TestNotificationConfiguration -v ./...
|
||||
```
|
||||
|
||||
##### Without envchain:
|
||||
```sh
|
||||
$ go test -run TestNotificationConfiguration -v ./...
|
||||
```
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ AgentPools = (*agentPools)(nil)
|
||||
|
||||
// AgentPools describes all the agent pool related methods that the Terraform
|
||||
// Cloud API supports. Note that agents are not available in Terraform Enterprise.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/cloud/api/agents.html
|
||||
type AgentPools interface {
|
||||
// List all the agent pools of the given organization.
|
||||
List(ctx context.Context, organization string, options AgentPoolListOptions) (*AgentPoolList, error)
|
||||
|
||||
// Create a new agent pool with the given options.
|
||||
Create(ctx context.Context, organization string, options AgentPoolCreateOptions) (*AgentPool, error)
|
||||
|
||||
// Read a agent pool by its ID.
|
||||
Read(ctx context.Context, agentPoolID string) (*AgentPool, error)
|
||||
|
||||
// Update an agent pool by its ID.
|
||||
Update(ctx context.Context, agentPool string, options AgentPoolUpdateOptions) (*AgentPool, error)
|
||||
|
||||
// Delete an agent pool by its ID.
|
||||
Delete(ctx context.Context, agentPoolID string) error
|
||||
}
|
||||
|
||||
// agentPools implements AgentPools.
|
||||
type agentPools struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// AgentPoolList represents a list of agent pools.
|
||||
type AgentPoolList struct {
|
||||
*Pagination
|
||||
Items []*AgentPool
|
||||
}
|
||||
|
||||
// AgentPool represents a Terraform Cloud agent pool.
|
||||
type AgentPool struct {
|
||||
ID string `jsonapi:"primary,agent-pools"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
|
||||
// Relations
|
||||
Organization *Organization `jsonapi:"relation,organization"`
|
||||
}
|
||||
|
||||
// AgentPoolListOptions represents the options for listing agent pools.
|
||||
type AgentPoolListOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// List all the agent pools of the given organization.
|
||||
func (s *agentPools) List(ctx context.Context, organization string, options AgentPoolListOptions) (*AgentPoolList, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/agent-pools", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
poolList := &AgentPoolList{}
|
||||
err = s.client.do(ctx, req, poolList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return poolList, nil
|
||||
}
|
||||
|
||||
// AgentPoolCreateOptions represents the options for creating an agent pool.
|
||||
type AgentPoolCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,agent-pools"`
|
||||
|
||||
// A name to identify the agent pool.
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
}
|
||||
|
||||
func (o AgentPoolCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(o.Name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new agent pool with the given options.
|
||||
func (s *agentPools) Create(ctx context.Context, organization string, options AgentPoolCreateOptions) (*AgentPool, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/agent-pools", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pool := &AgentPool{}
|
||||
err = s.client.do(ctx, req, pool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// Read a single agent pool by its ID.
|
||||
func (s *agentPools) Read(ctx context.Context, agentpoolID string) (*AgentPool, error) {
|
||||
if !validStringID(&agentpoolID) {
|
||||
return nil, errors.New("invalid value for agent pool ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentpoolID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pool := &AgentPool{}
|
||||
err = s.client.do(ctx, req, pool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// AgentPoolUpdateOptions represents the options for updating an agent pool.
|
||||
type AgentPoolUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,agent-pools"`
|
||||
|
||||
// A new name to identify the agent pool.
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
}
|
||||
|
||||
func (o AgentPoolUpdateOptions) valid() error {
|
||||
if o.Name != nil && !validStringID(o.Name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update an agent pool by its ID.
|
||||
func (s *agentPools) Update(ctx context.Context, agentPoolID string, options AgentPoolUpdateOptions) (*AgentPool, error) {
|
||||
if !validStringID(&agentPoolID) {
|
||||
return nil, errors.New("invalid value for agent pool ID")
|
||||
}
|
||||
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentPoolID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k := &AgentPool{}
|
||||
err = s.client.do(ctx, req, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// Delete an agent pool by its ID.
|
||||
func (s *agentPools) Delete(ctx context.Context, agentPoolID string) error {
|
||||
if !validStringID(&agentPoolID) {
|
||||
return errors.New("invalid value for agent pool ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentPoolID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ AgentTokens = (*agentTokens)(nil)
|
||||
|
||||
// AgentTokens describes all the agent token related methods that the
|
||||
// Terraform Cloud API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/cloud/api/agent-tokens.html
|
||||
type AgentTokens interface {
|
||||
// List all the agent tokens of the given agent pool.
|
||||
List(ctx context.Context, agentPoolID string) (*AgentTokenList, error)
|
||||
|
||||
// Generate a new agent token with the given options.
|
||||
Generate(ctx context.Context, agentPoolID string, options AgentTokenGenerateOptions) (*AgentToken, error)
|
||||
|
||||
// Read an agent token by its ID.
|
||||
Read(ctx context.Context, agentTokenID string) (*AgentToken, error)
|
||||
|
||||
// Delete an agent token by its ID.
|
||||
Delete(ctx context.Context, agentTokenID string) error
|
||||
}
|
||||
|
||||
// agentTokens implements AgentTokens.
|
||||
type agentTokens struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// AgentTokenList represents a list of agent tokens.
|
||||
type AgentTokenList struct {
|
||||
*Pagination
|
||||
Items []*AgentToken
|
||||
}
|
||||
|
||||
// AgentToken represents a Terraform Cloud agent token.
|
||||
type AgentToken struct {
|
||||
ID string `jsonapi:"primary,authentication-tokens"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
Description string `jsonapi:"attr,description"`
|
||||
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
|
||||
Token string `jsonapi:"attr,token"`
|
||||
}
|
||||
|
||||
// List all the agent tokens of the given agent pool.
|
||||
func (s *agentTokens) List(ctx context.Context, agentPoolID string) (*AgentTokenList, error) {
|
||||
if !validStringID(&agentPoolID) {
|
||||
return nil, errors.New("invalid value for agent pool ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("agent-pools/%s/authentication-tokens", url.QueryEscape(agentPoolID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenList := &AgentTokenList{}
|
||||
err = s.client.do(ctx, req, tokenList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tokenList, nil
|
||||
}
|
||||
|
||||
// AgentTokenGenerateOptions represents the options for creating an agent token.
|
||||
type AgentTokenGenerateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,agent-tokens"`
|
||||
|
||||
// Description of the token
|
||||
Description *string `jsonapi:"attr,description"`
|
||||
}
|
||||
|
||||
// Generate a new agent token with the given options.
|
||||
func (s *agentTokens) Generate(ctx context.Context, agentPoolID string, options AgentTokenGenerateOptions) (*AgentToken, error) {
|
||||
if !validStringID(&agentPoolID) {
|
||||
return nil, errors.New("invalid value for agent pool ID")
|
||||
}
|
||||
|
||||
if !validString(options.Description) {
|
||||
return nil, errors.New("agent token description can't be blank")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("agent-pools/%s/authentication-tokens", url.QueryEscape(agentPoolID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
at := &AgentToken{}
|
||||
err = s.client.do(ctx, req, at)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return at, err
|
||||
}
|
||||
|
||||
// Read an agent token by its ID.
|
||||
func (s *agentTokens) Read(ctx context.Context, agentTokenID string) (*AgentToken, error) {
|
||||
if !validStringID(&agentTokenID) {
|
||||
return nil, errors.New("invalid value for agent token ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("authentication-tokens/%s", url.QueryEscape(agentTokenID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
at := &AgentToken{}
|
||||
err = s.client.do(ctx, req, at)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return at, err
|
||||
}
|
||||
|
||||
// Delete an agent token by its ID.
|
||||
func (s *agentTokens) Delete(ctx context.Context, agentTokenID string) error {
|
||||
if !validStringID(&agentTokenID) {
|
||||
return errors.New("invalid value for agent token ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("authentication-tokens/%s", url.QueryEscape(agentTokenID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ Applies = (*applies)(nil)
|
||||
|
||||
// Applies describes all the apply related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/apply.html
|
||||
type Applies interface {
|
||||
// Read an apply by its ID.
|
||||
Read(ctx context.Context, applyID string) (*Apply, error)
|
||||
|
||||
// Logs retrieves the logs of an apply.
|
||||
Logs(ctx context.Context, applyID string) (io.Reader, error)
|
||||
}
|
||||
|
||||
// applies implements Applys.
|
||||
type applies struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// ApplyStatus represents an apply state.
|
||||
type ApplyStatus string
|
||||
|
||||
//List all available apply statuses.
|
||||
const (
|
||||
ApplyCanceled ApplyStatus = "canceled"
|
||||
ApplyCreated ApplyStatus = "created"
|
||||
ApplyErrored ApplyStatus = "errored"
|
||||
ApplyFinished ApplyStatus = "finished"
|
||||
ApplyMFAWaiting ApplyStatus = "mfa_waiting"
|
||||
ApplyPending ApplyStatus = "pending"
|
||||
ApplyQueued ApplyStatus = "queued"
|
||||
ApplyRunning ApplyStatus = "running"
|
||||
ApplyUnreachable ApplyStatus = "unreachable"
|
||||
)
|
||||
|
||||
// Apply represents a Terraform Enterprise apply.
|
||||
type Apply struct {
|
||||
ID string `jsonapi:"primary,applies"`
|
||||
LogReadURL string `jsonapi:"attr,log-read-url"`
|
||||
ResourceAdditions int `jsonapi:"attr,resource-additions"`
|
||||
ResourceChanges int `jsonapi:"attr,resource-changes"`
|
||||
ResourceDestructions int `jsonapi:"attr,resource-destructions"`
|
||||
Status ApplyStatus `jsonapi:"attr,status"`
|
||||
StatusTimestamps *ApplyStatusTimestamps `jsonapi:"attr,status-timestamps"`
|
||||
}
|
||||
|
||||
// ApplyStatusTimestamps holds the timestamps for individual apply statuses.
|
||||
type ApplyStatusTimestamps struct {
|
||||
CanceledAt time.Time `json:"canceled-at"`
|
||||
ErroredAt time.Time `json:"errored-at"`
|
||||
FinishedAt time.Time `json:"finished-at"`
|
||||
ForceCanceledAt time.Time `json:"force-canceled-at"`
|
||||
QueuedAt time.Time `json:"queued-at"`
|
||||
StartedAt time.Time `json:"started-at"`
|
||||
}
|
||||
|
||||
// Read an apply by its ID.
|
||||
func (s *applies) Read(ctx context.Context, applyID string) (*Apply, error) {
|
||||
if !validStringID(&applyID) {
|
||||
return nil, errors.New("invalid value for apply ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("applies/%s", url.QueryEscape(applyID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := &Apply{}
|
||||
err = s.client.do(ctx, req, a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Logs retrieves the logs of an apply.
|
||||
func (s *applies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
|
||||
if !validStringID(&applyID) {
|
||||
return nil, errors.New("invalid value for apply ID")
|
||||
}
|
||||
|
||||
// Get the apply to make sure it exists.
|
||||
a, err := s.Read(ctx, applyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return an error if the log URL is empty.
|
||||
if a.LogReadURL == "" {
|
||||
return nil, fmt.Errorf("apply %s does not have a log URL", applyID)
|
||||
}
|
||||
|
||||
u, err := url.Parse(a.LogReadURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid log URL: %v", err)
|
||||
}
|
||||
|
||||
done := func() (bool, error) {
|
||||
a, err := s.Read(ctx, a.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch a.Status {
|
||||
case ApplyCanceled, ApplyErrored, ApplyFinished, ApplyUnreachable:
|
||||
return true, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return &LogReader{
|
||||
client: s.client,
|
||||
ctx: ctx,
|
||||
done: done,
|
||||
logURL: u,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
slug "github.com/hashicorp/go-slug"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ ConfigurationVersions = (*configurationVersions)(nil)
|
||||
|
||||
// ConfigurationVersions describes all the configuration version related
|
||||
// methods that the Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/configuration-versions.html
|
||||
type ConfigurationVersions interface {
|
||||
// List returns all configuration versions of a workspace.
|
||||
List(ctx context.Context, workspaceID string, options ConfigurationVersionListOptions) (*ConfigurationVersionList, error)
|
||||
|
||||
// Create is used to create a new configuration version. The created
|
||||
// configuration version will be usable once data is uploaded to it.
|
||||
Create(ctx context.Context, workspaceID string, options ConfigurationVersionCreateOptions) (*ConfigurationVersion, error)
|
||||
|
||||
// Read a configuration version by its ID.
|
||||
Read(ctx context.Context, cvID string) (*ConfigurationVersion, error)
|
||||
|
||||
// Upload packages and uploads Terraform configuration files. It requires
|
||||
// the upload URL from a configuration version and the full path to the
|
||||
// configuration files on disk.
|
||||
Upload(ctx context.Context, url string, path string) error
|
||||
}
|
||||
|
||||
// configurationVersions implements ConfigurationVersions.
|
||||
type configurationVersions struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// ConfigurationStatus represents a configuration version status.
|
||||
type ConfigurationStatus string
|
||||
|
||||
//List all available configuration version statuses.
|
||||
const (
|
||||
ConfigurationErrored ConfigurationStatus = "errored"
|
||||
ConfigurationPending ConfigurationStatus = "pending"
|
||||
ConfigurationUploaded ConfigurationStatus = "uploaded"
|
||||
)
|
||||
|
||||
// ConfigurationSource represents a source of a configuration version.
|
||||
type ConfigurationSource string
|
||||
|
||||
// List all available configuration version sources.
|
||||
const (
|
||||
ConfigurationSourceAPI ConfigurationSource = "tfe-api"
|
||||
ConfigurationSourceBitbucket ConfigurationSource = "bitbucket"
|
||||
ConfigurationSourceGithub ConfigurationSource = "github"
|
||||
ConfigurationSourceGitlab ConfigurationSource = "gitlab"
|
||||
ConfigurationSourceTerraform ConfigurationSource = "terraform"
|
||||
)
|
||||
|
||||
// ConfigurationVersionList represents a list of configuration versions.
|
||||
type ConfigurationVersionList struct {
|
||||
*Pagination
|
||||
Items []*ConfigurationVersion
|
||||
}
|
||||
|
||||
// ConfigurationVersion is a representation of an uploaded or ingressed
|
||||
// Terraform configuration in TFE. A workspace must have at least one
|
||||
// configuration version before any runs may be queued on it.
|
||||
type ConfigurationVersion struct {
|
||||
ID string `jsonapi:"primary,configuration-versions"`
|
||||
AutoQueueRuns bool `jsonapi:"attr,auto-queue-runs"`
|
||||
Error string `jsonapi:"attr,error"`
|
||||
ErrorMessage string `jsonapi:"attr,error-message"`
|
||||
Source ConfigurationSource `jsonapi:"attr,source"`
|
||||
Speculative bool `jsonapi:"attr,speculative "`
|
||||
Status ConfigurationStatus `jsonapi:"attr,status"`
|
||||
StatusTimestamps *CVStatusTimestamps `jsonapi:"attr,status-timestamps"`
|
||||
UploadURL string `jsonapi:"attr,upload-url"`
|
||||
}
|
||||
|
||||
// CVStatusTimestamps holds the timestamps for individual configuration version
|
||||
// statuses.
|
||||
type CVStatusTimestamps struct {
|
||||
FinishedAt time.Time `json:"finished-at"`
|
||||
QueuedAt time.Time `json:"queued-at"`
|
||||
StartedAt time.Time `json:"started-at"`
|
||||
}
|
||||
|
||||
// ConfigurationVersionListOptions represents the options for listing
|
||||
// configuration versions.
|
||||
type ConfigurationVersionListOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// List returns all configuration versions of a workspace.
|
||||
func (s *configurationVersions) List(ctx context.Context, workspaceID string, options ConfigurationVersionListOptions) (*ConfigurationVersionList, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/configuration-versions", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cvl := &ConfigurationVersionList{}
|
||||
err = s.client.do(ctx, req, cvl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cvl, nil
|
||||
}
|
||||
|
||||
// ConfigurationVersionCreateOptions represents the options for creating a
|
||||
// configuration version.
|
||||
type ConfigurationVersionCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,configuration-versions"`
|
||||
|
||||
// When true, runs are queued automatically when the configuration version
|
||||
// is uploaded.
|
||||
AutoQueueRuns *bool `jsonapi:"attr,auto-queue-runs,omitempty"`
|
||||
|
||||
// When true, this configuration version can only be used for planning.
|
||||
Speculative *bool `jsonapi:"attr,speculative,omitempty"`
|
||||
}
|
||||
|
||||
// Create is used to create a new configuration version. The created
|
||||
// configuration version will be usable once data is uploaded to it.
|
||||
func (s *configurationVersions) Create(ctx context.Context, workspaceID string, options ConfigurationVersionCreateOptions) (*ConfigurationVersion, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/configuration-versions", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cv := &ConfigurationVersion{}
|
||||
err = s.client.do(ctx, req, cv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cv, nil
|
||||
}
|
||||
|
||||
// Read a configuration version by its ID.
|
||||
func (s *configurationVersions) Read(ctx context.Context, cvID string) (*ConfigurationVersion, error) {
|
||||
if !validStringID(&cvID) {
|
||||
return nil, errors.New("invalid value for configuration version ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("configuration-versions/%s", url.QueryEscape(cvID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cv := &ConfigurationVersion{}
|
||||
err = s.client.do(ctx, req, cv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cv, nil
|
||||
}
|
||||
|
||||
// Upload packages and uploads Terraform configuration files. It requires the
|
||||
// upload URL from a configuration version and the path to the configuration
|
||||
// files on disk.
|
||||
func (s *configurationVersions) Upload(ctx context.Context, url, path string) error {
|
||||
file, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !file.Mode().IsDir() {
|
||||
return errors.New("path needs to be an existing directory")
|
||||
}
|
||||
|
||||
body := bytes.NewBuffer(nil)
|
||||
|
||||
_, err = slug.Pack(path, body, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := s.client.newRequest("PUT", url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ CostEstimates = (*costEstimates)(nil)
|
||||
|
||||
// CostEstimates describes all the costEstimate related methods that
|
||||
// the Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/ (TBD)
|
||||
type CostEstimates interface {
|
||||
// Read a costEstimate by its ID.
|
||||
Read(ctx context.Context, costEstimateID string) (*CostEstimate, error)
|
||||
|
||||
// Logs retrieves the logs of a costEstimate.
|
||||
Logs(ctx context.Context, costEstimateID string) (io.Reader, error)
|
||||
}
|
||||
|
||||
// costEstimates implements CostEstimates.
|
||||
type costEstimates struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// CostEstimateStatus represents a costEstimate state.
|
||||
type CostEstimateStatus string
|
||||
|
||||
// List all available costEstimate statuses.
|
||||
const (
|
||||
CostEstimateCanceled CostEstimateStatus = "canceled"
|
||||
CostEstimateErrored CostEstimateStatus = "errored"
|
||||
CostEstimateFinished CostEstimateStatus = "finished"
|
||||
CostEstimatePending CostEstimateStatus = "pending"
|
||||
CostEstimateQueued CostEstimateStatus = "queued"
|
||||
CostEstimateSkippedDueToTargeting CostEstimateStatus = "skipped_due_to_targeting"
|
||||
)
|
||||
|
||||
// CostEstimate represents a Terraform Enterprise costEstimate.
|
||||
type CostEstimate struct {
|
||||
ID string `jsonapi:"primary,cost-estimates"`
|
||||
DeltaMonthlyCost string `jsonapi:"attr,delta-monthly-cost"`
|
||||
ErrorMessage string `jsonapi:"attr,error-message"`
|
||||
MatchedResourcesCount int `jsonapi:"attr,matched-resources-count"`
|
||||
PriorMonthlyCost string `jsonapi:"attr,prior-monthly-cost"`
|
||||
ProposedMonthlyCost string `jsonapi:"attr,proposed-monthly-cost"`
|
||||
ResourcesCount int `jsonapi:"attr,resources-count"`
|
||||
Status CostEstimateStatus `jsonapi:"attr,status"`
|
||||
StatusTimestamps *CostEstimateStatusTimestamps `jsonapi:"attr,status-timestamps"`
|
||||
UnmatchedResourcesCount int `jsonapi:"attr,unmatched-resources-count"`
|
||||
}
|
||||
|
||||
// CostEstimateStatusTimestamps holds the timestamps for individual costEstimate statuses.
|
||||
type CostEstimateStatusTimestamps struct {
|
||||
CanceledAt time.Time `json:"canceled-at"`
|
||||
ErroredAt time.Time `json:"errored-at"`
|
||||
FinishedAt time.Time `json:"finished-at"`
|
||||
PendingAt time.Time `json:"pending-at"`
|
||||
QueuedAt time.Time `json:"queued-at"`
|
||||
SkippedDueToTargetingAt time.Time `json:"skipped-due-to-targeting-at"`
|
||||
}
|
||||
|
||||
// Read a costEstimate by its ID.
|
||||
func (s *costEstimates) Read(ctx context.Context, costEstimateID string) (*CostEstimate, error) {
|
||||
if !validStringID(&costEstimateID) {
|
||||
return nil, errors.New("invalid value for cost estimate ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("cost-estimates/%s", url.QueryEscape(costEstimateID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ce := &CostEstimate{}
|
||||
err = s.client.do(ctx, req, ce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ce, nil
|
||||
}
|
||||
|
||||
// Logs retrieves the logs of a costEstimate.
|
||||
func (s *costEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) {
|
||||
if !validStringID(&costEstimateID) {
|
||||
return nil, errors.New("invalid value for cost estimate ID")
|
||||
}
|
||||
|
||||
// Loop until the context is canceled or the cost estimate is finished
|
||||
// running. The cost estimate logs are not streamed and so only available
|
||||
// once the estimate is finished.
|
||||
for {
|
||||
// Get the costEstimate to make sure it exists.
|
||||
ce, err := s.Read(ctx, costEstimateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch ce.Status {
|
||||
case CostEstimateQueued:
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(1000 * time.Millisecond):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("cost-estimates/%s/output", url.QueryEscape(costEstimateID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logs := bytes.NewBuffer(nil)
|
||||
err = s.client.do(ctx, req, logs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
module github.com/hashicorp/go-tfe
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/go-querystring v1.0.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0
|
||||
github.com/hashicorp/go-retryablehttp v0.5.2
|
||||
github.com/hashicorp/go-slug v0.4.1
|
||||
github.com/hashicorp/go-uuid v1.0.1
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
|
||||
)
|
||||
|
||||
go 1.14
|
|
@ -0,0 +1,23 @@
|
|||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.2 h1:AoISa4P4IsW0/m4T6St8Yw38gTl5GtBAgfkhYh1xAz4=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-slug v0.4.1 h1:/jAo8dNuLgSImoLXaX7Od7QB4TfYCVPam+OpAt5bZqc=
|
||||
github.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8=
|
||||
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d h1:Z4EH+5EffvBEhh37F0C0DnpklTMh00JOkjW5zK3ofBI=
|
||||
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
@ -0,0 +1,96 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ IPRanges = (*ipRanges)(nil)
|
||||
|
||||
// IP Ranges provides a list of Terraform Cloud and Enterprise's IP ranges.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/cloud/api/ip-ranges.html
|
||||
type IPRanges interface {
|
||||
// Retrieve TFC IP ranges. If `modifiedSince` is not an empty string
|
||||
// then it will only return the IP ranges changes since that date.
|
||||
// The format for `modifiedSince` can be found here:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
|
||||
Read(ctx context.Context, modifiedSince string) (*IPRange, error)
|
||||
}
|
||||
|
||||
type ipRanges struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
type IPRange struct {
|
||||
// List of IP ranges in CIDR notation used for connections from user site to Terraform Cloud APIs
|
||||
API []string `json:"api"`
|
||||
// List of IP ranges in CIDR notation used for notifications
|
||||
Notifications []string `json:"notifications"`
|
||||
// List of IP ranges in CIDR notation used for outbound requests from Sentinel policies
|
||||
Sentinel []string `json:"sentinel"`
|
||||
// List of IP ranges in CIDR notation used for connecting to VCS providers
|
||||
VCS []string `json:"vcs"`
|
||||
}
|
||||
|
||||
func (i *ipRanges) Read(ctx context.Context, modifiedSince string) (*IPRange, error) {
|
||||
i.client.baseURL.Path = "/api/"
|
||||
req, err := i.client.newRequest("GET", "meta/ip-ranges", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if modifiedSince != "" {
|
||||
req.Header.Add("If-Modified-Since", modifiedSince)
|
||||
}
|
||||
|
||||
ir := &IPRange{}
|
||||
err = i.customDo(ctx, req, ir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ir, nil
|
||||
}
|
||||
|
||||
// The IP ranges API is not returning jsonapi like every other endpoint
|
||||
// which means we need to handle it differently.
|
||||
func (i *ipRanges) customDo(ctx context.Context, req *retryablehttp.Request, ir *IPRange) error {
|
||||
// Wait will block until the limiter can obtain a new token
|
||||
// or returns an error if the given context is canceled.
|
||||
if err := i.client.limiter.Wait(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add the context to the request.
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
// Execute the request and check the response.
|
||||
resp, err := i.client.http.Do(req)
|
||||
if err != nil {
|
||||
// If we got an error, and the context has been canceled,
|
||||
// the context's error is probably more useful.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 && resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("Error HTTP response while retrieving IP ranges: %d", resp.StatusCode)
|
||||
} else if resp.StatusCode == 304 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(ir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogReader implements io.Reader for streaming logs.
|
||||
type LogReader struct {
|
||||
client *Client
|
||||
ctx context.Context
|
||||
done func() (bool, error)
|
||||
logURL *url.URL
|
||||
offset int64
|
||||
reads int
|
||||
startOfText bool
|
||||
endOfText bool
|
||||
}
|
||||
|
||||
// backoff will perform exponential backoff based on the iteration and
|
||||
// limited by the provided min and max (in milliseconds) durations.
|
||||
func backoff(min, max float64, iter int) time.Duration {
|
||||
backoff := math.Pow(2, float64(iter)/5) * min
|
||||
if backoff > max {
|
||||
backoff = max
|
||||
}
|
||||
return time.Duration(backoff) * time.Millisecond
|
||||
}
|
||||
|
||||
func (r *LogReader) Read(l []byte) (int, error) {
|
||||
if written, err := r.read(l); err != io.ErrNoProgress {
|
||||
return written, err
|
||||
}
|
||||
|
||||
// Loop until we can any data, the context is canceled or the
|
||||
// run is finsished. If we would return right away without any
|
||||
// data, we could and up causing a io.ErrNoProgress error.
|
||||
for r.reads = 1; ; r.reads++ {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return 0, r.ctx.Err()
|
||||
case <-time.After(backoff(500, 2000, r.reads)):
|
||||
if written, err := r.read(l); err != io.ErrNoProgress {
|
||||
return written, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LogReader) read(l []byte) (int, error) {
|
||||
// Update the query string.
|
||||
r.logURL.RawQuery = fmt.Sprintf("limit=%d&offset=%d", len(l), r.offset)
|
||||
|
||||
// Create a new request.
|
||||
req, err := http.NewRequest("GET", r.logURL.String(), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req = req.WithContext(r.ctx)
|
||||
|
||||
// Attach the default headers.
|
||||
for k, v := range r.client.headers {
|
||||
req.Header[k] = v
|
||||
}
|
||||
|
||||
// Retrieve the next chunk.
|
||||
resp, err := r.client.http.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Basic response checking.
|
||||
if err := checkResponseCode(resp); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Read the retrieved chunk.
|
||||
written, err := resp.Body.Read(l)
|
||||
if err != nil && err != io.EOF {
|
||||
// Ignore io.EOF errors returned when reading from the response
|
||||
// body as this indicates the end of the chunk and not the end
|
||||
// of the logfile.
|
||||
return written, err
|
||||
}
|
||||
|
||||
if written > 0 {
|
||||
// Check for an STX (Start of Text) ASCII control marker.
|
||||
if !r.startOfText && l[0] == byte(2) {
|
||||
r.startOfText = true
|
||||
|
||||
// Remove the STX marker from the received chunk.
|
||||
copy(l[:written-1], l[1:])
|
||||
l[written-1] = byte(0)
|
||||
r.offset++
|
||||
written--
|
||||
|
||||
// Return early if we only received the STX marker.
|
||||
if written == 0 {
|
||||
return 0, io.ErrNoProgress
|
||||
}
|
||||
}
|
||||
|
||||
// If we found an STX ASCII control character, start looking for
|
||||
// the ETX (End of Text) control character.
|
||||
if r.startOfText && l[written-1] == byte(3) {
|
||||
r.endOfText = true
|
||||
|
||||
// Remove the ETX marker from the received chunk.
|
||||
l[written-1] = byte(0)
|
||||
r.offset++
|
||||
written--
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to continue the loop and wait 500 miliseconds
|
||||
// before checking if there is a new chunk available or that the
|
||||
// run is finished and we are done reading all chunks.
|
||||
if written == 0 {
|
||||
if (r.startOfText && r.endOfText) || // The logstream finished without issues.
|
||||
(r.startOfText && r.reads%10 == 0) || // The logstream terminated unexpectedly.
|
||||
(!r.startOfText && r.reads > 1) { // The logstream doesn't support STX/ETX.
|
||||
done, err := r.done()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if done {
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
return 0, io.ErrNoProgress
|
||||
}
|
||||
|
||||
// Update the offset for the next read.
|
||||
r.offset += int64(written)
|
||||
|
||||
return written, nil
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ NotificationConfigurations = (*notificationConfigurations)(nil)
|
||||
|
||||
// NotificationConfigurations describes all the Notification Configuration
|
||||
// related methods that the Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/notification-configurations.html
|
||||
type NotificationConfigurations interface {
|
||||
// List all the notification configurations within a workspace.
|
||||
List(ctx context.Context, workspaceID string, options NotificationConfigurationListOptions) (*NotificationConfigurationList, error)
|
||||
|
||||
// Create a new notification configuration with the given options.
|
||||
Create(ctx context.Context, workspaceID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error)
|
||||
|
||||
// Read a notification configuration by its ID.
|
||||
Read(ctx context.Context, notificationConfigurationID string) (*NotificationConfiguration, error)
|
||||
|
||||
// Update an existing notification configuration.
|
||||
Update(ctx context.Context, notificationConfigurationID string, options NotificationConfigurationUpdateOptions) (*NotificationConfiguration, error)
|
||||
|
||||
// Delete a notification configuration by its ID.
|
||||
Delete(ctx context.Context, notificationConfigurationID string) error
|
||||
|
||||
// Verify a notification configuration by its ID.
|
||||
Verify(ctx context.Context, notificationConfigurationID string) (*NotificationConfiguration, error)
|
||||
}
|
||||
|
||||
// notificationConfigurations implements NotificationConfigurations.
|
||||
type notificationConfigurations struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// List of available notification triggers.
|
||||
const (
|
||||
NotificationTriggerCreated string = "run:created"
|
||||
NotificationTriggerPlanning string = "run:planning"
|
||||
NotificationTriggerNeedsAttention string = "run:needs_attention"
|
||||
NotificationTriggerApplying string = "run:applying"
|
||||
NotificationTriggerCompleted string = "run:completed"
|
||||
NotificationTriggerErrored string = "run:errored"
|
||||
)
|
||||
|
||||
// NotificationDestinationType represents the destination type of the
|
||||
// notification configuration.
|
||||
type NotificationDestinationType string
|
||||
|
||||
// List of available notification destination types.
|
||||
const (
|
||||
NotificationDestinationTypeEmail NotificationDestinationType = "email"
|
||||
NotificationDestinationTypeGeneric NotificationDestinationType = "generic"
|
||||
NotificationDestinationTypeSlack NotificationDestinationType = "slack"
|
||||
)
|
||||
|
||||
// NotificationConfigurationList represents a list of Notification
|
||||
// Configurations.
|
||||
type NotificationConfigurationList struct {
|
||||
*Pagination
|
||||
Items []*NotificationConfiguration
|
||||
}
|
||||
|
||||
// NotificationConfiguration represents a Notification Configuration.
|
||||
type NotificationConfiguration struct {
|
||||
ID string `jsonapi:"primary,notification-configurations"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
DeliveryResponses []*DeliveryResponse `jsonapi:"attr,delivery-responses"`
|
||||
DestinationType NotificationDestinationType `jsonapi:"attr,destination-type"`
|
||||
Enabled bool `jsonapi:"attr,enabled"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
Token string `jsonapi:"attr,token"`
|
||||
Triggers []string `jsonapi:"attr,triggers"`
|
||||
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
|
||||
URL string `jsonapi:"attr,url"`
|
||||
|
||||
// EmailAddresses is only available for TFE users. It is not available in TFC.
|
||||
EmailAddresses []string `jsonapi:"attr,email-addresses"`
|
||||
|
||||
// Relations
|
||||
Subscribable *Workspace `jsonapi:"relation,subscribable"`
|
||||
EmailUsers []*User `jsonapi:"relation,users"`
|
||||
}
|
||||
|
||||
// DeliveryResponse represents a notification configuration delivery response.
|
||||
type DeliveryResponse struct {
|
||||
Body string `json:"body"`
|
||||
Code int `json:"code"`
|
||||
Headers http.Header `json:"headers"`
|
||||
SentAt time.Time `json:"sent-at,iso8601"`
|
||||
Successful bool `json:"successful"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// NotificationConfigurationListOptions represents the options for listing
|
||||
// notification configurations.
|
||||
type NotificationConfigurationListOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// List all the notification configurations associated with a workspace.
|
||||
func (s *notificationConfigurations) List(ctx context.Context, workspaceID string, options NotificationConfigurationListOptions) (*NotificationConfigurationList, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/notification-configurations", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("GET", u, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ncl := &NotificationConfigurationList{}
|
||||
err = s.client.do(ctx, req, ncl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ncl, nil
|
||||
}
|
||||
|
||||
// NotificationConfigurationCreateOptions represents the options for
|
||||
// creating a new notification configuration.
|
||||
type NotificationConfigurationCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,notification-configurations"`
|
||||
|
||||
// The destination type of the notification configuration
|
||||
DestinationType *NotificationDestinationType `jsonapi:"attr,destination-type"`
|
||||
|
||||
// Whether the notification configuration should be enabled or not
|
||||
Enabled *bool `jsonapi:"attr,enabled"`
|
||||
|
||||
// The name of the notification configuration
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
|
||||
// The token of the notification configuration
|
||||
Token *string `jsonapi:"attr,token,omitempty"`
|
||||
|
||||
// The list of run events that will trigger notifications.
|
||||
Triggers []string `jsonapi:"attr,triggers,omitempty"`
|
||||
|
||||
// The url of the notification configuration
|
||||
URL *string `jsonapi:"attr,url,omitempty"`
|
||||
|
||||
// The list of email addresses that will receive notification emails.
|
||||
// EmailAddresses is only available for TFE users. It is not available in TFC.
|
||||
EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"`
|
||||
|
||||
// The list of users belonging to the organization that will receive notification emails.
|
||||
EmailUsers []*User `jsonapi:"relation,users,omitempty"`
|
||||
}
|
||||
|
||||
func (o NotificationConfigurationCreateOptions) valid() error {
|
||||
if o.DestinationType == nil {
|
||||
return errors.New("destination type is required")
|
||||
}
|
||||
if o.Enabled == nil {
|
||||
return errors.New("enabled is required")
|
||||
}
|
||||
if !validString(o.Name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
if *o.DestinationType == NotificationDestinationTypeGeneric || *o.DestinationType == NotificationDestinationTypeSlack {
|
||||
if o.URL == nil {
|
||||
return errors.New("url is required")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Creates a notification configuration with the given options.
|
||||
func (s *notificationConfigurations) Create(ctx context.Context, workspaceID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/notification-configurations", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc := &NotificationConfiguration{}
|
||||
err = s.client.do(ctx, req, nc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
// Read a notification configuration by its ID.
|
||||
func (s *notificationConfigurations) Read(ctx context.Context, notificationConfigurationID string) (*NotificationConfiguration, error) {
|
||||
if !validStringID(¬ificationConfigurationID) {
|
||||
return nil, errors.New("invalid value for notification configuration ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("notification-configurations/%s", url.QueryEscape(notificationConfigurationID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc := &NotificationConfiguration{}
|
||||
err = s.client.do(ctx, req, nc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
// NotificationConfigurationUpdateOptions represents the options for
|
||||
// updating a existing notification configuration.
|
||||
type NotificationConfigurationUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,notification-configurations"`
|
||||
|
||||
// Whether the notification configuration should be enabled or not
|
||||
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
|
||||
|
||||
// The name of the notification configuration
|
||||
Name *string `jsonapi:"attr,name,omitempty"`
|
||||
|
||||
// The token of the notification configuration
|
||||
Token *string `jsonapi:"attr,token,omitempty"`
|
||||
|
||||
// The list of run events that will trigger notifications.
|
||||
Triggers []string `jsonapi:"attr,triggers,omitempty"`
|
||||
|
||||
// The url of the notification configuration
|
||||
URL *string `jsonapi:"attr,url,omitempty"`
|
||||
|
||||
// The list of email addresses that will receive notification emails.
|
||||
// EmailAddresses is only available for TFE users. It is not available in TFC.
|
||||
EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"`
|
||||
|
||||
// The list of users belonging to the organization that will receive notification emails.
|
||||
EmailUsers []*User `jsonapi:"relation,users,omitempty"`
|
||||
}
|
||||
|
||||
// Updates a notification configuration with the given options.
|
||||
func (s *notificationConfigurations) Update(ctx context.Context, notificationConfigurationID string, options NotificationConfigurationUpdateOptions) (*NotificationConfiguration, error) {
|
||||
if !validStringID(¬ificationConfigurationID) {
|
||||
return nil, errors.New("invalid value for notification configuration ID")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("notification-configurations/%s", url.QueryEscape(notificationConfigurationID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc := &NotificationConfiguration{}
|
||||
err = s.client.do(ctx, req, nc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
// Delete a notifications configuration by its ID.
|
||||
func (s *notificationConfigurations) Delete(ctx context.Context, notificationConfigurationID string) error {
|
||||
if !validStringID(¬ificationConfigurationID) {
|
||||
return errors.New("invalid value for notification configuration ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("notification-configurations/%s", url.QueryEscape(notificationConfigurationID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Verifies a notification configuration by delivering a verification
|
||||
// payload to the configured url.
|
||||
func (s *notificationConfigurations) Verify(ctx context.Context, notificationConfigurationID string) (*NotificationConfiguration, error) {
|
||||
if !validStringID(¬ificationConfigurationID) {
|
||||
return nil, errors.New("invalid value for notification configuration ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"notification-configurations/%s/actions/verify", url.QueryEscape(notificationConfigurationID))
|
||||
req, err := s.client.newRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc := &NotificationConfiguration{}
|
||||
err = s.client.do(ctx, req, nc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nc, nil
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ OAuthClients = (*oAuthClients)(nil)
|
||||
|
||||
// OAuthClients describes all the OAuth client related methods that the
|
||||
// Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/oauth-clients.html
|
||||
type OAuthClients interface {
|
||||
// List all the OAuth clients for a given organization.
|
||||
List(ctx context.Context, organization string, options OAuthClientListOptions) (*OAuthClientList, error)
|
||||
|
||||
// Create an OAuth client to connect an organization and a VCS provider.
|
||||
Create(ctx context.Context, organization string, options OAuthClientCreateOptions) (*OAuthClient, error)
|
||||
|
||||
// Read an OAuth client by its ID.
|
||||
Read(ctx context.Context, oAuthClientID string) (*OAuthClient, error)
|
||||
|
||||
// Delete an OAuth client by its ID.
|
||||
Delete(ctx context.Context, oAuthClientID string) error
|
||||
}
|
||||
|
||||
// oAuthClients implements OAuthClients.
|
||||
type oAuthClients struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// ServiceProviderType represents a VCS type.
|
||||
type ServiceProviderType string
|
||||
|
||||
// List of available VCS types.
|
||||
const (
|
||||
ServiceProviderAzureDevOpsServer ServiceProviderType = "ado_server"
|
||||
ServiceProviderAzureDevOpsServices ServiceProviderType = "ado_services"
|
||||
ServiceProviderBitbucket ServiceProviderType = "bitbucket_hosted"
|
||||
// Bitbucket Server v5.4.0 and above
|
||||
ServiceProviderBitbucketServer ServiceProviderType = "bitbucket_server"
|
||||
// Bitbucket Server v5.3.0 and below
|
||||
ServiceProviderBitbucketServerLegacy ServiceProviderType = "bitbucket_server_legacy"
|
||||
ServiceProviderGithub ServiceProviderType = "github"
|
||||
ServiceProviderGithubEE ServiceProviderType = "github_enterprise"
|
||||
ServiceProviderGitlab ServiceProviderType = "gitlab_hosted"
|
||||
ServiceProviderGitlabCE ServiceProviderType = "gitlab_community_edition"
|
||||
ServiceProviderGitlabEE ServiceProviderType = "gitlab_enterprise_edition"
|
||||
)
|
||||
|
||||
// OAuthClientList represents a list of OAuth clients.
|
||||
type OAuthClientList struct {
|
||||
*Pagination
|
||||
Items []*OAuthClient
|
||||
}
|
||||
|
||||
// OAuthClient represents a connection between an organization and a VCS
|
||||
// provider.
|
||||
type OAuthClient struct {
|
||||
ID string `jsonapi:"primary,oauth-clients"`
|
||||
APIURL string `jsonapi:"attr,api-url"`
|
||||
CallbackURL string `jsonapi:"attr,callback-url"`
|
||||
ConnectPath string `jsonapi:"attr,connect-path"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
HTTPURL string `jsonapi:"attr,http-url"`
|
||||
Key string `jsonapi:"attr,key"`
|
||||
RSAPublicKey string `jsonapi:"attr,rsa-public-key"`
|
||||
ServiceProvider ServiceProviderType `jsonapi:"attr,service-provider"`
|
||||
ServiceProviderName string `jsonapi:"attr,service-provider-display-name"`
|
||||
|
||||
// Relations
|
||||
Organization *Organization `jsonapi:"relation,organization"`
|
||||
OAuthTokens []*OAuthToken `jsonapi:"relation,oauth-tokens"`
|
||||
}
|
||||
|
||||
// OAuthClientListOptions represents the options for listing
|
||||
// OAuth clients.
|
||||
type OAuthClientListOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// List all the OAuth clients for a given organization.
|
||||
func (s *oAuthClients) List(ctx context.Context, organization string, options OAuthClientListOptions) (*OAuthClientList, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/oauth-clients", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ocl := &OAuthClientList{}
|
||||
err = s.client.do(ctx, req, ocl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ocl, nil
|
||||
}
|
||||
|
||||
// OAuthClientCreateOptions represents the options for creating an OAuth client.
|
||||
type OAuthClientCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,oauth-clients"`
|
||||
|
||||
// The base URL of your VCS provider's API.
|
||||
APIURL *string `jsonapi:"attr,api-url"`
|
||||
|
||||
// The homepage of your VCS provider.
|
||||
HTTPURL *string `jsonapi:"attr,http-url"`
|
||||
|
||||
// The token string you were given by your VCS provider.
|
||||
OAuthToken *string `jsonapi:"attr,oauth-token-string"`
|
||||
|
||||
// Private key associated with this vcs provider - only available for ado_server
|
||||
PrivateKey *string `jsonapi:"attr,private-key"`
|
||||
|
||||
// The VCS provider being connected with.
|
||||
ServiceProvider *ServiceProviderType `jsonapi:"attr,service-provider"`
|
||||
}
|
||||
|
||||
func (o OAuthClientCreateOptions) valid() error {
|
||||
if !validString(o.APIURL) {
|
||||
return errors.New("API URL is required")
|
||||
}
|
||||
if !validString(o.HTTPURL) {
|
||||
return errors.New("HTTP URL is required")
|
||||
}
|
||||
if !validString(o.OAuthToken) {
|
||||
return errors.New("OAuth token is required")
|
||||
}
|
||||
if o.ServiceProvider == nil {
|
||||
return errors.New("service provider is required")
|
||||
}
|
||||
if validString(o.PrivateKey) && *o.ServiceProvider != *ServiceProvider(ServiceProviderAzureDevOpsServer) {
|
||||
return errors.New("Private Key can only be present with Azure DevOps Server service provider")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create an OAuth client to connect an organization and a VCS provider.
|
||||
func (s *oAuthClients) Create(ctx context.Context, organization string, options OAuthClientCreateOptions) (*OAuthClient, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/oauth-clients", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oc := &OAuthClient{}
|
||||
err = s.client.do(ctx, req, oc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return oc, nil
|
||||
}
|
||||
|
||||
// Read an OAuth client by its ID.
|
||||
func (s *oAuthClients) Read(ctx context.Context, oAuthClientID string) (*OAuthClient, error) {
|
||||
if !validStringID(&oAuthClientID) {
|
||||
return nil, errors.New("invalid value for OAuth client ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oc := &OAuthClient{}
|
||||
err = s.client.do(ctx, req, oc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return oc, err
|
||||
}
|
||||
|
||||
// Delete an OAuth client by its ID.
|
||||
func (s *oAuthClients) Delete(ctx context.Context, oAuthClientID string) error {
|
||||
if !validStringID(&oAuthClientID) {
|
||||
return errors.New("invalid value for OAuth client ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ OAuthTokens = (*oAuthTokens)(nil)
|
||||
|
||||
// OAuthTokens describes all the OAuth token related methods that the
|
||||
// Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/oauth-tokens.html
|
||||
type OAuthTokens interface {
|
||||
// List all the OAuth tokens for a given organization.
|
||||
List(ctx context.Context, organization string, options OAuthTokenListOptions) (*OAuthTokenList, error)
|
||||
// Read a OAuth token by its ID.
|
||||
Read(ctx context.Context, oAuthTokenID string) (*OAuthToken, error)
|
||||
|
||||
// Update an existing OAuth token.
|
||||
Update(ctx context.Context, oAuthTokenID string, options OAuthTokenUpdateOptions) (*OAuthToken, error)
|
||||
|
||||
// Delete a OAuth token by its ID.
|
||||
Delete(ctx context.Context, oAuthTokenID string) error
|
||||
}
|
||||
|
||||
// oAuthTokens implements OAuthTokens.
|
||||
type oAuthTokens struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// OAuthTokenList represents a list of OAuth tokens.
|
||||
type OAuthTokenList struct {
|
||||
*Pagination
|
||||
Items []*OAuthToken
|
||||
}
|
||||
|
||||
// OAuthToken represents a VCS configuration including the associated
|
||||
// OAuth token
|
||||
type OAuthToken struct {
|
||||
ID string `jsonapi:"primary,oauth-tokens"`
|
||||
UID string `jsonapi:"attr,uid"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
HasSSHKey bool `jsonapi:"attr,has-ssh-key"`
|
||||
ServiceProviderUser string `jsonapi:"attr,service-provider-user"`
|
||||
|
||||
// Relations
|
||||
OAuthClient *OAuthClient `jsonapi:"relation,oauth-client"`
|
||||
}
|
||||
|
||||
// OAuthTokenListOptions represents the options for listing
|
||||
// OAuth tokens.
|
||||
type OAuthTokenListOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// List all the OAuth tokens for a given organization.
|
||||
func (s *oAuthTokens) List(ctx context.Context, organization string, options OAuthTokenListOptions) (*OAuthTokenList, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/oauth-tokens", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
otl := &OAuthTokenList{}
|
||||
err = s.client.do(ctx, req, otl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return otl, nil
|
||||
}
|
||||
|
||||
// Read an OAuth token by its ID.
|
||||
func (s *oAuthTokens) Read(ctx context.Context, oAuthTokenID string) (*OAuthToken, error) {
|
||||
if !validStringID(&oAuthTokenID) {
|
||||
return nil, errors.New("invalid value for OAuth token ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ot := &OAuthToken{}
|
||||
err = s.client.do(ctx, req, ot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ot, err
|
||||
}
|
||||
|
||||
// OAuthTokenUpdateOptions represents the options for updating an OAuth token.
|
||||
type OAuthTokenUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,oauth-tokens"`
|
||||
|
||||
// A private SSH key to be used for git clone operations.
|
||||
PrivateSSHKey *string `jsonapi:"attr,ssh-key"`
|
||||
}
|
||||
|
||||
// Update an existing OAuth token.
|
||||
func (s *oAuthTokens) Update(ctx context.Context, oAuthTokenID string, options OAuthTokenUpdateOptions) (*OAuthToken, error) {
|
||||
if !validStringID(&oAuthTokenID) {
|
||||
return nil, errors.New("invalid value for OAuth token ID")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ot := &OAuthToken{}
|
||||
err = s.client.do(ctx, req, ot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ot, err
|
||||
}
|
||||
|
||||
// Delete an OAuth token by its ID.
|
||||
func (s *oAuthTokens) Delete(ctx context.Context, oAuthTokenID string) error {
|
||||
if !validStringID(&oAuthTokenID) {
|
||||
return errors.New("invalid value for OAuth token ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,372 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ Organizations = (*organizations)(nil)
|
||||
|
||||
// Organizations describes all the organization related methods that the
|
||||
// Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/organizations.html
|
||||
type Organizations interface {
|
||||
// List all the organizations visible to the current user.
|
||||
List(ctx context.Context, options OrganizationListOptions) (*OrganizationList, error)
|
||||
|
||||
// Create a new organization with the given options.
|
||||
Create(ctx context.Context, options OrganizationCreateOptions) (*Organization, error)
|
||||
|
||||
// Read an organization by its name.
|
||||
Read(ctx context.Context, organization string) (*Organization, error)
|
||||
|
||||
// Update attributes of an existing organization.
|
||||
Update(ctx context.Context, organization string, options OrganizationUpdateOptions) (*Organization, error)
|
||||
|
||||
// Delete an organization by its name.
|
||||
Delete(ctx context.Context, organization string) error
|
||||
|
||||
// Capacity shows the current run capacity of an organization.
|
||||
Capacity(ctx context.Context, organization string) (*Capacity, error)
|
||||
|
||||
// Entitlements shows the entitlements of an organization.
|
||||
Entitlements(ctx context.Context, organization string) (*Entitlements, error)
|
||||
|
||||
// RunQueue shows the current run queue of an organization.
|
||||
RunQueue(ctx context.Context, organization string, options RunQueueOptions) (*RunQueue, error)
|
||||
}
|
||||
|
||||
// organizations implements Organizations.
|
||||
type organizations struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// AuthPolicyType represents an authentication policy type.
|
||||
type AuthPolicyType string
|
||||
|
||||
// List of available authentication policies.
|
||||
const (
|
||||
AuthPolicyPassword AuthPolicyType = "password"
|
||||
AuthPolicyTwoFactor AuthPolicyType = "two_factor_mandatory"
|
||||
)
|
||||
|
||||
// EnterprisePlanType represents an enterprise plan type.
|
||||
type EnterprisePlanType string
|
||||
|
||||
// List of available enterprise plan types.
|
||||
const (
|
||||
EnterprisePlanDisabled EnterprisePlanType = "disabled"
|
||||
EnterprisePlanPremium EnterprisePlanType = "premium"
|
||||
EnterprisePlanPro EnterprisePlanType = "pro"
|
||||
EnterprisePlanTrial EnterprisePlanType = "trial"
|
||||
)
|
||||
|
||||
// OrganizationList represents a list of organizations.
|
||||
type OrganizationList struct {
|
||||
*Pagination
|
||||
Items []*Organization
|
||||
}
|
||||
|
||||
// Organization represents a Terraform Enterprise organization.
|
||||
type Organization struct {
|
||||
Name string `jsonapi:"primary,organizations"`
|
||||
CollaboratorAuthPolicy AuthPolicyType `jsonapi:"attr,collaborator-auth-policy"`
|
||||
CostEstimationEnabled bool `jsonapi:"attr,cost-estimation-enabled"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
Email string `jsonapi:"attr,email"`
|
||||
EnterprisePlan EnterprisePlanType `jsonapi:"attr,enterprise-plan"`
|
||||
ExternalID string `jsonapi:"attr,external-id"`
|
||||
OwnersTeamSAMLRoleID string `jsonapi:"attr,owners-team-saml-role-id"`
|
||||
Permissions *OrganizationPermissions `jsonapi:"attr,permissions"`
|
||||
SAMLEnabled bool `jsonapi:"attr,saml-enabled"`
|
||||
SessionRemember int `jsonapi:"attr,session-remember"`
|
||||
SessionTimeout int `jsonapi:"attr,session-timeout"`
|
||||
TrialExpiresAt time.Time `jsonapi:"attr,trial-expires-at,iso8601"`
|
||||
TwoFactorConformant bool `jsonapi:"attr,two-factor-conformant"`
|
||||
}
|
||||
|
||||
// Capacity represents the current run capacity of an organization.
|
||||
type Capacity struct {
|
||||
Organization string `jsonapi:"primary,organization-capacity"`
|
||||
Pending int `jsonapi:"attr,pending"`
|
||||
Running int `jsonapi:"attr,running"`
|
||||
}
|
||||
|
||||
// Entitlements represents the entitlements of an organization.
|
||||
type Entitlements struct {
|
||||
ID string `jsonapi:"primary,entitlement-sets"`
|
||||
Agents bool `jsonapi:"attr,agents"`
|
||||
AuditLogging bool `jsonapi:"attr,audit-logging"`
|
||||
CostEstimation bool `jsonapi:"attr,cost-estimation"`
|
||||
Operations bool `jsonapi:"attr,operations"`
|
||||
PrivateModuleRegistry bool `jsonapi:"attr,private-module-registry"`
|
||||
SSO bool `jsonapi:"attr,sso"`
|
||||
Sentinel bool `jsonapi:"attr,sentinel"`
|
||||
StateStorage bool `jsonapi:"attr,state-storage"`
|
||||
Teams bool `jsonapi:"attr,teams"`
|
||||
VCSIntegrations bool `jsonapi:"attr,vcs-integrations"`
|
||||
}
|
||||
|
||||
// RunQueue represents the current run queue of an organization.
|
||||
type RunQueue struct {
|
||||
*Pagination
|
||||
Items []*Run
|
||||
}
|
||||
|
||||
// OrganizationPermissions represents the organization permissions.
|
||||
type OrganizationPermissions struct {
|
||||
CanCreateTeam bool `json:"can-create-team"`
|
||||
CanCreateWorkspace bool `json:"can-create-workspace"`
|
||||
CanCreateWorkspaceMigration bool `json:"can-create-workspace-migration"`
|
||||
CanDestroy bool `json:"can-destroy"`
|
||||
CanTraverse bool `json:"can-traverse"`
|
||||
CanUpdate bool `json:"can-update"`
|
||||
CanUpdateAPIToken bool `json:"can-update-api-token"`
|
||||
CanUpdateOAuth bool `json:"can-update-oauth"`
|
||||
CanUpdateSentinel bool `json:"can-update-sentinel"`
|
||||
}
|
||||
|
||||
// OrganizationListOptions represents the options for listing organizations.
|
||||
type OrganizationListOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// List all the organizations visible to the current user.
|
||||
func (s *organizations) List(ctx context.Context, options OrganizationListOptions) (*OrganizationList, error) {
|
||||
req, err := s.client.newRequest("GET", "organizations", &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgl := &OrganizationList{}
|
||||
err = s.client.do(ctx, req, orgl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orgl, nil
|
||||
}
|
||||
|
||||
// OrganizationCreateOptions represents the options for creating an organization.
|
||||
type OrganizationCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,organizations"`
|
||||
|
||||
// Name of the organization.
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
|
||||
// Admin email address.
|
||||
Email *string `jsonapi:"attr,email"`
|
||||
|
||||
// Session expiration (minutes).
|
||||
SessionRemember *int `jsonapi:"attr,session-remember,omitempty"`
|
||||
|
||||
// Session timeout after inactivity (minutes).
|
||||
SessionTimeout *int `jsonapi:"attr,session-timeout,omitempty"`
|
||||
|
||||
// Authentication policy.
|
||||
CollaboratorAuthPolicy *AuthPolicyType `jsonapi:"attr,collaborator-auth-policy,omitempty"`
|
||||
|
||||
// Enable Cost Estimation
|
||||
CostEstimationEnabled *bool `jsonapi:"attr,cost-estimation-enabled,omitempty"`
|
||||
|
||||
// The name of the "owners" team
|
||||
OwnersTeamSAMLRoleID *string `jsonapi:"attr,owners-team-saml-role-id,omitempty"`
|
||||
}
|
||||
|
||||
func (o OrganizationCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(o.Name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
if !validString(o.Email) {
|
||||
return errors.New("email is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new organization with the given options.
|
||||
func (s *organizations) Create(ctx context.Context, options OrganizationCreateOptions) (*Organization, error) {
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
req, err := s.client.newRequest("POST", "organizations", &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
org := &Organization{}
|
||||
err = s.client.do(ctx, req, org)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return org, nil
|
||||
}
|
||||
|
||||
// Read an organization by its name.
|
||||
func (s *organizations) Read(ctx context.Context, organization string) (*Organization, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
org := &Organization{}
|
||||
err = s.client.do(ctx, req, org)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return org, nil
|
||||
}
|
||||
|
||||
// OrganizationUpdateOptions represents the options for updating an organization.
|
||||
type OrganizationUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,organizations"`
|
||||
|
||||
// New name for the organization.
|
||||
Name *string `jsonapi:"attr,name,omitempty"`
|
||||
|
||||
// New admin email address.
|
||||
Email *string `jsonapi:"attr,email,omitempty"`
|
||||
|
||||
// Session expiration (minutes).
|
||||
SessionRemember *int `jsonapi:"attr,session-remember,omitempty"`
|
||||
|
||||
// Session timeout after inactivity (minutes).
|
||||
SessionTimeout *int `jsonapi:"attr,session-timeout,omitempty"`
|
||||
|
||||
// Authentication policy.
|
||||
CollaboratorAuthPolicy *AuthPolicyType `jsonapi:"attr,collaborator-auth-policy,omitempty"`
|
||||
|
||||
// Enable Cost Estimation
|
||||
CostEstimationEnabled *bool `jsonapi:"attr,cost-estimation-enabled,omitempty"`
|
||||
|
||||
// The name of the "owners" team
|
||||
OwnersTeamSAMLRoleID *string `jsonapi:"attr,owners-team-saml-role-id,omitempty"`
|
||||
}
|
||||
|
||||
// Update attributes of an existing organization.
|
||||
func (s *organizations) Update(ctx context.Context, organization string, options OrganizationUpdateOptions) (*Organization, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
org := &Organization{}
|
||||
err = s.client.do(ctx, req, org)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return org, nil
|
||||
}
|
||||
|
||||
// Delete an organization by its name.
|
||||
func (s *organizations) Delete(ctx context.Context, organization string) error {
|
||||
if !validStringID(&organization) {
|
||||
return errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Capacity shows the currently used capacity of an organization.
|
||||
func (s *organizations) Capacity(ctx context.Context, organization string) (*Capacity, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/capacity", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Capacity{}
|
||||
err = s.client.do(ctx, req, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Entitlements shows the entitlements of an organization.
|
||||
func (s *organizations) Entitlements(ctx context.Context, organization string) (*Entitlements, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/entitlement-set", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := &Entitlements{}
|
||||
err = s.client.do(ctx, req, e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// RunQueueOptions represents the options for showing the queue.
|
||||
type RunQueueOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// RunQueue shows the current run queue of an organization.
|
||||
func (s *organizations) RunQueue(ctx context.Context, organization string, options RunQueueOptions) (*RunQueue, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/runs/queue", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rq := &RunQueue{}
|
||||
err = s.client.do(ctx, req, rq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rq, nil
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ OrganizationMemberships = (*organizationMemberships)(nil)
|
||||
|
||||
// OrganizationMemberships describes all the organization membership related methods that
|
||||
// the Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/cloud/api/organization-memberships.html
|
||||
type OrganizationMemberships interface {
|
||||
// List all the organization memberships of the given organization.
|
||||
List(ctx context.Context, organization string, options OrganizationMembershipListOptions) (*OrganizationMembershipList, error)
|
||||
|
||||
// Create a new organization membership with the given options.
|
||||
Create(ctx context.Context, organization string, options OrganizationMembershipCreateOptions) (*OrganizationMembership, error)
|
||||
|
||||
// Read an organization membership by ID
|
||||
Read(ctx context.Context, organizationMembershipID string) (*OrganizationMembership, error)
|
||||
|
||||
// Read an organization membership by ID with options
|
||||
ReadWithOptions(ctx context.Context, organizationMembershipID string, options OrganizationMembershipReadOptions) (*OrganizationMembership, error)
|
||||
|
||||
// Delete an organization membership by its ID.
|
||||
Delete(ctx context.Context, organizationMembershipID string) error
|
||||
}
|
||||
|
||||
// organizationMemberships implements OrganizationMemberships.
|
||||
type organizationMemberships struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// OrganizationMembershipStatus represents an organization membership status.
|
||||
type OrganizationMembershipStatus string
|
||||
|
||||
// List all available organization membership statuses.
|
||||
const (
|
||||
OrganizationMembershipActive = "active"
|
||||
OrganizationMembershipInvited = "invited"
|
||||
)
|
||||
|
||||
// OrganizationMembershipList represents a list of organization memberships.
|
||||
type OrganizationMembershipList struct {
|
||||
*Pagination
|
||||
Items []*OrganizationMembership
|
||||
}
|
||||
|
||||
// OrganizationMembership represents a Terraform Enterprise organization membership.
|
||||
type OrganizationMembership struct {
|
||||
ID string `jsonapi:"primary,organization-memberships"`
|
||||
Status OrganizationMembershipStatus `jsonapi:"attr,status"`
|
||||
Email string `jsonapi:"attr,email"`
|
||||
|
||||
// Relations
|
||||
Organization *Organization `jsonapi:"relation,organization"`
|
||||
User *User `jsonapi:"relation,user"`
|
||||
Teams []*Team `jsonapi:"relation,teams"`
|
||||
}
|
||||
|
||||
// OrganizationMembershipListOptions represents the options for listing organization memberships.
|
||||
type OrganizationMembershipListOptions struct {
|
||||
ListOptions
|
||||
|
||||
Include string `url:"include"`
|
||||
}
|
||||
|
||||
// List all the organization memberships of the given organization.
|
||||
func (s *organizationMemberships) List(ctx context.Context, organization string, options OrganizationMembershipListOptions) (*OrganizationMembershipList, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/organization-memberships", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ml := &OrganizationMembershipList{}
|
||||
err = s.client.do(ctx, req, ml)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ml, nil
|
||||
}
|
||||
|
||||
// OrganizationMembershipCreateOptions represents the options for creating an organization membership.
|
||||
type OrganizationMembershipCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,organization-memberships"`
|
||||
|
||||
// User's email address.
|
||||
Email *string `jsonapi:"attr,email"`
|
||||
}
|
||||
|
||||
func (o OrganizationMembershipCreateOptions) valid() error {
|
||||
if o.Email == nil {
|
||||
return errors.New("email is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create an organization membership with the given options.
|
||||
func (s *organizationMemberships) Create(ctx context.Context, organization string, options OrganizationMembershipCreateOptions) (*OrganizationMembership, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/organization-memberships", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &OrganizationMembership{}
|
||||
err = s.client.do(ctx, req, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Read an organization membership by its ID.
|
||||
func (s *organizationMemberships) Read(ctx context.Context, organizationMembershipID string) (*OrganizationMembership, error) {
|
||||
return s.ReadWithOptions(ctx, organizationMembershipID, OrganizationMembershipReadOptions{})
|
||||
}
|
||||
|
||||
// OrganizationMembershipReadOptions represents the options for reading organization memberships.
|
||||
type OrganizationMembershipReadOptions struct {
|
||||
Include string `url:"include"`
|
||||
}
|
||||
|
||||
// Read an organization membership by ID with options
|
||||
func (s *organizationMemberships) ReadWithOptions(ctx context.Context, organizationMembershipID string, options OrganizationMembershipReadOptions) (*OrganizationMembership, error) {
|
||||
if !validStringID(&organizationMembershipID) {
|
||||
return nil, errors.New("invalid value for membership")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organization-memberships/%s", url.QueryEscape(organizationMembershipID))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
|
||||
mem := &OrganizationMembership{}
|
||||
err = s.client.do(ctx, req, mem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mem, nil
|
||||
}
|
||||
|
||||
// Delete an organization membership by its ID.
|
||||
func (s *organizationMemberships) Delete(ctx context.Context, organizationMembershipID string) error {
|
||||
if !validStringID(&organizationMembershipID) {
|
||||
return errors.New("invalid value for membership")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organization-memberships/%s", url.QueryEscape(organizationMembershipID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ OrganizationTokens = (*organizationTokens)(nil)
|
||||
|
||||
// OrganizationTokens describes all the organization token related methods
|
||||
// that the Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/organization-tokens.html
|
||||
type OrganizationTokens interface {
|
||||
// Generate a new organization token, replacing any existing token.
|
||||
Generate(ctx context.Context, organization string) (*OrganizationToken, error)
|
||||
|
||||
// Read an organization token.
|
||||
Read(ctx context.Context, organization string) (*OrganizationToken, error)
|
||||
|
||||
// Delete an organization token.
|
||||
Delete(ctx context.Context, organization string) error
|
||||
}
|
||||
|
||||
// organizationTokens implements OrganizationTokens.
|
||||
type organizationTokens struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// OrganizationToken represents a Terraform Enterprise organization token.
|
||||
type OrganizationToken struct {
|
||||
ID string `jsonapi:"primary,authentication-tokens"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
Description string `jsonapi:"attr,description"`
|
||||
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
|
||||
Token string `jsonapi:"attr,token"`
|
||||
}
|
||||
|
||||
// Generate a new organization token, replacing any existing token.
|
||||
func (s *organizationTokens) Generate(ctx context.Context, organization string) (*OrganizationToken, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ot := &OrganizationToken{}
|
||||
err = s.client.do(ctx, req, ot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ot, err
|
||||
}
|
||||
|
||||
// Read an organization token.
|
||||
func (s *organizationTokens) Read(ctx context.Context, organization string) (*OrganizationToken, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ot := &OrganizationToken{}
|
||||
err = s.client.do(ctx, req, ot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ot, err
|
||||
}
|
||||
|
||||
// Delete an organization token.
|
||||
func (s *organizationTokens) Delete(ctx context.Context, organization string) error {
|
||||
if !validStringID(&organization) {
|
||||
return errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ Plans = (*plans)(nil)
|
||||
|
||||
// Plans describes all the plan related methods that the Terraform Enterprise
|
||||
// API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/plan.html
|
||||
type Plans interface {
|
||||
// Read a plan by its ID.
|
||||
Read(ctx context.Context, planID string) (*Plan, error)
|
||||
|
||||
// Logs retrieves the logs of a plan.
|
||||
Logs(ctx context.Context, planID string) (io.Reader, error)
|
||||
}
|
||||
|
||||
// plans implements Plans.
|
||||
type plans struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// PlanStatus represents a plan state.
|
||||
type PlanStatus string
|
||||
|
||||
//List all available plan statuses.
|
||||
const (
|
||||
PlanCanceled PlanStatus = "canceled"
|
||||
PlanCreated PlanStatus = "created"
|
||||
PlanErrored PlanStatus = "errored"
|
||||
PlanFinished PlanStatus = "finished"
|
||||
PlanMFAWaiting PlanStatus = "mfa_waiting"
|
||||
PlanPending PlanStatus = "pending"
|
||||
PlanQueued PlanStatus = "queued"
|
||||
PlanRunning PlanStatus = "running"
|
||||
PlanUnreachable PlanStatus = "unreachable"
|
||||
)
|
||||
|
||||
// Plan represents a Terraform Enterprise plan.
|
||||
type Plan struct {
|
||||
ID string `jsonapi:"primary,plans"`
|
||||
HasChanges bool `jsonapi:"attr,has-changes"`
|
||||
LogReadURL string `jsonapi:"attr,log-read-url"`
|
||||
ResourceAdditions int `jsonapi:"attr,resource-additions"`
|
||||
ResourceChanges int `jsonapi:"attr,resource-changes"`
|
||||
ResourceDestructions int `jsonapi:"attr,resource-destructions"`
|
||||
Status PlanStatus `jsonapi:"attr,status"`
|
||||
StatusTimestamps *PlanStatusTimestamps `jsonapi:"attr,status-timestamps"`
|
||||
|
||||
// Relations
|
||||
Exports []*PlanExport `jsonapi:"relation,exports"`
|
||||
}
|
||||
|
||||
// PlanStatusTimestamps holds the timestamps for individual plan statuses.
|
||||
type PlanStatusTimestamps struct {
|
||||
CanceledAt time.Time `json:"canceled-at"`
|
||||
ErroredAt time.Time `json:"errored-at"`
|
||||
FinishedAt time.Time `json:"finished-at"`
|
||||
ForceCanceledAt time.Time `json:"force-canceled-at"`
|
||||
QueuedAt time.Time `json:"queued-at"`
|
||||
StartedAt time.Time `json:"started-at"`
|
||||
}
|
||||
|
||||
// Read a plan by its ID.
|
||||
func (s *plans) Read(ctx context.Context, planID string) (*Plan, error) {
|
||||
if !validStringID(&planID) {
|
||||
return nil, errors.New("invalid value for plan ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("plans/%s", url.QueryEscape(planID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &Plan{}
|
||||
err = s.client.do(ctx, req, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Logs retrieves the logs of a plan.
|
||||
func (s *plans) Logs(ctx context.Context, planID string) (io.Reader, error) {
|
||||
if !validStringID(&planID) {
|
||||
return nil, errors.New("invalid value for plan ID")
|
||||
}
|
||||
|
||||
// Get the plan to make sure it exists.
|
||||
p, err := s.Read(ctx, planID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return an error if the log URL is empty.
|
||||
if p.LogReadURL == "" {
|
||||
return nil, fmt.Errorf("plan %s does not have a log URL", planID)
|
||||
}
|
||||
|
||||
u, err := url.Parse(p.LogReadURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid log URL: %v", err)
|
||||
}
|
||||
|
||||
done := func() (bool, error) {
|
||||
p, err := s.Read(ctx, p.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch p.Status {
|
||||
case PlanCanceled, PlanErrored, PlanFinished, PlanUnreachable:
|
||||
return true, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return &LogReader{
|
||||
client: s.client,
|
||||
ctx: ctx,
|
||||
done: done,
|
||||
logURL: u,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ PlanExports = (*planExports)(nil)
|
||||
|
||||
// PlanExports describes all the plan export related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/plan-exports.html
|
||||
type PlanExports interface {
|
||||
// Export a plan by its ID with the given options.
|
||||
Create(ctx context.Context, options PlanExportCreateOptions) (*PlanExport, error)
|
||||
|
||||
// Read a plan export by its ID.
|
||||
Read(ctx context.Context, planExportID string) (*PlanExport, error)
|
||||
|
||||
// Delete a plan export by its ID.
|
||||
Delete(ctx context.Context, planExportID string) error
|
||||
|
||||
// Download the data of an plan export.
|
||||
Download(ctx context.Context, planExportID string) ([]byte, error)
|
||||
}
|
||||
|
||||
// planExports implements PlanExports.
|
||||
type planExports struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// PlanExportDataType represents the type of data exported from a plan.
|
||||
type PlanExportDataType string
|
||||
|
||||
// List all available plan export data types.
|
||||
const (
|
||||
PlanExportSentinelMockBundleV0 PlanExportDataType = "sentinel-mock-bundle-v0"
|
||||
)
|
||||
|
||||
// PlanExportStatus represents a plan export state.
|
||||
type PlanExportStatus string
|
||||
|
||||
// List all available plan export statuses.
|
||||
const (
|
||||
PlanExportCanceled PlanExportStatus = "canceled"
|
||||
PlanExportErrored PlanExportStatus = "errored"
|
||||
PlanExportExpired PlanExportStatus = "expired"
|
||||
PlanExportFinished PlanExportStatus = "finished"
|
||||
PlanExportPending PlanExportStatus = "pending"
|
||||
PlanExportQueued PlanExportStatus = "queued"
|
||||
)
|
||||
|
||||
// PlanExportStatusTimestamps holds the timestamps for plan export statuses.
|
||||
type PlanExportStatusTimestamps struct {
|
||||
CanceledAt time.Time `json:"canceled-at"`
|
||||
ErroredAt time.Time `json:"errored-at"`
|
||||
ExpiredAt time.Time `json:"expired-at"`
|
||||
FinishedAt time.Time `json:"finished-at"`
|
||||
QueuedAt time.Time `json:"queued-at"`
|
||||
}
|
||||
|
||||
// PlanExport represents an export of Terraform Enterprise plan data.
|
||||
type PlanExport struct {
|
||||
ID string `jsonapi:"primary,plan-exports"`
|
||||
DataType PlanExportDataType `jsonapi:"attr,data-type"`
|
||||
Status PlanExportStatus `jsonapi:"attr,status"`
|
||||
StatusTimestamps *PlanExportStatusTimestamps `jsonapi:"attr,status-timestamps"`
|
||||
}
|
||||
|
||||
// PlanExportCreateOptions represents the options for exporting data from a plan.
|
||||
type PlanExportCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,plan-exports"`
|
||||
|
||||
// The plan to export.
|
||||
Plan *Plan `jsonapi:"relation,plan"`
|
||||
|
||||
// The name of the policy set.
|
||||
DataType *PlanExportDataType `jsonapi:"attr,data-type"`
|
||||
}
|
||||
|
||||
func (o PlanExportCreateOptions) valid() error {
|
||||
if o.Plan == nil {
|
||||
return errors.New("plan is required")
|
||||
}
|
||||
if o.DataType == nil {
|
||||
return errors.New("data type is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *planExports) Create(ctx context.Context, options PlanExportCreateOptions) (*PlanExport, error) {
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
req, err := s.client.newRequest("POST", "plan-exports", &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pe := &PlanExport{}
|
||||
err = s.client.do(ctx, req, pe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pe, err
|
||||
}
|
||||
|
||||
// Read a plan export by its ID.
|
||||
func (s *planExports) Read(ctx context.Context, planExportID string) (*PlanExport, error) {
|
||||
if !validStringID(&planExportID) {
|
||||
return nil, errors.New("invalid value for plan export ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("plan-exports/%s", url.QueryEscape(planExportID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pe := &PlanExport{}
|
||||
err = s.client.do(ctx, req, pe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pe, nil
|
||||
}
|
||||
|
||||
// Delete a plan export by ID.
|
||||
func (s *planExports) Delete(ctx context.Context, planExportID string) error {
|
||||
if !validStringID(&planExportID) {
|
||||
return errors.New("invalid value for plan export ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("plan-exports/%s", url.QueryEscape(planExportID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Download a plan export's data. Data is exported in a .tar.gz format.
|
||||
func (s *planExports) Download(ctx context.Context, planExportID string) ([]byte, error) {
|
||||
if !validStringID(&planExportID) {
|
||||
return nil, errors.New("invalid value for plan export ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("plan-exports/%s/download", url.QueryEscape(planExportID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = s.client.do(ctx, req, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
|
@ -0,0 +1,286 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ Policies = (*policies)(nil)
|
||||
|
||||
// Policies describes all the policy related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/policies.html
|
||||
type Policies interface {
|
||||
// List all the policies for a given organization
|
||||
List(ctx context.Context, organization string, options PolicyListOptions) (*PolicyList, error)
|
||||
|
||||
// Create a policy and associate it with an organization.
|
||||
Create(ctx context.Context, organization string, options PolicyCreateOptions) (*Policy, error)
|
||||
|
||||
// Read a policy by its ID.
|
||||
Read(ctx context.Context, policyID string) (*Policy, error)
|
||||
|
||||
// Update an existing policy.
|
||||
Update(ctx context.Context, policyID string, options PolicyUpdateOptions) (*Policy, error)
|
||||
|
||||
// Delete a policy by its ID.
|
||||
Delete(ctx context.Context, policyID string) error
|
||||
|
||||
// Upload the policy content of the policy.
|
||||
Upload(ctx context.Context, policyID string, content []byte) error
|
||||
|
||||
// Upload the policy content of the policy.
|
||||
Download(ctx context.Context, policyID string) ([]byte, error)
|
||||
}
|
||||
|
||||
// policies implements Policies.
|
||||
type policies struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// EnforcementLevel represents an enforcement level.
|
||||
type EnforcementLevel string
|
||||
|
||||
// List the available enforcement types.
|
||||
const (
|
||||
EnforcementAdvisory EnforcementLevel = "advisory"
|
||||
EnforcementHard EnforcementLevel = "hard-mandatory"
|
||||
EnforcementSoft EnforcementLevel = "soft-mandatory"
|
||||
)
|
||||
|
||||
// PolicyList represents a list of policies..
|
||||
type PolicyList struct {
|
||||
*Pagination
|
||||
Items []*Policy
|
||||
}
|
||||
|
||||
// Policy represents a Terraform Enterprise policy.
|
||||
type Policy struct {
|
||||
ID string `jsonapi:"primary,policies"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
Description string `jsonapi:"attr,description"`
|
||||
Enforce []*Enforcement `jsonapi:"attr,enforce"`
|
||||
PolicySetCount int `jsonapi:"attr,policy-set-count"`
|
||||
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
|
||||
|
||||
// Relations
|
||||
Organization *Organization `jsonapi:"relation,organization"`
|
||||
}
|
||||
|
||||
// Enforcement describes a enforcement.
|
||||
type Enforcement struct {
|
||||
Path string `json:"path"`
|
||||
Mode EnforcementLevel `json:"mode"`
|
||||
}
|
||||
|
||||
// PolicyListOptions represents the options for listing policies.
|
||||
type PolicyListOptions struct {
|
||||
ListOptions
|
||||
|
||||
// A search string (partial policy name) used to filter the results.
|
||||
Search *string `url:"search[name],omitempty"`
|
||||
}
|
||||
|
||||
// List all the policies for a given organization
|
||||
func (s *policies) List(ctx context.Context, organization string, options PolicyListOptions) (*PolicyList, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/policies", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pl := &PolicyList{}
|
||||
err = s.client.do(ctx, req, pl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pl, nil
|
||||
}
|
||||
|
||||
// PolicyCreateOptions represents the options for creating a new policy.
|
||||
type PolicyCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,policies"`
|
||||
|
||||
// The name of the policy.
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
|
||||
// A description of the policy's purpose.
|
||||
Description *string `jsonapi:"attr,description,omitempty"`
|
||||
|
||||
// The enforcements of the policy.
|
||||
Enforce []*EnforcementOptions `jsonapi:"attr,enforce"`
|
||||
}
|
||||
|
||||
// EnforcementOptions represents the enforcement options of a policy.
|
||||
type EnforcementOptions struct {
|
||||
Path *string `json:"path,omitempty"`
|
||||
Mode *EnforcementLevel `json:"mode"`
|
||||
}
|
||||
|
||||
func (o PolicyCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(o.Name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
if o.Enforce == nil {
|
||||
return errors.New("enforce is required")
|
||||
}
|
||||
for _, e := range o.Enforce {
|
||||
if !validString(e.Path) {
|
||||
return errors.New("enforcement path is required")
|
||||
}
|
||||
if e.Mode == nil {
|
||||
return errors.New("enforcement mode is required")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a policy and associate it with an organization.
|
||||
func (s *policies) Create(ctx context.Context, organization string, options PolicyCreateOptions) (*Policy, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/policies", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &Policy{}
|
||||
err = s.client.do(ctx, req, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, err
|
||||
}
|
||||
|
||||
// Read a policy by its ID.
|
||||
func (s *policies) Read(ctx context.Context, policyID string) (*Policy, error) {
|
||||
if !validStringID(&policyID) {
|
||||
return nil, errors.New("invalid value for policy ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &Policy{}
|
||||
err = s.client.do(ctx, req, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, err
|
||||
}
|
||||
|
||||
// PolicyUpdateOptions represents the options for updating a policy.
|
||||
type PolicyUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,policies"`
|
||||
|
||||
// A description of the policy's purpose.
|
||||
Description *string `jsonapi:"attr,description,omitempty"`
|
||||
|
||||
// The enforcements of the policy.
|
||||
Enforce []*EnforcementOptions `jsonapi:"attr,enforce,omitempty"`
|
||||
}
|
||||
|
||||
// Update an existing policy.
|
||||
func (s *policies) Update(ctx context.Context, policyID string, options PolicyUpdateOptions) (*Policy, error) {
|
||||
if !validStringID(&policyID) {
|
||||
return nil, errors.New("invalid value for policy ID")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &Policy{}
|
||||
err = s.client.do(ctx, req, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, err
|
||||
}
|
||||
|
||||
// Delete a policy by its ID.
|
||||
func (s *policies) Delete(ctx context.Context, policyID string) error {
|
||||
if !validStringID(&policyID) {
|
||||
return errors.New("invalid value for policy ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Upload the policy content of the policy.
|
||||
func (s *policies) Upload(ctx context.Context, policyID string, content []byte) error {
|
||||
if !validStringID(&policyID) {
|
||||
return errors.New("invalid value for policy ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policies/%s/upload", url.QueryEscape(policyID))
|
||||
req, err := s.client.newRequest("PUT", u, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Download the policy content of the policy.
|
||||
func (s *policies) Download(ctx context.Context, policyID string) ([]byte, error) {
|
||||
if !validStringID(&policyID) {
|
||||
return nil, errors.New("invalid value for policy ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policies/%s/download", url.QueryEscape(policyID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = s.client.do(ctx, req, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ PolicyChecks = (*policyChecks)(nil)
|
||||
|
||||
// PolicyChecks describes all the policy check related methods that the
|
||||
// Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/policy-checks.html
|
||||
type PolicyChecks interface {
|
||||
// List all policy checks of the given run.
|
||||
List(ctx context.Context, runID string, options PolicyCheckListOptions) (*PolicyCheckList, error)
|
||||
|
||||
// Read a policy check by its ID.
|
||||
Read(ctx context.Context, policyCheckID string) (*PolicyCheck, error)
|
||||
|
||||
// Override a soft-mandatory or warning policy.
|
||||
Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error)
|
||||
|
||||
// Logs retrieves the logs of a policy check.
|
||||
Logs(ctx context.Context, policyCheckID string) (io.Reader, error)
|
||||
}
|
||||
|
||||
// policyChecks implements PolicyChecks.
|
||||
type policyChecks struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// PolicyScope represents a policy scope.
|
||||
type PolicyScope string
|
||||
|
||||
// List all available policy scopes.
|
||||
const (
|
||||
PolicyScopeOrganization PolicyScope = "organization"
|
||||
PolicyScopeWorkspace PolicyScope = "workspace"
|
||||
)
|
||||
|
||||
// PolicyStatus represents a policy check state.
|
||||
type PolicyStatus string
|
||||
|
||||
//List all available policy check statuses.
|
||||
const (
|
||||
PolicyCanceled PolicyStatus = "canceled"
|
||||
PolicyErrored PolicyStatus = "errored"
|
||||
PolicyHardFailed PolicyStatus = "hard_failed"
|
||||
PolicyOverridden PolicyStatus = "overridden"
|
||||
PolicyPasses PolicyStatus = "passed"
|
||||
PolicyPending PolicyStatus = "pending"
|
||||
PolicyQueued PolicyStatus = "queued"
|
||||
PolicySoftFailed PolicyStatus = "soft_failed"
|
||||
PolicyUnreachable PolicyStatus = "unreachable"
|
||||
)
|
||||
|
||||
// PolicyCheckList represents a list of policy checks.
|
||||
type PolicyCheckList struct {
|
||||
*Pagination
|
||||
Items []*PolicyCheck
|
||||
}
|
||||
|
||||
// PolicyCheck represents a Terraform Enterprise policy check..
|
||||
type PolicyCheck struct {
|
||||
ID string `jsonapi:"primary,policy-checks"`
|
||||
Actions *PolicyActions `jsonapi:"attr,actions"`
|
||||
Permissions *PolicyPermissions `jsonapi:"attr,permissions"`
|
||||
Result *PolicyResult `jsonapi:"attr,result"`
|
||||
Scope PolicyScope `jsonapi:"attr,scope"`
|
||||
Status PolicyStatus `jsonapi:"attr,status"`
|
||||
StatusTimestamps *PolicyStatusTimestamps `jsonapi:"attr,status-timestamps"`
|
||||
Run *Run `jsonapi:"relation,run"`
|
||||
}
|
||||
|
||||
// PolicyActions represents the policy check actions.
|
||||
type PolicyActions struct {
|
||||
IsOverridable bool `json:"is-overridable"`
|
||||
}
|
||||
|
||||
// PolicyPermissions represents the policy check permissions.
|
||||
type PolicyPermissions struct {
|
||||
CanOverride bool `json:"can-override"`
|
||||
}
|
||||
|
||||
// PolicyResult represents the complete policy check result,
|
||||
type PolicyResult struct {
|
||||
AdvisoryFailed int `json:"advisory-failed"`
|
||||
Duration int `json:"duration"`
|
||||
HardFailed int `json:"hard-failed"`
|
||||
Passed int `json:"passed"`
|
||||
Result bool `json:"result"`
|
||||
// Sentinel *sentinel.EvalResult `json:"sentinel"`
|
||||
SoftFailed int `json:"soft-failed"`
|
||||
TotalFailed int `json:"total-failed"`
|
||||
}
|
||||
|
||||
// PolicyStatusTimestamps holds the timestamps for individual policy check
|
||||
// statuses.
|
||||
type PolicyStatusTimestamps struct {
|
||||
ErroredAt time.Time `json:"errored-at"`
|
||||
HardFailedAt time.Time `json:"hard-failed-at"`
|
||||
PassedAt time.Time `json:"passed-at"`
|
||||
QueuedAt time.Time `json:"queued-at"`
|
||||
SoftFailedAt time.Time `json:"soft-failed-at"`
|
||||
}
|
||||
|
||||
// PolicyCheckListOptions represents the options for listing policy checks.
|
||||
type PolicyCheckListOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// List all policy checks of the given run.
|
||||
func (s *policyChecks) List(ctx context.Context, runID string, options PolicyCheckListOptions) (*PolicyCheckList, error) {
|
||||
if !validStringID(&runID) {
|
||||
return nil, errors.New("invalid value for run ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("runs/%s/policy-checks", url.QueryEscape(runID))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pcl := &PolicyCheckList{}
|
||||
err = s.client.do(ctx, req, pcl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pcl, nil
|
||||
}
|
||||
|
||||
// Read a policy check by its ID.
|
||||
func (s *policyChecks) Read(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
|
||||
if !validStringID(&policyCheckID) {
|
||||
return nil, errors.New("invalid value for policy check ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-checks/%s", url.QueryEscape(policyCheckID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc := &PolicyCheck{}
|
||||
err = s.client.do(ctx, req, pc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pc, nil
|
||||
}
|
||||
|
||||
// Override a soft-mandatory or warning policy.
|
||||
func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
|
||||
if !validStringID(&policyCheckID) {
|
||||
return nil, errors.New("invalid value for policy check ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-checks/%s/actions/override", url.QueryEscape(policyCheckID))
|
||||
req, err := s.client.newRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc := &PolicyCheck{}
|
||||
err = s.client.do(ctx, req, pc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pc, nil
|
||||
}
|
||||
|
||||
// Logs retrieves the logs of a policy check.
|
||||
func (s *policyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) {
|
||||
if !validStringID(&policyCheckID) {
|
||||
return nil, errors.New("invalid value for policy check ID")
|
||||
}
|
||||
|
||||
// Loop until the context is canceled or the policy check is finished
|
||||
// running. The policy check logs are not streamed and so only available
|
||||
// once the check is finished.
|
||||
for {
|
||||
pc, err := s.Read(ctx, policyCheckID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch pc.Status {
|
||||
case PolicyPending, PolicyQueued:
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-checks/%s/output", url.QueryEscape(policyCheckID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logs := bytes.NewBuffer(nil)
|
||||
err = s.client.do(ctx, req, logs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,414 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ PolicySets = (*policySets)(nil)
|
||||
|
||||
// PolicySets describes all the policy set related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/policies.html
|
||||
type PolicySets interface {
|
||||
// List all the policy sets for a given organization.
|
||||
List(ctx context.Context, organization string, options PolicySetListOptions) (*PolicySetList, error)
|
||||
|
||||
// Create a policy set and associate it with an organization.
|
||||
Create(ctx context.Context, organization string, options PolicySetCreateOptions) (*PolicySet, error)
|
||||
|
||||
// Read a policy set by its ID.
|
||||
Read(ctx context.Context, policySetID string) (*PolicySet, error)
|
||||
|
||||
// Update an existing policy set.
|
||||
Update(ctx context.Context, policySetID string, options PolicySetUpdateOptions) (*PolicySet, error)
|
||||
|
||||
// Add policies to a policy set. This function can only be used when
|
||||
// there is no VCS repository associated with the policy set.
|
||||
AddPolicies(ctx context.Context, policySetID string, options PolicySetAddPoliciesOptions) error
|
||||
|
||||
// Remove policies from a policy set. This function can only be used
|
||||
// when there is no VCS repository associated with the policy set.
|
||||
RemovePolicies(ctx context.Context, policySetID string, options PolicySetRemovePoliciesOptions) error
|
||||
|
||||
// Add workspaces to a policy set.
|
||||
AddWorkspaces(ctx context.Context, policySetID string, options PolicySetAddWorkspacesOptions) error
|
||||
|
||||
// Remove workspaces from a policy set.
|
||||
RemoveWorkspaces(ctx context.Context, policySetID string, options PolicySetRemoveWorkspacesOptions) error
|
||||
|
||||
// Delete a policy set by its ID.
|
||||
Delete(ctx context.Context, policyID string) error
|
||||
}
|
||||
|
||||
// policySets implements PolicySets.
|
||||
type policySets struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// PolicySetList represents a list of policy sets.
|
||||
type PolicySetList struct {
|
||||
*Pagination
|
||||
Items []*PolicySet
|
||||
}
|
||||
|
||||
// PolicySet represents a Terraform Enterprise policy set.
|
||||
type PolicySet struct {
|
||||
ID string `jsonapi:"primary,policy-sets"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
Description string `jsonapi:"attr,description"`
|
||||
Global bool `jsonapi:"attr,global"`
|
||||
PoliciesPath string `jsonapi:"attr,policies-path"`
|
||||
PolicyCount int `jsonapi:"attr,policy-count"`
|
||||
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
|
||||
WorkspaceCount int `jsonapi:"attr,workspace-count"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
|
||||
|
||||
// Relations
|
||||
Organization *Organization `jsonapi:"relation,organization"`
|
||||
Policies []*Policy `jsonapi:"relation,policies"`
|
||||
Workspaces []*Workspace `jsonapi:"relation,workspaces"`
|
||||
}
|
||||
|
||||
// PolicySetListOptions represents the options for listing policy sets.
|
||||
type PolicySetListOptions struct {
|
||||
ListOptions
|
||||
|
||||
// A search string (partial policy set name) used to filter the results.
|
||||
Search *string `url:"search[name],omitempty"`
|
||||
}
|
||||
|
||||
// List all the policies for a given organization.
|
||||
func (s *policySets) List(ctx context.Context, organization string, options PolicySetListOptions) (*PolicySetList, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/policy-sets", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
psl := &PolicySetList{}
|
||||
err = s.client.do(ctx, req, psl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return psl, nil
|
||||
}
|
||||
|
||||
// PolicySetCreateOptions represents the options for creating a new policy set.
|
||||
type PolicySetCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,policy-sets"`
|
||||
|
||||
// The name of the policy set.
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
|
||||
// The description of the policy set.
|
||||
Description *string `jsonapi:"attr,description,omitempty"`
|
||||
|
||||
// Whether or not the policy set is global.
|
||||
Global *bool `jsonapi:"attr,global,omitempty"`
|
||||
|
||||
// The sub-path within the attached VCS repository to ingress. All
|
||||
// files and directories outside of this sub-path will be ignored.
|
||||
// This option may only be specified when a VCS repo is present.
|
||||
PoliciesPath *string `jsonapi:"attr,policies-path,omitempty"`
|
||||
|
||||
// The initial members of the policy set.
|
||||
Policies []*Policy `jsonapi:"relation,policies,omitempty"`
|
||||
|
||||
// VCS repository information. When present, the policies and
|
||||
// configuration will be sourced from the specified VCS repository
|
||||
// instead of being defined within the policy set itself. Note that
|
||||
// this option is mutually exclusive with the Policies option and
|
||||
// both cannot be used at the same time.
|
||||
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
|
||||
|
||||
// The initial list of workspaces for which the policy set should be enforced.
|
||||
Workspaces []*Workspace `jsonapi:"relation,workspaces,omitempty"`
|
||||
}
|
||||
|
||||
func (o PolicySetCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(o.Name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a policy set and associate it with an organization.
|
||||
func (s *policySets) Create(ctx context.Context, organization string, options PolicySetCreateOptions) (*PolicySet, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/policy-sets", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ps := &PolicySet{}
|
||||
err = s.client.do(ctx, req, ps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ps, err
|
||||
}
|
||||
|
||||
// Read a policy set by its ID.
|
||||
func (s *policySets) Read(ctx context.Context, policySetID string) (*PolicySet, error) {
|
||||
if !validStringID(&policySetID) {
|
||||
return nil, errors.New("invalid value for policy set ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s", url.QueryEscape(policySetID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ps := &PolicySet{}
|
||||
err = s.client.do(ctx, req, ps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ps, err
|
||||
}
|
||||
|
||||
// PolicySetUpdateOptions represents the options for updating a policy set.
|
||||
type PolicySetUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,policy-sets"`
|
||||
|
||||
/// The name of the policy set.
|
||||
Name *string `jsonapi:"attr,name,omitempty"`
|
||||
|
||||
// The description of the policy set.
|
||||
Description *string `jsonapi:"attr,description,omitempty"`
|
||||
|
||||
// Whether or not the policy set is global.
|
||||
Global *bool `jsonapi:"attr,global,omitempty"`
|
||||
|
||||
// The sub-path within the attached VCS repository to ingress. All
|
||||
// files and directories outside of this sub-path will be ignored.
|
||||
// This option may only be specified when a VCS repo is present.
|
||||
PoliciesPath *string `jsonapi:"attr,policies-path,omitempty"`
|
||||
|
||||
// VCS repository information. When present, the policies and
|
||||
// configuration will be sourced from the specified VCS repository
|
||||
// instead of being defined within the policy set itself. Note that
|
||||
// specifying this option may only be used on policy sets with no
|
||||
// directly-attached policies (*PolicySet.Policies). Specifying this
|
||||
// option when policies are already present will result in an error.
|
||||
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
|
||||
}
|
||||
|
||||
func (o PolicySetUpdateOptions) valid() error {
|
||||
if o.Name != nil && !validStringID(o.Name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update an existing policy set.
|
||||
func (s *policySets) Update(ctx context.Context, policySetID string, options PolicySetUpdateOptions) (*PolicySet, error) {
|
||||
if !validStringID(&policySetID) {
|
||||
return nil, errors.New("invalid value for policy set ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s", url.QueryEscape(policySetID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ps := &PolicySet{}
|
||||
err = s.client.do(ctx, req, ps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ps, err
|
||||
}
|
||||
|
||||
// PolicySetAddPoliciesOptions represents the options for adding policies
|
||||
// to a policy set.
|
||||
type PolicySetAddPoliciesOptions struct {
|
||||
/// The policies to add to the policy set.
|
||||
Policies []*Policy
|
||||
}
|
||||
|
||||
func (o PolicySetAddPoliciesOptions) valid() error {
|
||||
if o.Policies == nil {
|
||||
return errors.New("policies is required")
|
||||
}
|
||||
if len(o.Policies) == 0 {
|
||||
return errors.New("must provide at least one policy")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add policies to a policy set
|
||||
func (s *policySets) AddPolicies(ctx context.Context, policySetID string, options PolicySetAddPoliciesOptions) error {
|
||||
if !validStringID(&policySetID) {
|
||||
return errors.New("invalid value for policy set ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s/relationships/policies", url.QueryEscape(policySetID))
|
||||
req, err := s.client.newRequest("POST", u, options.Policies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// PolicySetRemovePoliciesOptions represents the options for removing
|
||||
// policies from a policy set.
|
||||
type PolicySetRemovePoliciesOptions struct {
|
||||
/// The policies to remove from the policy set.
|
||||
Policies []*Policy
|
||||
}
|
||||
|
||||
func (o PolicySetRemovePoliciesOptions) valid() error {
|
||||
if o.Policies == nil {
|
||||
return errors.New("policies is required")
|
||||
}
|
||||
if len(o.Policies) == 0 {
|
||||
return errors.New("must provide at least one policy")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove policies from a policy set
|
||||
func (s *policySets) RemovePolicies(ctx context.Context, policySetID string, options PolicySetRemovePoliciesOptions) error {
|
||||
if !validStringID(&policySetID) {
|
||||
return errors.New("invalid value for policy set ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s/relationships/policies", url.QueryEscape(policySetID))
|
||||
req, err := s.client.newRequest("DELETE", u, options.Policies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// PolicySetAddWorkspacesOptions represents the options for adding workspaces
|
||||
// to a policy set.
|
||||
type PolicySetAddWorkspacesOptions struct {
|
||||
/// The workspaces to add to the policy set.
|
||||
Workspaces []*Workspace
|
||||
}
|
||||
|
||||
func (o PolicySetAddWorkspacesOptions) valid() error {
|
||||
if o.Workspaces == nil {
|
||||
return errors.New("workspaces is required")
|
||||
}
|
||||
if len(o.Workspaces) == 0 {
|
||||
return errors.New("must provide at least one workspace")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add workspaces to a policy set.
|
||||
func (s *policySets) AddWorkspaces(ctx context.Context, policySetID string, options PolicySetAddWorkspacesOptions) error {
|
||||
if !validStringID(&policySetID) {
|
||||
return errors.New("invalid value for policy set ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s/relationships/workspaces", url.QueryEscape(policySetID))
|
||||
req, err := s.client.newRequest("POST", u, options.Workspaces)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// PolicySetRemoveWorkspacesOptions represents the options for removing
|
||||
// workspaces from a policy set.
|
||||
type PolicySetRemoveWorkspacesOptions struct {
|
||||
/// The workspaces to remove from the policy set.
|
||||
Workspaces []*Workspace
|
||||
}
|
||||
|
||||
func (o PolicySetRemoveWorkspacesOptions) valid() error {
|
||||
if o.Workspaces == nil {
|
||||
return errors.New("workspaces is required")
|
||||
}
|
||||
if len(o.Workspaces) == 0 {
|
||||
return errors.New("must provide at least one workspace")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove workspaces from a policy set.
|
||||
func (s *policySets) RemoveWorkspaces(ctx context.Context, policySetID string, options PolicySetRemoveWorkspacesOptions) error {
|
||||
if !validStringID(&policySetID) {
|
||||
return errors.New("invalid value for policy set ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s/relationships/workspaces", url.QueryEscape(policySetID))
|
||||
req, err := s.client.newRequest("DELETE", u, options.Workspaces)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Delete a policy set by its ID.
|
||||
func (s *policySets) Delete(ctx context.Context, policySetID string) error {
|
||||
if !validStringID(&policySetID) {
|
||||
return errors.New("invalid value for policy set ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s", url.QueryEscape(policySetID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ PolicySetParameters = (*policySetParameters)(nil)
|
||||
|
||||
// PolicySetParameters describes all the parameter related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/policy-set-params.html
|
||||
type PolicySetParameters interface {
|
||||
// List all the parameters associated with the given policy-set.
|
||||
List(ctx context.Context, policySetID string, options PolicySetParameterListOptions) (*PolicySetParameterList, error)
|
||||
|
||||
// Create is used to create a new parameter.
|
||||
Create(ctx context.Context, policySetID string, options PolicySetParameterCreateOptions) (*PolicySetParameter, error)
|
||||
|
||||
// Read a parameter by its ID.
|
||||
Read(ctx context.Context, policySetID string, parameterID string) (*PolicySetParameter, error)
|
||||
|
||||
// Update values of an existing parameter.
|
||||
Update(ctx context.Context, policySetID string, parameterID string, options PolicySetParameterUpdateOptions) (*PolicySetParameter, error)
|
||||
|
||||
// Delete a parameter by its ID.
|
||||
Delete(ctx context.Context, policySetID string, parameterID string) error
|
||||
}
|
||||
|
||||
// policySetParameters implements Parameters.
|
||||
type policySetParameters struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// PolicySetParameterList represents a list of parameters.
|
||||
type PolicySetParameterList struct {
|
||||
*Pagination
|
||||
Items []*PolicySetParameter
|
||||
}
|
||||
|
||||
// PolicySetParameter represents a Policy Set parameter
|
||||
type PolicySetParameter struct {
|
||||
ID string `jsonapi:"primary,vars"`
|
||||
Key string `jsonapi:"attr,key"`
|
||||
Value string `jsonapi:"attr,value"`
|
||||
Category CategoryType `jsonapi:"attr,category"`
|
||||
Sensitive bool `jsonapi:"attr,sensitive"`
|
||||
|
||||
// Relations
|
||||
PolicySet *PolicySet `jsonapi:"relation,configurable"`
|
||||
}
|
||||
|
||||
// PolicySetParameterListOptions represents the options for listing parameters.
|
||||
type PolicySetParameterListOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
func (o PolicySetParameterListOptions) valid() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// List all the parameters associated with the given policy-set.
|
||||
func (s *policySetParameters) List(ctx context.Context, policySetID string, options PolicySetParameterListOptions) (*PolicySetParameterList, error) {
|
||||
if !validStringID(&policySetID) {
|
||||
return nil, errors.New("invalid value for policy set ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s/parameters", policySetID)
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vl := &PolicySetParameterList{}
|
||||
err = s.client.do(ctx, req, vl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vl, nil
|
||||
}
|
||||
|
||||
// PolicySetParameterCreateOptions represents the options for creating a new parameter.
|
||||
type PolicySetParameterCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,vars"`
|
||||
|
||||
// The name of the parameter.
|
||||
Key *string `jsonapi:"attr,key"`
|
||||
|
||||
// The value of the parameter.
|
||||
Value *string `jsonapi:"attr,value,omitempty"`
|
||||
|
||||
// The Category of the parameter, should always be "policy-set"
|
||||
Category *CategoryType `jsonapi:"attr,category"`
|
||||
|
||||
// Whether the value is sensitive.
|
||||
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
|
||||
}
|
||||
|
||||
func (o PolicySetParameterCreateOptions) valid() error {
|
||||
if !validString(o.Key) {
|
||||
return errors.New("key is required")
|
||||
}
|
||||
if o.Category == nil {
|
||||
return errors.New("category is required")
|
||||
}
|
||||
if *o.Category != CategoryPolicySet {
|
||||
return errors.New("category must be policy-set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create is used to create a new parameter.
|
||||
func (s *policySetParameters) Create(ctx context.Context, policySetID string, options PolicySetParameterCreateOptions) (*PolicySetParameter, error) {
|
||||
if !validStringID(&policySetID) {
|
||||
return nil, errors.New("invalid value for policy set ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s/parameters", url.QueryEscape(policySetID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &PolicySetParameter{}
|
||||
err = s.client.do(ctx, req, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Read a parameter by its ID.
|
||||
func (s *policySetParameters) Read(ctx context.Context, policySetID string, parameterID string) (*PolicySetParameter, error) {
|
||||
if !validStringID(&policySetID) {
|
||||
return nil, errors.New("invalid value for policy set ID")
|
||||
}
|
||||
if !validStringID(¶meterID) {
|
||||
return nil, errors.New("invalid value for parameter ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.QueryEscape(policySetID), url.QueryEscape(parameterID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &PolicySetParameter{}
|
||||
err = s.client.do(ctx, req, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, err
|
||||
}
|
||||
|
||||
// PolicySetParameterUpdateOptions represents the options for updating a parameter.
|
||||
type PolicySetParameterUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,vars"`
|
||||
|
||||
// The name of the parameter.
|
||||
Key *string `jsonapi:"attr,key,omitempty"`
|
||||
|
||||
// The value of the parameter.
|
||||
Value *string `jsonapi:"attr,value,omitempty"`
|
||||
|
||||
// Whether the value is sensitive.
|
||||
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
|
||||
}
|
||||
|
||||
// Update values of an existing parameter.
|
||||
func (s *policySetParameters) Update(ctx context.Context, policySetID string, parameterID string, options PolicySetParameterUpdateOptions) (*PolicySetParameter, error) {
|
||||
if !validStringID(&policySetID) {
|
||||
return nil, errors.New("invalid value for policy set ID")
|
||||
}
|
||||
if !validStringID(¶meterID) {
|
||||
return nil, errors.New("invalid value for parameter ID")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = parameterID
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.QueryEscape(policySetID), url.QueryEscape(parameterID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &PolicySetParameter{}
|
||||
err = s.client.do(ctx, req, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Delete a parameter by its ID.
|
||||
func (s *policySetParameters) Delete(ctx context.Context, policySetID string, parameterID string) error {
|
||||
if !validStringID(&policySetID) {
|
||||
return errors.New("invalid value for policy set ID")
|
||||
}
|
||||
if !validStringID(¶meterID) {
|
||||
return errors.New("invalid value for parameter ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.QueryEscape(policySetID), url.QueryEscape(parameterID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,417 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ RegistryModules = (*registryModules)(nil)
|
||||
|
||||
// RegistryModules describes all the registry module related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/cloud/api/modules.html
|
||||
type RegistryModules interface {
|
||||
// Create a registry module without a VCS repo
|
||||
Create(ctx context.Context, organization string, options RegistryModuleCreateOptions) (*RegistryModule, error)
|
||||
|
||||
// Create a registry module version
|
||||
CreateVersion(ctx context.Context, organization string, name string, provider string, options RegistryModuleCreateVersionOptions) (*RegistryModuleVersion, error)
|
||||
|
||||
// Create and publish a registry module with a VCS repo
|
||||
CreateWithVCSConnection(ctx context.Context, options RegistryModuleCreateWithVCSConnectionOptions) (*RegistryModule, error)
|
||||
|
||||
// Read a registry module
|
||||
Read(ctx context.Context, organization string, name string, provider string) (*RegistryModule, error)
|
||||
|
||||
// Delete a registry module
|
||||
Delete(ctx context.Context, organization string, name string) error
|
||||
|
||||
// Delete a specific registry module provider
|
||||
DeleteProvider(ctx context.Context, organization string, name string, provider string) error
|
||||
|
||||
// Delete a specific registry module version
|
||||
DeleteVersion(ctx context.Context, organization string, name string, provider string, version string) error
|
||||
}
|
||||
|
||||
// registryModules implements RegistryModules.
|
||||
type registryModules struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// RegistryModuleStatus represents the status of the registry module
|
||||
type RegistryModuleStatus string
|
||||
|
||||
// List of available registry module statuses
|
||||
const (
|
||||
RegistryModuleStatusPending RegistryModuleStatus = "pending"
|
||||
RegistryModuleStatusNoVersionTags RegistryModuleStatus = "no_version_tags"
|
||||
RegistryModuleStatusSetupFailed RegistryModuleStatus = "setup_failed"
|
||||
RegistryModuleStatusSetupComplete RegistryModuleStatus = "setup_complete"
|
||||
)
|
||||
|
||||
// RegistryModuleVersionStatus represents the status of a specific version of a registry module
|
||||
type RegistryModuleVersionStatus string
|
||||
|
||||
// List of available registry module version statuses
|
||||
const (
|
||||
RegistryModuleVersionStatusPending RegistryModuleVersionStatus = "pending"
|
||||
RegistryModuleVersionStatusCloning RegistryModuleVersionStatus = "cloning"
|
||||
RegistryModuleVersionStatusCloneFailed RegistryModuleVersionStatus = "clone_failed"
|
||||
RegistryModuleVersionStatusRegIngressReqFailed RegistryModuleVersionStatus = "reg_ingress_req_failed"
|
||||
RegistryModuleVersionStatusRegIngressing RegistryModuleVersionStatus = "reg_ingressing"
|
||||
RegistryModuleVersionStatusRegIngressFailed RegistryModuleVersionStatus = "reg_ingress_failed"
|
||||
RegistryModuleVersionStatusOk RegistryModuleVersionStatus = "ok"
|
||||
)
|
||||
|
||||
// RegistryModule represents a registry module
|
||||
type RegistryModule struct {
|
||||
ID string `jsonapi:"primary,registry-modules"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
Provider string `jsonapi:"attr,provider"`
|
||||
Permissions *RegistryModulePermissions `jsonapi:"attr,permissions"`
|
||||
Status RegistryModuleStatus `jsonapi:"attr,status"`
|
||||
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
|
||||
VersionStatuses []RegistryModuleVersionStatuses `jsonapi:"attr,version-statuses"`
|
||||
CreatedAt string `jsonapi:"attr,created-at"`
|
||||
UpdatedAt string `jsonapi:"attr,updated-at"`
|
||||
|
||||
// Relations
|
||||
Organization *Organization `jsonapi:"relation,organization"`
|
||||
}
|
||||
|
||||
// RegistryModuleVersion represents a registry module version
|
||||
type RegistryModuleVersion struct {
|
||||
ID string `jsonapi:"primary,registry-module-versions"`
|
||||
Source string `jsonapi:"attr,source"`
|
||||
Status RegistryModuleVersionStatus `jsonapi:"attr,status"`
|
||||
Version string `jsonapi:"attr,version"`
|
||||
CreatedAt string `jsonapi:"attr,created-at"`
|
||||
UpdatedAt string `jsonapi:"attr,updated-at"`
|
||||
|
||||
// Relations
|
||||
RegistryModule *RegistryModule `jsonapi:"relation,registry-module"`
|
||||
}
|
||||
|
||||
type RegistryModulePermissions struct {
|
||||
CanDelete bool `json:"can-delete"`
|
||||
CanResync bool `json:"can-resync"`
|
||||
CanRetry bool `json:"can-retry"`
|
||||
}
|
||||
|
||||
type RegistryModuleVersionStatuses struct {
|
||||
Version string `json:"version"`
|
||||
Status RegistryModuleVersionStatus `json:"status"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// RegistryModuleCreateOptions is used when creating a registry module without a VCS repo
|
||||
type RegistryModuleCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,registry-modules"`
|
||||
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
Provider *string `jsonapi:"attr,provider"`
|
||||
}
|
||||
|
||||
func (o RegistryModuleCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(o.Name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
if !validString(o.Provider) {
|
||||
return errors.New("provider is required")
|
||||
}
|
||||
if !validStringID(o.Provider) {
|
||||
return errors.New("invalid value for provider")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new registry module without a VCS repo
|
||||
func (r *registryModules) Create(ctx context.Context, organization string, options RegistryModuleCreateOptions) (*RegistryModule, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"organizations/%s/registry-modules",
|
||||
url.QueryEscape(organization),
|
||||
)
|
||||
req, err := r.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rm := &RegistryModule{}
|
||||
err = r.client.do(ctx, req, rm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rm, nil
|
||||
}
|
||||
|
||||
// RegistryModuleCreateVersionOptions is used when creating a registry module version
|
||||
type RegistryModuleCreateVersionOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,registry-module-versions"`
|
||||
|
||||
Version *string `jsonapi:"attr,version"`
|
||||
}
|
||||
|
||||
func (o RegistryModuleCreateVersionOptions) valid() error {
|
||||
if !validString(o.Version) {
|
||||
return errors.New("version is required")
|
||||
}
|
||||
if !validStringID(o.Version) {
|
||||
return errors.New("invalid value for version")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new registry module version
|
||||
func (r *registryModules) CreateVersion(ctx context.Context, organization string, name string, provider string, options RegistryModuleCreateVersionOptions) (*RegistryModuleVersion, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if !validString(&name) {
|
||||
return nil, errors.New("name is required")
|
||||
}
|
||||
if !validStringID(&name) {
|
||||
return nil, errors.New("invalid value for name")
|
||||
}
|
||||
if !validString(&provider) {
|
||||
return nil, errors.New("provider is required")
|
||||
}
|
||||
if !validStringID(&provider) {
|
||||
return nil, errors.New("invalid value for provider")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"registry-modules/%s/%s/%s/versions",
|
||||
url.QueryEscape(organization),
|
||||
url.QueryEscape(name),
|
||||
url.QueryEscape(provider),
|
||||
)
|
||||
req, err := r.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rmv := &RegistryModuleVersion{}
|
||||
err = r.client.do(ctx, req, rmv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rmv, nil
|
||||
}
|
||||
|
||||
// RegistryModuleCreateWithVCSConnectionOptions is used when creating a registry module with a VCS repo
|
||||
type RegistryModuleCreateWithVCSConnectionOptions struct {
|
||||
ID string `jsonapi:"primary,registry-modules"`
|
||||
|
||||
// VCS repository information
|
||||
VCSRepo *RegistryModuleVCSRepoOptions `jsonapi:"attr,vcs-repo"`
|
||||
}
|
||||
|
||||
func (o RegistryModuleCreateWithVCSConnectionOptions) valid() error {
|
||||
if o.VCSRepo == nil {
|
||||
return errors.New("vcs repo is required")
|
||||
}
|
||||
return o.VCSRepo.valid()
|
||||
}
|
||||
|
||||
type RegistryModuleVCSRepoOptions struct {
|
||||
Identifier *string `json:"identifier"`
|
||||
OAuthTokenID *string `json:"oauth-token-id"`
|
||||
DisplayIdentifier *string `json:"display-identifier"`
|
||||
}
|
||||
|
||||
func (o RegistryModuleVCSRepoOptions) valid() error {
|
||||
if !validString(o.Identifier) {
|
||||
return errors.New("identifier is required")
|
||||
}
|
||||
if !validString(o.OAuthTokenID) {
|
||||
return errors.New("oauth token ID is required")
|
||||
}
|
||||
if !validString(o.DisplayIdentifier) {
|
||||
return errors.New("display identifier is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateWithVCSConnection is used to create and publish a new registry module with a VCS repo
|
||||
func (r *registryModules) CreateWithVCSConnection(ctx context.Context, options RegistryModuleCreateWithVCSConnectionOptions) (*RegistryModule, error) {
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
req, err := r.client.newRequest("POST", "registry-modules", &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rm := &RegistryModule{}
|
||||
err = r.client.do(ctx, req, rm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rm, nil
|
||||
}
|
||||
|
||||
// Read a specific registry module
|
||||
func (r *registryModules) Read(ctx context.Context, organization string, name string, provider string) (*RegistryModule, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if !validString(&name) {
|
||||
return nil, errors.New("name is required")
|
||||
}
|
||||
if !validStringID(&name) {
|
||||
return nil, errors.New("invalid value for name")
|
||||
}
|
||||
if !validString(&provider) {
|
||||
return nil, errors.New("provider is required")
|
||||
}
|
||||
if !validStringID(&provider) {
|
||||
return nil, errors.New("invalid value for provider")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"registry-modules/show/%s/%s/%s",
|
||||
url.QueryEscape(organization),
|
||||
url.QueryEscape(name),
|
||||
url.QueryEscape(provider),
|
||||
)
|
||||
req, err := r.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rm := &RegistryModule{}
|
||||
err = r.client.do(ctx, req, rm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rm, nil
|
||||
}
|
||||
|
||||
// Delete is used to delete the entire registry module
|
||||
func (r *registryModules) Delete(ctx context.Context, organization string, name string) error {
|
||||
if !validStringID(&organization) {
|
||||
return errors.New("invalid value for organization")
|
||||
}
|
||||
if !validString(&name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(&name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"registry-modules/actions/delete/%s/%s",
|
||||
url.QueryEscape(organization),
|
||||
url.QueryEscape(name),
|
||||
)
|
||||
req, err := r.client.newRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DeleteProvider is used to delete the specific registry module provider
|
||||
func (r *registryModules) DeleteProvider(ctx context.Context, organization string, name string, provider string) error {
|
||||
if !validStringID(&organization) {
|
||||
return errors.New("invalid value for organization")
|
||||
}
|
||||
if !validString(&name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(&name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
if !validString(&provider) {
|
||||
return errors.New("provider is required")
|
||||
}
|
||||
if !validStringID(&provider) {
|
||||
return errors.New("invalid value for provider")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"registry-modules/actions/delete/%s/%s/%s",
|
||||
url.QueryEscape(organization),
|
||||
url.QueryEscape(name),
|
||||
url.QueryEscape(provider),
|
||||
)
|
||||
req, err := r.client.newRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DeleteVersion is used to delete the specific registry module version
|
||||
func (r *registryModules) DeleteVersion(ctx context.Context, organization string, name string, provider string, version string) error {
|
||||
if !validStringID(&organization) {
|
||||
return errors.New("invalid value for organization")
|
||||
}
|
||||
if !validString(&name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(&name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
if !validString(&provider) {
|
||||
return errors.New("provider is required")
|
||||
}
|
||||
if !validStringID(&provider) {
|
||||
return errors.New("invalid value for provider")
|
||||
}
|
||||
if !validString(&version) {
|
||||
return errors.New("version is required")
|
||||
}
|
||||
if !validStringID(&version) {
|
||||
return errors.New("invalid value for version")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"registry-modules/actions/delete/%s/%s/%s/%s",
|
||||
url.QueryEscape(organization),
|
||||
url.QueryEscape(name),
|
||||
url.QueryEscape(provider),
|
||||
url.QueryEscape(version),
|
||||
)
|
||||
req, err := r.client.newRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ Runs = (*runs)(nil)
|
||||
|
||||
// Runs describes all the run related methods that the Terraform Enterprise
|
||||
// API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/run.html
|
||||
type Runs interface {
|
||||
// List all the runs of the given workspace.
|
||||
List(ctx context.Context, workspaceID string, options RunListOptions) (*RunList, error)
|
||||
|
||||
// Create a new run with the given options.
|
||||
Create(ctx context.Context, options RunCreateOptions) (*Run, error)
|
||||
|
||||
// Read a run by its ID.
|
||||
Read(ctx context.Context, runID string) (*Run, error)
|
||||
|
||||
// ReadWithOptions reads a run by its ID using the options supplied
|
||||
ReadWithOptions(ctx context.Context, runID string, options *RunReadOptions) (*Run, error)
|
||||
|
||||
// Apply a run by its ID.
|
||||
Apply(ctx context.Context, runID string, options RunApplyOptions) error
|
||||
|
||||
// Cancel a run by its ID.
|
||||
Cancel(ctx context.Context, runID string, options RunCancelOptions) error
|
||||
|
||||
// Force-cancel a run by its ID.
|
||||
ForceCancel(ctx context.Context, runID string, options RunForceCancelOptions) error
|
||||
|
||||
// Discard a run by its ID.
|
||||
Discard(ctx context.Context, runID string, options RunDiscardOptions) error
|
||||
}
|
||||
|
||||
// runs implements Runs.
|
||||
type runs struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// RunStatus represents a run state.
|
||||
type RunStatus string
|
||||
|
||||
//List all available run statuses.
|
||||
const (
|
||||
RunApplied RunStatus = "applied"
|
||||
RunApplyQueued RunStatus = "apply_queued"
|
||||
RunApplying RunStatus = "applying"
|
||||
RunCanceled RunStatus = "canceled"
|
||||
RunConfirmed RunStatus = "confirmed"
|
||||
RunCostEstimated RunStatus = "cost_estimated"
|
||||
RunCostEstimating RunStatus = "cost_estimating"
|
||||
RunDiscarded RunStatus = "discarded"
|
||||
RunErrored RunStatus = "errored"
|
||||
RunPending RunStatus = "pending"
|
||||
RunPlanQueued RunStatus = "plan_queued"
|
||||
RunPlanned RunStatus = "planned"
|
||||
RunPlannedAndFinished RunStatus = "planned_and_finished"
|
||||
RunPlanning RunStatus = "planning"
|
||||
RunPolicyChecked RunStatus = "policy_checked"
|
||||
RunPolicyChecking RunStatus = "policy_checking"
|
||||
RunPolicyOverride RunStatus = "policy_override"
|
||||
RunPolicySoftFailed RunStatus = "policy_soft_failed"
|
||||
)
|
||||
|
||||
// RunSource represents a source type of a run.
|
||||
type RunSource string
|
||||
|
||||
// List all available run sources.
|
||||
const (
|
||||
RunSourceAPI RunSource = "tfe-api"
|
||||
RunSourceConfigurationVersion RunSource = "tfe-configuration-version"
|
||||
RunSourceUI RunSource = "tfe-ui"
|
||||
)
|
||||
|
||||
// RunList represents a list of runs.
|
||||
type RunList struct {
|
||||
*Pagination
|
||||
Items []*Run
|
||||
}
|
||||
|
||||
// Run represents a Terraform Enterprise run.
|
||||
type Run struct {
|
||||
ID string `jsonapi:"primary,runs"`
|
||||
Actions *RunActions `jsonapi:"attr,actions"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
ForceCancelAvailableAt time.Time `jsonapi:"attr,force-cancel-available-at,iso8601"`
|
||||
HasChanges bool `jsonapi:"attr,has-changes"`
|
||||
IsDestroy bool `jsonapi:"attr,is-destroy"`
|
||||
Message string `jsonapi:"attr,message"`
|
||||
Permissions *RunPermissions `jsonapi:"attr,permissions"`
|
||||
PositionInQueue int `jsonapi:"attr,position-in-queue"`
|
||||
Source RunSource `jsonapi:"attr,source"`
|
||||
Status RunStatus `jsonapi:"attr,status"`
|
||||
StatusTimestamps *RunStatusTimestamps `jsonapi:"attr,status-timestamps"`
|
||||
TargetAddrs []string `jsonapi:"attr,target-addrs,omitempty"`
|
||||
|
||||
// Relations
|
||||
Apply *Apply `jsonapi:"relation,apply"`
|
||||
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
|
||||
CostEstimate *CostEstimate `jsonapi:"relation,cost-estimate"`
|
||||
CreatedBy *User `jsonapi:"relation,created-by"`
|
||||
Plan *Plan `jsonapi:"relation,plan"`
|
||||
PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"`
|
||||
Workspace *Workspace `jsonapi:"relation,workspace"`
|
||||
}
|
||||
|
||||
// RunActions represents the run actions.
|
||||
type RunActions struct {
|
||||
IsCancelable bool `json:"is-cancelable"`
|
||||
IsConfirmable bool `json:"is-confirmable"`
|
||||
IsDiscardable bool `json:"is-discardable"`
|
||||
IsForceCancelable bool `json:"is-force-cancelable"`
|
||||
}
|
||||
|
||||
// RunPermissions represents the run permissions.
|
||||
type RunPermissions struct {
|
||||
CanApply bool `json:"can-apply"`
|
||||
CanCancel bool `json:"can-cancel"`
|
||||
CanDiscard bool `json:"can-discard"`
|
||||
CanForceCancel bool `json:"can-force-cancel"`
|
||||
CanForceExecute bool `json:"can-force-execute"`
|
||||
}
|
||||
|
||||
// RunStatusTimestamps holds the timestamps for individual run statuses.
|
||||
type RunStatusTimestamps struct {
|
||||
ErroredAt time.Time `json:"errored-at"`
|
||||
FinishedAt time.Time `json:"finished-at"`
|
||||
QueuedAt time.Time `json:"queued-at"`
|
||||
StartedAt time.Time `json:"started-at"`
|
||||
ApplyingAt time.Time `json:"applying-at"`
|
||||
AppliedAt time.Time `json:"applied-at"`
|
||||
PlanningAt time.Time `json:"planning-at"`
|
||||
PlannedAt time.Time `json:"planned-at"`
|
||||
PlannedAndFinishedAt time.Time `json:"planned-and-finished-at"`
|
||||
PlanQueuabledAt time.Time `json:"plan-queueable-at"`
|
||||
}
|
||||
|
||||
// RunListOptions represents the options for listing runs.
|
||||
type RunListOptions struct {
|
||||
ListOptions
|
||||
|
||||
// A list of relations to include. See available resources:
|
||||
// https://www.terraform.io/docs/cloud/api/run.html#available-related-resources
|
||||
Include *string `url:"include"`
|
||||
}
|
||||
|
||||
// List all the runs of the given workspace.
|
||||
func (s *runs) List(ctx context.Context, workspaceID string, options RunListOptions) (*RunList, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/runs", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rl := &RunList{}
|
||||
err = s.client.do(ctx, req, rl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rl, nil
|
||||
}
|
||||
|
||||
// RunCreateOptions represents the options for creating a new run.
|
||||
type RunCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,runs"`
|
||||
|
||||
// Specifies if this plan is a destroy plan, which will destroy all
|
||||
// provisioned resources.
|
||||
IsDestroy *bool `jsonapi:"attr,is-destroy,omitempty"`
|
||||
|
||||
// Specifies the message to be associated with this run.
|
||||
Message *string `jsonapi:"attr,message,omitempty"`
|
||||
|
||||
// Specifies the configuration version to use for this run. If the
|
||||
// configuration version object is omitted, the run will be created using the
|
||||
// workspace's latest configuration version.
|
||||
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
|
||||
|
||||
// Specifies the workspace where the run will be executed.
|
||||
Workspace *Workspace `jsonapi:"relation,workspace"`
|
||||
|
||||
// If non-empty, requests that Terraform should create a plan including
|
||||
// actions only for the given objects (specified using resource address
|
||||
// syntax) and the objects they depend on.
|
||||
//
|
||||
// This capability is provided for exceptional circumstances only, such as
|
||||
// recovering from mistakes or working around existing Terraform
|
||||
// limitations. Terraform will generally mention the -target command line
|
||||
// option in its error messages describing situations where setting this
|
||||
// argument may be appropriate. This argument should not be used as part
|
||||
// of routine workflow and Terraform will emit warnings reminding about
|
||||
// this whenever this property is set.
|
||||
TargetAddrs []string `jsonapi:"attr,target-addrs,omitempty"`
|
||||
}
|
||||
|
||||
func (o RunCreateOptions) valid() error {
|
||||
if o.Workspace == nil {
|
||||
return errors.New("workspace is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new run with the given options.
|
||||
func (s *runs) Create(ctx context.Context, options RunCreateOptions) (*Run, error) {
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
req, err := s.client.newRequest("POST", "runs", &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &Run{}
|
||||
err = s.client.do(ctx, req, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Read a run by its ID.
|
||||
func (s *runs) Read(ctx context.Context, runID string) (*Run, error) {
|
||||
return s.ReadWithOptions(ctx, runID, nil)
|
||||
}
|
||||
|
||||
// RunReadOptions represents the options for reading a run.
|
||||
type RunReadOptions struct {
|
||||
Include string `url:"include"`
|
||||
}
|
||||
|
||||
// Read a run by its ID with the given options.
|
||||
func (s *runs) ReadWithOptions(ctx context.Context, runID string, options *RunReadOptions) (*Run, error) {
|
||||
if !validStringID(&runID) {
|
||||
return nil, errors.New("invalid value for run ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("runs/%s", url.QueryEscape(runID))
|
||||
req, err := s.client.newRequest("GET", u, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &Run{}
|
||||
err = s.client.do(ctx, req, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// RunApplyOptions represents the options for applying a run.
|
||||
type RunApplyOptions struct {
|
||||
// An optional comment about the run.
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Apply a run by its ID.
|
||||
func (s *runs) Apply(ctx context.Context, runID string, options RunApplyOptions) error {
|
||||
if !validStringID(&runID) {
|
||||
return errors.New("invalid value for run ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("runs/%s/actions/apply", url.QueryEscape(runID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RunCancelOptions represents the options for canceling a run.
|
||||
type RunCancelOptions struct {
|
||||
// An optional explanation for why the run was canceled.
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Cancel a run by its ID.
|
||||
func (s *runs) Cancel(ctx context.Context, runID string, options RunCancelOptions) error {
|
||||
if !validStringID(&runID) {
|
||||
return errors.New("invalid value for run ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("runs/%s/actions/cancel", url.QueryEscape(runID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RunForceCancelOptions represents the options for force-canceling a run.
|
||||
type RunForceCancelOptions struct {
|
||||
// An optional comment explaining the reason for the force-cancel.
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// ForceCancel is used to forcefully cancel a run by its ID.
|
||||
func (s *runs) ForceCancel(ctx context.Context, runID string, options RunForceCancelOptions) error {
|
||||
if !validStringID(&runID) {
|
||||
return errors.New("invalid value for run ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("runs/%s/actions/force-cancel", url.QueryEscape(runID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RunDiscardOptions represents the options for discarding a run.
|
||||
type RunDiscardOptions struct {
|
||||
// An optional explanation for why the run was discarded.
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Discard a run by its ID.
|
||||
func (s *runs) Discard(ctx context.Context, runID string, options RunDiscardOptions) error {
|
||||
if !validStringID(&runID) {
|
||||
return errors.New("invalid value for run ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("runs/%s/actions/discard", url.QueryEscape(runID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ RunTriggers = (*runTriggers)(nil)
|
||||
|
||||
// RunTriggers describes all the Run Trigger
|
||||
// related methods that the Terraform Cloud API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/cloud/api/run-triggers.html
|
||||
type RunTriggers interface {
|
||||
// List all the run triggers within a workspace.
|
||||
List(ctx context.Context, workspaceID string, options RunTriggerListOptions) (*RunTriggerList, error)
|
||||
|
||||
// Create a new run trigger with the given options.
|
||||
Create(ctx context.Context, workspaceID string, options RunTriggerCreateOptions) (*RunTrigger, error)
|
||||
|
||||
// Read a run trigger by its ID.
|
||||
Read(ctx context.Context, RunTriggerID string) (*RunTrigger, error)
|
||||
|
||||
// Delete a run trigger by its ID.
|
||||
Delete(ctx context.Context, RunTriggerID string) error
|
||||
}
|
||||
|
||||
// runTriggers implements RunTriggers.
|
||||
type runTriggers struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// RunTriggerList represents a list of Run Triggers
|
||||
type RunTriggerList struct {
|
||||
*Pagination
|
||||
Items []*RunTrigger
|
||||
}
|
||||
|
||||
// RunTrigger represents a run trigger.
|
||||
type RunTrigger struct {
|
||||
ID string `jsonapi:"primary,run-triggers"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
SourceableName string `jsonapi:"attr,sourceable-name"`
|
||||
WorkspaceName string `jsonapi:"attr,workspace-name"`
|
||||
|
||||
// Relations
|
||||
// TODO: this will eventually need to be polymorphic
|
||||
Sourceable *Workspace `jsonapi:"relation,sourceable"`
|
||||
Workspace *Workspace `jsonapi:"relation,workspace"`
|
||||
}
|
||||
|
||||
// RunTriggerListOptions represents the options for listing
|
||||
// run triggers.
|
||||
type RunTriggerListOptions struct {
|
||||
ListOptions
|
||||
RunTriggerType *string `url:"filter[run-trigger][type]"`
|
||||
}
|
||||
|
||||
func (o RunTriggerListOptions) valid() error {
|
||||
if !validString(o.RunTriggerType) {
|
||||
return errors.New("run-trigger type is required")
|
||||
}
|
||||
if *o.RunTriggerType != "inbound" && *o.RunTriggerType != "outbound" {
|
||||
return errors.New("invalid value for run-trigger type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List all the run triggers associated with a workspace.
|
||||
func (s *runTriggers) List(ctx context.Context, workspaceID string, options RunTriggerListOptions) (*RunTriggerList, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/run-triggers", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("GET", u, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtl := &RunTriggerList{}
|
||||
err = s.client.do(ctx, req, rtl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rtl, nil
|
||||
}
|
||||
|
||||
// RunTriggerCreateOptions represents the options for
|
||||
// creating a new run trigger.
|
||||
type RunTriggerCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,run-triggers"`
|
||||
|
||||
// The source workspace
|
||||
Sourceable *Workspace `jsonapi:"relation,sourceable"`
|
||||
}
|
||||
|
||||
func (o RunTriggerCreateOptions) valid() error {
|
||||
if o.Sourceable == nil {
|
||||
return errors.New("sourceable is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Creates a run trigger with the given options.
|
||||
func (s *runTriggers) Create(ctx context.Context, workspaceID string, options RunTriggerCreateOptions) (*RunTrigger, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/run-triggers", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rt := &RunTrigger{}
|
||||
err = s.client.do(ctx, req, rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
// Read a run trigger by its ID.
|
||||
func (s *runTriggers) Read(ctx context.Context, runTriggerID string) (*RunTrigger, error) {
|
||||
if !validStringID(&runTriggerID) {
|
||||
return nil, errors.New("invalid value for run trigger ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("run-triggers/%s", url.QueryEscape(runTriggerID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rt := &RunTrigger{}
|
||||
err = s.client.do(ctx, req, rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
// Delete a run trigger by its ID.
|
||||
func (s *runTriggers) Delete(ctx context.Context, runTriggerID string) error {
|
||||
if !validStringID(&runTriggerID) {
|
||||
return errors.New("invalid value for run trigger ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("run-triggers/%s", url.QueryEscape(runTriggerID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ SSHKeys = (*sshKeys)(nil)
|
||||
|
||||
// SSHKeys describes all the SSH key related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/ssh-keys.html
|
||||
type SSHKeys interface {
|
||||
// List all the SSH keys for a given organization
|
||||
List(ctx context.Context, organization string, options SSHKeyListOptions) (*SSHKeyList, error)
|
||||
|
||||
// Create an SSH key and associate it with an organization.
|
||||
Create(ctx context.Context, organization string, options SSHKeyCreateOptions) (*SSHKey, error)
|
||||
|
||||
// Read an SSH key by its ID.
|
||||
Read(ctx context.Context, sshKeyID string) (*SSHKey, error)
|
||||
|
||||
// Update an SSH key by its ID.
|
||||
Update(ctx context.Context, sshKeyID string, options SSHKeyUpdateOptions) (*SSHKey, error)
|
||||
|
||||
// Delete an SSH key by its ID.
|
||||
Delete(ctx context.Context, sshKeyID string) error
|
||||
}
|
||||
|
||||
// sshKeys implements SSHKeys.
|
||||
type sshKeys struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// SSHKeyList represents a list of SSH keys.
|
||||
type SSHKeyList struct {
|
||||
*Pagination
|
||||
Items []*SSHKey
|
||||
}
|
||||
|
||||
// SSHKey represents a SSH key.
|
||||
type SSHKey struct {
|
||||
ID string `jsonapi:"primary,ssh-keys"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
}
|
||||
|
||||
// SSHKeyListOptions represents the options for listing SSH keys.
|
||||
type SSHKeyListOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// List all the SSH keys for a given organization
|
||||
func (s *sshKeys) List(ctx context.Context, organization string, options SSHKeyListOptions) (*SSHKeyList, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/ssh-keys", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kl := &SSHKeyList{}
|
||||
err = s.client.do(ctx, req, kl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kl, nil
|
||||
}
|
||||
|
||||
// SSHKeyCreateOptions represents the options for creating an SSH key.
|
||||
type SSHKeyCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,ssh-keys"`
|
||||
|
||||
// A name to identify the SSH key.
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
|
||||
// The content of the SSH private key.
|
||||
Value *string `jsonapi:"attr,value"`
|
||||
}
|
||||
|
||||
func (o SSHKeyCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validString(o.Value) {
|
||||
return errors.New("value is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create an SSH key and associate it with an organization.
|
||||
func (s *sshKeys) Create(ctx context.Context, organization string, options SSHKeyCreateOptions) (*SSHKey, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/ssh-keys", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k := &SSHKey{}
|
||||
err = s.client.do(ctx, req, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// Read an SSH key by its ID.
|
||||
func (s *sshKeys) Read(ctx context.Context, sshKeyID string) (*SSHKey, error) {
|
||||
if !validStringID(&sshKeyID) {
|
||||
return nil, errors.New("invalid value for SSH key ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k := &SSHKey{}
|
||||
err = s.client.do(ctx, req, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// SSHKeyUpdateOptions represents the options for updating an SSH key.
|
||||
type SSHKeyUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,ssh-keys"`
|
||||
|
||||
// A new name to identify the SSH key.
|
||||
Name *string `jsonapi:"attr,name,omitempty"`
|
||||
|
||||
// Updated content of the SSH private key.
|
||||
Value *string `jsonapi:"attr,value,omitempty"`
|
||||
}
|
||||
|
||||
// Update an SSH key by its ID.
|
||||
func (s *sshKeys) Update(ctx context.Context, sshKeyID string, options SSHKeyUpdateOptions) (*SSHKey, error) {
|
||||
if !validStringID(&sshKeyID) {
|
||||
return nil, errors.New("invalid value for SSH key ID")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k := &SSHKey{}
|
||||
err = s.client.do(ctx, req, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// Delete an SSH key by its ID.
|
||||
func (s *sshKeys) Delete(ctx context.Context, sshKeyID string) error {
|
||||
if !validStringID(&sshKeyID) {
|
||||
return errors.New("invalid value for SSH key ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ StateVersions = (*stateVersions)(nil)
|
||||
|
||||
// StateVersions describes all the state version related methods that
|
||||
// the Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/state-versions.html
|
||||
type StateVersions interface {
|
||||
// List all the state versions for a given workspace.
|
||||
List(ctx context.Context, options StateVersionListOptions) (*StateVersionList, error)
|
||||
|
||||
// Create a new state version for the given workspace.
|
||||
Create(ctx context.Context, workspaceID string, options StateVersionCreateOptions) (*StateVersion, error)
|
||||
|
||||
// Read a state version by its ID.
|
||||
Read(ctx context.Context, svID string) (*StateVersion, error)
|
||||
|
||||
// ReadWithOptions reads a state version by its ID using the options supplied
|
||||
ReadWithOptions(ctx context.Context, svID string, options *StateVersionReadOptions) (*StateVersion, error)
|
||||
|
||||
// Current reads the latest available state from the given workspace.
|
||||
Current(ctx context.Context, workspaceID string) (*StateVersion, error)
|
||||
|
||||
// CurrentWithOptions reads the latest available state from the given workspace using the options supplied
|
||||
CurrentWithOptions(ctx context.Context, workspaceID string, options *StateVersionCurrentOptions) (*StateVersion, error)
|
||||
|
||||
// Download retrieves the actual stored state of a state version
|
||||
Download(ctx context.Context, url string) ([]byte, error)
|
||||
}
|
||||
|
||||
// stateVersions implements StateVersions.
|
||||
type stateVersions struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// StateVersionList represents a list of state versions.
|
||||
type StateVersionList struct {
|
||||
*Pagination
|
||||
Items []*StateVersion
|
||||
}
|
||||
|
||||
// StateVersion represents a Terraform Enterprise state version.
|
||||
type StateVersion struct {
|
||||
ID string `jsonapi:"primary,state-versions"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
DownloadURL string `jsonapi:"attr,hosted-state-download-url"`
|
||||
Serial int64 `jsonapi:"attr,serial"`
|
||||
VCSCommitSHA string `jsonapi:"attr,vcs-commit-sha"`
|
||||
VCSCommitURL string `jsonapi:"attr,vcs-commit-url"`
|
||||
|
||||
// Relations
|
||||
Run *Run `jsonapi:"relation,run"`
|
||||
Outputs []*StateVersionOutput `jsonapi:"relation,outputs"`
|
||||
}
|
||||
|
||||
// StateVersionListOptions represents the options for listing state versions.
|
||||
type StateVersionListOptions struct {
|
||||
ListOptions
|
||||
Organization *string `url:"filter[organization][name]"`
|
||||
Workspace *string `url:"filter[workspace][name]"`
|
||||
}
|
||||
|
||||
func (o StateVersionListOptions) valid() error {
|
||||
if !validString(o.Organization) {
|
||||
return errors.New("organization is required")
|
||||
}
|
||||
if !validString(o.Workspace) {
|
||||
return errors.New("workspace is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List all the state versions for a given workspace.
|
||||
func (s *stateVersions) List(ctx context.Context, options StateVersionListOptions) (*StateVersionList, error) {
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.newRequest("GET", "state-versions", &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svl := &StateVersionList{}
|
||||
err = s.client.do(ctx, req, svl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return svl, nil
|
||||
}
|
||||
|
||||
// StateVersionCreateOptions represents the options for creating a state version.
|
||||
type StateVersionCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,state-versions"`
|
||||
|
||||
// The lineage of the state.
|
||||
Lineage *string `jsonapi:"attr,lineage,omitempty"`
|
||||
|
||||
// The MD5 hash of the state version.
|
||||
MD5 *string `jsonapi:"attr,md5"`
|
||||
|
||||
// The serial of the state.
|
||||
Serial *int64 `jsonapi:"attr,serial"`
|
||||
|
||||
// The base64 encoded state.
|
||||
State *string `jsonapi:"attr,state"`
|
||||
|
||||
// Force can be set to skip certain validations. Wrong use
|
||||
// of this flag can cause data loss, so USE WITH CAUTION!
|
||||
Force *bool `jsonapi:"attr,force"`
|
||||
|
||||
// Specifies the run to associate the state with.
|
||||
Run *Run `jsonapi:"relation,run,omitempty"`
|
||||
}
|
||||
|
||||
func (o StateVersionCreateOptions) valid() error {
|
||||
if !validString(o.MD5) {
|
||||
return errors.New("MD5 is required")
|
||||
}
|
||||
if o.Serial == nil {
|
||||
return errors.New("serial is required")
|
||||
}
|
||||
if !validString(o.State) {
|
||||
return errors.New("state is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new state version for the given workspace.
|
||||
func (s *stateVersions) Create(ctx context.Context, workspaceID string, options StateVersionCreateOptions) (*StateVersion, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/state-versions", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sv := &StateVersion{}
|
||||
err = s.client.do(ctx, req, sv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sv, nil
|
||||
}
|
||||
|
||||
// StateVersionReadOptions represents the options for reading state version.
|
||||
type StateVersionReadOptions struct {
|
||||
Include string `url:"include"`
|
||||
}
|
||||
|
||||
// Read a state version by its ID.
|
||||
func (s *stateVersions) ReadWithOptions(ctx context.Context, svID string, options *StateVersionReadOptions) (*StateVersion, error) {
|
||||
if !validStringID(&svID) {
|
||||
return nil, errors.New("invalid value for state version ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("state-versions/%s", url.QueryEscape(svID))
|
||||
req, err := s.client.newRequest("GET", u, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sv := &StateVersion{}
|
||||
err = s.client.do(ctx, req, sv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sv, nil
|
||||
}
|
||||
|
||||
// Read a state version by its ID.
|
||||
func (s *stateVersions) Read(ctx context.Context, svID string) (*StateVersion, error) {
|
||||
return s.ReadWithOptions(ctx, svID, nil)
|
||||
}
|
||||
|
||||
// StateVersionCurrentOptions represents the options for reading the current state version.
|
||||
type StateVersionCurrentOptions struct {
|
||||
Include string `url:"include"`
|
||||
}
|
||||
|
||||
// CurrentWithOptions reads the latest available state from the given workspace using the options supplied.
|
||||
func (s *stateVersions) CurrentWithOptions(ctx context.Context, workspaceID string, options *StateVersionCurrentOptions) (*StateVersion, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/current-state-version", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("GET", u, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sv := &StateVersion{}
|
||||
err = s.client.do(ctx, req, sv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sv, nil
|
||||
}
|
||||
|
||||
// Current reads the latest available state from the given workspace.
|
||||
func (s *stateVersions) Current(ctx context.Context, workspaceID string) (*StateVersion, error) {
|
||||
return s.CurrentWithOptions(ctx, workspaceID, nil)
|
||||
}
|
||||
|
||||
// Download retrieves the actual stored state of a state version
|
||||
func (s *stateVersions) Download(ctx context.Context, url string) ([]byte, error) {
|
||||
req, err := s.client.newRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = s.client.do(ctx, req, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ StateVersionOutputs = (*stateVersionOutputs)(nil)
|
||||
|
||||
//State version outputs are the output values from a Terraform state file.
|
||||
//They include the name and value of the output, as well as a sensitive boolean
|
||||
//if the value should be hidden by default in UIs.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/cloud/api/state-version-outputs.html
|
||||
type StateVersionOutputs interface {
|
||||
Read(ctx context.Context, outputID string) (*StateVersionOutput, error)
|
||||
}
|
||||
|
||||
type stateVersionOutputs struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
type StateVersionOutput struct {
|
||||
ID string `jsonapi:"primary,state-version-outputs"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
Sensitive bool `jsonapi:"attr,sensitive"`
|
||||
Type string `jsonapi:"attr,type"`
|
||||
Value string `jsonapi:"attr,value"`
|
||||
}
|
||||
|
||||
func (s *stateVersionOutputs) Read(ctx context.Context, outputID string) (*StateVersionOutput, error) {
|
||||
if !validStringID(&outputID) {
|
||||
return nil, errors.New("invalid value for run ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("state-version-outputs/%s", url.QueryEscape(outputID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
so := &StateVersionOutput{}
|
||||
err = s.client.do(ctx, req, so)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return so, nil
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ Teams = (*teams)(nil)
|
||||
|
||||
// Teams describes all the team related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/teams.html
|
||||
type Teams interface {
|
||||
// List all the teams of the given organization.
|
||||
List(ctx context.Context, organization string, options TeamListOptions) (*TeamList, error)
|
||||
|
||||
// Create a new team with the given options.
|
||||
Create(ctx context.Context, organization string, options TeamCreateOptions) (*Team, error)
|
||||
|
||||
// Read a team by its ID.
|
||||
Read(ctx context.Context, teamID string) (*Team, error)
|
||||
|
||||
// Update a team by its ID.
|
||||
Update(ctx context.Context, teamID string, options TeamUpdateOptions) (*Team, error)
|
||||
|
||||
// Delete a team by its ID.
|
||||
Delete(ctx context.Context, teamID string) error
|
||||
}
|
||||
|
||||
// teams implements Teams.
|
||||
type teams struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// TeamList represents a list of teams.
|
||||
type TeamList struct {
|
||||
*Pagination
|
||||
Items []*Team
|
||||
}
|
||||
|
||||
// Team represents a Terraform Enterprise team.
|
||||
type Team struct {
|
||||
ID string `jsonapi:"primary,teams"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
OrganizationAccess *OrganizationAccess `jsonapi:"attr,organization-access"`
|
||||
Visibility string `jsonapi:"attr,visibility"`
|
||||
Permissions *TeamPermissions `jsonapi:"attr,permissions"`
|
||||
UserCount int `jsonapi:"attr,users-count"`
|
||||
|
||||
// Relations
|
||||
Users []*User `jsonapi:"relation,users"`
|
||||
OrganizationMemberships []*OrganizationMembership `jsonapi:"relation,organization-memberships"`
|
||||
}
|
||||
|
||||
// OrganizationAccess represents the team's permissions on its organization
|
||||
type OrganizationAccess struct {
|
||||
ManagePolicies bool `json:"manage-policies"`
|
||||
ManageWorkspaces bool `json:"manage-workspaces"`
|
||||
ManageVCSSettings bool `json:"manage-vcs-settings"`
|
||||
}
|
||||
|
||||
// TeamPermissions represents the current user's permissions on the team.
|
||||
type TeamPermissions struct {
|
||||
CanDestroy bool `json:"can-destroy"`
|
||||
CanUpdateMembership bool `json:"can-update-membership"`
|
||||
}
|
||||
|
||||
// TeamListOptions represents the options for listing teams.
|
||||
type TeamListOptions struct {
|
||||
ListOptions
|
||||
|
||||
Include string `url:"include"`
|
||||
}
|
||||
|
||||
// List all the teams of the given organization.
|
||||
func (s *teams) List(ctx context.Context, organization string, options TeamListOptions) (*TeamList, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/teams", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tl := &TeamList{}
|
||||
err = s.client.do(ctx, req, tl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tl, nil
|
||||
}
|
||||
|
||||
// TeamCreateOptions represents the options for creating a team.
|
||||
type TeamCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,teams"`
|
||||
|
||||
// Name of the team.
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
|
||||
// The team's organization access
|
||||
OrganizationAccess *OrganizationAccessOptions `jsonapi:"attr,organization-access,omitempty"`
|
||||
|
||||
// The team's visibility ("secret", "organization")
|
||||
Visibility *string `jsonapi:"attr,visibility,omitempty"`
|
||||
}
|
||||
|
||||
// OrganizationAccessOptions represents the organization access options of a team.
|
||||
type OrganizationAccessOptions struct {
|
||||
ManagePolicies *bool `json:"manage-policies,omitempty"`
|
||||
ManageWorkspaces *bool `json:"manage-workspaces,omitempty"`
|
||||
ManageVCSSettings *bool `json:"manage-vcs-settings,omitempty"`
|
||||
}
|
||||
|
||||
func (o TeamCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new team with the given options.
|
||||
func (s *teams) Create(ctx context.Context, organization string, options TeamCreateOptions) (*Team, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/teams", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &Team{}
|
||||
err = s.client.do(ctx, req, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Read a single team by its ID.
|
||||
func (s *teams) Read(ctx context.Context, teamID string) (*Team, error) {
|
||||
if !validStringID(&teamID) {
|
||||
return nil, errors.New("invalid value for team ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &Team{}
|
||||
err = s.client.do(ctx, req, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// TeamUpdateOptions represents the options for updating a team.
|
||||
type TeamUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,teams"`
|
||||
|
||||
// New name for the team
|
||||
Name *string `jsonapi:"attr,name,omitempty"`
|
||||
|
||||
// The team's organization access
|
||||
OrganizationAccess *OrganizationAccessOptions `jsonapi:"attr,organization-access,omitempty"`
|
||||
|
||||
// The team's visibility ("secret", "organization")
|
||||
Visibility *string `jsonapi:"attr,visibility,omitempty"`
|
||||
}
|
||||
|
||||
// Update a team by its ID.
|
||||
func (s *teams) Update(ctx context.Context, teamID string, options TeamUpdateOptions) (*Team, error) {
|
||||
if !validStringID(&teamID) {
|
||||
return nil, errors.New("invalid value for team ID")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &Team{}
|
||||
err = s.client.do(ctx, req, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Delete a team by its ID.
|
||||
func (s *teams) Delete(ctx context.Context, teamID string) error {
|
||||
if !validStringID(&teamID) {
|
||||
return errors.New("invalid value for team ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ TeamAccesses = (*teamAccesses)(nil)
|
||||
|
||||
// TeamAccesses describes all the team access related methods that the
|
||||
// Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/team-access.html
|
||||
type TeamAccesses interface {
|
||||
// List all the team accesses for a given workspace.
|
||||
List(ctx context.Context, options TeamAccessListOptions) (*TeamAccessList, error)
|
||||
|
||||
// Add team access for a workspace.
|
||||
Add(ctx context.Context, options TeamAccessAddOptions) (*TeamAccess, error)
|
||||
|
||||
// Read a team access by its ID.
|
||||
Read(ctx context.Context, teamAccessID string) (*TeamAccess, error)
|
||||
|
||||
// Update a team access by its ID.
|
||||
Update(ctx context.Context, teamAccessID string, options TeamAccessUpdateOptions) (*TeamAccess, error)
|
||||
|
||||
// Remove team access from a workspace.
|
||||
Remove(ctx context.Context, teamAccessID string) error
|
||||
}
|
||||
|
||||
// teamAccesses implements TeamAccesses.
|
||||
type teamAccesses struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// AccessType represents a team access type.
|
||||
type AccessType string
|
||||
|
||||
// RunsPermissionType represents the permissiontype to a workspace's runs.
|
||||
type RunsPermissionType string
|
||||
|
||||
// VariablesPermissionType represents the permissiontype to a workspace's variables.
|
||||
type VariablesPermissionType string
|
||||
|
||||
// StateVersionsPermissionType represents the permissiontype to a workspace's state versions.
|
||||
type StateVersionsPermissionType string
|
||||
|
||||
// SentinelMocksPermissionType represents the permissiontype to a workspace's Sentinel mocks.
|
||||
type SentinelMocksPermissionType string
|
||||
|
||||
// WorkspaceLockingPermissionType represents the permissiontype to lock or unlock a workspace.
|
||||
type WorkspaceLockingPermissionType bool
|
||||
|
||||
// List all available team access types and permissions.
|
||||
const (
|
||||
AccessAdmin AccessType = "admin"
|
||||
AccessPlan AccessType = "plan"
|
||||
AccessRead AccessType = "read"
|
||||
AccessWrite AccessType = "write"
|
||||
AccessCustom AccessType = "custom"
|
||||
|
||||
RunsPermissionRead RunsPermissionType = "read"
|
||||
RunsPermissionPlan RunsPermissionType = "plan"
|
||||
RunsPermissionApply RunsPermissionType = "apply"
|
||||
|
||||
VariablesPermissionNone VariablesPermissionType = "none"
|
||||
VariablesPermissionRead VariablesPermissionType = "read"
|
||||
VariablesPermissionWrite VariablesPermissionType = "write"
|
||||
|
||||
StateVersionsPermissionNone StateVersionsPermissionType = "none"
|
||||
StateVersionsPermissionReadOutputs StateVersionsPermissionType = "read-outputs"
|
||||
StateVersionsPermissionRead StateVersionsPermissionType = "read"
|
||||
StateVersionsPermissionWrite StateVersionsPermissionType = "write"
|
||||
|
||||
SentinelMocksPermissionNone SentinelMocksPermissionType = "none"
|
||||
SentinelMocksPermissionRead SentinelMocksPermissionType = "read"
|
||||
)
|
||||
|
||||
// TeamAccessList represents a list of team accesses.
|
||||
type TeamAccessList struct {
|
||||
*Pagination
|
||||
Items []*TeamAccess
|
||||
}
|
||||
|
||||
// TeamAccess represents the workspace access for a team.
|
||||
type TeamAccess struct {
|
||||
ID string `jsonapi:"primary,team-workspaces"`
|
||||
Access AccessType `jsonapi:"attr,access"`
|
||||
Runs RunsPermissionType `jsonapi:"attr,runs"`
|
||||
Variables VariablesPermissionType `jsonapi:"attr,variables"`
|
||||
StateVersions StateVersionsPermissionType `jsonapi:"attr,state-versions"`
|
||||
SentinelMocks SentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks"`
|
||||
WorkspaceLocking bool `jsonapi:"attr,workspace-locking"`
|
||||
|
||||
// Relations
|
||||
Team *Team `jsonapi:"relation,team"`
|
||||
Workspace *Workspace `jsonapi:"relation,workspace"`
|
||||
}
|
||||
|
||||
// TeamAccessListOptions represents the options for listing team accesses.
|
||||
type TeamAccessListOptions struct {
|
||||
ListOptions
|
||||
WorkspaceID *string `url:"filter[workspace][id],omitempty"`
|
||||
}
|
||||
|
||||
func (o TeamAccessListOptions) valid() error {
|
||||
if !validString(o.WorkspaceID) {
|
||||
return errors.New("workspace ID is required")
|
||||
}
|
||||
if !validStringID(o.WorkspaceID) {
|
||||
return errors.New("invalid value for workspace ID")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List all the team accesses for a given workspace.
|
||||
func (s *teamAccesses) List(ctx context.Context, options TeamAccessListOptions) (*TeamAccessList, error) {
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.newRequest("GET", "team-workspaces", &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tal := &TeamAccessList{}
|
||||
err = s.client.do(ctx, req, tal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tal, nil
|
||||
}
|
||||
|
||||
// TeamAccessAddOptions represents the options for adding team access.
|
||||
type TeamAccessAddOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,team-workspaces"`
|
||||
|
||||
// The type of access to grant.
|
||||
Access *AccessType `jsonapi:"attr,access"`
|
||||
|
||||
// Custom workspace access permissions. These can only be edited when Access is 'custom'; otherwise, they are
|
||||
// read-only and reflect the Access level's implicit permissions.
|
||||
Runs *RunsPermissionType `jsonapi:"attr,runs,omitempty"`
|
||||
Variables *VariablesPermissionType `jsonapi:"attr,variables,omitempty"`
|
||||
StateVersions *StateVersionsPermissionType `jsonapi:"attr,state-versions,omitempty"`
|
||||
SentinelMocks *SentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks,omitempty"`
|
||||
WorkspaceLocking *bool `jsonapi:"attr,workspace-locking,omitempty"`
|
||||
|
||||
// The team to add to the workspace
|
||||
Team *Team `jsonapi:"relation,team"`
|
||||
|
||||
// The workspace to which the team is to be added.
|
||||
Workspace *Workspace `jsonapi:"relation,workspace"`
|
||||
}
|
||||
|
||||
func (o TeamAccessAddOptions) valid() error {
|
||||
if o.Access == nil {
|
||||
return errors.New("access is required")
|
||||
}
|
||||
if o.Team == nil {
|
||||
return errors.New("team is required")
|
||||
}
|
||||
if o.Workspace == nil {
|
||||
return errors.New("workspace is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add team access for a workspace.
|
||||
func (s *teamAccesses) Add(ctx context.Context, options TeamAccessAddOptions) (*TeamAccess, error) {
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
req, err := s.client.newRequest("POST", "team-workspaces", &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ta := &TeamAccess{}
|
||||
err = s.client.do(ctx, req, ta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ta, nil
|
||||
}
|
||||
|
||||
// Read a team access by its ID.
|
||||
func (s *teamAccesses) Read(ctx context.Context, teamAccessID string) (*TeamAccess, error) {
|
||||
if !validStringID(&teamAccessID) {
|
||||
return nil, errors.New("invalid value for team access ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ta := &TeamAccess{}
|
||||
err = s.client.do(ctx, req, ta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ta, nil
|
||||
}
|
||||
|
||||
// TeamAccessUpdateOptions represents the options for updating team access.
|
||||
type TeamAccessUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,team-workspaces"`
|
||||
|
||||
// The type of access to grant.
|
||||
Access *AccessType `jsonapi:"attr,access,omitempty"`
|
||||
|
||||
// Custom workspace access permissions. These can only be edited when Access is 'custom'; otherwise, they are
|
||||
// read-only and reflect the Access level's implicit permissions.
|
||||
Runs *RunsPermissionType `jsonapi:"attr,runs,omitempty"`
|
||||
Variables *VariablesPermissionType `jsonapi:"attr,variables,omitempty"`
|
||||
StateVersions *StateVersionsPermissionType `jsonapi:"attr,state-versions,omitempty"`
|
||||
SentinelMocks *SentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks,omitempty"`
|
||||
WorkspaceLocking *bool `jsonapi:"attr,workspace-locking,omitempty"`
|
||||
}
|
||||
|
||||
// Update team access for a workspace
|
||||
func (s *teamAccesses) Update(ctx context.Context, teamAccessID string, options TeamAccessUpdateOptions) (*TeamAccess, error) {
|
||||
if !validStringID(&teamAccessID) {
|
||||
return nil, errors.New("invalid value for team access ID")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ta := &TeamAccess{}
|
||||
err = s.client.do(ctx, req, ta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ta, err
|
||||
}
|
||||
|
||||
// Remove team access from a workspace.
|
||||
func (s *teamAccesses) Remove(ctx context.Context, teamAccessID string) error {
|
||||
if !validStringID(&teamAccessID) {
|
||||
return errors.New("invalid value for team access ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
retryablehttp "github.com/hashicorp/go-retryablehttp"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ TeamMembers = (*teamMembers)(nil)
|
||||
|
||||
// TeamMembers describes all the team member related methods that the
|
||||
// Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/team-members.html
|
||||
type TeamMembers interface {
|
||||
// List returns all Users of a team calling ListUsers
|
||||
// See ListOrganizationMemberships for fetching memberships
|
||||
List(ctx context.Context, teamID string) ([]*User, error)
|
||||
|
||||
// ListUsers returns the Users of this team.
|
||||
ListUsers(ctx context.Context, teamID string) ([]*User, error)
|
||||
|
||||
// ListOrganizationMemberships returns the OrganizationMemberships of this team.
|
||||
ListOrganizationMemberships(ctx context.Context, teamID string) ([]*OrganizationMembership, error)
|
||||
|
||||
// Add multiple users to a team.
|
||||
Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error
|
||||
|
||||
// Remove multiple users from a team.
|
||||
Remove(ctx context.Context, teamID string, options TeamMemberRemoveOptions) error
|
||||
}
|
||||
|
||||
// teamMembers implements TeamMembers.
|
||||
type teamMembers struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
type teamMemberUser struct {
|
||||
Username string `jsonapi:"primary,users"`
|
||||
}
|
||||
|
||||
type teamMemberOrgMembership struct {
|
||||
ID string `jsonapi:"primary,organization-memberships"`
|
||||
}
|
||||
|
||||
// List returns all Users of a team calling ListUsers
|
||||
// See ListOrganizationMemberships for fetching memberships
|
||||
func (s *teamMembers) List(ctx context.Context, teamID string) ([]*User, error) {
|
||||
return s.ListUsers(ctx, teamID)
|
||||
}
|
||||
|
||||
// ListUsers returns the Users of this team.
|
||||
func (s *teamMembers) ListUsers(ctx context.Context, teamID string) ([]*User, error) {
|
||||
if !validStringID(&teamID) {
|
||||
return nil, errors.New("invalid value for team ID")
|
||||
}
|
||||
|
||||
options := struct {
|
||||
Include string `url:"include"`
|
||||
}{
|
||||
Include: "users",
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
|
||||
req, err := s.client.newRequest("GET", u, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &Team{}
|
||||
err = s.client.do(ctx, req, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t.Users, nil
|
||||
}
|
||||
|
||||
// ListOrganizationMemberships returns the OrganizationMemberships of this team.
|
||||
func (s *teamMembers) ListOrganizationMemberships(ctx context.Context, teamID string) ([]*OrganizationMembership, error) {
|
||||
if !validStringID(&teamID) {
|
||||
return nil, errors.New("invalid value for team ID")
|
||||
}
|
||||
|
||||
options := struct {
|
||||
Include string `url:"include"`
|
||||
}{
|
||||
Include: "organization-memberships",
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
|
||||
req, err := s.client.newRequest("GET", u, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &Team{}
|
||||
err = s.client.do(ctx, req, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t.OrganizationMemberships, nil
|
||||
}
|
||||
|
||||
// TeamMemberAddOptions represents the options for
|
||||
// adding or removing team members.
|
||||
type TeamMemberAddOptions struct {
|
||||
Usernames []string
|
||||
OrganizationMembershipIDs []string
|
||||
}
|
||||
|
||||
func (o *TeamMemberAddOptions) valid() error {
|
||||
if o.Usernames == nil && o.OrganizationMembershipIDs == nil {
|
||||
return errors.New("usernames or organization membership ids are required")
|
||||
}
|
||||
if o.Usernames != nil && o.OrganizationMembershipIDs != nil {
|
||||
return errors.New("only one of usernames or organization membership ids can be provided")
|
||||
}
|
||||
if o.Usernames != nil && len(o.Usernames) == 0 {
|
||||
return errors.New("invalid value for usernames")
|
||||
}
|
||||
if o.OrganizationMembershipIDs != nil && len(o.OrganizationMembershipIDs) == 0 {
|
||||
return errors.New("invalid value for organization membership ids")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// kind returns "users" or "organization-memberships"
|
||||
// depending on which is defined
|
||||
func (o *TeamMemberAddOptions) kind() string {
|
||||
if o.Usernames != nil && len(o.Usernames) != 0 {
|
||||
return "users"
|
||||
}
|
||||
return "organization-memberships"
|
||||
}
|
||||
|
||||
// Add multiple users to a team.
|
||||
func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error {
|
||||
if !validStringID(&teamID) {
|
||||
return errors.New("invalid value for team ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usersOrMemberships := options.kind()
|
||||
URL := fmt.Sprintf("teams/%s/relationships/%s", url.QueryEscape(teamID), usersOrMemberships)
|
||||
|
||||
var req *retryablehttp.Request
|
||||
|
||||
if usersOrMemberships == "users" {
|
||||
var err error
|
||||
var members []*teamMemberUser
|
||||
for _, name := range options.Usernames {
|
||||
members = append(members, &teamMemberUser{Username: name})
|
||||
}
|
||||
req, err = s.client.newRequest("POST", URL, members)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
var members []*teamMemberOrgMembership
|
||||
for _, ID := range options.OrganizationMembershipIDs {
|
||||
members = append(members, &teamMemberOrgMembership{ID: ID})
|
||||
}
|
||||
req, err = s.client.newRequest("POST", URL, members)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// TeamMemberRemoveOptions represents the options for
|
||||
// adding or removing team members.
|
||||
type TeamMemberRemoveOptions struct {
|
||||
Usernames []string
|
||||
OrganizationMembershipIDs []string
|
||||
}
|
||||
|
||||
func (o *TeamMemberRemoveOptions) valid() error {
|
||||
if o.Usernames == nil && o.OrganizationMembershipIDs == nil {
|
||||
return errors.New("usernames or organization membership ids are required")
|
||||
}
|
||||
if o.Usernames != nil && o.OrganizationMembershipIDs != nil {
|
||||
return errors.New("only one of usernames or organization membership ids can be provided")
|
||||
}
|
||||
if o.Usernames != nil && len(o.Usernames) == 0 {
|
||||
return errors.New("invalid value for usernames")
|
||||
}
|
||||
if o.OrganizationMembershipIDs != nil && len(o.OrganizationMembershipIDs) == 0 {
|
||||
return errors.New("invalid value for organization membership ids")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// kind returns "users" or "organization-memberships"
|
||||
// depending on which is defined
|
||||
func (o *TeamMemberRemoveOptions) kind() string {
|
||||
if o.Usernames != nil && len(o.Usernames) != 0 {
|
||||
return "users"
|
||||
}
|
||||
return "organization-memberships"
|
||||
}
|
||||
|
||||
// Remove multiple users from a team.
|
||||
func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMemberRemoveOptions) error {
|
||||
if !validStringID(&teamID) {
|
||||
return errors.New("invalid value for team ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usersOrMemberships := options.kind()
|
||||
URL := fmt.Sprintf("teams/%s/relationships/%s", url.QueryEscape(teamID), usersOrMemberships)
|
||||
|
||||
var req *retryablehttp.Request
|
||||
|
||||
if usersOrMemberships == "users" {
|
||||
var err error
|
||||
var members []*teamMemberUser
|
||||
for _, name := range options.Usernames {
|
||||
members = append(members, &teamMemberUser{Username: name})
|
||||
}
|
||||
req, err = s.client.newRequest("DELETE", URL, members)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
var members []*teamMemberOrgMembership
|
||||
for _, ID := range options.OrganizationMembershipIDs {
|
||||
members = append(members, &teamMemberOrgMembership{ID: ID})
|
||||
}
|
||||
req, err = s.client.newRequest("DELETE", URL, members)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ TeamTokens = (*teamTokens)(nil)
|
||||
|
||||
// TeamTokens describes all the team token related methods that the
|
||||
// Terraform Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/team-tokens.html
|
||||
type TeamTokens interface {
|
||||
// Generate a new team token, replacing any existing token.
|
||||
Generate(ctx context.Context, teamID string) (*TeamToken, error)
|
||||
|
||||
// Read a team token by its ID.
|
||||
Read(ctx context.Context, teamID string) (*TeamToken, error)
|
||||
|
||||
// Delete a team token by its ID.
|
||||
Delete(ctx context.Context, teamID string) error
|
||||
}
|
||||
|
||||
// teamTokens implements TeamTokens.
|
||||
type teamTokens struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// TeamToken represents a Terraform Enterprise team token.
|
||||
type TeamToken struct {
|
||||
ID string `jsonapi:"primary,authentication-tokens"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
Description string `jsonapi:"attr,description"`
|
||||
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
|
||||
Token string `jsonapi:"attr,token"`
|
||||
}
|
||||
|
||||
// Generate a new team token, replacing any existing token.
|
||||
func (s *teamTokens) Generate(ctx context.Context, teamID string) (*TeamToken, error) {
|
||||
if !validStringID(&teamID) {
|
||||
return nil, errors.New("invalid value for team ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
|
||||
req, err := s.client.newRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tt := &TeamToken{}
|
||||
err = s.client.do(ctx, req, tt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tt, err
|
||||
}
|
||||
|
||||
// Read a team token by its ID.
|
||||
func (s *teamTokens) Read(ctx context.Context, teamID string) (*TeamToken, error) {
|
||||
if !validStringID(&teamID) {
|
||||
return nil, errors.New("invalid value for team ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tt := &TeamToken{}
|
||||
err = s.client.do(ctx, req, tt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tt, err
|
||||
}
|
||||
|
||||
// Delete a team token by its ID.
|
||||
func (s *teamTokens) Delete(ctx context.Context, teamID string) error {
|
||||
if !validStringID(&teamID) {
|
||||
return errors.New("invalid value for team ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestAccountDetails represents the basic account information
|
||||
// of a TFE/TFC user.
|
||||
//
|
||||
// See FetchTestAccountDetails for more information.
|
||||
type TestAccountDetails struct {
|
||||
ID string `json:"id" jsonapi:"primary,users"`
|
||||
Username string `jsonapi:"attr,username"`
|
||||
Email string `jsonapi:"attr,email"`
|
||||
}
|
||||
|
||||
// FetchTestAccountDetails returns TestAccountDetails
|
||||
// of the user running the tests.
|
||||
//
|
||||
// Use this helper to fetch the username and email
|
||||
// address associated with the token used to run the tests.
|
||||
func FetchTestAccountDetails(t *testing.T, client *Client) *TestAccountDetails {
|
||||
tad := &TestAccountDetails{}
|
||||
req, err := client.newRequest("GET", "account/details", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create account details request: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err = client.do(ctx, req, tad)
|
||||
if err != nil {
|
||||
t.Fatalf("could not fetch test user details: %v", err)
|
||||
}
|
||||
return tad
|
||||
}
|
|
@ -0,0 +1,730 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/hashicorp/go-cleanhttp"
|
||||
retryablehttp "github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/svanharmelen/jsonapi"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const (
|
||||
userAgent = "go-tfe"
|
||||
headerRateLimit = "X-RateLimit-Limit"
|
||||
headerRateReset = "X-RateLimit-Reset"
|
||||
headerAPIVersion = "TFP-API-Version"
|
||||
|
||||
// DefaultAddress of Terraform Enterprise.
|
||||
DefaultAddress = "https://app.terraform.io"
|
||||
// DefaultBasePath on which the API is served.
|
||||
DefaultBasePath = "/api/v2/"
|
||||
// PingEndpoint is a no-op API endpoint used to configure the rate limiter
|
||||
PingEndpoint = "ping"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrWorkspaceLocked is returned when trying to lock a
|
||||
// locked workspace.
|
||||
ErrWorkspaceLocked = errors.New("workspace already locked")
|
||||
// ErrWorkspaceNotLocked is returned when trying to unlock
|
||||
// a unlocked workspace.
|
||||
ErrWorkspaceNotLocked = errors.New("workspace already unlocked")
|
||||
|
||||
// ErrUnauthorized is returned when a receiving a 401.
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
// ErrResourceNotFound is returned when a receiving a 404.
|
||||
ErrResourceNotFound = errors.New("resource not found")
|
||||
)
|
||||
|
||||
// RetryLogHook allows a function to run before each retry.
|
||||
type RetryLogHook func(attemptNum int, resp *http.Response)
|
||||
|
||||
// Config provides configuration details to the API client.
|
||||
type Config struct {
|
||||
// The address of the Terraform Enterprise API.
|
||||
Address string
|
||||
|
||||
// The base path on which the API is served.
|
||||
BasePath string
|
||||
|
||||
// API token used to access the Terraform Enterprise API.
|
||||
Token string
|
||||
|
||||
// Headers that will be added to every request.
|
||||
Headers http.Header
|
||||
|
||||
// A custom HTTP client to use.
|
||||
HTTPClient *http.Client
|
||||
|
||||
// RetryLogHook is invoked each time a request is retried.
|
||||
RetryLogHook RetryLogHook
|
||||
}
|
||||
|
||||
// DefaultConfig returns a default config structure.
|
||||
func DefaultConfig() *Config {
|
||||
config := &Config{
|
||||
Address: os.Getenv("TFE_ADDRESS"),
|
||||
BasePath: DefaultBasePath,
|
||||
Token: os.Getenv("TFE_TOKEN"),
|
||||
Headers: make(http.Header),
|
||||
HTTPClient: cleanhttp.DefaultPooledClient(),
|
||||
}
|
||||
|
||||
// Set the default address if none is given.
|
||||
if config.Address == "" {
|
||||
config.Address = DefaultAddress
|
||||
}
|
||||
|
||||
// Set the default user agent.
|
||||
config.Headers.Set("User-Agent", userAgent)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// Client is the Terraform Enterprise API client. It provides the basic
|
||||
// connectivity and configuration for accessing the TFE API.
|
||||
type Client struct {
|
||||
baseURL *url.URL
|
||||
token string
|
||||
headers http.Header
|
||||
http *retryablehttp.Client
|
||||
limiter *rate.Limiter
|
||||
retryLogHook RetryLogHook
|
||||
retryServerErrors bool
|
||||
remoteAPIVersion string
|
||||
|
||||
AgentPools AgentPools
|
||||
AgentTokens AgentTokens
|
||||
Applies Applies
|
||||
ConfigurationVersions ConfigurationVersions
|
||||
CostEstimates CostEstimates
|
||||
NotificationConfigurations NotificationConfigurations
|
||||
OAuthClients OAuthClients
|
||||
OAuthTokens OAuthTokens
|
||||
Organizations Organizations
|
||||
OrganizationMemberships OrganizationMemberships
|
||||
OrganizationTokens OrganizationTokens
|
||||
Plans Plans
|
||||
PlanExports PlanExports
|
||||
Policies Policies
|
||||
PolicyChecks PolicyChecks
|
||||
PolicySetParameters PolicySetParameters
|
||||
PolicySets PolicySets
|
||||
RegistryModules RegistryModules
|
||||
Runs Runs
|
||||
RunTriggers RunTriggers
|
||||
SSHKeys SSHKeys
|
||||
StateVersionOutputs StateVersionOutputs
|
||||
StateVersions StateVersions
|
||||
Teams Teams
|
||||
TeamAccess TeamAccesses
|
||||
TeamMembers TeamMembers
|
||||
TeamTokens TeamTokens
|
||||
Users Users
|
||||
UserTokens UserTokens
|
||||
Variables Variables
|
||||
Workspaces Workspaces
|
||||
|
||||
Meta Meta
|
||||
}
|
||||
|
||||
// Meta contains any Terraform Cloud APIs which provide data about the API itself.
|
||||
type Meta struct {
|
||||
IPRanges IPRanges
|
||||
}
|
||||
|
||||
// NewClient creates a new Terraform Enterprise API client.
|
||||
func NewClient(cfg *Config) (*Client, error) {
|
||||
config := DefaultConfig()
|
||||
|
||||
// Layer in the provided config for any non-blank values.
|
||||
if cfg != nil {
|
||||
if cfg.Address != "" {
|
||||
config.Address = cfg.Address
|
||||
}
|
||||
if cfg.BasePath != "" {
|
||||
config.BasePath = cfg.BasePath
|
||||
}
|
||||
if cfg.Token != "" {
|
||||
config.Token = cfg.Token
|
||||
}
|
||||
for k, v := range cfg.Headers {
|
||||
config.Headers[k] = v
|
||||
}
|
||||
if cfg.HTTPClient != nil {
|
||||
config.HTTPClient = cfg.HTTPClient
|
||||
}
|
||||
if cfg.RetryLogHook != nil {
|
||||
config.RetryLogHook = cfg.RetryLogHook
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the address to make sure its a valid URL.
|
||||
baseURL, err := url.Parse(config.Address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid address: %v", err)
|
||||
}
|
||||
|
||||
baseURL.Path = config.BasePath
|
||||
if !strings.HasSuffix(baseURL.Path, "/") {
|
||||
baseURL.Path += "/"
|
||||
}
|
||||
|
||||
// This value must be provided by the user.
|
||||
if config.Token == "" {
|
||||
return nil, fmt.Errorf("missing API token")
|
||||
}
|
||||
|
||||
// Create the client.
|
||||
client := &Client{
|
||||
baseURL: baseURL,
|
||||
token: config.Token,
|
||||
headers: config.Headers,
|
||||
retryLogHook: config.RetryLogHook,
|
||||
}
|
||||
|
||||
client.http = &retryablehttp.Client{
|
||||
Backoff: client.retryHTTPBackoff,
|
||||
CheckRetry: client.retryHTTPCheck,
|
||||
ErrorHandler: retryablehttp.PassthroughErrorHandler,
|
||||
HTTPClient: config.HTTPClient,
|
||||
RetryWaitMin: 100 * time.Millisecond,
|
||||
RetryWaitMax: 400 * time.Millisecond,
|
||||
RetryMax: 30,
|
||||
}
|
||||
|
||||
meta, err := client.getRawAPIMetadata()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Configure the rate limiter.
|
||||
client.configureLimiter(meta.RateLimit)
|
||||
|
||||
// Save the API version so we can return it from the RemoteAPIVersion
|
||||
// method later.
|
||||
client.remoteAPIVersion = meta.APIVersion
|
||||
|
||||
// Create the services.
|
||||
client.AgentPools = &agentPools{client: client}
|
||||
client.AgentTokens = &agentTokens{client: client}
|
||||
client.Applies = &applies{client: client}
|
||||
client.ConfigurationVersions = &configurationVersions{client: client}
|
||||
client.CostEstimates = &costEstimates{client: client}
|
||||
client.NotificationConfigurations = ¬ificationConfigurations{client: client}
|
||||
client.OAuthClients = &oAuthClients{client: client}
|
||||
client.OAuthTokens = &oAuthTokens{client: client}
|
||||
client.Organizations = &organizations{client: client}
|
||||
client.OrganizationMemberships = &organizationMemberships{client: client}
|
||||
client.OrganizationTokens = &organizationTokens{client: client}
|
||||
client.Plans = &plans{client: client}
|
||||
client.PlanExports = &planExports{client: client}
|
||||
client.Policies = &policies{client: client}
|
||||
client.PolicyChecks = &policyChecks{client: client}
|
||||
client.PolicySetParameters = &policySetParameters{client: client}
|
||||
client.PolicySets = &policySets{client: client}
|
||||
client.RegistryModules = ®istryModules{client: client}
|
||||
client.Runs = &runs{client: client}
|
||||
client.RunTriggers = &runTriggers{client: client}
|
||||
client.SSHKeys = &sshKeys{client: client}
|
||||
client.StateVersionOutputs = &stateVersionOutputs{client: client}
|
||||
client.StateVersions = &stateVersions{client: client}
|
||||
client.Teams = &teams{client: client}
|
||||
client.TeamAccess = &teamAccesses{client: client}
|
||||
client.TeamMembers = &teamMembers{client: client}
|
||||
client.TeamTokens = &teamTokens{client: client}
|
||||
client.Users = &users{client: client}
|
||||
client.UserTokens = &userTokens{client: client}
|
||||
client.Variables = &variables{client: client}
|
||||
client.Workspaces = &workspaces{client: client}
|
||||
|
||||
client.Meta = Meta{
|
||||
IPRanges: &ipRanges{client: client},
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// RemoteAPIVersion returns the server's declared API version string.
|
||||
//
|
||||
// A Terraform Cloud or Enterprise API server returns its API version in an
|
||||
// HTTP header field in all responses. The NewClient function saves the
|
||||
// version number returned in its initial setup request and RemoteAPIVersion
|
||||
// returns that cached value.
|
||||
//
|
||||
// The API protocol calls for this string to be a dotted-decimal version number
|
||||
// like 2.3.0, where the first number indicates the API major version while the
|
||||
// second indicates a minor version which may have introduced some
|
||||
// backward-compatible additional features compared to its predecessor.
|
||||
//
|
||||
// Explicit API versioning was added to the Terraform Cloud and Enterprise
|
||||
// APIs as a later addition, so older servers will not return version
|
||||
// information. In that case, this function returns an empty string as the
|
||||
// version.
|
||||
func (c *Client) RemoteAPIVersion() string {
|
||||
return c.remoteAPIVersion
|
||||
}
|
||||
|
||||
// SetFakeRemoteAPIVersion allows setting a given string as the client's remoteAPIVersion,
|
||||
// overriding the value pulled from the API header during client initialization.
|
||||
//
|
||||
// This is intended for use in tests, when you may want to configure your TFE client to
|
||||
// return something different than the actual API version in order to test error handling.
|
||||
func (c *Client) SetFakeRemoteAPIVersion(fakeAPIVersion string) {
|
||||
c.remoteAPIVersion = fakeAPIVersion
|
||||
}
|
||||
|
||||
// RetryServerErrors configures the retry HTTP check to also retry
|
||||
// unexpected errors or requests that failed with a server error.
|
||||
func (c *Client) RetryServerErrors(retry bool) {
|
||||
c.retryServerErrors = retry
|
||||
}
|
||||
|
||||
// retryHTTPCheck provides a callback for Client.CheckRetry which
|
||||
// will retry both rate limit (429) and server (>= 500) errors.
|
||||
func (c *Client) retryHTTPCheck(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
if err != nil {
|
||||
return c.retryServerErrors, err
|
||||
}
|
||||
if resp.StatusCode == 429 || (c.retryServerErrors && resp.StatusCode >= 500) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// retryHTTPBackoff provides a generic callback for Client.Backoff which
|
||||
// will pass through all calls based on the status code of the response.
|
||||
func (c *Client) retryHTTPBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
|
||||
if c.retryLogHook != nil {
|
||||
c.retryLogHook(attemptNum, resp)
|
||||
}
|
||||
|
||||
// Use the rate limit backoff function when we are rate limited.
|
||||
if resp != nil && resp.StatusCode == 429 {
|
||||
return rateLimitBackoff(min, max, attemptNum, resp)
|
||||
}
|
||||
|
||||
// Set custom duration's when we experience a service interruption.
|
||||
min = 700 * time.Millisecond
|
||||
max = 900 * time.Millisecond
|
||||
|
||||
return retryablehttp.LinearJitterBackoff(min, max, attemptNum, resp)
|
||||
}
|
||||
|
||||
// rateLimitBackoff provides a callback for Client.Backoff which will use the
|
||||
// X-RateLimit_Reset header to determine the time to wait. We add some jitter
|
||||
// to prevent a thundering herd.
|
||||
//
|
||||
// min and max are mainly used for bounding the jitter that will be added to
|
||||
// the reset time retrieved from the headers. But if the final wait time is
|
||||
// less then min, min will be used instead.
|
||||
func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
|
||||
// rnd is used to generate pseudo-random numbers.
|
||||
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
// First create some jitter bounded by the min and max durations.
|
||||
jitter := time.Duration(rnd.Float64() * float64(max-min))
|
||||
|
||||
if resp != nil {
|
||||
if v := resp.Header.Get(headerRateReset); v != "" {
|
||||
if reset, _ := strconv.ParseFloat(v, 64); reset > 0 {
|
||||
// Only update min if the given time to wait is longer.
|
||||
if wait := time.Duration(reset * 1e9); wait > min {
|
||||
min = wait
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return min + jitter
|
||||
}
|
||||
|
||||
type rawAPIMetadata struct {
|
||||
// APIVersion is the raw API version string reported by the server in the
|
||||
// TFP-API-Version response header, or an empty string if that header
|
||||
// field was not included in the response.
|
||||
APIVersion string
|
||||
|
||||
// RateLimit is the raw API version string reported by the server in the
|
||||
// X-RateLimit-Limit response header, or an empty string if that header
|
||||
// field was not included in the response.
|
||||
RateLimit string
|
||||
}
|
||||
|
||||
func (c *Client) getRawAPIMetadata() (rawAPIMetadata, error) {
|
||||
var meta rawAPIMetadata
|
||||
|
||||
// Create a new request.
|
||||
u, err := c.baseURL.Parse(PingEndpoint)
|
||||
if err != nil {
|
||||
return meta, err
|
||||
}
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return meta, err
|
||||
}
|
||||
|
||||
// Attach the default headers.
|
||||
for k, v := range c.headers {
|
||||
req.Header[k] = v
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.api+json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
|
||||
// Make a single request to retrieve the rate limit headers.
|
||||
resp, err := c.http.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return meta, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
meta.APIVersion = resp.Header.Get(headerAPIVersion)
|
||||
meta.RateLimit = resp.Header.Get(headerRateLimit)
|
||||
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
// configureLimiter configures the rate limiter.
|
||||
func (c *Client) configureLimiter(rawLimit string) {
|
||||
|
||||
// Set default values for when rate limiting is disabled.
|
||||
limit := rate.Inf
|
||||
burst := 0
|
||||
|
||||
if v := rawLimit; v != "" {
|
||||
if rateLimit, _ := strconv.ParseFloat(v, 64); rateLimit > 0 {
|
||||
// Configure the limit and burst using a split of 2/3 for the limit and
|
||||
// 1/3 for the burst. This enables clients to burst 1/3 of the allowed
|
||||
// calls before the limiter kicks in. The remaining calls will then be
|
||||
// spread out evenly using intervals of time.Second / limit which should
|
||||
// prevent hitting the rate limit.
|
||||
limit = rate.Limit(rateLimit * 0.66)
|
||||
burst = int(rateLimit * 0.33)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new limiter using the calculated values.
|
||||
c.limiter = rate.NewLimiter(limit, burst)
|
||||
}
|
||||
|
||||
// newRequest creates an API request. A relative URL path can be provided in
|
||||
// path, in which case it is resolved relative to the apiVersionPath of the
|
||||
// Client. Relative URL paths should always be specified without a preceding
|
||||
// slash.
|
||||
// If v is supplied, the value will be JSONAPI encoded and included as the
|
||||
// request body. If the method is GET, the value will be parsed and added as
|
||||
// query parameters.
|
||||
func (c *Client) newRequest(method, path string, v interface{}) (*retryablehttp.Request, error) {
|
||||
u, err := c.baseURL.Parse(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a request specific headers map.
|
||||
reqHeaders := make(http.Header)
|
||||
reqHeaders.Set("Authorization", "Bearer "+c.token)
|
||||
|
||||
var body interface{}
|
||||
switch method {
|
||||
case "GET":
|
||||
reqHeaders.Set("Accept", "application/vnd.api+json")
|
||||
|
||||
if v != nil {
|
||||
q, err := query.Values(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
}
|
||||
case "DELETE", "PATCH", "POST":
|
||||
reqHeaders.Set("Accept", "application/vnd.api+json")
|
||||
reqHeaders.Set("Content-Type", "application/vnd.api+json")
|
||||
|
||||
if v != nil {
|
||||
if body, err = serializeRequestBody(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
case "PUT":
|
||||
reqHeaders.Set("Accept", "application/json")
|
||||
reqHeaders.Set("Content-Type", "application/octet-stream")
|
||||
body = v
|
||||
}
|
||||
|
||||
req, err := retryablehttp.NewRequest(method, u.String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set the default headers.
|
||||
for k, v := range c.headers {
|
||||
req.Header[k] = v
|
||||
}
|
||||
|
||||
// Set the request specific headers.
|
||||
for k, v := range reqHeaders {
|
||||
req.Header[k] = v
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Helper method that serializes the given ptr or ptr slice into a JSON
|
||||
// request. It automatically uses jsonapi or json serialization, depending
|
||||
// on the body type's tags.
|
||||
func serializeRequestBody(v interface{}) (interface{}, error) {
|
||||
// The body can be a slice of pointers or a pointer. In either
|
||||
// case we want to choose the serialization type based on the
|
||||
// individual record type. To determine that type, we need
|
||||
// to either follow the pointer or examine the slice element type.
|
||||
// There are other theoretical possiblities (e. g. maps,
|
||||
// non-pointers) but they wouldn't work anyway because the
|
||||
// json-api library doesn't support serializing other things.
|
||||
var modelType reflect.Type
|
||||
bodyType := reflect.TypeOf(v)
|
||||
invalidBodyError := errors.New("go-tfe bug: DELETE/PATCH/POST body must be nil, ptr, or ptr slice")
|
||||
switch bodyType.Kind() {
|
||||
case reflect.Slice:
|
||||
sliceElem := bodyType.Elem()
|
||||
if sliceElem.Kind() != reflect.Ptr {
|
||||
return nil, invalidBodyError
|
||||
}
|
||||
modelType = sliceElem.Elem()
|
||||
case reflect.Ptr:
|
||||
modelType = reflect.ValueOf(v).Elem().Type()
|
||||
default:
|
||||
return nil, invalidBodyError
|
||||
}
|
||||
|
||||
// Infer whether the request uses jsonapi or regular json
|
||||
// serialization based on how the fields are tagged.
|
||||
jsonApiFields := 0
|
||||
jsonFields := 0
|
||||
for i := 0; i < modelType.NumField(); i++ {
|
||||
structField := modelType.Field(i)
|
||||
if structField.Tag.Get("jsonapi") != "" {
|
||||
jsonApiFields++
|
||||
}
|
||||
if structField.Tag.Get("json") != "" {
|
||||
jsonFields++
|
||||
}
|
||||
}
|
||||
if jsonApiFields > 0 && jsonFields > 0 {
|
||||
// Defining a struct with both json and jsonapi tags doesn't
|
||||
// make sense, because a struct can only be serialized
|
||||
// as one or another. If this does happen, it's a bug
|
||||
// in the library that should be fixed at development time
|
||||
return nil, errors.New("go-tfe bug: struct can't use both json and jsonapi attributes")
|
||||
}
|
||||
|
||||
if jsonFields > 0 {
|
||||
return json.Marshal(v)
|
||||
} else {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if err := jsonapi.MarshalPayloadWithoutIncluded(buf, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
}
|
||||
|
||||
// do sends an API request and returns the API response. The API response
|
||||
// is JSONAPI decoded and the document's primary data is stored in the value
|
||||
// pointed to by v, or returned as an error if an API error has occurred.
|
||||
|
||||
// If v implements the io.Writer interface, the raw response body will be
|
||||
// written to v, without attempting to first decode it.
|
||||
//
|
||||
// The provided ctx must be non-nil. If it is canceled or times out, ctx.Err()
|
||||
// will be returned.
|
||||
func (c *Client) do(ctx context.Context, req *retryablehttp.Request, v interface{}) error {
|
||||
// Wait will block until the limiter can obtain a new token
|
||||
// or returns an error if the given context is canceled.
|
||||
if err := c.limiter.Wait(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add the context to the request.
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
// Execute the request and check the response.
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
// If we got an error, and the context has been canceled,
|
||||
// the context's error is probably more useful.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Basic response checking.
|
||||
if err := checkResponseCode(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Return here if decoding the response isn't needed.
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If v implements io.Writer, write the raw response body.
|
||||
if w, ok := v.(io.Writer); ok {
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the value of v so we can test if it's a struct.
|
||||
dst := reflect.Indirect(reflect.ValueOf(v))
|
||||
|
||||
// Return an error if v is not a struct or an io.Writer.
|
||||
if dst.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("v must be a struct or an io.Writer")
|
||||
}
|
||||
|
||||
// Try to get the Items and Pagination struct fields.
|
||||
items := dst.FieldByName("Items")
|
||||
pagination := dst.FieldByName("Pagination")
|
||||
|
||||
// Unmarshal a single value if v does not contain the
|
||||
// Items and Pagination struct fields.
|
||||
if !items.IsValid() || !pagination.IsValid() {
|
||||
return jsonapi.UnmarshalPayload(resp.Body, v)
|
||||
}
|
||||
|
||||
// Return an error if v.Items is not a slice.
|
||||
if items.Type().Kind() != reflect.Slice {
|
||||
return fmt.Errorf("v.Items must be a slice")
|
||||
}
|
||||
|
||||
// Create a temporary buffer and copy all the read data into it.
|
||||
body := bytes.NewBuffer(nil)
|
||||
reader := io.TeeReader(resp.Body, body)
|
||||
|
||||
// Unmarshal as a list of values as v.Items is a slice.
|
||||
raw, err := jsonapi.UnmarshalManyPayload(reader, items.Type().Elem())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make a new slice to hold the results.
|
||||
sliceType := reflect.SliceOf(items.Type().Elem())
|
||||
result := reflect.MakeSlice(sliceType, 0, len(raw))
|
||||
|
||||
// Add all of the results to the new slice.
|
||||
for _, v := range raw {
|
||||
result = reflect.Append(result, reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
// Pointer-swap the result.
|
||||
items.Set(result)
|
||||
|
||||
// As we are getting a list of values, we need to decode
|
||||
// the pagination details out of the response body.
|
||||
p, err := parsePagination(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Pointer-swap the decoded pagination details.
|
||||
pagination.Set(reflect.ValueOf(p))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListOptions is used to specify pagination options when making API requests.
|
||||
// Pagination allows breaking up large result sets into chunks, or "pages".
|
||||
type ListOptions struct {
|
||||
// The page number to request. The results vary based on the PageSize.
|
||||
PageNumber int `url:"page[number],omitempty"`
|
||||
|
||||
// The number of elements returned in a single page.
|
||||
PageSize int `url:"page[size],omitempty"`
|
||||
}
|
||||
|
||||
// Pagination is used to return the pagination details of an API request.
|
||||
type Pagination struct {
|
||||
CurrentPage int `json:"current-page"`
|
||||
PreviousPage int `json:"prev-page"`
|
||||
NextPage int `json:"next-page"`
|
||||
TotalPages int `json:"total-pages"`
|
||||
TotalCount int `json:"total-count"`
|
||||
}
|
||||
|
||||
func parsePagination(body io.Reader) (*Pagination, error) {
|
||||
var raw struct {
|
||||
Meta struct {
|
||||
Pagination Pagination `json:"pagination"`
|
||||
} `json:"meta"`
|
||||
}
|
||||
|
||||
// JSON decode the raw response.
|
||||
if err := json.NewDecoder(body).Decode(&raw); err != nil {
|
||||
return &Pagination{}, err
|
||||
}
|
||||
|
||||
return &raw.Meta.Pagination, nil
|
||||
}
|
||||
|
||||
// checkResponseCode can be used to check the status code of an HTTP request.
|
||||
func checkResponseCode(r *http.Response) error {
|
||||
if r.StatusCode >= 200 && r.StatusCode <= 299 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch r.StatusCode {
|
||||
case 401:
|
||||
return ErrUnauthorized
|
||||
case 404:
|
||||
return ErrResourceNotFound
|
||||
case 409:
|
||||
switch {
|
||||
case strings.HasSuffix(r.Request.URL.Path, "actions/lock"):
|
||||
return ErrWorkspaceLocked
|
||||
case strings.HasSuffix(r.Request.URL.Path, "actions/unlock"):
|
||||
return ErrWorkspaceNotLocked
|
||||
case strings.HasSuffix(r.Request.URL.Path, "actions/force-unlock"):
|
||||
return ErrWorkspaceNotLocked
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the error payload.
|
||||
errPayload := &jsonapi.ErrorsPayload{}
|
||||
err := json.NewDecoder(r.Body).Decode(errPayload)
|
||||
if err != nil || len(errPayload.Errors) == 0 {
|
||||
return fmt.Errorf(r.Status)
|
||||
}
|
||||
|
||||
// Parse and format the errors.
|
||||
var errs []string
|
||||
for _, e := range errPayload.Errors {
|
||||
if e.Detail == "" {
|
||||
errs = append(errs, e.Title)
|
||||
} else {
|
||||
errs = append(errs, fmt.Sprintf("%s\n\n%s", e.Title, e.Detail))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf(strings.Join(errs, "\n"))
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package tfe
|
||||
|
||||
// Access returns a pointer to the given team access type.
|
||||
func Access(v AccessType) *AccessType {
|
||||
return &v
|
||||
}
|
||||
|
||||
// RunsPermission returns a pointer to the given team runs permission type.
|
||||
func RunsPermission(v RunsPermissionType) *RunsPermissionType {
|
||||
return &v
|
||||
}
|
||||
|
||||
// VariablesPermission returns a pointer to the given team variables permission type.
|
||||
func VariablesPermission(v VariablesPermissionType) *VariablesPermissionType {
|
||||
return &v
|
||||
}
|
||||
|
||||
// StateVersionsPermission returns a pointer to the given team state versions permission type.
|
||||
func StateVersionsPermission(v StateVersionsPermissionType) *StateVersionsPermissionType {
|
||||
return &v
|
||||
}
|
||||
|
||||
// SentinelMocksPermission returns a pointer to the given team Sentinel mocks permission type.
|
||||
func SentinelMocksPermission(v SentinelMocksPermissionType) *SentinelMocksPermissionType {
|
||||
return &v
|
||||
}
|
||||
|
||||
// AuthPolicy returns a pointer to the given authentication poliy.
|
||||
func AuthPolicy(v AuthPolicyType) *AuthPolicyType {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Bool returns a pointer to the given bool
|
||||
func Bool(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Category returns a pointer to the given category type.
|
||||
func Category(v CategoryType) *CategoryType {
|
||||
return &v
|
||||
}
|
||||
|
||||
// EnforcementMode returns a pointer to the given enforcement level.
|
||||
func EnforcementMode(v EnforcementLevel) *EnforcementLevel {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Int returns a pointer to the given int.
|
||||
func Int(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Int64 returns a pointer to the given int64.
|
||||
func Int64(v int64) *int64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// NotificationDestination returns a pointer to the given notification configuration destination type
|
||||
func NotificationDestination(v NotificationDestinationType) *NotificationDestinationType {
|
||||
return &v
|
||||
}
|
||||
|
||||
// PlanExportType returns a pointer to the given plan export data type.
|
||||
func PlanExportType(v PlanExportDataType) *PlanExportDataType {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ServiceProvider returns a pointer to the given service provider type.
|
||||
func ServiceProvider(v ServiceProviderType) *ServiceProviderType {
|
||||
return &v
|
||||
}
|
||||
|
||||
// String returns a pointer to the given string.
|
||||
func String(v string) *string {
|
||||
return &v
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ Users = (*users)(nil)
|
||||
|
||||
// Users describes all the user related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/user.html
|
||||
type Users interface {
|
||||
// ReadCurrent reads the details of the currently authenticated user.
|
||||
ReadCurrent(ctx context.Context) (*User, error)
|
||||
|
||||
// Update attributes of the currently authenticated user.
|
||||
Update(ctx context.Context, options UserUpdateOptions) (*User, error)
|
||||
}
|
||||
|
||||
// users implements Users.
|
||||
type users struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// User represents a Terraform Enterprise user.
|
||||
type User struct {
|
||||
ID string `jsonapi:"primary,users"`
|
||||
AvatarURL string `jsonapi:"attr,avatar-url"`
|
||||
Email string `jsonapi:"attr,email"`
|
||||
IsServiceAccount bool `jsonapi:"attr,is-service-account"`
|
||||
TwoFactor *TwoFactor `jsonapi:"attr,two-factor"`
|
||||
UnconfirmedEmail string `jsonapi:"attr,unconfirmed-email"`
|
||||
Username string `jsonapi:"attr,username"`
|
||||
V2Only bool `jsonapi:"attr,v2-only"`
|
||||
|
||||
// Relations
|
||||
// AuthenticationTokens *AuthenticationTokens `jsonapi:"relation,authentication-tokens"`
|
||||
}
|
||||
|
||||
// TwoFactor represents the organization permissions.
|
||||
type TwoFactor struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
// ReadCurrent reads the details of the currently authenticated user.
|
||||
func (s *users) ReadCurrent(ctx context.Context) (*User, error) {
|
||||
req, err := s.client.newRequest("GET", "account/details", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &User{}
|
||||
err = s.client.do(ctx, req, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// UserUpdateOptions represents the options for updating a user.
|
||||
type UserUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,users"`
|
||||
|
||||
// New username.
|
||||
Username *string `jsonapi:"attr,username,omitempty"`
|
||||
|
||||
// New email address (must be consumed afterwards to take effect).
|
||||
Email *string `jsonapi:"attr,email,omitempty"`
|
||||
}
|
||||
|
||||
// Update attributes of the currently authenticated user.
|
||||
func (s *users) Update(ctx context.Context, options UserUpdateOptions) (*User, error) {
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
req, err := s.client.newRequest("PATCH", "account/update", &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &User{}
|
||||
err = s.client.do(ctx, req, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ UserTokens = (*userTokens)(nil)
|
||||
|
||||
// UserTokens describes all the user token related methods that the
|
||||
// Terraform Cloud/Enterprise API supports.
|
||||
//
|
||||
// TFE API docs:
|
||||
// https://www.terraform.io/docs/enterprise/api/user-tokens.html
|
||||
type UserTokens interface {
|
||||
// List all the tokens of the given user ID.
|
||||
List(ctx context.Context, userID string) (*UserTokenList, error)
|
||||
|
||||
// Generate a new user token
|
||||
Generate(ctx context.Context, userID string, options UserTokenGenerateOptions) (*UserToken, error)
|
||||
|
||||
// Read a user token by its ID.
|
||||
Read(ctx context.Context, tokenID string) (*UserToken, error)
|
||||
|
||||
// Delete a user token by its ID.
|
||||
Delete(ctx context.Context, tokenID string) error
|
||||
}
|
||||
|
||||
// userTokens implements UserTokens.
|
||||
type userTokens struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// UserTokenList is a list of tokens for the given user ID.
|
||||
type UserTokenList struct {
|
||||
*Pagination
|
||||
Items []*UserToken
|
||||
}
|
||||
|
||||
// UserToken represents a Terraform Enterprise user token.
|
||||
type UserToken struct {
|
||||
ID string `jsonapi:"primary,authentication-tokens"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
Description string `jsonapi:"attr,description"`
|
||||
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
|
||||
Token string `jsonapi:"attr,token"`
|
||||
}
|
||||
|
||||
// UserTokenGenerateOptions the options for creating a user token.
|
||||
type UserTokenGenerateOptions struct {
|
||||
// Description of the token
|
||||
Description string `jsonapi:"attr,description,omitempty"`
|
||||
}
|
||||
|
||||
// Generate a new user token
|
||||
func (s *userTokens) Generate(ctx context.Context, userID string, options UserTokenGenerateOptions) (*UserToken, error) {
|
||||
if !validStringID(&userID) {
|
||||
return nil, errors.New("invalid value for user ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("users/%s/authentication-tokens", url.QueryEscape(userID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ut := &UserToken{}
|
||||
err = s.client.do(ctx, req, ut)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ut, err
|
||||
}
|
||||
|
||||
// List shows existing user tokens
|
||||
func (s *userTokens) List(ctx context.Context, userID string) (*UserTokenList, error) {
|
||||
if !validStringID(&userID) {
|
||||
return nil, errors.New("invalid value for user ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("users/%s/authentication-tokens", url.QueryEscape(userID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tl := &UserTokenList{}
|
||||
err = s.client.do(ctx, req, tl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tl, err
|
||||
}
|
||||
|
||||
// Read a user token by its ID.
|
||||
func (s *userTokens) Read(ctx context.Context, tokenID string) (*UserToken, error) {
|
||||
if !validStringID(&tokenID) {
|
||||
return nil, errors.New("invalid value for token ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("authentication-tokens/%s", url.QueryEscape(tokenID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tt := &UserToken{}
|
||||
err = s.client.do(ctx, req, tt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tt, err
|
||||
}
|
||||
|
||||
// Delete a user token by its ID.
|
||||
func (s *userTokens) Delete(ctx context.Context, tokenID string) error {
|
||||
if !validStringID(&tokenID) {
|
||||
return errors.New("invalid value for token ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("authentication-tokens/%s", url.QueryEscape(tokenID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// A regular expression used to validate common string ID patterns.
|
||||
var reStringID = regexp.MustCompile(`^[a-zA-Z0-9\-\._]+$`)
|
||||
|
||||
// validString checks if the given input is present and non-empty.
|
||||
func validString(v *string) bool {
|
||||
return v != nil && *v != ""
|
||||
}
|
||||
|
||||
// validStringID checks if the given string pointer is non-nil and
|
||||
// contains a typical string identifier.
|
||||
func validStringID(v *string) bool {
|
||||
return v != nil && reStringID.MatchString(*v)
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ Variables = (*variables)(nil)
|
||||
|
||||
// Variables describes all the variable related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/variables.html
|
||||
type Variables interface {
|
||||
// List all the variables associated with the given workspace.
|
||||
List(ctx context.Context, workspaceID string, options VariableListOptions) (*VariableList, error)
|
||||
|
||||
// Create is used to create a new variable.
|
||||
Create(ctx context.Context, workspaceID string, options VariableCreateOptions) (*Variable, error)
|
||||
|
||||
// Read a variable by its ID.
|
||||
Read(ctx context.Context, workspaceID string, variableID string) (*Variable, error)
|
||||
|
||||
// Update values of an existing variable.
|
||||
Update(ctx context.Context, workspaceID string, variableID string, options VariableUpdateOptions) (*Variable, error)
|
||||
|
||||
// Delete a variable by its ID.
|
||||
Delete(ctx context.Context, workspaceID string, variableID string) error
|
||||
}
|
||||
|
||||
// variables implements Variables.
|
||||
type variables struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// CategoryType represents a category type.
|
||||
type CategoryType string
|
||||
|
||||
//List all available categories.
|
||||
const (
|
||||
CategoryEnv CategoryType = "env"
|
||||
CategoryPolicySet CategoryType = "policy-set"
|
||||
CategoryTerraform CategoryType = "terraform"
|
||||
)
|
||||
|
||||
// VariableList represents a list of variables.
|
||||
type VariableList struct {
|
||||
*Pagination
|
||||
Items []*Variable
|
||||
}
|
||||
|
||||
// Variable represents a Terraform Enterprise variable.
|
||||
type Variable struct {
|
||||
ID string `jsonapi:"primary,vars"`
|
||||
Key string `jsonapi:"attr,key"`
|
||||
Value string `jsonapi:"attr,value"`
|
||||
Description string `jsonapi:"attr,description"`
|
||||
Category CategoryType `jsonapi:"attr,category"`
|
||||
HCL bool `jsonapi:"attr,hcl"`
|
||||
Sensitive bool `jsonapi:"attr,sensitive"`
|
||||
|
||||
// Relations
|
||||
Workspace *Workspace `jsonapi:"relation,configurable"`
|
||||
}
|
||||
|
||||
// VariableListOptions represents the options for listing variables.
|
||||
type VariableListOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// List all the variables associated with the given workspace.
|
||||
func (s *variables) List(ctx context.Context, workspaceID string, options VariableListOptions) (*VariableList, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/vars", workspaceID)
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vl := &VariableList{}
|
||||
err = s.client.do(ctx, req, vl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vl, nil
|
||||
}
|
||||
|
||||
// VariableCreateOptions represents the options for creating a new variable.
|
||||
type VariableCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,vars"`
|
||||
|
||||
// The name of the variable.
|
||||
Key *string `jsonapi:"attr,key"`
|
||||
|
||||
// The value of the variable.
|
||||
Value *string `jsonapi:"attr,value,omitempty"`
|
||||
|
||||
// The description of the variable.
|
||||
Description *string `jsonapi:"attr,description,omitempty"`
|
||||
|
||||
// Whether this is a Terraform or environment variable.
|
||||
Category *CategoryType `jsonapi:"attr,category"`
|
||||
|
||||
// Whether to evaluate the value of the variable as a string of HCL code.
|
||||
HCL *bool `jsonapi:"attr,hcl,omitempty"`
|
||||
|
||||
// Whether the value is sensitive.
|
||||
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
|
||||
}
|
||||
|
||||
func (o VariableCreateOptions) valid() error {
|
||||
if !validString(o.Key) {
|
||||
return errors.New("key is required")
|
||||
}
|
||||
if o.Category == nil {
|
||||
return errors.New("category is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create is used to create a new variable.
|
||||
func (s *variables) Create(ctx context.Context, workspaceID string, options VariableCreateOptions) (*Variable, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/vars", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := &Variable{}
|
||||
err = s.client.do(ctx, req, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Read a variable by its ID.
|
||||
func (s *variables) Read(ctx context.Context, workspaceID string, variableID string) (*Variable, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
if !validStringID(&variableID) {
|
||||
return nil, errors.New("invalid value for variable ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/vars/%s", url.QueryEscape(workspaceID), url.QueryEscape(variableID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := &Variable{}
|
||||
err = s.client.do(ctx, req, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
// VariableUpdateOptions represents the options for updating a variable.
|
||||
type VariableUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,vars"`
|
||||
|
||||
// The name of the variable.
|
||||
Key *string `jsonapi:"attr,key,omitempty"`
|
||||
|
||||
// The value of the variable.
|
||||
Value *string `jsonapi:"attr,value,omitempty"`
|
||||
|
||||
// The description of the variable.
|
||||
Description *string `jsonapi:"attr,description,omitempty"`
|
||||
|
||||
// Whether to evaluate the value of the variable as a string of HCL code.
|
||||
HCL *bool `jsonapi:"attr,hcl,omitempty"`
|
||||
|
||||
// Whether the value is sensitive.
|
||||
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
|
||||
}
|
||||
|
||||
// Update values of an existing variable.
|
||||
func (s *variables) Update(ctx context.Context, workspaceID string, variableID string, options VariableUpdateOptions) (*Variable, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
if !validStringID(&variableID) {
|
||||
return nil, errors.New("invalid value for variable ID")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = variableID
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/vars/%s", url.QueryEscape(workspaceID), url.QueryEscape(variableID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := &Variable{}
|
||||
err = s.client.do(ctx, req, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Delete a variable by its ID.
|
||||
func (s *variables) Delete(ctx context.Context, workspaceID string, variableID string) error {
|
||||
if !validStringID(&workspaceID) {
|
||||
return errors.New("invalid value for workspace ID")
|
||||
}
|
||||
if !validStringID(&variableID) {
|
||||
return errors.New("invalid value for variable ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/vars/%s", url.QueryEscape(workspaceID), url.QueryEscape(variableID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,723 @@
|
|||
package tfe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compile-time proof of interface implementation.
|
||||
var _ Workspaces = (*workspaces)(nil)
|
||||
|
||||
// Workspaces describes all the workspace related methods that the Terraform
|
||||
// Enterprise API supports.
|
||||
//
|
||||
// TFE API docs: https://www.terraform.io/docs/enterprise/api/workspaces.html
|
||||
type Workspaces interface {
|
||||
// List all the workspaces within an organization.
|
||||
List(ctx context.Context, organization string, options WorkspaceListOptions) (*WorkspaceList, error)
|
||||
|
||||
// Create is used to create a new workspace.
|
||||
Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error)
|
||||
|
||||
// Read a workspace by its name.
|
||||
Read(ctx context.Context, organization string, workspace string) (*Workspace, error)
|
||||
|
||||
// ReadByID reads a workspace by its ID.
|
||||
ReadByID(ctx context.Context, workspaceID string) (*Workspace, error)
|
||||
|
||||
// Update settings of an existing workspace.
|
||||
Update(ctx context.Context, organization string, workspace string, options WorkspaceUpdateOptions) (*Workspace, error)
|
||||
|
||||
// UpdateByID updates the settings of an existing workspace.
|
||||
UpdateByID(ctx context.Context, workspaceID string, options WorkspaceUpdateOptions) (*Workspace, error)
|
||||
|
||||
// Delete a workspace by its name.
|
||||
Delete(ctx context.Context, organization string, workspace string) error
|
||||
|
||||
// DeleteByID deletes a workspace by its ID.
|
||||
DeleteByID(ctx context.Context, workspaceID string) error
|
||||
|
||||
// RemoveVCSConnection from a workspace.
|
||||
RemoveVCSConnection(ctx context.Context, organization, workspace string) (*Workspace, error)
|
||||
|
||||
// RemoveVCSConnectionByID removes a VCS connection from a workspace.
|
||||
RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*Workspace, error)
|
||||
|
||||
// Lock a workspace by its ID.
|
||||
Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error)
|
||||
|
||||
// Unlock a workspace by its ID.
|
||||
Unlock(ctx context.Context, workspaceID string) (*Workspace, error)
|
||||
|
||||
// ForceUnlock a workspace by its ID.
|
||||
ForceUnlock(ctx context.Context, workspaceID string) (*Workspace, error)
|
||||
|
||||
// AssignSSHKey to a workspace.
|
||||
AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error)
|
||||
|
||||
// UnassignSSHKey from a workspace.
|
||||
UnassignSSHKey(ctx context.Context, workspaceID string) (*Workspace, error)
|
||||
}
|
||||
|
||||
// workspaces implements Workspaces.
|
||||
type workspaces struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// WorkspaceList represents a list of workspaces.
|
||||
type WorkspaceList struct {
|
||||
*Pagination
|
||||
Items []*Workspace
|
||||
}
|
||||
|
||||
// Workspace represents a Terraform Enterprise workspace.
|
||||
type Workspace struct {
|
||||
ID string `jsonapi:"primary,workspaces"`
|
||||
Actions *WorkspaceActions `jsonapi:"attr,actions"`
|
||||
AgentPoolID string `jsonapi:"attr,agent-pool-id"`
|
||||
AllowDestroyPlan bool `jsonapi:"attr,allow-destroy-plan"`
|
||||
AutoApply bool `jsonapi:"attr,auto-apply"`
|
||||
CanQueueDestroyPlan bool `jsonapi:"attr,can-queue-destroy-plan"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
Environment string `jsonapi:"attr,environment"`
|
||||
ExecutionMode string `jsonapi:"attr,execution-mode"`
|
||||
FileTriggersEnabled bool `jsonapi:"attr,file-triggers-enabled"`
|
||||
Locked bool `jsonapi:"attr,locked"`
|
||||
MigrationEnvironment string `jsonapi:"attr,migration-environment"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
Operations bool `jsonapi:"attr,operations"`
|
||||
Permissions *WorkspacePermissions `jsonapi:"attr,permissions"`
|
||||
QueueAllRuns bool `jsonapi:"attr,queue-all-runs"`
|
||||
SpeculativeEnabled bool `jsonapi:"attr,speculative-enabled"`
|
||||
TerraformVersion string `jsonapi:"attr,terraform-version"`
|
||||
TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes"`
|
||||
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
|
||||
WorkingDirectory string `jsonapi:"attr,working-directory"`
|
||||
|
||||
// Relations
|
||||
AgentPool *AgentPool `jsonapi:"relation,agent-pool"`
|
||||
CurrentRun *Run `jsonapi:"relation,current-run"`
|
||||
Organization *Organization `jsonapi:"relation,organization"`
|
||||
SSHKey *SSHKey `jsonapi:"relation,ssh-key"`
|
||||
}
|
||||
|
||||
// VCSRepo contains the configuration of a VCS integration.
|
||||
type VCSRepo struct {
|
||||
Branch string `json:"branch"`
|
||||
DisplayIdentifier string `json:"display-identifier"`
|
||||
Identifier string `json:"identifier"`
|
||||
IngressSubmodules bool `json:"ingress-submodules"`
|
||||
OAuthTokenID string `json:"oauth-token-id"`
|
||||
}
|
||||
|
||||
// WorkspaceActions represents the workspace actions.
|
||||
type WorkspaceActions struct {
|
||||
IsDestroyable bool `json:"is-destroyable"`
|
||||
}
|
||||
|
||||
// WorkspacePermissions represents the workspace permissions.
|
||||
type WorkspacePermissions struct {
|
||||
CanDestroy bool `json:"can-destroy"`
|
||||
CanForceUnlock bool `json:"can-force-unlock"`
|
||||
CanLock bool `json:"can-lock"`
|
||||
CanQueueApply bool `json:"can-queue-apply"`
|
||||
CanQueueDestroy bool `json:"can-queue-destroy"`
|
||||
CanQueueRun bool `json:"can-queue-run"`
|
||||
CanReadSettings bool `json:"can-read-settings"`
|
||||
CanUnlock bool `json:"can-unlock"`
|
||||
CanUpdate bool `json:"can-update"`
|
||||
CanUpdateVariable bool `json:"can-update-variable"`
|
||||
}
|
||||
|
||||
// WorkspaceListOptions represents the options for listing workspaces.
|
||||
type WorkspaceListOptions struct {
|
||||
ListOptions
|
||||
|
||||
// A search string (partial workspace name) used to filter the results.
|
||||
Search *string `url:"search[name],omitempty"`
|
||||
|
||||
// A list of relations to include. See available resources https://www.terraform.io/docs/cloud/api/workspaces.html#available-related-resources
|
||||
Include *string `url:"include"`
|
||||
}
|
||||
|
||||
// List all the workspaces within an organization.
|
||||
func (s *workspaces) List(ctx context.Context, organization string, options WorkspaceListOptions) (*WorkspaceList, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/workspaces", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("GET", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wl := &WorkspaceList{}
|
||||
err = s.client.do(ctx, req, wl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wl, nil
|
||||
}
|
||||
|
||||
// WorkspaceCreateOptions represents the options for creating a new workspace.
|
||||
type WorkspaceCreateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,workspaces"`
|
||||
|
||||
// Required when execution-mode is set to agent. The ID of the agent pool
|
||||
// belonging to the workspace's organization. This value must not be specified
|
||||
// if execution-mode is set to remote or local or if operations is set to true.
|
||||
AgentPoolID *string `jsonapi:"attr,agent-pool-id,omitempty"`
|
||||
|
||||
// Whether destroy plans can be queued on the workspace.
|
||||
AllowDestroyPlan *bool `jsonapi:"attr,allow-destroy-plan,omitempty"`
|
||||
|
||||
// Whether to automatically apply changes when a Terraform plan is successful.
|
||||
AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"`
|
||||
|
||||
// Which execution mode to use. Valid values are remote, local, and agent.
|
||||
// When set to local, the workspace will be used for state storage only.
|
||||
// This value must not be specified if operations is specified.
|
||||
// 'agent' execution mode is not available in Terraform Enterprise.
|
||||
ExecutionMode *string `jsonapi:"attr,execution-mode,omitempty"`
|
||||
|
||||
// Whether to filter runs based on the changed files in a VCS push. If
|
||||
// enabled, the working directory and trigger prefixes describe a set of
|
||||
// paths which must contain changes for a VCS push to trigger a run. If
|
||||
// disabled, any push will trigger a run.
|
||||
FileTriggersEnabled *bool `jsonapi:"attr,file-triggers-enabled,omitempty"`
|
||||
|
||||
// The legacy TFE environment to use as the source of the migration, in the
|
||||
// form organization/environment. Omit this unless you are migrating a legacy
|
||||
// environment.
|
||||
MigrationEnvironment *string `jsonapi:"attr,migration-environment,omitempty"`
|
||||
|
||||
// The name of the workspace, which can only include letters, numbers, -,
|
||||
// and _. This will be used as an identifier and must be unique in the
|
||||
// organization.
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
|
||||
// DEPRECATED. Whether the workspace will use remote or local execution mode.
|
||||
// Use ExecutionMode instead.
|
||||
Operations *bool `jsonapi:"attr,operations,omitempty"`
|
||||
|
||||
// Whether to queue all runs. Unless this is set to true, runs triggered by
|
||||
// a webhook will not be queued until at least one run is manually queued.
|
||||
QueueAllRuns *bool `jsonapi:"attr,queue-all-runs,omitempty"`
|
||||
|
||||
// Whether this workspace allows speculative plans. Setting this to false
|
||||
// prevents Terraform Cloud or the Terraform Enterprise instance from
|
||||
// running plans on pull requests, which can improve security if the VCS
|
||||
// repository is public or includes untrusted contributors.
|
||||
SpeculativeEnabled *bool `jsonapi:"attr,speculative-enabled,omitempty"`
|
||||
|
||||
// The version of Terraform to use for this workspace. Upon creating a
|
||||
// workspace, the latest version is selected unless otherwise specified.
|
||||
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
|
||||
|
||||
// List of repository-root-relative paths which list all locations to be
|
||||
// tracked for changes. See FileTriggersEnabled above for more details.
|
||||
TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes,omitempty"`
|
||||
|
||||
// Settings for the workspace's VCS repository. If omitted, the workspace is
|
||||
// created without a VCS repo. If included, you must specify at least the
|
||||
// oauth-token-id and identifier keys below.
|
||||
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
|
||||
|
||||
// A relative path that Terraform will execute within. This defaults to the
|
||||
// root of your repository and is typically set to a subdirectory matching the
|
||||
// environment when multiple environments exist within the same repository.
|
||||
WorkingDirectory *string `jsonapi:"attr,working-directory,omitempty"`
|
||||
}
|
||||
|
||||
// TODO: move this struct out. VCSRepoOptions is used by workspaces, policy sets, and registry modules
|
||||
// VCSRepoOptions represents the configuration options of a VCS integration.
|
||||
type VCSRepoOptions struct {
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
Identifier *string `json:"identifier,omitempty"`
|
||||
IngressSubmodules *bool `json:"ingress-submodules,omitempty"`
|
||||
OAuthTokenID *string `json:"oauth-token-id,omitempty"`
|
||||
}
|
||||
|
||||
func (o WorkspaceCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(o.Name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
if o.Operations != nil && o.ExecutionMode != nil {
|
||||
return errors.New("operations is deprecated and cannot be specified when execution mode is used")
|
||||
}
|
||||
if o.AgentPoolID != nil && (o.ExecutionMode == nil || *o.ExecutionMode != "agent") {
|
||||
return errors.New("specifying an agent pool ID requires 'agent' execution mode")
|
||||
}
|
||||
if o.AgentPoolID == nil && (o.ExecutionMode != nil && *o.ExecutionMode == "agent") {
|
||||
return errors.New("'agent' execution mode requires an agent pool ID to be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create is used to create a new workspace.
|
||||
func (s *workspaces) Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("organizations/%s/workspaces", url.QueryEscape(organization))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Read a workspace by its name.
|
||||
func (s *workspaces) Read(ctx context.Context, organization, workspace string) (*Workspace, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if !validStringID(&workspace) {
|
||||
return nil, errors.New("invalid value for workspace")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"organizations/%s/workspaces/%s",
|
||||
url.QueryEscape(organization),
|
||||
url.QueryEscape(workspace),
|
||||
)
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// ReadByID reads a workspace by its ID.
|
||||
func (s *workspaces) ReadByID(ctx context.Context, workspaceID string) (*Workspace, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// WorkspaceUpdateOptions represents the options for updating a workspace.
|
||||
type WorkspaceUpdateOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,workspaces"`
|
||||
|
||||
// Required when execution-mode is set to agent. The ID of the agent pool
|
||||
// belonging to the workspace's organization. This value must not be specified
|
||||
// if execution-mode is set to remote or local or if operations is set to true.
|
||||
AgentPoolID *string `jsonapi:"attr,agent-pool-id,omitempty"`
|
||||
|
||||
// Whether destroy plans can be queued on the workspace.
|
||||
AllowDestroyPlan *bool `jsonapi:"attr,allow-destroy-plan,omitempty"`
|
||||
|
||||
// Whether to automatically apply changes when a Terraform plan is successful.
|
||||
AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"`
|
||||
|
||||
// A new name for the workspace, which can only include letters, numbers, -,
|
||||
// and _. This will be used as an identifier and must be unique in the
|
||||
// organization. Warning: Changing a workspace's name changes its URL in the
|
||||
// API and UI.
|
||||
Name *string `jsonapi:"attr,name,omitempty"`
|
||||
|
||||
// Which execution mode to use. Valid values are remote, local, and agent.
|
||||
// When set to local, the workspace will be used for state storage only.
|
||||
// This value must not be specified if operations is specified.
|
||||
// 'agent' execution mode is not available in Terraform Enterprise.
|
||||
ExecutionMode *string `jsonapi:"attr,execution-mode,omitempty"`
|
||||
|
||||
// Whether to filter runs based on the changed files in a VCS push. If
|
||||
// enabled, the working directory and trigger prefixes describe a set of
|
||||
// paths which must contain changes for a VCS push to trigger a run. If
|
||||
// disabled, any push will trigger a run.
|
||||
FileTriggersEnabled *bool `jsonapi:"attr,file-triggers-enabled,omitempty"`
|
||||
|
||||
// DEPRECATED. Whether the workspace will use remote or local execution mode.
|
||||
// Use ExecutionMode instead.
|
||||
Operations *bool `jsonapi:"attr,operations,omitempty"`
|
||||
|
||||
// Whether to queue all runs. Unless this is set to true, runs triggered by
|
||||
// a webhook will not be queued until at least one run is manually queued.
|
||||
QueueAllRuns *bool `jsonapi:"attr,queue-all-runs,omitempty"`
|
||||
|
||||
// Whether this workspace allows speculative plans. Setting this to false
|
||||
// prevents Terraform Cloud or the Terraform Enterprise instance from
|
||||
// running plans on pull requests, which can improve security if the VCS
|
||||
// repository is public or includes untrusted contributors.
|
||||
SpeculativeEnabled *bool `jsonapi:"attr,speculative-enabled,omitempty"`
|
||||
|
||||
// The version of Terraform to use for this workspace.
|
||||
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
|
||||
|
||||
// List of repository-root-relative paths which list all locations to be
|
||||
// tracked for changes. See FileTriggersEnabled above for more details.
|
||||
TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes,omitempty"`
|
||||
|
||||
// To delete a workspace's existing VCS repo, specify null instead of an
|
||||
// object. To modify a workspace's existing VCS repo, include whichever of
|
||||
// the keys below you wish to modify. To add a new VCS repo to a workspace
|
||||
// that didn't previously have one, include at least the oauth-token-id and
|
||||
// identifier keys.
|
||||
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
|
||||
|
||||
// A relative path that Terraform will execute within. This defaults to the
|
||||
// root of your repository and is typically set to a subdirectory matching
|
||||
// the environment when multiple environments exist within the same
|
||||
// repository.
|
||||
WorkingDirectory *string `jsonapi:"attr,working-directory,omitempty"`
|
||||
}
|
||||
|
||||
func (o WorkspaceUpdateOptions) valid() error {
|
||||
if o.Name != nil && !validStringID(o.Name) {
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
if o.Operations != nil && o.ExecutionMode != nil {
|
||||
return errors.New("operations is deprecated and cannot be specified when execution mode is used")
|
||||
}
|
||||
if o.AgentPoolID == nil && (o.ExecutionMode != nil && *o.ExecutionMode == "agent") {
|
||||
return errors.New("'agent' execution mode requires an agent pool ID to be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update settings of an existing workspace.
|
||||
func (s *workspaces) Update(ctx context.Context, organization, workspace string, options WorkspaceUpdateOptions) (*Workspace, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if !validStringID(&workspace) {
|
||||
return nil, errors.New("invalid value for workspace")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"organizations/%s/workspaces/%s",
|
||||
url.QueryEscape(organization),
|
||||
url.QueryEscape(workspace),
|
||||
)
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// UpdateByID updates the settings of an existing workspace.
|
||||
func (s *workspaces) UpdateByID(ctx context.Context, workspaceID string, options WorkspaceUpdateOptions) (*Workspace, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Delete a workspace by its name.
|
||||
func (s *workspaces) Delete(ctx context.Context, organization, workspace string) error {
|
||||
if !validStringID(&organization) {
|
||||
return errors.New("invalid value for organization")
|
||||
}
|
||||
if !validStringID(&workspace) {
|
||||
return errors.New("invalid value for workspace")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"organizations/%s/workspaces/%s",
|
||||
url.QueryEscape(organization),
|
||||
url.QueryEscape(workspace),
|
||||
)
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DeleteByID deletes a workspace by its ID.
|
||||
func (s *workspaces) DeleteByID(ctx context.Context, workspaceID string) error {
|
||||
if !validStringID(&workspaceID) {
|
||||
return errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// workspaceRemoveVCSConnectionOptions
|
||||
type workspaceRemoveVCSConnectionOptions struct {
|
||||
ID string `jsonapi:"primary,workspaces"`
|
||||
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo"`
|
||||
}
|
||||
|
||||
// RemoveVCSConnection from a workspace.
|
||||
func (s *workspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*Workspace, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if !validStringID(&workspace) {
|
||||
return nil, errors.New("invalid value for workspace")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf(
|
||||
"organizations/%s/workspaces/%s",
|
||||
url.QueryEscape(organization),
|
||||
url.QueryEscape(workspace),
|
||||
)
|
||||
|
||||
req, err := s.client.newRequest("PATCH", u, &workspaceRemoveVCSConnectionOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// RemoveVCSConnectionByID removes a VCS connection from a workspace.
|
||||
func (s *workspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*Workspace, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID))
|
||||
|
||||
req, err := s.client.newRequest("PATCH", u, &workspaceRemoveVCSConnectionOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// WorkspaceLockOptions represents the options for locking a workspace.
|
||||
type WorkspaceLockOptions struct {
|
||||
// Specifies the reason for locking the workspace.
|
||||
Reason *string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// Lock a workspace by its ID.
|
||||
func (s *workspaces) Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/actions/lock", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("POST", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Unlock a workspace by its ID.
|
||||
func (s *workspaces) Unlock(ctx context.Context, workspaceID string) (*Workspace, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/actions/unlock", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// ForceUnlock a workspace by its ID.
|
||||
func (s *workspaces) ForceUnlock(ctx context.Context, workspaceID string) (*Workspace, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/actions/force-unlock", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// WorkspaceAssignSSHKeyOptions represents the options to assign an SSH key to
|
||||
// a workspace.
|
||||
type WorkspaceAssignSSHKeyOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,workspaces"`
|
||||
|
||||
// The SSH key ID to assign.
|
||||
SSHKeyID *string `jsonapi:"attr,id"`
|
||||
}
|
||||
|
||||
func (o WorkspaceAssignSSHKeyOptions) valid() error {
|
||||
if !validString(o.SSHKeyID) {
|
||||
return errors.New("SSH key ID is required")
|
||||
}
|
||||
if !validStringID(o.SSHKeyID) {
|
||||
return errors.New("invalid value for SSH key ID")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssignSSHKey to a workspace.
|
||||
func (s *workspaces) AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we don't send a user provided ID.
|
||||
options.ID = ""
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/relationships/ssh-key", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("PATCH", u, &options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// workspaceUnassignSSHKeyOptions represents the options to unassign an SSH key
|
||||
// to a workspace.
|
||||
type workspaceUnassignSSHKeyOptions struct {
|
||||
// For internal use only!
|
||||
ID string `jsonapi:"primary,workspaces"`
|
||||
|
||||
// Must be nil to unset the currently assigned SSH key.
|
||||
SSHKeyID *string `jsonapi:"attr,id"`
|
||||
}
|
||||
|
||||
// UnassignSSHKey from a workspace.
|
||||
func (s *workspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*Workspace, error) {
|
||||
if !validStringID(&workspaceID) {
|
||||
return nil, errors.New("invalid value for workspace ID")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("workspaces/%s/relationships/ssh-key", url.QueryEscape(workspaceID))
|
||||
req, err := s.client.newRequest("PATCH", u, &workspaceUnassignSSHKeyOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Workspace{}
|
||||
err = s.client.do(ctx, req, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
6
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/.gitignore
generated
vendored
Normal file
6
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
vault/plugins
|
||||
dist/
|
||||
*.test
|
||||
pkg
|
||||
bin
|
||||
tmp
|
32
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/.goreleaser.yml
generated
vendored
Normal file
32
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/.goreleaser.yml
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
builds:
|
||||
- main: ./cmd/vault-plugin-secrets-terraform
|
||||
id: "vault-plugin-secrets-terraform"
|
||||
binary: vault-plugin-secrets-terraform
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
release:
|
||||
prerelease: true
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
362
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/LICENSE
generated
vendored
Normal file
362
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,362 @@
|
|||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. "Contributor"
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms of
|
||||
a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
|
||||
means a work that combines Covered Software with other material, in a
|
||||
separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether
|
||||
at the time of the initial grant or subsequently, any and all of the
|
||||
rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the License,
|
||||
by the making, using, selling, offering for sale, having made, import,
|
||||
or transfer of either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, "control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights to
|
||||
grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter the
|
||||
recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
||||
limitations of liability) contained within the Source Code Form of the
|
||||
Covered Software, except that You may alter any license notices to the
|
||||
extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the
|
||||
limitations and the code they affect. Such description must be placed in a
|
||||
text file included with all distributions of the Covered Software under
|
||||
this License. Except to the extent prohibited by statute or regulation,
|
||||
such description must be sufficiently detailed for a recipient of ordinary
|
||||
skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
||||
basis, if such Contributor fails to notify You of the non-compliance by
|
||||
some reasonable means prior to 60 days after You have come back into
|
||||
compliance. Moreover, Your grants from a particular Contributor are
|
||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
||||
non-compliance by some reasonable means, this is the first time You have
|
||||
received notice of non-compliance with this License from such
|
||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||
of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an "as is" basis,
|
||||
without warranty of any kind, either expressed, implied, or statutory,
|
||||
including, without limitation, warranties that the Covered Software is free
|
||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
The entire risk as to the quality and performance of the Covered Software
|
||||
is with You. Should any Covered Software prove defective in any respect,
|
||||
You (not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
||||
part of this License. No use of any Covered Software is authorized under
|
||||
this License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party's negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts
|
||||
of a jurisdiction where the defendant maintains its principal place of
|
||||
business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
||||
in this Section shall prevent a party's ability to bring cross-claims or
|
||||
counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides that
|
||||
the language of a contract shall be construed against the drafter shall not
|
||||
be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this License must be
|
||||
attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to look for such a
|
||||
notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible
|
||||
With Secondary Licenses", as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
61
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/Makefile
generated
vendored
Normal file
61
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/Makefile
generated
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
GO_CMD?=go
|
||||
CGO_ENABLED?=0
|
||||
TOOL?=vault-plugin-secrets-terraform
|
||||
TEST?=$$($(GO_CMD) list ./... | grep -v /vendor/ | grep -v /integ)
|
||||
EXTERNAL_TOOLS=\
|
||||
github.com/mitchellh/gox
|
||||
BUILD_TAGS?=${TOOL}
|
||||
GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor)
|
||||
|
||||
# bin generates the releaseable binaries for this plugin
|
||||
bin: generate
|
||||
@CGO_ENABLED=0 BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/build.sh'"
|
||||
|
||||
default: dev
|
||||
|
||||
# dev creates binaries for testing Vault locally. These are put
|
||||
# into ./bin/ as well as $GOPATH/bin, except for quickdev which
|
||||
# is only put into /bin/
|
||||
quickdev: generate
|
||||
@CGO_ENABLED=0 go build -i -tags='$(BUILD_TAGS)' -o bin/${TOOL}
|
||||
|
||||
dev: generate
|
||||
@CGO_ENABLED=0 BUILD_TAGS='$(BUILD_TAGS)' VAULT_DEV_BUILD=1 sh -c "'$(CURDIR)/scripts/build.sh'"
|
||||
|
||||
testcompile: generate
|
||||
@for pkg in $(TEST) ; do \
|
||||
go test -v -c -tags='$(BUILD_TAGS)' $$pkg -parallel=4 ; \
|
||||
done
|
||||
|
||||
# test runs the unit tests and vets the code
|
||||
test: generate
|
||||
@if [ "$(TEST)" = "./..." ]; then \
|
||||
echo "ERROR: Set TEST to a specific package"; \
|
||||
exit 1; \
|
||||
fi
|
||||
CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC= go test -v -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -count=1 -timeout=10m -parallel=4
|
||||
|
||||
# testacc runs acceptance tests
|
||||
testacc:
|
||||
@if [ "$(TEST)" = "./..." ]; then \
|
||||
echo "ERROR: Set TEST to a specific package"; \
|
||||
exit 1; \
|
||||
fi
|
||||
CGO_ENABLED=0 VAULT_ACC=1 VAULT_TOKEN= $(GO_CMD) test -tags='$(BUILD_TAGS)' $(TEST) -v $(TESTARGS) -timeout=10m
|
||||
|
||||
# generate runs `go generate` to build the dynamically generated
|
||||
# source files.
|
||||
generate:
|
||||
@go generate $(go list ./... | grep -v /vendor/)
|
||||
|
||||
# bootstrap the build by downloading additional tools
|
||||
bootstrap:
|
||||
@for tool in $(EXTERNAL_TOOLS) ; do \
|
||||
echo "Installing/Updating $$tool" ; \
|
||||
go get -u $$tool; \
|
||||
done
|
||||
|
||||
fmt:
|
||||
gofmt -w $(GOFMT_FILES)
|
||||
|
||||
.PHONY: bin default generate test bootstrap fmt deps
|
155
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/README.md
generated
vendored
Normal file
155
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/README.md
generated
vendored
Normal file
|
@ -0,0 +1,155 @@
|
|||
# Vault Plugin: Terraform Cloud Secrets Backend [![HashiCorp](https://circleci.com/gh/hashicorp/vault-plugin-secrets-terraform.svg?style=svg)](https://circleci.com/gh/hashicorp/vault-plugin-secrets-terraform)
|
||||
|
||||
This is a standalone backend plugin for use with [Hashicorp
|
||||
Vault](https://www.github.com/hashicorp/vault). This plugin generates revocable,
|
||||
time-limited API tokens for [Terraform Cloud](https://www.terraform.io/cloud) users, as well as manages single API
|
||||
tokens for Terraform teams and Organizations. Please see Terraform Cloud's
|
||||
documentation on [API
|
||||
Tokens](https://www.terraform.io/docs/cloud/users-teams-organizations/api-tokens.html)
|
||||
for more information on the types of API tokens offered by the Terraform Cloud
|
||||
API.
|
||||
|
||||
**Please note**: We take Vault's security and our users' trust very seriously.
|
||||
If you believe you have found a security issue in Vault, _please responsibly
|
||||
disclose_ by contacting us at
|
||||
[security@hashicorp.com](mailto:security@hashicorp.com).
|
||||
|
||||
## Quick Links
|
||||
- [Vault Website](https://www.vaultproject.io)
|
||||
- [Terraform Cloud](https://www.terraform.io/cloud)
|
||||
- [Terraform Cloud Secrets
|
||||
Docs](https://www.vaultproject.io/docs/secrets/terraform/index.html)
|
||||
- [Terraform Cloud API token
|
||||
documentation](https://www.terraform.io/docs/cloud/users-teams-organizations/api-tokens.html)
|
||||
- [Vault Github Project](https://www.github.com/hashicorp/vault)
|
||||
|
||||
## Getting Started
|
||||
|
||||
This is a [Vault
|
||||
plugin](https://www.vaultproject.io/docs/internals/plugins.html) and is meant to
|
||||
work with Vault. This guide assumes you have already installed Vault and have a
|
||||
basic understanding of how Vault works.
|
||||
|
||||
Otherwise, first read this guide on how to [get started with
|
||||
Vault](https://www.vaultproject.io/intro/getting-started/install.html).
|
||||
|
||||
To learn specifically about how plugins work, see documentation on [Vault
|
||||
plugins](https://www.vaultproject.io/docs/internals/plugins.html).
|
||||
|
||||
## Usage
|
||||
|
||||
Please see [documentation for the
|
||||
plugin](https://www.vaultproject.io/docs/secrets/terraform/index.html) on the
|
||||
Vault website.
|
||||
|
||||
This plugin is currently built into Vault and by default is accessed at
|
||||
`terraform`. To enable this in a running Vault server:
|
||||
|
||||
```sh
|
||||
$ vault secrets enable terraform Success!
|
||||
Enabled the terraform secrets engine at: terraform/
|
||||
```
|
||||
|
||||
|
||||
## Developing
|
||||
|
||||
If you wish to work on this plugin, you'll first need
|
||||
[Go](https://www.golang.org) installed on your machine
|
||||
(version 1.15.3+ is *required*).
|
||||
|
||||
For local dev first make sure Go is properly installed, including
|
||||
setting up a [GOPATH](https://golang.org/doc/code.html#GOPATH).
|
||||
Next, clone this repository into
|
||||
`$GOPATH/src/github.com/hashicorp/vault-plugin-secrets-terraform`.
|
||||
You can then download any required build tools by bootstrapping your
|
||||
environment:
|
||||
|
||||
```sh
|
||||
$ make bootstrap
|
||||
```
|
||||
|
||||
To compile a development version of this plugin, run `make` or `make dev`.
|
||||
This will put the plugin binary in the `bin` and `$GOPATH/bin` folders. `dev`
|
||||
mode will only generate the binary for your platform and is faster:
|
||||
|
||||
```sh
|
||||
$ make
|
||||
$ make dev
|
||||
```
|
||||
|
||||
Put the plugin binary into a location of your choice. This directory will be
|
||||
specified as the
|
||||
[`plugin_directory`](https://www.vaultproject.io/docs/configuration/index.html#plugin_directory)
|
||||
in the Vault config used to start the server.
|
||||
|
||||
```json
|
||||
...
|
||||
plugin_directory = "path/to/plugin/directory"
|
||||
...
|
||||
```
|
||||
|
||||
Start a Vault server with this config file:
|
||||
```sh
|
||||
$ vault server -config=path/to/config.json ...
|
||||
...
|
||||
```
|
||||
|
||||
Once the server is started, register the plugin in the Vault server's [plugin
|
||||
catalog](https://www.vaultproject.io/docs/internals/plugins.html#plugin-catalog):
|
||||
|
||||
```sh
|
||||
$ vault write sys/plugins/catalog/terraform \
|
||||
sha256=<expected SHA256 Hex value of the plugin binary> \
|
||||
command="vault-plugin-secrets-terraform"
|
||||
...
|
||||
Success! Data written to: sys/plugins/catalog/terraform
|
||||
```
|
||||
|
||||
Note you should generate a new sha256 checksum if you have made changes
|
||||
to the plugin. Example using openssl:
|
||||
|
||||
```sh
|
||||
openssl dgst -sha256 $GOPATH/vault-plugin-secrets-terraform
|
||||
...
|
||||
SHA256(.../go/bin/vault-plugin-secrets-terraform)= 896c13c0f5305daed381952a128322e02bc28a57d0c862a78cbc2ea66e8c6fa1
|
||||
```
|
||||
|
||||
Enable the auth plugin backend using the secrets enable plugin command:
|
||||
|
||||
```sh
|
||||
$ vault secrets enable -plugin-name='terraform' plugin
|
||||
...
|
||||
|
||||
Successfully enabled 'plugin' at 'terraform'!
|
||||
```
|
||||
|
||||
#### Tests
|
||||
|
||||
If you are developing this plugin and want to verify it is still
|
||||
functioning (and you haven't broken anything else), we recommend
|
||||
running the tests.
|
||||
|
||||
To run the tests, invoke `make test`:
|
||||
|
||||
```sh
|
||||
$ make test
|
||||
```
|
||||
|
||||
You can also specify a `TESTARGS` variable to filter tests like so:
|
||||
|
||||
```sh
|
||||
$ make test TESTARGS='--run=TestTokenRole'
|
||||
```
|
||||
|
||||
The tests assume environment variables are set to use in order to perform the
|
||||
integration tests. Be sure to use appropriate values and be aware that real
|
||||
tokens will be created and destroyed in the course of running the tests.
|
||||
|
||||
Environment variables:
|
||||
|
||||
- `TEST_TF_TOKEN`: (required) API token used to configure the engine and make API calls
|
||||
- `TEST_TF_ADDRESS`: (optional) HTTP API address if using Terraform Enterprise.
|
||||
Defaults to `https://app.terraform.io` for Terraform Cloud.
|
||||
- `TEST_TF_ORGANIZATION`: The test organization to manage.
|
||||
- `TEST_TF_TEAM_ID`: The *team ID* for the test Team to manage.
|
||||
- `TEST_TF_USER_ID`: The *user ID* for the user to create dynamic tokens for.
|
109
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/backend.go
generated
vendored
Normal file
109
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/backend.go
generated
vendored
Normal file
|
@ -0,0 +1,109 @@
|
|||
package tfc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
// Factory returns a new backend as logical.Backend
|
||||
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
b := backend()
|
||||
if err := b.Setup(ctx, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
type tfBackend struct {
|
||||
*framework.Backend
|
||||
lock sync.RWMutex
|
||||
client *client
|
||||
}
|
||||
|
||||
func backend() *tfBackend {
|
||||
b := tfBackend{}
|
||||
|
||||
b.Backend = &framework.Backend{
|
||||
Help: strings.TrimSpace(backendHelp),
|
||||
PathsSpecial: &logical.Paths{
|
||||
LocalStorage: []string{
|
||||
framework.WALPrefix,
|
||||
},
|
||||
SealWrapStorage: []string{
|
||||
"config",
|
||||
"role/*",
|
||||
},
|
||||
},
|
||||
Paths: framework.PathAppend(
|
||||
pathRole(&b),
|
||||
[]*framework.Path{
|
||||
pathConfig(&b),
|
||||
pathCredentials(&b),
|
||||
},
|
||||
pathRotateRole(&b),
|
||||
),
|
||||
Secrets: []*framework.Secret{
|
||||
b.terraformToken(),
|
||||
},
|
||||
BackendType: logical.TypeLogical,
|
||||
Invalidate: b.invalidate,
|
||||
}
|
||||
|
||||
return &b
|
||||
}
|
||||
|
||||
func (b *tfBackend) reset() {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
b.client = nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) invalidate(ctx context.Context, key string) {
|
||||
if key == "config" {
|
||||
b.reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *tfBackend) getClient(ctx context.Context, s logical.Storage) (*client, error) {
|
||||
b.lock.RLock()
|
||||
unlockFunc := b.lock.RUnlock
|
||||
defer func() { unlockFunc() }()
|
||||
|
||||
if b.client != nil {
|
||||
return b.client, nil
|
||||
}
|
||||
|
||||
b.lock.RUnlock()
|
||||
b.lock.Lock()
|
||||
unlockFunc = b.lock.Unlock
|
||||
|
||||
config, err := getConfig(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b.client == nil {
|
||||
if config == nil {
|
||||
config = new(tfConfig)
|
||||
}
|
||||
}
|
||||
|
||||
b.client, err = newClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.client, nil
|
||||
}
|
||||
|
||||
const backendHelp = `
|
||||
The Terraform Cloud secrets backend dynamically generates organization
|
||||
and user tokens.
|
||||
|
||||
After mounting this backend, credentials to manage Terraform Cloud or
|
||||
Enterprise tokens must be configured with the "config/" endpoints.
|
||||
`
|
38
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/client.go
generated
vendored
Normal file
38
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/client.go
generated
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
package tfc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/go-tfe"
|
||||
)
|
||||
|
||||
type client struct {
|
||||
*tfe.Client
|
||||
}
|
||||
|
||||
type terraformToken struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func newClient(config *tfConfig) (*client, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("client configuration was nil")
|
||||
}
|
||||
|
||||
cfg := &tfe.Config{
|
||||
Address: config.Address,
|
||||
BasePath: config.BasePath,
|
||||
Token: config.Token,
|
||||
}
|
||||
|
||||
tfc, err := tfe.NewClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &client{
|
||||
tfc,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
module github.com/hashicorp/vault-plugin-secrets-terraform
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/hashicorp/go-hclog v0.14.1
|
||||
github.com/hashicorp/go-tfe v0.12.0
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f
|
||||
github.com/hashicorp/vault/sdk v0.1.14-0.20210106220500-0ddc32f2ab8a
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
)
|
410
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/go.sum
generated
vendored
Normal file
410
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/go.sum
generated
vendored
Normal file
|
@ -0,0 +1,410 @@
|
|||
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/hcsshim v0.8.9 h1:VrfodqvztU8YSOvygU+DN1BGaSGxmrNfqOv5oOuX2Bk=
|
||||
github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs=
|
||||
github.com/armon/go-metrics v0.3.3 h1:a9F4rlj7EWWrbj7BYw8J8+x+ZZkJeqzNyRk8hdPF+ro=
|
||||
github.com/armon/go-metrics v0.3.3/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
|
||||
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
|
||||
github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/containerd v1.3.4 h1:3o0smo5SKY7H6AJCmJhsnCjR2/V2T8VmiHt7seN2/kI=
|
||||
github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc h1:lDK/G7OlwUnJW3O6nv/8M89bMupV6FuLK6FXmC3ueWc=
|
||||
github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
|
||||
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
|
||||
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
|
||||
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
|
||||
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182 h1:Caj/qGJ9KyulC1WSksyPgp7r8+DKgTGfU39lmb2C5MQ=
|
||||
github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE=
|
||||
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
|
||||
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU=
|
||||
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc=
|
||||
github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 h1:xuTi5ZwjimfpvpL09jDE71smCBRpnF5xfo871BSX4gs=
|
||||
github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE=
|
||||
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-slug v0.4.1 h1:/jAo8dNuLgSImoLXaX7Od7QB4TfYCVPam+OpAt5bZqc=
|
||||
github.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-tfe v0.12.0 h1:teL523WPxwYzL5Gjc2QFxExndrMfWY4BXS2/olVpULM=
|
||||
github.com/hashicorp/go-tfe v0.12.0/go.mod h1:oT0AG5u/ROzWiw8JZFLDY6FLh6AZnJIG0Ahhvp10txg=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=
|
||||
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f h1:PYtnlUZzFSZxPcq7mYp5oC9N+BcJ8IKYf6/EG0GHM2Y=
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f/go.mod h1:euTFbi2YJgwcju3imEt919lhJKF68nN1cQPq3aA+kBE=
|
||||
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221530-14615acda45f/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
|
||||
github.com/hashicorp/vault/sdk v0.1.14-0.20210106220500-0ddc32f2ab8a h1:sNWA4c2EmEgJ5RhsgVKgMCmNS/RidljgHLOFYsLrClc=
|
||||
github.com/hashicorp/vault/sdk v0.1.14-0.20210106220500-0ddc32f2ab8a/go.mod h1:cAGI4nVnEfAyMeqt9oB+Mase8DNn3qA/LDNHURiwssY=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
|
||||
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d h1:Z4EH+5EffvBEhh37F0C0DnpklTMh00JOkjW5zK3ofBI=
|
||||
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
171
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_config.go
generated
vendored
Normal file
171
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_config.go
generated
vendored
Normal file
|
@ -0,0 +1,171 @@
|
|||
package tfc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const (
|
||||
configStoragePath = "config"
|
||||
)
|
||||
|
||||
type tfConfig struct {
|
||||
Token string `json:"token"`
|
||||
Address string `json:"address"`
|
||||
BasePath string `json:"base_path"`
|
||||
}
|
||||
|
||||
func pathConfig(b *tfBackend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "config",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"token": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The token to access Terraform Cloud",
|
||||
Required: true,
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
Name: "Token",
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
"address": {
|
||||
Type: framework.TypeString,
|
||||
Description: `The address to access Terraform Cloud or Enterprise.
|
||||
Default is "https://app.terraform.io".`,
|
||||
Default: "https://app.terraform.io",
|
||||
},
|
||||
"base_path": {
|
||||
Type: framework.TypeString,
|
||||
Description: `The base path for the Terraform Cloud or Enterprise API.
|
||||
Default is "/api/v2/".`,
|
||||
Default: "/api/v2/",
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: b.pathConfigRead,
|
||||
},
|
||||
logical.CreateOperation: &framework.PathOperation{
|
||||
Callback: b.pathConfigWrite,
|
||||
},
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: b.pathConfigWrite,
|
||||
},
|
||||
logical.DeleteOperation: &framework.PathOperation{
|
||||
Callback: b.pathConfigDelete,
|
||||
},
|
||||
},
|
||||
ExistenceCheck: b.pathConfigExistenceCheck,
|
||||
HelpSynopsis: pathConfigHelpSynopsis,
|
||||
HelpDescription: pathConfigHelpDescription,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *tfBackend) pathConfigExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
|
||||
out, err := req.Storage.Get(ctx, req.Path)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("existence check failed: %w", err)
|
||||
}
|
||||
|
||||
return out != nil, nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) pathConfigRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
config, err := getConfig(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"address": config.Address,
|
||||
"base_path": config.BasePath,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
config, err := getConfig(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
if req.Operation == logical.UpdateOperation {
|
||||
return nil, errors.New("config not found during update operation")
|
||||
}
|
||||
config = new(tfConfig)
|
||||
}
|
||||
|
||||
address := data.Get("address").(string)
|
||||
basePath := data.Get("base_path").(string)
|
||||
|
||||
config.Address = address
|
||||
config.BasePath = basePath
|
||||
|
||||
token, ok := data.GetOk("token")
|
||||
if ok {
|
||||
config.Token = token.(string)
|
||||
}
|
||||
|
||||
entry, err := logical.StorageEntryJSON(configStoragePath, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := req.Storage.Put(ctx, entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// reset the client so the next invocation will pick up the new configuration
|
||||
b.reset()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) pathConfigDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
err := req.Storage.Delete(ctx, configStoragePath)
|
||||
|
||||
if err == nil {
|
||||
b.reset()
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func getConfig(ctx context.Context, s logical.Storage) (*tfConfig, error) {
|
||||
entry, err := s.Get(ctx, configStoragePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
config := new(tfConfig)
|
||||
if err := entry.DecodeJSON(&config); err != nil {
|
||||
return nil, fmt.Errorf("error reading root configuration: %w", err)
|
||||
}
|
||||
|
||||
// return the config, we are done
|
||||
return config, nil
|
||||
}
|
||||
|
||||
const pathConfigHelpSynopsis = `Configure the Terraform Cloud / Enterprise backend.`
|
||||
|
||||
const pathConfigHelpDescription = `
|
||||
The Terraform Cloud / Enterprise secret backend requires credentials for managing
|
||||
organization and team tokens for Terraform Cloud or Enterprise. This endpoint
|
||||
is used to configure those credentials and the default values for the backend in general.
|
||||
|
||||
You must specify a Terraform Cloud or Enterprise token with organization access
|
||||
to allow Vault to create tokens.
|
||||
|
||||
If you are running Terraform Enterprise, you can specify the address and base path
|
||||
for your instance and API endpoint.
|
||||
`
|
143
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_credentials.go
generated
vendored
Normal file
143
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_credentials.go
generated
vendored
Normal file
|
@ -0,0 +1,143 @@
|
|||
package tfc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func pathCredentials(b *tfBackend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "creds/" + framework.GenericNameRegex("name"),
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeLowerCaseString,
|
||||
Description: "Name of the role",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathCredentialsRead,
|
||||
logical.UpdateOperation: b.pathCredentialsRead,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathCredentialsHelpSyn,
|
||||
HelpDescription: pathCredentialsHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *tfBackend) terraformToken() *framework.Secret {
|
||||
return &framework.Secret{
|
||||
Type: terraformTokenType,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"token": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Terraform Token",
|
||||
},
|
||||
},
|
||||
Revoke: b.terraformTokenRevoke,
|
||||
Renew: b.terraformTokenRenew,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *tfBackend) pathCredentialsRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
roleName := d.Get("name").(string)
|
||||
|
||||
roleEntry, err := b.getRole(ctx, req.Storage, roleName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving role: %w", err)
|
||||
}
|
||||
|
||||
if roleEntry == nil {
|
||||
return nil, errors.New("error retrieving role: role is nil")
|
||||
}
|
||||
|
||||
if roleEntry.UserID != "" {
|
||||
return b.createUserCreds(ctx, req, roleEntry)
|
||||
}
|
||||
|
||||
resp := &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"token_id": roleEntry.TokenID,
|
||||
"token": roleEntry.Token,
|
||||
"organization": roleEntry.Organization,
|
||||
"team_id": roleEntry.TeamID,
|
||||
"role": roleEntry.Name,
|
||||
},
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) createUserCreds(ctx context.Context, req *logical.Request, role *terraformRoleEntry) (*logical.Response, error) {
|
||||
token, err := b.createToken(ctx, req.Storage, role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := b.Secret(terraformTokenType).Response(map[string]interface{}{
|
||||
"token": token.Token,
|
||||
"token_id": token.ID,
|
||||
}, map[string]interface{}{
|
||||
"token_id": token.ID,
|
||||
"role": role.Name,
|
||||
})
|
||||
|
||||
if role.TTL > 0 {
|
||||
resp.Secret.TTL = role.TTL
|
||||
}
|
||||
|
||||
if role.MaxTTL > 0 {
|
||||
resp.Secret.MaxTTL = role.MaxTTL
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) createToken(ctx context.Context, s logical.Storage, roleEntry *terraformRoleEntry) (*terraformToken, error) {
|
||||
client, err := b.getClient(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var token *terraformToken
|
||||
|
||||
switch {
|
||||
case isOrgToken(roleEntry.Organization, roleEntry.TeamID):
|
||||
token, err = createOrgToken(ctx, client, roleEntry.Organization)
|
||||
case isTeamToken(roleEntry.TeamID):
|
||||
token, err = createTeamToken(ctx, client, roleEntry.TeamID)
|
||||
default:
|
||||
token, err = createUserToken(ctx, client, roleEntry.UserID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating Terraform token: %w", err)
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
return nil, errors.New("error creating Terraform token")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
const pathCredentialsHelpSyn = `
|
||||
Generate a Terraform Cloud or Enterprise API token from a specific Vault role.
|
||||
`
|
||||
|
||||
const pathCredentialsHelpDesc = `
|
||||
This path generates Terraform Cloud or Enterprise API Organization, Team, or
|
||||
User Tokens based on a particular role. A role can only represent a single type
|
||||
of Token; Organization, Team, or User, and so can only contain one parameter for
|
||||
organization, team_id, or user_id.
|
||||
|
||||
If the role has the team ID configured, this path generates a team token.
|
||||
|
||||
If this role only has the organization configured, this path generates an
|
||||
organization token.
|
||||
|
||||
If this role has a user ID configured, this path generates a user token.
|
||||
`
|
276
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_roles.go
generated
vendored
Normal file
276
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_roles.go
generated
vendored
Normal file
|
@ -0,0 +1,276 @@
|
|||
package tfc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
// terraformRoleEntry is a Vault role construct that maps to TFC/TFE
|
||||
type terraformRoleEntry struct {
|
||||
Name string `json:"name"`
|
||||
Organization string `json:"organization,omitempty"`
|
||||
TeamID string `json:"team_id,omitempty"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
TTL time.Duration `json:"ttl"`
|
||||
MaxTTL time.Duration `json:"max_ttl"`
|
||||
Token string `json:"token,omitempty"`
|
||||
TokenID string `json:"token_id,omitempty"`
|
||||
}
|
||||
|
||||
func (r *terraformRoleEntry) toResponseData() map[string]interface{} {
|
||||
respData := map[string]interface{}{
|
||||
"name": r.Name,
|
||||
"ttl": r.TTL.Seconds(),
|
||||
"max_ttl": r.MaxTTL.Seconds(),
|
||||
}
|
||||
if r.Organization != "" {
|
||||
respData["organization"] = r.Organization
|
||||
}
|
||||
if r.TeamID != "" {
|
||||
respData["team_id"] = r.TeamID
|
||||
}
|
||||
if r.UserID != "" {
|
||||
respData["user_id"] = r.UserID
|
||||
}
|
||||
return respData
|
||||
}
|
||||
|
||||
func pathRole(b *tfBackend) []*framework.Path {
|
||||
return []*framework.Path{
|
||||
{
|
||||
Pattern: "role/" + framework.GenericNameRegex("name"),
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeLowerCaseString,
|
||||
Description: "Name of the role",
|
||||
Required: true,
|
||||
},
|
||||
"organization": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the Terraform Cloud or Enterprise organization",
|
||||
},
|
||||
"team_id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "ID of the Terraform Cloud or Enterprise team under organization (e.g., settings/teams/team-xxxxxxxxxxxxx)",
|
||||
},
|
||||
"user_id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "ID of the Terraform Cloud or Enterprise user (e.g., user-xxxxxxxxxxxxxxxx)",
|
||||
},
|
||||
"ttl": {
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: "Default lease for generated credentials. If not set or set to 0, will use system default.",
|
||||
},
|
||||
"max_ttl": {
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: "Maximum time for role. If not set or set to 0, will use system default.",
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: b.pathRolesRead,
|
||||
},
|
||||
logical.CreateOperation: &framework.PathOperation{
|
||||
Callback: b.pathRolesWrite,
|
||||
},
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: b.pathRolesWrite,
|
||||
},
|
||||
logical.DeleteOperation: &framework.PathOperation{
|
||||
Callback: b.pathRolesDelete,
|
||||
},
|
||||
},
|
||||
HelpSynopsis: pathRoleHelpSynopsis,
|
||||
HelpDescription: pathRoleHelpDescription,
|
||||
},
|
||||
{
|
||||
Pattern: "role/?$",
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ListOperation: &framework.PathOperation{
|
||||
Callback: b.pathRolesList,
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: pathRoleListHelpSynopsis,
|
||||
HelpDescription: pathRoleListHelpDescription,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *tfBackend) pathRolesList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
entries, err := req.Storage.List(ctx, "role/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logical.ListResponse(entries), nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) pathRolesRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
entry, err := b.getRole(ctx, req.Storage, d.Get("name").(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: entry.toResponseData(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := d.Get("name").(string)
|
||||
if name == "" {
|
||||
return logical.ErrorResponse("missing role name"), nil
|
||||
}
|
||||
|
||||
roleEntry, err := b.getRole(ctx, req.Storage, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if roleEntry == nil {
|
||||
roleEntry = &terraformRoleEntry{}
|
||||
}
|
||||
|
||||
createOperation := (req.Operation == logical.CreateOperation)
|
||||
|
||||
roleEntry.Name = name
|
||||
if organization, ok := d.GetOk("organization"); ok {
|
||||
roleEntry.Organization = organization.(string)
|
||||
} else if createOperation {
|
||||
roleEntry.Organization = d.Get("organization").(string)
|
||||
}
|
||||
|
||||
if teamID, ok := d.GetOk("team_id"); ok {
|
||||
roleEntry.TeamID = teamID.(string)
|
||||
} else if createOperation {
|
||||
roleEntry.TeamID = d.Get("team_id").(string)
|
||||
}
|
||||
|
||||
if userID, ok := d.GetOk("user_id"); ok {
|
||||
roleEntry.UserID = userID.(string)
|
||||
} else if createOperation {
|
||||
roleEntry.UserID = d.Get("user_id").(string)
|
||||
}
|
||||
|
||||
if roleEntry.UserID != "" && (roleEntry.Organization != "" || roleEntry.TeamID != "") {
|
||||
return logical.ErrorResponse("cannot provide a user_id in combination with organization or team_id"), nil
|
||||
}
|
||||
|
||||
if roleEntry.UserID == "" && roleEntry.Organization == "" && roleEntry.TeamID == "" {
|
||||
return logical.ErrorResponse("must provide an organization name, team id, or user id"), nil
|
||||
}
|
||||
|
||||
if ttlRaw, ok := d.GetOk("ttl"); ok {
|
||||
roleEntry.TTL = time.Duration(ttlRaw.(int)) * time.Second
|
||||
} else if req.Operation == logical.CreateOperation {
|
||||
roleEntry.TTL = time.Duration(d.Get("ttl").(int)) * time.Second
|
||||
}
|
||||
|
||||
if maxTTLRaw, ok := d.GetOk("max_ttl"); ok {
|
||||
roleEntry.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second
|
||||
} else if req.Operation == logical.CreateOperation {
|
||||
roleEntry.MaxTTL = time.Duration(d.Get("max_ttl").(int)) * time.Second
|
||||
}
|
||||
|
||||
if roleEntry.MaxTTL != 0 && roleEntry.TTL > roleEntry.MaxTTL {
|
||||
return logical.ErrorResponse("ttl cannot be greater than max_ttl"), nil
|
||||
}
|
||||
|
||||
// if we're creating a role to manage a Team or Organization, we need to
|
||||
// create the token now. User tokens will be created when credentials are
|
||||
// read.
|
||||
if roleEntry.Organization != "" || roleEntry.TeamID != "" {
|
||||
token, err := b.createToken(ctx, req.Storage, roleEntry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleEntry.Token = token.Token
|
||||
roleEntry.TokenID = token.ID
|
||||
}
|
||||
|
||||
if err := setRole(ctx, req.Storage, name, roleEntry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
err := req.Storage.Delete(ctx, "role/"+d.Get("name").(string))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deleting terraform role: %w", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func setRole(ctx context.Context, s logical.Storage, name string, roleEntry *terraformRoleEntry) error {
|
||||
entry, err := logical.StorageEntryJSON("role/"+name, roleEntry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
return fmt.Errorf("failed to create storage entry for role")
|
||||
}
|
||||
|
||||
if err := s.Put(ctx, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) getRole(ctx context.Context, s logical.Storage, name string) (*terraformRoleEntry, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("missing role name")
|
||||
}
|
||||
|
||||
entry, err := s.Get(ctx, "role/"+name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var role terraformRoleEntry
|
||||
|
||||
if err := entry.DecodeJSON(&role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
const (
|
||||
pathRoleHelpSynopsis = `Manages the Vault role for generating Terraform Cloud / Enterprise tokens.`
|
||||
pathRoleHelpDescription = `
|
||||
This path allows you to read and write roles used to generate Terraform Cloud /
|
||||
Enterprise tokens. You can configure a role to manage an organization's token, a
|
||||
team's token, or a user's dynamic tokens.
|
||||
|
||||
A Terraform Cloud/Enterprise Organization can only have one active token at a
|
||||
time. To manage an Organization's token, set the organization field.
|
||||
|
||||
A Terraform Cloud/Enterprise Team can only have one active token at a time. To
|
||||
manage a Teams's token, set the team_id field.
|
||||
|
||||
A Terraform Cloud/Enterprise User can have multiple API tokens. To manage a
|
||||
User's token, set the user_id field.
|
||||
`
|
||||
|
||||
pathRoleListHelpSynopsis = `List the existing roles in Terraform Cloud / Enterprise backend`
|
||||
pathRoleListHelpDescription = `Roles will be listed by the role name.`
|
||||
)
|
75
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_rotate_role.go
generated
vendored
Normal file
75
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_rotate_role.go
generated
vendored
Normal file
|
@ -0,0 +1,75 @@
|
|||
package tfc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func pathRotateRole(b *tfBackend) []*framework.Path {
|
||||
return []*framework.Path{
|
||||
{
|
||||
Pattern: "rotate-role/" + framework.GenericNameRegex("name"),
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the team or organization role",
|
||||
},
|
||||
},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: b.pathRotateRole,
|
||||
ForwardPerformanceStandby: true,
|
||||
ForwardPerformanceSecondary: true,
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: pathRotateRoleHelpSyn,
|
||||
HelpDescription: pathRotateRoleHelpDesc,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *tfBackend) pathRotateRole(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := d.Get("name").(string)
|
||||
if name == "" {
|
||||
return logical.ErrorResponse("missing role name"), nil
|
||||
}
|
||||
|
||||
roleEntry, err := b.getRole(ctx, req.Storage, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if roleEntry == nil {
|
||||
return logical.ErrorResponse("missing role entry"), nil
|
||||
}
|
||||
|
||||
if roleEntry.UserID != "" {
|
||||
return logical.ErrorResponse("cannot rotate credentials for user roles"), nil
|
||||
}
|
||||
|
||||
token, err := b.createToken(ctx, req.Storage, roleEntry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleEntry.Token = token.Token
|
||||
|
||||
if err := setRole(ctx, req.Storage, name, roleEntry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
const pathRotateRoleHelpSyn = `
|
||||
Request to rotate the credentials for a team or organization.
|
||||
`
|
||||
|
||||
const pathRotateRoleHelpDesc = `
|
||||
This path attempts to rotate the credentials for the given team or organization role.
|
||||
This endpoint returns an error if attempting to rotate a user role.
|
||||
`
|
160
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/terraform_token.go
generated
vendored
Normal file
160
vendor/github.com/hashicorp/vault-plugin-secrets-terraform/terraform_token.go
generated
vendored
Normal file
|
@ -0,0 +1,160 @@
|
|||
package tfc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-tfe"
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const (
|
||||
terraformTokenType = "terraform_token"
|
||||
)
|
||||
|
||||
func isOrgToken(organization string, team string) bool {
|
||||
return organization != "" && team == ""
|
||||
}
|
||||
|
||||
func isTeamToken(team string) bool {
|
||||
return team != ""
|
||||
}
|
||||
|
||||
func createOrgToken(ctx context.Context, c *client, organization string) (*terraformToken, error) {
|
||||
if _, err := c.Organizations.Read(ctx, organization); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := c.OrganizationTokens.Generate(ctx, organization)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &terraformToken{
|
||||
ID: token.ID,
|
||||
Description: token.Description,
|
||||
Token: token.Token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createTeamToken(ctx context.Context, c *client, teamID string) (*terraformToken, error) {
|
||||
if _, err := c.Teams.Read(ctx, teamID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := c.TeamTokens.Generate(ctx, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &terraformToken{
|
||||
ID: token.ID,
|
||||
Description: token.Description,
|
||||
Token: token.Token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createUserToken(ctx context.Context, c *client, userID string) (*terraformToken, error) {
|
||||
token, err := c.UserTokens.Generate(ctx, userID, tfe.UserTokenGenerateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &terraformToken{
|
||||
ID: token.ID,
|
||||
Description: token.Description,
|
||||
Token: token.Token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) terraformTokenRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
client, err := b.getClient(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting client: %w", err)
|
||||
}
|
||||
|
||||
organization := ""
|
||||
organizationRaw, ok := req.Secret.InternalData["organization"]
|
||||
if ok {
|
||||
organization, ok = organizationRaw.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value for organization in secret internal data")
|
||||
}
|
||||
}
|
||||
|
||||
teamID := ""
|
||||
teamIDRaw, ok := req.Secret.InternalData["team_id"]
|
||||
if ok {
|
||||
teamID, ok = teamIDRaw.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value for team_id in secret internal data")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if isOrgToken(organization, teamID) {
|
||||
// revoke org API token
|
||||
if err := client.OrganizationTokens.Delete(ctx, organization); err != nil {
|
||||
return nil, fmt.Errorf("error revoking organization token: %w", err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if isTeamToken(teamID) {
|
||||
// revoke team API token
|
||||
if err := client.TeamTokens.Delete(ctx, teamID); err != nil {
|
||||
return nil, fmt.Errorf("error revoking team token: %w", err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if we haven't returned yet, then the token is a user API token
|
||||
tokenID := ""
|
||||
tokenIDRaw, ok := req.Secret.InternalData["token_id"]
|
||||
if ok {
|
||||
tokenID, ok = tokenIDRaw.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("secret is missing tokenID internal data")
|
||||
}
|
||||
}
|
||||
|
||||
if tokenID == "" {
|
||||
return nil, fmt.Errorf("secret is missing tokenID internal data")
|
||||
}
|
||||
|
||||
if err := client.UserTokens.Delete(ctx, tokenID); err != nil {
|
||||
return nil, fmt.Errorf("error revoking user token: %w", err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *tfBackend) terraformTokenRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
roleRaw, ok := req.Secret.InternalData["role"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("secret is missing role internal data")
|
||||
}
|
||||
|
||||
// get the role entry
|
||||
role := roleRaw.(string)
|
||||
roleEntry, err := b.getRole(ctx, req.Storage, role)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving role: %w", err)
|
||||
}
|
||||
|
||||
if roleEntry == nil {
|
||||
return nil, errors.New("error retrieving role: role is nil")
|
||||
}
|
||||
|
||||
resp := &logical.Response{Secret: req.Secret}
|
||||
|
||||
if roleEntry.TTL > 0 {
|
||||
resp.Secret.TTL = roleEntry.TTL
|
||||
}
|
||||
if roleEntry.MaxTTL > 0 {
|
||||
resp.Secret.MaxTTL = roleEntry.MaxTTL
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/examples/examples
|
|
@ -0,0 +1,7 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- tip
|
||||
script: go test ./... -v
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Google Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,457 @@
|
|||
# jsonapi
|
||||
|
||||
[![Build Status](https://travis-ci.org/google/jsonapi.svg?branch=master)](https://travis-ci.org/google/jsonapi)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/google/jsonapi)](https://goreportcard.com/report/github.com/google/jsonapi)
|
||||
[![GoDoc](https://godoc.org/github.com/google/jsonapi?status.svg)](http://godoc.org/github.com/google/jsonapi)
|
||||
|
||||
A serializer/deserializer for JSON payloads that comply to the
|
||||
[JSON API - jsonapi.org](http://jsonapi.org) spec in go.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get -u github.com/google/jsonapi
|
||||
```
|
||||
|
||||
Or, see [Alternative Installation](#alternative-installation).
|
||||
|
||||
## Background
|
||||
|
||||
You are working in your Go web application and you have a struct that is
|
||||
organized similarly to your database schema. You need to send and
|
||||
receive json payloads that adhere to the JSON API spec. Once you realize that
|
||||
your json needed to take on this special form, you go down the path of
|
||||
creating more structs to be able to serialize and deserialize JSON API
|
||||
payloads. Then there are more models required with this additional
|
||||
structure. Ugh! With JSON API, you can keep your model structs as is and
|
||||
use [StructTags](http://golang.org/pkg/reflect/#StructTag) to indicate
|
||||
to JSON API how you want your response built or your request
|
||||
deserialized. What about your relationships? JSON API supports
|
||||
relationships out of the box and will even put them in your response
|
||||
into an `included` side-loaded slice--that contains associated records.
|
||||
|
||||
## Introduction
|
||||
|
||||
JSON API uses [StructField](http://golang.org/pkg/reflect/#StructField)
|
||||
tags to annotate the structs fields that you already have and use in
|
||||
your app and then reads and writes [JSON API](http://jsonapi.org)
|
||||
output based on the instructions you give the library in your JSON API
|
||||
tags. Let's take an example. In your app, you most likely have structs
|
||||
that look similar to these:
|
||||
|
||||
|
||||
```go
|
||||
type Blog struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Posts []*Post `json:"posts"`
|
||||
CurrentPost *Post `json:"current_post"`
|
||||
CurrentPostId int `json:"current_post_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ViewCount int `json:"view_count"`
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
ID int `json:"id"`
|
||||
BlogID int `json:"blog_id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Comments []*Comment `json:"comments"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
Id int `json:"id"`
|
||||
PostID int `json:"post_id"`
|
||||
Body string `json:"body"`
|
||||
Likes uint `json:"likes_count,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
These structs may or may not resemble the layout of your database. But
|
||||
these are the ones that you want to use right? You wouldn't want to use
|
||||
structs like those that JSON API sends because it is difficult to get at
|
||||
all of your data easily.
|
||||
|
||||
## Example App
|
||||
|
||||
[examples/app.go](https://github.com/google/jsonapi/blob/master/examples/app.go)
|
||||
|
||||
This program demonstrates the implementation of a create, a show,
|
||||
and a list [http.Handler](http://golang.org/pkg/net/http#Handler). It
|
||||
outputs some example requests and responses as well as serialized
|
||||
examples of the source/target structs to json. That is to say, I show
|
||||
you that the library has successfully taken your JSON API request and
|
||||
turned it into your struct types.
|
||||
|
||||
To run,
|
||||
|
||||
* Make sure you have [Go installed](https://golang.org/doc/install)
|
||||
* Create the following directories or similar: `~/go`
|
||||
* Set `GOPATH` to `PWD` in your shell session, `export GOPATH=$PWD`
|
||||
* `go get github.com/google/jsonapi`. (Append `-u` after `get` if you
|
||||
are updating.)
|
||||
* `cd $GOPATH/src/github.com/google/jsonapi/examples`
|
||||
* `go build && ./examples`
|
||||
|
||||
## `jsonapi` Tag Reference
|
||||
|
||||
### Example
|
||||
|
||||
The `jsonapi` [StructTags](http://golang.org/pkg/reflect/#StructTag)
|
||||
tells this library how to marshal and unmarshal your structs into
|
||||
JSON API payloads and your JSON API payloads to structs, respectively.
|
||||
Then Use JSON API's Marshal and Unmarshal methods to construct and read
|
||||
your responses and replies. Here's an example of the structs above
|
||||
using JSON API tags:
|
||||
|
||||
```go
|
||||
type Blog struct {
|
||||
ID int `jsonapi:"primary,blogs"`
|
||||
Title string `jsonapi:"attr,title"`
|
||||
Posts []*Post `jsonapi:"relation,posts"`
|
||||
CurrentPost *Post `jsonapi:"relation,current_post"`
|
||||
CurrentPostID int `jsonapi:"attr,current_post_id"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created_at"`
|
||||
ViewCount int `jsonapi:"attr,view_count"`
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
ID int `jsonapi:"primary,posts"`
|
||||
BlogID int `jsonapi:"attr,blog_id"`
|
||||
Title string `jsonapi:"attr,title"`
|
||||
Body string `jsonapi:"attr,body"`
|
||||
Comments []*Comment `jsonapi:"relation,comments"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID int `jsonapi:"primary,comments"`
|
||||
PostID int `jsonapi:"attr,post_id"`
|
||||
Body string `jsonapi:"attr,body"`
|
||||
Likes uint `jsonapi:"attr,likes-count,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
### Permitted Tag Values
|
||||
|
||||
#### `primary`
|
||||
|
||||
```
|
||||
`jsonapi:"primary,<type field output>"`
|
||||
```
|
||||
|
||||
This indicates this is the primary key field for this struct type.
|
||||
Tag value arguments are comma separated. The first argument must be,
|
||||
`primary`, and the second must be the name that should appear in the
|
||||
`type`\* field for all data objects that represent this type of model.
|
||||
|
||||
\* According the [JSON API](http://jsonapi.org) spec, the plural record
|
||||
types are shown in the examples, but not required.
|
||||
|
||||
#### `attr`
|
||||
|
||||
```
|
||||
`jsonapi:"attr,<key name in attributes hash>,<optional: omitempty>"`
|
||||
```
|
||||
|
||||
These fields' values will end up in the `attributes`hash for a record.
|
||||
The first argument must be, `attr`, and the second should be the name
|
||||
for the key to display in the `attributes` hash for that record. The optional
|
||||
third argument is `omitempty` - if it is present the field will not be present
|
||||
in the `"attributes"` if the field's value is equivalent to the field types
|
||||
empty value (ie if the `count` field is of type `int`, `omitempty` will omit the
|
||||
field when `count` has a value of `0`). Lastly, the spec indicates that
|
||||
`attributes` key names should be dasherized for multiple word field names.
|
||||
|
||||
#### `relation`
|
||||
|
||||
```
|
||||
`jsonapi:"relation,<key name in relationships hash>,<optional: omitempty>"`
|
||||
```
|
||||
|
||||
Relations are struct fields that represent a one-to-one or one-to-many
|
||||
relationship with other structs. JSON API will traverse the graph of
|
||||
relationships and marshal or unmarshal records. The first argument must
|
||||
be, `relation`, and the second should be the name of the relationship,
|
||||
used as the key in the `relationships` hash for the record. The optional
|
||||
third argument is `omitempty` - if present will prevent non existent to-one and
|
||||
to-many from being serialized.
|
||||
|
||||
## Methods Reference
|
||||
|
||||
**All `Marshal` and `Unmarshal` methods expect pointers to struct
|
||||
instance or slices of the same contained with the `interface{}`s**
|
||||
|
||||
Now you have your structs prepared to be seralized or materialized, What
|
||||
about the rest?
|
||||
|
||||
### Create Record Example
|
||||
|
||||
You can Unmarshal a JSON API payload using
|
||||
[jsonapi.UnmarshalPayload](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload).
|
||||
It reads from an [io.Reader](https://golang.org/pkg/io/#Reader)
|
||||
containing a JSON API payload for one record (but can have related
|
||||
records). Then, it materializes a struct that you created and passed in
|
||||
(using new or &). Again, the method supports single records only, at
|
||||
the top level, in request payloads at the moment. Bulk creates and
|
||||
updates are not supported yet.
|
||||
|
||||
After saving your record, you can use,
|
||||
[MarshalOnePayload](http://godoc.org/github.com/google/jsonapi#MarshalOnePayload),
|
||||
to write the JSON API response to an
|
||||
[io.Writer](https://golang.org/pkg/io/#Writer).
|
||||
|
||||
#### `UnmarshalPayload`
|
||||
|
||||
```go
|
||||
UnmarshalPayload(in io.Reader, model interface{})
|
||||
```
|
||||
|
||||
Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload)
|
||||
|
||||
#### `MarshalPayload`
|
||||
|
||||
```go
|
||||
MarshalPayload(w io.Writer, models interface{}) error
|
||||
```
|
||||
|
||||
Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalPayload)
|
||||
|
||||
Writes a JSON API response, with related records sideloaded, into an
|
||||
`included` array. This method encodes a response for either a single record or
|
||||
many records.
|
||||
|
||||
##### Handler Example Code
|
||||
|
||||
```go
|
||||
func CreateBlog(w http.ResponseWriter, r *http.Request) {
|
||||
blog := new(Blog)
|
||||
|
||||
if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// ...save your blog...
|
||||
|
||||
w.Header().Set("Content-Type", jsonapi.MediaType)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
if err := jsonapi.MarshalPayload(w, blog); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Create Records Example
|
||||
|
||||
#### `UnmarshalManyPayload`
|
||||
|
||||
```go
|
||||
UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error)
|
||||
```
|
||||
|
||||
Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalManyPayload)
|
||||
|
||||
Takes an `io.Reader` and a `reflect.Type` representing the uniform type
|
||||
contained within the `"data"` JSON API member.
|
||||
|
||||
##### Handler Example Code
|
||||
|
||||
```go
|
||||
func CreateBlogs(w http.ResponseWriter, r *http.Request) {
|
||||
// ...create many blogs at once
|
||||
|
||||
blogs, err := UnmarshalManyPayload(r.Body, reflect.TypeOf(new(Blog)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, blog := range blogs {
|
||||
b, ok := blog.(*Blog)
|
||||
// ...save each of your blogs
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", jsonapi.MediaType)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
if err := jsonapi.MarshalPayload(w, blogs); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Links
|
||||
|
||||
If you need to include [link objects](http://jsonapi.org/format/#document-links) along with response data, implement the `Linkable` interface for document-links, and `RelationshipLinkable` for relationship links:
|
||||
|
||||
```go
|
||||
func (post Post) JSONAPILinks() *Links {
|
||||
return &Links{
|
||||
"self": "href": fmt.Sprintf("https://example.com/posts/%d", post.ID),
|
||||
"comments": Link{
|
||||
Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", post.ID),
|
||||
Meta: map[string]interface{}{
|
||||
"counts": map[string]uint{
|
||||
"likes": 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Invoked for each relationship defined on the Post struct when marshaled
|
||||
func (post Post) JSONAPIRelationshipLinks(relation string) *Links {
|
||||
if relation == "comments" {
|
||||
return &Links{
|
||||
"related": fmt.Sprintf("https://example.com/posts/%d/comments", post.ID),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Meta
|
||||
|
||||
If you need to include [meta objects](http://jsonapi.org/format/#document-meta) along with response data, implement the `Metable` interface for document-meta, and `RelationshipMetable` for relationship meta:
|
||||
|
||||
```go
|
||||
func (post Post) JSONAPIMeta() *Meta {
|
||||
return &Meta{
|
||||
"details": "sample details here",
|
||||
}
|
||||
}
|
||||
|
||||
// Invoked for each relationship defined on the Post struct when marshaled
|
||||
func (post Post) JSONAPIRelationshipMeta(relation string) *Meta {
|
||||
if relation == "comments" {
|
||||
return &Meta{
|
||||
"this": map[string]interface{}{
|
||||
"can": map[string]interface{}{
|
||||
"go": []interface{}{
|
||||
"as",
|
||||
"deep",
|
||||
map[string]interface{}{
|
||||
"as": "required",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Errors
|
||||
This package also implements support for JSON API compatible `errors` payloads using the following types.
|
||||
|
||||
#### `MarshalErrors`
|
||||
```go
|
||||
MarshalErrors(w io.Writer, errs []*ErrorObject) error
|
||||
```
|
||||
|
||||
Writes a JSON API response using the given `[]error`.
|
||||
|
||||
#### `ErrorsPayload`
|
||||
```go
|
||||
type ErrorsPayload struct {
|
||||
Errors []*ErrorObject `json:"errors"`
|
||||
}
|
||||
```
|
||||
|
||||
ErrorsPayload is a serializer struct for representing a valid JSON API errors payload.
|
||||
|
||||
#### `ErrorObject`
|
||||
```go
|
||||
type ErrorObject struct { ... }
|
||||
|
||||
// Error implements the `Error` interface.
|
||||
func (e *ErrorObject) Error() string {
|
||||
return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail)
|
||||
}
|
||||
```
|
||||
|
||||
ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object.
|
||||
|
||||
The main idea behind this struct is that you can use it directly in your code as an error type and pass it directly to `MarshalErrors` to get a valid JSON API errors payload.
|
||||
|
||||
##### Errors Example Code
|
||||
```go
|
||||
// An error has come up in your code, so set an appropriate status, and serialize the error.
|
||||
if err := validate(&myStructToValidate); err != nil {
|
||||
context.SetStatusCode(http.StatusBadRequest) // Or however you need to set a status.
|
||||
jsonapi.MarshalErrors(w, []*ErrorObject{{
|
||||
Title: "Validation Error",
|
||||
Detail: "Given request body was invalid.",
|
||||
Status: "400",
|
||||
Meta: map[string]interface{}{"field": "some_field", "error": "bad type", "expected": "string", "received": "float64"},
|
||||
}})
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### `MarshalOnePayloadEmbedded`
|
||||
|
||||
```go
|
||||
MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error
|
||||
```
|
||||
|
||||
Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalOnePayloadEmbedded)
|
||||
|
||||
This method is not strictly meant to for use in implementation code,
|
||||
although feel free. It was mainly created for use in tests; in most cases,
|
||||
your request payloads for create will be embedded rather than sideloaded
|
||||
for related records. This method will serialize a single struct pointer
|
||||
into an embedded json response. In other words, there will be no,
|
||||
`included`, array in the json; all relationships will be serialized
|
||||
inline with the data.
|
||||
|
||||
However, in tests, you may want to construct payloads to post to create
|
||||
methods that are embedded to most closely model the payloads that will
|
||||
be produced by the client. This method aims to enable that.
|
||||
|
||||
### Example
|
||||
|
||||
```go
|
||||
out := bytes.NewBuffer(nil)
|
||||
|
||||
// testModel returns a pointer to a Blog
|
||||
jsonapi.MarshalOnePayloadEmbedded(out, testModel())
|
||||
|
||||
h := new(BlogsHandler)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest(http.MethodPost, "/blogs", out)
|
||||
|
||||
h.CreateBlog(w, r)
|
||||
|
||||
blog := new(Blog)
|
||||
jsonapi.UnmarshalPayload(w.Body, blog)
|
||||
|
||||
// ... assert stuff about blog here ...
|
||||
```
|
||||
|
||||
## Alternative Installation
|
||||
I use git subtrees to manage dependencies rather than `go get` so that
|
||||
the src is committed to my repo.
|
||||
|
||||
```
|
||||
git subtree add --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master
|
||||
```
|
||||
|
||||
To update,
|
||||
|
||||
```
|
||||
git subtree pull --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master
|
||||
```
|
||||
|
||||
This assumes that I have my repo structured with a `src` dir containing
|
||||
a collection of packages and `GOPATH` is set to the root
|
||||
folder--containing `src`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Fork, Change, Pull Request *with tests*.
|
|
@ -0,0 +1,55 @@
|
|||
package jsonapi
|
||||
|
||||
const (
|
||||
// StructTag annotation strings
|
||||
annotationJSONAPI = "jsonapi"
|
||||
annotationPrimary = "primary"
|
||||
annotationClientID = "client-id"
|
||||
annotationAttribute = "attr"
|
||||
annotationRelation = "relation"
|
||||
annotationOmitEmpty = "omitempty"
|
||||
annotationISO8601 = "iso8601"
|
||||
annotationSeperator = ","
|
||||
|
||||
iso8601TimeFormat = "2006-01-02T15:04:05Z"
|
||||
|
||||
// MediaType is the identifier for the JSON API media type
|
||||
//
|
||||
// see http://jsonapi.org/format/#document-structure
|
||||
MediaType = "application/vnd.api+json"
|
||||
|
||||
// Pagination Constants
|
||||
//
|
||||
// http://jsonapi.org/format/#fetching-pagination
|
||||
|
||||
// KeyFirstPage is the key to the links object whose value contains a link to
|
||||
// the first page of data
|
||||
KeyFirstPage = "first"
|
||||
// KeyLastPage is the key to the links object whose value contains a link to
|
||||
// the last page of data
|
||||
KeyLastPage = "last"
|
||||
// KeyPreviousPage is the key to the links object whose value contains a link
|
||||
// to the previous page of data
|
||||
KeyPreviousPage = "prev"
|
||||
// KeyNextPage is the key to the links object whose value contains a link to
|
||||
// the next page of data
|
||||
KeyNextPage = "next"
|
||||
|
||||
// QueryParamPageNumber is a JSON API query parameter used in a page based
|
||||
// pagination strategy in conjunction with QueryParamPageSize
|
||||
QueryParamPageNumber = "page[number]"
|
||||
// QueryParamPageSize is a JSON API query parameter used in a page based
|
||||
// pagination strategy in conjunction with QueryParamPageNumber
|
||||
QueryParamPageSize = "page[size]"
|
||||
|
||||
// QueryParamPageOffset is a JSON API query parameter used in an offset based
|
||||
// pagination strategy in conjunction with QueryParamPageLimit
|
||||
QueryParamPageOffset = "page[offset]"
|
||||
// QueryParamPageLimit is a JSON API query parameter used in an offset based
|
||||
// pagination strategy in conjunction with QueryParamPageOffset
|
||||
QueryParamPageLimit = "page[limit]"
|
||||
|
||||
// QueryParamPageCursor is a JSON API query parameter used with a cursor-based
|
||||
// strategy
|
||||
QueryParamPageCursor = "page[cursor]"
|
||||
)
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Package jsonapi provides a serializer and deserializer for jsonapi.org spec payloads.
|
||||
|
||||
You can keep your model structs as is and use struct field tags to indicate to jsonapi
|
||||
how you want your response built or your request deserialzied. What about my relationships?
|
||||
jsonapi supports relationships out of the box and will even side load them in your response
|
||||
into an "included" array--that contains associated objects.
|
||||
|
||||
jsonapi uses StructField tags to annotate the structs fields that you already have and use
|
||||
in your app and then reads and writes jsonapi.org output based on the instructions you give
|
||||
the library in your jsonapi tags.
|
||||
|
||||
Example structs using a Blog > Post > Comment structure,
|
||||
|
||||
type Blog struct {
|
||||
ID int `jsonapi:"primary,blogs"`
|
||||
Title string `jsonapi:"attr,title"`
|
||||
Posts []*Post `jsonapi:"relation,posts"`
|
||||
CurrentPost *Post `jsonapi:"relation,current_post"`
|
||||
CurrentPostID int `jsonapi:"attr,current_post_id"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created_at"`
|
||||
ViewCount int `jsonapi:"attr,view_count"`
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
ID int `jsonapi:"primary,posts"`
|
||||
BlogID int `jsonapi:"attr,blog_id"`
|
||||
Title string `jsonapi:"attr,title"`
|
||||
Body string `jsonapi:"attr,body"`
|
||||
Comments []*Comment `jsonapi:"relation,comments"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID int `jsonapi:"primary,comments"`
|
||||
PostID int `jsonapi:"attr,post_id"`
|
||||
Body string `jsonapi:"attr,body"`
|
||||
}
|
||||
|
||||
jsonapi Tag Reference
|
||||
|
||||
Value, primary: "primary,<type field output>"
|
||||
|
||||
This indicates that this is the primary key field for this struct type. Tag
|
||||
value arguments are comma separated. The first argument must be, "primary", and
|
||||
the second must be the name that should appear in the "type" field for all data
|
||||
objects that represent this type of model.
|
||||
|
||||
Value, attr: "attr,<key name in attributes hash>[,<extra arguments>]"
|
||||
|
||||
These fields' values should end up in the "attribute" hash for a record. The first
|
||||
argument must be, "attr', and the second should be the name for the key to display in
|
||||
the the "attributes" hash for that record.
|
||||
|
||||
The following extra arguments are also supported:
|
||||
|
||||
"omitempty": excludes the fields value from the "attribute" hash.
|
||||
"iso8601": uses the ISO8601 timestamp format when serialising or deserialising the time.Time value.
|
||||
|
||||
Value, relation: "relation,<key name in relationships hash>"
|
||||
|
||||
Relations are struct fields that represent a one-to-one or one-to-many to other structs.
|
||||
jsonapi will traverse the graph of relationships and marshal or unmarshal records. The first
|
||||
argument must be, "relation", and the second should be the name of the relationship, used as
|
||||
the key in the "relationships" hash for the record.
|
||||
|
||||
Use the methods below to Marshal and Unmarshal jsonapi.org json payloads.
|
||||
|
||||
Visit the readme at https://github.com/google/jsonapi
|
||||
*/
|
||||
package jsonapi
|
|
@ -0,0 +1,55 @@
|
|||
package jsonapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// MarshalErrors writes a JSON API response using the given `[]error`.
|
||||
//
|
||||
// For more information on JSON API error payloads, see the spec here:
|
||||
// http://jsonapi.org/format/#document-top-level
|
||||
// and here: http://jsonapi.org/format/#error-objects.
|
||||
func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error {
|
||||
if err := json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrorsPayload is a serializer struct for representing a valid JSON API errors payload.
|
||||
type ErrorsPayload struct {
|
||||
Errors []*ErrorObject `json:"errors"`
|
||||
}
|
||||
|
||||
// ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object.
|
||||
//
|
||||
// The main idea behind this struct is that you can use it directly in your code as an error type
|
||||
// and pass it directly to `MarshalErrors` to get a valid JSON API errors payload.
|
||||
// For more information on Golang errors, see: https://golang.org/pkg/errors/
|
||||
// For more information on the JSON API spec's error objects, see: http://jsonapi.org/format/#error-objects
|
||||
type ErrorObject struct {
|
||||
// ID is a unique identifier for this particular occurrence of a problem.
|
||||
ID string `json:"id,omitempty"`
|
||||
|
||||
// Title is a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
|
||||
Title string `json:"title,omitempty"`
|
||||
|
||||
// Detail is a human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized.
|
||||
Detail string `json:"detail,omitempty"`
|
||||
|
||||
// Status is the HTTP status code applicable to this problem, expressed as a string value.
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
// Code is an application-specific error code, expressed as a string value.
|
||||
Code string `json:"code,omitempty"`
|
||||
|
||||
// Meta is an object containing non-standard meta-information about the error.
|
||||
Meta *map[string]interface{} `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// Error implements the `Error` interface.
|
||||
func (e *ErrorObject) Error() string {
|
||||
return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail)
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package jsonapi
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Payloader is used to encapsulate the One and Many payload types
|
||||
type Payloader interface {
|
||||
clearIncluded()
|
||||
}
|
||||
|
||||
// OnePayload is used to represent a generic JSON API payload where a single
|
||||
// resource (Node) was included as an {} in the "data" key
|
||||
type OnePayload struct {
|
||||
Data *Node `json:"data"`
|
||||
Included []*Node `json:"included,omitempty"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
func (p *OnePayload) clearIncluded() {
|
||||
p.Included = []*Node{}
|
||||
}
|
||||
|
||||
// ManyPayload is used to represent a generic JSON API payload where many
|
||||
// resources (Nodes) were included in an [] in the "data" key
|
||||
type ManyPayload struct {
|
||||
Data []*Node `json:"data"`
|
||||
Included []*Node `json:"included,omitempty"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
func (p *ManyPayload) clearIncluded() {
|
||||
p.Included = []*Node{}
|
||||
}
|
||||
|
||||
// Node is used to represent a generic JSON API Resource
|
||||
type Node struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id,omitempty"`
|
||||
ClientID string `json:"client-id,omitempty"`
|
||||
Attributes map[string]interface{} `json:"attributes,omitempty"`
|
||||
Relationships map[string]interface{} `json:"relationships,omitempty"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// RelationshipOneNode is used to represent a generic has one JSON API relation
|
||||
type RelationshipOneNode struct {
|
||||
Data *Node `json:"data"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// RelationshipManyNode is used to represent a generic has many JSON API
|
||||
// relation
|
||||
type RelationshipManyNode struct {
|
||||
Data []*Node `json:"data"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// Links is used to represent a `links` object.
|
||||
// http://jsonapi.org/format/#document-links
|
||||
type Links map[string]interface{}
|
||||
|
||||
func (l *Links) validate() (err error) {
|
||||
// Each member of a links object is a “link”. A link MUST be represented as
|
||||
// either:
|
||||
// - a string containing the link’s URL.
|
||||
// - an object (“link object”) which can contain the following members:
|
||||
// - href: a string containing the link’s URL.
|
||||
// - meta: a meta object containing non-standard meta-information about the
|
||||
// link.
|
||||
for k, v := range *l {
|
||||
_, isString := v.(string)
|
||||
_, isLink := v.(Link)
|
||||
|
||||
if !(isString || isLink) {
|
||||
return fmt.Errorf(
|
||||
"The %s member of the links object was not a string or link object",
|
||||
k,
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Link is used to represent a member of the `links` object.
|
||||
type Link struct {
|
||||
Href string `json:"href"`
|
||||
Meta Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// Linkable is used to include document links in response data
|
||||
// e.g. {"self": "http://example.com/posts/1"}
|
||||
type Linkable interface {
|
||||
JSONAPILinks() *Links
|
||||
}
|
||||
|
||||
// RelationshipLinkable is used to include relationship links in response data
|
||||
// e.g. {"related": "http://example.com/posts/1/comments"}
|
||||
type RelationshipLinkable interface {
|
||||
// JSONAPIRelationshipLinks will be invoked for each relationship with the corresponding relation name (e.g. `comments`)
|
||||
JSONAPIRelationshipLinks(relation string) *Links
|
||||
}
|
||||
|
||||
// Meta is used to represent a `meta` object.
|
||||
// http://jsonapi.org/format/#document-meta
|
||||
type Meta map[string]interface{}
|
||||
|
||||
// Metable is used to include document meta in response data
|
||||
// e.g. {"foo": "bar"}
|
||||
type Metable interface {
|
||||
JSONAPIMeta() *Meta
|
||||
}
|
||||
|
||||
// RelationshipMetable is used to include relationship meta in response data
|
||||
type RelationshipMetable interface {
|
||||
// JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`)
|
||||
JSONAPIRelationshipMeta(relation string) *Meta
|
||||
}
|
|
@ -0,0 +1,680 @@
|
|||
package jsonapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidTime is returned when a struct has a time.Time type field, but
|
||||
// the JSON value was not a unix timestamp integer.
|
||||
ErrInvalidTime = errors.New("Only numbers can be parsed as dates, unix timestamps")
|
||||
// ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes
|
||||
// "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string.
|
||||
ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps")
|
||||
// ErrUnknownFieldNumberType is returned when the JSON value was a float
|
||||
// (numeric) but the Struct field was a non numeric type (i.e. not int, uint,
|
||||
// float, etc)
|
||||
ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type")
|
||||
// ErrInvalidType is returned when the given type is incompatible with the expected type.
|
||||
ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation.
|
||||
|
||||
)
|
||||
|
||||
// ErrUnsupportedPtrType is returned when the Struct field was a pointer but
|
||||
// the JSON value was of a different type
|
||||
type ErrUnsupportedPtrType struct {
|
||||
rf reflect.Value
|
||||
t reflect.Type
|
||||
structField reflect.StructField
|
||||
}
|
||||
|
||||
func (eupt ErrUnsupportedPtrType) Error() string {
|
||||
typeName := eupt.t.Elem().Name()
|
||||
kind := eupt.t.Elem().Kind()
|
||||
if kind.String() != "" && kind.String() != typeName {
|
||||
typeName = fmt.Sprintf("%s (%s)", typeName, kind.String())
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"jsonapi: Can't unmarshal %+v (%s) to struct field `%s`, which is a pointer to `%s`",
|
||||
eupt.rf, eupt.rf.Type().Kind(), eupt.structField.Name, typeName,
|
||||
)
|
||||
}
|
||||
|
||||
func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) error {
|
||||
return ErrUnsupportedPtrType{rf, t, structField}
|
||||
}
|
||||
|
||||
// UnmarshalPayload converts an io into a struct instance using jsonapi tags on
|
||||
// struct fields. This method supports single request payloads only, at the
|
||||
// moment. Bulk creates and updates are not supported yet.
|
||||
//
|
||||
// Will Unmarshal embedded and sideloaded payloads. The latter is only possible if the
|
||||
// object graph is complete. That is, in the "relationships" data there are type and id,
|
||||
// keys that correspond to records in the "included" array.
|
||||
//
|
||||
// For example you could pass it, in, req.Body and, model, a BlogPost
|
||||
// struct instance to populate in an http handler,
|
||||
//
|
||||
// func CreateBlog(w http.ResponseWriter, r *http.Request) {
|
||||
// blog := new(Blog)
|
||||
//
|
||||
// if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil {
|
||||
// http.Error(w, err.Error(), 500)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // ...do stuff with your blog...
|
||||
//
|
||||
// w.Header().Set("Content-Type", jsonapi.MediaType)
|
||||
// w.WriteHeader(201)
|
||||
//
|
||||
// if err := jsonapi.MarshalPayload(w, blog); err != nil {
|
||||
// http.Error(w, err.Error(), 500)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// Visit https://github.com/google/jsonapi#create for more info.
|
||||
//
|
||||
// model interface{} should be a pointer to a struct.
|
||||
func UnmarshalPayload(in io.Reader, model interface{}) error {
|
||||
payload := new(OnePayload)
|
||||
|
||||
if err := json.NewDecoder(in).Decode(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if payload.Included != nil {
|
||||
includedMap := make(map[string]*Node)
|
||||
for _, included := range payload.Included {
|
||||
key := fmt.Sprintf("%s,%s", included.Type, included.ID)
|
||||
includedMap[key] = included
|
||||
}
|
||||
|
||||
return unmarshalNode(payload.Data, reflect.ValueOf(model), &includedMap)
|
||||
}
|
||||
return unmarshalNode(payload.Data, reflect.ValueOf(model), nil)
|
||||
}
|
||||
|
||||
// UnmarshalManyPayload converts an io into a set of struct instances using
|
||||
// jsonapi tags on the type's struct fields.
|
||||
func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
|
||||
payload := new(ManyPayload)
|
||||
|
||||
if err := json.NewDecoder(in).Decode(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
models := []interface{}{} // will be populated from the "data"
|
||||
includedMap := map[string]*Node{} // will be populate from the "included"
|
||||
|
||||
if payload.Included != nil {
|
||||
for _, included := range payload.Included {
|
||||
key := fmt.Sprintf("%s,%s", included.Type, included.ID)
|
||||
includedMap[key] = included
|
||||
}
|
||||
}
|
||||
|
||||
for _, data := range payload.Data {
|
||||
model := reflect.New(t.Elem())
|
||||
err := unmarshalNode(data, model, &includedMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
models = append(models, model.Interface())
|
||||
}
|
||||
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("data is not a jsonapi representation of '%v'", model.Type())
|
||||
}
|
||||
}()
|
||||
|
||||
modelValue := model.Elem()
|
||||
modelType := model.Type().Elem()
|
||||
|
||||
var er error
|
||||
|
||||
for i := 0; i < modelValue.NumField(); i++ {
|
||||
fieldType := modelType.Field(i)
|
||||
tag := fieldType.Tag.Get("jsonapi")
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldValue := modelValue.Field(i)
|
||||
|
||||
args := strings.Split(tag, ",")
|
||||
if len(args) < 1 {
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
}
|
||||
|
||||
annotation := args[0]
|
||||
|
||||
if (annotation == annotationClientID && len(args) != 1) ||
|
||||
(annotation != annotationClientID && len(args) < 2) {
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
}
|
||||
|
||||
if annotation == annotationPrimary {
|
||||
if data.ID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check the JSON API Type
|
||||
if data.Type != args[1] {
|
||||
er = fmt.Errorf(
|
||||
"Trying to Unmarshal an object of type %#v, but %#v does not match",
|
||||
data.Type,
|
||||
args[1],
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
// ID will have to be transmitted as astring per the JSON API spec
|
||||
v := reflect.ValueOf(data.ID)
|
||||
|
||||
// Deal with PTRS
|
||||
var kind reflect.Kind
|
||||
if fieldValue.Kind() == reflect.Ptr {
|
||||
kind = fieldType.Type.Elem().Kind()
|
||||
} else {
|
||||
kind = fieldType.Type.Kind()
|
||||
}
|
||||
|
||||
// Handle String case
|
||||
if kind == reflect.String {
|
||||
assign(fieldValue, v)
|
||||
continue
|
||||
}
|
||||
|
||||
// Value was not a string... only other supported type was a numeric,
|
||||
// which would have been sent as a float value.
|
||||
floatValue, err := strconv.ParseFloat(data.ID, 64)
|
||||
if err != nil {
|
||||
// Could not convert the value in the "id" attr to a float
|
||||
er = ErrBadJSONAPIID
|
||||
break
|
||||
}
|
||||
|
||||
// Convert the numeric float to one of the supported ID numeric types
|
||||
// (int[8,16,32,64] or uint[8,16,32,64])
|
||||
var idValue reflect.Value
|
||||
switch kind {
|
||||
case reflect.Int:
|
||||
n := int(floatValue)
|
||||
idValue = reflect.ValueOf(&n)
|
||||
case reflect.Int8:
|
||||
n := int8(floatValue)
|
||||
idValue = reflect.ValueOf(&n)
|
||||
case reflect.Int16:
|
||||
n := int16(floatValue)
|
||||
idValue = reflect.ValueOf(&n)
|
||||
case reflect.Int32:
|
||||
n := int32(floatValue)
|
||||
idValue = reflect.ValueOf(&n)
|
||||
case reflect.Int64:
|
||||
n := int64(floatValue)
|
||||
idValue = reflect.ValueOf(&n)
|
||||
case reflect.Uint:
|
||||
n := uint(floatValue)
|
||||
idValue = reflect.ValueOf(&n)
|
||||
case reflect.Uint8:
|
||||
n := uint8(floatValue)
|
||||
idValue = reflect.ValueOf(&n)
|
||||
case reflect.Uint16:
|
||||
n := uint16(floatValue)
|
||||
idValue = reflect.ValueOf(&n)
|
||||
case reflect.Uint32:
|
||||
n := uint32(floatValue)
|
||||
idValue = reflect.ValueOf(&n)
|
||||
case reflect.Uint64:
|
||||
n := uint64(floatValue)
|
||||
idValue = reflect.ValueOf(&n)
|
||||
default:
|
||||
// We had a JSON float (numeric), but our field was not one of the
|
||||
// allowed numeric types
|
||||
er = ErrBadJSONAPIID
|
||||
break
|
||||
}
|
||||
|
||||
assign(fieldValue, idValue)
|
||||
} else if annotation == annotationClientID {
|
||||
if data.ClientID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldValue.Set(reflect.ValueOf(data.ClientID))
|
||||
} else if annotation == annotationAttribute {
|
||||
attributes := data.Attributes
|
||||
|
||||
if attributes == nil || len(data.Attributes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
attribute := attributes[args[1]]
|
||||
|
||||
// continue if the attribute was not included in the request
|
||||
if attribute == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
structField := fieldType
|
||||
value, err := unmarshalAttribute(attribute, args, structField, fieldValue)
|
||||
if err != nil {
|
||||
er = err
|
||||
break
|
||||
}
|
||||
|
||||
assign(fieldValue, value)
|
||||
continue
|
||||
|
||||
} else if annotation == annotationRelation {
|
||||
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
||||
|
||||
if data.Relationships == nil || data.Relationships[args[1]] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if isSlice {
|
||||
// to-many relationship
|
||||
relationship := new(RelationshipManyNode)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
json.NewEncoder(buf).Encode(data.Relationships[args[1]])
|
||||
json.NewDecoder(buf).Decode(relationship)
|
||||
|
||||
data := relationship.Data
|
||||
models := reflect.New(fieldValue.Type()).Elem()
|
||||
|
||||
for _, n := range data {
|
||||
m := reflect.New(fieldValue.Type().Elem().Elem())
|
||||
|
||||
if err := unmarshalNode(
|
||||
fullNode(n, included),
|
||||
m,
|
||||
included,
|
||||
); err != nil {
|
||||
er = err
|
||||
break
|
||||
}
|
||||
|
||||
models = reflect.Append(models, m)
|
||||
}
|
||||
|
||||
fieldValue.Set(models)
|
||||
} else {
|
||||
// to-one relationships
|
||||
relationship := new(RelationshipOneNode)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
json.NewEncoder(buf).Encode(
|
||||
data.Relationships[args[1]],
|
||||
)
|
||||
json.NewDecoder(buf).Decode(relationship)
|
||||
|
||||
/*
|
||||
http://jsonapi.org/format/#document-resource-object-relationships
|
||||
http://jsonapi.org/format/#document-resource-object-linkage
|
||||
relationship can have a data node set to null (e.g. to disassociate the relationship)
|
||||
so unmarshal and set fieldValue only if data obj is not null
|
||||
*/
|
||||
if relationship.Data == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
m := reflect.New(fieldValue.Type().Elem())
|
||||
if err := unmarshalNode(
|
||||
fullNode(relationship.Data, included),
|
||||
m,
|
||||
included,
|
||||
); err != nil {
|
||||
er = err
|
||||
break
|
||||
}
|
||||
|
||||
fieldValue.Set(m)
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
er = fmt.Errorf(unsuportedStructTagMsg, annotation)
|
||||
}
|
||||
}
|
||||
|
||||
return er
|
||||
}
|
||||
|
||||
func fullNode(n *Node, included *map[string]*Node) *Node {
|
||||
includedKey := fmt.Sprintf("%s,%s", n.Type, n.ID)
|
||||
|
||||
if included != nil && (*included)[includedKey] != nil {
|
||||
return (*included)[includedKey]
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// assign will take the value specified and assign it to the field; if
|
||||
// field is expecting a ptr assign will assign a ptr.
|
||||
func assign(field, value reflect.Value) {
|
||||
value = reflect.Indirect(value)
|
||||
|
||||
if field.Kind() == reflect.Ptr {
|
||||
// initialize pointer so it's value
|
||||
// can be set by assignValue
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
assignValue(field.Elem(), value)
|
||||
} else {
|
||||
assignValue(field, value)
|
||||
}
|
||||
}
|
||||
|
||||
// assign assigns the specified value to the field,
|
||||
// expecting both values not to be pointer types.
|
||||
func assignValue(field, value reflect.Value) {
|
||||
switch field.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16,
|
||||
reflect.Int32, reflect.Int64:
|
||||
field.SetInt(value.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16,
|
||||
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
field.SetUint(value.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
field.SetFloat(value.Float())
|
||||
case reflect.String:
|
||||
field.SetString(value.String())
|
||||
case reflect.Bool:
|
||||
field.SetBool(value.Bool())
|
||||
default:
|
||||
field.Set(value)
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalAttribute(
|
||||
attribute interface{},
|
||||
args []string,
|
||||
structField reflect.StructField,
|
||||
fieldValue reflect.Value) (value reflect.Value, err error) {
|
||||
value = reflect.ValueOf(attribute)
|
||||
fieldType := structField.Type
|
||||
|
||||
// Handle field of type []string
|
||||
if fieldValue.Type() == reflect.TypeOf([]string{}) {
|
||||
value, err = handleStringSlice(attribute, args, fieldType, fieldValue)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle field of type time.Time
|
||||
if fieldValue.Type() == reflect.TypeOf(time.Time{}) ||
|
||||
fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
||||
value, err = handleTime(attribute, args, fieldType, fieldValue)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle field of type struct
|
||||
if fieldValue.Type().Kind() == reflect.Struct {
|
||||
value, err = handleStruct(attribute, args, fieldType, fieldValue)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle field containing slice of structs
|
||||
if fieldValue.Type().Kind() == reflect.Slice {
|
||||
elem := reflect.TypeOf(fieldValue.Interface()).Elem()
|
||||
if elem.Kind() == reflect.Ptr {
|
||||
elem = elem.Elem()
|
||||
}
|
||||
|
||||
if elem.Kind() == reflect.Struct {
|
||||
value, err = handleStructSlice(attribute, args, fieldType, fieldValue)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// JSON value was a float (numeric)
|
||||
if value.Kind() == reflect.Float64 {
|
||||
value, err = handleNumeric(attribute, args, fieldType, fieldValue)
|
||||
return
|
||||
}
|
||||
|
||||
// Field was a Pointer type
|
||||
if fieldValue.Kind() == reflect.Ptr {
|
||||
value, err = handlePointer(attribute, args, fieldType, fieldValue, structField)
|
||||
return
|
||||
}
|
||||
|
||||
// As a final catch-all, ensure types line up to avoid a runtime panic.
|
||||
if fieldValue.Kind() != value.Kind() {
|
||||
err = ErrInvalidType
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handleStringSlice(
|
||||
attribute interface{},
|
||||
args []string,
|
||||
fieldType reflect.Type,
|
||||
fieldValue reflect.Value) (reflect.Value, error) {
|
||||
v := reflect.ValueOf(attribute)
|
||||
values := make([]string, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
values[i] = v.Index(i).Interface().(string)
|
||||
}
|
||||
|
||||
return reflect.ValueOf(values), nil
|
||||
}
|
||||
|
||||
func handleTime(
|
||||
attribute interface{},
|
||||
args []string,
|
||||
fieldType reflect.Type,
|
||||
fieldValue reflect.Value) (reflect.Value, error) {
|
||||
var isIso8601 bool
|
||||
v := reflect.ValueOf(attribute)
|
||||
|
||||
if len(args) > 2 {
|
||||
for _, arg := range args[2:] {
|
||||
if arg == annotationISO8601 {
|
||||
isIso8601 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isIso8601 {
|
||||
var tm string
|
||||
if v.Kind() == reflect.String {
|
||||
tm = v.Interface().(string)
|
||||
} else {
|
||||
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
|
||||
}
|
||||
|
||||
t, err := time.Parse(iso8601TimeFormat, tm)
|
||||
if err != nil {
|
||||
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
|
||||
}
|
||||
|
||||
if fieldValue.Kind() == reflect.Ptr {
|
||||
return reflect.ValueOf(&t), nil
|
||||
}
|
||||
|
||||
return reflect.ValueOf(t), nil
|
||||
}
|
||||
|
||||
var at int64
|
||||
|
||||
if v.Kind() == reflect.Float64 {
|
||||
at = int64(v.Interface().(float64))
|
||||
} else if v.Kind() == reflect.Int {
|
||||
at = v.Int()
|
||||
} else {
|
||||
return reflect.ValueOf(time.Now()), ErrInvalidTime
|
||||
}
|
||||
|
||||
t := time.Unix(at, 0)
|
||||
|
||||
return reflect.ValueOf(t), nil
|
||||
}
|
||||
|
||||
func handleNumeric(
|
||||
attribute interface{},
|
||||
args []string,
|
||||
fieldType reflect.Type,
|
||||
fieldValue reflect.Value) (reflect.Value, error) {
|
||||
v := reflect.ValueOf(attribute)
|
||||
floatValue := v.Interface().(float64)
|
||||
|
||||
var kind reflect.Kind
|
||||
if fieldValue.Kind() == reflect.Ptr {
|
||||
kind = fieldType.Elem().Kind()
|
||||
} else {
|
||||
kind = fieldType.Kind()
|
||||
}
|
||||
|
||||
var numericValue reflect.Value
|
||||
|
||||
switch kind {
|
||||
case reflect.Int:
|
||||
n := int(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Int8:
|
||||
n := int8(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Int16:
|
||||
n := int16(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Int32:
|
||||
n := int32(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Int64:
|
||||
n := int64(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Uint:
|
||||
n := uint(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Uint8:
|
||||
n := uint8(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Uint16:
|
||||
n := uint16(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Uint32:
|
||||
n := uint32(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Uint64:
|
||||
n := uint64(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Float32:
|
||||
n := float32(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Float64:
|
||||
n := floatValue
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
default:
|
||||
return reflect.Value{}, ErrUnknownFieldNumberType
|
||||
}
|
||||
|
||||
return numericValue, nil
|
||||
}
|
||||
|
||||
func handlePointer(
|
||||
attribute interface{},
|
||||
args []string,
|
||||
fieldType reflect.Type,
|
||||
fieldValue reflect.Value,
|
||||
structField reflect.StructField) (reflect.Value, error) {
|
||||
t := fieldValue.Type()
|
||||
var concreteVal reflect.Value
|
||||
|
||||
switch cVal := attribute.(type) {
|
||||
case string:
|
||||
concreteVal = reflect.ValueOf(&cVal)
|
||||
case bool:
|
||||
concreteVal = reflect.ValueOf(&cVal)
|
||||
case complex64, complex128, uintptr:
|
||||
concreteVal = reflect.ValueOf(&cVal)
|
||||
case map[string]interface{}:
|
||||
var err error
|
||||
concreteVal, err = handleStruct(attribute, args, fieldType, fieldValue)
|
||||
if err != nil {
|
||||
return reflect.Value{}, newErrUnsupportedPtrType(
|
||||
reflect.ValueOf(attribute), fieldType, structField)
|
||||
}
|
||||
return concreteVal.Elem(), err
|
||||
default:
|
||||
return reflect.Value{}, newErrUnsupportedPtrType(
|
||||
reflect.ValueOf(attribute), fieldType, structField)
|
||||
}
|
||||
|
||||
if t != concreteVal.Type() {
|
||||
return reflect.Value{}, newErrUnsupportedPtrType(
|
||||
reflect.ValueOf(attribute), fieldType, structField)
|
||||
}
|
||||
|
||||
return concreteVal, nil
|
||||
}
|
||||
|
||||
func handleStruct(
|
||||
attribute interface{},
|
||||
args []string,
|
||||
fieldType reflect.Type,
|
||||
fieldValue reflect.Value) (reflect.Value, error) {
|
||||
model := reflect.New(fieldValue.Type())
|
||||
|
||||
data, err := json.Marshal(attribute)
|
||||
if err != nil {
|
||||
return model, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, model.Interface())
|
||||
|
||||
if err != nil {
|
||||
return model, err
|
||||
}
|
||||
|
||||
return model, err
|
||||
}
|
||||
|
||||
func handleStructSlice(
|
||||
attribute interface{},
|
||||
args []string,
|
||||
fieldType reflect.Type,
|
||||
fieldValue reflect.Value) (reflect.Value, error) {
|
||||
models := reflect.New(fieldValue.Type()).Elem()
|
||||
dataMap := reflect.ValueOf(attribute).Interface().([]interface{})
|
||||
for _, data := range dataMap {
|
||||
model := reflect.New(fieldValue.Type().Elem()).Elem()
|
||||
modelType := model.Type()
|
||||
|
||||
value, err := handleStruct(data, []string{}, modelType, model)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
models = reflect.Append(models, reflect.Indirect(value))
|
||||
}
|
||||
|
||||
return models, nil
|
||||
}
|
|
@ -0,0 +1,539 @@
|
|||
package jsonapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrBadJSONAPIStructTag is returned when the Struct field's JSON API
|
||||
// annotation is invalid.
|
||||
ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format")
|
||||
// ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field
|
||||
// was not a valid numeric type.
|
||||
ErrBadJSONAPIID = errors.New(
|
||||
"id should be either string, int(8,16,32,64) or uint(8,16,32,64)")
|
||||
// ErrExpectedSlice is returned when a variable or argument was expected to
|
||||
// be a slice of *Structs; MarshalMany will return this error when its
|
||||
// interface{} argument is invalid.
|
||||
ErrExpectedSlice = errors.New("models should be a slice of struct pointers")
|
||||
// ErrUnexpectedType is returned when marshalling an interface; the interface
|
||||
// had to be a pointer or a slice; otherwise this error is returned.
|
||||
ErrUnexpectedType = errors.New("models should be a struct pointer or slice of struct pointers")
|
||||
)
|
||||
|
||||
// MarshalPayload writes a jsonapi response for one or many records. The
|
||||
// related records are sideloaded into the "included" array. If this method is
|
||||
// given a struct pointer as an argument it will serialize in the form
|
||||
// "data": {...}. If this method is given a slice of pointers, this method will
|
||||
// serialize in the form "data": [...]
|
||||
//
|
||||
// One Example: you could pass it, w, your http.ResponseWriter, and, models, a
|
||||
// ptr to a Blog to be written to the response body:
|
||||
//
|
||||
// func ShowBlog(w http.ResponseWriter, r *http.Request) {
|
||||
// blog := &Blog{}
|
||||
//
|
||||
// w.Header().Set("Content-Type", jsonapi.MediaType)
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
//
|
||||
// if err := jsonapi.MarshalPayload(w, blog); err != nil {
|
||||
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Many Example: you could pass it, w, your http.ResponseWriter, and, models, a
|
||||
// slice of Blog struct instance pointers to be written to the response body:
|
||||
//
|
||||
// func ListBlogs(w http.ResponseWriter, r *http.Request) {
|
||||
// blogs := []*Blog{}
|
||||
//
|
||||
// w.Header().Set("Content-Type", jsonapi.MediaType)
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
//
|
||||
// if err := jsonapi.MarshalPayload(w, blogs); err != nil {
|
||||
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
func MarshalPayload(w io.Writer, models interface{}) error {
|
||||
payload, err := Marshal(models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Marshal does the same as MarshalPayload except it just returns the payload
|
||||
// and doesn't write out results. Useful if you use your own JSON rendering
|
||||
// library.
|
||||
func Marshal(models interface{}) (Payloader, error) {
|
||||
switch vals := reflect.ValueOf(models); vals.Kind() {
|
||||
case reflect.Slice:
|
||||
m, err := convertToSliceInterface(&models)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload, err := marshalMany(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if linkableModels, isLinkable := models.(Linkable); isLinkable {
|
||||
jl := linkableModels.JSONAPILinks()
|
||||
if er := jl.validate(); er != nil {
|
||||
return nil, er
|
||||
}
|
||||
payload.Links = linkableModels.JSONAPILinks()
|
||||
}
|
||||
|
||||
if metableModels, ok := models.(Metable); ok {
|
||||
payload.Meta = metableModels.JSONAPIMeta()
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
case reflect.Ptr:
|
||||
// Check that the pointer was to a struct
|
||||
if reflect.Indirect(vals).Kind() != reflect.Struct {
|
||||
return nil, ErrUnexpectedType
|
||||
}
|
||||
return marshalOne(models)
|
||||
default:
|
||||
return nil, ErrUnexpectedType
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalPayloadWithoutIncluded writes a jsonapi response with one or many
|
||||
// records, without the related records sideloaded into "included" array.
|
||||
// If you want to serialize the relations into the "included" array see
|
||||
// MarshalPayload.
|
||||
//
|
||||
// models interface{} should be either a struct pointer or a slice of struct
|
||||
// pointers.
|
||||
func MarshalPayloadWithoutIncluded(w io.Writer, model interface{}) error {
|
||||
payload, err := Marshal(model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload.clearIncluded()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalOne does the same as MarshalOnePayload except it just returns the
|
||||
// payload and doesn't write out results. Useful is you use your JSON rendering
|
||||
// library.
|
||||
func marshalOne(model interface{}) (*OnePayload, error) {
|
||||
included := make(map[string]*Node)
|
||||
|
||||
rootNode, err := visitModelNode(model, &included, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := &OnePayload{Data: rootNode}
|
||||
|
||||
payload.Included = nodeMapValues(&included)
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// marshalMany does the same as MarshalManyPayload except it just returns the
|
||||
// payload and doesn't write out results. Useful is you use your JSON rendering
|
||||
// library.
|
||||
func marshalMany(models []interface{}) (*ManyPayload, error) {
|
||||
payload := &ManyPayload{
|
||||
Data: []*Node{},
|
||||
}
|
||||
included := map[string]*Node{}
|
||||
|
||||
for _, model := range models {
|
||||
node, err := visitModelNode(model, &included, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload.Data = append(payload.Data, node)
|
||||
}
|
||||
payload.Included = nodeMapValues(&included)
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// MarshalOnePayloadEmbedded - This method not meant to for use in
|
||||
// implementation code, although feel free. The purpose of this
|
||||
// method is for use in tests. In most cases, your request
|
||||
// payloads for create will be embedded rather than sideloaded for
|
||||
// related records. This method will serialize a single struct
|
||||
// pointer into an embedded json response. In other words, there
|
||||
// will be no, "included", array in the json all relationships will
|
||||
// be serailized inline in the data.
|
||||
//
|
||||
// However, in tests, you may want to construct payloads to post
|
||||
// to create methods that are embedded to most closely resemble
|
||||
// the payloads that will be produced by the client. This is what
|
||||
// this method is intended for.
|
||||
//
|
||||
// model interface{} should be a pointer to a struct.
|
||||
func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
|
||||
rootNode, err := visitModelNode(model, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := &OnePayload{Data: rootNode}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func visitModelNode(model interface{}, included *map[string]*Node,
|
||||
sideload bool) (*Node, error) {
|
||||
node := new(Node)
|
||||
|
||||
var er error
|
||||
value := reflect.ValueOf(model)
|
||||
if value.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
modelValue := value.Elem()
|
||||
modelType := value.Type().Elem()
|
||||
|
||||
for i := 0; i < modelValue.NumField(); i++ {
|
||||
structField := modelValue.Type().Field(i)
|
||||
tag := structField.Tag.Get(annotationJSONAPI)
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldValue := modelValue.Field(i)
|
||||
fieldType := modelType.Field(i)
|
||||
|
||||
args := strings.Split(tag, annotationSeperator)
|
||||
|
||||
if len(args) < 1 {
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
}
|
||||
|
||||
annotation := args[0]
|
||||
|
||||
if (annotation == annotationClientID && len(args) != 1) ||
|
||||
(annotation != annotationClientID && len(args) < 2) {
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
}
|
||||
|
||||
if annotation == annotationPrimary {
|
||||
v := fieldValue
|
||||
|
||||
// Deal with PTRS
|
||||
var kind reflect.Kind
|
||||
if fieldValue.Kind() == reflect.Ptr {
|
||||
kind = fieldType.Type.Elem().Kind()
|
||||
v = reflect.Indirect(fieldValue)
|
||||
} else {
|
||||
kind = fieldType.Type.Kind()
|
||||
}
|
||||
|
||||
// Handle allowed types
|
||||
switch kind {
|
||||
case reflect.String:
|
||||
node.ID = v.Interface().(string)
|
||||
case reflect.Int:
|
||||
node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10)
|
||||
case reflect.Int8:
|
||||
node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10)
|
||||
case reflect.Int16:
|
||||
node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10)
|
||||
case reflect.Int32:
|
||||
node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10)
|
||||
case reflect.Int64:
|
||||
node.ID = strconv.FormatInt(v.Interface().(int64), 10)
|
||||
case reflect.Uint:
|
||||
node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10)
|
||||
case reflect.Uint8:
|
||||
node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10)
|
||||
case reflect.Uint16:
|
||||
node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10)
|
||||
case reflect.Uint32:
|
||||
node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10)
|
||||
case reflect.Uint64:
|
||||
node.ID = strconv.FormatUint(v.Interface().(uint64), 10)
|
||||
default:
|
||||
// We had a JSON float (numeric), but our field was not one of the
|
||||
// allowed numeric types
|
||||
er = ErrBadJSONAPIID
|
||||
break
|
||||
}
|
||||
|
||||
node.Type = args[1]
|
||||
} else if annotation == annotationClientID {
|
||||
clientID := fieldValue.String()
|
||||
if clientID != "" {
|
||||
node.ClientID = clientID
|
||||
}
|
||||
} else if annotation == annotationAttribute {
|
||||
var omitEmpty, iso8601 bool
|
||||
|
||||
if len(args) > 2 {
|
||||
for _, arg := range args[2:] {
|
||||
switch arg {
|
||||
case annotationOmitEmpty:
|
||||
omitEmpty = true
|
||||
case annotationISO8601:
|
||||
iso8601 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if node.Attributes == nil {
|
||||
node.Attributes = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
|
||||
t := fieldValue.Interface().(time.Time)
|
||||
|
||||
if t.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
if iso8601 {
|
||||
node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat)
|
||||
} else {
|
||||
node.Attributes[args[1]] = t.Unix()
|
||||
}
|
||||
} else if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
||||
// A time pointer may be nil
|
||||
if fieldValue.IsNil() {
|
||||
if omitEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
node.Attributes[args[1]] = nil
|
||||
} else {
|
||||
tm := fieldValue.Interface().(*time.Time)
|
||||
|
||||
if tm.IsZero() && omitEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
if iso8601 {
|
||||
node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat)
|
||||
} else {
|
||||
node.Attributes[args[1]] = tm.Unix()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Dealing with a fieldValue that is not a time
|
||||
emptyValue := reflect.Zero(fieldValue.Type())
|
||||
|
||||
// See if we need to omit this field
|
||||
if omitEmpty && reflect.DeepEqual(fieldValue.Interface(), emptyValue.Interface()) {
|
||||
continue
|
||||
}
|
||||
|
||||
strAttr, ok := fieldValue.Interface().(string)
|
||||
if ok {
|
||||
node.Attributes[args[1]] = strAttr
|
||||
} else {
|
||||
node.Attributes[args[1]] = fieldValue.Interface()
|
||||
}
|
||||
}
|
||||
} else if annotation == annotationRelation {
|
||||
var omitEmpty bool
|
||||
|
||||
//add support for 'omitempty' struct tag for marshaling as absent
|
||||
if len(args) > 2 {
|
||||
omitEmpty = args[2] == annotationOmitEmpty
|
||||
}
|
||||
|
||||
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
||||
if omitEmpty &&
|
||||
(isSlice && fieldValue.Len() < 1 ||
|
||||
(!isSlice && fieldValue.IsNil())) {
|
||||
continue
|
||||
}
|
||||
|
||||
if node.Relationships == nil {
|
||||
node.Relationships = make(map[string]interface{})
|
||||
}
|
||||
|
||||
var relLinks *Links
|
||||
if linkableModel, ok := model.(RelationshipLinkable); ok {
|
||||
relLinks = linkableModel.JSONAPIRelationshipLinks(args[1])
|
||||
}
|
||||
|
||||
var relMeta *Meta
|
||||
if metableModel, ok := model.(RelationshipMetable); ok {
|
||||
relMeta = metableModel.JSONAPIRelationshipMeta(args[1])
|
||||
}
|
||||
|
||||
if isSlice {
|
||||
// to-many relationship
|
||||
relationship, err := visitModelNodeRelationships(
|
||||
fieldValue,
|
||||
included,
|
||||
sideload,
|
||||
)
|
||||
if err != nil {
|
||||
er = err
|
||||
break
|
||||
}
|
||||
relationship.Links = relLinks
|
||||
relationship.Meta = relMeta
|
||||
|
||||
if sideload {
|
||||
shallowNodes := []*Node{}
|
||||
for _, n := range relationship.Data {
|
||||
appendIncluded(included, n)
|
||||
shallowNodes = append(shallowNodes, toShallowNode(n))
|
||||
}
|
||||
|
||||
node.Relationships[args[1]] = &RelationshipManyNode{
|
||||
Data: shallowNodes,
|
||||
Links: relationship.Links,
|
||||
Meta: relationship.Meta,
|
||||
}
|
||||
} else {
|
||||
node.Relationships[args[1]] = relationship
|
||||
}
|
||||
} else {
|
||||
// to-one relationships
|
||||
|
||||
// Handle null relationship case
|
||||
if fieldValue.IsNil() {
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{Data: nil}
|
||||
continue
|
||||
}
|
||||
|
||||
relationship, err := visitModelNode(
|
||||
fieldValue.Interface(),
|
||||
included,
|
||||
sideload,
|
||||
)
|
||||
if err != nil {
|
||||
er = err
|
||||
break
|
||||
}
|
||||
|
||||
if sideload {
|
||||
appendIncluded(included, relationship)
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||
Data: toShallowNode(relationship),
|
||||
Links: relLinks,
|
||||
Meta: relMeta,
|
||||
}
|
||||
} else {
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||
Data: relationship,
|
||||
Links: relLinks,
|
||||
Meta: relMeta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if er != nil {
|
||||
return nil, er
|
||||
}
|
||||
|
||||
if linkableModel, isLinkable := model.(Linkable); isLinkable {
|
||||
jl := linkableModel.JSONAPILinks()
|
||||
if er := jl.validate(); er != nil {
|
||||
return nil, er
|
||||
}
|
||||
node.Links = linkableModel.JSONAPILinks()
|
||||
}
|
||||
|
||||
if metableModel, ok := model.(Metable); ok {
|
||||
node.Meta = metableModel.JSONAPIMeta()
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func toShallowNode(node *Node) *Node {
|
||||
return &Node{
|
||||
ID: node.ID,
|
||||
Type: node.Type,
|
||||
}
|
||||
}
|
||||
|
||||
func visitModelNodeRelationships(models reflect.Value, included *map[string]*Node,
|
||||
sideload bool) (*RelationshipManyNode, error) {
|
||||
nodes := []*Node{}
|
||||
|
||||
for i := 0; i < models.Len(); i++ {
|
||||
n := models.Index(i).Interface()
|
||||
|
||||
node, err := visitModelNode(n, included, sideload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
|
||||
return &RelationshipManyNode{Data: nodes}, nil
|
||||
}
|
||||
|
||||
func appendIncluded(m *map[string]*Node, nodes ...*Node) {
|
||||
included := *m
|
||||
|
||||
for _, n := range nodes {
|
||||
k := fmt.Sprintf("%s,%s", n.Type, n.ID)
|
||||
|
||||
if _, hasNode := included[k]; hasNode {
|
||||
continue
|
||||
}
|
||||
|
||||
included[k] = n
|
||||
}
|
||||
}
|
||||
|
||||
func nodeMapValues(m *map[string]*Node) []*Node {
|
||||
mp := *m
|
||||
nodes := make([]*Node, len(mp))
|
||||
|
||||
i := 0
|
||||
for _, n := range mp {
|
||||
nodes[i] = n
|
||||
i++
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
func convertToSliceInterface(i *interface{}) ([]interface{}, error) {
|
||||
vals := reflect.ValueOf(*i)
|
||||
if vals.Kind() != reflect.Slice {
|
||||
return nil, ErrExpectedSlice
|
||||
}
|
||||
var response []interface{}
|
||||
for x := 0; x < vals.Len(); x++ {
|
||||
response = append(response, vals.Index(x).Interface())
|
||||
}
|
||||
return response, nil
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package jsonapi
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Event int
|
||||
|
||||
const (
|
||||
UnmarshalStart Event = iota
|
||||
UnmarshalStop
|
||||
MarshalStart
|
||||
MarshalStop
|
||||
)
|
||||
|
||||
type Runtime struct {
|
||||
ctx map[string]interface{}
|
||||
}
|
||||
|
||||
type Events func(*Runtime, Event, string, time.Duration)
|
||||
|
||||
var Instrumentation Events
|
||||
|
||||
func NewRuntime() *Runtime { return &Runtime{make(map[string]interface{})} }
|
||||
|
||||
func (r *Runtime) WithValue(key string, value interface{}) *Runtime {
|
||||
r.ctx[key] = value
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Runtime) Value(key string) interface{} {
|
||||
return r.ctx[key]
|
||||
}
|
||||
|
||||
func (r *Runtime) Instrument(key string) *Runtime {
|
||||
return r.WithValue("instrument", key)
|
||||
}
|
||||
|
||||
func (r *Runtime) shouldInstrument() bool {
|
||||
return Instrumentation != nil
|
||||
}
|
||||
|
||||
func (r *Runtime) UnmarshalPayload(reader io.Reader, model interface{}) error {
|
||||
return r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error {
|
||||
return UnmarshalPayload(reader, model)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Runtime) UnmarshalManyPayload(reader io.Reader, kind reflect.Type) (elems []interface{}, err error) {
|
||||
r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error {
|
||||
elems, err = UnmarshalManyPayload(reader, kind)
|
||||
return err
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Runtime) MarshalPayload(w io.Writer, model interface{}) error {
|
||||
return r.instrumentCall(MarshalStart, MarshalStop, func() error {
|
||||
return MarshalPayload(w, model)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Runtime) instrumentCall(start Event, stop Event, c func() error) error {
|
||||
if !r.shouldInstrument() {
|
||||
return c()
|
||||
}
|
||||
|
||||
instrumentationGUID, err := newUUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
begin := time.Now()
|
||||
Instrumentation(r, start, instrumentationGUID, time.Duration(0))
|
||||
|
||||
if err := c(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
diff := time.Duration(time.Now().UnixNano() - begin.UnixNano())
|
||||
Instrumentation(r, stop, instrumentationGUID, diff)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// citation: http://play.golang.org/p/4FkNSiUDMg
|
||||
func newUUID() (string, error) {
|
||||
uuid := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, uuid); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// variant bits; see section 4.1.1
|
||||
uuid[8] = uuid[8]&^0xc0 | 0x80
|
||||
// version 4 (pseudo-random); see section 4.1.3
|
||||
uuid[6] = uuid[6]&^0xf0 | 0x40
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
|
||||
}
|
|
@ -497,11 +497,15 @@ github.com/hashicorp/go-raftchunking/types
|
|||
github.com/hashicorp/go-retryablehttp
|
||||
# github.com/hashicorp/go-rootcerts v1.0.2
|
||||
github.com/hashicorp/go-rootcerts
|
||||
# github.com/hashicorp/go-slug v0.4.1
|
||||
github.com/hashicorp/go-slug
|
||||
# github.com/hashicorp/go-sockaddr v1.0.2
|
||||
github.com/hashicorp/go-sockaddr
|
||||
github.com/hashicorp/go-sockaddr/template
|
||||
# github.com/hashicorp/go-syslog v1.0.0
|
||||
github.com/hashicorp/go-syslog
|
||||
# github.com/hashicorp/go-tfe v0.12.0
|
||||
github.com/hashicorp/go-tfe
|
||||
# github.com/hashicorp/go-uuid v1.0.2
|
||||
github.com/hashicorp/go-uuid
|
||||
# github.com/hashicorp/go-version v1.2.1
|
||||
|
@ -590,6 +594,8 @@ github.com/hashicorp/vault-plugin-secrets-mongodbatlas
|
|||
# github.com/hashicorp/vault-plugin-secrets-openldap v0.1.6-0.20210201204049-4f0f91977798
|
||||
github.com/hashicorp/vault-plugin-secrets-openldap
|
||||
github.com/hashicorp/vault-plugin-secrets-openldap/client
|
||||
# github.com/hashicorp/vault-plugin-secrets-terraform v0.1.0
|
||||
github.com/hashicorp/vault-plugin-secrets-terraform
|
||||
# github.com/hashicorp/vault/api v1.0.5-0.20201001211907-38d91b749c77 => ./api
|
||||
github.com/hashicorp/vault/api
|
||||
# github.com/hashicorp/vault/sdk v0.1.14-0.20210127185906-6b455835fa8c => ./sdk
|
||||
|
@ -976,6 +982,8 @@ github.com/stretchr/objx
|
|||
github.com/stretchr/testify/assert
|
||||
github.com/stretchr/testify/mock
|
||||
github.com/stretchr/testify/require
|
||||
# github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d
|
||||
github.com/svanharmelen/jsonapi
|
||||
# github.com/tencentcloud/tencentcloud-sdk-go v3.0.171+incompatible
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors
|
||||
|
|
Loading…
Reference in New Issue