Skip to content

Commit c369cdc

Browse files
authored
Add ListResource RPC (#525)
* Add ListResource RPC * Add trace logging for each event * simplify * Add examples/terraform-provider-primes * lint
1 parent 15d30b9 commit c369cdc

File tree

14 files changed

+638
-34
lines changed

14 files changed

+638
-34
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# terraform-provider-primes
2+
3+
```bash
4+
$ TF_LOG_SDK_PROTO=trace TF_PLUGIN_MAGIC_COOKIE=[value] go run ./examples/terraform-provider-primes
5+
6+
{"@level":"debug","@message":"plugin address","@timestamp":"2037-06-30T10:10:00.627659+04:00","address":"...","network":"unix"}
7+
Provider started. To attach Terraform CLI, set the TF_REATTACH_PROVIDERS environment variable with the following:
8+
9+
TF_REATTACH_PROVIDERS='{"terraform.io/playground/primes":{"Protocol":"grpc","ProtocolVersion":5,"Pid":...,"Test":true,"Addr":{"Network":"unix","String":"..."}}}'
10+
11+
Writing reattach configuration to env file at path reattach.env
12+
13+
$ grpcurl -plaintext $(examples/terraform-provider-primes/reattach.sh) list
14+
grpc.health.v1.Health
15+
grpc.reflection.v1.ServerReflection
16+
grpc.reflection.v1alpha.ServerReflection
17+
plugin.GRPCBroker
18+
plugin.GRPCController
19+
plugin.GRPCStdio
20+
tfplugin5.Provider
21+
22+
$ grpcurl -plaintext $(examples/terraform-provider-primes/reattach.sh) tfplugin5.Provider.GetSchema
23+
{
24+
"provider": {},
25+
"listResourceSchemas": {
26+
"prime": {
27+
"block": {
28+
"attributes": [
29+
{
30+
"name": "number",
31+
"type": "Im51bWJlciI=",
32+
"description": "The nth prime"
33+
},
34+
{
35+
"name": "ordinal",
36+
"type": "Im51bWJlciI=",
37+
"description": "n"
38+
}
39+
]
40+
}
41+
}
42+
}
43+
}
44+
45+
$ grpcurl -plaintext $(examples/terraform-provider-primes/reattach.sh) tfplugin5.Provider.ListResource
46+
{
47+
"displayName": "primes[1]: 2",
48+
"resourceObject": {
49+
"msgpack": "gqZudW1iZXICp29yZGluYWwB"
50+
}
51+
}
52+
{
53+
"displayName": "primes[2]: 3",
54+
"resourceObject": {
55+
"msgpack": "gqZudW1iZXIDp29yZGluYWwC"
56+
}
57+
}
58+
{
59+
"displayName": "primes[3]: 5",
60+
"resourceObject": {
61+
"msgpack": "gqZudW1iZXIFp29yZGluYWwD"
62+
}
63+
}
64+
{
65+
"displayName": "primes[4]: 7",
66+
"resourceObject": {
67+
"msgpack": "gqZudW1iZXIHp29yZGluYWwE"
68+
}
69+
}
70+
...
71+
```
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package main
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
8+
"github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server"
9+
)
10+
11+
func main() {
12+
providerFn := func() tfprotov5.ProviderServer {
13+
return PrimeNumberProvider{}
14+
}
15+
16+
serveOpts := []tf5server.ServeOpt{
17+
tf5server.WithManagedDebug(),
18+
tf5server.WithManagedDebugEnvFilePath(".env.reattach"),
19+
}
20+
tf5server.Serve("terraform.io/playground/primes", providerFn, serveOpts...) //nolint:errcheck
21+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package main
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"time"
10+
11+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
12+
"github.com/hashicorp/terraform-plugin-go/tftypes"
13+
)
14+
15+
var _ tfprotov5.ProviderServerWithListResource = PrimeNumberProvider{} //nolint:staticcheck
16+
17+
type PrimeNumberProvider struct {
18+
}
19+
20+
func (p PrimeNumberProvider) GetMetadata(ctx context.Context, request *tfprotov5.GetMetadataRequest) (*tfprotov5.GetMetadataResponse, error) {
21+
return &tfprotov5.GetMetadataResponse{
22+
ListResources: []tfprotov5.ListResourceMetadata{
23+
{
24+
TypeName: "prime",
25+
},
26+
},
27+
}, nil
28+
}
29+
30+
func (p PrimeNumberProvider) GetResourceIdentitySchemas(ctx context.Context, request *tfprotov5.GetResourceIdentitySchemasRequest) (*tfprotov5.GetResourceIdentitySchemasResponse, error) {
31+
return &tfprotov5.GetResourceIdentitySchemasResponse{
32+
IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{
33+
"prime": {
34+
IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{
35+
{
36+
Name: "number",
37+
Type: tftypes.Number,
38+
RequiredForImport: true,
39+
},
40+
},
41+
},
42+
},
43+
}, nil
44+
}
45+
46+
func (p PrimeNumberProvider) GetProviderSchema(ctx context.Context, request *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) {
47+
return &tfprotov5.GetProviderSchemaResponse{
48+
Provider: &tfprotov5.Schema{
49+
Block: &tfprotov5.SchemaBlock{
50+
Description: "Prime Provider",
51+
DescriptionKind: tfprotov5.StringKindPlain,
52+
},
53+
},
54+
ResourceSchemas: map[string]*tfprotov5.Schema{
55+
"prime": p.primeSchema(),
56+
},
57+
ListResourceSchemas: map[string]*tfprotov5.Schema{
58+
"prime": p.primeSchema(),
59+
},
60+
}, nil
61+
}
62+
63+
func (p PrimeNumberProvider) primeSchema() *tfprotov5.Schema {
64+
return &tfprotov5.Schema{
65+
Block: &tfprotov5.SchemaBlock{
66+
Attributes: []*tfprotov5.SchemaAttribute{
67+
{
68+
Name: "number",
69+
Type: tftypes.Number,
70+
Description: "The nth prime",
71+
DescriptionKind: tfprotov5.StringKindPlain,
72+
Optional: true,
73+
},
74+
{
75+
Name: "ordinal",
76+
Type: tftypes.Number,
77+
Description: "n",
78+
DescriptionKind: tfprotov5.StringKindPlain,
79+
Optional: true,
80+
},
81+
},
82+
},
83+
}
84+
}
85+
86+
func (p PrimeNumberProvider) PrepareProviderConfig(ctx context.Context, request *tfprotov5.PrepareProviderConfigRequest) (*tfprotov5.PrepareProviderConfigResponse, error) {
87+
return &tfprotov5.PrepareProviderConfigResponse{
88+
PreparedConfig: request.Config,
89+
}, nil
90+
}
91+
92+
func (p PrimeNumberProvider) ConfigureProvider(ctx context.Context, request *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) {
93+
return &tfprotov5.ConfigureProviderResponse{}, nil
94+
}
95+
96+
func (p PrimeNumberProvider) convertToDynamicValue(number int, ordinal int) (tfprotov5.DynamicValue, error) {
97+
typ := p.primeSchema().ValueType()
98+
value := map[string]tftypes.Value{
99+
"number": tftypes.NewValue(tftypes.Number, number),
100+
"ordinal": tftypes.NewValue(tftypes.Number, ordinal),
101+
}
102+
103+
return tfprotov5.NewDynamicValue(typ, tftypes.NewValue(typ, value))
104+
}
105+
106+
func (p PrimeNumberProvider) ListResource(ctx context.Context, request *tfprotov5.ListResourceRequest) (*tfprotov5.ListResourceServerStream, error) {
107+
primes := []int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97}
108+
109+
results := func(push func(tfprotov5.ListResourceResult) bool) {
110+
for i, prime := range primes {
111+
ordinal := i + 1
112+
displayName := fmt.Sprintf("primes[%d]: %d", ordinal, prime)
113+
114+
resourceObject, err := p.convertToDynamicValue(prime, ordinal)
115+
if err != nil {
116+
panic(err)
117+
}
118+
119+
protoEv := tfprotov5.ListResourceResult{
120+
DisplayName: displayName,
121+
Resource: &resourceObject,
122+
}
123+
if !push(protoEv) {
124+
fmt.Println("let's stop here")
125+
return
126+
}
127+
128+
fmt.Println("1 second nap")
129+
time.Sleep(1 * time.Second)
130+
}
131+
}
132+
133+
return &tfprotov5.ListResourceServerStream{
134+
Results: results,
135+
}, nil
136+
}
137+
138+
func (p PrimeNumberProvider) ValidateListResourceConfig(ctx context.Context, request *tfprotov5.ValidateListResourceConfigRequest) (*tfprotov5.ValidateListResourceConfigResponse, error) {
139+
return &tfprotov5.ValidateListResourceConfigResponse{}, nil
140+
}
141+
142+
func (p PrimeNumberProvider) StopProvider(ctx context.Context, request *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) {
143+
return &tfprotov5.StopProviderResponse{}, nil
144+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package main
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
10+
)
11+
12+
func (p PrimeNumberProvider) UpgradeResourceIdentity(ctx context.Context, request *tfprotov5.UpgradeResourceIdentityRequest) (*tfprotov5.UpgradeResourceIdentityResponse, error) {
13+
panic("not implemented")
14+
}
15+
16+
func (p PrimeNumberProvider) ValidateResourceTypeConfig(ctx context.Context, request *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) {
17+
panic("not implemented")
18+
}
19+
20+
func (p PrimeNumberProvider) UpgradeResourceState(ctx context.Context, request *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) {
21+
panic("not implemented")
22+
}
23+
24+
func (p PrimeNumberProvider) ReadResource(ctx context.Context, request *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) {
25+
panic("not implemented")
26+
}
27+
28+
func (p PrimeNumberProvider) PlanResourceChange(ctx context.Context, request *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) {
29+
panic("not implemented")
30+
}
31+
32+
func (p PrimeNumberProvider) ApplyResourceChange(ctx context.Context, request *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) {
33+
panic("not implemented")
34+
}
35+
36+
func (p PrimeNumberProvider) ImportResourceState(ctx context.Context, request *tfprotov5.ImportResourceStateRequest) (*tfprotov5.ImportResourceStateResponse, error) {
37+
panic("not implemented")
38+
}
39+
40+
func (p PrimeNumberProvider) MoveResourceState(ctx context.Context, request *tfprotov5.MoveResourceStateRequest) (*tfprotov5.MoveResourceStateResponse, error) {
41+
panic("not implemented")
42+
}
43+
44+
func (p PrimeNumberProvider) ValidateDataSourceConfig(ctx context.Context, request *tfprotov5.ValidateDataSourceConfigRequest) (*tfprotov5.ValidateDataSourceConfigResponse, error) {
45+
panic("not implemented")
46+
}
47+
48+
func (p PrimeNumberProvider) ReadDataSource(ctx context.Context, request *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) {
49+
panic("not implemented")
50+
}
51+
52+
func (p PrimeNumberProvider) CallFunction(ctx context.Context, request *tfprotov5.CallFunctionRequest) (*tfprotov5.CallFunctionResponse, error) {
53+
panic("not implemented")
54+
}
55+
56+
func (p PrimeNumberProvider) GetFunctions(ctx context.Context, request *tfprotov5.GetFunctionsRequest) (*tfprotov5.GetFunctionsResponse, error) {
57+
panic("not implemented")
58+
}
59+
60+
func (p PrimeNumberProvider) ValidateEphemeralResourceConfig(ctx context.Context, request *tfprotov5.ValidateEphemeralResourceConfigRequest) (*tfprotov5.ValidateEphemeralResourceConfigResponse, error) {
61+
panic("not implemented")
62+
}
63+
64+
func (p PrimeNumberProvider) OpenEphemeralResource(ctx context.Context, request *tfprotov5.OpenEphemeralResourceRequest) (*tfprotov5.OpenEphemeralResourceResponse, error) {
65+
panic("not implemented")
66+
}
67+
68+
func (p PrimeNumberProvider) RenewEphemeralResource(ctx context.Context, request *tfprotov5.RenewEphemeralResourceRequest) (*tfprotov5.RenewEphemeralResourceResponse, error) {
69+
panic("not implemented")
70+
}
71+
72+
func (p PrimeNumberProvider) CloseEphemeralResource(ctx context.Context, request *tfprotov5.CloseEphemeralResourceRequest) (*tfprotov5.CloseEphemeralResourceResponse, error) {
73+
panic("not implemented")
74+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) HashiCorp, Inc.
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
6+
source .env.reattach
7+
echo $TF_REATTACH_PROVIDERS | jq -r '.["terraform.io/playground/primes"].Addr.Network + "://" + .["terraform.io/playground/primes"].Addr.String'

