Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for resource schema properties with nested structures #155

Open
dikhan opened this issue May 6, 2019 · 10 comments
Open

Add support for resource schema properties with nested structures #155

dikhan opened this issue May 6, 2019 · 10 comments
Labels
enhancement New feature or request subsystem/types Issues and feature requests related to the type system of Terraform and our shims around it. terraform-plugin-framework Resolved in terraform-plugin-framework

Comments

@dikhan
Copy link

dikhan commented May 6, 2019

Current Terraform Version

Terraform v0.11.13

Use-cases

As a service provider building my own custom terraform provider, I would like to be able to have resources with schemas that contain properties with nested structures.

More context:

One of the APIs I am building a custom terraform plugin for, requires support for nested objects (nested structures).

After building the custom terraform provider providing support for the API aforementioned, I ran into the following error message when managing such resource:

Error: swaggercodegen_cdn_v1.my_cdn: object_nested_scheme_property (object_property): '' expected type 'string', got unconvertible type '[]map[string]interface {}'

The terraform config looks like this:

resource "swaggercodegen_cdn_v1" "my_cdn" {
  object_nested_scheme_property = {
    name = "dani"
    object_property = {
      account = "something"
    }
  }
}

For the sake of more context, the API in question has the following OpenAPI v2 spec model.

definitions:
  ExampleModel:
    type: "object"
    required:
      - object_nested_scheme_property
    properties:
      id:
        type: "string"
        readOnly: true
      object_nested_scheme_property:
        type: object # nested properties required type equal object to be considered as object
        properties:
          name:
            type: string
          object_property:
            type: "object"
            properties:
              account:
                type: string

Attempted Solutions

So far I have just tried to understand where the error is being thrown, and that's where I ran into the following line of code from the terraform core library where it clearly states that nested structs are not currently supported:

Link to potentially the source of the issue:
https://github.com/hashicorp/terraform/blob/master/helper/schema/schema.go#L1646

Pasting here the comment line the above link points to:

Comment:
// TODO: We don't actually support this (yet)
// but silently pass the validation, until we decide
// how to handle nested structures in maps

The translated terraform resource schema struct for the above definition looks as follows:

