hashicorp / terraform-cdk

Define infrastructure resources using programming constructs and provision them using HashiCorp Terraform
https://www.terraform.io/cdktf
Mozilla Public License 2.0
4.84k stars 450 forks source link

CDKTF convert Errors on array on Nutanix Provider/Virtual Machne Resource, Synthing manually made python cdktf leaves array fields blank #3170

Closed bijenkins closed 5 months ago

bijenkins commented 11 months ago

Expected Behavior

TL;DR After converting the Nutanix Provider, following the provided docs, converting a vm resource from terraform using the cdktf convert tool errors on conversion, it's expected to convert the provider and convert the main.tffile

cat main.tf | cdktf convert --provider 'nutanix/nutanix' --language python > test.py results in a array error from the provider-generator/ .../resource-parser.js

This lines up with the behavior experienced if one converts the provider the themselves and constructs a Terraform Stack in Python code themselves. Using the Terraform stack in this way, results in array fields on the Nutanix VM array parameter fields being empty in the cdktf.out.json.

Read

The cdktf conversion process should be able to convert this Nutanix VM Terraform manifest. Following instructions from the cdktf convert subcommand.

The cdktf being used via Python, manually writing the code to create a Nutanix Terraform stack should convert all fields of a VM without blank fields.

These two different yet connected scenarios we believe incorrectly convert types of arrays for disk_list, and nic_list.

Actual Behavior

See steps to reproduce.

Steps to Reproduce

Manual Conversion of Provider and Running Created Stack code:

The manual conversion process is documented in a issue in the nutanix github space:

https://github.com/nutanix/terraform-provider-nutanix/issues/624

After contacting Nutanix they stated to go through Hashicorp Support due to them not supporting CDKTF. They are a trusted partner according to their registry page.

Automated Conversion Steps

  1. cdktf installed
  2. With files installed at root level below execute:
cat main.tf | cdktf convert --provider 'nutanix/nutanix' --language python --stack > test.py

Output


──> cat main.tf | cdktf convert --provider 'nutanix/nutanix' --language python  > test.py                7 ↵ ──()─┘
Internal Error: unexpected array
Error: unexpected array
    at Parser.renderAttributeType (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/resource-parser.js:182:23)
    at Parser.renderAttributesForBlock (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/resource-parser.js:241:31)
    at Parser.resourceFrom (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/resource-parser.js:102:33)
    at ResourceParser.parse (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/resource-parser.js:431:33)
    at /Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/provider-generator.js:65:121
    at Array.map (<anonymous>)
    at TerraformProviderGenerator.buildResourceModels (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/provider-generator.js:65:75)
    at /Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/hcl2cdk/lib/index.js:92:31
    at Array.reduce (<anonymous>)
    at convertToTypescript (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/hcl2cdk/lib/index.js:90:79)
Collecting Debug Information...
/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/yoga-layout-prebuilt/yoga-layout/build/Release/nbind.js:53
        throw ex;
        ^

Error: unexpected array
    at Parser.renderAttributeType (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/resource-parser.js:182:23)
    at Parser.renderAttributesForBlock (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/resource-parser.js:241:31)
    at Parser.resourceFrom (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/resource-parser.js:102:33)
    at ResourceParser.parse (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/resource-parser.js:431:33)
    at /Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/provider-generator.js:65:121
    at Array.map (<anonymous>)
    at TerraformProviderGenerator.buildResourceModels (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/provider-generator/lib/get/generator/provider-generator.js:65:75)
    at /Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/hcl2cdk/lib/index.js:92:31
    at Array.reduce (<anonymous>)
    at convertToTypescript (/Users/billyjenkins/.nvm/versions/node/v20.2.0/lib/node_modules/cdktf-cli/node_modules/@cdktf/hcl2cdk/lib/index.js:90:79) {
  language: 'python',
  __type: 'Internal'
}

Node.js v20.2.0

Note

I tested this on node 18 to node 20 using NVM.

Files:

cdktf.json

{
    "language": "python",
    "app": "python test.py",
    "projectId": "50bac325-efb8-4839-b885-242d94bf5c43",
    "sendCrashReports": "false",
    "terraformProviders": [
      "nutanix/nutanix"
    ],
    "terraformModules": [],
    "codeMakerOutput": "imports",
    "context": {
      "excludeStackIdFromLogicalIds": "true",
      "allowSepCharsInLogicalIds": "true"
    }
 }

