diff --git a/docs/resource-ids.md b/docs/resource-ids.md new file mode 100644 index 0000000000..e06a35686f --- /dev/null +++ b/docs/resource-ids.md @@ -0,0 +1,51 @@ +# Resource IDs + +Every Pulumi resource must have an `"id"` field. `"id"` must be a string, and it must be +set by the provider (`Computed` and *not* `Optional` in Terraform parlance). + +## SDKv1 and SDKv2 Based Providers + +The ID requirement is easy to satisfy for SDKv{1,2} based providers since both SDKs +require an ID field of type string set on the provider. If the provider being bridged is +based on SDKv1 or SDKv2, then the bridge handles Pulumi's ID field without intervention. + +## PF Based Providers + +Most PF based providers have an attribute called `"id"` of the right kind for the bridge +to use. If your provider doesn't, then you will see an error when `make tfgen` is +run. Each error has a different kind of resolution. + +### ID of the wrong type +``` +error: Resource dnsimple_email_forward has a problem: "id" attribute is of type "int", expected type "string". To map this resource consider overriding the SchemaInfo.Type field or specifying ResourceInfo.ComputeID. +``` + +This error happens when the upstream resource has an ID but it's not a string. You can fix +it by setting the `SchemaInfo.Type` override for the `"id"` field: + +```go + "dnsimple_email_forward": { + Fields: map[string]*tfbridge.SchemaInfo{ + "id": {Type: "string"}, + }, + }, +``` + + +For providers[^1] where every resource's ID has the wrong type, you can use a `for` loop to apply this: + +```go + prov.P.ResourcesMap().Range(func(key string, value shim.Resource) bool { + if value.Schema().Get("id").Type() != shim.TypeString { + r := prov.Resources[key] + if r.Fields == nil { + r.Fields = make(map[string]*tfbridge.SchemaInfo, 1) + } + r.Fields["id"] = &tfbridge.SchemaInfo{Type: "string"} + } + return true + }) +``` + + +[^1]: https://github.com/pulumi/pulumi-dnsimple/blob/7d7e5f3d88082306f15c3600f3481516ae19454a/provider/resources.go#L126-L140 diff --git a/pf/internal/check/checks.go b/pf/internal/check/checks.go index aad95f0e9e..0c50b2e3f4 100644 --- a/pf/internal/check/checks.go +++ b/pf/internal/check/checks.go @@ -19,7 +19,6 @@ import ( "fmt" "github.com/pulumi/pulumi/sdk/v3/go/common/diag" - "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" "github.com/pulumi/pulumi-terraform-bridge/pf/internal/muxer" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" @@ -58,15 +57,13 @@ func checkIDProperties(sink diag.Sink, info tfbridge.ProviderInfo, isPFResource if resourceHasComputeID(info, rname) { return true } - ok, reason := resourceHasRegularID(resource, info.Resources[rname]) - if ok { + err := resourceHasRegularID(rname, resource, info.Resources[rname]) + if err == nil { return true } - m := fmt.Sprintf("Resource %s has a problem: %s. "+ - "To map this resource consider specifying ResourceInfo.ComputeID", - rname, reason) + errors++ - sink.Errorf(&diag.Diag{Message: m}) + sink.Errorf(&diag.Diag{Message: resourceError{rname, err}.Error()}) return true }) @@ -78,26 +75,79 @@ func checkIDProperties(sink diag.Sink, info tfbridge.ProviderInfo, isPFResource return nil } -func resourceHasRegularID(resource shim.Resource, resourceInfo *tfbridge.ResourceInfo) (bool, string) { +type resourceError struct { + token string + err error +} + +func (err resourceError) Error() string { + msg := fmt.Sprintf("Resource %s has a problem", err.token) + if err.err != nil { + msg += ": " + err.err.Error() + } + return msg +} + +type errSensativeID struct { + token string +} + +func (err errSensativeID) Error() string { + msg := `"id" attribute is sensitive, but cannot be kept secret.` + if err.token != "" { + msg += fmt.Sprintf( + " To accept exposing ID, set `ProviderInfo.Resources[%q].Fields[%q].Secret = tfbridge.True()`", + err.token, "id") + } + return msg +} + +type errWrongIDType struct { + actualType string +} + +func (err errWrongIDType) Error() string { + msg := `"id" attribute is not of type "string"` + const postfix = ". To map this resource consider overriding the SchemaInfo.Type" + + " field or specifying ResourceInfo.ComputeID" + if err.actualType != "" { + msg = fmt.Sprintf( + `"id" attribute is of type %q, expected type "string"`, + err.actualType) + } + return msg + postfix +} + +type errMissingIDAttribute struct{} + +func (errMissingIDAttribute) Error() string { + return `no "id" attribute. To map this resource consider specifying ResourceInfo.ComputeID` +} + +func resourceHasRegularID(rname string, resource shim.Resource, resourceInfo *tfbridge.ResourceInfo) error { idSchema, gotID := resource.Schema().GetOk("id") if !gotID { - return false, `no "id" attribute` + return errMissingIDAttribute{} } - var typeOverride tokens.Type + var info tfbridge.SchemaInfo if resourceInfo != nil { if id := resourceInfo.Fields["id"]; id != nil { - typeOverride = id.Type + info = *id } } // If the user over-rode the type to be a string, don't reject. - if idSchema.Type() != shim.TypeString && typeOverride != "string" { - return false, `"id" attribute is not of type String` + if idSchema.Type() != shim.TypeString && info.Type != "string" { + actual := idSchema.Type().String() + if info.Type != "" { + actual = string(info.Type) + } + return errWrongIDType{actualType: actual} } - if idSchema.Sensitive() { - return false, `"id" attribute is sensitive` + if idSchema.Sensitive() && (info.Secret == nil || !*info.Secret) { + return errSensativeID{rname} } - return true, "" + return nil } func resourceHasComputeID(info tfbridge.ProviderInfo, resname string) bool {