pulumi/pkg/codegen/pcl/binder_resource.go

676 lines
22 KiB
Go
Raw Normal View History

// Copyright 2016-2020, 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 pcl
import (
[program-gen/pcl] Avoid pretty printing large object graphs when encountering bind error in resource properties (#14864) # Description When we bind PCL program and a resource property doesn't type-check, we pretty print the type of the resource property in its _entirety_ in the error message and that includes nested objects, nested maps, nested lists, unions etc. instead of using type references: we pretty print the `model.Type` derived from `schema.Type` and this resolves the entire graph. This PR changes it such that we print the `schema.Type` in string form in the error message when we encounter a bind error in resource properties instead of fully resolving and printing the full `model.Type`. Fixes #14317 ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [ ] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-20 01:20:42 +00:00
"fmt"
[go/program-gen] Fix union type type resolution in Go program generation (#16297) # Description Fixes https://github.com/pulumi/pulumi-azure-native/issues/1554 ### Context The problem here is that when we compute `InputType: model.Type` in `pcl.Resource`, we map the types of input properties of resources from `schema.Type` into `model.Type`. When one of these properties is a `schema.UnionType` (union of objects to be exact), we map that _as is_ to `model.UnionType` which trips up Go program-gen as it doesn't know how to reduce the type to the actual _selected_ object type based on the resource inputs. ### Resolution The way to fix this is not in Go program-gen, instead we _reduce_ the computed union types during the binding phase into the actual object types based on the resource inputs so that all program generators only work against explicit objects rather than having to deal with unions of objects ### Example: ```pcl resource "example" "azure-native:eventgrid:EventSubscription" { destination = { endpointType = "EventHub" resourceId = "example" } expirationTimeUtc = "example" scope = "example" } ``` Before: ```go pulumi.Run(func(ctx *pulumi.Context) error { _, err := eventgrid.NewEventSubscription(ctx, "example", &eventgrid.EventSubscriptionArgs{ Destination: eventgrid.EventHubEventSubscriptionDestination{ EndpointType: "EventHub", ResourceId: "example", }, ExpirationTimeUtc: pulumi.String("example"), Scope: pulumi.String("example"), }) if err != nil { return err } return nil }) ``` After: ```go pulumi.Run(func(ctx *pulumi.Context) error { _, err := eventgrid.NewEventSubscription(ctx, "example", &eventgrid.EventSubscriptionArgs{ Destination: &eventgrid.EventHubEventSubscriptionDestinationArgs{ EndpointType: pulumi.String("EventHub"), ResourceId: pulumi.String("example"), }, ExpirationTimeUtc: pulumi.String("example"), Scope: pulumi.String("example"), }) if err != nil { return err } return nil }) ``` ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2024-06-06 19:13:19 +00:00
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/pulumi/pulumi/pkg/v3/codegen"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/zclconf/go-cty/cty"
)
func getResourceToken(node *Resource) (string, hcl.Range) {
return node.syntax.Labels[1], node.syntax.LabelRanges[1]
}
func (b *binder) bindResource(node *Resource) hcl.Diagnostics {
var diagnostics hcl.Diagnostics
typeDiags := b.bindResourceTypes(node)
diagnostics = append(diagnostics, typeDiags...)
bodyDiags := b.bindResourceBody(node)
diagnostics = append(diagnostics, bodyDiags...)
return diagnostics
}
func annotateAttributeValue(expr model.Expression, attributeType schema.Type) model.Expression {
if optionalType, ok := attributeType.(*schema.OptionalType); ok {
return annotateAttributeValue(expr, optionalType.ElementType)
}
switch attrValue := expr.(type) {
case *model.ObjectConsExpression:
if schemaObjectType, ok := attributeType.(*schema.ObjectType); ok {
schemaProperties := make(map[string]schema.Type)
for _, schemaProperty := range schemaObjectType.Properties {
schemaProperties[schemaProperty.Name] = schemaProperty.Type
}
for _, item := range attrValue.Items {
[program-gen/pcl] Fixes type-annotating nested resource properties when these have quoted keys (#15001) # Description When binding resource properties and annotating these with types from their schemas, we seem to skip this annotation process when resource properties and their nested objects use _quoted_ keys `{ "key" = <value> }` rather than using _literal_ keys `{ key = <value> }`. This results in issues such as https://github.com/pulumi/kube2pulumi/issues/60 where a csharp property name override was not correctly applied because 1) kube2pulumi generated properties for resources that are quoted (this should be fine) 2) binding the resource properties skipped annotating the nested object with its corresponding schema type 3) program-gen in dotnet didn't have access to the schema type of the nested object to correctly apply the property override This PR fixes this issue by extending PCL resource binding to also check for properties which have quoted keys. Fixes https://github.com/pulumi/kube2pulumi/issues/60 and potentially other issues that arise from converters generating PCL with quoted keys. ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [ ] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-27 12:42:52 +00:00
// annotate the nested object properties
// here when the key is a literal such as { key = <inner value> }
keyLiteral, isLit := item.Key.(*model.LiteralValueExpression)
if isLit {
correspondingSchemaType, ok := schemaProperties[keyLiteral.Value.AsString()]
if ok {
item.Value = annotateAttributeValue(item.Value, correspondingSchemaType)
}
}
[program-gen/pcl] Fixes type-annotating nested resource properties when these have quoted keys (#15001) # Description When binding resource properties and annotating these with types from their schemas, we seem to skip this annotation process when resource properties and their nested objects use _quoted_ keys `{ "key" = <value> }` rather than using _literal_ keys `{ key = <value> }`. This results in issues such as https://github.com/pulumi/kube2pulumi/issues/60 where a csharp property name override was not correctly applied because 1) kube2pulumi generated properties for resources that are quoted (this should be fine) 2) binding the resource properties skipped annotating the nested object with its corresponding schema type 3) program-gen in dotnet didn't have access to the schema type of the nested object to correctly apply the property override This PR fixes this issue by extending PCL resource binding to also check for properties which have quoted keys. Fixes https://github.com/pulumi/kube2pulumi/issues/60 and potentially other issues that arise from converters generating PCL with quoted keys. ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [ ] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-27 12:42:52 +00:00
// here when the key is a quoted literal such as { "key" = <inner value> }
if templateExpression, ok := item.Key.(*model.TemplateExpression); ok && len(templateExpression.Parts) == 1 {
if literalValue, ok := templateExpression.Parts[0].(*model.LiteralValueExpression); ok {
if correspondingSchemaType, ok := schemaProperties[literalValue.Value.AsString()]; ok {
item.Value = annotateAttributeValue(item.Value, correspondingSchemaType)
}
}
}
}
return attrValue.WithType(func(attrValueType model.Type) *model.ObjectConsExpression {
annotateObjectProperties(attrValueType, attributeType)
return attrValue
})
}
return attrValue
case *model.TupleConsExpression:
if schemaArrayType, ok := attributeType.(*schema.ArrayType); ok {
elementType := schemaArrayType.ElementType
for _, arrayExpr := range attrValue.Expressions {
annotateAttributeValue(arrayExpr, elementType)
}
}
return attrValue
case *model.FunctionCallExpression:
if attrValue.Name == IntrinsicConvert {
converterArg := attrValue.Args[0]
annotateAttributeValue(converterArg, attributeType)
}
return attrValue
default:
return expr
}
}
func AnnotateAttributeValue(expr model.Expression, attributeType schema.Type) model.Expression {
return annotateAttributeValue(expr, attributeType)
}
func AnnotateResourceInputs(node *Resource) {
if node.Schema == nil {
// skip annotations for resource which don't have a schema
return
}
resourceProperties := make(map[string]*schema.Property)
for _, property := range node.Schema.Properties {
resourceProperties[property.Name] = property
}
// add type annotations to the attributes
// and their nested objects
for index := range node.Inputs {
attr := node.Inputs[index]
if property, ok := resourceProperties[attr.Name]; ok {
node.Inputs[index] = &model.Attribute{
Tokens: attr.Tokens,
Name: attr.Name,
Syntax: attr.Syntax,
Value: AnnotateAttributeValue(attr.Value, property.Type),
}
}
}
}
[go/program-gen] Fix union type type resolution in Go program generation (#16297) # Description Fixes https://github.com/pulumi/pulumi-azure-native/issues/1554 ### Context The problem here is that when we compute `InputType: model.Type` in `pcl.Resource`, we map the types of input properties of resources from `schema.Type` into `model.Type`. When one of these properties is a `schema.UnionType` (union of objects to be exact), we map that _as is_ to `model.UnionType` which trips up Go program-gen as it doesn't know how to reduce the type to the actual _selected_ object type based on the resource inputs. ### Resolution The way to fix this is not in Go program-gen, instead we _reduce_ the computed union types during the binding phase into the actual object types based on the resource inputs so that all program generators only work against explicit objects rather than having to deal with unions of objects ### Example: ```pcl resource "example" "azure-native:eventgrid:EventSubscription" { destination = { endpointType = "EventHub" resourceId = "example" } expirationTimeUtc = "example" scope = "example" } ``` Before: ```go pulumi.Run(func(ctx *pulumi.Context) error { _, err := eventgrid.NewEventSubscription(ctx, "example", &eventgrid.EventSubscriptionArgs{ Destination: eventgrid.EventHubEventSubscriptionDestination{ EndpointType: "EventHub", ResourceId: "example", }, ExpirationTimeUtc: pulumi.String("example"), Scope: pulumi.String("example"), }) if err != nil { return err } return nil }) ``` After: ```go pulumi.Run(func(ctx *pulumi.Context) error { _, err := eventgrid.NewEventSubscription(ctx, "example", &eventgrid.EventSubscriptionArgs{ Destination: &eventgrid.EventHubEventSubscriptionDestinationArgs{ EndpointType: pulumi.String("EventHub"), ResourceId: pulumi.String("example"), }, ExpirationTimeUtc: pulumi.String("example"), Scope: pulumi.String("example"), }) if err != nil { return err } return nil }) ``` ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2024-06-06 19:13:19 +00:00
// resolveUnionOfObjects takes an object expression and its corresponding schema union type,
// then tries to find out which type from the union is the one that matches the object expression.
// We do this based on the discriminator field in the object expression.
func resolveUnionOfObjects(objectExpr *model.ObjectConsExpression, union *schema.UnionType) schema.Type {
var discriminatorValue string
for _, item := range objectExpr.Items {
if key, ok := item.Key.(*model.LiteralValueExpression); ok {
if key.Value.AsString() == union.Discriminator {
if value, ok := item.Value.(*model.LiteralValueExpression); ok {
discriminatorValue = value.Value.AsString()
break
}
if value, ok := item.Value.(*model.TemplateExpression); ok && len(value.Parts) == 1 {
if literalValue, ok := value.Parts[0].(*model.LiteralValueExpression); ok {
discriminatorValue = literalValue.Value.AsString()
break
}
}
}
}
}
if discriminatorValue == "" {
return union
}
if correspondingTypeToken, ok := union.Mapping[discriminatorValue]; ok {
for _, schemaType := range union.ElementTypes {
if schemaObjectType, ok := codegen.UnwrapType(schemaType).(*schema.ObjectType); ok {
parsedTypeToken, err := tokens.ParseTypeToken(correspondingTypeToken)
if err != nil {
continue
}
parsedObjectToken, err := tokens.ParseTypeToken(schemaObjectType.Token)
if err != nil {
continue
}
if string(parsedTypeToken.Name()) == string(parsedObjectToken.Name()) {
// found the corresponding object type
return schemaObjectType
}
}
}
}
return union
}
// resolveInputUnions will take input expressions and their corresponding schema type,
// if the schema type is a union of objects and the input is an object, we use the
// the discriminator field to determine which object type to use and reduce the union into
// just an object type. This way program generators can easily work with the object type
// directly instead of working out which object type they should be using based on the union
func (b *binder) resolveInputUnions(
inputs map[string]model.Expression,
inputProperties []*schema.Property,
) []*schema.Property {
resolvedProperties := make([]*schema.Property, len(inputProperties))
for i, property := range inputProperties {
resolvedType := property.Type
if value, ok := inputs[property.Name]; ok {
switch schemaType := codegen.UnwrapType(property.Type).(type) {
case *schema.UnionType:
if objectExpr, ok := value.(*model.ObjectConsExpression); ok {
resolvedType = resolveUnionOfObjects(objectExpr, schemaType)
switch resolvedType := resolvedType.(type) {
case *schema.ObjectType:
// found the corresponding object type for this PCL expression, resolve nested unions if any
nestedInputs := map[string]model.Expression{}
for _, item := range objectExpr.Items {
if key, ok := literalExprValue(item.Key); ok && key.Type().Equals(cty.String) {
nestedInputs[key.AsString()] = item.Value
}
}
b.resolveInputUnions(nestedInputs, resolvedType.Properties)
}
}
}
}
resolvedProperties[i] = &schema.Property{
Type: resolvedType,
Name: property.Name,
DeprecationMessage: property.DeprecationMessage,
ConstValue: property.ConstValue,
Secret: property.Secret,
Plain: property.Plain,
Language: property.Language,
Comment: property.Comment,
DefaultValue: property.DefaultValue,
ReplaceOnChanges: property.ReplaceOnChanges,
WillReplaceOnChanges: property.WillReplaceOnChanges,
}
}
return resolvedProperties
}
// rawResourceInputs returns the raw inputs for a resource. This is useful when we need to resolve unions of objects
// and reduce them to just an object when possible. The inputs of a resource contain the discriminator field of which
// the value is used to determine which object type to use and thus reduce unions into objects.
func (b *binder) rawResourceInputs(node *Resource) map[string]model.Expression {
inputs := map[string]model.Expression{}
scopes := newResourceScopes(b.root, node, nil, nil)
block, _ := model.BindBlock(node.syntax, scopes, b.tokens, b.options.modelOptions()...)
for _, item := range block.Body.Items {
switch item := item.(type) {
case *model.Attribute:
inputs[item.Name] = item.Value
}
}
return inputs
}
// reduceInputUnionTypes reduces the input types of a resource which are unions of objects
// into just an object when possible. We use the actual inputs of the resource to determine which type object we should
// use because objects in a union have a discriminator field which is used to determine which object type to use.
func (b *binder) reduceInputUnionTypes(node *Resource, inputProperties []*schema.Property) []*schema.Property {
inputs := b.rawResourceInputs(node)
return b.resolveInputUnions(inputs, inputProperties)
}
// bindResourceTypes binds the input and output types for a resource.
func (b *binder) bindResourceTypes(node *Resource) hcl.Diagnostics {
// Set the input and output types to dynamic by default.
node.InputType, node.OutputType = model.DynamicType, model.DynamicType
// Find the resource's schema.
token, tokenRange := getResourceToken(node)
pkg, module, name, diagnostics := DecomposeToken(token, tokenRange)
if diagnostics.HasErrors() {
return diagnostics
}
makeResourceDynamic := func() {
// make the inputs and outputs of the resource dynamic
node.Token = token
node.OutputType = model.DynamicType
inferredInputProperties := map[string]model.Type{}
for _, attr := range node.Inputs {
inferredInputProperties[attr.Name] = attr.Type()
}
node.InputType = model.NewObjectType(inferredInputProperties)
}
isProvider := false
2022-10-20 20:37:10 +00:00
if pkg == "pulumi" && module == "providers" {
pkg, isProvider = name, true
}
2022-08-19 17:27:05 +00:00
var pkgSchema *packageSchema
2022-10-10 23:01:53 +00:00
// It is important that we call `loadPackageSchema` instead of `getPackageSchema` here
[go/program-gen] Fix union type type resolution in Go program generation (#16297) # Description Fixes https://github.com/pulumi/pulumi-azure-native/issues/1554 ### Context The problem here is that when we compute `InputType: model.Type` in `pcl.Resource`, we map the types of input properties of resources from `schema.Type` into `model.Type`. When one of these properties is a `schema.UnionType` (union of objects to be exact), we map that _as is_ to `model.UnionType` which trips up Go program-gen as it doesn't know how to reduce the type to the actual _selected_ object type based on the resource inputs. ### Resolution The way to fix this is not in Go program-gen, instead we _reduce_ the computed union types during the binding phase into the actual object types based on the resource inputs so that all program generators only work against explicit objects rather than having to deal with unions of objects ### Example: ```pcl resource "example" "azure-native:eventgrid:EventSubscription" { destination = { endpointType = "EventHub" resourceId = "example" } expirationTimeUtc = "example" scope = "example" } ``` Before: ```go pulumi.Run(func(ctx *pulumi.Context) error { _, err := eventgrid.NewEventSubscription(ctx, "example", &eventgrid.EventSubscriptionArgs{ Destination: eventgrid.EventHubEventSubscriptionDestination{ EndpointType: "EventHub", ResourceId: "example", }, ExpirationTimeUtc: pulumi.String("example"), Scope: pulumi.String("example"), }) if err != nil { return err } return nil }) ``` After: ```go pulumi.Run(func(ctx *pulumi.Context) error { _, err := eventgrid.NewEventSubscription(ctx, "example", &eventgrid.EventSubscriptionArgs{ Destination: &eventgrid.EventHubEventSubscriptionDestinationArgs{ EndpointType: pulumi.String("EventHub"), ResourceId: pulumi.String("example"), }, ExpirationTimeUtc: pulumi.String("example"), Scope: pulumi.String("example"), }) if err != nil { return err } return nil }) ``` ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2024-06-06 19:13:19 +00:00
// because the version may be wrong. When the version should not be empty,
2022-10-10 23:01:53 +00:00
// `loadPackageSchema` will load the default version while `getPackageSchema` will
// simply fail. We can't give a populated version field since we have not processed
// the body, and thus the version yet.
pkgSchema, err := b.options.packageCache.loadPackageSchema(b.options.loader, pkg, "")
if err != nil {
e := unknownPackage(pkg, tokenRange)
e.Detail = err.Error()
if b.options.skipResourceTypecheck {
makeResourceDynamic()
return hcl.Diagnostics{asWarningDiagnostic(e)}
}
return hcl.Diagnostics{e}
}
var res *schema.Resource
var inputProperties, properties []*schema.Property
if isProvider {
r, err := pkgSchema.schema.Provider()
if err != nil {
if b.options.skipResourceTypecheck {
makeResourceDynamic()
return diagnostics
}
return hcl.Diagnostics{resourceLoadError(token, err, tokenRange)}
}
res = r
} else {
r, tk, ok, err := pkgSchema.LookupResource(token)
if err != nil {
if b.options.skipResourceTypecheck {
makeResourceDynamic()
return diagnostics
}
return hcl.Diagnostics{resourceLoadError(token, err, tokenRange)}
} else if !ok {
if b.options.skipResourceTypecheck {
makeResourceDynamic()
return diagnostics
}
return hcl.Diagnostics{unknownResourceType(token, tokenRange)}
}
res = r
token = tk
}
node.Schema = res
inputProperties, properties = res.InputProperties, res.Properties
node.Token = token
// Create input and output types for the schema.
[go/program-gen] Fix union type type resolution in Go program generation (#16297) # Description Fixes https://github.com/pulumi/pulumi-azure-native/issues/1554 ### Context The problem here is that when we compute `InputType: model.Type` in `pcl.Resource`, we map the types of input properties of resources from `schema.Type` into `model.Type`. When one of these properties is a `schema.UnionType` (union of objects to be exact), we map that _as is_ to `model.UnionType` which trips up Go program-gen as it doesn't know how to reduce the type to the actual _selected_ object type based on the resource inputs. ### Resolution The way to fix this is not in Go program-gen, instead we _reduce_ the computed union types during the binding phase into the actual object types based on the resource inputs so that all program generators only work against explicit objects rather than having to deal with unions of objects ### Example: ```pcl resource "example" "azure-native:eventgrid:EventSubscription" { destination = { endpointType = "EventHub" resourceId = "example" } expirationTimeUtc = "example" scope = "example" } ``` Before: ```go pulumi.Run(func(ctx *pulumi.Context) error { _, err := eventgrid.NewEventSubscription(ctx, "example", &eventgrid.EventSubscriptionArgs{ Destination: eventgrid.EventHubEventSubscriptionDestination{ EndpointType: "EventHub", ResourceId: "example", }, ExpirationTimeUtc: pulumi.String("example"), Scope: pulumi.String("example"), }) if err != nil { return err } return nil }) ``` After: ```go pulumi.Run(func(ctx *pulumi.Context) error { _, err := eventgrid.NewEventSubscription(ctx, "example", &eventgrid.EventSubscriptionArgs{ Destination: &eventgrid.EventHubEventSubscriptionDestinationArgs{ EndpointType: pulumi.String("EventHub"), ResourceId: pulumi.String("example"), }, ExpirationTimeUtc: pulumi.String("example"), Scope: pulumi.String("example"), }) if err != nil { return err } return nil }) ``` ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2024-06-06 19:13:19 +00:00
// first reduce property types which are unions of objects into just an object when possible
inputObjectType := &schema.ObjectType{Properties: b.reduceInputUnionTypes(node, inputProperties)}
inputType := b.schemaTypeToType(inputObjectType)
outputProperties := map[string]model.Type{
"id": model.NewOutputType(model.StringType),
"urn": model.NewOutputType(model.StringType),
}
for _, prop := range properties {
outputProperties[prop.Name] = model.NewOutputType(b.schemaTypeToType(prop.Type))
}
outputType := model.NewObjectType(outputProperties, &schema.ObjectType{Properties: properties})
node.InputType, node.OutputType = inputType, outputType
findTransitivePackageReferences := func(schemaType schema.Type) {
if objectType, ok := schemaType.(*schema.ObjectType); ok && objectType.PackageReference != nil {
ref := objectType.PackageReference
if _, found := b.referencedPackages[ref.Name()]; !found {
b.referencedPackages[ref.Name()] = ref
}
}
}
codegen.VisitTypeClosure(inputProperties, findTransitivePackageReferences)
codegen.VisitTypeClosure(properties, findTransitivePackageReferences)
return diagnostics
}
type resourceScopes struct {
root *model.Scope
withRange *model.Scope
resource *Resource
}
func newResourceScopes(root *model.Scope, resource *Resource, rangeKey, rangeValue model.Type) model.Scopes {
scopes := &resourceScopes{
root: root,
withRange: root,
resource: resource,
}
if rangeValue != nil {
properties := map[string]model.Type{
"value": rangeValue,
}
if rangeKey != nil {
properties["key"] = rangeKey
}
scopes.withRange = root.Push(syntax.None)
scopes.withRange.Define("range", &model.Variable{
Name: "range",
VariableType: model.NewObjectType(properties),
})
}
return scopes
}
func (s *resourceScopes) GetScopesForBlock(block *hclsyntax.Block) (model.Scopes, hcl.Diagnostics) {
if block.Type == "options" {
return &optionsScopes{root: s.root, resource: s.resource}, nil
}
return model.StaticScope(s.withRange), nil
}
func (s *resourceScopes) GetScopeForAttribute(attr *hclsyntax.Attribute) (*model.Scope, hcl.Diagnostics) {
return s.withRange, nil
}
type optionsScopes struct {
root *model.Scope
resource *Resource
}
func (s *optionsScopes) GetScopesForBlock(block *hclsyntax.Block) (model.Scopes, hcl.Diagnostics) {
return model.StaticScope(s.root), nil
}
func (s *optionsScopes) GetScopeForAttribute(attr *hclsyntax.Attribute) (*model.Scope, hcl.Diagnostics) {
if attr.Name == "ignoreChanges" {
obj, ok := model.ResolveOutputs(s.resource.InputType).(*model.ObjectType)
if !ok {
return nil, nil
}
scope := model.NewRootScope(syntax.None)
for k, t := range obj.Properties {
scope.Define(k, &ResourceProperty{
Path: hcl.Traversal{hcl.TraverseRoot{Name: k}},
PropertyType: t,
})
}
return scope, nil
}
return s.root, nil
}
func bindResourceOptions(options *model.Block) (*ResourceOptions, hcl.Diagnostics) {
resourceOptions := &ResourceOptions{}
var diagnostics hcl.Diagnostics
for _, item := range options.Body.Items {
switch item := item.(type) {
case *model.Attribute:
var t model.Type
switch item.Name {
case "range":
t = model.NewUnionType(model.BoolType, model.NumberType, model.NewListType(model.DynamicType),
model.NewMapType(model.DynamicType))
resourceOptions.Range = item.Value
case "parent":
t = model.DynamicType
resourceOptions.Parent = item.Value
case "provider":
t = model.DynamicType
resourceOptions.Provider = item.Value
case "dependsOn":
t = model.NewListType(model.DynamicType)
resourceOptions.DependsOn = item.Value
case "protect":
t = model.BoolType
resourceOptions.Protect = item.Value
case "retainOnDelete":
t = model.BoolType
resourceOptions.RetainOnDelete = item.Value
case "ignoreChanges":
t = model.NewListType(ResourcePropertyType)
resourceOptions.IgnoreChanges = item.Value
case "version":
t = model.StringType
resourceOptions.Version = item.Value
case "pluginDownloadURL":
t = model.StringType
resourceOptions.PluginDownloadURL = item.Value
case "deletedWith":
t = model.DynamicType
resourceOptions.DeletedWith = item.Value
default:
diagnostics = append(diagnostics, unsupportedAttribute(item.Name, item.Syntax.NameRange))
continue
}
if model.InputType(t).ConversionFrom(item.Value.Type()) == model.NoConversion {
diagnostics = append(diagnostics, model.ExprNotConvertible(model.InputType(t), item.Value))
}
case *model.Block:
diagnostics = append(diagnostics, unsupportedBlock(item.Type, item.Syntax.TypeRange))
}
}
return resourceOptions, diagnostics
}
// bindResourceBody binds the body of a resource.
func (b *binder) bindResourceBody(node *Resource) hcl.Diagnostics {
var diagnostics hcl.Diagnostics
// Allow for lenient traversal when we choose to skip resource type-checking.
node.LenientTraversal = b.options.skipResourceTypecheck
node.VariableType = node.OutputType
// If the resource has a range option, we need to know the type of the collection being ranged over. Pre-bind the
// range expression now, but ignore the diagnostics.
var rangeKey, rangeValue model.Type
for _, block := range node.syntax.Body.Blocks {
if block.Type == "options" {
if rng, hasRange := block.Body.Attributes["range"]; hasRange {
expr, _ := model.BindExpression(rng.Expr, b.root, b.tokens, b.options.modelOptions()...)
typ := model.ResolveOutputs(expr.Type())
resourceVar := &model.Variable{
Name: "r",
VariableType: node.VariableType,
}
switch {
case model.InputType(model.BoolType).ConversionFrom(typ) == model.SafeConversion:
condExpr := &model.ConditionalExpression{
Condition: expr,
TrueResult: model.VariableReference(resourceVar),
FalseResult: model.ConstantReference(&model.Constant{
Name: "null",
ConstantValue: cty.NullVal(cty.DynamicPseudoType),
}),
}
diags := condExpr.Typecheck(false)
contract.Assertf(len(diags) == 0, "failed to typecheck conditional expression: %v", diags)
node.VariableType = condExpr.Type()
2023-06-08 19:43:54 +00:00
case model.InputType(model.NumberType).ConversionFrom(typ) == model.SafeConversion:
functions := pulumiBuiltins(b.options)
rangeArgs := []model.Expression{expr}
rangeSig, _ := functions["range"].GetSignature(rangeArgs)
rangeExpr := &model.ForExpression{
ValueVariable: &model.Variable{
Name: "_",
VariableType: model.NumberType,
},
Collection: &model.FunctionCallExpression{
Name: "range",
Signature: rangeSig,
Args: rangeArgs,
},
Value: model.VariableReference(resourceVar),
}
diags := rangeExpr.Typecheck(false)
contract.Assertf(len(diags) == 0, "failed to typecheck range expression: %v", diags)
rangeValue = model.IntType
node.VariableType = rangeExpr.Type()
default:
strictCollectionType := !b.options.skipRangeTypecheck
rk, rv, diags := model.GetCollectionTypes(typ, rng.Range(), strictCollectionType)
rangeKey, rangeValue, diagnostics = rk, rv, append(diagnostics, diags...)
iterationExpr := &model.ForExpression{
ValueVariable: &model.Variable{
Name: "_",
VariableType: rangeValue,
},
Collection: expr,
Value: model.VariableReference(resourceVar),
StrictCollectionTypechecking: strictCollectionType,
}
diags = iterationExpr.Typecheck(false)
contract.Ignore(diags) // Any relevant diagnostics were reported by GetCollectionTypes.
node.VariableType = iterationExpr.Type()
}
}
}
}
// Bind the resource's body.
scopes := newResourceScopes(b.root, node, rangeKey, rangeValue)
block, blockDiags := model.BindBlock(node.syntax, scopes, b.tokens, b.options.modelOptions()...)
diagnostics = append(diagnostics, blockDiags...)
var options *model.Block
for _, item := range block.Body.Items {
switch item := item.(type) {
case *model.Attribute:
if item.Name == LogicalNamePropertyKey {
logicalName, lDiags := getStringAttrValue(item)
if lDiags != nil {
diagnostics = diagnostics.Append(lDiags)
} else {
node.logicalName = logicalName
}
continue
}
node.Inputs = append(node.Inputs, item)
case *model.Block:
switch item.Type {
case "options":
if options != nil {
diagnostics = append(diagnostics, duplicateBlock(item.Type, item.Syntax.TypeRange))
} else {
options = item
}
default:
diagnostics = append(diagnostics, unsupportedBlock(item.Type, item.Syntax.TypeRange))
}
}
}
[program-gen/pcl] Avoid pretty printing large object graphs when encountering bind error in resource properties (#14864) # Description When we bind PCL program and a resource property doesn't type-check, we pretty print the type of the resource property in its _entirety_ in the error message and that includes nested objects, nested maps, nested lists, unions etc. instead of using type references: we pretty print the `model.Type` derived from `schema.Type` and this resolves the entire graph. This PR changes it such that we print the `schema.Type` in string form in the error message when we encounter a bind error in resource properties instead of fully resolving and printing the full `model.Type`. Fixes #14317 ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [ ] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-20 01:20:42 +00:00
resourceProperties := make(map[string]schema.Type)
if node.Schema != nil {
for _, property := range node.Schema.Properties {
resourceProperties[property.Name] = property.Type
}
}
// Typecheck the attributes.
if objectType, ok := node.InputType.(*model.ObjectType); ok {
diag := func(d *hcl.Diagnostic) {
if b.options.skipResourceTypecheck && d.Severity == hcl.DiagError {
d.Severity = hcl.DiagWarning
}
diagnostics = append(diagnostics, d)
}
attrNames := codegen.StringSet{}
for _, attr := range node.Inputs {
attrNames.Add(attr.Name)
if typ, ok := objectType.Properties[attr.Name]; ok {
conversion := typ.ConversionFrom(attr.Value.Type())
if !conversion.Exists() {
[program-gen/pcl] Avoid pretty printing large object graphs when encountering bind error in resource properties (#14864) # Description When we bind PCL program and a resource property doesn't type-check, we pretty print the type of the resource property in its _entirety_ in the error message and that includes nested objects, nested maps, nested lists, unions etc. instead of using type references: we pretty print the `model.Type` derived from `schema.Type` and this resolves the entire graph. This PR changes it such that we print the `schema.Type` in string form in the error message when we encounter a bind error in resource properties instead of fully resolving and printing the full `model.Type`. Fixes #14317 ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [ ] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-20 01:20:42 +00:00
if propertyType, ok := resourceProperties[attr.Name]; ok {
attributeRange := attr.Value.SyntaxNode().Range()
diag(&hcl.Diagnostic{
Severity: hcl.DiagError,
Subject: &attributeRange,
Detail: fmt.Sprintf("Cannot assign value %s to attribute of type %q for resource %q",
attr.Value.Type().Pretty().String(),
propertyType.String(),
node.Token),
})
}
}
} else {
diag(unsupportedAttribute(attr.Name, attr.Syntax.NameRange))
}
}
for _, k := range codegen.SortedKeys(objectType.Properties) {
typ := objectType.Properties[k]
if model.IsOptionalType(typ) || attrNames.Has(k) {
// The type is present or optional. No error.
continue
}
if model.IsConstType(objectType.Properties[k]) {
// The type is const, so the value is implied. No error.
continue
}
diag(missingRequiredAttribute(k, block.Body.Syntax.MissingItemRange()))
}
}
// Typecheck the options block.
if options != nil {
resourceOptions, optionsDiags := bindResourceOptions(options)
diagnostics = append(diagnostics, optionsDiags...)
node.Options = resourceOptions
}
node.Definition = block
return diagnostics
}