diff --git a/cmd/tofu/commands.go b/cmd/tofu/commands.go index 7df4b6dc48..241d538c2b 100644 --- a/cmd/tofu/commands.go +++ b/cmd/tofu/commands.go @@ -12,10 +12,10 @@ import ( "github.com/hashicorp/go-plugin" "github.com/hashicorp/go-retryablehttp" - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/auth" - "github.com/hashicorp/terraform-svchost/disco" "github.com/mitchellh/cli" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" + "github.com/opentofu/svchost/svcauth" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/command" @@ -486,7 +486,7 @@ func makeShutdownCh() <-chan struct{} { return resultCh } -func credentialsSource(config *cliconfig.Config) (auth.CredentialsSource, error) { +func credentialsSource(config *cliconfig.Config) (svcauth.CredentialsSource, error) { helperPlugins := pluginDiscovery.FindPlugins("credentials", globalPluginDirs()) return config.CredentialsSource(helperPlugins) } diff --git a/cmd/tofu/provider_source.go b/cmd/tofu/provider_source.go index 30b4a4ba37..fe385365d2 100644 --- a/cmd/tofu/provider_source.go +++ b/cmd/tofu/provider_source.go @@ -14,7 +14,7 @@ import ( "path/filepath" "github.com/apparentlymart/go-userdirs/userdirs" - "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost/disco" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/command/cliconfig" diff --git a/cmd/tofu/registries_disco.go b/cmd/tofu/registries_disco.go index 897449d778..c7d793f179 100644 --- a/cmd/tofu/registries_disco.go +++ b/cmd/tofu/registries_disco.go @@ -13,11 +13,11 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - "github.com/hashicorp/terraform-svchost/auth" - "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost/disco" + "github.com/opentofu/svchost/svcauth" + "github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/logging" - "github.com/opentofu/opentofu/version" ) const ( @@ -43,27 +43,15 @@ const ( // object should obtain authentication credentials for service discovery // requests. Passing a nil credSrc is acceptable and means that all discovery // requests are to be made anonymously. -func newServiceDiscovery(_ context.Context, credSrc auth.CredentialsSource) *disco.Disco { - services := disco.NewWithCredentialsSource(credSrc) - services.SetUserAgent(httpclient.OpenTofuUserAgent(version.String())) - +func newServiceDiscovery(ctx context.Context, credSrc svcauth.CredentialsSource) *disco.Disco { // For historical reasons, the registry request retry policy also applies // to all service discovery requests, which we implement by using transport - // from a HTTP client that is configured for registry client use. - // - // TEMP: The disco.Disco API isn't yet set up to pass through - // context.Context, so we're intentionally ignoring the passed-in ctx - // here to prevent the created client from having OpenTelemetry - // instrumentation added to it. This is just a low-risk temporary trick - // for the v1.10 release; we intend to update disco.Disco to properly - // support context.Context at some point during the v1.11 development - // period. This relies on the fact that httpclient.New uses the context - // we're (indirectly) passing it only to find out if there's an active - // OpenTelemetry span, which should be a valid assumption for as long as - // this very temporary workaround lasts. - client := newRegistryHTTPClient(context.TODO()) - services.Transport = client.HTTPClient.Transport - + // from a HTTP httpClient that is configured for registry httpClient use. + registryHTTPClient := newRegistryHTTPClient(ctx) + services := disco.New( + disco.WithHTTPClient(registryHTTPClient.HTTPClient), + disco.WithCredentials(credSrc), + ) return services } diff --git a/go.mod b/go.mod index 3f15dce4ba..9a3864acf6 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,6 @@ require ( github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.20.1 github.com/hashicorp/jsonapi v1.3.1 - github.com/hashicorp/terraform-svchost v0.1.1 github.com/jmespath/go-jmespath v0.4.0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/lib/pq v1.10.3 @@ -78,7 +77,8 @@ require ( github.com/openbao/openbao/api/v2 v2.1.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 - github.com/opentofu/registry-address v0.0.0-20230920144404-f1e51167f633 + github.com/opentofu/registry-address/v2 v2.0.0-20250611143131-d0a99bd8acdd + github.com/opentofu/svchost v0.0.0-20250610175836-86c9e5e3d8c8 github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db github.com/pkg/errors v0.9.1 github.com/posener/complete v1.2.3 @@ -99,14 +99,14 @@ require ( go.opentelemetry.io/otel/sdk v1.35.0 go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/mock v0.4.0 - golang.org/x/crypto v0.35.0 + golang.org/x/crypto v0.38.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/mod v0.21.0 - golang.org/x/net v0.36.0 - golang.org/x/oauth2 v0.16.0 - golang.org/x/sys v0.30.0 - golang.org/x/term v0.29.0 - golang.org/x/text v0.22.0 + golang.org/x/net v0.40.0 + golang.org/x/oauth2 v0.30.0 + golang.org/x/sys v0.33.0 + golang.org/x/term v0.32.0 + golang.org/x/text v0.25.0 golang.org/x/tools v0.25.0 google.golang.org/api v0.155.0 google.golang.org/grpc v1.62.1 @@ -122,8 +122,7 @@ require ( require ( cloud.google.com/go v0.112.0 // indirect - cloud.google.com/go/compute v1.23.3 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/iam v1.1.5 // indirect github.com/AlecAivazis/survey/v2 v2.3.6 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect @@ -258,9 +257,8 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect - golang.org/x/sync v0.11.0 // indirect + golang.org/x/sync v0.14.0 // indirect golang.org/x/time v0.9.0 // indirect - google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect diff --git a/go.sum b/go.sum index a8e7efdc6e..00b75346de 100644 --- a/go.sum +++ b/go.sum @@ -70,10 +70,8 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= @@ -723,8 +721,6 @@ github.com/hashicorp/serf v0.9.6 h1:uuEX1kLR6aoda1TBttmJQKDLZE1Ob7KN0NPdE7EtCDc= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= -github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= @@ -927,8 +923,10 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opentofu/hcl/v2 v2.20.2-0.20250121132637-504036cd70e7 h1:QHUIrylb/q3pQdQcnAr74cGWsXS1lmA8GqP+RWFMK6U= github.com/opentofu/hcl/v2 v2.20.2-0.20250121132637-504036cd70e7/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= -github.com/opentofu/registry-address v0.0.0-20230920144404-f1e51167f633 h1:81TBkM/XGIFlVvyabp0CJl00UHeVUiQjz0fddLMi848= -github.com/opentofu/registry-address v0.0.0-20230920144404-f1e51167f633/go.mod h1:HzQhpVo/NJnGmN+7FPECCVCA5ijU7AUcvf39enBKYOc= +github.com/opentofu/registry-address/v2 v2.0.0-20250611143131-d0a99bd8acdd h1:YAAnzmyOoMvm5SuGXL4hhlfBgqz92XDfORGPV3kmQFc= +github.com/opentofu/registry-address/v2 v2.0.0-20250611143131-d0a99bd8acdd/go.mod h1:7M92SvuJm1WBriIpa4j0XmruU9pxkgPXmRdc6FfAvAk= +github.com/opentofu/svchost v0.0.0-20250610175836-86c9e5e3d8c8 h1:J3pmsVB+nGdfNp5HWdEAC96asYgc7S6J724ICrYDCTk= +github.com/opentofu/svchost v0.0.0-20250610175836-86c9e5e3d8c8/go.mod h1:0kKTcD9hUrbAz41GWp8USa/+OuI8QKirU3qdCWNa3jI= github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db h1:9uViuKtx1jrlXLBW/pMnhOfzn3iSEdLase/But/IZRU= github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -1136,8 +1134,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1246,8 +1244,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= -golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1273,8 +1271,8 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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= @@ -1290,8 +1288,8 @@ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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= @@ -1395,8 +1393,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1407,8 +1405,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= @@ -1419,14 +1417,13 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1560,8 +1557,6 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/internal/addrs/module_package.go b/internal/addrs/module_package.go index bdf59bacbd..e70fceed2a 100644 --- a/internal/addrs/module_package.go +++ b/internal/addrs/module_package.go @@ -6,7 +6,7 @@ package addrs import ( - tfaddr "github.com/opentofu/registry-address" + regaddr "github.com/opentofu/registry-address/v2" ) // A ModulePackage represents a physical location where OpenTofu can retrieve @@ -48,4 +48,4 @@ func (p ModulePackage) String() string { // registry in order to find a real module package address. These being // distinct is intended to help future maintainers more easily follow the // series of steps in the module installer, with the help of the type checker. -type ModuleRegistryPackage = tfaddr.ModulePackage +type ModuleRegistryPackage = regaddr.ModulePackage diff --git a/internal/addrs/module_source.go b/internal/addrs/module_source.go index 463d1203c8..5c258533e7 100644 --- a/internal/addrs/module_source.go +++ b/internal/addrs/module_source.go @@ -11,7 +11,7 @@ import ( "strings" "github.com/opentofu/opentofu/internal/getmodules" - tfaddr "github.com/opentofu/registry-address" + regaddr "github.com/opentofu/registry-address/v2" ) // ModuleSource is the general type for all three of the possible module source @@ -201,11 +201,11 @@ func (s ModuleSourceLocal) ForDisplay() string { // combination of a ModuleSourceRegistry and a module version number into // a concrete ModuleSourceRemote that OpenTofu will then download and // install. -type ModuleSourceRegistry tfaddr.Module +type ModuleSourceRegistry regaddr.Module // DefaultModuleRegistryHost is the hostname used for registry-based module // source addresses that do not have an explicit hostname. -const DefaultModuleRegistryHost = tfaddr.DefaultModuleRegistryHost +const DefaultModuleRegistryHost = regaddr.DefaultModuleRegistryHost // ParseModuleSourceRegistry is a variant of ParseModuleSource which only // accepts module registry addresses, and will reject any other address type. @@ -222,7 +222,7 @@ func ParseModuleSourceRegistry(raw string) (ModuleSource, error) { return ModuleSourceRegistry{}, fmt.Errorf("can't use local directory %q as a module registry address", raw) } - src, err := tfaddr.ParseModuleSource(raw) + src, err := regaddr.ParseModuleSource(raw) if err != nil { return nil, err } diff --git a/internal/addrs/module_source_test.go b/internal/addrs/module_source_test.go index aa166ca2d9..aaa6c2811d 100644 --- a/internal/addrs/module_source_test.go +++ b/internal/addrs/module_source_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" ) func TestParseModuleSource(t *testing.T) { diff --git a/internal/addrs/provider.go b/internal/addrs/provider.go index e6a17d9311..78ffa7a445 100644 --- a/internal/addrs/provider.go +++ b/internal/addrs/provider.go @@ -7,23 +7,32 @@ package addrs import ( "github.com/hashicorp/hcl/v2" - svchost "github.com/hashicorp/terraform-svchost" + regaddr "github.com/opentofu/registry-address/v2" + "github.com/opentofu/svchost" + "github.com/opentofu/opentofu/internal/tfdiags" - tfaddr "github.com/opentofu/registry-address" ) // Provider encapsulates a single provider type. In the future this will be // extended to include additional fields including Namespace and SourceHost -type Provider = tfaddr.Provider +type Provider = regaddr.Provider // DefaultProviderRegistryHost is the hostname used for provider addresses that do // not have an explicit hostname. -const DefaultProviderRegistryHost = tfaddr.DefaultProviderRegistryHost +const DefaultProviderRegistryHost = regaddr.DefaultProviderRegistryHost // BuiltInProviderHost is the pseudo-hostname used for the "built-in" provider // namespace. Built-in provider addresses must also have their namespace set // to BuiltInProviderNamespace in order to be considered as built-in. -const BuiltInProviderHost = tfaddr.BuiltInProviderHost +// +// Since we currently only have one built-in provider and it was inherited +// from OpenTofu's predecessor, we currently exclusively use the "transitional" +// builtin provider host that matches what the predecessor used, thereby +// helping with cross-compatibility. If we introduce any OpenTofu-specific +// built-in providers in future then we should consider using +// [regaddr.BuiltInProviderHost] for those ones instead, since that one +// uses a hostname that belongs to the OpenTofu project. +const BuiltInProviderHost = regaddr.TransitionalBuiltInProviderHost // BuiltInProviderNamespace is the provider namespace used for "built-in" // providers. Built-in provider addresses must also have their hostname @@ -32,14 +41,14 @@ const BuiltInProviderHost = tfaddr.BuiltInProviderHost // The this namespace is literally named "builtin", in the hope that users // who see FQNs containing this will be able to infer the way in which they are // special, even if they haven't encountered the concept formally yet. -const BuiltInProviderNamespace = tfaddr.BuiltInProviderNamespace +const BuiltInProviderNamespace = regaddr.BuiltInProviderNamespace // LegacyProviderNamespace is the special string used in the Namespace field // of type Provider to mark a legacy provider address. This special namespace // value would normally be invalid, and can be used only when the hostname is // DefaultRegistryHost because that host owns the mapping from legacy name to // FQN. -const LegacyProviderNamespace = tfaddr.LegacyProviderNamespace +const LegacyProviderNamespace = regaddr.LegacyProviderNamespace func IsDefaultProvider(addr Provider) bool { return addr.Hostname == DefaultProviderRegistryHost && addr.Namespace == "hashicorp" @@ -57,7 +66,7 @@ func IsDefaultProvider(addr Provider) bool { // When accepting namespace or type values from outside the program, use // ParseProviderPart first to check that the given value is valid. func NewProvider(hostname svchost.Hostname, namespace, typeName string) Provider { - return tfaddr.NewProvider(hostname, namespace, typeName) + return regaddr.NewProvider(hostname, namespace, typeName) } // ImpliedProviderForUnqualifiedType represents the rules for inferring what @@ -87,7 +96,7 @@ func ImpliedProviderForUnqualifiedType(typeName string) Provider { // NewDefaultProvider returns the default address of a HashiCorp-maintained, // Registry-hosted provider. func NewDefaultProvider(name string) Provider { - return tfaddr.Provider{ + return regaddr.Provider{ Type: MustParseProviderPart(name), Namespace: "hashicorp", Hostname: DefaultProviderRegistryHost, @@ -97,7 +106,7 @@ func NewDefaultProvider(name string) Provider { // NewBuiltInProvider returns the address of a "built-in" provider. See // the docs for Provider.IsBuiltIn for more information. func NewBuiltInProvider(name string) Provider { - return tfaddr.Provider{ + return regaddr.Provider{ Type: MustParseProviderPart(name), Namespace: BuiltInProviderNamespace, Hostname: BuiltInProviderHost, @@ -127,11 +136,11 @@ func NewLegacyProvider(name string) Provider { // - name // - namespace/name // - hostname/namespace/name -func ParseProviderSourceString(str string) (tfaddr.Provider, tfdiags.Diagnostics) { +func ParseProviderSourceString(str string) (regaddr.Provider, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - ret, err := tfaddr.ParseProviderSource(str) - if pe, ok := err.(*tfaddr.ParserError); ok { + ret, err := regaddr.ParseProviderSource(str) + if pe, ok := err.(*regaddr.ParserError); ok { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: pe.Summary, @@ -184,7 +193,7 @@ func MustParseProviderSourceString(str string) Provider { // It's valid to pass the result of this function as the argument to a // subsequent call, in which case the result will be identical. func ParseProviderPart(given string) (string, error) { - return tfaddr.ParseProviderPart(given) + return regaddr.ParseProviderPart(given) } // MustParseProviderPart is a wrapper around ParseProviderPart that panics if diff --git a/internal/addrs/provider_test.go b/internal/addrs/provider_test.go index 261c9fe956..dd72a3c488 100644 --- a/internal/addrs/provider_test.go +++ b/internal/addrs/provider_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/go-test/deep" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" ) func TestProviderString(t *testing.T) { diff --git a/internal/backend/backend.go b/internal/backend/backend.go index cd599f5ea6..d903f920ed 100644 --- a/internal/backend/backend.go +++ b/internal/backend/backend.go @@ -15,8 +15,10 @@ import ( "log" "os" - svchost "github.com/hashicorp/terraform-svchost" "github.com/mitchellh/go-homedir" + "github.com/opentofu/svchost" + "github.com/zclconf/go-cty/cty" + "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/command/clistate" "github.com/opentofu/opentofu/internal/command/views" @@ -31,7 +33,6 @@ import ( "github.com/opentofu/opentofu/internal/states/statemgr" "github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tofu" - "github.com/zclconf/go-cty/cty" ) // DefaultStateName is the name of the default, initial state that every diff --git a/internal/backend/init/init.go b/internal/backend/init/init.go index 02f7aa6529..800c86ddd7 100644 --- a/internal/backend/init/init.go +++ b/internal/backend/init/init.go @@ -10,12 +10,10 @@ package init import ( "sync" - "github.com/hashicorp/terraform-svchost/disco" - "github.com/opentofu/opentofu/internal/backend" - "github.com/opentofu/opentofu/internal/encryption" - "github.com/opentofu/opentofu/internal/tfdiags" + "github.com/opentofu/svchost/disco" "github.com/zclconf/go-cty/cty" + "github.com/opentofu/opentofu/internal/backend" backendLocal "github.com/opentofu/opentofu/internal/backend/local" backendRemote "github.com/opentofu/opentofu/internal/backend/remote" backendAzure "github.com/opentofu/opentofu/internal/backend/remote-state/azure" @@ -29,6 +27,8 @@ import ( backendPg "github.com/opentofu/opentofu/internal/backend/remote-state/pg" backendS3 "github.com/opentofu/opentofu/internal/backend/remote-state/s3" backendCloud "github.com/opentofu/opentofu/internal/cloud" + "github.com/opentofu/opentofu/internal/encryption" + "github.com/opentofu/opentofu/internal/tfdiags" ) // backends is the list of available backends. This is a global variable diff --git a/internal/backend/remote/backend.go b/internal/backend/remote/backend.go index 78c6e4d9d5..6370de5343 100644 --- a/internal/backend/remote/backend.go +++ b/internal/backend/remote/backend.go @@ -19,13 +19,15 @@ import ( tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/disco" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" + "github.com/opentofu/svchost/svcauth" "github.com/zclconf/go-cty/cty" "github.com/opentofu/opentofu/internal/backend" + backendLocal "github.com/opentofu/opentofu/internal/backend/local" "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/httpclient" @@ -35,8 +37,6 @@ import ( "github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tofu" tfversion "github.com/opentofu/opentofu/version" - - backendLocal "github.com/opentofu/opentofu/internal/backend/local" ) const ( @@ -273,15 +273,18 @@ func (b *Remote) Configure(ctx context.Context, obj cty.Value) tfdiags.Diagnosti // Discover the service URL for this host to confirm that it provides // a remote backend API and to get the version constraints. - service, constraints, err := b.discover(serviceID) + service, err := b.discover(serviceID) - // First check any constraints we might have received. - if constraints != nil { - diags = diags.Append(b.checkConstraints(constraints)) - if diags.HasErrors() { - return diags - } - } + // Historical note: in OpenTofu's predecessor project there was an + // extra step here of checking some metadata returned by the remote + // API describing which versions of the predecessor's CLI it considers + // itself to be compatible with. Since OpenTofu's version numbers have + // little relationship with those of its predecessor, and since this + // API is intended for interacting with the commercial service offered + // by the predecessor's vendor (so highly unlikely to be set with + // OpenTofu's releases in mind) we just skip that here and let the + // subsequent requests fail if the remote API isn't compatible with + // the current implementation. // When we don't have any constraints errors, also check for discovery // errors before we continue. @@ -389,127 +392,24 @@ func (b *Remote) Configure(ctx context.Context, obj cty.Value) tfdiags.Diagnosti } // discover the remote backend API service URL and version constraints. -func (b *Remote) discover(serviceID string) (*url.URL, *disco.Constraints, error) { +func (b *Remote) discover(serviceID string) (*url.URL, error) { hostname, err := svchost.ForComparison(b.hostname) if err != nil { - return nil, nil, err + return nil, err } - host, err := b.services.Discover(hostname) + host, err := b.services.Discover(context.TODO(), hostname) if err != nil { - return nil, nil, err + return nil, err } service, err := host.ServiceURL(serviceID) // Return the error, unless its a disco.ErrVersionNotSupported error. if _, ok := err.(*disco.ErrVersionNotSupported); !ok && err != nil { - return nil, nil, err + return nil, err } - // We purposefully ignore the error and return the previous error, as - // checking for version constraints is considered optional. - constraints, _ := host.VersionConstraints(serviceID, "terraform") - - return service, constraints, err -} - -// checkConstraints checks service version constrains against our own -// version and returns rich and informational diagnostics in case any -// incompatibilities are detected. -func (b *Remote) checkConstraints(c *disco.Constraints) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - if c == nil || c.Minimum == "" || c.Maximum == "" { - return diags - } - - // Generate a parsable constraints string. - excluding := "" - if len(c.Excluding) > 0 { - excluding = fmt.Sprintf(", != %s", strings.Join(c.Excluding, ", != ")) - } - constStr := fmt.Sprintf(">= %s%s, <= %s", c.Minimum, excluding, c.Maximum) - - // Create the constraints to check against. - constraints, err := version.NewConstraint(constStr) - if err != nil { - return diags.Append(checkConstraintsWarning(err)) - } - - // Create the version to check. - v, err := version.NewVersion(tfversion.Version) - if err != nil { - return diags.Append(checkConstraintsWarning(err)) - } - - // Return if we satisfy all constraints. - if constraints.Check(v) { - return diags - } - - // Find out what action (upgrade/downgrade) we should advice. - minimum, err := version.NewVersion(c.Minimum) - if err != nil { - return diags.Append(checkConstraintsWarning(err)) - } - - maximum, err := version.NewVersion(c.Maximum) - if err != nil { - return diags.Append(checkConstraintsWarning(err)) - } - - var excludes []*version.Version - for _, exclude := range c.Excluding { - v, err := version.NewVersion(exclude) - if err != nil { - return diags.Append(checkConstraintsWarning(err)) - } - excludes = append(excludes, v) - } - - // Sort all the excludes. - sort.Sort(version.Collection(excludes)) - - var action, toVersion string - switch { - case minimum.GreaterThan(v): - action = "upgrade" - toVersion = ">= " + minimum.String() - case maximum.LessThan(v): - action = "downgrade" - toVersion = "<= " + maximum.String() - case len(excludes) > 0: - // Get the latest excluded version. - action = "upgrade" - toVersion = "> " + excludes[len(excludes)-1].String() - } - - switch { - case len(excludes) == 1: - excluding = fmt.Sprintf(", excluding version %s", excludes[0].String()) - case len(excludes) > 1: - var vs []string - for _, v := range excludes { - vs = append(vs, v.String()) - } - excluding = fmt.Sprintf(", excluding versions %s", strings.Join(vs, ", ")) - default: - excluding = "" - } - - summary := fmt.Sprintf("Incompatible OpenTofu version v%s", v.String()) - details := fmt.Sprintf( - "The configured remote backend is compatible with OpenTofu "+ - "versions >= %s, <= %s%s.", c.Minimum, c.Maximum, excluding, - ) - - if action != "" && toVersion != "" { - summary = fmt.Sprintf("Please %s OpenTofu to %s", action, toVersion) - details += fmt.Sprintf(" Please %s to a supported version and try again.", action) - } - - // Return the customized and informational error message. - return diags.Append(tfdiags.Sourceless(tfdiags.Error, summary, details)) + return service, nil } // token returns the token for this host as configured in the credentials @@ -520,12 +420,24 @@ func (b *Remote) token() (string, error) { if err != nil { return "", err } - creds, err := b.services.CredentialsForHost(hostname) + creds, err := b.services.CredentialsForHost(context.TODO(), hostname) if err != nil { log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err) return "", nil } - if creds != nil { + + // HostCredentialsWithToken is a variant of [svcauth.HostCredentials] + // that also offers direct access to a stored token. This is a weird + // need that applies only to this legacy cloud backend since it uses + // a client library for a particular vendor's API that isn't designed + // to integrate with svcauth. This is a surgical patch to keep this + // working similarly to how it did in our predecessor project until + // we decide on a more definite future for this backend. + type HostCredentialsWithToken interface { + svcauth.HostCredentials + Token() string + } + if creds, ok := creds.(HostCredentialsWithToken); ok { return creds.Token(), nil } return "", nil diff --git a/internal/backend/remote/backend_test.go b/internal/backend/remote/backend_test.go index 98bf3f5eac..efd8d4711c 100644 --- a/internal/backend/remote/backend_test.go +++ b/internal/backend/remote/backend_test.go @@ -14,15 +14,14 @@ import ( tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-svchost/disco" + "github.com/zclconf/go-cty/cty" + "github.com/opentofu/opentofu/internal/backend" + backendLocal "github.com/opentofu/opentofu/internal/backend/local" "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/states/statemgr" "github.com/opentofu/opentofu/internal/tfdiags" tfversion "github.com/opentofu/opentofu/version" - "github.com/zclconf/go-cty/cty" - - backendLocal "github.com/opentofu/opentofu/internal/backend/local" ) func TestRemote(t *testing.T) { @@ -172,84 +171,6 @@ func TestRemote_config(t *testing.T) { } } -func TestRemote_versionConstraints(t *testing.T) { - cases := map[string]struct { - config cty.Value - prerelease string - version string - result string - }{ - "compatible version": { - config: cty.ObjectVal(map[string]cty.Value{ - "hostname": cty.StringVal(mockedBackendHost), - "organization": cty.StringVal("hashicorp"), - "token": cty.NullVal(cty.String), - "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "prefix": cty.NullVal(cty.String), - }), - }), - version: "0.11.1", - }, - "version too old": { - config: cty.ObjectVal(map[string]cty.Value{ - "hostname": cty.StringVal(mockedBackendHost), - "organization": cty.StringVal("hashicorp"), - "token": cty.NullVal(cty.String), - "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "prefix": cty.NullVal(cty.String), - }), - }), - version: "0.0.1", - result: "upgrade OpenTofu to >= 0.1.0", - }, - "version too new": { - config: cty.ObjectVal(map[string]cty.Value{ - "hostname": cty.StringVal(mockedBackendHost), - "organization": cty.StringVal("hashicorp"), - "token": cty.NullVal(cty.String), - "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "prefix": cty.NullVal(cty.String), - }), - }), - version: "10.0.1", - result: "downgrade OpenTofu to <= 10.0.0", - }, - } - - // Save and restore the actual version. - p := tfversion.Prerelease - v := tfversion.Version - defer func() { - tfversion.Prerelease = p - tfversion.Version = v - }() - - for name, tc := range cases { - s := testServer(t) - b := New(testDisco(s), encryption.StateEncryptionDisabled()) - - // Set the version for this test. - tfversion.Prerelease = tc.prerelease - tfversion.Version = tc.version - - // Validate - _, valDiags := b.PrepareConfig(tc.config) - if valDiags.HasErrors() { - t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) - } - - // Configure - confDiags := b.Configure(t.Context(), tc.config) - if (confDiags.Err() != nil || tc.result != "") && - (confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.result)) { - t.Fatalf("%s: unexpected configure result: %v", name, confDiags.Err()) - } - } -} - func TestRemote_localBackend(t *testing.T) { b, bCleanup := testBackendDefault(t) defer bCleanup() @@ -371,108 +292,6 @@ func TestRemote_addAndRemoveWorkspacesNoDefault(t *testing.T) { } } -func TestRemote_checkConstraints(t *testing.T) { - b, bCleanup := testBackendDefault(t) - defer bCleanup() - - cases := map[string]struct { - constraints *disco.Constraints - prerelease string - version string - result string - }{ - "compatible version": { - constraints: &disco.Constraints{ - Minimum: "0.11.0", - Maximum: "0.11.11", - }, - version: "0.11.1", - result: "", - }, - "version too old": { - constraints: &disco.Constraints{ - Minimum: "0.11.0", - Maximum: "0.11.11", - }, - version: "0.10.1", - result: "upgrade OpenTofu to >= 0.11.0", - }, - "version too new": { - constraints: &disco.Constraints{ - Minimum: "0.11.0", - Maximum: "0.11.11", - }, - version: "0.12.0", - result: "downgrade OpenTofu to <= 0.11.11", - }, - "version excluded - ordered": { - constraints: &disco.Constraints{ - Minimum: "0.11.0", - Excluding: []string{"0.11.7", "0.11.8"}, - Maximum: "0.11.11", - }, - version: "0.11.7", - result: "upgrade OpenTofu to > 0.11.8", - }, - "version excluded - unordered": { - constraints: &disco.Constraints{ - Minimum: "0.11.0", - Excluding: []string{"0.11.8", "0.11.6"}, - Maximum: "0.11.11", - }, - version: "0.11.6", - result: "upgrade OpenTofu to > 0.11.8", - }, - "list versions": { - constraints: &disco.Constraints{ - Minimum: "0.11.0", - Maximum: "0.11.11", - }, - version: "0.10.1", - result: "versions >= 0.11.0, <= 0.11.11.", - }, - "list exclusion": { - constraints: &disco.Constraints{ - Minimum: "0.11.0", - Excluding: []string{"0.11.6"}, - Maximum: "0.11.11", - }, - version: "0.11.6", - result: "excluding version 0.11.6.", - }, - "list exclusions": { - constraints: &disco.Constraints{ - Minimum: "0.11.0", - Excluding: []string{"0.11.8", "0.11.6"}, - Maximum: "0.11.11", - }, - version: "0.11.6", - result: "excluding versions 0.11.6, 0.11.8.", - }, - } - - // Save and restore the actual version. - p := tfversion.Prerelease - v := tfversion.Version - defer func() { - tfversion.Prerelease = p - tfversion.Version = v - }() - - for name, tc := range cases { - // Set the version for this test. - tfversion.Prerelease = tc.prerelease - tfversion.Version = tc.version - - // Check the constraints. - diags := b.checkConstraints(tc.constraints) - if (diags.Err() != nil || tc.result != "") && - (diags.Err() == nil || !strings.Contains(diags.Err().Error(), tc.result)) { - t.Fatalf("%s: unexpected constraints result: %v", name, diags.Err()) - } - } -} - func TestRemote_StateMgr_versionCheck(t *testing.T) { b, bCleanup := testBackendDefault(t) defer bCleanup() diff --git a/internal/backend/remote/testing.go b/internal/backend/remote/testing.go index 7754dad554..c20f7f0612 100644 --- a/internal/backend/remote/testing.go +++ b/internal/backend/remote/testing.go @@ -16,24 +16,22 @@ import ( "time" tfe "github.com/hashicorp/go-tfe" - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/auth" - "github.com/hashicorp/terraform-svchost/disco" "github.com/mitchellh/cli" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" + "github.com/opentofu/svchost/svcauth" + "github.com/zclconf/go-cty/cty" + "github.com/opentofu/opentofu/internal/backend" + backendLocal "github.com/opentofu/opentofu/internal/backend/local" "github.com/opentofu/opentofu/internal/cloud" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/encryption" - "github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/states/remote" "github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tofu" - "github.com/opentofu/opentofu/version" - "github.com/zclconf/go-cty/cty" - - backendLocal "github.com/opentofu/opentofu/internal/backend/local" ) const ( @@ -42,8 +40,8 @@ const ( var ( mockedBackendHost = "app.example.com" - credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ - svchost.Hostname(mockedBackendHost): {"token": testCred}, + credsSrc = svcauth.StaticCredentialsSource(map[svchost.Hostname]svcauth.HostCredentials{ + svchost.Hostname(mockedBackendHost): svcauth.HostCredentialsToken(testCred), }) ) @@ -68,10 +66,12 @@ func (m *mockInput) Input(ctx context.Context, opts *tofu.InputOpts) (string, er } func testInput(t *testing.T, answers map[string]string) *mockInput { + t.Helper() return &mockInput{answers: answers} } func testBackendDefault(t *testing.T) (*Remote, func()) { + t.Helper() obj := cty.ObjectVal(map[string]cty.Value{ "hostname": cty.StringVal(mockedBackendHost), "organization": cty.StringVal("hashicorp"), @@ -98,6 +98,7 @@ func testBackendNoDefault(t *testing.T) (*Remote, func()) { } func testBackendNoOperations(t *testing.T) (*Remote, func()) { + t.Helper() obj := cty.ObjectVal(map[string]cty.Value{ "hostname": cty.StringVal(mockedBackendHost), "organization": cty.StringVal("no-operations"), @@ -111,6 +112,7 @@ func testBackendNoOperations(t *testing.T) (*Remote, func()) { } func testRemoteClient(t *testing.T) remote.Client { + t.Helper() b, bCleanup := testBackendDefault(t) defer bCleanup() @@ -123,6 +125,8 @@ func testRemoteClient(t *testing.T) remote.Client { } func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) { + t.Helper() + s := testServer(t) b := New(testDisco(s), encryption.StateEncryptionDisabled()) @@ -182,6 +186,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) { } func testLocalBackend(t *testing.T, remote *Remote) backend.Enhanced { + t.Helper() b := backendLocal.NewWithBackend(remote, nil) // Add a test provider to the local backend. @@ -205,6 +210,7 @@ func testLocalBackend(t *testing.T, remote *Remote) backend.Enhanced { // testServer returns a *httptest.Server used for local testing. func testServer(t *testing.T) *httptest.Server { + t.Helper() mux := http.NewServeMux() // Respond to service discovery calls. @@ -297,8 +303,10 @@ func testDisco(s *httptest.Server) *disco.Disco { "tfe.v2.1": fmt.Sprintf("%s/api/v2/", s.URL), "versions.v1": fmt.Sprintf("%s/v1/versions/", s.URL), } - d := disco.NewWithCredentialsSource(credsSrc) - d.SetUserAgent(httpclient.OpenTofuUserAgent(version.String())) + d := disco.New( + disco.WithCredentials(credsSrc), + disco.WithHTTPClient(s.Client()), + ) d.ForceHostServices(svchost.Hostname(mockedBackendHost), services) d.ForceHostServices(svchost.Hostname("localhost"), services) diff --git a/internal/cloud/backend.go b/internal/cloud/backend.go index e5accf8fab..a9e648e257 100644 --- a/internal/cloud/backend.go +++ b/internal/cloud/backend.go @@ -20,10 +20,11 @@ import ( tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/disco" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" + "github.com/opentofu/svchost/svcauth" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/gocty" @@ -490,7 +491,7 @@ func (b *Cloud) discover() (*url.URL, error) { return nil, err } - host, err := b.services.Discover(hostname) + host, err := b.services.Discover(context.TODO(), hostname) if err != nil { var serviceDiscoErr *disco.ErrServiceDiscoveryNetworkRequest @@ -520,12 +521,24 @@ func (b *Cloud) cliConfigToken() (string, error) { if err != nil { return "", err } - creds, err := b.services.CredentialsForHost(hostname) + creds, err := b.services.CredentialsForHost(context.TODO(), hostname) if err != nil { log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err) return "", nil } - if creds != nil { + + // HostCredentialsWithToken is a variant of [svcauth.HostCredentials] + // that also offers direct access to a stored token. This is a weird + // need that applies only to this legacy cloud backend since it uses + // a client library for a particular vendor's API that isn't designed + // to integrate with svcauth. This is a surgical patch to keep this + // working similarly to how it did in our predecessor project until + // we decide on a more definite future for this backend. + type HostCredentialsWithToken interface { + svcauth.HostCredentials + Token() string + } + if creds, ok := creds.(HostCredentialsWithToken); ok { return creds.Token(), nil } return "", nil diff --git a/internal/cloud/testing.go b/internal/cloud/testing.go index 1c828027d8..3fa991a4c3 100644 --- a/internal/cloud/testing.go +++ b/internal/cloud/testing.go @@ -21,26 +21,23 @@ import ( "time" tfe "github.com/hashicorp/go-tfe" - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/auth" - "github.com/hashicorp/terraform-svchost/disco" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" + "github.com/opentofu/svchost/svcauth" "github.com/zclconf/go-cty/cty" "github.com/opentofu/opentofu/internal/backend" + backendLocal "github.com/opentofu/opentofu/internal/backend/local" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/encryption" - "github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/states/statefile" "github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tofu" - "github.com/opentofu/opentofu/version" - - backendLocal "github.com/opentofu/opentofu/internal/backend/local" ) const ( @@ -49,8 +46,8 @@ const ( var ( tfeHost = "app.terraform.io" - credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ - svchost.Hostname(tfeHost): {"token": testCred}, + credsSrc = svcauth.StaticCredentialsSource(map[svchost.Hostname]svcauth.HostCredentials{ + svchost.Hostname(tfeHost): svcauth.HostCredentialsToken("testCred"), }) testBackendSingleWorkspaceName = "app-prod" defaultTFCPing = map[string]func(http.ResponseWriter, *http.Request){ @@ -598,8 +595,10 @@ func testDisco(s *httptest.Server) *disco.Disco { services := map[string]interface{}{ "tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL), } - d := disco.NewWithCredentialsSource(credsSrc) - d.SetUserAgent(httpclient.OpenTofuUserAgent(version.String())) + d := disco.New( + disco.WithCredentials(credsSrc), + disco.WithHTTPClient(s.Client()), + ) d.ForceHostServices(svchost.Hostname(tfeHost), services) d.ForceHostServices(svchost.Hostname("localhost"), services) diff --git a/internal/command/cliconfig/cliconfig.go b/internal/command/cliconfig/cliconfig.go index c1edaa40a7..4e1843eb1e 100644 --- a/internal/command/cliconfig/cliconfig.go +++ b/internal/command/cliconfig/cliconfig.go @@ -24,7 +24,7 @@ import ( "strings" "github.com/hashicorp/hcl" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" "github.com/opentofu/opentofu/internal/tfdiags" ) diff --git a/internal/command/cliconfig/credentials.go b/internal/command/cliconfig/credentials.go index bd041024a1..6b7835376f 100644 --- a/internal/command/cliconfig/credentials.go +++ b/internal/command/cliconfig/credentials.go @@ -7,6 +7,7 @@ package cliconfig import ( "bytes" + "context" "encoding/json" "fmt" "log" @@ -14,12 +15,12 @@ import ( "path/filepath" "strings" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/svcauth" "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" - svchost "github.com/hashicorp/terraform-svchost" - svcauth "github.com/hashicorp/terraform-svchost/auth" - + "github.com/opentofu/opentofu/internal/command/cliconfig/svcauthconfig" "github.com/opentofu/opentofu/internal/configs/hcl2shim" pluginDiscovery "github.com/opentofu/opentofu/internal/plugin/discovery" "github.com/opentofu/opentofu/internal/replacefile" @@ -47,7 +48,7 @@ func (c *Config) CredentialsSource(helperPlugins pluginDiscovery.PluginMetaSet) return nil, fmt.Errorf("can't locate credentials file: %w", err) } - var helper svcauth.CredentialsSource + var helper svcauth.CredentialsStore var helperType string for givenType, givenConfig := range c.CredentialsHelpers { available := helperPlugins.WithName(givenType) @@ -58,8 +59,8 @@ func (c *Config) CredentialsSource(helperPlugins pluginDiscovery.PluginMetaSet) selected := available.Newest() - helperSource := svcauth.HelperProgramCredentialsSource(selected.Path, givenConfig.Args...) - helper = svcauth.CachingCredentialsSource(helperSource) // cached because external operation may be slow/expensive + helperSource := svcauthconfig.NewHelperProgramCredentialsStore(selected.Path, givenConfig.Args...) + helper = svcauth.CachingCredentialsStore(helperSource) helperType = givenType // There should only be zero or one "credentials_helper" blocks. We @@ -85,7 +86,7 @@ func EmptyCredentialsSourceForTests(credentialsFilePath string) *CredentialsSour // credentialsSource is an internal factory for the credentials source which // allows overriding the credentials file path, which allows setting it to // a temporary file location when testing. -func (c *Config) credentialsSource(helperType string, helper svcauth.CredentialsSource, credentialsFilePath string) *CredentialsSource { +func (c *Config) credentialsSource(helperType string, helper svcauth.CredentialsStore, credentialsFilePath string) *CredentialsSource { configured := map[svchost.Hostname]cty.Value{} for userHost, creds := range c.Credentials { host, err := svchost.ForComparison(userHost) @@ -221,7 +222,7 @@ type CredentialsSource struct { // hostnames not explicitly represented in "configured". Any writes to // the credentials store will also be sent to a configured helper instead // of the credentials.tfrc.json file. - helper svcauth.CredentialsSource + helper svcauth.CredentialsStore // helperType is the name of the type of credentials helper that is // referenced in "helper", or the empty string if "helper" is nil. @@ -231,7 +232,7 @@ type CredentialsSource struct { // Assertion that credentialsSource implements CredentialsSource var _ svcauth.CredentialsSource = (*CredentialsSource)(nil) -func (s *CredentialsSource) ForHost(host svchost.Hostname) (svcauth.HostCredentials, error) { +func (s *CredentialsSource) ForHost(ctx context.Context, host svchost.Hostname) (svcauth.HostCredentials, error) { // The first order of precedence for credentials is a host-specific environment variable if envCreds := hostCredentialsFromEnv(host); envCreds != nil { return envCreds, nil @@ -240,23 +241,23 @@ func (s *CredentialsSource) ForHost(host svchost.Hostname) (svcauth.HostCredenti // Then, any credentials block present in the CLI config v, ok := s.configured[host] if ok { - return svcauth.HostCredentialsFromObject(v), nil + return svcauthconfig.HostCredentialsFromObject(v), nil } // And finally, the credentials helper if s.helper != nil { - return s.helper.ForHost(host) + return s.helper.ForHost(ctx, host) } return nil, nil } -func (s *CredentialsSource) StoreForHost(host svchost.Hostname, credentials svcauth.HostCredentialsWritable) error { - return s.updateHostCredentials(host, credentials) +func (s *CredentialsSource) StoreForHost(ctx context.Context, host svchost.Hostname, credentials svcauth.NewHostCredentials) error { + return s.updateHostCredentials(ctx, host, credentials) } -func (s *CredentialsSource) ForgetForHost(host svchost.Hostname) error { - return s.updateHostCredentials(host, nil) +func (s *CredentialsSource) ForgetForHost(ctx context.Context, host svchost.Hostname) error { + return s.updateHostCredentials(ctx, host, nil) } // HostCredentialsLocation returns a value indicating what type of storage is @@ -297,7 +298,7 @@ func (s *CredentialsSource) CredentialsHelperType() string { return s.helperType } -func (s *CredentialsSource) updateHostCredentials(host svchost.Hostname, new svcauth.HostCredentialsWritable) error { +func (s *CredentialsSource) updateHostCredentials(ctx context.Context, host svchost.Hostname, new svcauth.NewHostCredentials) error { switch loc := s.HostCredentialsLocation(host); loc { case CredentialsInOtherFile: return ErrUnwritableHostCredentials(host) @@ -311,16 +312,16 @@ func (s *CredentialsSource) updateHostCredentials(host svchost.Hostname, new svc case CredentialsViaHelper: // Delegate entirely to the helper, then. if new == nil { - return s.helper.ForgetForHost(host) + return s.helper.ForgetForHost(ctx, host) } - return s.helper.StoreForHost(host, new) + return s.helper.StoreForHost(ctx, host, new) default: // Should never happen because the above cases are exhaustive return fmt.Errorf("invalid credentials location %#v", loc) } } -func (s *CredentialsSource) updateLocalHostCredentials(host svchost.Hostname, new svcauth.HostCredentialsWritable) error { +func (s *CredentialsSource) updateLocalHostCredentials(host svchost.Hostname, new svcauth.NewHostCredentials) error { // This function updates the local credentials file in particular, // regardless of whether a credentials helper is active. It should be // called only indirectly via updateHostCredentials. diff --git a/internal/command/cliconfig/credentials_test.go b/internal/command/cliconfig/credentials_test.go index 00323b6038..0ae9b3ae46 100644 --- a/internal/command/cliconfig/credentials_test.go +++ b/internal/command/cliconfig/credentials_test.go @@ -6,15 +6,18 @@ package cliconfig import ( + "context" + "fmt" "net/http" "path/filepath" "testing" "github.com/google/go-cmp/cmp" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/svcauth" "github.com/zclconf/go-cty/cty" - svchost "github.com/hashicorp/terraform-svchost" - svcauth "github.com/hashicorp/terraform-svchost/auth" + "github.com/opentofu/opentofu/internal/command/cliconfig/svcauthconfig" ) func TestCredentialsForHost(t *testing.T) { @@ -32,17 +35,15 @@ func TestCredentialsForHost(t *testing.T) { // a credentials helper program, since we're only testing the logic // for choosing when to delegate to the helper here. The logic for // interacting with a helper program is tested in the svcauth package. - helper: svcauth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ - "from-helper.example.com": { - "token": "from-helper", - }, + helper: readOnlyCredentialsStore{ + svcauth.StaticCredentialsSource(map[svchost.Hostname]svcauth.HostCredentials{ + "from-helper.example.com": svcauth.HostCredentialsToken("from-helper"), - // This should be shadowed by the "configured" entry with the same - // hostname above. - "configured.example.com": { - "token": "incorrectly-from-helper", - }, - }), + // This should be shadowed by the "configured" entry with the same + // hostname above. + "configured.example.com": svcauth.HostCredentialsToken("incorrectly-from-helper"), + }), + }, helperType: "fake", } @@ -62,7 +63,7 @@ func TestCredentialsForHost(t *testing.T) { } t.Run("configured", func(t *testing.T) { - creds, err := credSrc.ForHost(svchost.Hostname("configured.example.com")) + creds, err := credSrc.ForHost(t.Context(), svchost.Hostname("configured.example.com")) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -71,7 +72,7 @@ func TestCredentialsForHost(t *testing.T) { } }) t.Run("from helper", func(t *testing.T) { - creds, err := credSrc.ForHost(svchost.Hostname("from-helper.example.com")) + creds, err := credSrc.ForHost(t.Context(), svchost.Hostname("from-helper.example.com")) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -80,7 +81,7 @@ func TestCredentialsForHost(t *testing.T) { } }) t.Run("not available", func(t *testing.T) { - creds, err := credSrc.ForHost(svchost.Hostname("unavailable.example.com")) + creds, err := credSrc.ForHost(t.Context(), svchost.Hostname("unavailable.example.com")) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -94,7 +95,7 @@ func TestCredentialsForHost(t *testing.T) { expectedToken := "configured-by-env" t.Setenv(envName, expectedToken) - creds, err := credSrc.ForHost(svchost.Hostname("configured.example.com")) + creds, err := credSrc.ForHost(t.Context(), svchost.Hostname("configured.example.com")) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -103,7 +104,7 @@ func TestCredentialsForHost(t *testing.T) { t.Fatal("no credentials found") } - if got := creds.Token(); got != expectedToken { + if got := svcauthconfig.HostCredentialsBearerToken(t, creds); got != expectedToken { t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken) } }) @@ -115,7 +116,7 @@ func TestCredentialsForHost(t *testing.T) { t.Setenv(envName, expectedToken) hostname, _ := svchost.ForComparison("env.ドメイン名例.com") - creds, err := credSrc.ForHost(hostname) + creds, err := credSrc.ForHost(t.Context(), hostname) if err != nil { t.Fatalf("unexpected error: %s", err) @@ -125,7 +126,7 @@ func TestCredentialsForHost(t *testing.T) { t.Fatal("no credentials found") } - if got := creds.Token(); got != expectedToken { + if got := svcauthconfig.HostCredentialsBearerToken(t, creds); got != expectedToken { t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken) } }) @@ -137,7 +138,7 @@ func TestCredentialsForHost(t *testing.T) { t.Setenv(envName, expectedToken) hostname, _ := svchost.ForComparison("env.café.fr") - creds, err := credSrc.ForHost(hostname) + creds, err := credSrc.ForHost(t.Context(), hostname) if err != nil { t.Fatalf("unexpected error: %s", err) @@ -147,7 +148,7 @@ func TestCredentialsForHost(t *testing.T) { t.Fatal("no credentials found") } - if got := creds.Token(); got != expectedToken { + if got := svcauthconfig.HostCredentialsBearerToken(t, creds); got != expectedToken { t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken) } }) @@ -159,7 +160,7 @@ func TestCredentialsForHost(t *testing.T) { t.Setenv(envName, expectedToken) hostname, _ := svchost.ForComparison("configured.example.com") - creds, err := credSrc.ForHost(hostname) + creds, err := credSrc.ForHost(t.Context(), hostname) if err != nil { t.Fatalf("unexpected error: %s", err) @@ -169,7 +170,7 @@ func TestCredentialsForHost(t *testing.T) { t.Fatal("no credentials found") } - if got := creds.Token(); got != expectedToken { + if got := svcauthconfig.HostCredentialsBearerToken(t, creds); got != expectedToken { t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken) } }) @@ -181,7 +182,7 @@ func TestCredentialsForHost(t *testing.T) { t.Setenv(envName, expectedToken) hostname, _ := svchost.ForComparison("configureduppercase.example.com") - creds, err := credSrc.ForHost(hostname) + creds, err := credSrc.ForHost(t.Context(), hostname) if err != nil { t.Fatalf("unexpected error: %s", err) @@ -191,7 +192,7 @@ func TestCredentialsForHost(t *testing.T) { t.Fatal("no credentials found") } - if got := creds.Token(); got != expectedToken { + if got := svcauthconfig.HostCredentialsBearerToken(t, creds); got != expectedToken { t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken) } }) @@ -239,6 +240,7 @@ func TestCredentialsStoreForget(t *testing.T) { // Otherwise downstream tests might fail in confusing ways. { err := credSrc.StoreForHost( + t.Context(), svchost.Hostname("manually-configured.example.com"), svcauth.HostCredentialsToken("not-manually-configured"), ) @@ -251,6 +253,7 @@ func TestCredentialsStoreForget(t *testing.T) { } { err := credSrc.ForgetForHost( + t.Context(), svchost.Hostname("manually-configured.example.com"), ) if err == nil { @@ -264,6 +267,7 @@ func TestCredentialsStoreForget(t *testing.T) { // We don't have a credentials file at all yet, so this first call // must create it. err := credSrc.StoreForHost( + t.Context(), svchost.Hostname("stored-locally.example.com"), svcauth.HostCredentialsToken("stored-locally"), ) @@ -271,7 +275,7 @@ func TestCredentialsStoreForget(t *testing.T) { t.Fatalf("unexpected error storing locally: %s", err) } - creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com")) + creds, err := credSrc.ForHost(t.Context(), svchost.Hostname("stored-locally.example.com")) if err != nil { t.Fatalf("failed to read back stored-locally credentials: %s", err) } @@ -303,6 +307,7 @@ func TestCredentialsStoreForget(t *testing.T) { ) { err := credSrc.StoreForHost( + t.Context(), svchost.Hostname("manually-configured.example.com"), svcauth.HostCredentialsToken("not-manually-configured"), ) @@ -312,6 +317,7 @@ func TestCredentialsStoreForget(t *testing.T) { } { err := credSrc.StoreForHost( + t.Context(), svchost.Hostname("stored-in-helper.example.com"), svcauth.HostCredentialsToken("stored-in-helper"), ) @@ -319,7 +325,7 @@ func TestCredentialsStoreForget(t *testing.T) { t.Fatalf("unexpected error storing in helper: %s", err) } - creds, err := credSrc.ForHost(svchost.Hostname("stored-in-helper.example.com")) + creds, err := credSrc.ForHost(t.Context(), svchost.Hostname("stored-in-helper.example.com")) if err != nil { t.Fatalf("failed to read back stored-in-helper credentials: %s", err) } @@ -341,6 +347,7 @@ func TestCredentialsStoreForget(t *testing.T) { // Because stored-locally is already in the credentials file, a new // store should be sent there rather than to the credentials helper. err := credSrc.StoreForHost( + t.Context(), svchost.Hostname("stored-locally.example.com"), svcauth.HostCredentialsToken("stored-locally-again"), ) @@ -348,7 +355,7 @@ func TestCredentialsStoreForget(t *testing.T) { t.Fatalf("unexpected error storing locally again: %s", err) } - creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com")) + creds, err := credSrc.ForHost(t.Context(), svchost.Hostname("stored-locally.example.com")) if err != nil { t.Fatalf("failed to read back stored-locally credentials: %s", err) } @@ -361,13 +368,14 @@ func TestCredentialsStoreForget(t *testing.T) { // Forgetting a host already in the credentials file should remove it // from the credentials file, not from the helper. err := credSrc.ForgetForHost( + t.Context(), svchost.Hostname("stored-locally.example.com"), ) if err != nil { t.Fatalf("unexpected error forgetting locally: %s", err) } - creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com")) + creds, err := credSrc.ForHost(t.Context(), svchost.Hostname("stored-locally.example.com")) if err != nil { t.Fatalf("failed to read back stored-locally credentials: %s", err) } @@ -385,13 +393,14 @@ func TestCredentialsStoreForget(t *testing.T) { } { err := credSrc.ForgetForHost( + t.Context(), svchost.Hostname("stored-in-helper.example.com"), ) if err != nil { t.Fatalf("unexpected error forgetting in helper: %s", err) } - creds, err := credSrc.ForHost(svchost.Hostname("stored-in-helper.example.com")) + creds, err := credSrc.ForHost(t.Context(), svchost.Hostname("stored-in-helper.example.com")) if err != nil { t.Fatalf("failed to read back stored-in-helper credentials: %s", err) } @@ -434,15 +443,15 @@ type mockCredentialsHelper struct { // Assertion that mockCredentialsHelper implements svcauth.CredentialsSource var _ svcauth.CredentialsSource = (*mockCredentialsHelper)(nil) -func (s *mockCredentialsHelper) ForHost(hostname svchost.Hostname) (svcauth.HostCredentials, error) { +func (s *mockCredentialsHelper) ForHost(_ context.Context, hostname svchost.Hostname) (svcauth.HostCredentials, error) { v, ok := s.current[hostname] if !ok { return nil, nil } - return svcauth.HostCredentialsFromObject(v), nil + return svcauthconfig.HostCredentialsFromObject(v), nil } -func (s *mockCredentialsHelper) StoreForHost(hostname svchost.Hostname, new svcauth.HostCredentialsWritable) error { +func (s *mockCredentialsHelper) StoreForHost(_ context.Context, hostname svchost.Hostname, new svcauth.NewHostCredentials) error { s.log = append(s.log, mockCredentialsHelperChange{ Host: hostname, Action: "store", @@ -451,7 +460,7 @@ func (s *mockCredentialsHelper) StoreForHost(hostname svchost.Hostname, new svca return nil } -func (s *mockCredentialsHelper) ForgetForHost(hostname svchost.Hostname) error { +func (s *mockCredentialsHelper) ForgetForHost(_ context.Context, hostname svchost.Hostname) error { s.log = append(s.log, mockCredentialsHelperChange{ Host: hostname, Action: "forget", @@ -459,3 +468,29 @@ func (s *mockCredentialsHelper) ForgetForHost(hostname svchost.Hostname) error { delete(s.current, hostname) return nil } + +// readOnlyCredentialsStore is an adapter to make a [svcauth.CredentialsSource] +// appear to implement [svcauth.CredentialsStore] for testing purposes. +// +// It only statically implements that larger set of methods. If any of the +// store-specific methods are called they will immediately return an error. +type readOnlyCredentialsStore struct { + source svcauth.CredentialsSource +} + +var _ svcauth.CredentialsStore = readOnlyCredentialsStore{} + +// ForHost implements svcauth.CredentialsStore. +func (r readOnlyCredentialsStore) ForHost(ctx context.Context, host svchost.Hostname) (svcauth.HostCredentials, error) { + return r.source.ForHost(ctx, host) +} + +// ForgetForHost implements svcauth.CredentialsStore. +func (r readOnlyCredentialsStore) ForgetForHost(ctx context.Context, host svchost.Hostname) error { + return fmt.Errorf("this credentials store is actually read-only, despite implementing svcauth.CredentialsSource") +} + +// StoreForHost implements svcauth.CredentialsStore. +func (r readOnlyCredentialsStore) StoreForHost(ctx context.Context, host svchost.Hostname, credentials svcauth.NewHostCredentials) error { + return fmt.Errorf("this credentials store is actually read-only, despite implementing svcauth.CredentialsSource") +} diff --git a/internal/command/cliconfig/provider_installation.go b/internal/command/cliconfig/provider_installation.go index f38d1034b5..50577c1f6b 100644 --- a/internal/command/cliconfig/provider_installation.go +++ b/internal/command/cliconfig/provider_installation.go @@ -14,7 +14,7 @@ import ( hcltoken "github.com/hashicorp/hcl/hcl/token" hcl2 "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" diff --git a/internal/command/cliconfig/provider_installation_test.go b/internal/command/cliconfig/provider_installation_test.go index 0036c64dbe..807ee8b61b 100644 --- a/internal/command/cliconfig/provider_installation_test.go +++ b/internal/command/cliconfig/provider_installation_test.go @@ -11,7 +11,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" + "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/getproviders" ) diff --git a/internal/command/cliconfig/svcauthconfig/credentials_from_map.go b/internal/command/cliconfig/svcauthconfig/credentials_from_map.go new file mode 100644 index 0000000000..946cbd1456 --- /dev/null +++ b/internal/command/cliconfig/svcauthconfig/credentials_from_map.go @@ -0,0 +1,54 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package svcauthconfig + +import ( + "github.com/opentofu/svchost/svcauth" + "github.com/zclconf/go-cty/cty" +) + +// 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]any) svcauth.HostCredentials { + if m == nil { + return nil + } + if token, ok := m["token"].(string); ok { + return svcauth.HostCredentialsToken(token) + } + return nil +} + +// HostCredentialsFromObject converts a cty.Value of an object type into a +// HostCredentials object if possible, or returns nil if no credentials could +// be extracted from the map. +// +// This function ignores object attributes it is unfamiliar with, to allow for +// future expansion of the credentials object structure for new credential types. +// +// If the given value is not of an object type, this function will panic. +func HostCredentialsFromObject(obj cty.Value) svcauth.HostCredentials { + if !obj.Type().HasAttribute("token") { + return nil + } + + tokenV := obj.GetAttr("token") + if tokenV.IsNull() || !tokenV.IsKnown() { + return nil + } + if !cty.String.Equals(tokenV.Type()) { + // Weird, but maybe some future version accepts an object here for some + // reason, so we'll tolerate that for forward-compatibility. + return nil + } + + return svcauth.HostCredentialsToken(tokenV.AsString()) +} diff --git a/internal/command/cliconfig/svcauthconfig/doc.go b/internal/command/cliconfig/svcauthconfig/doc.go new file mode 100644 index 0000000000..d1e6ec4702 --- /dev/null +++ b/internal/command/cliconfig/svcauthconfig/doc.go @@ -0,0 +1,20 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package svcauthconfig contains some helper functions and types to support +// the cliconfig package's use of [github.com/opentofu/svchost/svcauth], +// which is our mechanism for representing the policy for authenticating to +// OpenTofu-native services such as implementations OpenTofu's provider registry +// protocol. +// +// The intended separation of concerns is that the upstream library provides +// the "vocabulary types" that other parts of OpenTofu interact with, while +// this package contains concrete implementations of those types and helpers +// to assist in constructing them which should be used _only_ by package +// cliconfig to satisfy the upstream interfaces. This separation means that +// we can evolve the implementation details of service authentication by +// changes only in this repository, thereby avoiding the complexity of always +// having to update both codebases in lockstep. +package svcauthconfig diff --git a/internal/command/cliconfig/svcauthconfig/helper_program.go b/internal/command/cliconfig/svcauthconfig/helper_program.go new file mode 100644 index 0000000000..2681924fcb --- /dev/null +++ b/internal/command/cliconfig/svcauthconfig/helper_program.go @@ -0,0 +1,153 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package svcauthconfig + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os/exec" + "path/filepath" + + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/svcauth" + ctyjson "github.com/zclconf/go-cty/cty/json" +) + +type helperProgramCredentialsSource struct { + executable string + args []string +} + +// NewHelperProgramCredentialsStore returns a [svcauth.CredentialsStore] that +// runs the given program with the given arguments in order to obtain or store +// 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 NewHelperProgramCredentialsStore(executable string, args ...string) svcauth.CredentialsStore { + if !filepath.IsAbs(executable) { + panic("HelperProgramCredentialsStore 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(ctx context.Context, host svchost.Hostname) (svcauth.HostCredentials, error) { + args := make([]string, len(s.args), len(s.args)+2) + copy(args, s.args) + args = append(args, "get", 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 +} + +func (s *helperProgramCredentialsSource) StoreForHost(ctx context.Context, host svchost.Hostname, credentials svcauth.NewHostCredentials) error { + args := make([]string, len(s.args), len(s.args)+2) + copy(args, s.args) + args = append(args, "store", string(host)) + + toStore := credentials.ToStore() + toStoreRaw, err := ctyjson.Marshal(toStore, toStore.Type()) + if err != nil { + return fmt.Errorf("can't serialize credentials to store: %s", err) + } + + inReader := bytes.NewReader(toStoreRaw) + errBuf := bytes.Buffer{} + + cmd := exec.Cmd{ + Path: s.executable, + Args: args, + Stdin: inReader, + Stderr: &errBuf, + Stdout: nil, + } + err = cmd.Run() + if _, isExitErr := err.(*exec.ExitError); isExitErr { + errText := errBuf.String() + if errText == "" { + // Shouldn't happen for a well-behaved helper program + return fmt.Errorf("error in %s, but it produced no error message", s.executable) + } + return fmt.Errorf("error in %s: %s", s.executable, errText) + } else if err != nil { + return fmt.Errorf("failed to run %s: %s", s.executable, err) + } + + return nil +} + +func (s *helperProgramCredentialsSource) ForgetForHost(ctx context.Context, host svchost.Hostname) error { + args := make([]string, len(s.args), len(s.args)+2) + copy(args, s.args) + args = append(args, "forget", string(host)) + + errBuf := bytes.Buffer{} + + cmd := exec.Cmd{ + Path: s.executable, + Args: args, + Stdin: nil, + Stderr: &errBuf, + Stdout: nil, + } + err := cmd.Run() + if _, isExitErr := err.(*exec.ExitError); isExitErr { + errText := errBuf.String() + if errText == "" { + // Shouldn't happen for a well-behaved helper program + return fmt.Errorf("error in %s, but it produced no error message", s.executable) + } + return fmt.Errorf("error in %s: %s", s.executable, errText) + } else if err != nil { + return fmt.Errorf("failed to run %s: %s", s.executable, err) + } + + return nil +} diff --git a/internal/command/cliconfig/svcauthconfig/helper_program_test.go b/internal/command/cliconfig/svcauthconfig/helper_program_test.go new file mode 100644 index 0000000000..5590086cd9 --- /dev/null +++ b/internal/command/cliconfig/svcauthconfig/helper_program_test.go @@ -0,0 +1,98 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package svcauthconfig + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/svcauth" +) + +func TestHelperProgramCredentialsSource(t *testing.T) { + // The helper script used in this test currently assumes a Unix-like + // environment where scripts are directly executable based on their #! + // line and where bash is available. This is an assumption we inherited + // from our predecessor which we'd like to address someday, but for now + // we'll just skip this test unless we're on Linux or macOS since those + // are the two OSes most commonly used for OpenTofu development where + // we can expect this to work. (Other unixes could potentially work + // but we don't want to maintain a huge list here.) + if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { + t.Skip("this test only works on Unix-like systems") + } + + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + program := filepath.Join(wd, "testdata", "helperprog", "test-helper") + t.Logf("testing with helper at %s", program) + + src := NewHelperProgramCredentialsStore(program) + + t.Run("happy path", func(t *testing.T) { + creds, err := src.ForHost(t.Context(), svchost.Hostname("example.com")) + if err != nil { + t.Fatal(err) + } + if got, want := HostCredentialsBearerToken(t, creds), "example-token"; got != want { + t.Errorf("wrong token %q; want %q", got, want) + } + }) + t.Run("no credentials", func(t *testing.T) { + creds, err := src.ForHost(t.Context(), 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(t.Context(), 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(t.Context(), svchost.Hostname("fail.example.com")) + if err == nil { + t.Error("completed successfully; want error") + } + }) + t.Run("store happy path", func(t *testing.T) { + err := src.StoreForHost(t.Context(), svchost.Hostname("example.com"), svcauth.HostCredentialsToken("example-token")) + if err != nil { + t.Fatal(err) + } + }) + t.Run("store error", func(t *testing.T) { + err := src.StoreForHost(t.Context(), svchost.Hostname("fail.example.com"), svcauth.HostCredentialsToken("example-token")) + if err == nil { + t.Error("completed successfully; want error") + } + }) + t.Run("forget happy path", func(t *testing.T) { + err := src.ForgetForHost(t.Context(), svchost.Hostname("example.com")) + if err != nil { + t.Fatal(err) + } + }) + t.Run("forget error", func(t *testing.T) { + err := src.ForgetForHost(t.Context(), svchost.Hostname("fail.example.com")) + if err == nil { + t.Error("completed successfully; want error") + } + }) +} diff --git a/internal/command/cliconfig/svcauthconfig/testdata/helperprog/.gitignore b/internal/command/cliconfig/svcauthconfig/testdata/helperprog/.gitignore new file mode 100644 index 0000000000..ba2906d066 --- /dev/null +++ b/internal/command/cliconfig/svcauthconfig/testdata/helperprog/.gitignore @@ -0,0 +1 @@ +main diff --git a/internal/command/cliconfig/svcauthconfig/testdata/helperprog/main.go b/internal/command/cliconfig/svcauthconfig/testdata/helperprog/main.go new file mode 100644 index 0000000000..25a0eae21a --- /dev/null +++ b/internal/command/cliconfig/svcauthconfig/testdata/helperprog/main.go @@ -0,0 +1,72 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package main + +import ( + "encoding/json" + "fmt" + "io" + "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") + } + + host := args[2] + switch args[1] { + case "get": + 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 + } + case "store": + dataSrc, err := io.ReadAll(os.Stdin) + if err != nil { + die("invalid input: %s", err) + } + var data map[string]interface{} + err = json.Unmarshal(dataSrc, &data) + if err != nil { + die("json unmarshal failed") + } + + switch host { + case "example.com": + if data["token"] != "example-token" { + die("incorrect token value to store") + } + default: + die("can't store credentials for %s", host) + } + case "forget": + switch host { + case "example.com": + // okay! + default: + die("can't forget credentials for %s", host) + } + default: + die("unknown subcommand %q\n", args[1]) + } +} + +func die(f string, args ...interface{}) { + fmt.Fprintf(os.Stderr, f, args...) + os.Exit(1) +} diff --git a/internal/command/cliconfig/svcauthconfig/testdata/helperprog/test-helper b/internal/command/cliconfig/svcauthconfig/testdata/helperprog/test-helper new file mode 100755 index 0000000000..0ed3396c56 --- /dev/null +++ b/internal/command/cliconfig/svcauthconfig/testdata/helperprog/test-helper @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -eu + +cd "$( dirname "${BASH_SOURCE[0]}" )" +[ -x main ] || go build -o main . +exec ./main "$@" diff --git a/internal/command/cliconfig/svcauthconfig/testing.go b/internal/command/cliconfig/svcauthconfig/testing.go new file mode 100644 index 0000000000..1cfdc7afda --- /dev/null +++ b/internal/command/cliconfig/svcauthconfig/testing.go @@ -0,0 +1,57 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package svcauthconfig + +import ( + "net/http" + "strings" + "testing" + + "github.com/opentofu/svchost/svcauth" +) + +// The symbols in this file are intended for use in tests only, and are +// not appropriate for use in normal code. + +// HostCredentialsBearerToken is a testing helper implementing a little +// abstraction inversion to get the bearer token used by a credentials source +// even though the [svcauth.HostCredentials] API is designed to be generic over +// what kind of credentials it encloses. +// +// This only works for a [svcauth.HostCredentials] implementation whose +// behavior is to add an Authorization header field to the request using +// the "Bearer" scheme, in which case it returns whatever content appears +// after that scheme. HostCredentials implementations that don't match that +// pattern must be tested in a different way. +// +// This helper should not be used in non-test code. The svcauth API +// intentionally encapsulates the details of how credentials are applied to +// a request so that it can potentially be extended in future to support +// other authentication schemes such as HTTP Basic authentication. +func HostCredentialsBearerToken(t testing.TB, creds svcauth.HostCredentials) string { + t.Helper() + + fakeReq, err := http.NewRequest("GET", "http://example.com/", nil) + if err != nil { + t.Fatalf("failed to create fake request: %s", err) // should be impossible, since we control all the inputs + } + creds.PrepareRequest(fakeReq) + + header := fakeReq.Header + authz := header.Values("authorization") + if len(authz) == 0 { + t.Fatal("the svcauth.HostCredentials implementation did not add an Authorization header field") + } + if len(authz) > 1 { + t.Fatalf("the svcauth.HostCredentials implementation added %d Authorization header fields; want exactly one", len(authz)) + } + + raw := strings.TrimPrefix(strings.ToLower(authz[0]), "bearer ") + if len(raw) == len(authz[0]) { + t.Fatal("the svchost.HostCredentials implemented added an Authorization header that does not use the Bearer scheme") + } + return strings.TrimSpace(raw) +} diff --git a/internal/command/command_test.go b/internal/command/command_test.go index f474c993c3..dde6d44859 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -26,8 +26,8 @@ import ( "github.com/google/go-cmp/cmp" - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" "github.com/zclconf/go-cty/cty" "github.com/opentofu/opentofu/internal/addrs" diff --git a/internal/command/init.go b/internal/command/init.go index 773cc97b30..e92afa3be3 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -14,7 +14,7 @@ import ( "strings" "github.com/hashicorp/hcl/v2" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" "github.com/posener/complete" "github.com/zclconf/go-cty/cty" otelAttr "go.opentelemetry.io/otel/attribute" diff --git a/internal/command/login.go b/internal/command/login.go index 1f1e9d0d40..1b3387b080 100644 --- a/internal/command/login.go +++ b/internal/command/login.go @@ -22,9 +22,9 @@ import ( "strings" tfe "github.com/hashicorp/go-tfe" - svchost "github.com/hashicorp/terraform-svchost" - svcauth "github.com/hashicorp/terraform-svchost/auth" - "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" + "github.com/opentofu/svchost/svcauth" "github.com/opentofu/opentofu/internal/command/cliconfig" "github.com/opentofu/opentofu/internal/httpclient" @@ -98,7 +98,7 @@ func (c *LoginCommand) Run(args []string) int { // working as expected. (Perhaps the normalization is part of the cause.) dispHostname := hostname.ForDisplay() - host, err := c.Services.Discover(hostname) + host, err := c.Services.Discover(ctx, hostname) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -220,7 +220,7 @@ func (c *LoginCommand) Run(args []string) int { return 1 } - err = creds.StoreForHost(hostname, token) + err = creds.StoreForHost(ctx, hostname, token) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, diff --git a/internal/command/login_test.go b/internal/command/login_test.go index bcf9ec62c0..78a58877b8 100644 --- a/internal/command/login_test.go +++ b/internal/command/login_test.go @@ -13,16 +13,15 @@ import ( "testing" "github.com/mitchellh/cli" - - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" "github.com/opentofu/opentofu/internal/command/cliconfig" + "github.com/opentofu/opentofu/internal/command/cliconfig/svcauthconfig" oauthserver "github.com/opentofu/opentofu/internal/command/testdata/login-oauth-server" tfeserver "github.com/opentofu/opentofu/internal/command/testdata/login-tfe-server" "github.com/opentofu/opentofu/internal/command/webbrowser" "github.com/opentofu/opentofu/internal/httpclient" - "github.com/opentofu/opentofu/version" ) func TestLogin(t *testing.T) { @@ -57,8 +56,10 @@ func TestLogin(t *testing.T) { } creds := cliconfig.EmptyCredentialsSourceForTests(filepath.Join(workDir, "credentials.tfrc.json")) - svcs := disco.NewWithCredentialsSource(creds) - svcs.SetUserAgent(httpclient.OpenTofuUserAgent(version.String())) + svcs := disco.New( + disco.WithCredentials(creds), + disco.WithHTTPClient(httpclient.New(t.Context())), + ) svcs.ForceHostServices(svchost.Hostname("example.com"), map[string]interface{}{ "login.v1": map[string]interface{}{ @@ -135,11 +136,11 @@ func TestLogin(t *testing.T) { } credsSrc := c.Services.CredentialsSource() - creds, err := credsSrc.ForHost(svchost.Hostname(tfeHost)) + creds, err := credsSrc.ForHost(t.Context(), svchost.Hostname(tfeHost)) if err != nil { t.Errorf("failed to retrieve credentials: %s", err) } - if got, want := creds.Token(), "good-token"; got != want { + if got, want := svcauthconfig.HostCredentialsBearerToken(t, creds), "good-token"; got != want { t.Errorf("wrong token %q; want %q", got, want) } if got, want := ui.OutputWriter.String(), "Welcome to the cloud backend!"; !strings.Contains(got, want) { @@ -158,11 +159,11 @@ func TestLogin(t *testing.T) { } credsSrc := c.Services.CredentialsSource() - creds, err := credsSrc.ForHost(svchost.Hostname("example.com")) + creds, err := credsSrc.ForHost(t.Context(), svchost.Hostname("example.com")) if err != nil { t.Errorf("failed to retrieve credentials: %s", err) } - if got, want := creds.Token(), "good-token"; got != want { + if got, want := svcauthconfig.HostCredentialsBearerToken(t, creds), "good-token"; got != want { t.Errorf("wrong token %q; want %q", got, want) } @@ -173,7 +174,7 @@ func TestLogin(t *testing.T) { t.Run("example.com results in no scopes", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) { - host, _ := c.Services.Discover("example.com") + host, _ := c.Services.Discover(t.Context(), "example.com") client, _ := host.ServiceOAuthClient("login.v1") if len(client.Scopes) != 0 { t.Errorf("unexpected scopes %q; expected none", client.Scopes) @@ -191,13 +192,13 @@ func TestLogin(t *testing.T) { } credsSrc := c.Services.CredentialsSource() - creds, err := credsSrc.ForHost(svchost.Hostname("with-scopes.example.com")) + creds, err := credsSrc.ForHost(t.Context(), svchost.Hostname("with-scopes.example.com")) if err != nil { t.Errorf("failed to retrieve credentials: %s", err) } - if got, want := creds.Token(), "good-token"; got != want { + if got, want := svcauthconfig.HostCredentialsBearerToken(t, creds), "good-token"; got != want { t.Errorf("wrong token %q; want %q", got, want) } @@ -208,7 +209,7 @@ func TestLogin(t *testing.T) { t.Run("with-scopes.example.com results in expected scopes", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) { - host, _ := c.Services.Discover("with-scopes.example.com") + host, _ := c.Services.Discover(t.Context(), "with-scopes.example.com") client, _ := host.ServiceOAuthClient("login.v1") expectedScopes := [2]string{"app1.full_access", "app2.read_only"} @@ -234,11 +235,11 @@ func TestLogin(t *testing.T) { } credsSrc := c.Services.CredentialsSource() - creds, err := credsSrc.ForHost(svchost.Hostname("tfe.acme.com")) + creds, err := credsSrc.ForHost(t.Context(), svchost.Hostname("tfe.acme.com")) if err != nil { t.Errorf("failed to retrieve credentials: %s", err) } - if got, want := creds.Token(), "good-token"; got != want { + if got, want := svcauthconfig.HostCredentialsBearerToken(t, creds), "good-token"; got != want { t.Errorf("wrong token %q; want %q", got, want) } @@ -259,12 +260,12 @@ func TestLogin(t *testing.T) { } credsSrc := c.Services.CredentialsSource() - creds, err := credsSrc.ForHost(svchost.Hostname("tfe.acme.com")) + creds, err := credsSrc.ForHost(t.Context(), svchost.Hostname("tfe.acme.com")) if err != nil { t.Errorf("failed to retrieve credentials: %s", err) } if creds != nil { - t.Errorf("wrong token %q; should have no token", creds.Token()) + t.Errorf("wrong token %q; should have no token", svcauthconfig.HostCredentialsBearerToken(t, creds)) } }, true)) diff --git a/internal/command/logout.go b/internal/command/logout.go index f9e478ee44..6b5b5a78e8 100644 --- a/internal/command/logout.go +++ b/internal/command/logout.go @@ -10,7 +10,7 @@ import ( "path/filepath" "strings" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" "github.com/opentofu/opentofu/internal/command/cliconfig" "github.com/opentofu/opentofu/internal/tfdiags" @@ -24,6 +24,8 @@ type LogoutCommand struct { // Run implements cli.Command. func (c *LogoutCommand) Run(args []string) int { + ctx := c.CommandContext() + args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("logout") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } @@ -91,7 +93,7 @@ func (c *LogoutCommand) Run(args []string) int { c.Ui.Output(fmt.Sprintf("Removing the stored credentials for %s from the following file:\n %s\n", dispHostname, credsCtx.LocalFilename)) } - err = creds.ForgetForHost(hostname) + err = creds.ForgetForHost(ctx, hostname) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, diff --git a/internal/command/logout_test.go b/internal/command/logout_test.go index 4247a1e2bc..e103367ac7 100644 --- a/internal/command/logout_test.go +++ b/internal/command/logout_test.go @@ -11,11 +11,12 @@ import ( "testing" "github.com/mitchellh/cli" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" + "github.com/opentofu/svchost/svcauth" - svchost "github.com/hashicorp/terraform-svchost" - svcauth "github.com/hashicorp/terraform-svchost/auth" - "github.com/hashicorp/terraform-svchost/disco" "github.com/opentofu/opentofu/internal/command/cliconfig" + "github.com/opentofu/opentofu/internal/command/cliconfig/svcauthconfig" ) func TestLogout(t *testing.T) { @@ -26,8 +27,10 @@ func TestLogout(t *testing.T) { c := &LogoutCommand{ Meta: Meta{ - Ui: ui, - Services: disco.NewWithCredentialsSource(credsSrc), + Ui: ui, + Services: disco.New( + disco.WithCredentials(credsSrc), + ), }, } @@ -61,7 +64,7 @@ func TestLogout(t *testing.T) { for _, tc := range testCases { host := svchost.Hostname(tc.hostname) token := svcauth.HostCredentialsToken("some-token") - err := credsSrc.StoreForHost(host, token) + err := credsSrc.StoreForHost(t.Context(), host, token) if err != nil { t.Fatalf("unexpected error storing credentials: %s", err) } @@ -71,16 +74,16 @@ func TestLogout(t *testing.T) { t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String()) } - creds, err := credsSrc.ForHost(host) + creds, err := credsSrc.ForHost(t.Context(), host) if err != nil { t.Errorf("failed to retrieve credentials: %s", err) } if tc.shouldRemove { if creds != nil { - t.Errorf("wrong token %q; should have no token", creds.Token()) + t.Errorf("wrong token %q; should have no token", svcauthconfig.HostCredentialsBearerToken(t, creds)) } } else { - if got, want := creds.Token(), "some-token"; got != want { + if got, want := svcauthconfig.HostCredentialsBearerToken(t, creds), "some-token"; got != want { t.Errorf("wrong token %q; want %q", got, want) } } diff --git a/internal/command/meta.go b/internal/command/meta.go index e76cb567ff..399dd7757e 100644 --- a/internal/command/meta.go +++ b/internal/command/meta.go @@ -21,9 +21,9 @@ import ( "github.com/hashicorp/go-plugin" "github.com/hashicorp/go-retryablehttp" - "github.com/hashicorp/terraform-svchost/disco" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" + "github.com/opentofu/svchost/disco" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/backend" diff --git a/internal/configs/config_test.go b/internal/configs/config_test.go index 5a14e57bee..8bd2aae595 100644 --- a/internal/configs/config_test.go +++ b/internal/configs/config_test.go @@ -24,7 +24,7 @@ import ( version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2/hclsyntax" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/depsfile" diff --git a/internal/depsfile/locks.go b/internal/depsfile/locks.go index 12357d9809..b28eb326f2 100644 --- a/internal/depsfile/locks.go +++ b/internal/depsfile/locks.go @@ -9,7 +9,7 @@ import ( "fmt" "sort" - svchost "github.com/hashicorp/terraform-svchost" + svchost "github.com/opentofu/svchost" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/getproviders" diff --git a/internal/getproviders/didyoumean.go b/internal/getproviders/didyoumean.go index 5b631017f9..74e978d738 100644 --- a/internal/getproviders/didyoumean.go +++ b/internal/getproviders/didyoumean.go @@ -15,7 +15,8 @@ import ( "path" "github.com/hashicorp/go-retryablehttp" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" + "github.com/opentofu/opentofu/internal/addrs" ) diff --git a/internal/getproviders/didyoumean_test.go b/internal/getproviders/didyoumean_test.go index 3790aca938..1142d1e397 100644 --- a/internal/getproviders/didyoumean_test.go +++ b/internal/getproviders/didyoumean_test.go @@ -9,7 +9,8 @@ import ( "context" "testing" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" + "github.com/opentofu/opentofu/internal/addrs" ) diff --git a/internal/getproviders/errors.go b/internal/getproviders/errors.go index 3c1ab1bddc..5b860b6730 100644 --- a/internal/getproviders/errors.go +++ b/internal/getproviders/errors.go @@ -9,7 +9,7 @@ import ( "fmt" "net/url" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" "github.com/opentofu/opentofu/internal/addrs" ) diff --git a/internal/getproviders/filesystem_mirror_source_test.go b/internal/getproviders/filesystem_mirror_source_test.go index 92d6654c2c..0ecf87649d 100644 --- a/internal/getproviders/filesystem_mirror_source_test.go +++ b/internal/getproviders/filesystem_mirror_source_test.go @@ -12,8 +12,8 @@ import ( "github.com/apparentlymart/go-versions/versions" "github.com/google/go-cmp/cmp" + "github.com/opentofu/svchost" - svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/opentofu/internal/addrs" ) diff --git a/internal/getproviders/filesystem_search.go b/internal/getproviders/filesystem_search.go index e7169fe50b..f74dcc776a 100644 --- a/internal/getproviders/filesystem_search.go +++ b/internal/getproviders/filesystem_search.go @@ -12,7 +12,7 @@ import ( "path/filepath" "strings" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" "github.com/opentofu/opentofu/internal/addrs" ) diff --git a/internal/getproviders/http_mirror_source.go b/internal/getproviders/http_mirror_source.go index 5a3fbbc827..4a7320a740 100644 --- a/internal/getproviders/http_mirror_source.go +++ b/internal/getproviders/http_mirror_source.go @@ -19,8 +19,8 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - svchost "github.com/hashicorp/terraform-svchost" - svcauth "github.com/hashicorp/terraform-svchost/auth" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/svcauth" "golang.org/x/net/idna" "github.com/opentofu/opentofu/internal/addrs" @@ -254,7 +254,7 @@ func (s *HTTPMirrorSource) mirrorHost() (svchost.Hostname, error) { // // It might return an error if the mirror base URL is invalid, or if the // credentials lookup itself fails. -func (s *HTTPMirrorSource) mirrorHostCredentials() (svcauth.HostCredentials, error) { +func (s *HTTPMirrorSource) mirrorHostCredentials(ctx context.Context) (svcauth.HostCredentials, error) { hostname, err := s.mirrorHost() if err != nil { return nil, fmt.Errorf("invalid provider mirror base URL %s: %w", s.baseURL.String(), err) @@ -265,7 +265,7 @@ func (s *HTTPMirrorSource) mirrorHostCredentials() (svcauth.HostCredentials, err return nil, nil } - return s.creds.ForHost(hostname) + return s.creds.ForHost(ctx, hostname) } // get is the shared functionality for querying a JSON index from a mirror. @@ -292,7 +292,7 @@ func (s *HTTPMirrorSource) get(ctx context.Context, relativePath string) (status } req = req.WithContext(ctx) req.Request.Header.Set(terraformVersionHeader, version.String()) - creds, err := s.mirrorHostCredentials() + creds, err := s.mirrorHostCredentials(ctx) if err != nil { return 0, nil, endpointURL, fmt.Errorf("failed to determine request credentials: %w", err) } diff --git a/internal/getproviders/http_mirror_source_test.go b/internal/getproviders/http_mirror_source_test.go index 8d89f2e1f8..0588dbe303 100644 --- a/internal/getproviders/http_mirror_source_test.go +++ b/internal/getproviders/http_mirror_source_test.go @@ -15,8 +15,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-retryablehttp" - svchost "github.com/hashicorp/terraform-svchost" - svcauth "github.com/hashicorp/terraform-svchost/auth" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/svcauth" "github.com/opentofu/opentofu/internal/addrs" ) @@ -33,10 +33,8 @@ func TestHTTPMirrorSource(t *testing.T) { if err != nil { t.Fatalf("httptest.NewTLSServer returned a server with an invalid URL") } - creds := svcauth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ - svchost.Hostname(baseURL.Host): { - "token": "placeholder-token", - }, + creds := svcauth.StaticCredentialsSource(map[svchost.Hostname]svcauth.HostCredentials{ + svchost.Hostname(baseURL.Host): svcauth.HostCredentialsToken("placeholder-token"), }) retryHTTPClient := retryablehttp.NewClient() retryHTTPClient.HTTPClient = httpClient diff --git a/internal/getproviders/multi_source.go b/internal/getproviders/multi_source.go index 48578a1fcb..8d693c282a 100644 --- a/internal/getproviders/multi_source.go +++ b/internal/getproviders/multi_source.go @@ -10,7 +10,7 @@ import ( "fmt" "strings" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" "github.com/opentofu/opentofu/internal/addrs" ) diff --git a/internal/getproviders/oci_registry_mirror_source_test.go b/internal/getproviders/oci_registry_mirror_source_test.go index af98e3e38a..62376902b0 100644 --- a/internal/getproviders/oci_registry_mirror_source_test.go +++ b/internal/getproviders/oci_registry_mirror_source_test.go @@ -17,10 +17,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" - svchost "github.com/hashicorp/terraform-svchost" ociDigest "github.com/opencontainers/go-digest" ociSpecs "github.com/opencontainers/image-spec/specs-go" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/opentofu/svchost" orasContent "oras.land/oras-go/v2/content" orasOCI "oras.land/oras-go/v2/content/oci" orasErrors "oras.land/oras-go/v2/errdef" diff --git a/internal/getproviders/registry_client.go b/internal/getproviders/registry_client.go index ed20d90d75..626059b005 100644 --- a/internal/getproviders/registry_client.go +++ b/internal/getproviders/registry_client.go @@ -18,8 +18,8 @@ import ( "path" "github.com/hashicorp/go-retryablehttp" - svchost "github.com/hashicorp/terraform-svchost" - svcauth "github.com/hashicorp/terraform-svchost/auth" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/svcauth" otelAttr "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" diff --git a/internal/getproviders/registry_client_test.go b/internal/getproviders/registry_client_test.go index eee8111422..b7230ec961 100644 --- a/internal/getproviders/registry_client_test.go +++ b/internal/getproviders/registry_client_test.go @@ -16,8 +16,9 @@ import ( "github.com/apparentlymart/go-versions/versions" "github.com/google/go-cmp/cmp" - svchost "github.com/hashicorp/terraform-svchost" - disco "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost" + disco "github.com/opentofu/svchost/disco" + "github.com/opentofu/opentofu/internal/addrs" ) diff --git a/internal/getproviders/registry_source.go b/internal/getproviders/registry_source.go index ea87bc35a7..9b10b72a5f 100644 --- a/internal/getproviders/registry_source.go +++ b/internal/getproviders/registry_source.go @@ -11,8 +11,8 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - svchost "github.com/hashicorp/terraform-svchost" - disco "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost" + disco "github.com/opentofu/svchost/disco" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/httpclient" @@ -122,7 +122,7 @@ func (s *RegistrySource) PackageMeta(ctx context.Context, provider addrs.Provide } func (s *RegistrySource) registryClient(ctx context.Context, hostname svchost.Hostname) (*registryClient, error) { - host, err := s.services.Discover(hostname) + host, err := s.services.Discover(ctx, hostname) if err != nil { return nil, ErrHostUnreachable{ Hostname: hostname, @@ -151,7 +151,7 @@ func (s *RegistrySource) registryClient(ctx context.Context, hostname svchost.Ho } // Check if we have credentials configured for this hostname. - creds, err := s.services.CredentialsForHost(hostname) + creds, err := s.services.CredentialsForHost(ctx, hostname) if err != nil { // This indicates that a credentials helper failed, which means we // can't do anything better than just pass through the helper's diff --git a/internal/getproviders/registry_source_test.go b/internal/getproviders/registry_source_test.go index 659a1755db..6c334a9e17 100644 --- a/internal/getproviders/registry_source_test.go +++ b/internal/getproviders/registry_source_test.go @@ -12,11 +12,10 @@ import ( "strings" "testing" - tfaddr "github.com/opentofu/registry-address" - "github.com/apparentlymart/go-versions/versions" "github.com/google/go-cmp/cmp" - svchost "github.com/hashicorp/terraform-svchost" + regaddr "github.com/opentofu/registry-address/v2" + "github.com/opentofu/svchost" "github.com/opentofu/opentofu/internal/addrs" ) @@ -142,7 +141,7 @@ func TestSourcePackageMeta(t *testing.T) { []SigningKey{ {ASCIIArmor: TestingPublicKey}, }, - tfaddr.Provider{Hostname: "example.com", Namespace: "awesomesauce", Type: "happycloud"}, + regaddr.Provider{Hostname: "example.com", Namespace: "awesomesauce", Type: "happycloud"}, ), ) diff --git a/internal/initwd/module_install_test.go b/internal/initwd/module_install_test.go index 2fc42d00ff..be0293378b 100644 --- a/internal/initwd/module_install_test.go +++ b/internal/initwd/module_install_test.go @@ -18,7 +18,7 @@ import ( "github.com/go-test/deep" "github.com/google/go-cmp/cmp" version "github.com/hashicorp/go-version" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs" diff --git a/internal/providercache/installer_test.go b/internal/providercache/installer_test.go index 39e3449847..cf7e0ad5cf 100644 --- a/internal/providercache/installer_test.go +++ b/internal/providercache/installer_test.go @@ -22,8 +22,8 @@ import ( "github.com/apparentlymart/go-versions/versions/constraints" "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/depsfile" diff --git a/internal/registry/client.go b/internal/registry/client.go index 004835b460..17ea9d7451 100644 --- a/internal/registry/client.go +++ b/internal/registry/client.go @@ -18,8 +18,8 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" otelAttr "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -71,8 +71,8 @@ func NewClient(ctx context.Context, services *disco.Disco, client *retryablehttp } // Discover queries the host, and returns the url for the registry. -func (c *Client) Discover(host svchost.Hostname, serviceID string) (*url.URL, error) { - service, err := c.services.DiscoverServiceURL(host, serviceID) +func (c *Client) Discover(ctx context.Context, host svchost.Hostname, serviceID string) (*url.URL, error) { + service, err := c.services.DiscoverServiceURL(ctx, host, serviceID) if err != nil { return nil, &ServiceUnreachableError{err} } @@ -96,7 +96,7 @@ func (c *Client) ModuleVersions(ctx context.Context, module *regsrc.Module) (*re return nil, err } - service, err := c.Discover(host, modulesServiceID) + service, err := c.Discover(ctx, host, modulesServiceID) if err != nil { return nil, err } @@ -116,7 +116,7 @@ func (c *Client) ModuleVersions(ctx context.Context, module *regsrc.Module) (*re } req = req.WithContext(ctx) - c.addRequestCreds(host, req.Request) + c.addRequestCreds(ctx, host, req.Request) req.Header.Set(xTerraformVersion, tfVersion) resp, err := c.client.Do(req) @@ -150,8 +150,8 @@ func (c *Client) ModuleVersions(ctx context.Context, module *regsrc.Module) (*re return &versions, nil } -func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) { - creds, err := c.services.CredentialsForHost(host) +func (c *Client) addRequestCreds(ctx context.Context, host svchost.Hostname, req *http.Request) { + creds, err := c.services.CredentialsForHost(ctx, host) if err != nil { log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err) return @@ -179,7 +179,7 @@ func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, vers return "", err } - service, err := c.Discover(host, modulesServiceID) + service, err := c.Discover(ctx, host, modulesServiceID) if err != nil { return "", err } @@ -204,7 +204,7 @@ func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, vers req = req.WithContext(ctx) - c.addRequestCreds(host, req.Request) + c.addRequestCreds(ctx, host, req.Request) req.Header.Set(xTerraformVersion, tfVersion) resp, err := c.client.Do(req) diff --git a/internal/registry/client_test.go b/internal/registry/client_test.go index ecde3c7e8c..45ee81f5b7 100644 --- a/internal/registry/client_test.go +++ b/internal/registry/client_test.go @@ -18,13 +18,12 @@ import ( "github.com/hashicorp/go-retryablehttp" version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost/disco" "github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/registry/regsrc" "github.com/opentofu/opentofu/internal/registry/response" "github.com/opentofu/opentofu/internal/registry/test" - tfversion "github.com/opentofu/opentofu/version" ) func TestLookupModuleVersions(t *testing.T) { @@ -150,8 +149,9 @@ func TestAccLookupModuleVersions(t *testing.T) { if os.Getenv("TF_ACC") == "" { t.Skip() } - regDisco := disco.New() - regDisco.SetUserAgent(httpclient.OpenTofuUserAgent(tfversion.String())) + regDisco := disco.New( + disco.WithHTTPClient(httpclient.New(t.Context())), + ) // test with and without a hostname for _, src := range []string{ diff --git a/internal/registry/errors.go b/internal/registry/errors.go index 6489a081c8..97d7e9d126 100644 --- a/internal/registry/errors.go +++ b/internal/registry/errors.go @@ -8,7 +8,8 @@ package registry import ( "fmt" - "github.com/hashicorp/terraform-svchost/disco" + "github.com/opentofu/svchost/disco" + "github.com/opentofu/opentofu/internal/registry/regsrc" ) diff --git a/internal/registry/regsrc/friendly_host.go b/internal/registry/regsrc/friendly_host.go index a63d63ca27..aebbb0331e 100644 --- a/internal/registry/regsrc/friendly_host.go +++ b/internal/registry/regsrc/friendly_host.go @@ -9,7 +9,7 @@ import ( "regexp" "strings" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" ) var ( diff --git a/internal/registry/regsrc/module.go b/internal/registry/regsrc/module.go index ce37736e4a..66fe456c98 100644 --- a/internal/registry/regsrc/module.go +++ b/internal/registry/regsrc/module.go @@ -11,7 +11,8 @@ import ( "regexp" "strings" - svchost "github.com/hashicorp/terraform-svchost" + "github.com/opentofu/svchost" + "github.com/opentofu/opentofu/internal/addrs" ) diff --git a/internal/registry/test/mock_registry.go b/internal/registry/test/mock_registry.go index 1b733edfff..b4bec85409 100644 --- a/internal/registry/test/mock_registry.go +++ b/internal/registry/test/mock_registry.go @@ -10,13 +10,12 @@ import ( "regexp" "strings" - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/auth" - "github.com/hashicorp/terraform-svchost/disco" - "github.com/opentofu/opentofu/internal/httpclient" + "github.com/opentofu/svchost" + "github.com/opentofu/svchost/disco" + "github.com/opentofu/svchost/svcauth" + "github.com/opentofu/opentofu/internal/registry/regsrc" "github.com/opentofu/opentofu/internal/registry/response" - tfversion "github.com/opentofu/opentofu/version" ) // Disco return a *disco.Disco mapping registry.opentofu.org, localhost, @@ -28,8 +27,10 @@ func Disco(s *httptest.Server) *disco.Disco { "modules.v1": fmt.Sprintf("%s/v1/modules", s.URL), "providers.v1": fmt.Sprintf("%s/v1/providers", s.URL), } - d := disco.NewWithCredentialsSource(credsSrc) - d.SetUserAgent(httpclient.OpenTofuUserAgent(tfversion.String())) + d := disco.New( + disco.WithCredentials(credsSrc), + disco.WithHTTPClient(s.Client()), + ) d.ForceHostServices(svchost.Hostname("registry.opentofu.org"), services) d.ForceHostServices(svchost.Hostname("localhost"), services) @@ -58,8 +59,8 @@ const ( var ( regHost = svchost.Hostname(regsrc.PublicRegistryHost.Normalized()) - credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ - regHost: {"token": testCred}, + credsSrc = svcauth.StaticCredentialsSource(map[svchost.Hostname]svcauth.HostCredentials{ + regHost: svcauth.HostCredentialsToken(testCred), }) ) diff --git a/internal/states/remote/state_test.go b/internal/states/remote/state_test.go index a5dd5a5cf2..0799afef4e 100644 --- a/internal/states/remote/state_test.go +++ b/internal/states/remote/state_test.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + regaddr "github.com/opentofu/registry-address/v2" "github.com/zclconf/go-cty/cty" "github.com/opentofu/opentofu/internal/addrs" @@ -20,7 +21,6 @@ import ( "github.com/opentofu/opentofu/internal/states/statefile" "github.com/opentofu/opentofu/internal/states/statemgr" "github.com/opentofu/opentofu/version" - tfaddr "github.com/opentofu/registry-address" ) func TestState_impl(t *testing.T) { @@ -103,7 +103,7 @@ func TestStatePersist(t *testing.T) { Status: states.ObjectReady, }, addrs.AbsProviderConfig{ - Provider: tfaddr.Provider{Namespace: "local"}, + Provider: regaddr.Provider{Namespace: "local"}, }, addrs.NoKey, ) diff --git a/internal/tofumigrate/tofumigrate.go b/internal/tofumigrate/tofumigrate.go index 3fac3eeb92..4fdfc9b91e 100644 --- a/internal/tofumigrate/tofumigrate.go +++ b/internal/tofumigrate/tofumigrate.go @@ -9,8 +9,9 @@ import ( "os" "github.com/hashicorp/hcl/v2" - tfaddr "github.com/opentofu/registry-address" + regaddr "github.com/opentofu/registry-address/v2" + "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/getproviders" "github.com/opentofu/opentofu/internal/states" @@ -66,8 +67,8 @@ func MigrateStateProviderAddresses(config *configs.Config, state *states.State) for _, module := range stateCopy.Modules { for _, resource := range module.Resources { _, referencedInConfig := providers[resource.ProviderConfig.Provider] - if resource.ProviderConfig.Provider.Hostname == "registry.terraform.io" && !referencedInConfig { - resource.ProviderConfig.Provider.Hostname = tfaddr.DefaultProviderRegistryHost + if resource.ProviderConfig.Provider.Hostname == regaddr.TransitionalDefaultProviderRegistryHost && !referencedInConfig { + resource.ProviderConfig.Provider.Hostname = addrs.DefaultProviderRegistryHost } } }