main.tf

#############################################################################
# Example main.tf for Nutanix + Terraform
#
# Author: jon@nutanix.com
#
# This script is a quick demo of how to use the following provider objects:
# - providers
#     - terraform-provider-nutanix
# - resources
#     - nutanix_virtual_machine
#     - nutanix_subnet
#     - nutanix_image
# - data sources
#     - nutanix_clusters
# - script Variables
#     - clusterid's for targeting clusters within prism central
#
# Feel free to reuse, comment, and contribute, so that others may learn.
#
#############################################################################
### Define Provider Info for terraform-provider-nutanix
### This is where you define the credentials for ** Prism Central **
###
### NOTE:
###   While it may be possible to use Prism Element directly, Nutanix's
###   provider is not structured or tested for this. Using Prism Central will
###   give the broadest capabilities across the board
terraform{
  required_providers{
    nutanix = {
      source = "nutanix/nutanix"
      version = "1.9.3"
    }
  }
}
provider "nutanix" {
  username  = "terraform_admin"
  password  = ""
  endpoint  = "10.6.100.20"
  insecure  = true
  port      = 9440
}

data "nutanix_clusters" "clusters" {
}

##########################
### Data Sources
##########################
### These are "lookups" to simply define an already existing object as a
### plain text name
### This is useful when managing a nutanix prism central instance from multiple
### state files, or deploying terraform into an existing / brownfield environment

### Virtual Machine Data Sources
# data "nutanix_virtual_machine" "nutanix_virtual_machine" {
#   vm_id = nutanix_virtual_machine.vm1.id
# }

## Image Data Sources
 data "nutanix_image" "s2019" {
     image_id = "e"

 }

### Subnet Data Sources
 data "nutanix_subnet" "VLAN_618" {
    subnet_id = "cd38c43c-fa7a-****-9bf0-***********"
}

### Cluster Data Sources
 data "nutanix_cluster" "cluster1" {
    cluster_id = "0005f342-2700-****-467a-*********"
 }

### Virtual Machine Resources
## Related Product Docs:
##    https://portal.nutanix.com/#/page/docs/details?targetId=Prism-Central-Guide-Prism-v58:mul-vm-create-manage-pc-c.html
## Related Developer Docs:
##    http://developer.nutanix.com/reference/prism_central/v3/#vms
## Implementation Notes on VMs
# These are VMs managed by Prism Central, which could span across AHV, ESXi, or
# Prism Self Service Portal. That said, if you're doing ESXi, it is most likely
# that would deploy them via a VMware provider against vCenter APIs.

resource "nutanix_virtual_machine" "ggtest01" {
  # General Information
  name                 = "test01"
  description          = "demo Frontend Web Server"
  num_vcpus_per_socket = 2
  num_sockets          = 1
  memory_size_mib      = 16000

  # What cluster will this VLAN live on?
  cluster_uuid = data.nutanix_cluster.cluster1.cluster_id

  # What networks will this be attached to?
  nic_list {
    # subnet_reference is saying, which VLAN/network do you want to attach here?
    subnet_uuid = data.nutanix_subnet.VLAN_618.subnet_id 
    # Used to set static IP.
    # ip_endpoint_list {
    #   ip   = "10.xx.xx.xx"
    #   type = "ASSIGNED"
    # }
  }

  # What disk/cdrom configuration will this have?
  disk_list {
    # data_source_reference in the Nutanix API refers to where the source for
    # the disk device will come from. Could be a clone of a different VM or a
    # image like we're doing here
    data_source_reference = {
        kind = "image"
        uuid = data.nutanix_image.s2019.image_id}

    device_properties {
      disk_address = {
        device_index = 0
        adapter_type = "SCSI"
      }

      device_type = "DISK"
    }
    storage_config {
      storage_container_reference {
        kind = "storage_container"
        uuid = "17a6c666-db20-4179-9a7c-*********"
      }   
    }
  }  
}

# Show IP address
#output "ip_address" {
#  value = nutanix_virtual_machine.demo-01-web.nic_list_status[0].ip_endpoint_list[0].ip
#}

Versions

