Skip to content

Commit c47140e

Browse files
Document the architecture for dynamic providers (#2137)
Fixes pulumi/home#3471 As part of documenting what each package does, I moved `loader*.go` from `shim` to `shim/run`, which I think reads clearer. There are no runtime changes in this commit. --------- Co-authored-by: VenelinMartinov <[email protected]>
1 parent 8ca6390 commit c47140e

File tree

5 files changed

+173
-17
lines changed

5 files changed

+173
-17
lines changed

dynamic/README.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,158 @@ sequenceDiagram
9595
destroy B
9696
B-->>P: Cancel done
9797
```
98+
99+
Diving deeper into how the repo is laid out, we see:
100+
101+
``` console
102+
$ eza -T --classify=always -I 'test*' --git-ignore
103+
./
104+
├── go.mod
105+
├── go.sum
106+
├── info.go
107+
├── internal/
108+
│ └── shim/
109+
│ ├── go.mod
110+
│ ├── go.sum
111+
│ ├── protov5/
112+
│ │ ├── provider.go
113+
│ │ └── translate/
114+
│ │ └── tfplugin5.go
115+
│ ├── protov6/
116+
│ │ ├── provider.go
117+
│ │ └── translate/
118+
│ │ └── tfplugin6.go
119+
│ └── run/
120+
│ ├── loader.go
121+
│ └── loader_test.go
122+
├── main.go
123+
├── Makefile
124+
├── provider_test.go
125+
├── README.md
126+
└── version/
127+
└── version.go
128+
```
129+
130+
The dynamic provider layer is by-design as simple and straight-forward as possible. Each package does one
131+
thing only and there isn't that much code. As of time of writing, the entire `dynamic` folder is only 2288
132+
lines of go code[^1]. I'll go through each package in turn.
133+
134+
[^1]: `loc --exclude '*._test.go'`
135+
136+
### `package main`
137+
138+
`package main` is responsible for launching a Pulumi provider and setting up the parameterize call. It does
139+
this by calling [`pf/tfbridge.Main`](https://pkg.go.dev/github.com/pulumi/pulumi-terraform-bridge/[email protected]/tfbridge#Main), passing in an empty PF provider (from
140+
[`pf/proto.Empty()`](https://pkg.go.dev/github.com/pulumi/pulumi-terraform-bridge/[email protected]/proto#Empty)). [`pf/tfbridge.ProviderMetadata`](https://pkg.go.dev/github.com/pulumi/pulumi-terraform-bridge/[email protected]/tfbridge#ProviderMetadata) allows overriding the `Parameterize` and
141+
`GetSchema` call (and we override both).
142+
143+
When `Parameterize` is called, we launch the underlying Terraform provider via
144+
`internal/shim/run.LocalProvider` or `internal/shim/run.NamedProvider` (downloading as necessary). Both
145+
functions return a [`tfprotov6.ProviderServer`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-go/tfprotov6#ProviderServer) which is used to re-initialize the running provider via
146+
[`pf/tfbridge.XParameterizeResetProvider`](https://pkg.go.dev/github.com/pulumi/pulumi-terraform-bridge/[email protected]/tfbridge#XParameterizeResetProvider).
147+
148+
When `GetSchema` is called, it generates a schema from the currently equipped provider with
149+
[`pkg/tfgen.GenerateSchemaWithOptions`](https://pkg.go.dev/github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen#GenerateSchemaWithOptions) and returns is. All type translation, documentation generation, etc
150+
are done with standard bridge based functionality.
151+
152+
All other gRPC calls (`Create`, `Read`, `Update`, `Delete`, etc.) are handled normally by `pf`'s existing
153+
server.
154+
155+
### `package version`
156+
157+
`version.version` is used as a link-time target to bake in the release version to the provider binary. This is
158+
the same mechanism that Pulumi uses to embed versions in all of our binaries.
159+
160+
### `package run`
161+
162+
`run` defines a running provider for the purposes of `dynamic`.
163+
164+
``` go
165+
type Provider interface {
166+
tfprotov6.ProviderServer
167+
io.Closer
168+
169+
Name() string
170+
Version() string
171+
}
172+
```
173+
174+
`run` also defines functions to "run" the underlying TF provider:
175+
176+
- `run.NamedProvider` takes a provider definition like `("cloudfront/cloudfront", ">= 3.2.0")` and loads the
177+
provider (downloading it if necessary). Named Terraform providers are cached in
178+
`PULUMI_DYNAMIC_TF_PLUGIN_CACHE_DIR` (defaulting to `$PULUMI_HOME/dynamic_tf_plugins`).
179+
180+
- `run.LocalProvider` takes a path to a Terraform provider and runs it.
181+
182+
When `run` launches a Terraform provider, the provider may implement either the
183+
[`tfplugin5.ProviderClient`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfplugin5#ProviderClient) or [`tfplugin6.ProviderClient`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfplugin6#ProviderClient) interface. `run` must return a
184+
[`tfprotov6.ProviderServer`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-go/tfprotov6#ProviderServer). The Terraform ecosystem helps with [translating from v5 to v6](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-mux/tf5to6server#UpgradeServer):
185+
186+
``` go
187+
func tf5to6server.UpgradeServer(context.Context, func() tfprotov5.ProviderServer) (tfprotov6.ProviderServer, error)
188+
```
189+
190+
We still need to be able to translate from [`tfplugin5.ProviderClient`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfplugin5#ProviderClient) and [`tfplugin6.ProviderClient`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfplugin6#ProviderClient)
191+
to [`tfprotov5.ProviderServer`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-go/tfprotov5#ProviderServer) and [`tfprotov6.ProviderServer`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-go/tfprotov6#ProviderServer) respectively. For that, see the next
192+
section.
193+
194+
### `package protov5` & `package protov6`
195+
196+
`package protov5` and `package protov6` are nearly identical packages that translate between gRPC level client
197+
types to just above gRPC level server types. Both packages are identical in structure, exposing one end point:
198+
199+
``` go
200+
func New(tfplugin5.ProviderClient) tfprotov5.ProviderServer
201+
202+
func New(tfplugin6.ProviderClient) tfprotov6.ProviderServer
203+
```
204+
205+
Both packages delegate type conversions to a `translate` sub-package, restricting themselves to fielding gRPC
206+
calls.
207+
208+
A representative gRPC handler looks like this:
209+
210+
``` go
211+
// tfprotov6/provider.go
212+
import (
213+
"github.com/opentofu/opentofu/internal/tfplugin6"
214+
"github.com/opentofu/opentofu/shim/protov6/translate"
215+
)
216+
217+
...
218+
219+
func (p shimProvider) ReadResource(
220+
ctx context.Context, req *tfprotov6.ReadResourceRequest,
221+
) (*tfprotov6.ReadResourceResponse, error) {
222+
return translateGRPC(ctx,
223+
p.remote.ReadResource,
224+
translate.ReadResourceRequest(req),
225+
translate.ReadResourceResponse)
226+
}
227+
```
228+
229+
The `translate.ReadResourceRequest` call looks like this:
230+
231+
``` go
232+
// tfprotov6/translate/tfplugin6.go
233+
import (
234+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
235+
"github.com/opentofu/opentofu/internal/tfplugin6"
236+
)
237+
238+
...
239+
240+
func ReadResourceRequest(i *tfprotov6.ReadResourceRequest) *tfplugin6.ReadResource_Request {
241+
if i == nil {
242+
return nil
243+
}
244+
245+
return &tfplugin6.ReadResource_Request{
246+
TypeName: i.TypeName,
247+
CurrentState: dynamicValueRequest(i.CurrentState),
248+
Private: i.Private,
249+
ProviderMeta: dynamicValueRequest(i.ProviderMeta),
250+
}
251+
}
252+
```

dynamic/info.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ import (
2020
"path"
2121
"strings"
2222

23-
"github.com/opentofu/opentofu/shim"
23+
"github.com/opentofu/opentofu/shim/run"
2424

2525
"github.com/pulumi/pulumi-terraform-bridge/pf/proto"
2626
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge"
2727
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/tokens"
2828
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
2929
)
3030

31-
func providerInfo(ctx context.Context, p shim.Provider) tfbridge.ProviderInfo {
31+
func providerInfo(ctx context.Context, p run.Provider) tfbridge.ProviderInfo {
3232
prov := tfbridge.ProviderInfo{
3333
P: proto.New(ctx, p),
3434
Name: p.Name(),

dynamic/internal/shim/loader.go renamed to dynamic/internal/shim/run/loader.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package shim
15+
package run
1616

1717
import (
1818
"context"
@@ -61,13 +61,13 @@ type Provider interface {
6161
Version() string
6262
}
6363

64-
// Load a TF provider with key and version specified.
64+
// NamedProvider loads a TF provider with key and version specified.
6565
//
6666
// If version is "", then whatever version is currently installed will be used. If no
6767
// version is installed then the latest version can be used.
6868
//
6969
// `=`, `<=`, `>=` sigils can be used just like in TF.
70-
func LoadProvider(ctx context.Context, key, version string) (Provider, error) {
70+
func NamedProvider(ctx context.Context, key, version string) (Provider, error) {
7171

7272
p, err := tfaddr.ParseProviderSource(key)
7373
if err != nil {
@@ -82,8 +82,8 @@ func LoadProvider(ctx context.Context, key, version string) (Provider, error) {
8282
return getProviderServer(ctx, p, v, disco.New())
8383
}
8484

85-
// RunLocalProvider runs a provider by it's path.
86-
func RunLocalProvider(ctx context.Context, path string) (Provider, error) {
85+
// LocalProvider runs a provider by it's path.
86+
func LocalProvider(ctx context.Context, path string) (Provider, error) {
8787
dir, name, ok := cutLast(path, "terraform-provider-")
8888
if !ok {
8989
return nil, fmt.Errorf("expected path to end with %q", "terraform-provider-${NAME}")

dynamic/internal/shim/loader_test.go renamed to dynamic/internal/shim/run/loader_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package shim
15+
package run
1616

1717
import (
1818
"context"
@@ -39,7 +39,7 @@ func TestLoadProvider(t *testing.T) {
3939
Integration(t)
4040
ctx := context.Background()
4141

42-
p, err := LoadProvider(ctx, "hashicorp/tls", "<4.0.5,>4.0.3")
42+
p, err := NamedProvider(ctx, "hashicorp/tls", "<4.0.5,>4.0.3")
4343
require.NoError(t, err)
4444

4545
require.Equal(t, "4.0.4", p.Version())

dynamic/main.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,22 @@ import (
2121
"os"
2222

2323
"github.com/blang/semver"
24-
"github.com/opentofu/opentofu/shim"
24+
"github.com/opentofu/opentofu/shim/run"
25+
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
26+
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
27+
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
28+
2529
"github.com/pulumi/pulumi-terraform-bridge/pf/proto"
2630
pfbridge "github.com/pulumi/pulumi-terraform-bridge/pf/tfbridge"
2731
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge"
2832
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen"
29-
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
30-
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
31-
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
3233

3334
"github.com/pulumi/pulumi-terraform-bridge/dynamic/version"
3435
)
3536

3637
func initialSetup() (tfbridge.ProviderInfo, pfbridge.ProviderMetadata, func() error) {
3738

38-
var tfServer shim.Provider
39+
var tfServer run.Provider
3940
info := tfbridge.ProviderInfo{
4041
DisplayName: "Any Terraform Provider",
4142
P: proto.Empty(),
@@ -139,10 +140,10 @@ func main() {
139140
pfbridge.Main(ctx, "terraform-bridge", defaultInfo, metadata)
140141
}
141142

142-
func getProvider(ctx context.Context, args paramaterizeArgs) (shim.Provider, error) {
143+
func getProvider(ctx context.Context, args paramaterizeArgs) (run.Provider, error) {
143144
if args.path != "" {
144-
return shim.RunLocalProvider(ctx, args.path)
145+
return run.LocalProvider(ctx, args.path)
145146
}
146147

147-
return shim.LoadProvider(ctx, args.name, args.version)
148+
return run.NamedProvider(ctx, args.name, args.version)
148149
}

0 commit comments

Comments
 (0)