tfprotov5/internal/fromproto/list_resource.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ import (
88
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
99
)
1010

11+
func ListResourceRequest(in *tfplugin5.ListResource_Request) *tfprotov5.ListResourceRequest {
12+
if in == nil {
13+
return nil
14+
}
15+
16+
return &tfprotov5.ListResourceRequest{
17+
TypeName: in.TypeName,
18+
Config: DynamicValue(in.Config),
19+
IncludeResource: in.IncludeResourceObject,
20+
}
21+
}
22+
1123
func ValidateListResourceConfigRequest(in *tfplugin5.ValidateListResourceConfig_Request) *tfprotov5.ValidateListResourceConfigRequest {
1224
if in == nil {
1325
return nil

tfprotov5/internal/tf5serverlogging/downstream_request.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,25 @@ func DownstreamRequest(ctx context.Context) context.Context {
2424
return ctx
2525
}
2626

27+
// DownstreamServerEvent generates the following logging:
28+
//
29+
// - TRACE "Received downstream server event" log with time elapsed since
30+
// request start and diagnostic severity counts
31+
// - Per-diagnostic logs
32+
func DownstreamServerEvent(ctx context.Context, diagnostics diag.Diagnostics) {
33+
eventFields := map[string]interface{}{
34+
logging.KeyDiagnosticErrorCount: diagnostics.ErrorCount(),
35+
logging.KeyDiagnosticWarningCount: diagnostics.WarningCount(),
36+
}
37+
38+
if requestStart, ok := ctx.Value(ContextKeyDownstreamRequestStartTime{}).(time.Time); ok {
39+
eventFields[logging.KeyRequestDurationMs] = time.Since(requestStart).Milliseconds()
40+
}
41+
42+
logging.ProtocolTrace(ctx, "Received downstream server event", eventFields)
43+
diagnostics.Log(ctx)
44+
}
45+
2746
// DownstreamResponse generates the following logging:
2847
//
2948
// - TRACE "Received downstream response" log with request duration and

tfprotov5/internal/toproto/list_resource.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ func GetMetadata_ListResourceMetadata(in *tfprotov5.ListResourceMetadata) *tfplu
1818
}
1919
}
2020

21+
func ListResource_ListResourceEvent(in *tfprotov5.ListResourceResult) *tfplugin5.ListResource_Event {
22+
return &tfplugin5.ListResource_Event{
23+
DisplayName: in.DisplayName,
24+
ResourceObject: DynamicValue(in.Resource),
25+
Identity: ResourceIdentityData(in.Identity),
26+
Diagnostic: Diagnostics(in.Diagnostics),
27+
}
28+
}
29+
2130
func ValidateListResourceConfig_Response(in *tfprotov5.ValidateListResourceConfigResponse) *tfplugin5.ValidateListResourceConfig_Response {
2231
if in == nil {
2332
return nil

0 commit comments

Comments
 (0)