Adopt OpenTofu's own "svchost" module

Previously we were using a third-party library, but that doesn't have any
support for passing context.Context through its API and so isn't suitable
for our goals of adding OpenTelemetry tracing for all outgoing network
requests.

We now have our own fork that is updated to use context.Context. It also
has a slightly reduced scope no longer including various details that
are tightly-coupled to our cliconfig mechanism and so better placed in the
main OpenTofu codebase so we can evolve it in future without making
lockstep library releases.

The "registry-address" library also uses svchost and uses some of its types
in its public API, so this also incorporates v2 of that library that is
updated to use our own svchost module.

Unfortunately this commit is a mix of mechanical updates to the new
libraries and some new code dealing with the functionality that is removed
in our fork of svchost. The new code is primarily in the "svcauthconfig"
package, which is similar in purpose "ociauthconfig" but for OpenTofu's
own auth mechanism instead of the OCI Distribution protocol's auth
mechanism.

This includes some additional plumbing of context.Context where it was
possible to do so without broad changes to files that would not otherwise
have been included in this commit, but there are a few leftover spots that
are context.TODO() which we'll address separately in later commits.

This removes the temporary workaround from d079da6e9e, since we are now
able to plumb the OpenTelemetry span tree all the way to the service
discovery requests.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins
2025-05-14 17:19:22 -07:00
parent 78a325732d
commit d2bef1fd47
62 changed files with 838 additions and 587 deletions

View File

@@ -12,10 +12,10 @@ import (
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
"github.com/hashicorp/go-retryablehttp" "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/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/addrs"
"github.com/opentofu/opentofu/internal/command" "github.com/opentofu/opentofu/internal/command"
@@ -486,7 +486,7 @@ func makeShutdownCh() <-chan struct{} {
return resultCh return resultCh
} }
func credentialsSource(config *cliconfig.Config) (auth.CredentialsSource, error) { func credentialsSource(config *cliconfig.Config) (svcauth.CredentialsSource, error) {
helperPlugins := pluginDiscovery.FindPlugins("credentials", globalPluginDirs()) helperPlugins := pluginDiscovery.FindPlugins("credentials", globalPluginDirs())
return config.CredentialsSource(helperPlugins) return config.CredentialsSource(helperPlugins)
} }

View File