cdktf debug language: python cdktf-cli: 0.18.0 node: v20.2.0 terraform: 1.5.6 arch: arm64 os: darwin 21.3.0

Providers

┌─────────────────┬──────────────────┬───────┬────────────┬──────────────┬─────────────────┐ │ Provider Name │ Provider Version │ CDKTF │ Constraint │ Package Name │ Package Version │ ├─────────────────┼──────────────────┼───────┼────────────┼──────────────┼─────────────────┤ │ nutanix/nutanix │ 1.9.3 │ │ │ │ │

Gist

No response

Possible Solutions

We believe the struct that nutanix has defined in their provider is incorrectly setting type, or that sequence is not being picked up by jsii.

Converted code using cdktf get:

class VirtualMachineConfig(_cdktf_9a9027ec.TerraformMetaArguments):
    def __init__(
        self,
        *,
        connection: typing.Optional[typing.Union[typing.Union[_cdktf_9a9027ec.SSHProvisionerConnection, typing.Dict[builtins.str, typing.Any]], typing.Union[_cdktf_9a9027ec.WinrmProvisionerConnection, typing.Dict[builtins.str, typing.Any]]]] = None,
        count: typing.Optional[typing.Union[jsii.Number, _cdktf_9a9027ec.TerraformCount]] = None,
        depends_on: typing.Optional[typing.Sequence[_cdktf_9a9027ec.ITerraformDependable]] = None,
        for_each: typing.Optional[_cdktf_9a9027ec.ITerraformIterator] = None,
        lifecycle: typing.Optional[typing.Union[_cdktf_9a9027ec.TerraformResourceLifecycle, typing.Dict[builtins.str, typing.Any]]] = None,
        provider: typing.Optional[_cdktf_9a9027ec.TerraformProvider] = None,
        provisioners: typing.Optional[typing.Sequence[typing.Union[typing.Union[_cdktf_9a9027ec.FileProvisioner, typing.Dict[builtins.str, typing.Any]], typing.Union[_cdktf_9a9027ec.LocalExecProvisioner, typing.Dict[builtins.str, typing.Any]], typing.Union[_cdktf_9a9027ec.RemoteExecProvisioner, typing.Dict[builtins.str, typing.Any]]]]] = None,
        cluster_uuid: builtins.str,
        name: builtins.str,
        availability_zone_reference: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None,
        boot_device_disk_address: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None,
        boot_device_mac_address: typing.Optional[builtins.str] = None,
        boot_device_order_list: typing.Optional[typing.Sequence[builtins.str]] = None,
        boot_type: typing.Optional[builtins.str] = None,
        categories: typing.Optional[typing.Union[_cdktf_9a9027ec.IResolvable, typing.Sequence[typing.Union[VirtualMachineCategories, typing.Dict[builtins.str, typing.Any]]]]] = None,
        cloud_init_cdrom_uuid: typing.Optional[builtins.str] = None,
        description: typing.Optional[builtins.str] = None,
        disk_list: typing.Optional[typing.Union[_cdktf_9a9027ec.IResolvable, typing.Sequence[typing.Union["VirtualMachineDiskListStruct", typing.Dict[builtins.str, typing.Any]]]]] = None, <<< Sequence
        enable_cpu_passthrough: typing.Optional[typing.Union[builtins.bool, _cdktf_9a9027ec.IResolvable]] = None,
        enable_script_exec: typing.Optional[typing.Union[builtins.bool, _cdktf_9a9027ec.IResolvable]] = None,
        gpu_list: typing.Optional[typing.Union[_cdktf_9a9027ec.IResolvable, typing.Sequence[typing.Union["VirtualMachineGpuListStruct", typing.Dict[builtins.str, typing.Any]]]]] = None,
        guest_customization_cloud_init_custom_key_values: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None,
        guest_customization_cloud_init_meta_data: typing.Optional[builtins.str] = None,
        guest_customization_cloud_init_user_data: typing.Optional[builtins.str] = None,
        guest_customization_is_overridable: typing.Optional[typing.Union[builtins.bool, _cdktf_9a9027ec.IResolvable]] = None,
        guest_customization_sysprep: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None,
        guest_customization_sysprep_custom_key_values: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None,
        guest_os_id: typing.Optional[builtins.str] = None,
        hardware_clock_timezone: typing.Optional[builtins.str] = None,
        id: typing.Optional[builtins.str] = None,
        is_vcpu_hard_pinned: typing.Optional[typing.Union[builtins.bool, _cdktf_9a9027ec.IResolvable]] = None,
        machine_type: typing.Optional[builtins.str] = None,
        memory_size_mib: typing.Optional[jsii.Number] = None,
        ngt_credentials: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None,
        ngt_enabled_capability_list: typing.Optional[typing.Sequence[builtins.str]] = None,
        nic_list: typing.Optional[typing.Union[_cdktf_9a9027ec.IResolvable, typing.Sequence[typing.Union["VirtualMachineNicListStruct", typing.Dict[builtins.str, typing.Any]]]]] = None, <<<<<< Sequence not converting correctly it appears
        num_sockets: typing.Optional[jsii.Number] = None,
        num_vcpus_per_socket: typing.Optional[jsii.Number] = None,
        num_vnuma_nodes: typing.Optional[jsii.Number] = None,
        nutanix_guest_tools: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None,
        owner_reference: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None,
        parent_reference: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None,
        power_state_mechanism: typing.Optional[builtins.str] = None,
        project_reference: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None,
        serial_port_list: typing.Optional[typing.Union[_cdktf_9a9027ec.IResolvable, typing.Sequence[typing.Union["VirtualMachineSerialPortListStruct", typing.Dict[builtins.str, typing.Any]]]]] = None,
        should_fail_on_script_failure: typing.Optional[typing.Union[builtins.bool, _cdktf_9a9027ec.IResolvable]] = None,
        timeouts: typing.Optional[typing.Union["VirtualMachineTimeouts", typing.Dict[builtins.str, typing.Any]]] = None,
        use_hot_add: typing.Optional[typing.Union[builtins.bool, _cdktf_9a9027ec.IResolvable]] = None,
        vga_console_enabled: typing.Optional[typing.Union[builtins.bool, _cdktf_9a9027ec.IResolvable]] = None,
    ) -> None:

Workarounds

There's none, we can't convert this module and use it using the provided conversion tools, and trying to create the python code manually results in unexpected blank Sequence fields.

Anything Else?

We also tested conversion using javascript instead of python with VERY similar errors.

References

No response

Help Wanted

Community Note

xiehan commented 11 months ago

I'll leave it for the rest of the team to comment on the technical details, but FWIW this looks like a duplicate of #3111 (though this one has a lot more detail and should help the team with debugging; thank you!).

bijenkins commented 11 months ago

We are willing to work with whoever to get this resolved, we are using the CDKTF extensively and this is a blocker for us. Thank you @xiehan

bijenkins commented 10 months ago

@DanielMSchmidt could we schedule a time to meetup? I'm available whenever you are.

bijenkins commented 8 months ago

This has been sitting quite a while @xiehan can we get a status update by chance?

DanielMSchmidt commented 8 months ago

I tried to reproduce this and for me it worked fine with 0.20.1. For me it resulted in this cdk code (i made it a stack and copied it into a fresh project):

#!/usr/bin/env python
from constructs import Construct
from cdktf import App, TerraformStack

from cdktf import Token
#
# Provider bindings are generated by running `cdktf get`.
# See https://cdk.tf/provider-generation for more details.
#
from imports.nutanix.data_nutanix_cluster import DataNutanixCluster
from imports.nutanix.data_nutanix_clusters import DataNutanixClusters
from imports.nutanix.data_nutanix_image import DataNutanixImage
from imports.nutanix.data_nutanix_subnet import DataNutanixSubnet
from imports.nutanix.provider import NutanixProvider
from imports.nutanix.virtual_machine import VirtualMachine

