From e0d011cd51ecfb633df3d012e9d37db066d08dc0 Mon Sep 17 00:00:00 2001 From: Alex Pilon Date: Wed, 30 Oct 2019 18:01:15 -0400 Subject: [PATCH 1/2] Merge pull request #23063 from hashicorp/svchost-migration Replace svchost package with hashicorp/terraform-svchost --- go.mod | 12 +- go.sum | 23 +- internal/configs/configload/loader.go | 2 +- internal/configs/configload/module_mgr.go | 2 +- internal/plugin/discovery/get.go | 2 +- internal/plugin/discovery/get_test.go | 7 +- internal/registry/client.go | 11 +- internal/registry/client_test.go | 5 +- internal/registry/errors.go | 2 +- internal/registry/regsrc/friendly_host.go | 2 +- internal/registry/regsrc/module.go | 2 +- .../registry/regsrc/terraform_provider.go | 2 +- internal/registry/test/mock_registry.go | 9 +- internal/svchost/auth/cache.go | 45 --- internal/svchost/auth/credentials.go | 63 ---- internal/svchost/auth/from_map.go | 18 - internal/svchost/auth/helper_program.go | 80 ---- internal/svchost/auth/helper_program_test.go | 59 --- internal/svchost/auth/static.go | 28 -- internal/svchost/auth/static_test.go | 38 -- internal/svchost/auth/testdata/.gitignore | 1 - internal/svchost/auth/testdata/main.go | 39 -- internal/svchost/auth/testdata/test-helper | 7 - internal/svchost/auth/token_credentials.go | 25 -- .../svchost/auth/token_credentials_test.go | 18 - internal/svchost/disco/disco.go | 259 ------------- internal/svchost/disco/disco_test.go | 357 ------------------ internal/svchost/disco/host.go | 264 ------------- internal/svchost/disco/host_test.go | 290 -------------- internal/svchost/label_iter.go | 49 --- internal/svchost/svchost.go | 207 ---------- internal/svchost/svchost_test.go | 218 ----------- 32 files changed, 56 insertions(+), 2090 deletions(-) delete mode 100644 internal/svchost/auth/cache.go delete mode 100644 internal/svchost/auth/credentials.go delete mode 100644 internal/svchost/auth/from_map.go delete mode 100644 internal/svchost/auth/helper_program.go delete mode 100644 internal/svchost/auth/helper_program_test.go delete mode 100644 internal/svchost/auth/static.go delete mode 100644 internal/svchost/auth/static_test.go delete mode 100644 internal/svchost/auth/testdata/.gitignore delete mode 100644 internal/svchost/auth/testdata/main.go delete mode 100755 internal/svchost/auth/testdata/test-helper delete mode 100644 internal/svchost/auth/token_credentials.go delete mode 100644 internal/svchost/auth/token_credentials_test.go delete mode 100644 internal/svchost/disco/disco.go delete mode 100644 internal/svchost/disco/disco_test.go delete mode 100644 internal/svchost/disco/host.go delete mode 100644 internal/svchost/disco/host_test.go delete mode 100644 internal/svchost/label_iter.go delete mode 100644 internal/svchost/svchost.go delete mode 100644 internal/svchost/svchost_test.go diff --git a/go.mod b/go.mod index b94cae4c3fa..5a4e6b2daf5 100644 --- a/go.mod +++ b/go.mod @@ -6,15 +6,15 @@ require ( github.com/agext/levenshtein v1.2.2 github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/apparentlymart/go-cidr v1.0.1 - github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 + github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 github.com/armon/go-radix v1.0.0 // indirect - github.com/aws/aws-sdk-go v1.19.39 // indirect + github.com/aws/aws-sdk-go v1.25.3 // indirect github.com/davecgh/go-spew v1.1.1 github.com/go-test/deep v1.0.3 github.com/golang/mock v1.3.1 github.com/golang/protobuf v1.3.2 github.com/golang/snappy v0.0.1 - github.com/google/go-cmp v0.3.0 + github.com/google/go-cmp v0.3.1 github.com/google/uuid v1.1.1 github.com/hashicorp/errwrap v1.0.0 github.com/hashicorp/go-cleanhttp v0.5.1 @@ -28,8 +28,10 @@ require ( github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590 github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4 + github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596 github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba + github.com/mattn/go-colorable v0.1.1 // indirect github.com/mitchellh/cli v1.0.0 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db github.com/mitchellh/copystructure v1.0.0 @@ -40,10 +42,12 @@ require ( github.com/pierrec/lz4 v2.0.5+incompatible github.com/posener/complete v1.2.1 // indirect github.com/spf13/afero v1.2.2 + github.com/vmihailenco/msgpack v4.0.1+incompatible // indirect github.com/zclconf/go-cty v1.1.0 github.com/zclconf/go-cty-yaml v1.0.1 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 - golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 + golang.org/x/net v0.0.0-20191009170851-d66e71096ffb + golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa // indirect golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0 google.golang.org/grpc v1.23.0 ) diff --git a/go.sum b/go.sum index f53d1a1a89c..bee7a7d5c39 100644 --- a/go.sum +++ b/go.sum @@ -18,14 +18,16 @@ github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfT github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I= +github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 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.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= -github.com/aws/aws-sdk-go v1.19.39 h1:pIez14zQWSd/TER2Scohm7aCEG2TgoyXSOX6srOKt6o= -github.com/aws/aws-sdk-go v1.19.39/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.25.3 h1:uM16hIw9BotjZKMZlX05SN2EFtaWfi/NonPKIARiBLQ= +github.com/aws/aws-sdk-go v1.25.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= @@ -59,6 +61,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -107,6 +111,8 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4 h1:fTkL0YwjohGyN7AqsDhz6bwcGBpT+xBqi3Qhpw58Juw= github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4/go.mod h1:JDmizlhaP5P0rYTTZB0reDMefAiJyfWPEtugV4in1oI= +github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596 h1:hjyO2JsNZUKT1ym+FAdlBEkGPevazYsmVgIMw7dVELg= +github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= 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= @@ -128,9 +134,13 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -178,6 +188,8 @@ github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/vmihailenco/msgpack v3.3.3+incompatible h1:wapg9xDUZDzGCNFlwc5SqI1rvcciqcxEHac4CYj89xI= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack v4.0.1+incompatible h1:RMF1enSPeKTlXrXdOcqjFUElywVZjjC6pqse21bKbEU= +github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= @@ -214,8 +226,8 @@ golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191009170851-d66e71096ffb h1:TR699M2v0qoKTOHxeLgp6zPqaQNs74f01a/ob9W0qko= +golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= @@ -230,6 +242,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -238,6 +251,8 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= diff --git a/internal/configs/configload/loader.go b/internal/configs/configload/loader.go index 302b6b5d7dc..142366a9784 100644 --- a/internal/configs/configload/loader.go +++ b/internal/configs/configload/loader.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/internal/configs" "github.com/hashicorp/terraform-plugin-sdk/internal/registry" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/disco" + "github.com/hashicorp/terraform-svchost/disco" "github.com/spf13/afero" ) diff --git a/internal/configs/configload/module_mgr.go b/internal/configs/configload/module_mgr.go index 17032026623..797f50d2423 100644 --- a/internal/configs/configload/module_mgr.go +++ b/internal/configs/configload/module_mgr.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/internal/modsdir" "github.com/hashicorp/terraform-plugin-sdk/internal/registry" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/disco" + "github.com/hashicorp/terraform-svchost/disco" "github.com/spf13/afero" ) diff --git a/internal/plugin/discovery/get.go b/internal/plugin/discovery/get.go index ff90a266a99..35be2ee6c3c 100644 --- a/internal/plugin/discovery/get.go +++ b/internal/plugin/discovery/get.go @@ -21,7 +21,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/internal/registry" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/response" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/disco" + "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" tfversion "github.com/hashicorp/terraform-plugin-sdk/internal/version" "github.com/mitchellh/cli" diff --git a/internal/plugin/discovery/get_test.go b/internal/plugin/discovery/get_test.go index 1171c448df0..a7fdd1bd105 100644 --- a/internal/plugin/discovery/get_test.go +++ b/internal/plugin/discovery/get_test.go @@ -20,9 +20,11 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" "github.com/hashicorp/terraform-plugin-sdk/internal/registry" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/response" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/disco" + "github.com/hashicorp/terraform-svchost" + "github.com/hashicorp/terraform-svchost/disco" "github.com/mitchellh/cli" + "github.com/hashicorp/terraform-plugin-sdk/httpclient" + "github.com/hashicorp/terraform-plugin-sdk/internal/version" ) const testProviderFile = "test provider binary" @@ -741,6 +743,7 @@ func Disco(s *httptest.Server) *disco.Disco { "providers.v1": fmt.Sprintf("%s/v1/providers", s.URL), } d := disco.New() + d.SetUserAgent(httpclient.TerraformUserAgent(version.String())) d.ForceHostServices(svchost.Hostname("registry.terraform.io"), services) d.ForceHostServices(svchost.Hostname("localhost"), services) diff --git a/internal/registry/client.go b/internal/registry/client.go index 313951b38f8..4d4a84c6b2f 100644 --- a/internal/registry/client.go +++ b/internal/registry/client.go @@ -11,11 +11,12 @@ import ( "strings" "time" - "github.com/hashicorp/terraform-plugin-sdk/internal/httpclient" + internalhttpclient "github.com/hashicorp/terraform-plugin-sdk/internal/httpclient" + "github.com/hashicorp/terraform-plugin-sdk/httpclient" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/response" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/disco" + "github.com/hashicorp/terraform-svchost" + "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform-plugin-sdk/internal/version" ) @@ -46,12 +47,14 @@ func NewClient(services *disco.Disco, client *http.Client) *Client { } if client == nil { - client = httpclient.New() + client = internalhttpclient.New() client.Timeout = requestTimeout } services.Transport = client.Transport + services.SetUserAgent(httpclient.TerraformUserAgent(version.String())) + return &Client{ client: client, services: services, diff --git a/internal/registry/client_test.go b/internal/registry/client_test.go index 7c05ebac931..e2f52869e8a 100644 --- a/internal/registry/client_test.go +++ b/internal/registry/client_test.go @@ -9,7 +9,9 @@ import ( version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/test" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/disco" + "github.com/hashicorp/terraform-svchost/disco" + tfversion "github.com/hashicorp/terraform-plugin-sdk/internal/version" + "github.com/hashicorp/terraform-plugin-sdk/httpclient" ) func TestLookupModuleVersions(t *testing.T) { @@ -136,6 +138,7 @@ func TestAccLookupModuleVersions(t *testing.T) { t.Skip() } regDisco := disco.New() + regDisco.SetUserAgent(httpclient.TerraformUserAgent(tfversion.String())) // test with and without a hostname for _, src := range []string{ diff --git a/internal/registry/errors.go b/internal/registry/errors.go index 8b22ec14229..dc1524ad6a5 100644 --- a/internal/registry/errors.go +++ b/internal/registry/errors.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/disco" + "github.com/hashicorp/terraform-svchost/disco" ) type errModuleNotFound struct { diff --git a/internal/registry/regsrc/friendly_host.go b/internal/registry/regsrc/friendly_host.go index e67942a713a..c9bc40bee8f 100644 --- a/internal/registry/regsrc/friendly_host.go +++ b/internal/registry/regsrc/friendly_host.go @@ -4,7 +4,7 @@ import ( "regexp" "strings" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" + "github.com/hashicorp/terraform-svchost" ) var ( diff --git a/internal/registry/regsrc/module.go b/internal/registry/regsrc/module.go index 4c83afadb30..c3edd7d8709 100644 --- a/internal/registry/regsrc/module.go +++ b/internal/registry/regsrc/module.go @@ -6,7 +6,7 @@ import ( "regexp" "strings" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" + "github.com/hashicorp/terraform-svchost" ) var ( diff --git a/internal/registry/regsrc/terraform_provider.go b/internal/registry/regsrc/terraform_provider.go index 42ab3f77c08..7205d03b8c7 100644 --- a/internal/registry/regsrc/terraform_provider.go +++ b/internal/registry/regsrc/terraform_provider.go @@ -5,7 +5,7 @@ import ( "runtime" "strings" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" + "github.com/hashicorp/terraform-svchost" ) var ( diff --git a/internal/registry/test/mock_registry.go b/internal/registry/test/mock_registry.go index 19dc85e2ccb..9695973064e 100644 --- a/internal/registry/test/mock_registry.go +++ b/internal/registry/test/mock_registry.go @@ -12,9 +12,11 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/response" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/auth" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/disco" + "github.com/hashicorp/terraform-svchost" + "github.com/hashicorp/terraform-svchost/auth" + "github.com/hashicorp/terraform-svchost/disco" + tfversion "github.com/hashicorp/terraform-plugin-sdk/internal/version" + "github.com/hashicorp/terraform-plugin-sdk/httpclient" ) // Disco return a *disco.Disco mapping registry.terraform.io, localhost, @@ -27,6 +29,7 @@ func Disco(s *httptest.Server) *disco.Disco { "providers.v1": fmt.Sprintf("%s/v1/providers", s.URL), } d := disco.NewWithCredentialsSource(credsSrc) + d.SetUserAgent(httpclient.TerraformUserAgent(tfversion.String())) d.ForceHostServices(svchost.Hostname("registry.terraform.io"), services) d.ForceHostServices(svchost.Hostname("localhost"), services) diff --git a/internal/svchost/auth/cache.go b/internal/svchost/auth/cache.go deleted file mode 100644 index 99e2c0306af..00000000000 --- a/internal/svchost/auth/cache.go +++ /dev/null @@ -1,45 +0,0 @@ -package auth - -import ( - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" -) - -// CachingCredentialsSource creates a new credentials source that wraps another -// and caches its results in memory, on a per-hostname basis. -// -// No means is provided for expiration of cached credentials, so a caching -// credentials source should have a limited lifetime (one Terraform operation, -// for example) to ensure that time-limited credentials don't expire before -// their cache entries do. -func CachingCredentialsSource(source CredentialsSource) CredentialsSource { - return &cachingCredentialsSource{ - source: source, - cache: map[svchost.Hostname]HostCredentials{}, - } -} - -type cachingCredentialsSource struct { - source CredentialsSource - cache map[svchost.Hostname]HostCredentials -} - -// ForHost passes the given hostname on to the wrapped credentials source and -// caches the result to return for future requests with the same hostname. -// -// Both credentials and non-credentials (nil) responses are cached. -// -// No cache entry is created if the wrapped source returns an error, to allow -// the caller to retry the failing operation. -func (s *cachingCredentialsSource) ForHost(host svchost.Hostname) (HostCredentials, error) { - if cache, cached := s.cache[host]; cached { - return cache, nil - } - - result, err := s.source.ForHost(host) - if err != nil { - return result, err - } - - s.cache[host] = result - return result, nil -} diff --git a/internal/svchost/auth/credentials.go b/internal/svchost/auth/credentials.go deleted file mode 100644 index 00042a0a543..00000000000 --- a/internal/svchost/auth/credentials.go +++ /dev/null @@ -1,63 +0,0 @@ -// Package auth contains types and functions to manage authentication -// credentials for service hosts. -package auth - -import ( - "net/http" - - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" -) - -// Credentials is a list of CredentialsSource objects that can be tried in -// turn until one returns credentials for a host, or one returns an error. -// -// A Credentials is itself a CredentialsSource, wrapping its members. -// In principle one CredentialsSource can be nested inside another, though -// there is no good reason to do so. -type Credentials []CredentialsSource - -// NoCredentials is an empty CredentialsSource that always returns nil -// when asked for credentials. -var NoCredentials CredentialsSource = Credentials{} - -// A CredentialsSource is an object that may be able to provide credentials -// for a given host. -// -// Credentials lookups are not guaranteed to be concurrency-safe. Callers -// using these facilities in concurrent code must use external concurrency -// primitives to prevent race conditions. -type CredentialsSource interface { - // ForHost returns a non-nil HostCredentials if the source has credentials - // available for the host, and a nil HostCredentials if it does not. - // - // If an error is returned, progress through a list of CredentialsSources - // is halted and the error is returned to the user. - ForHost(host svchost.Hostname) (HostCredentials, error) -} - -// HostCredentials represents a single set of credentials for a particular -// host. -type HostCredentials interface { - // PrepareRequest modifies the given request in-place to apply the - // receiving credentials. The usual behavior of this method is to - // add some sort of Authorization header to the request. - PrepareRequest(req *http.Request) - - // Token returns the authentication token. - Token() string -} - -// ForHost iterates over the contained CredentialsSource objects and -// tries to obtain credentials for the given host from each one in turn. -// -// If any source returns either a non-nil HostCredentials or a non-nil error -// then this result is returned. Otherwise, the result is nil, nil. -func (c Credentials) ForHost(host svchost.Hostname) (HostCredentials, error) { - for _, source := range c { - creds, err := source.ForHost(host) - if creds != nil || err != nil { - return creds, err - } - } - return nil, nil -} diff --git a/internal/svchost/auth/from_map.go b/internal/svchost/auth/from_map.go deleted file mode 100644 index f91006aece7..00000000000 --- a/internal/svchost/auth/from_map.go +++ /dev/null @@ -1,18 +0,0 @@ -package auth - -// HostCredentialsFromMap converts a map of key-value pairs from a credentials -// definition provided by the user (e.g. in a config file, or via a credentials -// helper) into a HostCredentials object if possible, or returns nil if -// no credentials could be extracted from the map. -// -// This function ignores map keys it is unfamiliar with, to allow for future -// expansion of the credentials map format for new credential types. -func HostCredentialsFromMap(m map[string]interface{}) HostCredentials { - if m == nil { - return nil - } - if token, ok := m["token"].(string); ok { - return HostCredentialsToken(token) - } - return nil -} diff --git a/internal/svchost/auth/helper_program.go b/internal/svchost/auth/helper_program.go deleted file mode 100644 index 93f52604a4b..00000000000 --- a/internal/svchost/auth/helper_program.go +++ /dev/null @@ -1,80 +0,0 @@ -package auth - -import ( - "bytes" - "encoding/json" - "fmt" - "os/exec" - "path/filepath" - - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" -) - -type helperProgramCredentialsSource struct { - executable string - args []string -} - -// HelperProgramCredentialsSource returns a CredentialsSource that runs the -// given program with the given arguments in order to obtain credentials. -// -// The given executable path must be an absolute path; it is the caller's -// responsibility to validate and process a relative path or other input -// provided by an end-user. If the given path is not absolute, this -// function will panic. -// -// When credentials are requested, the program will be run in a child process -// with the given arguments along with two additional arguments added to the -// end of the list: the literal string "get", followed by the requested -// hostname in ASCII compatibility form (punycode form). -func HelperProgramCredentialsSource(executable string, args ...string) CredentialsSource { - if !filepath.IsAbs(executable) { - panic("NewCredentialsSourceHelperProgram requires absolute path to executable") - } - - fullArgs := make([]string, len(args)+1) - fullArgs[0] = executable - copy(fullArgs[1:], args) - - return &helperProgramCredentialsSource{ - executable: executable, - args: fullArgs, - } -} - -func (s *helperProgramCredentialsSource) ForHost(host svchost.Hostname) (HostCredentials, error) { - args := make([]string, len(s.args), len(s.args)+2) - copy(args, s.args) - args = append(args, "get") - args = append(args, string(host)) - - outBuf := bytes.Buffer{} - errBuf := bytes.Buffer{} - - cmd := exec.Cmd{ - Path: s.executable, - Args: args, - Stdin: nil, - Stdout: &outBuf, - Stderr: &errBuf, - } - err := cmd.Run() - if _, isExitErr := err.(*exec.ExitError); isExitErr { - errText := errBuf.String() - if errText == "" { - // Shouldn't happen for a well-behaved helper program - return nil, fmt.Errorf("error in %s, but it produced no error message", s.executable) - } - return nil, fmt.Errorf("error in %s: %s", s.executable, errText) - } else if err != nil { - return nil, fmt.Errorf("failed to run %s: %s", s.executable, err) - } - - var m map[string]interface{} - err = json.Unmarshal(outBuf.Bytes(), &m) - if err != nil { - return nil, fmt.Errorf("malformed output from %s: %s", s.executable, err) - } - - return HostCredentialsFromMap(m), nil -} diff --git a/internal/svchost/auth/helper_program_test.go b/internal/svchost/auth/helper_program_test.go deleted file mode 100644 index 505ed481b1b..00000000000 --- a/internal/svchost/auth/helper_program_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package auth - -import ( - "os" - "path/filepath" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" -) - -func TestHelperProgramCredentialsSource(t *testing.T) { - wd, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - - program := filepath.Join(wd, "testdata/test-helper") - t.Logf("testing with helper at %s", program) - - src := HelperProgramCredentialsSource(program) - - t.Run("happy path", func(t *testing.T) { - creds, err := src.ForHost(svchost.Hostname("example.com")) - if err != nil { - t.Fatal(err) - } - if tokCreds, isTok := creds.(HostCredentialsToken); isTok { - if got, want := string(tokCreds), "example-token"; got != want { - t.Errorf("wrong token %q; want %q", got, want) - } - } else { - t.Errorf("wrong type of credentials %T", creds) - } - }) - t.Run("no credentials", func(t *testing.T) { - creds, err := src.ForHost(svchost.Hostname("nothing.example.com")) - if err != nil { - t.Fatal(err) - } - if creds != nil { - t.Errorf("got credentials; want nil") - } - }) - t.Run("unsupported credentials type", func(t *testing.T) { - creds, err := src.ForHost(svchost.Hostname("other-cred-type.example.com")) - if err != nil { - t.Fatal(err) - } - if creds != nil { - t.Errorf("got credentials; want nil") - } - }) - t.Run("lookup error", func(t *testing.T) { - _, err := src.ForHost(svchost.Hostname("fail.example.com")) - if err == nil { - t.Error("completed successfully; want error") - } - }) -} diff --git a/internal/svchost/auth/static.go b/internal/svchost/auth/static.go deleted file mode 100644 index b5108a4a2ec..00000000000 --- a/internal/svchost/auth/static.go +++ /dev/null @@ -1,28 +0,0 @@ -package auth - -import ( - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" -) - -// StaticCredentialsSource is a credentials source that retrieves credentials -// from the provided map. It returns nil if a requested hostname is not -// present in the map. -// -// The caller should not modify the given map after passing it to this function. -func StaticCredentialsSource(creds map[svchost.Hostname]map[string]interface{}) CredentialsSource { - return staticCredentialsSource(creds) -} - -type staticCredentialsSource map[svchost.Hostname]map[string]interface{} - -func (s staticCredentialsSource) ForHost(host svchost.Hostname) (HostCredentials, error) { - if s == nil { - return nil, nil - } - - if m, exists := s[host]; exists { - return HostCredentialsFromMap(m), nil - } - - return nil, nil -} diff --git a/internal/svchost/auth/static_test.go b/internal/svchost/auth/static_test.go deleted file mode 100644 index c3172608f51..00000000000 --- a/internal/svchost/auth/static_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package auth - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" -) - -func TestStaticCredentialsSource(t *testing.T) { - src := StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ - svchost.Hostname("example.com"): map[string]interface{}{ - "token": "abc123", - }, - }) - - t.Run("exists", func(t *testing.T) { - creds, err := src.ForHost(svchost.Hostname("example.com")) - if err != nil { - t.Fatal(err) - } - if tokCreds, isToken := creds.(HostCredentialsToken); isToken { - if got, want := string(tokCreds), "abc123"; got != want { - t.Errorf("wrong token %q; want %q", got, want) - } - } else { - t.Errorf("creds is %#v; want HostCredentialsToken", creds) - } - }) - t.Run("does not exist", func(t *testing.T) { - creds, err := src.ForHost(svchost.Hostname("example.net")) - if err != nil { - t.Fatal(err) - } - if creds != nil { - t.Errorf("creds is %#v; want nil", creds) - } - }) -} diff --git a/internal/svchost/auth/testdata/.gitignore b/internal/svchost/auth/testdata/.gitignore deleted file mode 100644 index ba2906d0666..00000000000 --- a/internal/svchost/auth/testdata/.gitignore +++ /dev/null @@ -1 +0,0 @@ -main diff --git a/internal/svchost/auth/testdata/main.go b/internal/svchost/auth/testdata/main.go deleted file mode 100644 index 3b1b72fd506..00000000000 --- a/internal/svchost/auth/testdata/main.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "fmt" - "os" -) - -// This is a simple program that implements the "helper program" protocol -// for the svchost/auth package for unit testing purposes. - -func main() { - args := os.Args - - if len(args) < 3 { - die("not enough arguments\n") - } - - if args[1] != "get" { - die("unknown subcommand %q\n", args[1]) - } - - host := args[2] - - switch host { - case "example.com": - fmt.Print(`{"token":"example-token"}`) - case "other-cred-type.example.com": - fmt.Print(`{"username":"alfred"}`) // unrecognized by main program - case "fail.example.com": - die("failing because you told me to fail\n") - default: - fmt.Print("{}") // no credentials available - } -} - -func die(f string, args ...interface{}) { - fmt.Fprintf(os.Stderr, fmt.Sprintf(f, args...)) - os.Exit(1) -} diff --git a/internal/svchost/auth/testdata/test-helper b/internal/svchost/auth/testdata/test-helper deleted file mode 100755 index 0ed3396c563..00000000000 --- a/internal/svchost/auth/testdata/test-helper +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -eu - -cd "$( dirname "${BASH_SOURCE[0]}" )" -[ -x main ] || go build -o main . -exec ./main "$@" diff --git a/internal/svchost/auth/token_credentials.go b/internal/svchost/auth/token_credentials.go deleted file mode 100644 index 9358bcb6444..00000000000 --- a/internal/svchost/auth/token_credentials.go +++ /dev/null @@ -1,25 +0,0 @@ -package auth - -import ( - "net/http" -) - -// HostCredentialsToken is a HostCredentials implementation that represents a -// single "bearer token", to be sent to the server via an Authorization header -// with the auth type set to "Bearer" -type HostCredentialsToken string - -// PrepareRequest alters the given HTTP request by setting its Authorization -// header to the string "Bearer " followed by the encapsulated authentication -// token. -func (tc HostCredentialsToken) PrepareRequest(req *http.Request) { - if req.Header == nil { - req.Header = http.Header{} - } - req.Header.Set("Authorization", "Bearer "+string(tc)) -} - -// Token returns the authentication token. -func (tc HostCredentialsToken) Token() string { - return string(tc) -} diff --git a/internal/svchost/auth/token_credentials_test.go b/internal/svchost/auth/token_credentials_test.go deleted file mode 100644 index 3f7355063e6..00000000000 --- a/internal/svchost/auth/token_credentials_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package auth - -import ( - "net/http" - "testing" -) - -func TestHostCredentialsToken(t *testing.T) { - creds := HostCredentialsToken("foo-bar") - req := &http.Request{} - - creds.PrepareRequest(req) - - authStr := req.Header.Get("authorization") - if got, want := authStr, "Bearer foo-bar"; got != want { - t.Errorf("wrong Authorization header value %q; want %q", got, want) - } -} diff --git a/internal/svchost/disco/disco.go b/internal/svchost/disco/disco.go deleted file mode 100644 index c770338be1b..00000000000 --- a/internal/svchost/disco/disco.go +++ /dev/null @@ -1,259 +0,0 @@ -// Package disco handles Terraform's remote service discovery protocol. -// -// This protocol allows mapping from a service hostname, as produced by the -// svchost package, to a set of services supported by that host and the -// endpoint information for each supported service. -package disco - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "mime" - "net/http" - "net/url" - "time" - - cleanhttp "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/terraform-plugin-sdk/internal/httpclient" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/auth" -) - -const ( - // Fixed path to the discovery manifest. - discoPath = "/.well-known/terraform.json" - - // Arbitrary-but-small number to prevent runaway redirect loops. - maxRedirects = 3 - - // Arbitrary-but-small time limit to prevent UI "hangs" during discovery. - discoTimeout = 11 * time.Second - - // 1MB - to prevent abusive services from using loads of our memory. - maxDiscoDocBytes = 1 * 1024 * 1024 -) - -// httpTransport is overridden during tests, to skip TLS verification. -var httpTransport = cleanhttp.DefaultPooledTransport() - -// Disco is the main type in this package, which allows discovery on given -// hostnames and caches the results by hostname to avoid repeated requests -// for the same information. -type Disco struct { - hostCache map[svchost.Hostname]*Host - credsSrc auth.CredentialsSource - - // Transport is a custom http.RoundTripper to use. - Transport http.RoundTripper -} - -// New returns a new initialized discovery object. -func New() *Disco { - return NewWithCredentialsSource(nil) -} - -// NewWithCredentialsSource returns a new discovery object initialized with -// the given credentials source. -func NewWithCredentialsSource(credsSrc auth.CredentialsSource) *Disco { - return &Disco{ - hostCache: make(map[svchost.Hostname]*Host), - credsSrc: credsSrc, - Transport: httpTransport, - } -} - -// SetCredentialsSource provides a credentials source that will be used to -// add credentials to outgoing discovery requests, where available. -// -// If this method is never called, no outgoing discovery requests will have -// credentials. -func (d *Disco) SetCredentialsSource(src auth.CredentialsSource) { - d.credsSrc = src -} - -// CredentialsForHost returns a non-nil HostCredentials if the embedded source has -// credentials available for the host, and a nil HostCredentials if it does not. -func (d *Disco) CredentialsForHost(hostname svchost.Hostname) (auth.HostCredentials, error) { - if d.credsSrc == nil { - return nil, nil - } - return d.credsSrc.ForHost(hostname) -} - -// ForceHostServices provides a pre-defined set of services for a given -// host, which prevents the receiver from attempting network-based discovery -// for the given host. Instead, the given services map will be returned -// verbatim. -// -// When providing "forced" services, any relative URLs are resolved against -// the initial discovery URL that would have been used for network-based -// discovery, yielding the same results as if the given map were published -// at the host's default discovery URL, though using absolute URLs is strongly -// recommended to make the configured behavior more explicit. -func (d *Disco) ForceHostServices(hostname svchost.Hostname, services map[string]interface{}) { - if services == nil { - services = map[string]interface{}{} - } - - d.hostCache[hostname] = &Host{ - discoURL: &url.URL{ - Scheme: "https", - Host: string(hostname), - Path: discoPath, - }, - hostname: hostname.ForDisplay(), - services: services, - transport: d.Transport, - } -} - -// Discover runs the discovery protocol against the given hostname (which must -// already have been validated and prepared with svchost.ForComparison) and -// returns an object describing the services available at that host. -// -// If a given hostname supports no Terraform services at all, a non-nil but -// empty Host object is returned. When giving feedback to the end user about -// such situations, we say "host does not provide a service", -// regardless of whether that is due to that service specifically being absent -// or due to the host not providing Terraform services at all, since we don't -// wish to expose the detail of whole-host discovery to an end-user. -func (d *Disco) Discover(hostname svchost.Hostname) (*Host, error) { - if host, cached := d.hostCache[hostname]; cached { - return host, nil - } - - host, err := d.discover(hostname) - if err != nil { - return nil, err - } - d.hostCache[hostname] = host - - return host, nil -} - -// DiscoverServiceURL is a convenience wrapper for discovery on a given -// hostname and then looking up a particular service in the result. -func (d *Disco) DiscoverServiceURL(hostname svchost.Hostname, serviceID string) (*url.URL, error) { - host, err := d.Discover(hostname) - if err != nil { - return nil, err - } - return host.ServiceURL(serviceID) -} - -// discover implements the actual discovery process, with its result cached -// by the public-facing Discover method. -func (d *Disco) discover(hostname svchost.Hostname) (*Host, error) { - discoURL := &url.URL{ - Scheme: "https", - Host: hostname.String(), - Path: discoPath, - } - - client := &http.Client{ - Transport: d.Transport, - Timeout: discoTimeout, - - CheckRedirect: func(req *http.Request, via []*http.Request) error { - log.Printf("[DEBUG] Service discovery redirected to %s", req.URL) - if len(via) > maxRedirects { - return errors.New("too many redirects") // this error will never actually be seen - } - return nil - }, - } - - req := &http.Request{ - Header: make(http.Header), - Method: "GET", - URL: discoURL, - } - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", httpclient.UserAgentString()) - - creds, err := d.CredentialsForHost(hostname) - if err != nil { - log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", hostname, err) - } - if creds != nil { - // Update the request to include credentials. - creds.PrepareRequest(req) - } - - log.Printf("[DEBUG] Service discovery for %s at %s", hostname, discoURL) - - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("Failed to request discovery document: %v", err) - } - defer resp.Body.Close() - - host := &Host{ - // Use the discovery URL from resp.Request in - // case the client followed any redirects. - discoURL: resp.Request.URL, - hostname: hostname.ForDisplay(), - transport: d.Transport, - } - - // Return the host without any services. - if resp.StatusCode == 404 { - return host, nil - } - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Failed to request discovery document: %s", resp.Status) - } - - contentType := resp.Header.Get("Content-Type") - mediaType, _, err := mime.ParseMediaType(contentType) - if err != nil { - return nil, fmt.Errorf("Discovery URL has a malformed Content-Type %q", contentType) - } - if mediaType != "application/json" { - return nil, fmt.Errorf("Discovery URL returned an unsupported Content-Type %q", mediaType) - } - - // This doesn't catch chunked encoding, because ContentLength is -1 in that case. - if resp.ContentLength > maxDiscoDocBytes { - // Size limit here is not a contractual requirement and so we may - // adjust it over time if we find a different limit is warranted. - return nil, fmt.Errorf( - "Discovery doc response is too large (got %d bytes; limit %d)", - resp.ContentLength, maxDiscoDocBytes, - ) - } - - // If the response is using chunked encoding then we can't predict its - // size, but we'll at least prevent reading the entire thing into memory. - lr := io.LimitReader(resp.Body, maxDiscoDocBytes) - - servicesBytes, err := ioutil.ReadAll(lr) - if err != nil { - return nil, fmt.Errorf("Error reading discovery document body: %v", err) - } - - var services map[string]interface{} - err = json.Unmarshal(servicesBytes, &services) - if err != nil { - return nil, fmt.Errorf("Failed to decode discovery document as a JSON object: %v", err) - } - host.services = services - - return host, nil -} - -// Forget invalidates any cached record of the given hostname. If the host -// has no cache entry then this is a no-op. -func (d *Disco) Forget(hostname svchost.Hostname) { - delete(d.hostCache, hostname) -} - -// ForgetAll is like Forget, but for all of the hostnames that have cache entries. -func (d *Disco) ForgetAll() { - d.hostCache = make(map[svchost.Hostname]*Host) -} diff --git a/internal/svchost/disco/disco_test.go b/internal/svchost/disco/disco_test.go deleted file mode 100644 index 9db30b4f8f9..00000000000 --- a/internal/svchost/disco/disco_test.go +++ /dev/null @@ -1,357 +0,0 @@ -package disco - -import ( - "crypto/tls" - "net/http" - "net/http/httptest" - "net/url" - "os" - "strconv" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost" - "github.com/hashicorp/terraform-plugin-sdk/internal/svchost/auth" -) - -func TestMain(m *testing.M) { - // During all tests we override the HTTP transport we use for discovery - // so it'll tolerate the locally-generated TLS certificates we use - // for test URLs. - httpTransport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - - os.Exit(m.Run()) -} - -func TestDiscover(t *testing.T) { - t.Run("happy path", func(t *testing.T) { - portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) { - resp := []byte(` -{ -"thingy.v1": "http://example.com/foo", -"wotsit.v2": "http://example.net/bar" -} -`) - w.Header().Add("Content-Type", "application/json") - w.Header().Add("Content-Length", strconv.Itoa(len(resp))) - w.Write(resp) - }) - defer close() - - givenHost := "localhost" + portStr - host, err := svchost.ForComparison(givenHost) - if err != nil { - t.Fatalf("test server hostname is invalid: %s", err) - } - - d := New() - discovered, err := d.Discover(host) - if err != nil { - t.Fatalf("unexpected discovery error: %s", err) - } - - gotURL, err := discovered.ServiceURL("thingy.v1") - if err != nil { - t.Fatalf("unexpected service URL error: %s", err) - } - if gotURL == nil { - t.Fatalf("found no URL for thingy.v1") - } - if got, want := gotURL.String(), "http://example.com/foo"; got != want { - t.Fatalf("wrong result %q; want %q", got, want) - } - }) - t.Run("chunked encoding", func(t *testing.T) { - portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) { - resp := []byte(` -{ -"thingy.v1": "http://example.com/foo", -"wotsit.v2": "http://example.net/bar" -} -`) - w.Header().Add("Content-Type", "application/json") - // We're going to force chunked encoding here -- and thus prevent - // the server from predicting the length -- so we can make sure - // our client is tolerant of servers using this encoding. - w.Write(resp[:5]) - w.(http.Flusher).Flush() - w.Write(resp[5:]) - w.(http.Flusher).Flush() - }) - defer close() - - givenHost := "localhost" + portStr - host, err := svchost.ForComparison(givenHost) - if err != nil { - t.Fatalf("test server hostname is invalid: %s", err) - } - - d := New() - discovered, err := d.Discover(host) - if err != nil { - t.Fatalf("unexpected discovery error: %s", err) - } - - gotURL, err := discovered.ServiceURL("wotsit.v2") - if err != nil { - t.Fatalf("unexpected service URL error: %s", err) - } - if gotURL == nil { - t.Fatalf("found no URL for wotsit.v2") - } - if got, want := gotURL.String(), "http://example.net/bar"; got != want { - t.Fatalf("wrong result %q; want %q", got, want) - } - }) - t.Run("with credentials", func(t *testing.T) { - var authHeaderText string - portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) { - resp := []byte(`{}`) - authHeaderText = r.Header.Get("Authorization") - w.Header().Add("Content-Type", "application/json") - w.Header().Add("Content-Length", strconv.Itoa(len(resp))) - w.Write(resp) - }) - defer close() - - givenHost := "localhost" + portStr - host, err := svchost.ForComparison(givenHost) - if err != nil { - t.Fatalf("test server hostname is invalid: %s", err) - } - - d := New() - d.SetCredentialsSource(auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ - host: map[string]interface{}{ - "token": "abc123", - }, - })) - d.Discover(host) - if got, want := authHeaderText, "Bearer abc123"; got != want { - t.Fatalf("wrong Authorization header\ngot: %s\nwant: %s", got, want) - } - }) - t.Run("forced services override", func(t *testing.T) { - forced := map[string]interface{}{ - "thingy.v1": "http://example.net/foo", - "wotsit.v2": "/foo", - } - - d := New() - d.ForceHostServices(svchost.Hostname("example.com"), forced) - - givenHost := "example.com" - host, err := svchost.ForComparison(givenHost) - if err != nil { - t.Fatalf("test server hostname is invalid: %s", err) - } - - discovered, err := d.Discover(host) - if err != nil { - t.Fatalf("unexpected discovery error: %s", err) - } - { - gotURL, err := discovered.ServiceURL("thingy.v1") - if err != nil { - t.Fatalf("unexpected service URL error: %s", err) - } - if gotURL == nil { - t.Fatalf("found no URL for thingy.v1") - } - if got, want := gotURL.String(), "http://example.net/foo"; got != want { - t.Fatalf("wrong result %q; want %q", got, want) - } - } - { - gotURL, err := discovered.ServiceURL("wotsit.v2") - if err != nil { - t.Fatalf("unexpected service URL error: %s", err) - } - if gotURL == nil { - t.Fatalf("found no URL for wotsit.v2") - } - if got, want := gotURL.String(), "https://example.com/foo"; got != want { - t.Fatalf("wrong result %q; want %q", got, want) - } - } - }) - t.Run("not JSON", func(t *testing.T) { - portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) { - resp := []byte(`{"thingy.v1": "http://example.com/foo"}`) - w.Header().Add("Content-Type", "application/octet-stream") - w.Write(resp) - }) - defer close() - - givenHost := "localhost" + portStr - host, err := svchost.ForComparison(givenHost) - if err != nil { - t.Fatalf("test server hostname is invalid: %s", err) - } - - d := New() - discovered, err := d.Discover(host) - if err == nil { - t.Fatalf("expected a discovery error") - } - - // Returned discovered should be nil. - if discovered != nil { - t.Errorf("discovered not nil; should be") - } - }) - t.Run("malformed JSON", func(t *testing.T) { - portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) { - resp := []byte(`{"thingy.v1": "htt`) // truncated, for example... - w.Header().Add("Content-Type", "application/json") - w.Write(resp) - }) - defer close() - - givenHost := "localhost" + portStr - host, err := svchost.ForComparison(givenHost) - if err != nil { - t.Fatalf("test server hostname is invalid: %s", err) - } - - d := New() - discovered, err := d.Discover(host) - if err == nil { - t.Fatalf("expected a discovery error") - } - - // Returned discovered should be nil. - if discovered != nil { - t.Errorf("discovered not nil; should be") - } - }) - t.Run("JSON with redundant charset", func(t *testing.T) { - // The JSON RFC defines no parameters for the application/json - // MIME type, but some servers have a weird tendency to just add - // "charset" to everything, so we'll make sure we ignore it successfully. - // (JSON uses content sniffing for encoding detection, not media type params.) - portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) { - resp := []byte(`{"thingy.v1": "http://example.com/foo"}`) - w.Header().Add("Content-Type", "application/json; charset=latin-1") - w.Write(resp) - }) - defer close() - - givenHost := "localhost" + portStr - host, err := svchost.ForComparison(givenHost) - if err != nil { - t.Fatalf("test server hostname is invalid: %s", err) - } - - d := New() - discovered, err := d.Discover(host) - if err != nil { - t.Fatalf("unexpected discovery error: %s", err) - } - - if discovered.services == nil { - t.Errorf("response is empty; shouldn't be") - } - }) - t.Run("no discovery doc", func(t *testing.T) { - portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(404) - }) - defer close() - - givenHost := "localhost" + portStr - host, err := svchost.ForComparison(givenHost) - if err != nil { - t.Fatalf("test server hostname is invalid: %s", err) - } - - d := New() - discovered, err := d.Discover(host) - if err != nil { - t.Fatalf("unexpected discovery error: %s", err) - } - - // Returned discovered.services should be nil (empty). - if discovered.services != nil { - t.Errorf("discovered.services not nil (empty); should be") - } - }) - t.Run("redirect", func(t *testing.T) { - // For this test, we have two servers and one redirects to the other - portStr1, close1 := testServer(func(w http.ResponseWriter, r *http.Request) { - // This server is the one that returns a real response. - resp := []byte(`{"thingy.v1": "http://example.com/foo"}`) - w.Header().Add("Content-Type", "application/json") - w.Header().Add("Content-Length", strconv.Itoa(len(resp))) - w.Write(resp) - }) - portStr2, close2 := testServer(func(w http.ResponseWriter, r *http.Request) { - // This server is the one that redirects. - http.Redirect(w, r, "https://127.0.0.1"+portStr1+"/.well-known/terraform.json", 302) - }) - defer close1() - defer close2() - - givenHost := "localhost" + portStr2 - host, err := svchost.ForComparison(givenHost) - if err != nil { - t.Fatalf("test server hostname is invalid: %s", err) - } - - d := New() - discovered, err := d.Discover(host) - if err != nil { - t.Fatalf("unexpected discovery error: %s", err) - } - - gotURL, err := discovered.ServiceURL("thingy.v1") - if err != nil { - t.Fatalf("unexpected service URL error: %s", err) - } - if gotURL == nil { - t.Fatalf("found no URL for thingy.v1") - } - if got, want := gotURL.String(), "http://example.com/foo"; got != want { - t.Fatalf("wrong result %q; want %q", got, want) - } - - // The base URL for the host object should be the URL we redirected to, - // rather than the we redirected _from_. - gotBaseURL := discovered.discoURL.String() - wantBaseURL := "https://127.0.0.1" + portStr1 + "/.well-known/terraform.json" - if gotBaseURL != wantBaseURL { - t.Errorf("incorrect base url %s; want %s", gotBaseURL, wantBaseURL) - } - - }) -} - -func testServer(h func(w http.ResponseWriter, r *http.Request)) (portStr string, close func()) { - server := httptest.NewTLSServer(http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - // Test server always returns 404 if the URL isn't what we expect - if r.URL.Path != "/.well-known/terraform.json" { - w.WriteHeader(404) - w.Write([]byte("not found")) - return - } - - // If the URL is correct then the given hander decides the response - h(w, r) - }, - )) - - serverURL, _ := url.Parse(server.URL) - - portStr = serverURL.Port() - if portStr != "" { - portStr = ":" + portStr - } - - close = func() { - server.Close() - } - - return portStr, close -} diff --git a/internal/svchost/disco/host.go b/internal/svchost/disco/host.go deleted file mode 100644 index 0d6ef038378..00000000000 --- a/internal/svchost/disco/host.go +++ /dev/null @@ -1,264 +0,0 @@ -package disco - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "net/url" - "os" - "strconv" - "strings" - "time" - - "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-plugin-sdk/internal/httpclient" -) - -const versionServiceID = "versions.v1" - -// Host represents a service discovered host. -type Host struct { - discoURL *url.URL - hostname string - services map[string]interface{} - transport http.RoundTripper -} - -// Constraints represents the version constraints of a service. -type Constraints struct { - Service string `json:"service"` - Product string `json:"product"` - Minimum string `json:"minimum"` - Maximum string `json:"maximum"` - Excluding []string `json:"excluding"` -} - -// ErrServiceNotProvided is returned when the service is not provided. -type ErrServiceNotProvided struct { - hostname string - service string -} - -// Error returns a customized error message. -func (e *ErrServiceNotProvided) Error() string { - if e.hostname == "" { - return fmt.Sprintf("host does not provide a %s service", e.service) - } - return fmt.Sprintf("host %s does not provide a %s service", e.hostname, e.service) -} - -// ErrVersionNotSupported is returned when the version is not supported. -type ErrVersionNotSupported struct { - hostname string - service string - version string -} - -// Error returns a customized error message. -func (e *ErrVersionNotSupported) Error() string { - if e.hostname == "" { - return fmt.Sprintf("host does not support %s version %s", e.service, e.version) - } - return fmt.Sprintf("host %s does not support %s version %s", e.hostname, e.service, e.version) -} - -// ErrNoVersionConstraints is returned when checkpoint was disabled -// or the endpoint to query for version constraints was unavailable. -type ErrNoVersionConstraints struct { - disabled bool -} - -// Error returns a customized error message. -func (e *ErrNoVersionConstraints) Error() string { - if e.disabled { - return "checkpoint disabled" - } - return "unable to contact versions service" -} - -// ServiceURL returns the URL associated with the given service identifier, -// which should be of the form "servicename.vN". -// -// A non-nil result is always an absolute URL with a scheme of either HTTPS -// or HTTP. -func (h *Host) ServiceURL(id string) (*url.URL, error) { - svc, ver, err := parseServiceID(id) - if err != nil { - return nil, err - } - - // No services supported for an empty Host. - if h == nil || h.services == nil { - return nil, &ErrServiceNotProvided{service: svc} - } - - urlStr, ok := h.services[id].(string) - if !ok { - // See if we have a matching service as that would indicate - // the service is supported, but not the requested version. - for serviceID := range h.services { - if strings.HasPrefix(serviceID, svc+".") { - return nil, &ErrVersionNotSupported{ - hostname: h.hostname, - service: svc, - version: ver.Original(), - } - } - } - - // No discovered services match the requested service. - return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc} - } - - u, err := url.Parse(urlStr) - if err != nil { - return nil, fmt.Errorf("Failed to parse service URL: %v", err) - } - - // Make relative URLs absolute using our discovery URL. - if !u.IsAbs() { - u = h.discoURL.ResolveReference(u) - } - - if u.Scheme != "https" && u.Scheme != "http" { - return nil, fmt.Errorf("Service URL is using an unsupported scheme: %s", u.Scheme) - } - if u.User != nil { - return nil, fmt.Errorf("Embedded username/password information is not permitted") - } - - // Fragment part is irrelevant, since we're not a browser. - u.Fragment = "" - - return h.discoURL.ResolveReference(u), nil -} - -// VersionConstraints returns the contraints for a given service identifier -// (which should be of the form "servicename.vN") and product. -// -// When an exact (service and version) match is found, the constraints for -// that service are returned. -// -// When the requested version is not provided but the service is, we will -// search for all alternative versions. If mutliple alternative versions -// are found, the contrains of the latest available version are returned. -// -// When a service is not provided at all an error will be returned instead. -// -// When checkpoint is disabled or when a 404 is returned after making the -// HTTP call, an ErrNoVersionConstraints error will be returned. -func (h *Host) VersionConstraints(id, product string) (*Constraints, error) { - svc, _, err := parseServiceID(id) - if err != nil { - return nil, err - } - - // Return early if checkpoint is disabled. - if disabled := os.Getenv("CHECKPOINT_DISABLE"); disabled != "" { - return nil, &ErrNoVersionConstraints{disabled: true} - } - - // No services supported for an empty Host. - if h == nil || h.services == nil { - return nil, &ErrServiceNotProvided{service: svc} - } - - // Try to get the service URL for the version service and - // return early if the service isn't provided by the host. - u, err := h.ServiceURL(versionServiceID) - if err != nil { - return nil, err - } - - // Check if we have an exact (service and version) match. - if _, ok := h.services[id].(string); !ok { - // If we don't have an exact match, we search for all matching - // services and then use the service ID of the latest version. - var services []string - for serviceID := range h.services { - if strings.HasPrefix(serviceID, svc+".") { - services = append(services, serviceID) - } - } - - if len(services) == 0 { - // No discovered services match the requested service. - return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc} - } - - // Set id to the latest service ID we found. - var latest *version.Version - for _, serviceID := range services { - if _, ver, err := parseServiceID(serviceID); err == nil { - if latest == nil || latest.LessThan(ver) { - id = serviceID - latest = ver - } - } - } - } - - // Set a default timeout of 1 sec for the versions request (in milliseconds) - timeout := 1000 - if v, err := strconv.Atoi(os.Getenv("CHECKPOINT_TIMEOUT")); err == nil { - timeout = v - } - - client := &http.Client{ - Transport: h.transport, - Timeout: time.Duration(timeout) * time.Millisecond, - } - - // Prepare the service URL by setting the service and product. - v := u.Query() - v.Set("product", product) - u.Path += id - u.RawQuery = v.Encode() - - // Create a new request. - req, err := http.NewRequest("GET", u.String(), nil) - if err != nil { - return nil, fmt.Errorf("Failed to create version constraints request: %v", err) - } - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", httpclient.UserAgentString()) - - log.Printf("[DEBUG] Retrieve version constraints for service %s and product %s", id, product) - - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("Failed to request version constraints: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode == 404 { - return nil, &ErrNoVersionConstraints{disabled: false} - } - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Failed to request version constraints: %s", resp.Status) - } - - // Parse the constraints from the response body. - result := &Constraints{} - if err := json.NewDecoder(resp.Body).Decode(result); err != nil { - return nil, fmt.Errorf("Error parsing version constraints: %v", err) - } - - return result, nil -} - -func parseServiceID(id string) (string, *version.Version, error) { - parts := strings.SplitN(id, ".", 2) - if len(parts) != 2 { - return "", nil, fmt.Errorf("Invalid service ID format (i.e. service.vN): %s", id) - } - - version, err := version.NewVersion(parts[1]) - if err != nil { - return "", nil, fmt.Errorf("Invalid service version: %v", err) - } - - return parts[0], version, nil -} diff --git a/internal/svchost/disco/host_test.go b/internal/svchost/disco/host_test.go deleted file mode 100644 index 5a96aa418cf..00000000000 --- a/internal/svchost/disco/host_test.go +++ /dev/null @@ -1,290 +0,0 @@ -package disco - -import ( - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "os" - "path" - "reflect" - "strconv" - "strings" - "testing" -) - -func TestHostServiceURL(t *testing.T) { - baseURL, _ := url.Parse("https://example.com/disco/foo.json") - host := Host{ - discoURL: baseURL, - hostname: "test-server", - services: map[string]interface{}{ - "absolute.v1": "http://example.net/foo/bar", - "absolutewithport.v1": "http://example.net:8080/foo/bar", - "relative.v1": "./stu/", - "rootrelative.v1": "/baz", - "protorelative.v1": "//example.net/", - "withfragment.v1": "http://example.org/#foo", - "querystring.v1": "https://example.net/baz?foo=bar", - "nothttp.v1": "ftp://127.0.0.1/pub/", - "invalid.v1": "***not A URL at all!:/<@@@@>***", - }, - } - - tests := []struct { - ID string - want string - err string - }{ - {"absolute.v1", "http://example.net/foo/bar", ""}, - {"absolutewithport.v1", "http://example.net:8080/foo/bar", ""}, - {"relative.v1", "https://example.com/disco/stu/", ""}, - {"rootrelative.v1", "https://example.com/baz", ""}, - {"protorelative.v1", "https://example.net/", ""}, - {"withfragment.v1", "http://example.org/", ""}, - {"querystring.v1", "https://example.net/baz?foo=bar", ""}, - {"nothttp.v1", "", "unsupported scheme"}, - {"invalid.v1", "", "Failed to parse service URL"}, - } - - for _, test := range tests { - t.Run(test.ID, func(t *testing.T) { - url, err := host.ServiceURL(test.ID) - if (err != nil || test.err != "") && - (err == nil || !strings.Contains(err.Error(), test.err)) { - t.Fatalf("unexpected service URL error: %s", err) - } - - var got string - if url != nil { - got = url.String() - } else { - got = "" - } - - if got != test.want { - t.Errorf("wrong result\ngot: %s\nwant: %s", got, test.want) - } - }) - } -} - -func TestVersionConstrains(t *testing.T) { - baseURL, _ := url.Parse("https://example.com/disco/foo.json") - - t.Run("exact service version is provided", func(t *testing.T) { - portStr, close := testVersionsServer(func(w http.ResponseWriter, r *http.Request) { - resp := []byte(` -{ - "service": "%s", - "product": "%s", - "minimum": "0.11.8", - "maximum": "0.12.0" -}`) - // Add the requested service and product to the response. - service := path.Base(r.URL.Path) - product := r.URL.Query().Get("product") - resp = []byte(fmt.Sprintf(string(resp), service, product)) - - w.Header().Add("Content-Type", "application/json") - w.Header().Add("Content-Length", strconv.Itoa(len(resp))) - w.Write(resp) - }) - defer close() - - host := Host{ - discoURL: baseURL, - hostname: "test-server", - transport: httpTransport, - services: map[string]interface{}{ - "thingy.v1": "/api/v1/", - "thingy.v2": "/api/v2/", - "versions.v1": "https://localhost" + portStr + "/v1/versions/", - }, - } - - expected := &Constraints{ - Service: "thingy.v1", - Product: "terraform", - Minimum: "0.11.8", - Maximum: "0.12.0", - } - - actual, err := host.VersionConstraints("thingy.v1", "terraform") - if err != nil { - t.Fatalf("unexpected version constraints error: %s", err) - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("expected %#v, got: %#v", expected, actual) - } - }) - - t.Run("service provided with different versions", func(t *testing.T) { - portStr, close := testVersionsServer(func(w http.ResponseWriter, r *http.Request) { - resp := []byte(` -{ - "service": "%s", - "product": "%s", - "minimum": "0.11.8", - "maximum": "0.12.0" -}`) - // Add the requested service and product to the response. - service := path.Base(r.URL.Path) - product := r.URL.Query().Get("product") - resp = []byte(fmt.Sprintf(string(resp), service, product)) - - w.Header().Add("Content-Type", "application/json") - w.Header().Add("Content-Length", strconv.Itoa(len(resp))) - w.Write(resp) - }) - defer close() - - host := Host{ - discoURL: baseURL, - hostname: "test-server", - transport: httpTransport, - services: map[string]interface{}{ - "thingy.v2": "/api/v2/", - "thingy.v3": "/api/v3/", - "versions.v1": "https://localhost" + portStr + "/v1/versions/", - }, - } - - expected := &Constraints{ - Service: "thingy.v3", - Product: "terraform", - Minimum: "0.11.8", - Maximum: "0.12.0", - } - - actual, err := host.VersionConstraints("thingy.v1", "terraform") - if err != nil { - t.Fatalf("unexpected version constraints error: %s", err) - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("expected %#v, got: %#v", expected, actual) - } - }) - - t.Run("service not provided", func(t *testing.T) { - host := Host{ - discoURL: baseURL, - hostname: "test-server", - transport: httpTransport, - services: map[string]interface{}{ - "versions.v1": "https://localhost/v1/versions/", - }, - } - - _, err := host.VersionConstraints("thingy.v1", "terraform") - if _, ok := err.(*ErrServiceNotProvided); !ok { - t.Fatalf("expected service not provided error, got: %v", err) - } - }) - - t.Run("versions service returns a 404", func(t *testing.T) { - portStr, close := testVersionsServer(nil) - defer close() - - host := Host{ - discoURL: baseURL, - hostname: "test-server", - transport: httpTransport, - services: map[string]interface{}{ - "thingy.v1": "/api/v1/", - "versions.v1": "https://localhost" + portStr + "/v1/non-existent/", - }, - } - - _, err := host.VersionConstraints("thingy.v1", "terraform") - if _, ok := err.(*ErrNoVersionConstraints); !ok { - t.Fatalf("expected service not provided error, got: %v", err) - } - }) - - t.Run("checkpoint is disabled", func(t *testing.T) { - if err := os.Setenv("CHECKPOINT_DISABLE", "1"); err != nil { - t.Fatalf("unexpected error: %v", err) - } - defer os.Unsetenv("CHECKPOINT_DISABLE") - - host := Host{ - discoURL: baseURL, - hostname: "test-server", - transport: httpTransport, - services: map[string]interface{}{ - "thingy.v1": "/api/v1/", - "versions.v1": "https://localhost/v1/versions/", - }, - } - - _, err := host.VersionConstraints("thingy.v1", "terraform") - if _, ok := err.(*ErrNoVersionConstraints); !ok { - t.Fatalf("expected service not provided error, got: %v", err) - } - }) - - t.Run("versions service not discovered", func(t *testing.T) { - host := Host{ - discoURL: baseURL, - hostname: "test-server", - transport: httpTransport, - services: map[string]interface{}{ - "thingy.v1": "/api/v1/", - }, - } - - _, err := host.VersionConstraints("thingy.v1", "terraform") - if _, ok := err.(*ErrServiceNotProvided); !ok { - t.Fatalf("expected service not provided error, got: %v", err) - } - }) - - t.Run("versions service version not discovered", func(t *testing.T) { - host := Host{ - discoURL: baseURL, - hostname: "test-server", - transport: httpTransport, - services: map[string]interface{}{ - "thingy.v1": "/api/v1/", - "versions.v2": "https://localhost/v2/versions/", - }, - } - - _, err := host.VersionConstraints("thingy.v1", "terraform") - if _, ok := err.(*ErrVersionNotSupported); !ok { - t.Fatalf("expected service not provided error, got: %v", err) - } - }) -} - -func testVersionsServer(h func(w http.ResponseWriter, r *http.Request)) (portStr string, close func()) { - server := httptest.NewTLSServer(http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - // Test server always returns 404 if the URL isn't what we expect - if !strings.HasPrefix(r.URL.Path, "/v1/versions/") { - w.WriteHeader(404) - w.Write([]byte("not found")) - return - } - - // If the URL is correct then the given hander decides the response - h(w, r) - }, - )) - - serverURL, _ := url.Parse(server.URL) - - portStr = serverURL.Port() - if portStr != "" { - portStr = ":" + portStr - } - - close = func() { - server.Close() - } - - return portStr, close -} diff --git a/internal/svchost/label_iter.go b/internal/svchost/label_iter.go deleted file mode 100644 index 6e0e47b73f2..00000000000 --- a/internal/svchost/label_iter.go +++ /dev/null @@ -1,49 +0,0 @@ -package svchost - -import ( - "strings" -) - -// A labelIter allows iterating over domain name labels. -// -// This type is copied from golang.org/x/net/idna, where it is used -// to segment hostnames into their separate labels for analysis. We use -// it for the same purpose here, in ForComparison. -type labelIter struct { - orig string - slice []string - curStart int - curEnd int - i int -} - -func (l *labelIter) done() bool { - return l.curStart >= len(l.orig) -} - -func (l *labelIter) label() string { - if l.slice != nil { - return l.slice[l.i] - } - p := strings.IndexByte(l.orig[l.curStart:], '.') - l.curEnd = l.curStart + p - if p == -1 { - l.curEnd = len(l.orig) - } - return l.orig[l.curStart:l.curEnd] -} - -// next sets the value to the next label. It skips the last label if it is empty. -func (l *labelIter) next() { - l.i++ - if l.slice != nil { - if l.i >= len(l.slice) || l.i == len(l.slice)-1 && l.slice[l.i] == "" { - l.curStart = len(l.orig) - } - } else { - l.curStart = l.curEnd + 1 - if l.curStart == len(l.orig)-1 && l.orig[l.curStart] == '.' { - l.curStart = len(l.orig) - } - } -} diff --git a/internal/svchost/svchost.go b/internal/svchost/svchost.go deleted file mode 100644 index 4060b767e58..00000000000 --- a/internal/svchost/svchost.go +++ /dev/null @@ -1,207 +0,0 @@ -// Package svchost deals with the representations of the so-called "friendly -// hostnames" that we use to represent systems that provide Terraform-native -// remote services, such as module registry, remote operations, etc. -// -// Friendly hostnames are specified such that, as much as possible, they -// are consistent with how web browsers think of hostnames, so that users -// can bring their intuitions about how hostnames behave when they access -// a Terraform Enterprise instance's web UI (or indeed any other website) -// and have this behave in a similar way. -package svchost - -import ( - "errors" - "fmt" - "strconv" - "strings" - - "golang.org/x/net/idna" -) - -// Hostname is specialized name for string that indicates that the string -// has been converted to (or was already in) the storage and comparison form. -// -// Hostname values are not suitable for display in the user-interface. Use -// the ForDisplay method to obtain a form suitable for display in the UI. -// -// Unlike user-supplied hostnames, strings of type Hostname (assuming they -// were constructed by a function within this package) can be compared for -// equality using the standard Go == operator. -type Hostname string - -// acePrefix is the ASCII Compatible Encoding prefix, used to indicate that -// a domain name label is in "punycode" form. -const acePrefix = "xn--" - -// displayProfile is a very liberal idna profile that we use to do -// normalization for display without imposing validation rules. -var displayProfile = idna.New( - idna.MapForLookup(), - idna.Transitional(true), -) - -// ForDisplay takes a user-specified hostname and returns a normalized form of -// it suitable for display in the UI. -// -// If the input is so invalid that no normalization can be performed then -// this will return the input, assuming that the caller still wants to -// display _something_. This function is, however, more tolerant than the -// other functions in this package and will make a best effort to prepare -// _any_ given hostname for display. -// -// For validation, use either IsValid (for explicit validation) or -// ForComparison (which implicitly validates, returning an error if invalid). -func ForDisplay(given string) string { - var portPortion string - if colonPos := strings.Index(given, ":"); colonPos != -1 { - given, portPortion = given[:colonPos], given[colonPos:] - } - portPortion, _ = normalizePortPortion(portPortion) - - ascii, err := displayProfile.ToASCII(given) - if err != nil { - return given + portPortion - } - display, err := displayProfile.ToUnicode(ascii) - if err != nil { - return given + portPortion - } - return display + portPortion -} - -// IsValid returns true if the given user-specified hostname is a valid -// service hostname. -// -// Validity is determined by complying with the RFC 5891 requirements for -// names that are valid for domain lookup (section 5), with the additional -// requirement that user-supplied forms must not _already_ contain -// Punycode segments. -func IsValid(given string) bool { - _, err := ForComparison(given) - return err == nil -} - -// ForComparison takes a user-specified hostname and returns a normalized -// form of it suitable for storage and comparison. The result is not suitable -// for display to end-users because it uses Punycode to represent non-ASCII -// characters, and this form is unreadable for non-ASCII-speaking humans. -// -// The result is typed as Hostname -- a specialized name for string -- so that -// other APIs can make it clear within the type system whether they expect a -// user-specified or display-form hostname or a value already normalized for -// comparison. -// -// The returned Hostname is not valid if the returned error is non-nil. -func ForComparison(given string) (Hostname, error) { - var portPortion string - if colonPos := strings.Index(given, ":"); colonPos != -1 { - given, portPortion = given[:colonPos], given[colonPos:] - } - - var err error - portPortion, err = normalizePortPortion(portPortion) - if err != nil { - return Hostname(""), err - } - - if given == "" { - return Hostname(""), fmt.Errorf("empty string is not a valid hostname") - } - - // First we'll apply our additional constraint that Punycode must not - // be given directly by the user. This is not an IDN specification - // requirement, but we prohibit it to force users to use human-readable - // hostname forms within Terraform configuration. - labels := labelIter{orig: given} - for ; !labels.done(); labels.next() { - label := labels.label() - if label == "" { - return Hostname(""), fmt.Errorf( - "hostname contains empty label (two consecutive periods)", - ) - } - if strings.HasPrefix(label, acePrefix) { - return Hostname(""), fmt.Errorf( - "hostname label %q specified in punycode format; service hostnames must be given in unicode", - label, - ) - } - } - - result, err := idna.Lookup.ToASCII(given) - if err != nil { - return Hostname(""), err - } - return Hostname(result + portPortion), nil -} - -// ForDisplay returns a version of the receiver that is appropriate for display -// in the UI. This includes converting any punycode labels to their -// corresponding Unicode characters. -// -// A round-trip through ForComparison and this ForDisplay method does not -// guarantee the same result as calling this package's top-level ForDisplay -// function, since a round-trip through the Hostname type implies stricter -// handling than we do when doing basic display-only processing. -func (h Hostname) ForDisplay() string { - given := string(h) - var portPortion string - if colonPos := strings.Index(given, ":"); colonPos != -1 { - given, portPortion = given[:colonPos], given[colonPos:] - } - // We don't normalize the port portion here because we assume it's - // already been normalized on the way in. - - result, err := idna.Lookup.ToUnicode(given) - if err != nil { - // Should never happen, since type Hostname indicates that a string - // passed through our validation rules. - panic(fmt.Errorf("ForDisplay called on invalid Hostname: %s", err)) - } - return result + portPortion -} - -func (h Hostname) String() string { - return string(h) -} - -func (h Hostname) GoString() string { - return fmt.Sprintf("svchost.Hostname(%q)", string(h)) -} - -// normalizePortPortion attempts to normalize the "port portion" of a hostname, -// which begins with the first colon in the hostname and should be followed -// by a string of decimal digits. -// -// If the port portion is valid, a normalized version of it is returned along -// with a nil error. -// -// If the port portion is invalid, the input string is returned verbatim along -// with a non-nil error. -// -// An empty string is a valid port portion representing the absence of a port. -// If non-empty, the first character must be a colon. -func normalizePortPortion(s string) (string, error) { - if s == "" { - return s, nil - } - - if s[0] != ':' { - // should never happen, since caller tends to guarantee the presence - // of a colon due to how it's extracted from the string. - return s, errors.New("port portion is missing its initial colon") - } - - numStr := s[1:] - num, err := strconv.Atoi(numStr) - if err != nil { - return s, errors.New("port portion contains non-digit characters") - } - if num == 443 { - return "", nil // ":443" is the default - } - if num > 65535 { - return s, errors.New("port number is greater than 65535") - } - return fmt.Sprintf(":%d", num), nil -} diff --git a/internal/svchost/svchost_test.go b/internal/svchost/svchost_test.go deleted file mode 100644 index 2eda3acbe5b..00000000000 --- a/internal/svchost/svchost_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package svchost - -import "testing" - -func TestForDisplay(t *testing.T) { - tests := []struct { - Input string - Want string - }{ - { - "", - "", - }, - { - "example.com", - "example.com", - }, - { - "invalid", - "invalid", - }, - { - "localhost", - "localhost", - }, - { - "localhost:1211", - "localhost:1211", - }, - { - "HashiCorp.com", - "hashicorp.com", - }, - { - "Испытание.com", - "испытание.com", - }, - { - "münchen.de", // this is a precomposed u with diaeresis - "münchen.de", // this is a precomposed u with diaeresis - }, - { - "münchen.de", // this is a separate u and combining diaeresis - "münchen.de", // this is a precomposed u with diaeresis - }, - { - "example.com:443", - "example.com", - }, - { - "example.com:81", - "example.com:81", - }, - { - "example.com:boo", - "example.com:boo", // invalid, but tolerated for display purposes - }, - { - "example.com:boo:boo", - "example.com:boo:boo", // invalid, but tolerated for display purposes - }, - { - "example.com:081", - "example.com:81", - }, - } - - for _, test := range tests { - t.Run(test.Input, func(t *testing.T) { - got := ForDisplay(test.Input) - if got != test.Want { - t.Errorf("wrong result\ninput: %s\ngot: %s\nwant: %s", test.Input, got, test.Want) - } - }) - } -} - -func TestForComparison(t *testing.T) { - tests := []struct { - Input string - Want string - Err bool - }{ - { - "", - "", - true, - }, - { - "example.com", - "example.com", - false, - }, - { - "example.com:443", - "example.com", - false, - }, - { - "example.com:81", - "example.com:81", - false, - }, - { - "example.com:081", - "example.com:81", - false, - }, - { - "invalid", - "invalid", - false, // the "invalid" TLD is, confusingly, a valid hostname syntactically - }, - { - "localhost", // supported for local testing only - "localhost", - false, - }, - { - "localhost:1211", // supported for local testing only - "localhost:1211", - false, - }, - { - "HashiCorp.com", - "hashicorp.com", - false, - }, - { - "1example.com", - "1example.com", - false, - }, - { - "Испытание.com", - "xn--80akhbyknj4f.com", - false, - }, - { - "münchen.de", // this is a precomposed u with diaeresis - "xn--mnchen-3ya.de", - false, - }, - { - "münchen.de", // this is a separate u and combining diaeresis - "xn--mnchen-3ya.de", - false, - }, - { - "blah..blah", - "", - true, - }, - { - "example.com:boo", - "", - true, - }, - { - "example.com:80:boo", - "", - true, - }, - } - - for _, test := range tests { - t.Run(test.Input, func(t *testing.T) { - got, err := ForComparison(test.Input) - if (err != nil) != test.Err { - if test.Err { - t.Error("unexpected success; want error") - } else { - t.Errorf("unexpected error; want success\nerror: %s", err) - } - } - if string(got) != test.Want { - t.Errorf("wrong result\ninput: %s\ngot: %s\nwant: %s", test.Input, got, test.Want) - } - }) - } -} - -func TestHostnameForDisplay(t *testing.T) { - tests := []struct { - Input string - Want string - }{ - { - "example.com", - "example.com", - }, - { - "example.com:81", - "example.com:81", - }, - { - "xn--80akhbyknj4f.com", - "испытание.com", - }, - { - "xn--80akhbyknj4f.com:8080", - "испытание.com:8080", - }, - { - "xn--mnchen-3ya.de", - "münchen.de", // this is a precomposed u with diaeresis - }, - } - - for _, test := range tests { - t.Run(test.Input, func(t *testing.T) { - got := Hostname(test.Input).ForDisplay() - if got != test.Want { - t.Errorf("wrong result\ninput: %s\ngot: %s\nwant: %s", test.Input, got, test.Want) - } - }) - } -} From 4ef565e08c7870c87e135981d56a29a5d95be7aa Mon Sep 17 00:00:00 2001 From: Alex Pilon Date: Wed, 30 Oct 2019 18:23:48 -0400 Subject: [PATCH 2/2] gofmt --- internal/plugin/discovery/get.go | 2 +- internal/plugin/discovery/get_test.go | 4 ++-- internal/registry/client.go | 4 ++-- internal/registry/client_test.go | 4 ++-- internal/registry/test/mock_registry.go | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/plugin/discovery/get.go b/internal/plugin/discovery/get.go index 35be2ee6c3c..367a1f53a0a 100644 --- a/internal/plugin/discovery/get.go +++ b/internal/plugin/discovery/get.go @@ -21,9 +21,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/internal/registry" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/response" - "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" tfversion "github.com/hashicorp/terraform-plugin-sdk/internal/version" + "github.com/hashicorp/terraform-svchost/disco" "github.com/mitchellh/cli" ) diff --git a/internal/plugin/discovery/get_test.go b/internal/plugin/discovery/get_test.go index a7fdd1bd105..3acb22cfe68 100644 --- a/internal/plugin/discovery/get_test.go +++ b/internal/plugin/discovery/get_test.go @@ -17,14 +17,14 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform-plugin-sdk/httpclient" "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" "github.com/hashicorp/terraform-plugin-sdk/internal/registry" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/response" + "github.com/hashicorp/terraform-plugin-sdk/internal/version" "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/disco" "github.com/mitchellh/cli" - "github.com/hashicorp/terraform-plugin-sdk/httpclient" - "github.com/hashicorp/terraform-plugin-sdk/internal/version" ) const testProviderFile = "test provider binary" diff --git a/internal/registry/client.go b/internal/registry/client.go index 4d4a84c6b2f..4ef22052c3d 100644 --- a/internal/registry/client.go +++ b/internal/registry/client.go @@ -11,13 +11,13 @@ import ( "strings" "time" - internalhttpclient "github.com/hashicorp/terraform-plugin-sdk/internal/httpclient" "github.com/hashicorp/terraform-plugin-sdk/httpclient" + internalhttpclient "github.com/hashicorp/terraform-plugin-sdk/internal/httpclient" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/response" + "github.com/hashicorp/terraform-plugin-sdk/internal/version" "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/disco" - "github.com/hashicorp/terraform-plugin-sdk/internal/version" ) const ( diff --git a/internal/registry/client_test.go b/internal/registry/client_test.go index e2f52869e8a..c4fad7f1072 100644 --- a/internal/registry/client_test.go +++ b/internal/registry/client_test.go @@ -7,11 +7,11 @@ import ( "testing" version "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-sdk/httpclient" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/test" - "github.com/hashicorp/terraform-svchost/disco" tfversion "github.com/hashicorp/terraform-plugin-sdk/internal/version" - "github.com/hashicorp/terraform-plugin-sdk/httpclient" + "github.com/hashicorp/terraform-svchost/disco" ) func TestLookupModuleVersions(t *testing.T) { diff --git a/internal/registry/test/mock_registry.go b/internal/registry/test/mock_registry.go index 9695973064e..7a4d87ce973 100644 --- a/internal/registry/test/mock_registry.go +++ b/internal/registry/test/mock_registry.go @@ -10,13 +10,13 @@ import ( "regexp" "strings" + "github.com/hashicorp/terraform-plugin-sdk/httpclient" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" "github.com/hashicorp/terraform-plugin-sdk/internal/registry/response" + tfversion "github.com/hashicorp/terraform-plugin-sdk/internal/version" "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/auth" "github.com/hashicorp/terraform-svchost/disco" - tfversion "github.com/hashicorp/terraform-plugin-sdk/internal/version" - "github.com/hashicorp/terraform-plugin-sdk/httpclient" ) // Disco return a *disco.Disco mapping registry.terraform.io, localhost,