Skip to content

Commit f0fee4f

Browse files
authored
OmitType and TypeName to support sharing types (#2409)
Introduce OmitType and TypeName flags to enforce type sharing. Some of the upstream providers generate very large concrete schemata. TF is not being materially affected, just high RAM demands for in-memory processing. The example is inspired by QuickSight types in AWS. Pulumi is affected significantly. In Pulumi the default projection is going to generate named types for every instance of the shared schema. This leads to SDK bloat and issues with "filename too long." With this change it is possible for the provider maintainer opt into explicit sharing of types, and ensure that the type names for the shared types have shorter meaningful prefixes. At definition type the user can specify the type name to generate, which can be very short, and replace the automatically implied ReallyLongPrefixedTypeName like this: ```go "visuals": { Elem: &info.Schema{ TypeName: tfbridge.Ref("Visual"), }, }, ``` At reference time in another resource, the user can reuse an already generated type by token. This already worked before this change but had the downside of still generating unused helper types and causing SDK bloat. ```go "visuals": { Elem: &info.Schema{ Type: "testprov:index/Visual:Visual", }, }, ``` With this change it is possible to instruct the bridge to stop generating the unused helper types: ```go "visuals": { Elem: &info.Schema{ Type: "testprov:index/Visual:Visual", OmitType: true }, }, ```
1 parent fbc2abd commit f0fee4f

File tree

6 files changed

+403
-3
lines changed

6 files changed

+403
-3
lines changed

pkg/tfbridge/info.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,18 @@ func MakeResource(pkg string, mod string, res string) tokens.Type {
318318
return tokens.NewTypeToken(modT, tokens.TypeName(res))
319319
}
320320

321-
// BoolRef returns a reference to the bool argument.
321+
// BoolRef returns a reference to the bool argument. Retained for backwards compatibility. Prefer [Ref] for new usage
322+
// and other types like strings.
322323
func BoolRef(b bool) *bool {
323324
return &b
324325
}
325326

327+
// Fluently construct a reference to the argument. This utility function is needed to ease configuring the bridge where
328+
// references are expected instead of plain boolean or string literals.
329+
func Ref[T any](x T) *T {
330+
return &x
331+
}
332+
326333
// StringValue gets a string value from a property map if present, else ""
327334
func StringValue(vars resource.PropertyMap, prop resource.PropertyKey) string {
328335
val, ok := vars[prop]

pkg/tfbridge/info/info.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,19 @@ 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+
//
452+
// It is an error to set [OmitType] to true without specifying [Type].
453+
//
454+
// Experimental.
455+
OmitType bool
456+
447457
// alternative types that can be used instead of the override.
448458
AltTypes []tokens.Type
449459

@@ -502,6 +512,24 @@ type Schema struct {
502512

503513
// whether or not to treat this property as secret
504514
Secret *bool
515+
516+
// Specifies the exact name to use for the generated type.
517+
//
518+
// When generating types for properties, by default Pulumi picks reasonable names based on the property path
519+
// prefix and the name of the property. Use [TypeName] to override this decision when the default names for
520+
// nested properties are too long or otherwise undesirable. The choice will further affect the automatically
521+
// generated names for any properties nested under the current one.
522+
//
523+
// Example use:
524+
//
525+
// TypeName: tfbridge.Ref("Visual")
526+
//
527+
// Note that the type name, and not the full token like "aws:quicksight/Visual:Visual" is specified. The token
528+
// will be picked based on the current module ("quicksight" in the above example) where the parent resource or
529+
// data source is found.
530+
//
531+
// Experimental.
532+
TypeName *string
505533
}
506534

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

pkg/tfgen/generate.go

Lines changed: 27 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+
typeName *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.typeName = info.TypeName
360+
}
356361

357362
var elemInfo *tfbridge.SchemaInfo
358363
if info != nil {
@@ -456,10 +461,23 @@ 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 {
467+
if info.Type == "" {
468+
return nil, fmt.Errorf("Cannot set info.OmitType without also setting info.Type")
469+
}
470+
return &propertyType{typ: info.Type}, nil
471+
}
472+
459473
t := &propertyType{
460474
kind: kindObject,
461475
}
462476

477+
if info != nil {
478+
t.typeName = info.TypeName
479+
}
480+
463481
if info != nil {
464482
t.typ = info.Type
465483
t.nestedType = info.NestedType
@@ -549,6 +567,15 @@ func (t *propertyType) equals(other *propertyType) bool {
549567
if len(t.properties) != len(other.properties) {
550568
return false
551569
}
570+
switch {
571+
case t.typeName != nil && other.typeName == nil:
572+
return false
573+
case t.typeName == nil && other.typeName != nil:
574+
return false
575+
case t.typeName != nil && other.typeName != nil &&
576+
*t.typeName != *other.typeName:
577+
return false
578+
}
552579
for i, p := range t.properties {
553580
o := other.properties[i]
554581
if p.name != o.name {

pkg/tfgen/generate_schema.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,15 @@ func (nt *schemaNestedTypes) declareType(typePath paths.TypePath, declarer decla
120120
typ *propertyType, isInput bool) string {
121121

122122
// Generate a name for this nested type.
123-
typeName := namePrefix + cases.Title(language.Und, cases.NoLower).String(name)
123+
var typeName string
124+
125+
if typ.typeName != nil {
126+
// Use an explicit name if provided.
127+
typeName = *typ.typeName
128+
} else {
129+
// Otherwise build one based on the current property name and prefix.
130+
typeName = namePrefix + cases.Title(language.Und, cases.NoLower).String(name)
131+
}
124132

125133
// Override the nested type name, if necessary.
126134
if typ.nestedType.Name().String() != "" {

pkg/tfgen/generate_schema_test.go

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

2527
"github.com/hashicorp/hcl/v2"
28+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
2629
"github.com/hexops/autogold/v2"
2730
csgen "github.com/pulumi/pulumi/pkg/v3/codegen/dotnet"
2831
gogen "github.com/pulumi/pulumi/pkg/v3/codegen/go"
@@ -35,7 +38,9 @@ import (
3538

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,167 @@ 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 in Go,
125+
// 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 explicit sharing.
128+
func TestTypeSharing(t *testing.T) {
129+
if runtime.GOOS == "windows" {
130+
t.Skipf("Skipping on Windows due to a test setup issue")
131+
}
132+
133+
tmpdir := t.TempDir()
134+
barCharVisualSchema := func() *schema.Schema {
135+
return &schema.Schema{
136+
Type: schema.TypeList,
137+
Optional: true,
138+
MinItems: 1,
139+
MaxItems: 1,
140+
Elem: &schema.Resource{
141+
Schema: map[string]*schema.Schema{
142+
"nest": {
143+
Type: schema.TypeList,
144+
MaxItems: 1,
145+
Optional: true,
146+
Elem: &schema.Resource{
147+
Schema: map[string]*schema.Schema{
148+
"nested_prop": {
149+
Type: schema.TypeBool,
150+
Optional: true,
151+
},
152+
},
153+
},
154+
},
155+
},
156+
},
157+
}
158+
}
159+
visualsSchema := func() *schema.Schema {
160+
return &schema.Schema{
161+
Type: schema.TypeList,
162+
MinItems: 1,
163+
MaxItems: 50,
164+
Optional: true,
165+
Elem: &schema.Resource{
166+
Schema: map[string]*schema.Schema{
167+
"bar_chart_visual": barCharVisualSchema(),
168+
"box_plot_visual": barCharVisualSchema(),
169+
},
170+
},
171+
}
172+
}
173+
provider := info.Provider{
174+
Name: "testprov",
175+
P: sdkv2.NewProvider(&schema.Provider{
176+
ResourcesMap: map[string]*schema.Resource{
177+
"testprov_r1": {
178+
Schema: map[string]*schema.Schema{
179+
"sheets": {
180+
Type: schema.TypeList,
181+
MinItems: 1,
182+
MaxItems: 20,
183+
Optional: true,
184+
Elem: &schema.Resource{
185+
Schema: map[string]*schema.Schema{
186+
"visuals": visualsSchema(),
187+
},
188+
},
189+
},
190+
},
191+
},
192+
"testprov_r2": {
193+
Schema: map[string]*schema.Schema{
194+
"x": {
195+
Type: schema.TypeInt,
196+
Optional: true,
197+
},
198+
"sheets": {
199+
Type: schema.TypeList,
200+
MinItems: 1,
201+
MaxItems: 20,
202+
Optional: true,
203+
Elem: &schema.Resource{
204+
Schema: map[string]*schema.Schema{
205+
"y": {
206+
Type: schema.TypeBool,
207+
Optional: true,
208+
},
209+
"visuals": visualsSchema(),
210+
},
211+
},
212+
},
213+
},
214+
},
215+
},
216+
}),
217+
UpstreamRepoPath: tmpdir,
218+
Resources: map[string]*info.Resource{
219+
"testprov_r1": {
220+
Tok: "testprov:index:R1",
221+
Fields: map[string]*info.Schema{
222+
"sheets": {
223+
Elem: &info.Schema{
224+
Fields: map[string]*info.Schema{
225+
"visuals": {
226+
Elem: &info.Schema{
227+
TypeName: tfbridge.Ref("Visual"),
228+
},
229+
},
230+
},
231+
},
232+
},
233+
},
234+
},
235+
"testprov_r2": {
236+
Tok: "testprov:index:R2",
237+
Fields: map[string]*info.Schema{
238+
"sheets": {
239+
Elem: &info.Schema{
240+
Fields: map[string]*info.Schema{
241+
"visuals": {
242+
Elem: &info.Schema{
243+
Type: "testprov:index/Visual:Visual",
244+
OmitType: true,
245+
},
246+
},
247+
},
248+
},
249+
},
250+
},
251+
},
252+
},
253+
}
254+
255+
var buf bytes.Buffer
256+
schema, err := GenerateSchema(provider, diag.DefaultSink(&buf, &buf, diag.FormatOptions{
257+
Color: colors.Never,
258+
}))
259+
require.NoError(t, err)
260+
261+
t.Logf("%s", buf.String())
262+
263+
keys := []string{}
264+
for k := range schema.Types {
265+
keys = append(keys, k)
266+
}
267+
sort.Strings(keys)
268+
269+
// Note that there is only one set of helper types, and they are not prefixed by any of the resource names.
270+
autogold.Expect([]string{
271+
"testprov:index/R1Sheet:R1Sheet", "testprov:index/R2Sheet:R2Sheet",
272+
"testprov:index/Visual:Visual",
273+
"testprov:index/VisualBarChartVisual:VisualBarChartVisual",
274+
"testprov:index/VisualBarChartVisualNest:VisualBarChartVisualNest",
275+
"testprov:index/VisualBoxPlotVisual:VisualBoxPlotVisual",
276+
"testprov:index/VisualBoxPlotVisualNest:VisualBoxPlotVisualNest",
277+
}).Equal(t, keys)
278+
279+
bytes, err := json.MarshalIndent(schema, "", " ")
280+
require.NoError(t, err)
281+
282+
autogold.ExpectFile(t, autogold.Raw(string(bytes)))
283+
}
284+
119285
// TestPropertyDocumentationEdits tests that documentation edits are applied to
120286
// individual properties. This includes both the property description and
121287
// deprecation message. This tests the following workflow

0 commit comments

Comments
 (0)