diff --git a/changelog/10931.txt b/changelog/10931.txt new file mode 100644 index 000000000..e51642b9d --- /dev/null +++ b/changelog/10931.txt @@ -0,0 +1,3 @@ +```release-note:feature +secrets/terraform: New secret engine for managing Terraform Cloud API tokens +``` \ No newline at end of file diff --git a/command/base_predict_test.go b/command/base_predict_test.go index 8401fbe22..20a3b4459 100644 --- a/command/base_predict_test.go +++ b/command/base_predict_test.go @@ -392,6 +392,7 @@ func TestPredict_Plugins(t *testing.T) { "redshift-database-plugin", "snowflake-database-plugin", "ssh", + "terraform", "totp", "transform", "transit", diff --git a/go.mod b/go.mod index c9881a4ad..beede086d 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 42257ccfa..2599f19c2 100644 --- a/go.sum +++ b/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= diff --git a/helper/builtinplugins/registry.go b/helper/builtinplugins/registry.go index 7b3d77389..325658654 100644 --- a/helper/builtinplugins/registry.go +++ b/helper/builtinplugins/registry.go @@ -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, }, diff --git a/scripts/gen_openapi.sh b/scripts/gen_openapi.sh index 1a85e4979..74946bf69 100755 --- a/scripts/gen_openapi.sh +++ b/scripts/gen_openapi.sh @@ -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 diff --git a/vendor/github.com/hashicorp/go-slug/.gitignore b/vendor/github.com/hashicorp/go-slug/.gitignore new file mode 100644 index 000000000..f1c181ec9 --- /dev/null +++ b/vendor/github.com/hashicorp/go-slug/.gitignore @@ -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 diff --git a/vendor/github.com/hashicorp/go-slug/LICENSE b/vendor/github.com/hashicorp/go-slug/LICENSE new file mode 100644 index 000000000..a612ad981 --- /dev/null +++ b/vendor/github.com/hashicorp/go-slug/LICENSE @@ -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. diff --git a/vendor/github.com/hashicorp/go-slug/README.md b/vendor/github.com/hashicorp/go-slug/README.md new file mode 100644 index 000000000..5c9b7c584 --- /dev/null +++ b/vendor/github.com/hashicorp/go-slug/README.md @@ -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. diff --git a/vendor/github.com/hashicorp/go-slug/go.mod b/vendor/github.com/hashicorp/go-slug/go.mod new file mode 100644 index 000000000..49ed430a4 --- /dev/null +++ b/vendor/github.com/hashicorp/go-slug/go.mod @@ -0,0 +1,3 @@ +module github.com/hashicorp/go-slug + +go 1.12 diff --git a/vendor/github.com/hashicorp/go-slug/slug.go b/vendor/github.com/hashicorp/go-slug/slug.go new file mode 100644 index 000000000..059d26773 --- /dev/null +++ b/vendor/github.com/hashicorp/go-slug/slug.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-slug/terraformignore.go b/vendor/github.com/hashicorp/go-slug/terraformignore.go new file mode 100644 index 000000000..ac7486934 --- /dev/null +++ b/vendor/github.com/hashicorp/go-slug/terraformignore.go @@ -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...) + } + } +} diff --git a/vendor/github.com/hashicorp/go-tfe/.go-version b/vendor/github.com/hashicorp/go-tfe/.go-version new file mode 100644 index 000000000..9beda55f8 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/.go-version @@ -0,0 +1 @@ +1.14.10 diff --git a/vendor/github.com/hashicorp/go-tfe/LICENSE b/vendor/github.com/hashicorp/go-tfe/LICENSE new file mode 100644 index 000000000..c33dcc7c9 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/LICENSE @@ -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. + diff --git a/vendor/github.com/hashicorp/go-tfe/README.md b/vendor/github.com/hashicorp/go-tfe/README.md new file mode 100644 index 000000000..9a8e70c9a --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/README.md @@ -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. + diff --git a/vendor/github.com/hashicorp/go-tfe/TESTS.md b/vendor/github.com/hashicorp/go-tfe/TESTS.md new file mode 100644 index 000000000..581498476 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/TESTS.md @@ -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--` +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 ./... +``` + diff --git a/vendor/github.com/hashicorp/go-tfe/agent_pool.go b/vendor/github.com/hashicorp/go-tfe/agent_pool.go new file mode 100644 index 000000000..6ee141643 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/agent_pool.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/agent_token.go b/vendor/github.com/hashicorp/go-tfe/agent_token.go new file mode 100644 index 000000000..be806f59e --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/agent_token.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/apply.go b/vendor/github.com/hashicorp/go-tfe/apply.go new file mode 100644 index 000000000..2c92896bd --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/apply.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/configuration_version.go b/vendor/github.com/hashicorp/go-tfe/configuration_version.go new file mode 100644 index 000000000..9a88b2d48 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/configuration_version.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/cost_estimate.go b/vendor/github.com/hashicorp/go-tfe/cost_estimate.go new file mode 100644 index 000000000..c7f33493f --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/cost_estimate.go @@ -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 + } +} diff --git a/vendor/github.com/hashicorp/go-tfe/go.mod b/vendor/github.com/hashicorp/go-tfe/go.mod new file mode 100644 index 000000000..5b196ce42 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/go.mod @@ -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 diff --git a/vendor/github.com/hashicorp/go-tfe/go.sum b/vendor/github.com/hashicorp/go-tfe/go.sum new file mode 100644 index 000000000..74c1e9523 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/go.sum @@ -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= diff --git a/vendor/github.com/hashicorp/go-tfe/ip_ranges.go b/vendor/github.com/hashicorp/go-tfe/ip_ranges.go new file mode 100644 index 000000000..a268faf41 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/ip_ranges.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/logreader.go b/vendor/github.com/hashicorp/go-tfe/logreader.go new file mode 100644 index 000000000..aee4472fe --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/logreader.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/notification_configuration.go b/vendor/github.com/hashicorp/go-tfe/notification_configuration.go new file mode 100644 index 000000000..ec33a51fd --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/notification_configuration.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/oauth_client.go b/vendor/github.com/hashicorp/go-tfe/oauth_client.go new file mode 100644 index 000000000..3a3c3168e --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/oauth_client.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/oauth_token.go b/vendor/github.com/hashicorp/go-tfe/oauth_token.go new file mode 100644 index 000000000..73ab48010 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/oauth_token.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/organization.go b/vendor/github.com/hashicorp/go-tfe/organization.go new file mode 100644 index 000000000..729cdbb27 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/organization.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/organization_membership.go b/vendor/github.com/hashicorp/go-tfe/organization_membership.go new file mode 100644 index 000000000..2806f929e --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/organization_membership.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/organization_token.go b/vendor/github.com/hashicorp/go-tfe/organization_token.go new file mode 100644 index 000000000..e2b0e0dfa --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/organization_token.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/plan.go b/vendor/github.com/hashicorp/go-tfe/plan.go new file mode 100644 index 000000000..328380f45 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/plan.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/plan_export.go b/vendor/github.com/hashicorp/go-tfe/plan_export.go new file mode 100644 index 000000000..cf9e9ced8 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/plan_export.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/policy.go b/vendor/github.com/hashicorp/go-tfe/policy.go new file mode 100644 index 000000000..b558b2608 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/policy.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/policy_check.go b/vendor/github.com/hashicorp/go-tfe/policy_check.go new file mode 100644 index 000000000..c63ecc6b4 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/policy_check.go @@ -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 + } +} diff --git a/vendor/github.com/hashicorp/go-tfe/policy_set.go b/vendor/github.com/hashicorp/go-tfe/policy_set.go new file mode 100644 index 000000000..56a31e307 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/policy_set.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/policy_set_parameter.go b/vendor/github.com/hashicorp/go-tfe/policy_set_parameter.go new file mode 100644 index 000000000..6e2587b90 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/policy_set_parameter.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/registry_module.go b/vendor/github.com/hashicorp/go-tfe/registry_module.go new file mode 100644 index 000000000..1cba103cf --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/registry_module.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/run.go b/vendor/github.com/hashicorp/go-tfe/run.go new file mode 100644 index 000000000..f31c9f944 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/run.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/run_trigger.go b/vendor/github.com/hashicorp/go-tfe/run_trigger.go new file mode 100644 index 000000000..a4c6f4f65 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/run_trigger.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/ssh_key.go b/vendor/github.com/hashicorp/go-tfe/ssh_key.go new file mode 100644 index 000000000..8735d0717 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/ssh_key.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/state_version.go b/vendor/github.com/hashicorp/go-tfe/state_version.go new file mode 100644 index 000000000..bf95010a8 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/state_version.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/state_version_output.go b/vendor/github.com/hashicorp/go-tfe/state_version_output.go new file mode 100644 index 000000000..2937b2e25 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/state_version_output.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/team.go b/vendor/github.com/hashicorp/go-tfe/team.go new file mode 100644 index 000000000..91d7435da --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/team.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/team_access.go b/vendor/github.com/hashicorp/go-tfe/team_access.go new file mode 100644 index 000000000..c04301111 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/team_access.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/team_member.go b/vendor/github.com/hashicorp/go-tfe/team_member.go new file mode 100644 index 000000000..450e57472 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/team_member.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/team_token.go b/vendor/github.com/hashicorp/go-tfe/team_token.go new file mode 100644 index 000000000..6763b6604 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/team_token.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/testing.go b/vendor/github.com/hashicorp/go-tfe/testing.go new file mode 100644 index 000000000..79f9c218a --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/testing.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/tfe.go b/vendor/github.com/hashicorp/go-tfe/tfe.go new file mode 100644 index 000000000..27330e7f1 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/tfe.go @@ -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")) +} diff --git a/vendor/github.com/hashicorp/go-tfe/type_helpers.go b/vendor/github.com/hashicorp/go-tfe/type_helpers.go new file mode 100644 index 000000000..6b38125b1 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/type_helpers.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/user.go b/vendor/github.com/hashicorp/go-tfe/user.go new file mode 100644 index 000000000..f0ca28ee3 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/user.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/user_token.go b/vendor/github.com/hashicorp/go-tfe/user_token.go new file mode 100644 index 000000000..5025313f0 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/user_token.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/validations.go b/vendor/github.com/hashicorp/go-tfe/validations.go new file mode 100644 index 000000000..38d95a681 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/validations.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/variable.go b/vendor/github.com/hashicorp/go-tfe/variable.go new file mode 100644 index 000000000..6ac8b5969 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/variable.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/workspace.go b/vendor/github.com/hashicorp/go-tfe/workspace.go new file mode 100644 index 000000000..59b74ed1c --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/workspace.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/.gitignore b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/.gitignore new file mode 100644 index 000000000..0abc3b529 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/.gitignore @@ -0,0 +1,6 @@ +vault/plugins +dist/ +*.test +pkg +bin +tmp diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/.goreleaser.yml b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/.goreleaser.yml new file mode 100644 index 000000000..a717b1b32 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/.goreleaser.yml @@ -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:' diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/LICENSE b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/LICENSE new file mode 100644 index 000000000..f0e5c79e1 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/LICENSE @@ -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. \ No newline at end of file diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/Makefile b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/Makefile new file mode 100644 index 000000000..faee2ceed --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/Makefile @@ -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 diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/README.md b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/README.md new file mode 100644 index 000000000..75a928fbb --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/README.md @@ -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= \ + 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. diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/backend.go b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/backend.go new file mode 100644 index 000000000..4429ec834 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/backend.go @@ -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. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/client.go b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/client.go new file mode 100644 index 000000000..392339e14 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/client.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/go.mod b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/go.mod new file mode 100644 index 000000000..6972e753b --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/go.mod @@ -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 +) diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/go.sum b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/go.sum new file mode 100644 index 000000000..361c45c81 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/go.sum @@ -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= diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_config.go b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_config.go new file mode 100644 index 000000000..5889c42dd --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_config.go @@ -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. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_credentials.go b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_credentials.go new file mode 100644 index 000000000..b68cd670b --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_credentials.go @@ -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. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_roles.go b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_roles.go new file mode 100644 index 000000000..cb600ffe1 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_roles.go @@ -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.` +) diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_rotate_role.go b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_rotate_role.go new file mode 100644 index 000000000..72732a937 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/path_rotate_role.go @@ -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. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/terraform_token.go b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/terraform_token.go new file mode 100644 index 000000000..f5f2d9b4c --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-terraform/terraform_token.go @@ -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 +} diff --git a/vendor/github.com/svanharmelen/jsonapi/.gitignore b/vendor/github.com/svanharmelen/jsonapi/.gitignore new file mode 100644 index 000000000..19b1e1cf0 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/.gitignore @@ -0,0 +1 @@ +/examples/examples diff --git a/vendor/github.com/svanharmelen/jsonapi/.travis.yml b/vendor/github.com/svanharmelen/jsonapi/.travis.yml new file mode 100644 index 000000000..d07c5f5f5 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/.travis.yml @@ -0,0 +1,7 @@ +language: go +go: + - 1.8.x + - 1.9.x + - 1.10.x + - tip +script: go test ./... -v diff --git a/vendor/github.com/svanharmelen/jsonapi/LICENSE b/vendor/github.com/svanharmelen/jsonapi/LICENSE new file mode 100644 index 000000000..c97912cef --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/LICENSE @@ -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. diff --git a/vendor/github.com/svanharmelen/jsonapi/README.md b/vendor/github.com/svanharmelen/jsonapi/README.md new file mode 100644 index 000000000..44b054181 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/README.md @@ -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,"` +``` + +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,,"` +``` + +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,,"` +``` + +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*. diff --git a/vendor/github.com/svanharmelen/jsonapi/constants.go b/vendor/github.com/svanharmelen/jsonapi/constants.go new file mode 100644 index 000000000..23288d311 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/constants.go @@ -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]" +) diff --git a/vendor/github.com/svanharmelen/jsonapi/doc.go b/vendor/github.com/svanharmelen/jsonapi/doc.go new file mode 100644 index 000000000..29d7a14ba --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/doc.go @@ -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," + +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,[,]" + +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," + +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 diff --git a/vendor/github.com/svanharmelen/jsonapi/errors.go b/vendor/github.com/svanharmelen/jsonapi/errors.go new file mode 100644 index 000000000..ed7fa9f75 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/errors.go @@ -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) +} diff --git a/vendor/github.com/svanharmelen/jsonapi/node.go b/vendor/github.com/svanharmelen/jsonapi/node.go new file mode 100644 index 000000000..a58488c82 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/node.go @@ -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 +} diff --git a/vendor/github.com/svanharmelen/jsonapi/request.go b/vendor/github.com/svanharmelen/jsonapi/request.go new file mode 100644 index 000000000..e3543428a --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/request.go @@ -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 +} diff --git a/vendor/github.com/svanharmelen/jsonapi/response.go b/vendor/github.com/svanharmelen/jsonapi/response.go new file mode 100644 index 000000000..e8e85fa42 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/response.go @@ -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 +} diff --git a/vendor/github.com/svanharmelen/jsonapi/runtime.go b/vendor/github.com/svanharmelen/jsonapi/runtime.go new file mode 100644 index 000000000..7dc658155 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/runtime.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4d4cb036a..fd8ff63e3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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