(*schema.Resource)(0xc0002b9260)({
 Schema: (map[string]*schema.Schema) (len=9) {
  (string) (len=29) "object_nested_scheme_property": (*schema.Schema)(0xc0001573b0)({
   Type: (schema.ValueType) TypeMap,
   Optional: (bool) false,
   Required: (bool) true,
   DiffSuppressFunc: (schema.SchemaDiffSuppressFunc) <nil>,
   Default: (interface {}) <nil>,
   DefaultFunc: (schema.SchemaDefaultFunc) <nil>,
   Description: (string) "",
   InputDefault: (string) "",
   Computed: (bool) false,
   ForceNew: (bool) false,
   StateFunc: (schema.SchemaStateFunc) <nil>,
   Elem: (*schema.Resource)(0xc0002b8850)({
    Schema: (map[string]*schema.Schema) (len=2) {
     (string) (len=15) "object_property": (*schema.Schema)(0xc000157590)({
      Type: (schema.ValueType) TypeMap,
      Optional: (bool) true,
      Required: (bool) false,
      DiffSuppressFunc: (schema.SchemaDiffSuppressFunc) <nil>,
      Default: (interface {}) <nil>,
      DefaultFunc: (schema.SchemaDefaultFunc) <nil>,
      Description: (string) "",
      InputDefault: (string) "",
      Computed: (bool) false,
      ForceNew: (bool) false,
      StateFunc: (schema.SchemaStateFunc) <nil>,
      Elem: (*schema.Resource)(0xc0002b87e0)({
       Schema: (map[string]*schema.Schema) (len=1) {
        (string) (len=7) "account": (*schema.Schema)(0xc000157680)({
         Type: (schema.ValueType) TypeString,
         Optional: (bool) true,
         Required: (bool) false,
         DiffSuppressFunc: (schema.SchemaDiffSuppressFunc) <nil>,
         Default: (interface {}) <nil>,
         DefaultFunc: (schema.SchemaDefaultFunc) <nil>,
         Description: (string) "",
         InputDefault: (string) "",
         Computed: (bool) false,
         ForceNew: (bool) false,
         StateFunc: (schema.SchemaStateFunc) <nil>,
         Elem: (interface {}) <nil>,
         MaxItems: (int) 0,
         MinItems: (int) 0,
         PromoteSingle: (bool) false,
         Set: (schema.SchemaSetFunc) <nil>,
         ComputedWhen: ([]string) <nil>,
         ConflictsWith: ([]string) <nil>,
         Deprecated: (string) "",
         Removed: (string) "",
         ValidateFunc: (schema.SchemaValidateFunc) 0x1bf1c80,
         Sensitive: (bool) false
        })
       },
       SchemaVersion: (int) 0,
       MigrateState: (schema.StateMigrateFunc) <nil>,
       Create: (schema.CreateFunc) <nil>,
       Read: (schema.ReadFunc) <nil>,
       Update: (schema.UpdateFunc) <nil>,
       Delete: (schema.DeleteFunc) <nil>,
       Exists: (schema.ExistsFunc) <nil>,
       CustomizeDiff: (schema.CustomizeDiffFunc) <nil>,
       Importer: (*schema.ResourceImporter)(<nil>),
       DeprecationMessage: (string) "",
       Timeouts: (*schema.ResourceTimeout)(<nil>)
      }),
      MaxItems: (int) 0,
      MinItems: (int) 0,
      PromoteSingle: (bool) false,
      Set: (schema.SchemaSetFunc) <nil>,
      ComputedWhen: ([]string) <nil>,
      ConflictsWith: ([]string) <nil>,
      Deprecated: (string) "",
      Removed: (string) "",
      ValidateFunc: (schema.SchemaValidateFunc) <nil>,
      Sensitive: (bool) false
     }),
     (string) (len=4) "name": (*schema.Schema)(0xc0001574a0)({
      Type: (schema.ValueType) TypeString,
      Optional: (bool) true,
      Required: (bool) false,
      DiffSuppressFunc: (schema.SchemaDiffSuppressFunc) <nil>,
      Default: (interface {}) <nil>,
      DefaultFunc: (schema.SchemaDefaultFunc) <nil>,
      Description: (string) "",
      InputDefault: (string) "",
      Computed: (bool) false,
      ForceNew: (bool) false,
      StateFunc: (schema.SchemaStateFunc) <nil>,
      Elem: (interface {}) <nil>,
      MaxItems: (int) 0,
      MinItems: (int) 0,
      PromoteSingle: (bool) false,
      Set: (schema.SchemaSetFunc) <nil>,
      ComputedWhen: ([]string) <nil>,
      ConflictsWith: ([]string) <nil>,
      Deprecated: (string) "",
      Removed: (string) "",
      ValidateFunc: (schema.SchemaValidateFunc) 0x1bf1c80,
      Sensitive: (bool) false
     })
    },
    SchemaVersion: (int) 0,
    MigrateState: (schema.StateMigrateFunc) <nil>,
    Create: (schema.CreateFunc) <nil>,
    Read: (schema.ReadFunc) <nil>,
    Update: (schema.UpdateFunc) <nil>,
    Delete: (schema.DeleteFunc) <nil>,
    Exists: (schema.ExistsFunc) <nil>,
    CustomizeDiff: (schema.CustomizeDiffFunc) <nil>,
    Importer: (*schema.ResourceImporter)(<nil>),
    DeprecationMessage: (string) "",
    Timeouts: (*schema.ResourceTimeout)(<nil>)
   }),
   MaxItems: (int) 0,
   MinItems: (int) 0,
   PromoteSingle: (bool) false,
   Set: (schema.SchemaSetFunc) <nil>,
   ComputedWhen: ([]string) <nil>,
   ConflictsWith: ([]string) <nil>,
   Deprecated: (string) "",
   Removed: (string) "",
   ValidateFunc: (schema.SchemaValidateFunc) <nil>,
   Sensitive: (bool) false
  }),
 },
 SchemaVersion: (int) 0,
 MigrateState: (schema.StateMigrateFunc) <nil>,
 Create: (schema.CreateFunc) 0x1bf8ef0,
 Read: (schema.ReadFunc) 0x1bf8ff0,
 Update: (schema.UpdateFunc) 0x1bf91f0,
 Delete: (schema.DeleteFunc) 0x1bf90f0,
 Exists: (schema.ExistsFunc) <nil>,
 CustomizeDiff: (schema.CustomizeDiffFunc) <nil>,
 Importer: (*schema.ResourceImporter)(0xc000010368)({
  State: (schema.StateFunc) 0x1bf34b0
 }),
 DeprecationMessage: (string) "",
 Timeouts: (*schema.ResourceTimeout)(0xc000370ba0)({
  Create: (*time.Duration)(0xc0003ba690)(30s),
  Read: (*time.Duration)(<nil>),
  Update: (*time.Duration)(<nil>),
  Delete: (*time.Duration)(<nil>),
  Default: (*time.Duration)(0xc000370ac0)(10m0s)
 })
})

I am happy yo contribute to add support for this feature, however the comment above seems to suggest the terraform team has not decided yet how to handle nested structures in maps so I was wondering if you could provide more insight about this.

Thanks!

Proposal

References

