diff --git a/dynamic/README.md b/dynamic/README.md index d8d0226f3..812c0ed4d 100644 --- a/dynamic/README.md +++ b/dynamic/README.md @@ -243,6 +243,28 @@ func ReadResourceRequest(i *tfprotov6.ReadResourceRequest) *tfplugin6.ReadResour } } ``` + +### `package parameterize` + +`package parameterize` is responsible for reading and writing the values passed in the [Parameterize](https://pulumi-developer-docs.readthedocs.io/latest/docs/_generated/proto/provider.html#parameterize) gRPC +call. `args.go` is responsible for handling the CLI args (`ParametersArgs)` version of Parameterize, while +`value.go` is responsible for handling the `ParametersValue` version of `Parameterize`. + +#### Args + +Arg values are parsed with [`cobra`](https://github.com/spf13/cobra). + +For maintainers: there are two hidden flags (use `PULUMI_DEV=true` to display) used in example generation: + +| Flag | Description | +|----------------------|------------------------------------------------------------------------------| +| `--fullDocs` | Attempt to generate full docs with documentation. | +| `--upstreamRepoPath` | The local path to the repository root where the upstream provider docs live. | + +These flags are hidden because they are expected to be used by other Pulumi processes, not by end users. + +#### Value + ## Releasing & [`pulumi/pulumi-terraform-provider`](https://github.com/pulumi/pulumi-terraform-provider) The `pulumi-terraform-provider` codebase is located in @@ -283,4 +305,4 @@ Dynamically bridged providers allow the Terraform provider interactions to be re 1. To reproduce the behaviour, maintainers should use the `tf-logs.json` like in `dynamic/log_replay_provider.go:TestLogReplayProviderWithProgram`: 1. Dump the sanitized log file under `testadata`. 1. Use `NewLogReplayProvider` to create a provider which will replay the calls encountered by the user. - 2. Use the `pulcheck` utility to mimic the user actions which triggered the behaviour, like `Preview` and `Up` \ No newline at end of file + 2. Use the `pulcheck` utility to mimic the user actions which triggered the behaviour, like `Preview` and `Up` diff --git a/dynamic/go.mod b/dynamic/go.mod index 750036e8d..ecd2efe2d 100644 --- a/dynamic/go.mod +++ b/dynamic/go.mod @@ -25,15 +25,37 @@ require ( ) require ( + cloud.google.com/go/logging v1.9.0 // indirect + cloud.google.com/go/longrunning v0.5.5 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gofrs/uuid v4.2.0+incompatible // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/wire v0.6.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 // indirect + github.com/hashicorp/vault/api v1.12.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/nxadm/tail v1.4.11 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pulumi/terraform-diff-reader v0.0.2 // indirect github.com/teekennedy/goldmark-markdown v0.3.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yuin/goldmark v1.7.4 // indirect + gocloud.dev v0.37.0 // indirect + gocloud.dev/secrets/hashivault v0.37.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gotest.tools v2.2.0+incompatible // indirect @@ -214,7 +236,7 @@ require ( github.com/skeema/knownhosts v1.2.2 // indirect github.com/spf13/afero v1.9.5 github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect diff --git a/dynamic/go.sum b/dynamic/go.sum index 52977d317..f765bdc8a 100644 --- a/dynamic/go.sum +++ b/dynamic/go.sum @@ -1151,7 +1151,6 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= -github.com/Azure/azure-sdk-for-go v59.2.0+incompatible h1:mbxiZy1K820hQ+dI+YIO/+a0wQDYqOu18BAGe4lXjVk= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= @@ -1241,6 +1240,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHH github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15 h1:7Zwtt/lP3KNRkeZre7soMELMGNoBrutx8nobg1jKWmo= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15/go.mod h1:436h2adoHb57yd+8W+gYPrrA9U/R/SuAuOO42Ushzhw= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= @@ -1520,6 +1521,10 @@ github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMc github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= +github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= +github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk= +github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy+tME4bwyqPcwWbNlUI1Mcg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -1552,6 +1557,7 @@ github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -2155,6 +2161,7 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= @@ -2302,6 +2309,7 @@ golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -2361,6 +2369,7 @@ golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2492,6 +2501,7 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= @@ -2600,6 +2610,7 @@ golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/dynamic/internal/shim/run/loader.go b/dynamic/internal/shim/run/loader.go index 5be199a9e..22398ce78 100644 --- a/dynamic/internal/shim/run/loader.go +++ b/dynamic/internal/shim/run/loader.go @@ -79,7 +79,7 @@ func NamedProvider(ctx context.Context, key, version string) (Provider, error) { v, err := getproviders.ParseVersionConstraints(version) if err != nil { - return nil, err + return nil, fmt.Errorf("could not parse version constraint %q: %w", version, err) } return getProviderServer(ctx, p, v, disco.New()) diff --git a/dynamic/main.go b/dynamic/main.go index e7c108a64..b737cba8b 100644 --- a/dynamic/main.go +++ b/dynamic/main.go @@ -122,7 +122,7 @@ func initialSetup() (info.Provider, pfbridge.ProviderMetadata, func() error) { args = value.IntoArgs() case *plugin.ParameterizeArgs: var err error - args, err = parameterize.ParseArgs(params.Args) + args, err = parameterize.ParseArgs(ctx, params.Args) if err != nil { return plugin.ParameterizeResponse{}, err } diff --git a/dynamic/parameterize/args.go b/dynamic/parameterize/args.go index aa7352899..beeed7e58 100644 --- a/dynamic/parameterize/args.go +++ b/dynamic/parameterize/args.go @@ -15,8 +15,17 @@ package parameterize import ( + "bytes" + "context" + "errors" "fmt" "strings" + + "github.com/pulumi/pulumi/sdk/v3/go/common/env" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + "github.com/spf13/cobra" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" ) // Args represents a parsed CLI argument from a parameterize call. @@ -31,6 +40,7 @@ type RemoteArgs struct { Name string // Version is the (possibly empty) version constraint on the provider. Version string + // Docs indicates if full schema documentation should be generated. Docs bool } @@ -39,6 +49,7 @@ type RemoteArgs struct { type LocalArgs struct { // Path is the path to the provider binary. It can be relative or absolute. Path string + // UpstreamRepoPath (if provided) is the local path to the dynamically bridged Terraform provider's repo. // // If set, full documentation will be generated for the provider. @@ -46,62 +57,87 @@ type LocalArgs struct { UpstreamRepoPath string } -func ParseArgs(args []string) (Args, error) { - // Check for a leading '.' or '/' to indicate a path - if len(args) >= 1 && - (strings.HasPrefix(args[0], "./") || strings.HasPrefix(args[0], "/")) { +func ParseArgs(ctx context.Context, a []string) (Args, error) { + var args Args + var fullDocs bool + var upstreamRepoPath string + cmd := cobra.Command{ + Use: "./local | remote version", + RunE: func(cmd *cobra.Command, a []string) error { + var err error + args, err = parseArgs(cmd.Context(), a, fullDocs, upstreamRepoPath) + return err + }, + Args: cobra.RangeArgs(1, 2), + } + cmd.Flags().BoolVar(&fullDocs, "fullDocs", false, + "Generate a schema with full docs, at the expense of speed") + cmd.Flags().StringVar(&upstreamRepoPath, "upstreamRepoPath", "", + "Specify a local file path to the root of the Git repository of the provider being dynamically bridged") + + // We hide docs flags since they are not intended for end users, and they may not be stable. + if !env.Dev.Value() { + contract.AssertNoErrorf( + errors.Join( + cmd.Flags().MarkHidden("fullDocs"), + cmd.Flags().MarkHidden("upstreamRepoPath"), + ), + "impossible - these are static values and should never fail", + ) + } + + cmd.SetArgs(a) + + // We want to show the stdout of this command to the user, if there is + // any. pulumi/pulumi#17943 started hiding unstructured output by default. This + // block writes the output of `cmd` to `out`, and then logs what was written to + // `out` to info, which will be displayed directly to the user (without any + // prefix, warning and error have a prefix). + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + defer func() { + if out.Len() == 0 { + return + } + tfbridge.GetLogger(ctx).Info(out.String()) + }() + + return args, cmd.ExecuteContext(ctx) +} + +func parseArgs(_ context.Context, args []string, fullDocs bool, upstreamRepoPath string) (Args, error) { + // If we see a local prefix (starts with '.' or '/'), parse args for a local provider + if strings.HasPrefix(args[0], ".") || strings.HasPrefix(args[0], "/") { if len(args) > 1 { - docsArg := args[1] - upstreamRepoPath, found := strings.CutPrefix(docsArg, "upstreamRepoPath=") - if !found { - return Args{}, fmt.Errorf( - "path based providers are only parameterized by 2 arguments: " + - "[upstreamRepoPath=]", - ) - } + return Args{}, fmt.Errorf("local providers only accept one argument, found %d", len(args)) + } + if fullDocs { + msg := "fullDocs only applies to remote providers" if upstreamRepoPath == "" { - return Args{}, fmt.Errorf( - "upstreamRepoPath must be set to a non-empty value: " + - "upstreamRepoPath=path/to/files", - ) + msg += ", consider specifying upstreamRepoPath instead" } - return Args{Local: &LocalArgs{Path: args[0], UpstreamRepoPath: upstreamRepoPath}}, nil + return Args{}, errors.New(msg) } - return Args{Local: &LocalArgs{Path: args[0]}}, nil + return Args{Local: &LocalArgs{Path: args[0], UpstreamRepoPath: upstreamRepoPath}}, nil } - // This is a registry based provider - var remote RemoteArgs - switch len(args) { - // The third argument, if any, is the full docs option for when we need to generate docs - case 3: - docsArg := args[2] - errMsg := "expected third parameterized argument to be 'fullDocs=' or be empty" - - fullDocs, found := strings.CutPrefix(docsArg, "fullDocs=") - if !found { - return Args{}, fmt.Errorf("%s", errMsg) - } - - switch fullDocs { - case "true": - remote.Docs = true - case "false": - // Do nothing - default: - return Args{}, fmt.Errorf("%s", errMsg) + if upstreamRepoPath != "" { + msg := "upstreamRepoPath only applies to local providers" + if upstreamRepoPath == "" { + msg += ", consider specifying fullDocs instead" } + return Args{}, errors.New(msg) + } - fallthrough - // The second argument, if any is the version - case 2: - remote.Version = args[1] - fallthrough - // The first argument is the provider name - case 1: - remote.Name = args[0] - return Args{Remote: &remote}, nil - default: - return Args{}, fmt.Errorf("expected to be parameterized by 1-3 arguments: [version] [fullDocs=]") + var version string + if len(args) > 1 { + version = args[1] } + + return Args{Remote: &RemoteArgs{ + Name: args[0], + Version: version, + Docs: fullDocs, + }}, nil } diff --git a/dynamic/parameterize/args_test.go b/dynamic/parameterize/args_test.go index f3530e36b..11060e175 100644 --- a/dynamic/parameterize/args_test.go +++ b/dynamic/parameterize/args_test.go @@ -15,11 +15,14 @@ package parameterize import ( + "context" "testing" "github.com/hexops/autogold/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/pulumi/pulumi-terraform-bridge/v3/unstable/testutil" ) func TestParseArgs(t *testing.T) { @@ -36,15 +39,13 @@ func TestParseArgs(t *testing.T) { expect: Args{Local: &LocalArgs{Path: "./my-provider"}}, }, { - name: "local too many args", - args: []string{"./my-provider", "nonsense"}, - errMsg: autogold.Expect( - "path based providers are only parameterized by 2 arguments: [upstreamRepoPath=]", - ), + name: "local too many args", + args: []string{"./my-provider", "nonsense"}, + errMsg: autogold.Expect(`local providers only accept one argument, found 2`), }, { name: "local with docs location", - args: []string{"./my-provider", "upstreamRepoPath=./my-provider"}, + args: []string{"./my-provider", "--upstreamRepoPath=./my-provider"}, expect: Args{ Local: &LocalArgs{ Path: "./my-provider", @@ -52,13 +53,6 @@ func TestParseArgs(t *testing.T) { }, }, }, - { - name: "local empty upstreamRepoPath", - args: []string{"./my-provider", "upstreamRepoPath="}, - errMsg: autogold.Expect( - "upstreamRepoPath must be set to a non-empty value: upstreamRepoPath=path/to/files", - ), - }, { name: "remote", args: []string{"my-registry.io/typ"}, @@ -75,19 +69,17 @@ func TestParseArgs(t *testing.T) { { name: "no args", args: []string{}, - errMsg: autogold.Expect("expected to be parameterized by 1-3 arguments: [version] [fullDocs=]"), + errMsg: autogold.Expect("accepts between 1 and 2 arg(s), received 0"), }, { name: "too many args", args: []string{"arg1", "arg2", "arg3", "arg4"}, - errMsg: autogold.Expect("expected to be parameterized by 1-3 arguments: [version] [fullDocs=]"), + errMsg: autogold.Expect("accepts between 1 and 2 arg(s), received 4"), }, { - name: "invalid third arg", - args: []string{"arg1", "arg2", "arg3"}, - errMsg: autogold.Expect( - "expected third parameterized argument to be 'fullDocs=' or be empty", - ), + name: "invalid third arg", + args: []string{"arg1", "arg2", "arg3"}, + errMsg: autogold.Expect(`accepts between 1 and 2 arg(s), received 3`), }, { name: "empty third arg", @@ -100,7 +92,7 @@ func TestParseArgs(t *testing.T) { }, { name: "valid third arg true", - args: []string{"my-registry.io/typ", "1.2.3", "fullDocs=true"}, + args: []string{"my-registry.io/typ", "1.2.3", "--fullDocs=true"}, expect: Args{Remote: &RemoteArgs{ Name: "my-registry.io/typ", Version: "1.2.3", @@ -109,7 +101,7 @@ func TestParseArgs(t *testing.T) { }, { name: "valid third arg false", - args: []string{"my-registry.io/typ", "1.2.3", "fullDocs=false"}, + args: []string{"my-registry.io/typ", "1.2.3", "--fullDocs=false"}, expect: Args{Remote: &RemoteArgs{ Name: "my-registry.io/typ", Version: "1.2.3", @@ -118,9 +110,10 @@ func TestParseArgs(t *testing.T) { }, { name: "third arg invalid input", - args: []string{"my-registry.io/typ", "1.2.3", "fullDocs=invalid-input"}, + args: []string{"my-registry.io/typ", "1.2.3", "--fullDocs=invalid-input"}, + //nolint:lll errMsg: autogold.Expect( - "expected third parameterized argument to be 'fullDocs=' or be empty", + `invalid argument "invalid-input" for "--fullDocs" flag: strconv.ParseBool: parsing "invalid-input": invalid syntax`, ), }, } @@ -128,7 +121,8 @@ func TestParseArgs(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - actual, err := ParseArgs(tt.args) + ctx := testutil.InitLogging(t, context.Background(), nil) + actual, err := ParseArgs(ctx, tt.args) if tt.errMsg == nil { require.NoError(t, err) assert.Equal(t, tt.expect, actual) diff --git a/dynamic/provider_test.go b/dynamic/provider_test.go index c921fba03..a62b19d85 100644 --- a/dynamic/provider_test.go +++ b/dynamic/provider_test.go @@ -469,7 +469,7 @@ func TestSchemaGenerationFullDocs(t *testing.T) { //nolint:paralleltest tc := testCase{ name: "hashicorp/random", version: "3.6.3", - fullDocs: "fullDocs=true", + fullDocs: "--fullDocs", } t.Run(strings.Join([]string{tc.name, tc.version}, "-"), func(t *testing.T) { diff --git a/unstable/testutil/logging.go b/unstable/testutil/logging.go new file mode 100644 index 000000000..9d0753dd8 --- /dev/null +++ b/unstable/testutil/logging.go @@ -0,0 +1,57 @@ +// Copyright 2016-2024, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutil + +import ( + "context" + "testing" + + "github.com/pulumi/pulumi/sdk/v3/go/common/diag" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + + "github.com/pulumi/pulumi-terraform-bridge/v3/internal/logging" +) + +// A sink that [tfbridge.GetLogger] can write to. +// +// This API is experimental and may change or be removed during minor releases. +type LoggingSink interface { + Log(context context.Context, sev diag.Severity, urn resource.URN, msg string) error + LogStatus(context context.Context, sev diag.Severity, urn resource.URN, msg string) error +} + +type discardSink struct{} + +func (*discardSink) Log(context.Context, diag.Severity, resource.URN, string) error { + return nil +} + +func (*discardSink) LogStatus(context.Context, diag.Severity, resource.URN, string) error { + return nil +} + +// InitLogging equips ctx with a logger usable by [tfbridge.GetLogger]. +// +// This API is experimental and may change or be removed during minor releases. +// +//nolint:revive // Let t come before ctx. +func InitLogging(t *testing.T, ctx context.Context, sink LoggingSink) context.Context { + contract.Assertf(t != nil, "t cannot be nil") + if sink == nil { + sink = &discardSink{} + } + return logging.InitLogging(ctx, logging.LogOptions{LogSink: sink}) +}