@@ -14,7 +14,7 @@ import (
"path/filepath" "path/filepath"
"github.com/apparentlymart/go-userdirs/userdirs" "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/addrs"
"github.com/opentofu/opentofu/internal/command/cliconfig" "github.com/opentofu/opentofu/internal/command/cliconfig"

View File

@@ -13,11 +13,11 @@ import (
"time" "time"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
"github.com/hashicorp/terraform-svchost/auth" "github.com/opentofu/svchost/disco"
"github.com/hashicorp/terraform-svchost/disco" "github.com/opentofu/svchost/svcauth"
"github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/httpclient"
"github.com/opentofu/opentofu/internal/logging" "github.com/opentofu/opentofu/internal/logging"
"github.com/opentofu/opentofu/version"
) )
const ( const (
@@ -43,27 +43,15 @@ const (
// object should obtain authentication credentials for service discovery // object should obtain authentication credentials for service discovery
// requests. Passing a nil credSrc is acceptable and means that all discovery // requests. Passing a nil credSrc is acceptable and means that all discovery
// requests are to be made anonymously. // requests are to be made anonymously.
func newServiceDiscovery(_ context.Context, credSrc auth.CredentialsSource) *disco.Disco { func newServiceDiscovery(ctx context.Context, credSrc svcauth.CredentialsSource) *disco.Disco {
services := disco.NewWithCredentialsSource(credSrc)
services.SetUserAgent(httpclient.OpenTofuUserAgent(version.String()))
// For historical reasons, the registry request retry policy also applies // For historical reasons, the registry request retry policy also applies
// to all service discovery requests, which we implement by using transport // to all service discovery requests, which we implement by using transport
// from a HTTP client that is configured for registry client use. // from a HTTP httpClient that is configured for registry httpClient use.
// registryHTTPClient := newRegistryHTTPClient(ctx)
// TEMP: The disco.Disco API isn't yet set up to pass through services := disco.New(
// context.Context, so we're intentionally ignoring the passed-in ctx disco.WithHTTPClient(registryHTTPClient.HTTPClient),
// here to prevent the created client from having OpenTelemetry disco.WithCredentials(credSrc),
// 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
return services return services
} }

22
go.mod
View File

@@ -58,7 +58,6 @@ require (
github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl v1.0.0
github.com/hashicorp/hcl/v2 v2.20.1 github.com/hashicorp/hcl/v2 v2.20.1
github.com/hashicorp/jsonapi v1.3.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/jmespath/go-jmespath v0.4.0
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/lib/pq v1.10.3 github.com/lib/pq v1.10.3
@@ -78,7 +77,8 @@ require (
github.com/openbao/openbao/api/v2 v2.1.0 github.com/openbao/openbao/api/v2 v2.1.0
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.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/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/posener/complete v1.2.3 github.com/posener/complete v1.2.3
@@ -99,14 +99,14 @@ require (
go.opentelemetry.io/otel/sdk v1.35.0 go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/trace v1.35.0 go.opentelemetry.io/otel/trace v1.35.0
go.uber.org/mock v0.4.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/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/mod v0.21.0 golang.org/x/mod v0.21.0
golang.org/x/net v0.36.0 golang.org/x/net v0.40.0
golang.org/x/oauth2 v0.16.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sys v0.30.0 golang.org/x/sys v0.33.0
golang.org/x/term v0.29.0 golang.org/x/term v0.32.0
golang.org/x/text v0.22.0 golang.org/x/text v0.25.0
golang.org/x/tools v0.25.0 golang.org/x/tools v0.25.0
google.golang.org/api v0.155.0 google.golang.org/api v0.155.0
google.golang.org/grpc v1.62.1 google.golang.org/grpc v1.62.1
@@ -122,8 +122,7 @@ require (
require ( require (
cloud.google.com/go v0.112.0 // indirect cloud.google.com/go v0.112.0 // indirect
cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.3.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.5 // indirect cloud.google.com/go/iam v1.1.5 // indirect
github.com/AlecAivazis/survey/v2 v2.3.6 // indirect github.com/AlecAivazis/survey/v2 v2.3.6 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // 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/otel/metric v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.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/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 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 v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/api 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 google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect

45
go.sum
View File

@@ -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.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.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.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
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/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= 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/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= 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/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 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= 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/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/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= 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/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 h1:QHUIrylb/q3pQdQcnAr74cGWsXS1lmA8GqP+RWFMK6U=
github.com/opentofu/hcl/v2 v2.20.2-0.20250121132637-504036cd70e7/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= 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/v2 v2.0.0-20250611143131-d0a99bd8acdd h1:YAAnzmyOoMvm5SuGXL4hhlfBgqz92XDfORGPV3kmQFc=
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/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 h1:9uViuKtx1jrlXLBW/pMnhOfzn3iSEdLase/But/IZRU=
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk= 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= 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.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.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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 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.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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= 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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/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.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= 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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-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-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/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.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.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.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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/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.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-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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=

View File

@@ -6,7 +6,7 @@
package addrs package addrs
import ( import (
tfaddr "github.com/opentofu/registry-address" regaddr "github.com/opentofu/registry-address/v2"
) )
// A ModulePackage represents a physical location where OpenTofu can retrieve // 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 // registry in order to find a real module package address. These being
// distinct is intended to help future maintainers more easily follow the // 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. // series of steps in the module installer, with the help of the type checker.
type ModuleRegistryPackage = tfaddr.ModulePackage type ModuleRegistryPackage = regaddr.ModulePackage

View File

@@ -11,7 +11,7 @@ import (
"strings" "strings"
"github.com/opentofu/opentofu/internal/getmodules" "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 // 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 // combination of a ModuleSourceRegistry and a module version number into
// a concrete ModuleSourceRemote that OpenTofu will then download and // a concrete ModuleSourceRemote that OpenTofu will then download and
// install. // install.
type ModuleSourceRegistry tfaddr.Module type ModuleSourceRegistry regaddr.Module
// DefaultModuleRegistryHost is the hostname used for registry-based module // DefaultModuleRegistryHost is the hostname used for registry-based module
// source addresses that do not have an explicit hostname. // 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 // ParseModuleSourceRegistry is a variant of ParseModuleSource which only
// accepts module registry addresses, and will reject any other address type. // 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) 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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -10,7 +10,7 @@ import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
) )
func TestParseModuleSource(t *testing.T) { func TestParseModuleSource(t *testing.T) {

View File

@@ -7,23 +7,32 @@ package addrs
import ( import (
"github.com/hashicorp/hcl/v2" "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" "github.com/opentofu/opentofu/internal/tfdiags"
tfaddr "github.com/opentofu/registry-address"
) )
// Provider encapsulates a single provider type. In the future this will be // Provider encapsulates a single provider type. In the future this will be
// extended to include additional fields including Namespace and SourceHost // 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 // DefaultProviderRegistryHost is the hostname used for provider addresses that do
// not have an explicit hostname. // not have an explicit hostname.
const DefaultProviderRegistryHost = tfaddr.DefaultProviderRegistryHost const DefaultProviderRegistryHost = regaddr.DefaultProviderRegistryHost
// BuiltInProviderHost is the pseudo-hostname used for the "built-in" provider // BuiltInProviderHost is the pseudo-hostname used for the "built-in" provider
// namespace. Built-in provider addresses must also have their namespace set // namespace. Built-in provider addresses must also have their namespace set
// to BuiltInProviderNamespace in order to be considered as built-in. // 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" // BuiltInProviderNamespace is the provider namespace used for "built-in"
// providers. Built-in provider addresses must also have their hostname // 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 // 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 // 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. // 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 // LegacyProviderNamespace is the special string used in the Namespace field
// of type Provider to mark a legacy provider address. This special namespace // 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 // 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 // DefaultRegistryHost because that host owns the mapping from legacy name to
// FQN. // FQN.
const LegacyProviderNamespace = tfaddr.LegacyProviderNamespace const LegacyProviderNamespace = regaddr.LegacyProviderNamespace
func IsDefaultProvider(addr Provider) bool { func IsDefaultProvider(addr Provider) bool {
return addr.Hostname == DefaultProviderRegistryHost && addr.Namespace == "hashicorp" 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 // When accepting namespace or type values from outside the program, use
// ParseProviderPart first to check that the given value is valid. // ParseProviderPart first to check that the given value is valid.
func NewProvider(hostname svchost.Hostname, namespace, typeName string) Provider { 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 // 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, // NewDefaultProvider returns the default address of a HashiCorp-maintained,
// Registry-hosted provider. // Registry-hosted provider.
func NewDefaultProvider(name string) Provider { func NewDefaultProvider(name string) Provider {
return tfaddr.Provider{ return regaddr.Provider{
Type: MustParseProviderPart(name), Type: MustParseProviderPart(name),
Namespace: "hashicorp", Namespace: "hashicorp",
Hostname: DefaultProviderRegistryHost, Hostname: DefaultProviderRegistryHost,
@@ -97,7 +106,7 @@ func NewDefaultProvider(name string) Provider {
// NewBuiltInProvider returns the address of a "built-in" provider. See // NewBuiltInProvider returns the address of a "built-in" provider. See
// the docs for Provider.IsBuiltIn for more information. // the docs for Provider.IsBuiltIn for more information.
func NewBuiltInProvider(name string) Provider { func NewBuiltInProvider(name string) Provider {
return tfaddr.Provider{ return regaddr.Provider{
Type: MustParseProviderPart(name), Type: MustParseProviderPart(name),
Namespace: BuiltInProviderNamespace, Namespace: BuiltInProviderNamespace,
Hostname: BuiltInProviderHost, Hostname: BuiltInProviderHost,
@@ -127,11 +136,11 @@ func NewLegacyProvider(name string) Provider {
// - name // - name
// - namespace/name // - namespace/name
// - hostname/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 var diags tfdiags.Diagnostics
ret, err := tfaddr.ParseProviderSource(str) ret, err := regaddr.ParseProviderSource(str)
if pe, ok := err.(*tfaddr.ParserError); ok { if pe, ok := err.(*regaddr.ParserError); ok {
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: pe.Summary, 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 // 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. // subsequent call, in which case the result will be identical.
func ParseProviderPart(given string) (string, error) { func ParseProviderPart(given string) (string, error) {
return tfaddr.ParseProviderPart(given) return regaddr.ParseProviderPart(given)
} }
// MustParseProviderPart is a wrapper around ParseProviderPart that panics if // MustParseProviderPart is a wrapper around ParseProviderPart that panics if

View File

@@ -9,7 +9,7 @@ import (
"testing" "testing"
"github.com/go-test/deep" "github.com/go-test/deep"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
) )
func TestProviderString(t *testing.T) { func TestProviderString(t *testing.T) {

View File

@@ -15,8 +15,10 @@ import (
"log" "log"
"os" "os"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/mitchellh/go-homedir" "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/addrs"
"github.com/opentofu/opentofu/internal/command/clistate" "github.com/opentofu/opentofu/internal/command/clistate"
"github.com/opentofu/opentofu/internal/command/views" "github.com/opentofu/opentofu/internal/command/views"
@@ -31,7 +33,6 @@ import (
"github.com/opentofu/opentofu/internal/states/statemgr" "github.com/opentofu/opentofu/internal/states/statemgr"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
"github.com/opentofu/opentofu/internal/tofu" "github.com/opentofu/opentofu/internal/tofu"
"github.com/zclconf/go-cty/cty"
) )
// DefaultStateName is the name of the default, initial state that every // DefaultStateName is the name of the default, initial state that every

View File

@@ -10,12 +10,10 @@ package init
import ( import (
"sync" "sync"
"github.com/hashicorp/terraform-svchost/disco" "github.com/opentofu/svchost/disco"
"github.com/opentofu/opentofu/internal/backend"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/backend"
backendLocal "github.com/opentofu/opentofu/internal/backend/local" backendLocal "github.com/opentofu/opentofu/internal/backend/local"
backendRemote "github.com/opentofu/opentofu/internal/backend/remote" backendRemote "github.com/opentofu/opentofu/internal/backend/remote"
backendAzure "github.com/opentofu/opentofu/internal/backend/remote-state/azure" 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" backendPg "github.com/opentofu/opentofu/internal/backend/remote-state/pg"
backendS3 "github.com/opentofu/opentofu/internal/backend/remote-state/s3" backendS3 "github.com/opentofu/opentofu/internal/backend/remote-state/s3"
backendCloud "github.com/opentofu/opentofu/internal/cloud" 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 // backends is the list of available backends. This is a global variable

View File

@@ -19,13 +19,15 @@ import (
tfe "github.com/hashicorp/go-tfe" tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version" 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/cli"
"github.com/mitchellh/colorstring" "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"
"github.com/opentofu/opentofu/internal/backend" "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/configs/configschema"
"github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/httpclient"
@@ -35,8 +37,6 @@ import (
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
"github.com/opentofu/opentofu/internal/tofu" "github.com/opentofu/opentofu/internal/tofu"
tfversion "github.com/opentofu/opentofu/version" tfversion "github.com/opentofu/opentofu/version"
backendLocal "github.com/opentofu/opentofu/internal/backend/local"
) )
const ( 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 // Discover the service URL for this host to confirm that it provides
// a remote backend API and to get the version constraints. // 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. // Historical note: in OpenTofu's predecessor project there was an
if constraints != nil { // extra step here of checking some metadata returned by the remote
diags = diags.Append(b.checkConstraints(constraints)) // API describing which versions of the predecessor's CLI it considers
if diags.HasErrors() { // itself to be compatible with. Since OpenTofu's version numbers have
return diags // 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 // When we don't have any constraints errors, also check for discovery
// errors before we continue. // 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. // 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) hostname, err := svchost.ForComparison(b.hostname)
if err != nil { 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 { if err != nil {
return nil, nil, err return nil, err
} }
service, err := host.ServiceURL(serviceID) service, err := host.ServiceURL(serviceID)
// Return the error, unless its a disco.ErrVersionNotSupported error. // Return the error, unless its a disco.ErrVersionNotSupported error.
if _, ok := err.(*disco.ErrVersionNotSupported); !ok && err != nil { 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 return service, nil
// 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))
} }
// token returns the token for this host as configured in the credentials // 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 { if err != nil {
return "", err return "", err
} }
creds, err := b.services.CredentialsForHost(hostname) creds, err := b.services.CredentialsForHost(context.TODO(), hostname)
if err != nil { if err != nil {
log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err) log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err)
return "", nil 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 creds.Token(), nil
} }
return "", nil return "", nil

View File

@@ -14,15 +14,14 @@ import (
tfe "github.com/hashicorp/go-tfe" tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-svchost/disco" "github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/backend" "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/encryption"
"github.com/opentofu/opentofu/internal/states/statemgr" "github.com/opentofu/opentofu/internal/states/statemgr"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
tfversion "github.com/opentofu/opentofu/version" 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) { 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) { func TestRemote_localBackend(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() 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) { func TestRemote_StateMgr_versionCheck(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()

View File

@@ -16,24 +16,22 @@ import (
"time" "time"
tfe "github.com/hashicorp/go-tfe" 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/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" "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/cloud"
"github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/httpclient"
"github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/states/remote" "github.com/opentofu/opentofu/internal/states/remote"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
"github.com/opentofu/opentofu/internal/tofu" "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 ( const (
@@ -42,8 +40,8 @@ const (
var ( var (
mockedBackendHost = "app.example.com" mockedBackendHost = "app.example.com"
credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ credsSrc = svcauth.StaticCredentialsSource(map[svchost.Hostname]svcauth.HostCredentials{
svchost.Hostname(mockedBackendHost): {"token": testCred}, 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 { func testInput(t *testing.T, answers map[string]string) *mockInput {
t.Helper()
return &mockInput{answers: answers} return &mockInput{answers: answers}
} }
func testBackendDefault(t *testing.T) (*Remote, func()) { func testBackendDefault(t *testing.T) (*Remote, func()) {
t.Helper()
obj := cty.ObjectVal(map[string]cty.Value{ obj := cty.ObjectVal(map[string]cty.Value{
"hostname": cty.StringVal(mockedBackendHost), "hostname": cty.StringVal(mockedBackendHost),
"organization": cty.StringVal("hashicorp"), "organization": cty.StringVal("hashicorp"),
@@ -98,6 +98,7 @@ func testBackendNoDefault(t *testing.T) (*Remote, func()) {
} }
func testBackendNoOperations(t *testing.T) (*Remote, func()) { func testBackendNoOperations(t *testing.T) (*Remote, func()) {
t.Helper()
obj := cty.ObjectVal(map[string]cty.Value{ obj := cty.ObjectVal(map[string]cty.Value{
"hostname": cty.StringVal(mockedBackendHost), "hostname": cty.StringVal(mockedBackendHost),
"organization": cty.StringVal("no-operations"), "organization": cty.StringVal("no-operations"),
@@ -111,6 +112,7 @@ func testBackendNoOperations(t *testing.T) (*Remote, func()) {
} }
func testRemoteClient(t *testing.T) remote.Client { func testRemoteClient(t *testing.T) remote.Client {
t.Helper()
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
@@ -123,6 +125,8 @@ func testRemoteClient(t *testing.T) remote.Client {
} }
func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) { func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) {
t.Helper()
s := testServer(t) s := testServer(t)
b := New(testDisco(s), encryption.StateEncryptionDisabled()) 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 { func testLocalBackend(t *testing.T, remote *Remote) backend.Enhanced {
t.Helper()
b := backendLocal.NewWithBackend(remote, nil) b := backendLocal.NewWithBackend(remote, nil)
// Add a test provider to the local backend. // 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. // testServer returns a *httptest.Server used for local testing.
func testServer(t *testing.T) *httptest.Server { func testServer(t *testing.T) *httptest.Server {
t.Helper()
mux := http.NewServeMux() mux := http.NewServeMux()
// Respond to service discovery calls. // 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), "tfe.v2.1": fmt.Sprintf("%s/api/v2/", s.URL),
"versions.v1": fmt.Sprintf("%s/v1/versions/", s.URL), "versions.v1": fmt.Sprintf("%s/v1/versions/", s.URL),
} }
d := disco.NewWithCredentialsSource(credsSrc) d := disco.New(
d.SetUserAgent(httpclient.OpenTofuUserAgent(version.String())) disco.WithCredentials(credsSrc),
disco.WithHTTPClient(s.Client()),
)
d.ForceHostServices(svchost.Hostname(mockedBackendHost), services) d.ForceHostServices(svchost.Hostname(mockedBackendHost), services)
d.ForceHostServices(svchost.Hostname("localhost"), services) d.ForceHostServices(svchost.Hostname("localhost"), services)

View File

@@ -20,10 +20,11 @@ import (
tfe "github.com/hashicorp/go-tfe" tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version" 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/cli"
"github.com/mitchellh/colorstring" "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"
"github.com/zclconf/go-cty/cty/gocty" "github.com/zclconf/go-cty/cty/gocty"
@@ -490,7 +491,7 @@ func (b *Cloud) discover() (*url.URL, error) {
return nil, err return nil, err
} }
host, err := b.services.Discover(hostname) host, err := b.services.Discover(context.TODO(), hostname)
if err != nil { if err != nil {
var serviceDiscoErr *disco.ErrServiceDiscoveryNetworkRequest var serviceDiscoErr *disco.ErrServiceDiscoveryNetworkRequest
@@ -520,12 +521,24 @@ func (b *Cloud) cliConfigToken() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
creds, err := b.services.CredentialsForHost(hostname) creds, err := b.services.CredentialsForHost(context.TODO(), hostname)
if err != nil { if err != nil {
log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err) log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err)
return "", nil 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 creds.Token(), nil
} }
return "", nil return "", nil

View File

@@ -21,26 +21,23 @@ import (
"time" "time"
tfe "github.com/hashicorp/go-tfe" 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/cli"
"github.com/mitchellh/colorstring" "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"
"github.com/opentofu/opentofu/internal/backend" "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"
"github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/httpclient"
"github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/states/statefile" "github.com/opentofu/opentofu/internal/states/statefile"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
"github.com/opentofu/opentofu/internal/tofu" "github.com/opentofu/opentofu/internal/tofu"
"github.com/opentofu/opentofu/version"
backendLocal "github.com/opentofu/opentofu/internal/backend/local"
) )
const ( const (
@@ -49,8 +46,8 @@ const (
var ( var (
tfeHost = "app.terraform.io" tfeHost = "app.terraform.io"
credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ credsSrc = svcauth.StaticCredentialsSource(map[svchost.Hostname]svcauth.HostCredentials{
svchost.Hostname(tfeHost): {"token": testCred}, svchost.Hostname(tfeHost): svcauth.HostCredentialsToken("testCred"),
}) })
testBackendSingleWorkspaceName = "app-prod" testBackendSingleWorkspaceName = "app-prod"
defaultTFCPing = map[string]func(http.ResponseWriter, *http.Request){ defaultTFCPing = map[string]func(http.ResponseWriter, *http.Request){
@@ -598,8 +595,10 @@ func testDisco(s *httptest.Server) *disco.Disco {
services := map[string]interface{}{ services := map[string]interface{}{
"tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL), "tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL),
} }
d := disco.NewWithCredentialsSource(credsSrc) d := disco.New(
d.SetUserAgent(httpclient.OpenTofuUserAgent(version.String())) disco.WithCredentials(credsSrc),
disco.WithHTTPClient(s.Client()),
)
d.ForceHostServices(svchost.Hostname(tfeHost), services) d.ForceHostServices(svchost.Hostname(tfeHost), services)
d.ForceHostServices(svchost.Hostname("localhost"), services) d.ForceHostServices(svchost.Hostname("localhost"), services)

View File

@@ -24,7 +24,7 @@ import (
"strings" "strings"
"github.com/hashicorp/hcl" "github.com/hashicorp/hcl"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
) )

View File

@@ -7,6 +7,7 @@ package cliconfig
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
@@ -14,12 +15,12 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/opentofu/svchost"
"github.com/opentofu/svchost/svcauth"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json" ctyjson "github.com/zclconf/go-cty/cty/json"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/opentofu/internal/command/cliconfig/svcauthconfig"
svcauth "github.com/hashicorp/terraform-svchost/auth"
"github.com/opentofu/opentofu/internal/configs/hcl2shim" "github.com/opentofu/opentofu/internal/configs/hcl2shim"
pluginDiscovery "github.com/opentofu/opentofu/internal/plugin/discovery" pluginDiscovery "github.com/opentofu/opentofu/internal/plugin/discovery"
"github.com/opentofu/opentofu/internal/replacefile" "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) return nil, fmt.Errorf("can't locate credentials file: %w", err)
} }
var helper svcauth.CredentialsSource var helper svcauth.CredentialsStore
var helperType string var helperType string
for givenType, givenConfig := range c.CredentialsHelpers { for givenType, givenConfig := range c.CredentialsHelpers {
available := helperPlugins.WithName(givenType) available := helperPlugins.WithName(givenType)
@@ -58,8 +59,8 @@ func (c *Config) CredentialsSource(helperPlugins pluginDiscovery.PluginMetaSet)
selected := available.Newest() selected := available.Newest()
helperSource := svcauth.HelperProgramCredentialsSource(selected.Path, givenConfig.Args...) helperSource := svcauthconfig.NewHelperProgramCredentialsStore(selected.Path, givenConfig.Args...)
helper = svcauth.CachingCredentialsSource(helperSource) // cached because external operation may be slow/expensive helper = svcauth.CachingCredentialsStore(helperSource)
helperType = givenType helperType = givenType
// There should only be zero or one "credentials_helper" blocks. We // 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 // credentialsSource is an internal factory for the credentials source which
// allows overriding the credentials file path, which allows setting it to // allows overriding the credentials file path, which allows setting it to
// a temporary file location when testing. // 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{} configured := map[svchost.Hostname]cty.Value{}
for userHost, creds := range c.Credentials { for userHost, creds := range c.Credentials {
host, err := svchost.ForComparison(userHost) host, err := svchost.ForComparison(userHost)
@@ -221,7 +222,7 @@ type CredentialsSource struct {
// hostnames not explicitly represented in "configured". Any writes to // hostnames not explicitly represented in "configured". Any writes to
// the credentials store will also be sent to a configured helper instead // the credentials store will also be sent to a configured helper instead
// of the credentials.tfrc.json file. // of the credentials.tfrc.json file.
helper svcauth.CredentialsSource helper svcauth.CredentialsStore
// helperType is the name of the type of credentials helper that is // helperType is the name of the type of credentials helper that is
// referenced in "helper", or the empty string if "helper" is nil. // referenced in "helper", or the empty string if "helper" is nil.
@@ -231,7 +232,7 @@ type CredentialsSource struct {
// Assertion that credentialsSource implements CredentialsSource // Assertion that credentialsSource implements CredentialsSource
var _ svcauth.CredentialsSource = (*CredentialsSource)(nil) 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 // The first order of precedence for credentials is a host-specific environment variable
if envCreds := hostCredentialsFromEnv(host); envCreds != nil { if envCreds := hostCredentialsFromEnv(host); envCreds != nil {
return 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 // Then, any credentials block present in the CLI config
v, ok := s.configured[host] v, ok := s.configured[host]
if ok { if ok {
return svcauth.HostCredentialsFromObject(v), nil return svcauthconfig.HostCredentialsFromObject(v), nil
} }
// And finally, the credentials helper // And finally, the credentials helper
if s.helper != nil { if s.helper != nil {
return s.helper.ForHost(host) return s.helper.ForHost(ctx, host)
} }
return nil, nil return nil, nil
} }
func (s *CredentialsSource) StoreForHost(host svchost.Hostname, credentials svcauth.HostCredentialsWritable) error { func (s *CredentialsSource) StoreForHost(ctx context.Context, host svchost.Hostname, credentials svcauth.NewHostCredentials) error {
return s.updateHostCredentials(host, credentials) return s.updateHostCredentials(ctx, host, credentials)
} }
func (s *CredentialsSource) ForgetForHost(host svchost.Hostname) error { func (s *CredentialsSource) ForgetForHost(ctx context.Context, host svchost.Hostname) error {
return s.updateHostCredentials(host, nil) return s.updateHostCredentials(ctx, host, nil)
} }
// HostCredentialsLocation returns a value indicating what type of storage is // HostCredentialsLocation returns a value indicating what type of storage is
@@ -297,7 +298,7 @@ func (s *CredentialsSource) CredentialsHelperType() string {
return s.helperType 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 { switch loc := s.HostCredentialsLocation(host); loc {
case CredentialsInOtherFile: case CredentialsInOtherFile:
return ErrUnwritableHostCredentials(host) return ErrUnwritableHostCredentials(host)
@@ -311,16 +312,16 @@ func (s *CredentialsSource) updateHostCredentials(host svchost.Hostname, new svc
case CredentialsViaHelper: case CredentialsViaHelper:
// Delegate entirely to the helper, then. // Delegate entirely to the helper, then.
if new == nil { 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: default:
// Should never happen because the above cases are exhaustive // Should never happen because the above cases are exhaustive
return fmt.Errorf("invalid credentials location %#v", loc) 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, // This function updates the local credentials file in particular,
// regardless of whether a credentials helper is active. It should be // regardless of whether a credentials helper is active. It should be
// called only indirectly via updateHostCredentials. // called only indirectly via updateHostCredentials.

View File

@@ -6,15 +6,18 @@
package cliconfig package cliconfig
import ( import (
"context"
"fmt"
"net/http" "net/http"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/opentofu/svchost"
"github.com/opentofu/svchost/svcauth"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/opentofu/internal/command/cliconfig/svcauthconfig"
svcauth "github.com/hashicorp/terraform-svchost/auth"
) )
func TestCredentialsForHost(t *testing.T) { 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 // a credentials helper program, since we're only testing the logic
// for choosing when to delegate to the helper here. The logic for // for choosing when to delegate to the helper here. The logic for
// interacting with a helper program is tested in the svcauth package. // interacting with a helper program is tested in the svcauth package.
helper: svcauth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ helper: readOnlyCredentialsStore{
"from-helper.example.com": { svcauth.StaticCredentialsSource(map[svchost.Hostname]svcauth.HostCredentials{
"token": "from-helper", "from-helper.example.com": svcauth.HostCredentialsToken("from-helper"),
},
// This should be shadowed by the "configured" entry with the same // This should be shadowed by the "configured" entry with the same
// hostname above. // hostname above.
"configured.example.com": { "configured.example.com": svcauth.HostCredentialsToken("incorrectly-from-helper"),
"token": "incorrectly-from-helper", }),
}, },
}),
helperType: "fake", helperType: "fake",
} }
@@ -62,7 +63,7 @@ func TestCredentialsForHost(t *testing.T) {
} }
t.Run("configured", func(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 { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
@@ -71,7 +72,7 @@ func TestCredentialsForHost(t *testing.T) {
} }
}) })
t.Run("from helper", func(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 { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
@@ -80,7 +81,7 @@ func TestCredentialsForHost(t *testing.T) {
} }
}) })
t.Run("not available", func(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 { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
@@ -94,7 +95,7 @@ func TestCredentialsForHost(t *testing.T) {
expectedToken := "configured-by-env" expectedToken := "configured-by-env"
t.Setenv(envName, expectedToken) 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 { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
@@ -103,7 +104,7 @@ func TestCredentialsForHost(t *testing.T) {
t.Fatal("no credentials found") 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) t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
} }
}) })
@@ -115,7 +116,7 @@ func TestCredentialsForHost(t *testing.T) {
t.Setenv(envName, expectedToken) t.Setenv(envName, expectedToken)
hostname, _ := svchost.ForComparison("env.ドメイン名例.com") hostname, _ := svchost.ForComparison("env.ドメイン名例.com")
creds, err := credSrc.ForHost(hostname) creds, err := credSrc.ForHost(t.Context(), hostname)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
@@ -125,7 +126,7 @@ func TestCredentialsForHost(t *testing.T) {
t.Fatal("no credentials found") 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) t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
} }
}) })
@@ -137,7 +138,7 @@ func TestCredentialsForHost(t *testing.T) {
t.Setenv(envName, expectedToken) t.Setenv(envName, expectedToken)
hostname, _ := svchost.ForComparison("env.café.fr") hostname, _ := svchost.ForComparison("env.café.fr")
creds, err := credSrc.ForHost(hostname) creds, err := credSrc.ForHost(t.Context(), hostname)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
@@ -147,7 +148,7 @@ func TestCredentialsForHost(t *testing.T) {
t.Fatal("no credentials found") 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) t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
} }
}) })
@@ -159,7 +160,7 @@ func TestCredentialsForHost(t *testing.T) {
t.Setenv(envName, expectedToken) t.Setenv(envName, expectedToken)
hostname, _ := svchost.ForComparison("configured.example.com") hostname, _ := svchost.ForComparison("configured.example.com")
creds, err := credSrc.ForHost(hostname) creds, err := credSrc.ForHost(t.Context(), hostname)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
@@ -169,7 +170,7 @@ func TestCredentialsForHost(t *testing.T) {
t.Fatal("no credentials found") 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) t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
} }
}) })
@@ -181,7 +182,7 @@ func TestCredentialsForHost(t *testing.T) {
t.Setenv(envName, expectedToken) t.Setenv(envName, expectedToken)
hostname, _ := svchost.ForComparison("configureduppercase.example.com") hostname, _ := svchost.ForComparison("configureduppercase.example.com")
creds, err := credSrc.ForHost(hostname) creds, err := credSrc.ForHost(t.Context(), hostname)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
@@ -191,7 +192,7 @@ func TestCredentialsForHost(t *testing.T) {
t.Fatal("no credentials found") 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) 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. // Otherwise downstream tests might fail in confusing ways.
{ {
err := credSrc.StoreForHost( err := credSrc.StoreForHost(
t.Context(),
svchost.Hostname("manually-configured.example.com"), svchost.Hostname("manually-configured.example.com"),
svcauth.HostCredentialsToken("not-manually-configured"), svcauth.HostCredentialsToken("not-manually-configured"),
) )
@@ -251,6 +253,7 @@ func TestCredentialsStoreForget(t *testing.T) {
} }
{ {
err := credSrc.ForgetForHost( err := credSrc.ForgetForHost(
t.Context(),
svchost.Hostname("manually-configured.example.com"), svchost.Hostname("manually-configured.example.com"),
) )
if err == nil { 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 // We don't have a credentials file at all yet, so this first call
// must create it. // must create it.
err := credSrc.StoreForHost( err := credSrc.StoreForHost(
t.Context(),
svchost.Hostname("stored-locally.example.com"), svchost.Hostname("stored-locally.example.com"),
svcauth.HostCredentialsToken("stored-locally"), svcauth.HostCredentialsToken("stored-locally"),
) )
@@ -271,7 +275,7 @@ func TestCredentialsStoreForget(t *testing.T) {
t.Fatalf("unexpected error storing locally: %s", err) 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 { if err != nil {
t.Fatalf("failed to read back stored-locally credentials: %s", err) t.Fatalf("failed to read back stored-locally credentials: %s", err)
} }
@@ -303,6 +307,7 @@ func TestCredentialsStoreForget(t *testing.T) {
) )
{ {
err := credSrc.StoreForHost( err := credSrc.StoreForHost(
t.Context(),
svchost.Hostname("manually-configured.example.com"), svchost.Hostname("manually-configured.example.com"),
svcauth.HostCredentialsToken("not-manually-configured"), svcauth.HostCredentialsToken("not-manually-configured"),
) )
@@ -312,6 +317,7 @@ func TestCredentialsStoreForget(t *testing.T) {
} }
{ {
err := credSrc.StoreForHost( err := credSrc.StoreForHost(
t.Context(),
svchost.Hostname("stored-in-helper.example.com"), svchost.Hostname("stored-in-helper.example.com"),
svcauth.HostCredentialsToken("stored-in-helper"), svcauth.HostCredentialsToken("stored-in-helper"),
) )
@@ -319,7 +325,7 @@ func TestCredentialsStoreForget(t *testing.T) {
t.Fatalf("unexpected error storing in helper: %s", err) 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 { if err != nil {
t.Fatalf("failed to read back stored-in-helper credentials: %s", err) 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 // Because stored-locally is already in the credentials file, a new
// store should be sent there rather than to the credentials helper. // store should be sent there rather than to the credentials helper.
err := credSrc.StoreForHost( err := credSrc.StoreForHost(
t.Context(),
svchost.Hostname("stored-locally.example.com"), svchost.Hostname("stored-locally.example.com"),
svcauth.HostCredentialsToken("stored-locally-again"), svcauth.HostCredentialsToken("stored-locally-again"),
) )
@@ -348,7 +355,7 @@ func TestCredentialsStoreForget(t *testing.T) {
t.Fatalf("unexpected error storing locally again: %s", err) 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 { if err != nil {
t.Fatalf("failed to read back stored-locally credentials: %s", err) 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 // Forgetting a host already in the credentials file should remove it
// from the credentials file, not from the helper. // from the credentials file, not from the helper.
err := credSrc.ForgetForHost( err := credSrc.ForgetForHost(
t.Context(),
svchost.Hostname("stored-locally.example.com"), svchost.Hostname("stored-locally.example.com"),
) )
if err != nil { if err != nil {
t.Fatalf("unexpected error forgetting locally: %s", err) 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 { if err != nil {
t.Fatalf("failed to read back stored-locally credentials: %s", err) t.Fatalf("failed to read back stored-locally credentials: %s", err)
} }
@@ -385,13 +393,14 @@ func TestCredentialsStoreForget(t *testing.T) {
} }
{ {
err := credSrc.ForgetForHost( err := credSrc.ForgetForHost(
t.Context(),
svchost.Hostname("stored-in-helper.example.com"), svchost.Hostname("stored-in-helper.example.com"),
) )
if err != nil { if err != nil {
t.Fatalf("unexpected error forgetting in helper: %s", err) 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 { if err != nil {
t.Fatalf("failed to read back stored-in-helper credentials: %s", err) 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 // Assertion that mockCredentialsHelper implements svcauth.CredentialsSource
var _ svcauth.CredentialsSource = (*mockCredentialsHelper)(nil) 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] v, ok := s.current[hostname]
if !ok { if !ok {
return nil, nil 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{ s.log = append(s.log, mockCredentialsHelperChange{
Host: hostname, Host: hostname,
Action: "store", Action: "store",
@@ -451,7 +460,7 @@ func (s *mockCredentialsHelper) StoreForHost(hostname svchost.Hostname, new svca
return nil 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{ s.log = append(s.log, mockCredentialsHelperChange{
Host: hostname, Host: hostname,
Action: "forget", Action: "forget",
@@ -459,3 +468,29 @@ func (s *mockCredentialsHelper) ForgetForHost(hostname svchost.Hostname) error {
delete(s.current, hostname) delete(s.current, hostname)
return nil 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")
}

View File

@@ -14,7 +14,7 @@ import (
hcltoken "github.com/hashicorp/hcl/hcl/token" hcltoken "github.com/hashicorp/hcl/hcl/token"
hcl2 "github.com/hashicorp/hcl/v2" hcl2 "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax" "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"
"github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/convert"

View File

@@ -11,7 +11,8 @@ import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "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/addrs"
"github.com/opentofu/opentofu/internal/getproviders" "github.com/opentofu/opentofu/internal/getproviders"
) )

View File

@@ -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())
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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")
}
})
}

View File

@@ -0,0 +1 @@
main

View File

@@ -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)
}

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -eu
cd "$( dirname "${BASH_SOURCE[0]}" )"
[ -x main ] || go build -o main .
exec ./main "$@"

View File

@@ -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)
}

View File

@@ -26,8 +26,8 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/hashicorp/terraform-svchost/disco" "github.com/opentofu/svchost/disco"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"

View File

@@ -14,7 +14,7 @@ import (
"strings" "strings"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
otelAttr "go.opentelemetry.io/otel/attribute" otelAttr "go.opentelemetry.io/otel/attribute"

View File

@@ -22,9 +22,9 @@ import (
"strings" "strings"
tfe "github.com/hashicorp/go-tfe" tfe "github.com/hashicorp/go-tfe"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
svcauth "github.com/hashicorp/terraform-svchost/auth" "github.com/opentofu/svchost/disco"
"github.com/hashicorp/terraform-svchost/disco" "github.com/opentofu/svchost/svcauth"
"github.com/opentofu/opentofu/internal/command/cliconfig" "github.com/opentofu/opentofu/internal/command/cliconfig"
"github.com/opentofu/opentofu/internal/httpclient" "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.) // working as expected. (Perhaps the normalization is part of the cause.)
dispHostname := hostname.ForDisplay() dispHostname := hostname.ForDisplay()
host, err := c.Services.Discover(hostname) host, err := c.Services.Discover(ctx, hostname)
if err != nil { if err != nil {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,
@@ -220,7 +220,7 @@ func (c *LoginCommand) Run(args []string) int {
return 1 return 1
} }
err = creds.StoreForHost(hostname, token) err = creds.StoreForHost(ctx, hostname, token)
if err != nil { if err != nil {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,

View File

@@ -13,16 +13,15 @@ import (
"testing" "testing"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/opentofu/svchost"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost/disco"
"github.com/hashicorp/terraform-svchost/disco"
"github.com/opentofu/opentofu/internal/command/cliconfig" "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" oauthserver "github.com/opentofu/opentofu/internal/command/testdata/login-oauth-server"
tfeserver "github.com/opentofu/opentofu/internal/command/testdata/login-tfe-server" tfeserver "github.com/opentofu/opentofu/internal/command/testdata/login-tfe-server"
"github.com/opentofu/opentofu/internal/command/webbrowser" "github.com/opentofu/opentofu/internal/command/webbrowser"
"github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/httpclient"
"github.com/opentofu/opentofu/version"
) )
func TestLogin(t *testing.T) { func TestLogin(t *testing.T) {
@@ -57,8 +56,10 @@ func TestLogin(t *testing.T) {
} }
creds := cliconfig.EmptyCredentialsSourceForTests(filepath.Join(workDir, "credentials.tfrc.json")) creds := cliconfig.EmptyCredentialsSourceForTests(filepath.Join(workDir, "credentials.tfrc.json"))
svcs := disco.NewWithCredentialsSource(creds) svcs := disco.New(
svcs.SetUserAgent(httpclient.OpenTofuUserAgent(version.String())) disco.WithCredentials(creds),
disco.WithHTTPClient(httpclient.New(t.Context())),
)
svcs.ForceHostServices(svchost.Hostname("example.com"), map[string]interface{}{ svcs.ForceHostServices(svchost.Hostname("example.com"), map[string]interface{}{
"login.v1": map[string]interface{}{ "login.v1": map[string]interface{}{
@@ -135,11 +136,11 @@ func TestLogin(t *testing.T) {
} }
credsSrc := c.Services.CredentialsSource() credsSrc := c.Services.CredentialsSource()
creds, err := credsSrc.ForHost(svchost.Hostname(tfeHost)) creds, err := credsSrc.ForHost(t.Context(), svchost.Hostname(tfeHost))
if err != nil { if err != nil {
t.Errorf("failed to retrieve credentials: %s", err) 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) t.Errorf("wrong token %q; want %q", got, want)
} }
if got, want := ui.OutputWriter.String(), "Welcome to the cloud backend!"; !strings.Contains(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() 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 { if err != nil {
t.Errorf("failed to retrieve credentials: %s", err) 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) 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) { 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") client, _ := host.ServiceOAuthClient("login.v1")
if len(client.Scopes) != 0 { if len(client.Scopes) != 0 {
t.Errorf("unexpected scopes %q; expected none", client.Scopes) t.Errorf("unexpected scopes %q; expected none", client.Scopes)
@@ -191,13 +192,13 @@ func TestLogin(t *testing.T) {
} }
credsSrc := c.Services.CredentialsSource() 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 { if err != nil {
t.Errorf("failed to retrieve credentials: %s", err) 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) 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) { 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") client, _ := host.ServiceOAuthClient("login.v1")
expectedScopes := [2]string{"app1.full_access", "app2.read_only"} expectedScopes := [2]string{"app1.full_access", "app2.read_only"}
@@ -234,11 +235,11 @@ func TestLogin(t *testing.T) {
} }
credsSrc := c.Services.CredentialsSource() 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 { if err != nil {
t.Errorf("failed to retrieve credentials: %s", err) 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) t.Errorf("wrong token %q; want %q", got, want)
} }
@@ -259,12 +260,12 @@ func TestLogin(t *testing.T) {
} }
credsSrc := c.Services.CredentialsSource() 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 { if err != nil {
t.Errorf("failed to retrieve credentials: %s", err) t.Errorf("failed to retrieve credentials: %s", err)
} }
if creds != nil { 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)) }, true))

View File

@@ -10,7 +10,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/command/cliconfig" "github.com/opentofu/opentofu/internal/command/cliconfig"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
@@ -24,6 +24,8 @@ type LogoutCommand struct {
// Run implements cli.Command. // Run implements cli.Command.
func (c *LogoutCommand) Run(args []string) int { func (c *LogoutCommand) Run(args []string) int {
ctx := c.CommandContext()
args = c.Meta.process(args) args = c.Meta.process(args)
cmdFlags := c.Meta.defaultFlagSet("logout") cmdFlags := c.Meta.defaultFlagSet("logout")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 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)) 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 { if err != nil {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,

View File

@@ -11,11 +11,12 @@ import (
"testing" "testing"
"github.com/mitchellh/cli" "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"
"github.com/opentofu/opentofu/internal/command/cliconfig/svcauthconfig"
) )
func TestLogout(t *testing.T) { func TestLogout(t *testing.T) {
@@ -26,8 +27,10 @@ func TestLogout(t *testing.T) {
c := &LogoutCommand{ c := &LogoutCommand{
Meta: Meta{ Meta: Meta{
Ui: ui, Ui: ui,
Services: disco.NewWithCredentialsSource(credsSrc), Services: disco.New(
disco.WithCredentials(credsSrc),
),
}, },
} }
@@ -61,7 +64,7 @@ func TestLogout(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
host := svchost.Hostname(tc.hostname) host := svchost.Hostname(tc.hostname)
token := svcauth.HostCredentialsToken("some-token") token := svcauth.HostCredentialsToken("some-token")
err := credsSrc.StoreForHost(host, token) err := credsSrc.StoreForHost(t.Context(), host, token)
if err != nil { if err != nil {
t.Fatalf("unexpected error storing credentials: %s", err) 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()) 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 { if err != nil {
t.Errorf("failed to retrieve credentials: %s", err) t.Errorf("failed to retrieve credentials: %s", err)
} }
if tc.shouldRemove { if tc.shouldRemove {
if creds != nil { 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 { } 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) t.Errorf("wrong token %q; want %q", got, want)
} }
} }

View File

@@ -21,9 +21,9 @@ import (
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
"github.com/hashicorp/terraform-svchost/disco"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/mitchellh/colorstring" "github.com/mitchellh/colorstring"
"github.com/opentofu/svchost/disco"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/backend" "github.com/opentofu/opentofu/internal/backend"

View File

@@ -24,7 +24,7 @@ import (
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2/hclsyntax" "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/addrs"
"github.com/opentofu/opentofu/internal/depsfile" "github.com/opentofu/opentofu/internal/depsfile"

View File

@@ -9,7 +9,7 @@ import (
"fmt" "fmt"
"sort" "sort"
svchost "github.com/hashicorp/terraform-svchost" svchost "github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/getproviders" "github.com/opentofu/opentofu/internal/getproviders"

View File

@@ -15,7 +15,8 @@ import (
"path" "path"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
) )

View File

@@ -9,7 +9,8 @@ import (
"context" "context"
"testing" "testing"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
) )

View File

@@ -9,7 +9,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
) )

View File

@@ -12,8 +12,8 @@ import (
"github.com/apparentlymart/go-versions/versions" "github.com/apparentlymart/go-versions/versions"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/opentofu/svchost"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
) )

View File

@@ -12,7 +12,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
) )

View File

@@ -19,8 +19,8 @@ import (
"time" "time"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
svcauth "github.com/hashicorp/terraform-svchost/auth" "github.com/opentofu/svchost/svcauth"
"golang.org/x/net/idna" "golang.org/x/net/idna"
"github.com/opentofu/opentofu/internal/addrs" "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 // It might return an error if the mirror base URL is invalid, or if the
// credentials lookup itself fails. // 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() hostname, err := s.mirrorHost()
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid provider mirror base URL %s: %w", s.baseURL.String(), err) 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 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. // 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 = req.WithContext(ctx)
req.Request.Header.Set(terraformVersionHeader, version.String()) req.Request.Header.Set(terraformVersionHeader, version.String())
creds, err := s.mirrorHostCredentials() creds, err := s.mirrorHostCredentials(ctx)
if err != nil { if err != nil {
return 0, nil, endpointURL, fmt.Errorf("failed to determine request credentials: %w", err) return 0, nil, endpointURL, fmt.Errorf("failed to determine request credentials: %w", err)
} }

View File

@@ -15,8 +15,8 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
svcauth "github.com/hashicorp/terraform-svchost/auth" "github.com/opentofu/svchost/svcauth"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
) )
@@ -33,10 +33,8 @@ func TestHTTPMirrorSource(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("httptest.NewTLSServer returned a server with an invalid URL") t.Fatalf("httptest.NewTLSServer returned a server with an invalid URL")
} }
creds := svcauth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ creds := svcauth.StaticCredentialsSource(map[svchost.Hostname]svcauth.HostCredentials{
svchost.Hostname(baseURL.Host): { svchost.Hostname(baseURL.Host): svcauth.HostCredentialsToken("placeholder-token"),
"token": "placeholder-token",
},
}) })
retryHTTPClient := retryablehttp.NewClient() retryHTTPClient := retryablehttp.NewClient()
retryHTTPClient.HTTPClient = httpClient retryHTTPClient.HTTPClient = httpClient

View File

@@ -10,7 +10,7 @@ import (
"fmt" "fmt"
"strings" "strings"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
) )

View File

@@ -17,10 +17,10 @@ import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
svchost "github.com/hashicorp/terraform-svchost"
ociDigest "github.com/opencontainers/go-digest" ociDigest "github.com/opencontainers/go-digest"
ociSpecs "github.com/opencontainers/image-spec/specs-go" ociSpecs "github.com/opencontainers/image-spec/specs-go"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1" ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opentofu/svchost"
orasContent "oras.land/oras-go/v2/content" orasContent "oras.land/oras-go/v2/content"
orasOCI "oras.land/oras-go/v2/content/oci" orasOCI "oras.land/oras-go/v2/content/oci"
orasErrors "oras.land/oras-go/v2/errdef" orasErrors "oras.land/oras-go/v2/errdef"

View File

@@ -18,8 +18,8 @@ import (
"path" "path"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
svcauth "github.com/hashicorp/terraform-svchost/auth" "github.com/opentofu/svchost/svcauth"
otelAttr "go.opentelemetry.io/otel/attribute" otelAttr "go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0" semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"

View File

@@ -16,8 +16,9 @@ import (
"github.com/apparentlymart/go-versions/versions" "github.com/apparentlymart/go-versions/versions"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
disco "github.com/hashicorp/terraform-svchost/disco" disco "github.com/opentofu/svchost/disco"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
) )

View File

@@ -11,8 +11,8 @@ import (
"time" "time"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
disco "github.com/hashicorp/terraform-svchost/disco" disco "github.com/opentofu/svchost/disco"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/httpclient" "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) { 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 { if err != nil {
return nil, ErrHostUnreachable{ return nil, ErrHostUnreachable{
Hostname: hostname, 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. // 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 { if err != nil {
// This indicates that a credentials helper failed, which means we // This indicates that a credentials helper failed, which means we
// can't do anything better than just pass through the helper's // can't do anything better than just pass through the helper's

View File

@@ -12,11 +12,10 @@ import (
"strings" "strings"
"testing" "testing"
tfaddr "github.com/opentofu/registry-address"
"github.com/apparentlymart/go-versions/versions" "github.com/apparentlymart/go-versions/versions"
"github.com/google/go-cmp/cmp" "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" "github.com/opentofu/opentofu/internal/addrs"
) )
@@ -142,7 +141,7 @@ func TestSourcePackageMeta(t *testing.T) {
[]SigningKey{ []SigningKey{
{ASCIIArmor: TestingPublicKey}, {ASCIIArmor: TestingPublicKey},
}, },
tfaddr.Provider{Hostname: "example.com", Namespace: "awesomesauce", Type: "happycloud"}, regaddr.Provider{Hostname: "example.com", Namespace: "awesomesauce", Type: "happycloud"},
), ),
) )

View File

@@ -18,7 +18,7 @@ import (
"github.com/go-test/deep" "github.com/go-test/deep"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
version "github.com/hashicorp/go-version" 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/addrs"
"github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs"

View File

@@ -22,8 +22,8 @@ import (
"github.com/apparentlymart/go-versions/versions/constraints" "github.com/apparentlymart/go-versions/versions/constraints"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/hashicorp/terraform-svchost/disco" "github.com/opentofu/svchost/disco"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/depsfile" "github.com/opentofu/opentofu/internal/depsfile"

View File

@@ -18,8 +18,8 @@ import (
"time" "time"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/hashicorp/terraform-svchost/disco" "github.com/opentofu/svchost/disco"
otelAttr "go.opentelemetry.io/otel/attribute" otelAttr "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace" "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. // Discover queries the host, and returns the url for the registry.
func (c *Client) Discover(host svchost.Hostname, serviceID string) (*url.URL, error) { func (c *Client) Discover(ctx context.Context, host svchost.Hostname, serviceID string) (*url.URL, error) {
service, err := c.services.DiscoverServiceURL(host, serviceID) service, err := c.services.DiscoverServiceURL(ctx, host, serviceID)
if err != nil { if err != nil {
return nil, &ServiceUnreachableError{err} return nil, &ServiceUnreachableError{err}
} }
@@ -96,7 +96,7 @@ func (c *Client) ModuleVersions(ctx context.Context, module *regsrc.Module) (*re
return nil, err return nil, err
} }
service, err := c.Discover(host, modulesServiceID) service, err := c.Discover(ctx, host, modulesServiceID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -116,7 +116,7 @@ func (c *Client) ModuleVersions(ctx context.Context, module *regsrc.Module) (*re
} }
req = req.WithContext(ctx) req = req.WithContext(ctx)
c.addRequestCreds(host, req.Request) c.addRequestCreds(ctx, host, req.Request)
req.Header.Set(xTerraformVersion, tfVersion) req.Header.Set(xTerraformVersion, tfVersion)
resp, err := c.client.Do(req) resp, err := c.client.Do(req)
@@ -150,8 +150,8 @@ func (c *Client) ModuleVersions(ctx context.Context, module *regsrc.Module) (*re
return &versions, nil return &versions, nil
} }
func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) { func (c *Client) addRequestCreds(ctx context.Context, host svchost.Hostname, req *http.Request) {
creds, err := c.services.CredentialsForHost(host) creds, err := c.services.CredentialsForHost(ctx, host)
if err != nil { if err != nil {
log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err) log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err)
return return
@@ -179,7 +179,7 @@ func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, vers
return "", err return "", err
} }
service, err := c.Discover(host, modulesServiceID) service, err := c.Discover(ctx, host, modulesServiceID)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -204,7 +204,7 @@ func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, vers
req = req.WithContext(ctx) req = req.WithContext(ctx)
c.addRequestCreds(host, req.Request) c.addRequestCreds(ctx, host, req.Request)
req.Header.Set(xTerraformVersion, tfVersion) req.Header.Set(xTerraformVersion, tfVersion)
resp, err := c.client.Do(req) resp, err := c.client.Do(req)

View File

@@ -18,13 +18,12 @@ import (
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
version "github.com/hashicorp/go-version" 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/httpclient"
"github.com/opentofu/opentofu/internal/registry/regsrc" "github.com/opentofu/opentofu/internal/registry/regsrc"
"github.com/opentofu/opentofu/internal/registry/response" "github.com/opentofu/opentofu/internal/registry/response"
"github.com/opentofu/opentofu/internal/registry/test" "github.com/opentofu/opentofu/internal/registry/test"
tfversion "github.com/opentofu/opentofu/version"
) )
func TestLookupModuleVersions(t *testing.T) { func TestLookupModuleVersions(t *testing.T) {
@@ -150,8 +149,9 @@ func TestAccLookupModuleVersions(t *testing.T) {
if os.Getenv("TF_ACC") == "" { if os.Getenv("TF_ACC") == "" {
t.Skip() t.Skip()
} }
regDisco := disco.New() regDisco := disco.New(
regDisco.SetUserAgent(httpclient.OpenTofuUserAgent(tfversion.String())) disco.WithHTTPClient(httpclient.New(t.Context())),
)
// test with and without a hostname // test with and without a hostname
for _, src := range []string{ for _, src := range []string{

View File

@@ -8,7 +8,8 @@ package registry
import ( import (
"fmt" "fmt"
"github.com/hashicorp/terraform-svchost/disco" "github.com/opentofu/svchost/disco"
"github.com/opentofu/opentofu/internal/registry/regsrc" "github.com/opentofu/opentofu/internal/registry/regsrc"
) )

View File

@@ -9,7 +9,7 @@ import (
"regexp" "regexp"
"strings" "strings"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
) )
var ( var (

View File

@@ -11,7 +11,8 @@ import (
"regexp" "regexp"
"strings" "strings"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
) )

View File

@@ -10,13 +10,12 @@ import (
"regexp" "regexp"
"strings" "strings"
svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/svchost"
"github.com/hashicorp/terraform-svchost/auth" "github.com/opentofu/svchost/disco"
"github.com/hashicorp/terraform-svchost/disco" "github.com/opentofu/svchost/svcauth"
"github.com/opentofu/opentofu/internal/httpclient"
"github.com/opentofu/opentofu/internal/registry/regsrc" "github.com/opentofu/opentofu/internal/registry/regsrc"
"github.com/opentofu/opentofu/internal/registry/response" "github.com/opentofu/opentofu/internal/registry/response"
tfversion "github.com/opentofu/opentofu/version"
) )
// Disco return a *disco.Disco mapping registry.opentofu.org, localhost, // 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), "modules.v1": fmt.Sprintf("%s/v1/modules", s.URL),
"providers.v1": fmt.Sprintf("%s/v1/providers", s.URL), "providers.v1": fmt.Sprintf("%s/v1/providers", s.URL),
} }
d := disco.NewWithCredentialsSource(credsSrc) d := disco.New(
d.SetUserAgent(httpclient.OpenTofuUserAgent(tfversion.String())) disco.WithCredentials(credsSrc),
disco.WithHTTPClient(s.Client()),
)
d.ForceHostServices(svchost.Hostname("registry.opentofu.org"), services) d.ForceHostServices(svchost.Hostname("registry.opentofu.org"), services)
d.ForceHostServices(svchost.Hostname("localhost"), services) d.ForceHostServices(svchost.Hostname("localhost"), services)
@@ -58,8 +59,8 @@ const (
var ( var (
regHost = svchost.Hostname(regsrc.PublicRegistryHost.Normalized()) regHost = svchost.Hostname(regsrc.PublicRegistryHost.Normalized())
credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ credsSrc = svcauth.StaticCredentialsSource(map[svchost.Hostname]svcauth.HostCredentials{
regHost: {"token": testCred}, regHost: svcauth.HostCredentialsToken(testCred),
}) })
) )

View File

@@ -12,6 +12,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
regaddr "github.com/opentofu/registry-address/v2"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
@@ -20,7 +21,6 @@ import (
"github.com/opentofu/opentofu/internal/states/statefile" "github.com/opentofu/opentofu/internal/states/statefile"
"github.com/opentofu/opentofu/internal/states/statemgr" "github.com/opentofu/opentofu/internal/states/statemgr"
"github.com/opentofu/opentofu/version" "github.com/opentofu/opentofu/version"
tfaddr "github.com/opentofu/registry-address"
) )
func TestState_impl(t *testing.T) { func TestState_impl(t *testing.T) {
@@ -103,7 +103,7 @@ func TestStatePersist(t *testing.T) {
Status: states.ObjectReady, Status: states.ObjectReady,
}, },
addrs.AbsProviderConfig{ addrs.AbsProviderConfig{
Provider: tfaddr.Provider{Namespace: "local"}, Provider: regaddr.Provider{Namespace: "local"},
}, },
addrs.NoKey, addrs.NoKey,
) )

View File

@@ -9,8 +9,9 @@ import (
"os" "os"
"github.com/hashicorp/hcl/v2" "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/configs"
"github.com/opentofu/opentofu/internal/getproviders" "github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/states"
@@ -66,8 +67,8 @@ func MigrateStateProviderAddresses(config *configs.Config, state *states.State)
for _, module := range stateCopy.Modules { for _, module := range stateCopy.Modules {
for _, resource := range module.Resources { for _, resource := range module.Resources {
_, referencedInConfig := providers[resource.ProviderConfig.Provider] _, referencedInConfig := providers[resource.ProviderConfig.Provider]
if resource.ProviderConfig.Provider.Hostname == "registry.terraform.io" && !referencedInConfig { if resource.ProviderConfig.Provider.Hostname == regaddr.TransitionalDefaultProviderRegistryHost && !referencedInConfig {
resource.ProviderConfig.Provider.Hostname = tfaddr.DefaultProviderRegistryHost resource.ProviderConfig.Provider.Hostname = addrs.DefaultProviderRegistryHost
} }
} }
} }