@apparentlymart
Copy link
Contributor

Hi @dikhan!

The comment you found is accurate in that it isn't currently supported, but it is no longer correct that there isn't a known way to support this. In fact, the forthcoming Terraform v0.12 has a new protocol for providers that lays the groundwork for this being possible, but the SDK itself is not ready to support it for the moment because it is still trying to support Terraform 0.10, 0.11 and 0.12 at once and so is constrained a little.

After the final Terraform v0.12 release is out, we'll begin a project to improve the SDK to support the new v0.12 features (which is likely to also mark the end of ongoing Terraform 0.10 and 0.11 support), which should allow what you wanted to do here.

In the meantime, existing providers have achieved an approximation of what you want to do here by using a TypeList attribute with MaxItems: 1 set and its Elem set to a nested *schema.Resource, which can then be used with syntax like this:

resource "swaggercodegen_cdn_v1" "my_cdn" {
  object_nested_scheme_property {
    name = "dani"
    object_property {
      account = "something"
    }
  }
}

The provider code will see object_nested_scheme_property as being a single-element list, but the block syntax in configuration makes that list-ness less apparent to the end-user.

In this case we usually give the list attribute a name that makes it appear that it's declaring the existence of a new object or a category of settings. I can't give a specific example because your issue here didn't include any specific nouns to work with, but you can see similar usage in a number of existing providers, including ones for other CDNs like the AWS provider's support for CloudFront and the Fastly provider. (Some of those are using TypeSet instead of TypeList for historical reasons; I would recommend sticking to TypeList because the rendering of changes to a list element of a large object type in the plan output tends to be more user-friendly.)

@dikhan
Copy link
Author

dikhan commented May 11, 2019

Hi @apparentlymart !

Thanks a lot for the quick response, and apologies for my slow response xP...I managed to put the work around together based on your suggestion and it worked :)

Here's the tf config example:

resource "swaggercodegen_cdn_v1" "my_cdn" {
  label = "label" ## This is an immutable property (refer to swagger file)
  ips = ["127.0.0.1"] ## This is a force-new property (refer to swagger file)
  hostnames = ["origin.com"]

  object_nested_scheme_property {
    object_property {
      account = "something"
    }
  }
}

And this is the result after the apply:

Terraform will perform the following actions:

  + swaggercodegen_cdn_v1.my_cdn
      id:                                                        <computed>
      hostnames.#:                                               "1"
      hostnames.0:                                               "origin.com"
      ips.#:                                                     "1"
      ips.0:                                                     "127.0.0.1"
      label:                                                     "label"
      object_nested_scheme_property.#:                           "1"
      object_nested_scheme_property.0.name:                      <computed>
      object_nested_scheme_property.0.object_property.#:         "1"
      object_nested_scheme_property.0.object_property.0.account: "something"


Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

swaggercodegen_cdn_v1.my_cdn: Creating...
  hostnames.#:                                               "" => "1"
  hostnames.0:                                               "" => "origin.com"
  ips.#:                                                     "" => "1"
  ips.0:                                                     "" => "127.0.0.1"
  label:                                                     "" => "label"
  object_nested_scheme_property.#:                           "" => "1"
  object_nested_scheme_property.0.name:                      "" => "<computed>"
  object_nested_scheme_property.0.object_property.#:         "" => "1"
  object_nested_scheme_property.0.object_property.0.account: "" => "something"
swaggercodegen_cdn_v1.my_cdn: Creation complete after 0s (ID: aeb15688-c7e9-4ffb-9bc5-cc5d3e2dab22)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

And the state file for the ref:

{
    "version": 3,
    "terraform_version": "0.11.13",
    "serial": 1,
    "lineage": "346a248e-fc4e-6903-f750-51bf034b43f7",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "swaggercodegen_cdn_v1.my_cdn": {
                    "type": "swaggercodegen_cdn_v1",
                    "depends_on": [],
                    "primary": {
                        "id": "aeb15688-c7e9-4ffb-9bc5-cc5d3e2dab22",
                        "attributes": {
                            "hostnames.#": "1",
                            "hostnames.0": "origin.com",
                            "id": "aeb15688-c7e9-4ffb-9bc5-cc5d3e2dab22",
                            "ips.#": "1",
                            "ips.0": "127.0.0.1",
                            "label": "label",
                            "object_nested_scheme_property.#": "1",
                            "object_nested_scheme_property.0.name": "autogenerated name",
                            "object_nested_scheme_property.0.object_property.#": "1",
                            "object_nested_scheme_property.0.object_property.0.account": "something",
                        },
                        "meta": {
                            "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0": {
                                "create": 30000000000,
                                "default": 600000000000,
                                "delete": 600000000000,
                                "read": 600000000000,
                                "update": 600000000000
                            }
                        },
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": "provider.swaggercodegen"
                }
            },
            "depends_on": []
        }
    ]
}