class MyStack(TerraformStack):
    def __init__(self, scope, name):
        super().__init__(scope, name)
        NutanixProvider(self, "nutanix",
            endpoint="10.6.100.20",
            insecure=True,
            password="",
            port=Token.as_string(9440),
            username="terraform_admin"
        )
        cluster1 = DataNutanixCluster(self, "cluster1",
            cluster_id="0005f342-2700-****-467a-*********"
        )
        DataNutanixClusters(self, "clusters")
        s2019 = DataNutanixImage(self, "s2019",
            image_id="e"
        )
        vlan618 = DataNutanixSubnet(self, "VLAN_618",
            subnet_id="cd38c43c-fa7a-****-9bf0-***********"
        )
        VirtualMachine(self, "ggtest01",
            cluster_uuid=Token.as_string(cluster1.cluster_id),
            description="demo Frontend Web Server",
            disk_list=[{
                "data_source_reference": {
                    "kind": "image",
                    "uuid": Token.as_string(s2019.image_id)
                },
                "device_properties": {
                    "device_type": "DISK",
                    "disk_address": {
                        "adapter_type": "SCSI",
                        "device_index": Token.as_string(0)
                    }
                },
                "storage_config": {
                    "storage_container_reference": [{
                        "kind": "storage_container",
                        "uuid": "17a6c666-db20-4179-9a7c-*********"
                    }
                    ]
                }
            }
            ],
            memory_size_mib=16000,
            name="test01",
            nic_list=[{
                "subnet_uuid": Token.as_string(vlan618.subnet_id)
            }
            ],
            num_sockets=1,
            num_vcpus_per_socket=2
        )
app = App()
MyStack(app, "tmp.axnj99KS6U")

app.synth()

And I got this synthed JSON which to me looks also correct

{
  "//": {
    "metadata": {
      "backend": "local",
      "stackName": "tmp.axnj99KS6U",
      "version": "0.20.1"
    },
    "outputs": {
    }
  },
  "data": {
    "nutanix_cluster": {
      "cluster1": {
        "//": {
          "metadata": {
            "path": "tmp.axnj99KS6U/cluster1",
            "uniqueId": "cluster1"
          }
        },
        "cluster_id": "0005f342-2700-****-467a-*********"
      }
    },
    "nutanix_clusters": {
      "clusters": {
        "//": {
          "metadata": {
            "path": "tmp.axnj99KS6U/clusters",
            "uniqueId": "clusters"
          }
        }
      }
    },
    "nutanix_image": {
      "s2019": {
        "//": {
          "metadata": {
            "path": "tmp.axnj99KS6U/s2019",
            "uniqueId": "s2019"
          }
        },
        "image_id": "e"
      }
    },
    "nutanix_subnet": {
      "VLAN_618": {
        "//": {
          "metadata": {
            "path": "tmp.axnj99KS6U/VLAN_618",
            "uniqueId": "VLAN_618"
          }
        },
        "subnet_id": "cd38c43c-fa7a-****-9bf0-***********"
      }
    }
  },
  "provider": {
    "nutanix": [
      {
        "endpoint": "10.6.100.20",
        "insecure": true,
        "password": "",
        "port": 9440,
        "username": "terraform_admin"
      }
    ]
  },
  "resource": {
    "nutanix_virtual_machine": {
      "ggtest01": {
        "//": {
          "metadata": {
            "path": "tmp.axnj99KS6U/ggtest01",
            "uniqueId": "ggtest01"
          }
        },
        "cluster_uuid": "${data.nutanix_cluster.cluster1.cluster_id}",
        "description": "demo Frontend Web Server",
        "disk_list": [
          {
          }
        ],
        "memory_size_mib": 16000,
        "name": "test01",
        "nic_list": [
          {
          }
        ],
        "num_sockets": 1,
        "num_vcpus_per_socket": 2
      }
    }
  },
  "terraform": {
    "backend": {
      "local": {
        "path": "/private/var/folders/m4/673s3vwn1_g7c72bmvq521g00000gn/T/tmp.axnj99KS6U/terraform.tmp.axnj99KS6U.tfstate"
      }
    },
    "required_providers": {
      "nutanix": {
        "source": "nutanix/nutanix",
        "version": "1.9.5"
      }
    }
  }
}

Could you try again with the current cdktf version?

bijenkins commented 7 months ago

@DanielMSchmidt if you notice your synthesized json output has a empty disk_list and nic_list that is under resource.nutanix_virtual_machine.ggtest01. The issue is present in your example.

Those fields should not be empty, as you try to run that via a applyor deploy it will throw a error.

bijenkins commented 6 months ago

Looking for a status update as it's been months @DanielMSchmidt

github-actions[bot] commented 4 months ago

I'm going to lock this issue because it has been closed for 30 days. This helps our maintainers find and focus on the active issues. If you've found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.