Skip to content

Commit 3dd9c26

Browse files
committed
Implement type sharing support
1 parent fbc2abd commit 3dd9c26

File tree

6 files changed

+367
-1
lines changed

6 files changed

+367
-1
lines changed

pkg/tfbridge/info.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ func BoolRef(b bool) *bool {
323323
return &b
324324
}
325325

326+
// String returns a reference to the string argument.
327+
func StringRef(s string) *string {
328+
return &s
329+
}
330+
326331
// StringValue gets a string value from a property map if present, else ""
327332
func StringValue(vars resource.PropertyMap, prop resource.PropertyKey) string {
328333
val, ok := vars[prop]

pkg/tfbridge/info/info.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,15 @@ type Schema struct {
441441
// a name to override the default when targeting C#; "" uses the default.
442442
CSharpName string
443443

444-
// a type to override the default; "" uses the default.
444+
// An optional Pulumi type token to use for the Pulumi type projection of the current property. When unset, the
445+
// default behavior is to generate fresh named Pulumi types as needed to represent the schema. To force the use
446+
// of a known type and avoid generating unnecessary types, use both [Type] and [OmitType].
445447
Type tokens.Type
446448

449+
// Used together with [Type] to omit generating any Pulumi types whatsoever for the current property, and
450+
// instead use the object type identified by the token setup in [Type].
451+
OmitType bool
452+
447453
// alternative types that can be used instead of the override.
448454
AltTypes []tokens.Type
449455

@@ -502,6 +508,12 @@ type Schema struct {
502508

503509
// whether or not to treat this property as secret
504510
Secret *bool
511+
512+
// If specified, resets the type name prefix for any types that Pulumi needs to generate to represent the schema
513+
// of the current property. Normally the names for helper object types are built from concatenating fragments
514+
// representing the path to the type in the schema. The default strategy can lead to unacceptably long type
515+
// names, reducing the SDK usability. Using TypePrefixOverride allows the maintainer to get shorter type names.
516+
TypePrefixOverride *string
505517
}
506518

507519
// Config represents a synthetic configuration variable that is Pulumi-only, and not passed to Terraform.

pkg/tfgen/generate.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,8 @@ type propertyType struct {
342342
nestedType tokens.Type
343343
altTypes []tokens.Type
344344
asset *tfbridge.AssetTranslation
345+
346+
typePrefixOverride *string
345347
}
346348

347349
func (g *Generator) Sink() diag.Sink {
@@ -353,6 +355,9 @@ func (g *Generator) makePropertyType(typePath paths.TypePath,
353355
entityDocs entityDocs) (*propertyType, error) {
354356

355357
t := &propertyType{}
358+
if info != nil {
359+
t.typePrefixOverride = info.TypePrefixOverride
360+
}
356361

357362
var elemInfo *tfbridge.SchemaInfo
358363
if info != nil {
@@ -456,10 +461,20 @@ func getDocsFromSchemaMap(key string, schemaMap shim.SchemaMap) string {
456461
func (g *Generator) makeObjectPropertyType(typePath paths.TypePath,
457462
res shim.Resource, info *tfbridge.SchemaInfo,
458463
out bool, entityDocs entityDocs) (*propertyType, error) {
464+
465+
// If the user supplied an explicit Type token override, omit generating types and short-circuit.
466+
if info != nil && info.OmitType && info.Type != "" {
467+
return &propertyType{typ: info.Type}, nil
468+
}
469+
459470
t := &propertyType{
460471
kind: kindObject,
461472
}
462473

474+
if info != nil {
475+
t.typePrefixOverride = info.TypePrefixOverride
476+
}
477+
463478
if info != nil {
464479
t.typ = info.Type
465480
t.nestedType = info.NestedType
@@ -549,6 +564,14 @@ func (t *propertyType) equals(other *propertyType) bool {
549564
if len(t.properties) != len(other.properties) {
550565
return false
551566
}
567+
switch {
568+
case t.typePrefixOverride != nil && other.typePrefixOverride == nil:
569+
return false
570+
case t.typePrefixOverride == nil && other.typePrefixOverride != nil:
571+
return false
572+
case t.typePrefixOverride != nil && other.typePrefixOverride != nil && *t.typePrefixOverride != *other.typePrefixOverride:
573+
return false
574+
}
552575
for i, p := range t.properties {
553576
o := other.properties[i]
554577
if p.name != o.name {

pkg/tfgen/generate_schema.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ type declarer interface {
119119
func (nt *schemaNestedTypes) declareType(typePath paths.TypePath, declarer declarer, namePrefix, name string,
120120
typ *propertyType, isInput bool) string {
121121

122+
if typ.typePrefixOverride != nil {
123+
namePrefix = *typ.typePrefixOverride
124+
}
125+
122126
// Generate a name for this nested type.
123127
typeName := namePrefix + cases.Title(language.Und, cases.NoLower).String(name)
124128

pkg/tfgen/generate_schema_test.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"encoding/json"
2020
"fmt"
2121
"io"
22+
"os"
23+
"sort"
2224
"testing"
2325
"text/template"
2426

@@ -33,9 +35,12 @@ import (
3335
"github.com/stretchr/testify/assert"
3436
"github.com/stretchr/testify/require"
3537

38+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
3639
bridgetesting "github.com/pulumi/pulumi-terraform-bridge/v3/internal/testing"
3740
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge"
41+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info"
3842
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen/internal/testprovider"
43+
sdkv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2"
3944
"github.com/pulumi/pulumi-terraform-bridge/v3/unstable/metadata"
4045
"github.com/pulumi/pulumi-terraform-bridge/x/muxer"
4146
)
@@ -116,6 +121,159 @@ func TestCSharpMiniRandom(t *testing.T) {
116121
bridgetesting.AssertEqualsJSONFile(t, "test_data/minirandom-schema-csharp.json", schema)
117122
}
118123

124+
// Test the ability to force type sharing. Some of the upstream providers generate very large concrete schemata by in
125+
// Go, with TF not being materially affected. The example is inspired by QuickSight types in AWS. In Pulumi the default
126+
// projection is going to generate named types for every instance of the shared schema. This may lead to SDK bloat. Test
127+
// the ability of the provider author to curb the bloat and force an explit sharing.
128+
func TestTypeSharing(t *testing.T) {
129+
tmpdir := t.TempDir()
130+
barCharVisualSchema := func() *schema.Schema {
131+
return &schema.Schema{
132+
Type: schema.TypeList,
133+
Optional: true,
134+
MinItems: 1,
135+
MaxItems: 1,
136+
Elem: &schema.Resource{
137+
Schema: map[string]*schema.Schema{
138+
"nest": {
139+
Type: schema.TypeList,
140+
MaxItems: 1,
141+
Optional: true,
142+
Elem: &schema.Resource{
143+
Schema: map[string]*schema.Schema{
144+
"nested_prop": {
145+
Type: schema.TypeBool,
146+
Optional: true,
147+
},
148+
},
149+
},
150+
},
151+
},
152+
},
153+
}
154+
}
155+
visualsSchema := func() *schema.Schema {
156+
return &schema.Schema{
157+
Type: schema.TypeList,
158+
MinItems: 1,
159+
MaxItems: 50,
160+
Optional: true,
161+
Elem: &schema.Resource{
162+
Schema: map[string]*schema.Schema{
163+
"bar_chart_visual": barCharVisualSchema(),
164+
"box_plot_visual": barCharVisualSchema(),
165+
},
166+
},
167+
}
168+
}
169+
provider := info.Provider{
170+
Name: "testprov",
171+
P: sdkv2.NewProvider(&schema.Provider{
172+
ResourcesMap: map[string]*schema.Resource{
173+
"testprov_r1": {
174+
Schema: map[string]*schema.Schema{
175+
"sheets": {
176+
Type: schema.TypeList,
177+
MinItems: 1,
178+
MaxItems: 20,
179+
Optional: true,
180+
Elem: &schema.Resource{
181+
Schema: map[string]*schema.Schema{
182+
"visuals": visualsSchema(),
183+
},
184+
},
185+
},
186+
},
187+
},
188+
"testprov_r2": {
189+
Schema: map[string]*schema.Schema{
190+
"x": {
191+
Type: schema.TypeInt,
192+
Optional: true,
193+
},
194+
"sheets": {
195+
Type: schema.TypeList,
196+
MinItems: 1,
197+
MaxItems: 20,
198+
Optional: true,
199+
Elem: &schema.Resource{
200+
Schema: map[string]*schema.Schema{
201+
"y": {
202+
Type: schema.TypeBool,
203+
Optional: true,
204+
},
205+
"visuals": visualsSchema(),
206+
},
207+
},
208+
},
209+
},
210+
},
211+
},
212+
}),
213+
UpstreamRepoPath: tmpdir,
214+
Resources: map[string]*info.Resource{
215+
"testprov_r1": {
216+
Tok: "testprov:index:R1",
217+
Fields: map[string]*info.Schema{
218+
"sheets": {
219+
Elem: &info.Schema{
220+
Fields: map[string]*info.Schema{
221+
"visuals": {
222+
Elem: &info.Schema{
223+
TypePrefixOverride: tfbridge.StringRef(""),
224+
},
225+
},
226+
},
227+
},
228+
},
229+
},
230+
},
231+
"testprov_r2": {
232+
Tok: "testprov:index:R2",
233+
Fields: map[string]*info.Schema{
234+
"sheets": {
235+
Elem: &info.Schema{
236+
Fields: map[string]*info.Schema{
237+
"visuals": {
238+
Elem: &info.Schema{
239+
Type: "testprov:index/Visual:Visual",
240+
OmitType: true,
241+
},
242+
},
243+
},
244+
},
245+
},
246+
},
247+
},
248+
},
249+
}
250+
schema, err := GenerateSchema(provider, diag.DefaultSink(os.Stdout, os.Stdout, diag.FormatOptions{
251+
Color: colors.Never,
252+
}))
253+
require.NoError(t, err)
254+
255+
keys := []string{}
256+
for k := range schema.Types {
257+
keys = append(keys, string(k))
258+
}
259+
sort.Strings(keys)
260+
261+
// Note that there is only one set of helper types, and they are not prefixed by any of the resource names.
262+
autogold.Expect([]string{
263+
"testprov:index/R1Sheet:R1Sheet", "testprov:index/R2Sheet:R2Sheet",
264+
"testprov:index/Visual:Visual",
265+
"testprov:index/VisualBarChartVisual:VisualBarChartVisual",
266+
"testprov:index/VisualBarChartVisualNest:VisualBarChartVisualNest",
267+
"testprov:index/VisualBoxPlotVisual:VisualBoxPlotVisual",
268+
"testprov:index/VisualBoxPlotVisualNest:VisualBoxPlotVisualNest",
269+
}).Equal(t, keys)
270+
271+
bytes, err := json.MarshalIndent(schema, "", " ")
272+
require.NoError(t, err)
273+
274+
autogold.ExpectFile(t, autogold.Raw(string(bytes)))
275+
}
276+
119277
// TestPropertyDocumentationEdits tests that documentation edits are applied to
120278
// individual properties. This includes both the property description and
121279
// deprecation message. This tests the following workflow

0 commit comments

Comments
 (0)