Limiting the list to just one element does give the feeling that the object property is list-ness; however this might cause some confusion to the users when they interpolate these properties in other resources as they will need to be aware of the type and index accordingly the array to fetch the value. Nevertheless, this is not something that is a blocker at the moment and cannot be solved with proper documentation that clarifies this behaviour so the user is aware of it.

So all good :)

Looking forward to seeing the new version 0.12 in action. Do you think there will be a lot of incompatible changes when migrating custom providers to the new SDK?

Please feel free to close this feature request considering the workaround suggested and also the fact that 0.12 sdk will have this use case covered.

Thanks!
Dani

dikhan referenced this issue in dikhan/terraform-provider-openapi May 13, 2019
As recommended by terraform main maintainer (see ticket ref below), limiting the
property to MAX one elem so internally terraform will not perform basic validation which
was causing issues previously.

If a given property is marked with the following extension: x-terraform-nested-object,
the property will be translated to internal terraform property schema as
a list of one elem (to show the listless appearing in the configuration)

More info about it in the following ticket:

https://github.com/hashicorp/terraform/issues/21217
dikhan referenced this issue in dikhan/terraform-provider-openapi Jun 22, 2019
…imiting the

property to MAX one elem so internally terraform will not perform basic validation which
was causing issues previously.

Created TODO placeholders for the team to fill out.

More info about it in the following ticket:

https://github.com/hashicorp/terraform/issues/21217#issuecomment-489699737
@simar7
Copy link

simar7 commented Jun 24, 2019

I'm curious if a schema like the following will be supported by the approach of approximation of using a TypeList attribute with MaxItems: 1 set and its Elem set to a nested *schema.Resource:

resource "swaggercodegen_cdn_v1" "my_cdn" {
  object_nested_scheme_property = {
    name = "foo"
    object_property = {
      account = "something"
    }
  }
  object_nested_scheme_property = {
    name = "bar"
    object_property = {
      account = "something"
    }
  }
}

@chkp-royl
Copy link

chkp-royl commented Jul 10, 2019

What is the best practice to get only the attributes the user set in .tf of nested structures?
Currently, I use schema.TypeSet and convert it into map[string]interface{} but then I get all the attributes of the set: attributes the user set in .tf + attributes with default values (which I don't want)
For non-nested attributes (e.g first level attributes) I use d.GetOk() which tells me if the user set that attribute or not but as far as I know, I cant use d.GetOk() on nested structures of my schema
Thanks, Roy

@hashibot hashibot transferred this issue from hashicorp/terraform Sep 26, 2019
@hashibot hashibot added the enhancement New feature or request label Oct 2, 2019
@radeksimko
Copy link
Member

Just for discoverability purposes through search I will say that I sometimes refer to this as TypeObject. This shouldn't imply any particular implementation details though - no implementation or planning was really started yet.

@MaciejKaras
Copy link

@radeksimko I'm also wondering if you are considering support for element of type interface{} or so called any type for TypeMap?

This is a real case scenario for https://github.com/terraform-providers/terraform-provider-vra7 where you create blueprints for resources and each vRA client can have different keys for those configurations. So this provider should expect anything inside TypeMap, even an inner map or other structure and pass it through API as is. Are you aware of any workaround for that problem?

@paultyng
Copy link
Contributor

0.12 core does support that, the SDK does not yet. In core there is a concept of a DynamicPseudoType that can be any arbitrarily nested type information (see #248)

@MaciejKaras
Copy link

@paultyng Thanks for your answer. Moving with conversation to that enhancement #248 then.

@philomory
Copy link

@paultyng #248 was closed, do you know if there is still any plan to add support for nested data structures to the SDK? The comments in #248 were somewhat ambiguous.

My specific use case is that I'm creating a custom data-source provider that queries an external API and returns arbitrary JSON data (depending on the query provided by the user). Right now I'm just returning a JSON string that the end user then must pass to jsondecode(), but that's kind of a PITA so I'd really prefer to just return the data as a map[string]interface{} (or really as just interface{} since in theory the JSON string returned by the user's query could also just be "foo")

@paultyng
Copy link
Contributor

I no longer work at HashiCorp (so will tag in @paddycarver) but I believe the answer to this is that you can currently only use the DynamicPseudoType via https://github.com/hashicorp/terraform-plugin-go (the lower level SDK), but it is possible to switch which SDK you are using on a resource by resource basis.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request subsystem/types Issues and feature requests related to the type system of Terraform and our shims around it. terraform-plugin-framework Resolved in terraform-plugin-framework
Projects
None yet
Development

No branches or pull requests