From 422300d7c9aca9e6b3aa25275e563210bd378e98 Mon Sep 17 00:00:00 2001 From: Deepak Mettem Date: Thu, 31 Oct 2019 14:56:40 -0700 Subject: [PATCH] Add deployment resource and data sources for catalog_item and deployment (#91) Signed-off-by: Deepak Mettem --- examples/deployment/README.md | 11 + examples/deployment/blueprint/README.md | 26 ++ examples/deployment/blueprint/main.tf | 25 + .../blueprint/terraform.tfvars.sample | 6 + examples/deployment/blueprint/variables.tf | 17 + examples/deployment/blueprint/versions.tf | 4 + examples/deployment/catalog_item/README.md | 26 ++ examples/deployment/catalog_item/main.tf | 31 ++ .../catalog_item/terraform.tfvars.sample | 7 + examples/deployment/catalog_item/variables.tf | 20 + examples/deployment/catalog_item/versions.tf | 4 + examples/deployment/no_resources/README.md | 24 + examples/deployment/no_resources/main.tf | 16 + .../no_resources/terraform.tfvars.sample | 5 + examples/deployment/no_resources/variables.tf | 14 + examples/deployment/no_resources/versions.tf | 4 + vra/catalog_item_version.go | 49 ++ vra/data_source_catalog_item.go | 147 ++++++ vra/data_source_catalog_item_test.go | 50 ++ vra/data_source_deployment.go | 209 +++++++++ vra/data_source_deployment_test.go | 76 ++++ vra/expense.go | 83 ++++ vra/provider.go | 3 + vra/provider_test.go | 55 +++ vra/resource.go | 142 ++++++ vra/resource_deployment.go | 430 ++++++++++++++++++ vra/resource_deployment_test.go | 193 ++++++++ vra/resource_reference.go | 61 +++ vra/structure.go | 15 +- 29 files changed, 1752 insertions(+), 1 deletion(-) create mode 100644 examples/deployment/README.md create mode 100644 examples/deployment/blueprint/README.md create mode 100644 examples/deployment/blueprint/main.tf create mode 100644 examples/deployment/blueprint/terraform.tfvars.sample create mode 100644 examples/deployment/blueprint/variables.tf create mode 100644 examples/deployment/blueprint/versions.tf create mode 100644 examples/deployment/catalog_item/README.md create mode 100644 examples/deployment/catalog_item/main.tf create mode 100644 examples/deployment/catalog_item/terraform.tfvars.sample create mode 100644 examples/deployment/catalog_item/variables.tf create mode 100644 examples/deployment/catalog_item/versions.tf create mode 100644 examples/deployment/no_resources/README.md create mode 100644 examples/deployment/no_resources/main.tf create mode 100644 examples/deployment/no_resources/terraform.tfvars.sample create mode 100644 examples/deployment/no_resources/variables.tf create mode 100644 examples/deployment/no_resources/versions.tf create mode 100644 vra/catalog_item_version.go create mode 100644 vra/data_source_catalog_item.go create mode 100644 vra/data_source_catalog_item_test.go create mode 100644 vra/data_source_deployment.go create mode 100644 vra/data_source_deployment_test.go create mode 100644 vra/expense.go create mode 100644 vra/resource.go create mode 100644 vra/resource_deployment.go create mode 100644 vra/resource_deployment_test.go create mode 100644 vra/resource_reference.go diff --git a/examples/deployment/README.md b/examples/deployment/README.md new file mode 100644 index 00000000..968af935 --- /dev/null +++ b/examples/deployment/README.md @@ -0,0 +1,11 @@ +# deployment examples + +This directory covers examples on how to create a deployment in different ways. + +## Getting Started + +Follow these examples for creating deployments: + +* [Deployment](blueprint/README.md) with a blueprint id +* [Deployment](catalog_item/README.md) with a catalog item id +* [Deployment](no_resources/README.md) with no resources \ No newline at end of file diff --git a/examples/deployment/blueprint/README.md b/examples/deployment/blueprint/README.md new file mode 100644 index 00000000..6687d876 --- /dev/null +++ b/examples/deployment/blueprint/README.md @@ -0,0 +1,26 @@ +# deployment with a blueprint id example + +This is an example on how to crete a deployment in vRealize Automation(vRA) using an existing blueprint. + +## Getting Started + +There are variables which need to be added to terraform.tfvars. + +* `url` - The URL for the vRealize Automation (vRA) endpoint +* `refresh_token` - The refresh token for the vRA account +* `project_name` - Project Name +* `blueprint_id` - Blueprint ID +* `blueprint_version` - Blueprint Version +* `deployment_name` - Deployment Name + +To facilitate adding these variables, a sample tfvars file can be copied first: +```shell +cp terraform.tfvars.sample terraform.tfvars +``` + +Once the information is added to `terraform.tfvars`, the cloud account can be brought up via: + +```shell +terraform init +terraform apply +``` diff --git a/examples/deployment/blueprint/main.tf b/examples/deployment/blueprint/main.tf new file mode 100644 index 00000000..bb8cf9d9 --- /dev/null +++ b/examples/deployment/blueprint/main.tf @@ -0,0 +1,25 @@ +provider "vra" { + url = var.url + refresh_token = var.refresh_token +} + +data "vra_project" "this" { + name = var.project_name +} + +resource "vra_deployment" "this" { + name = var.deployment_name + description = "terraform test deployment" + + blueprint_id = var.blueprint_id + blueprint_version = var.blueprint_version + project_id = data.vra_project.this.id + + inputs = { + flavor = "small" + image = "centos" + } + + expand_resources = true + expand_last_request = true +} diff --git a/examples/deployment/blueprint/terraform.tfvars.sample b/examples/deployment/blueprint/terraform.tfvars.sample new file mode 100644 index 00000000..70706297 --- /dev/null +++ b/examples/deployment/blueprint/terraform.tfvars.sample @@ -0,0 +1,6 @@ +refresh_token = "" +url = "" +project_name = "" +blueprint_id = "" +blueprint_version = "" +deployment_name = "" diff --git a/examples/deployment/blueprint/variables.tf b/examples/deployment/blueprint/variables.tf new file mode 100644 index 00000000..7bf76588 --- /dev/null +++ b/examples/deployment/blueprint/variables.tf @@ -0,0 +1,17 @@ +variable "url" { +} + +variable "refresh_token" { +} + +variable "project_name" { +} + +variable "blueprint_id" { +} + +variable "blueprint_version" { +} + +variable "deployment_name" { +} diff --git a/examples/deployment/blueprint/versions.tf b/examples/deployment/blueprint/versions.tf new file mode 100644 index 00000000..ac97c6ac --- /dev/null +++ b/examples/deployment/blueprint/versions.tf @@ -0,0 +1,4 @@ + +terraform { + required_version = ">= 0.12" +} diff --git a/examples/deployment/catalog_item/README.md b/examples/deployment/catalog_item/README.md new file mode 100644 index 00000000..9d17d5e5 --- /dev/null +++ b/examples/deployment/catalog_item/README.md @@ -0,0 +1,26 @@ +# deployment with a catalog item example + +This is an example on how to request/create a deployment in vRealize Automation(vRA) using an existing catalog item. + +## Getting Started + +There are variables which need to be added to terraform.tfvars. + +* `url` - The URL for the vRealize Automation (vRA) endpoint +* `refresh_token` - The refresh token for the vRA account +* `project_name` - Project Name +* `catalog_item_name` - Catalog Item Name +* `catalog_item_version` - Catalog Item Version +* `deployment_name` - Deployment Name + +To facilitate adding these variables, a sample tfvars file can be copied first: +```shell +cp terraform.tfvars.sample terraform.tfvars +``` + +Once the information is added to `terraform.tfvars`, the cloud account can be brought up via: + +```shell +terraform init +terraform apply +``` diff --git a/examples/deployment/catalog_item/main.tf b/examples/deployment/catalog_item/main.tf new file mode 100644 index 00000000..7c41f0b3 --- /dev/null +++ b/examples/deployment/catalog_item/main.tf @@ -0,0 +1,31 @@ +provider "vra" { + url = var.url + refresh_token = var.refresh_token + insecure = var.insecure +} + +data "vra_project" "this" { + name = var.project_name +} + +data "vra_catalog_item" "this" { + name = var.catalog_item_name + expand_versions = true +} + +resource "vra_deployment" "this" { + name = var.deployment_name + description = "terraform test deployment" + + catalog_item_id = data.vra_catalog_item.this.id + catalog_item_version = var.catalog_item_version + project_id = data.vra_project.this.id + + inputs = { + flavor = "small" + image = "centos" + } + + expand_resources = true + expand_last_request = true +} diff --git a/examples/deployment/catalog_item/terraform.tfvars.sample b/examples/deployment/catalog_item/terraform.tfvars.sample new file mode 100644 index 00000000..1c19c521 --- /dev/null +++ b/examples/deployment/catalog_item/terraform.tfvars.sample @@ -0,0 +1,7 @@ +refresh_token = "" +url = "" +insecure = "" +project_name = "" +catalog_item_name = "" +catalog_item_version = "" +deployment_name = "" diff --git a/examples/deployment/catalog_item/variables.tf b/examples/deployment/catalog_item/variables.tf new file mode 100644 index 00000000..3ed963d3 --- /dev/null +++ b/examples/deployment/catalog_item/variables.tf @@ -0,0 +1,20 @@ +variable "url" { +} + +variable "refresh_token" { +} + +variable "insecure" { +} + +variable "project_name" { +} + +variable "catalog_item_name" { +} + +variable "catalog_item_version" { +} + +variable "deployment_name" { +} diff --git a/examples/deployment/catalog_item/versions.tf b/examples/deployment/catalog_item/versions.tf new file mode 100644 index 00000000..ac97c6ac --- /dev/null +++ b/examples/deployment/catalog_item/versions.tf @@ -0,0 +1,4 @@ + +terraform { + required_version = ">= 0.12" +} diff --git a/examples/deployment/no_resources/README.md b/examples/deployment/no_resources/README.md new file mode 100644 index 00000000..6dd16159 --- /dev/null +++ b/examples/deployment/no_resources/README.md @@ -0,0 +1,24 @@ +# deployment with no resources example + +This is an example on how to crete a deployment in vRealize Automation(vRA) without any resources. + +## Getting Started + +There are variables which need to be added to terraform.tfvars. + +* `url` - The URL for the vRealize Automation (vRA) endpoint +* `refresh_token` - The refresh token for the vRA account +* `project_name` - Project Name +* `deployment_name` - Deployment Name + +To facilitate adding these variables, a sample tfvars file can be copied first: +```shell +cp terraform.tfvars.sample terraform.tfvars +``` + +Once the information is added to `terraform.tfvars`, the cloud account can be brought up via: + +```shell +terraform init +terraform apply +``` diff --git a/examples/deployment/no_resources/main.tf b/examples/deployment/no_resources/main.tf new file mode 100644 index 00000000..51ecce63 --- /dev/null +++ b/examples/deployment/no_resources/main.tf @@ -0,0 +1,16 @@ +provider "vra" { + url = var.url + refresh_token = var.refresh_token + insecure = var.insecure +} + +data "vra_project" "this" { + name = var.project_name +} + +resource "vra_deployment" "this" { + name = var.deployment_name + description = "terraform test deployment" + + project_id = data.vra_project.this.id +} diff --git a/examples/deployment/no_resources/terraform.tfvars.sample b/examples/deployment/no_resources/terraform.tfvars.sample new file mode 100644 index 00000000..95bd481f --- /dev/null +++ b/examples/deployment/no_resources/terraform.tfvars.sample @@ -0,0 +1,5 @@ +refresh_token = "" +url = "" +insecure = "" +project_name = "" +deployment_name = "" diff --git a/examples/deployment/no_resources/variables.tf b/examples/deployment/no_resources/variables.tf new file mode 100644 index 00000000..22c9cd46 --- /dev/null +++ b/examples/deployment/no_resources/variables.tf @@ -0,0 +1,14 @@ +variable "url" { +} + +variable "refresh_token" { +} + +variable "insecure" { +} + +variable "project_name" { +} + +variable "deployment_name" { +} diff --git a/examples/deployment/no_resources/versions.tf b/examples/deployment/no_resources/versions.tf new file mode 100644 index 00000000..ac97c6ac --- /dev/null +++ b/examples/deployment/no_resources/versions.tf @@ -0,0 +1,4 @@ + +terraform { + required_version = ">= 0.12" +} diff --git a/vra/catalog_item_version.go b/vra/catalog_item_version.go new file mode 100644 index 00000000..64778c1b --- /dev/null +++ b/vra/catalog_item_version.go @@ -0,0 +1,49 @@ +package vra + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/vmware/vra-sdk-go/pkg/models" +) + +// resourceReferenceSchema returns the schema to use for the catalog item type property +func catalogItemVersionSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "created_at": { + Type: schema.TypeString, + Optional: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } +} + +func flattenCatalogItemVersions(catalogItemVersions []*models.CatalogItemVersion) []map[string]interface{} { + if len(catalogItemVersions) == 0 { + return make([]map[string]interface{}, 0) + } + + versions := make([]map[string]interface{}, 0, len(catalogItemVersions)) + + for _, version := range catalogItemVersions { + helper := make(map[string]interface{}) + helper["created_at"] = version.CreatedAt.String() + helper["description"] = version.Description + helper["id"] = version.ID + + versions = append(versions, helper) + } + + return versions +} diff --git a/vra/data_source_catalog_item.go b/vra/data_source_catalog_item.go new file mode 100644 index 00000000..11062875 --- /dev/null +++ b/vra/data_source_catalog_item.go @@ -0,0 +1,147 @@ +package vra + +import ( + "encoding/json" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/vmware/vra-sdk-go/pkg/client/catalog_items" + "github.com/vmware/vra-sdk-go/pkg/models" +) + +func dataSourceCatalogItem() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCatalogItemRead, + + Schema: map[string]*schema.Schema{ + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + "created_by": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "expand_projects": { + Type: schema.TypeBool, + Optional: true, + }, + "expand_versions": { + Type: schema.TypeBool, + Optional: true, + }, + "id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "last_updated_at": { + Type: schema.TypeString, + Computed: true, + }, + "last_updated_by": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "project_ids": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "projects": resourceReferenceSchema(), + "schema": { + Type: schema.TypeString, + Computed: true, + }, + "source_id": { + Type: schema.TypeString, + Computed: true, + }, + "source_name": { + Type: schema.TypeString, + Computed: true, + }, + "type": resourceReferenceSchema(), + "versions": catalogItemVersionSchema(), + }, + } +} + +func dataSourceCatalogItemRead(d *schema.ResourceData, meta interface{}) error { + apiClient := meta.(*Client).apiClient + + id, idOk := d.GetOk("id") + name, nameOk := d.GetOk("name") + + if !idOk && !nameOk { + return fmt.Errorf("one of id or name must be assigned") + } + + expandProjects := d.Get("expand_projects").(bool) + + getItemsResp, err := apiClient.CatalogItems.GetCatalogItemsUsingGET1( + catalog_items.NewGetCatalogItemsUsingGET1Params(). + WithSearch(withString(name.(string))). + WithExpandProjects(withBool(expandProjects))) + if err != nil { + return err + } + fmt.Printf(id.(string)) + fmt.Print(getItemsResp.GetPayload()) + + setFields := func(catalogItem *models.CatalogItem, versions []*models.CatalogItemVersion) { + d.SetId(catalogItem.ID.String()) + d.Set("created_at", catalogItem.CreatedAt) + d.Set("created_by", catalogItem.CreatedBy) + d.Set("description", catalogItem.Description) + d.Set("last_updated_at", catalogItem.LastUpdatedAt) + d.Set("last_updated_by", catalogItem.LastUpdatedBy) + d.Set("name", catalogItem.Name) + d.Set("project_ids", catalogItem.ProjectIds) + d.Set("projects", flattenResourceReferences(catalogItem.Projects)) + d.Set("source_id", catalogItem.SourceID.String()) + d.Set("source_name", catalogItem.SourceName) + d.Set("type", flattenResourceReference(catalogItem.Type)) + d.Set("versions", flattenCatalogItemVersions(versions)) + + schemaJson, _ := json.Marshal(catalogItem.Schema) + d.Set("schema", string(schemaJson)) + } + + for _, catalogItem := range getItemsResp.Payload.Content { + if (idOk && catalogItem.ID.String() == id) || (nameOk && *catalogItem.Name == name.(string)) { + getItemResp, err := apiClient.CatalogItems.GetCatalogItemUsingGET1(catalog_items.NewGetCatalogItemUsingGET1Params().WithID(*catalogItem.ID).WithExpandProjects(withBool(expandProjects))) + + if err != nil { + return err + } + + if d.Get("expand_versions").(bool) { + getVersionsResp, err := apiClient.CatalogItems.GetVersionsUsingGET(catalog_items.NewGetVersionsUsingGETParams().WithID(*catalogItem.ID)) + + if err != nil { + return err + } + + setFields(getItemResp.Payload, getVersionsResp.Payload.Content) + } else { + setFields(getItemResp.Payload, nil) + } + + return nil + } + } + + return fmt.Errorf("catalog item %s not found", name) +} diff --git a/vra/data_source_catalog_item_test.go b/vra/data_source_catalog_item_test.go new file mode 100644 index 00000000..12b81dff --- /dev/null +++ b/vra/data_source_catalog_item_test.go @@ -0,0 +1,50 @@ +package vra + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + + "os" + "regexp" + "testing" +) + +func TestAccDataSourceVRACatalogItem(t *testing.T) { + dataSource := "data.vra_catalog_item.this" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckCatalogItem(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceVRACatalogItemNotFound(), + ExpectError: regexp.MustCompile("catalog item foobar not found"), + }, + { + Config: testAccDataSourceVRACatalogItemFound(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSource, "name", os.Getenv("VRA_CATALOG_ITEM_NAME")), + ), + }, + }, + }) +} + +func testAccDataSourceVRACatalogItemBase(catalogItemName string) string { + return fmt.Sprintf(` + data "vra_catalog_item" "this" { + name = "%s" + expand_versions = true + }`, catalogItemName) +} + +func testAccDataSourceVRACatalogItemNotFound() string { + return testAccDataSourceVRACatalogItemBase("foobar") +} + +func testAccDataSourceVRACatalogItemFound() string { + // Need valid catalog item name since this is looking for real catalog item + catalogItemName := os.Getenv("VRA_CATALOG_ITEM_NAME") + return testAccDataSourceVRACatalogItemBase(catalogItemName) +} diff --git a/vra/data_source_deployment.go b/vra/data_source_deployment.go new file mode 100644 index 00000000..0e095c35 --- /dev/null +++ b/vra/data_source_deployment.go @@ -0,0 +1,209 @@ +package vra + +import ( + "fmt" + + "github.com/go-openapi/strfmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/vmware/vra-sdk-go/pkg/client/deployments" + "github.com/vmware/vra-sdk-go/pkg/models" + + "log" +) + +func dataSourceDeployment() *schema.Resource { + return &schema.Resource{ + Read: dataSourceDeploymentRead, + + Schema: map[string]*schema.Schema{ + "blueprint_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "blueprint_version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "blueprint_content": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "catalog_item_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "catalog_item_version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "created_at": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "created_by": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "expand_last_request": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "expand_project": { + Type: schema.TypeBool, + Optional: true, + }, + "expand_resources": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "expense": expenseSchema(), + "id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "inputs": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + //TODO: add last_request + "last_updated_at": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "last_updated_by": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "lease_expire_at": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "project": resourceReferenceSchema(), + "project_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "resources": resourcesSchema(), + "simulated": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "status": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func dataSourceDeploymentRead(d *schema.ResourceData, m interface{}) error { + log.Printf("Reading the vra_deployment resource with name %s", d.Get("name")) + apiClient := m.(*Client).apiClient + + id, idOk := d.GetOk("id") + name, nameOk := d.GetOk("name") + + if !idOk && !nameOk { + return fmt.Errorf("one of id or name must be assigned") + } + + expandLastRequest := d.Get("expand_last_request").(bool) + expandProject := d.Get("expand_project").(bool) + expandResources := d.Get("expand_resources").(bool) + + setFields := func(deployment *models.Deployment) error { + d.SetId(deployment.ID.String()) + d.Set("name", deployment.Name) + d.Set("description", deployment.Description) + d.Set("blueprint_id", deployment.BlueprintID) + d.Set("blueprint_version", deployment.BlueprintVersion) + d.Set("catalog_item_id", deployment.CatalogItemID) + d.Set("catalog_item_version", deployment.CatalogItemVersion) + d.Set("created_at", deployment.CreatedAt) + d.Set("created_by", deployment.CreatedBy) + //TODO: Set last_request + d.Set("last_updated_at", deployment.LastUpdatedAt) + d.Set("last_updated_by", deployment.LastUpdatedBy) + d.Set("lease_expire_at", deployment.LeaseExpireAt) + d.Set("project_id", deployment.ProjectID) + d.Set("simulated", deployment.Simulated) + d.Set("status", deployment.Status) + + if err := d.Set("project", flattenResourceReference(deployment.Project)); err != nil { + return fmt.Errorf("error setting project in deployment - error: %#v", err) + } + + if err := d.Set("resources", flattenResources(deployment.Resources)); err != nil { + return fmt.Errorf("error setting resources in deployment - error: %#v", err) + } + + if err := d.Set("expense", flattenExpense(deployment.Expense)); err != nil { + return fmt.Errorf("error setting deployment expense - error: %#v", err) + } + + if err := d.Set("inputs", expandInputs(deployment.Inputs)); err != nil { + return fmt.Errorf("error setting deployment inputs - error: %#v", err) + } + return nil + } + + if nameOk { + getAllResp, err := apiClient.Deployments.GetDeploymentsUsingGET( + deployments.NewGetDeploymentsUsingGETParams().WithName(withString(name.(string)))) + + if err != nil { + return err + } + + if getAllResp.Payload.NumberOfElements == 1 { + deployment := getAllResp.Payload.Content[0] + id = deployment.ID.String() + } else { + return fmt.Errorf("deployment %s not found", name) + } + } + + // Get the deployment details with all the user provided flags + getResp, err := apiClient.Deployments.GetDeploymentByIDUsingGET( + deployments.NewGetDeploymentByIDUsingGETParams().WithDepID(strfmt.UUID(id.(string))). + WithExpandProject(withBool(expandProject)).WithExpandResources(withBool(expandResources)). + WithExpandLastRequest(withBool(expandLastRequest))) + + if err != nil { + return err + } + + deployment := getResp.Payload + if err = setFields(deployment); err != nil { + return err + } + return nil +} diff --git a/vra/data_source_deployment_test.go b/vra/data_source_deployment_test.go new file mode 100644 index 00000000..752ebd2c --- /dev/null +++ b/vra/data_source_deployment_test.go @@ -0,0 +1,76 @@ +package vra + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + + "os" + "regexp" + "testing" +) + +func TestAccDataSourceVRADeployment(t *testing.T) { + rInt := acctest.RandInt() + resourceName1 := "vra_deployment.this" + dataSourceName1 := "data.vra_deployment.this" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckDeploymentDataSource(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceVRADeploymentNoneConfig(rInt), + ExpectError: regexp.MustCompile("deployment invalid-name not found"), + }, + { + Config: testAccDataSourceVRADeploymentOneConfig(rInt), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName1, "description", dataSourceName1, "description"), + resource.TestCheckResourceAttrPair(resourceName1, "id", dataSourceName1, "id"), + resource.TestCheckResourceAttrPair(resourceName1, "name", dataSourceName1, "name"), + resource.TestCheckResourceAttrPair(resourceName1, "project_id", dataSourceName1, "project_id"), + resource.TestCheckResourceAttrPair(resourceName1, "catalog_item_id", dataSourceName1, "catalog_item_id"), + ), + }, + }, + }) +} + +func testAccDataSourceVRADeployment(rInt int) string { + // Need valid details since this is creating a real deployment + catalogItemName := os.Getenv("VRA_CATALOG_ITEM_NAME") + projectName := os.Getenv("VRA_PROJECT_NAME") + + return fmt.Sprintf(` + data "vra_project" "this" { + name = "%s" + } + + data "vra_catalog_item" "this" { + name = "%s" + } + + resource "vra_deployment" "this" { + name = "test-deployment-%d" + description = "terraform test deployment" + + catalog_item_id = data.vra_catalog_item.this.id + project_id = data.vra_project.this.id + }`, projectName, catalogItemName, rInt) +} + +func testAccDataSourceVRADeploymentNoneConfig(rInt int) string { + return testAccDataSourceVRADeployment(rInt) + ` + data "vra_deployment" "this" { + name = "invalid-name" + }` +} + +func testAccDataSourceVRADeploymentOneConfig(rInt int) string { + return testAccDataSourceVRADeployment(rInt) + ` + data "vra_deployment" "this" { + name = "${vra_deployment.this.name}" + }` +} diff --git a/vra/expense.go b/vra/expense.go new file mode 100644 index 00000000..af9388ef --- /dev/null +++ b/vra/expense.go @@ -0,0 +1,83 @@ +package vra + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/vmware/vra-sdk-go/pkg/models" +) + +// expenseSchema returns the schema to use for the expense property +func expenseSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "additional_expense": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + Computed: true, + }, + "code": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "compute_expense": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + Computed: true, + }, + "last_update_time": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "message": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "network_expense": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + Computed: true, + }, + "storage_expense": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + Computed: true, + }, + "total_expense": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + Computed: true, + }, + "unit": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + } +} + +func flattenExpense(expense *models.Expense) []interface{} { + if expense == nil { + return make([]interface{}, 0) + } + + helper := make(map[string]interface{}) + + helper["additional_expense"] = expense.AdditionalExpense + helper["code"] = expense.Code + helper["compute_expense"] = expense.ComputeExpense + helper["last_update_time"] = expense.LastUpdatedTime.String() + helper["message"] = expense.Message + helper["network_expense"] = expense.NetworkExpense + helper["storage_expense"] = expense.StorageExpense + helper["total_expense"] = expense.TotalExpense + helper["unit"] = expense.Unit + + return []interface{}{helper} +} diff --git a/vra/provider.go b/vra/provider.go index c4aa3597..ba95e56a 100644 --- a/vra/provider.go +++ b/vra/provider.go @@ -39,6 +39,7 @@ func Provider() *schema.Provider { }, DataSourcesMap: map[string]*schema.Resource{ + "vra_catalog_item": dataSourceCatalogItem(), "vra_cloud_account_aws": dataSourceCloudAccountAWS(), "vra_cloud_account_azure": dataSourceCloudAccountAzure(), "vra_cloud_account_gcp": dataSourceCloudAccountGCP(), @@ -47,6 +48,7 @@ func Provider() *schema.Provider { "vra_cloud_account_vmc": dataSourceCloudAccountVMC(), "vra_cloud_account_vsphere": dataSourceCloudAccountVsphere(), "vra_data_collector": dataSourceDataCollector(), + "vra_deployment": dataSourceDeployment(), "vra_fabric_network": dataSourceFabricNetwork(), "vra_image": dataSourceImage(), "vra_network": dataSourceNetwork(), @@ -67,6 +69,7 @@ func Provider() *schema.Provider { "vra_cloud_account_nsxv": resourceCloudAccountNSXV(), "vra_cloud_account_vmc": resourceCloudAccountVMC(), "vra_cloud_account_vsphere": resourceCloudAccountVsphere(), + "vra_deployment": resourceDeployment(), "vra_flavor_profile": resourceFlavorProfile(), "vra_image_profile": resourceImageProfile(), "vra_load_balancer": resourceLoadBalancer(), diff --git a/vra/provider_test.go b/vra/provider_test.go index c64a5c6a..15dc374b 100644 --- a/vra/provider_test.go +++ b/vra/provider_test.go @@ -243,6 +243,61 @@ func testAccPreCheckNSXT(t *testing.T) { } } +func testAccPreCheckCatalogItem(t *testing.T) { + if os.Getenv("VRA_REFRESH_TOKEN") == "" && os.Getenv("VRA_ACCESS_TOKEN") == "" { + t.Fatal("VRA_REFRESH_TOKEN or VRA_ACCESS_TOKEN must be set for acceptance tests") + } + + envVars := [...]string{ + "VRA_URL", + "VRA_CATALOG_ITEM_NAME", + } + + for _, name := range envVars { + if v := os.Getenv(name); v == "" { + t.Fatalf("%s must be set for acceptance tests\n", name) + } + } +} + +func testAccPreCheckDeployment(t *testing.T) { + if os.Getenv("VRA_REFRESH_TOKEN") == "" && os.Getenv("VRA_ACCESS_TOKEN") == "" { + t.Fatal("VRA_REFRESH_TOKEN or VRA_ACCESS_TOKEN must be set for acceptance tests") + } + + envVars := [...]string{ + "VRA_URL", + "VRA_CATALOG_ITEM_NAME", + "VRA_PROJECT_NAME", + "VRA_BLUEPRINT_ID", + "VRA_BLUEPRINT_VERSION", + } + + for _, name := range envVars { + if v := os.Getenv(name); v == "" { + t.Fatalf("%s must be set for acceptance tests\n", name) + } + } +} + +func testAccPreCheckDeploymentDataSource(t *testing.T) { + if os.Getenv("VRA_REFRESH_TOKEN") == "" && os.Getenv("VRA_ACCESS_TOKEN") == "" { + t.Fatal("VRA_REFRESH_TOKEN or VRA_ACCESS_TOKEN must be set for acceptance tests") + } + + envVars := [...]string{ + "VRA_URL", + "VRA_CATALOG_ITEM_NAME", + "VRA_PROJECT_NAME", + } + + for _, name := range envVars { + if v := os.Getenv(name); v == "" { + t.Fatalf("%s must be set for acceptance tests\n", name) + } + } +} + func testAccPreCheckVra(t *testing.T) { if v := os.Getenv("VRA_URL"); v == "" { t.Fatal("VRA_URL must be set for acceptance tests") diff --git a/vra/resource.go b/vra/resource.go new file mode 100644 index 00000000..57fdfea3 --- /dev/null +++ b/vra/resource.go @@ -0,0 +1,142 @@ +package vra + +import ( + "encoding/json" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/vmware/vra-sdk-go/pkg/models" +) + +// resourcesSchema returns the schema to use for the resource property +func resourcesSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "created_at": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "depends_on": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "expense": expenseSchema(), + // TODO: Add metadata + "id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "properties_json": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "state": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "sync_status": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } +} + +func flattenResources(resources []*models.Resource) []map[string]interface{} { + if len(resources) == 0 { + return make([]map[string]interface{}, 0) + } + + configResources := make([]map[string]interface{}, 0, len(resources)) + + for _, value := range resources { + helper := make(map[string]interface{}) + + helper["created_at"] = value.CreatedAt.String() + helper["depends_on"] = value.DependsOn + helper["description"] = value.Description + helper["id"] = value.ID + helper["name"] = value.Name + helper["state"] = value.State + helper["sync_status"] = value.SyncStatus + helper["type"] = value.Type + helper["expense"] = flattenExpense(value.Expense) + + propertiesSlice, _ := json.Marshal(value.Properties) + helper["properties_json"] = string(propertiesSlice) + + configResources = append(configResources, helper) + } + + return configResources +} + +//func expandResources(configResources []interface{}) []*models.Resource { +// resources := make([]*models.Resource, 0, len(configResources)) +// +// for _, configResource := range configResources { +// resourceMap := configResource.(map[string]interface{}) +// +// resource := models.Resource{ +// ID: strfmt.UUID(resourceMap["id"].(string)), +// } +// +// if v, ok := resourceMap["created_at"].(string); ok && v != "" { +// resource.CreatedAt, _ = strfmt.ParseDateTime(v) +// } +// +// if v, ok := resourceMap["depends_on"].([]interface{}); ok && len(v) != 0 { +// dependsOn := make([]string, 0) +// +// for _, value := range v { +// dependsOn = append(dependsOn, value.(string)) +// } +// +// resource.DependsOn = dependsOn +// } +// +// if v, ok := resourceMap["description"].(string); ok && v != "" { +// resource.Description = v +// } +// +// if v, ok := resourceMap["name"].(string); ok && v != "" { +// resource.Name = withString(v) +// } +// +// resource.Properties = expandCustomProperties(resourceMap["properties"].(map[string]interface{})) +// +// if v, ok := resourceMap["state"].(string); ok && v != "" { +// resource.State = v +// } +// +// if v, ok := resourceMap["sync_status"].(string); ok && v != "" { +// resource.SyncStatus = v +// } +// +// if v, ok := resourceMap["type"].(string); ok && v != "" { +// resource.Type = &v +// } +// +// resources = append(resources, &resource) +// } +// +// return resources +//} diff --git a/vra/resource_deployment.go b/vra/resource_deployment.go new file mode 100644 index 00000000..0af3706e --- /dev/null +++ b/vra/resource_deployment.go @@ -0,0 +1,430 @@ +package vra + +import ( + "fmt" + + "github.com/go-openapi/strfmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/vmware/vra-sdk-go/pkg/client" + "github.com/vmware/vra-sdk-go/pkg/client/blueprint_requests" + "github.com/vmware/vra-sdk-go/pkg/client/catalog_items" + "github.com/vmware/vra-sdk-go/pkg/client/deployments" + "github.com/vmware/vra-sdk-go/pkg/models" + + "log" + "reflect" + "time" +) + +func resourceDeployment() *schema.Resource { + return &schema.Resource{ + Create: resourceDeploymentCreate, + Read: resourceDeploymentRead, + Update: resourceDeploymentUpdate, + Delete: resourceDeploymentDelete, + + Schema: map[string]*schema.Schema{ + "blueprint_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "blueprint_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "blueprint_content": { + Type: schema.TypeString, + Optional: true, + }, + "catalog_item_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "catalog_item_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + "created_by": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "expand_last_request": { + Type: schema.TypeBool, + Optional: true, + }, + "expand_project": { + Type: schema.TypeBool, + Optional: true, + }, + "expand_resources": { + Type: schema.TypeBool, + Optional: true, + }, + "expense": expenseSchema(), + "inputs": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + //TODO: add last_request + "last_updated_at": { + Type: schema.TypeString, + Computed: true, + }, + "last_updated_by": { + Type: schema.TypeString, + Computed: true, + }, + "lease_expire_at": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "project": resourceReferenceSchema(), + "project_id": { + Type: schema.TypeString, + Required: true, + }, + "resources": resourcesSchema(), + // TODO: Add plan / simulate feature + "simulated": { + Type: schema.TypeBool, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + } +} + +func resourceDeploymentCreate(d *schema.ResourceData, m interface{}) error { + log.Printf("starting to create vra_deployment resource") + apiClient := m.(*Client).apiClient + + blueprintId, catalogItemId, blueprintContent := "", "", "" + if v, ok := d.GetOk("blueprint_id"); ok { + blueprintId = v.(string) + } + + if v, ok := d.GetOk("catalog_item_id"); ok { + catalogItemId = v.(string) + } + + if v, ok := d.GetOk("blueprint_content"); ok { + blueprintContent = v.(string) + } + + if blueprintId != "" && catalogItemId != "" { + return fmt.Errorf("only one of (blueprint_id, catalog_item_id) required") + } + + deploymentName := d.Get("name").(string) + projectId := d.Get("project_id").(string) + + getResp, err := apiClient.Deployments.CheckDeploymentNameUsingGET(deployments.NewCheckDeploymentNameUsingGETParams().WithName(deploymentName)) + log.Printf("getResp: %v, err: %v", getResp, err) + + if err != nil { + switch err.(type) { + case *deployments.CheckDeploymentNameUsingGETNotFound: + log.Printf("deployment '%v' doesn't exist already and hence can be created", deploymentName) + } + } else { + return fmt.Errorf("a deployment with name '%v' exists already. Try with a differnet name", deploymentName) + } + + // If catalog_item_id is provided, request deployment with the catalog item + if catalogItemId != "" { + log.Printf("requesting vra_deployment '%s' from catalog item", d.Get("name")) + catalogItemRequest := models.CatalogItemRequest{ + DeploymentName: deploymentName, + ProjectID: projectId, + } + + if v, ok := d.GetOk("inputs"); ok { + catalogItemRequest.Inputs = v + } else { + catalogItemRequest.Inputs = make(map[string]interface{}) + } + + if v, ok := d.GetOk("description"); ok { + catalogItemRequest.Reason = v.(string) + } + + if v, ok := d.GetOk("catalog_item_version"); ok { + catalogItemRequest.Version = v.(string) + } + + log.Printf("[DEBUG] create deployment: %#v", catalogItemRequest) + postOk, err := apiClient.CatalogItems.RequestCatalogItemUsingPOST( + catalog_items.NewRequestCatalogItemUsingPOSTParams().WithID(strfmt.UUID(catalogItemId)). + WithRequest(&catalogItemRequest)) + + if err != nil { + return err + } + + d.SetId(postOk.GetPayload().DeploymentID) + log.Printf("finished requesting vra_deployment '%s' from catalog item", d.Get("name")) + } else { + blueprintRequest := models.BlueprintRequest{ + DeploymentName: deploymentName, + ProjectID: projectId, + } + + if blueprintId != "" { + blueprintRequest.BlueprintID = strfmt.UUID(blueprintId) + } else { + // Create empty content in the blueprint + blueprintRequest.Content = " " + } + + if v, ok := d.GetOk("blueprint_version"); ok { + blueprintRequest.BlueprintVersion = v.(string) + } + + if blueprintContent != "" { + blueprintRequest.Content = blueprintContent + } + + if v, ok := d.GetOk("description"); ok { + blueprintRequest.Description = v.(string) + } + + if v, ok := d.GetOk("inputs"); ok { + blueprintRequest.Inputs = v + } else { + blueprintRequest.Inputs = make(map[string]interface{}) + } + + bpRequestCreated, bpRequestAccepted, err := apiClient.BlueprintRequests.CreateBlueprintRequestUsingPOST1( + blueprint_requests.NewCreateBlueprintRequestUsingPOST1Params().WithRequest(&blueprintRequest)) + + if err != nil { + log.Printf("received error. err=%s, bpRequestCreated=%v, bpRequestAccepted=%v", err, bpRequestCreated, bpRequestAccepted) + return err + } + + // blueprint_requests service may return either 201 or 202 depending on whether the request is in terminal state vs or in-progress + log.Printf("requested deployment from blueprint. bpRequestCreated=%v, bpRequestAccepted=%v", bpRequestCreated, bpRequestAccepted) + deploymentId, status, failureMessage := "", "", "" + var bpRequest *models.BlueprintRequest + if bpRequestAccepted != nil { + bpRequest = bpRequestAccepted.GetPayload() + } else { + bpRequest = bpRequestCreated.GetPayload() + } + + if bpRequest != nil { + deploymentId = bpRequest.DeploymentID + status = bpRequest.Status + failureMessage = bpRequest.FailureMessage + } + + if deploymentId != "" { + d.SetId(deploymentId) + } else { + return fmt.Errorf("failed to request for a deployment. status: %v, message: %v", status, failureMessage) + } + + log.Printf("finished requesting vra_deployment '%s' from blueprint %s", d.Get("name"), blueprintId) + } + + stateChangeFunc := resource.StateChangeConf{ + Delay: 5 * time.Second, + Pending: []string{models.DeploymentStatusCREATEINPROGRESS}, + Refresh: deploymentCreateStatusRefreshFunc(*apiClient, d.Id()), + Target: []string{models.DeploymentStatusCREATESUCCESSFUL}, + Timeout: d.Timeout(schema.TimeoutCreate), + MinTimeout: 5 * time.Second, + } + + deploymentId, err := stateChangeFunc.WaitForState() + if err != nil { + return err + } + + d.SetId(deploymentId.(string)) + log.Printf("finished to create vra_deployment resource with name %s", d.Get("name")) + + return resourceDeploymentRead(d, m) +} + +func deploymentCreateStatusRefreshFunc(apiClient client.MulticloudIaaS, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + ret, err := apiClient.Deployments.GetDeploymentByIDUsingGET( + deployments.NewGetDeploymentByIDUsingGETParams().WithDepID(strfmt.UUID(id))) + if err != nil { + return "", models.DeploymentStatusCREATEFAILED, err + } + + status := ret.Payload.Status + switch status { + case models.DeploymentStatusCREATEFAILED: + return []string{""}, status, fmt.Errorf(ret.Error()) + case models.DeploymentStatusCREATEINPROGRESS: + return [...]string{id}, status, nil + case models.DeploymentStatusCREATESUCCESSFUL: + deploymentId := ret.Payload.ID + return deploymentId.String(), status, nil + default: + return [...]string{id}, ret.Error(), fmt.Errorf("deploymentCreateStatusRefreshFunc: unknown status %v", status) + } + } +} + +func resourceDeploymentRead(d *schema.ResourceData, m interface{}) error { + log.Printf("Reading the vra_deployment resource with name %s", d.Get("name")) + apiClient := m.(*Client).apiClient + + id := d.Id() + expandLastRequest := d.Get("expand_last_request").(bool) + expandProject := d.Get("expand_project").(bool) + expandResources := d.Get("expand_resources").(bool) + + resp, err := apiClient.Deployments.GetDeploymentByIDUsingGET( + deployments.NewGetDeploymentByIDUsingGETParams().WithDepID(strfmt.UUID(id)). + WithExpandResources(withBool(expandResources)).WithExpandLastRequest(withBool(expandLastRequest)). + WithExpandProject(withBool(expandProject))) + if err != nil { + switch err.(type) { + case *deployments.GetDeploymentByIDUsingGETNotFound: + d.SetId("") + return nil + } + return err + } + + deployment := *resp.Payload + d.Set("name", deployment.Name) + d.Set("description", deployment.Description) + d.Set("blueprint_id", deployment.BlueprintID) + d.Set("blueprint_version", deployment.BlueprintVersion) + d.Set("catalog_item_id", deployment.CatalogItemID) + d.Set("catalog_item_version", deployment.CatalogItemVersion) + d.Set("created_at", deployment.CreatedAt) + d.Set("created_by", deployment.CreatedBy) + //TODO: Set last_request + d.Set("last_updated_at", deployment.LastUpdatedAt) + d.Set("last_updated_by", deployment.LastUpdatedBy) + d.Set("lease_expire_at", deployment.LeaseExpireAt) + d.Set("project_id", deployment.ProjectID) + d.Set("simulated", deployment.Simulated) + d.Set("status", deployment.Status) + + if err := d.Set("project", flattenResourceReference(deployment.Project)); err != nil { + return fmt.Errorf("error setting project in deployment - error: %#v", err) + } + + if err := d.Set("resources", flattenResources(deployment.Resources)); err != nil { + return fmt.Errorf("error setting resources in deployment - error: %#v", err) + } + + if err := d.Set("expense", flattenExpense(deployment.Expense)); err != nil { + return fmt.Errorf("error setting deployment expense - error: %#v", err) + } + + if err := d.Set("inputs", expandInputs(deployment.Inputs)); err != nil { + return fmt.Errorf("error setting deployment inputs - error: %#v", err) + } + + log.Printf("finished reading the vra_deployment resource with name %s", d.Get("name")) + return nil +} + +func resourceDeploymentUpdate(d *schema.ResourceData, m interface{}) error { + log.Printf("starting to update the vra_deployment resource with name %s", d.Get("name")) + apiClient := m.(*Client).apiClient + + id := d.Id() + description := d.Get("description").(string) + name := d.Get("name").(string) + + updateDeploymentSpecification := models.DeploymentUpdate{ + Description: description, + Name: name, + } + + log.Printf("[DEBUG] update deployment: %#v", updateDeploymentSpecification) + _, err := apiClient.Deployments.PatchDeploymentUsingPATCH(deployments.NewPatchDeploymentUsingPATCHParams().WithDepID(strfmt.UUID(id)).WithUpdate(&updateDeploymentSpecification)) + if err != nil { + return err + } + + log.Printf("finished updating the vra_deployment resource with name %s", d.Get("name")) + return resourceDeploymentRead(d, m) +} + +func resourceDeploymentDelete(d *schema.ResourceData, m interface{}) error { + log.Printf("starting to delete the vra_deployment resource with name %s", d.Get("name")) + apiClient := m.(*Client).apiClient + + id := d.Id() + _, err := apiClient.Deployments.DeleteDeploymentUsingDELETE(deployments.NewDeleteDeploymentUsingDELETEParams().WithDepID(strfmt.UUID(id))) + if err != nil { + return err + } + + log.Printf("requested for deleting the vra_deployment resource with name %s", d.Get("name")) + + stateChangeFunc := resource.StateChangeConf{ + Delay: 5 * time.Second, + Pending: []string{reflect.TypeOf((*deployments.GetDeploymentByIDUsingGETOK)(nil)).String()}, + Refresh: deploymentDeleteStatusRefreshFunc(*apiClient, d.Id()), + Target: []string{reflect.TypeOf((*deployments.GetDeploymentByIDUsingGETNotFound)(nil)).String()}, + Timeout: d.Timeout(schema.TimeoutDelete), + MinTimeout: 5 * time.Second, + } + + _, err = stateChangeFunc.WaitForState() + if err != nil { + return err + } + + d.SetId("") + log.Printf("finished deleting the vra_deployment resource with name %s", d.Get("name")) + return nil +} + +func deploymentDeleteStatusRefreshFunc(apiClient client.MulticloudIaaS, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + ret, err := apiClient.Deployments.GetDeploymentByIDUsingGET( + deployments.NewGetDeploymentByIDUsingGETParams().WithDepID(strfmt.UUID(id))) + if err != nil { + switch err.(type) { + case *deployments.GetDeploymentByIDUsingGETNotFound: + return "", reflect.TypeOf(err).String(), nil + default: + return [...]string{id}, reflect.TypeOf(err).String(), fmt.Errorf(ret.Error()) + } + } + + return [...]string{id}, reflect.TypeOf(ret).String(), nil + } +} diff --git a/vra/resource_deployment_test.go b/vra/resource_deployment_test.go new file mode 100644 index 00000000..4c15a4b2 --- /dev/null +++ b/vra/resource_deployment_test.go @@ -0,0 +1,193 @@ +package vra + +import ( + "fmt" + + "github.com/go-openapi/strfmt" + "github.com/vmware/vra-sdk-go/pkg/client/deployments" + + "os" + "regexp" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccVRADeployment_CatalogItem(t *testing.T) { + rInt := acctest.RandInt() + resource1 := "vra_deployment.this" + project := "data.vra_project.this" + catalogItem := "data.vra_catalog_item.this" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckDeployment(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVRADeploymentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckVRADeploymentCatalogItemConfig(rInt), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVRADeploymentExists(resource1), + resource.TestMatchResourceAttr(resource1, "name", regexp.MustCompile("^test-deployment-"+strconv.Itoa(rInt))), + resource.TestCheckResourceAttrPair(resource1, "project_id", project, "id"), + resource.TestCheckResourceAttrPair(resource1, "catalog_item_id", catalogItem, "id"), + ), + }, + }, + }) +} + +func TestAccVRADeployment_Empty(t *testing.T) { + rInt := acctest.RandInt() + resource1 := "vra_deployment.this" + project := "data.vra_project.this" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckDeployment(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVRADeploymentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckVRADeploymentEmptyConfig(rInt), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVRADeploymentExists(resource1), + resource.TestMatchResourceAttr(resource1, "name", regexp.MustCompile("^test-deployment-"+strconv.Itoa(rInt))), + resource.TestCheckResourceAttrPair(resource1, "project_id", project, "id"), + ), + }, + { + Config: testAccCheckVRADeploymentDuplicateConfig(rInt), + ExpectError: regexp.MustCompile(fmt.Sprintf("a deployment with name '%v' exists already. Try with a differnet name", "test-deployment-"+strconv.Itoa(rInt))), + }, + }, + }) +} + +func TestAccVRADeployment_Blueprint(t *testing.T) { + rInt := acctest.RandInt() + resource1 := "vra_deployment.this" + project := "data.vra_project.this" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckDeployment(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVRADeploymentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckVRADeploymentBlueprintConfig(rInt), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVRADeploymentExists(resource1), + resource.TestMatchResourceAttr(resource1, "name", regexp.MustCompile("^test-deployment-"+strconv.Itoa(rInt))), + resource.TestCheckResourceAttrPair(resource1, "project_id", project, "id"), + ), + }, + }, + }) +} + +func testAccCheckVRADeploymentExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no deployment ID is set") + } + + return nil + } +} + +func testAccCheckVRADeploymentDestroy(s *terraform.State) error { + apiClient := testAccProviderVRA.Meta().(*Client).apiClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vra_deployment" { + continue + } + + _, err := apiClient.Deployments.GetDeploymentByIDUsingGET(deployments.NewGetDeploymentByIDUsingGETParams().WithDepID(strfmt.UUID(rs.Primary.ID))) + if err == nil { + return fmt.Errorf("resource 'vra_deployment' still exists with id %s", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckVRADeploymentCatalogItemConfig(rInt int) string { + // Need valid details since this is creating a real deployment + catalogItemName := os.Getenv("VRA_CATALOG_ITEM_NAME") + projectName := os.Getenv("VRA_PROJECT_NAME") + + return fmt.Sprintf(` + data "vra_project" "this" { + name = "%s" + } + + data "vra_catalog_item" "this" { + name = "%s" + } + + resource "vra_deployment" "this" { + name = "test-deployment-%d" + description = "terraform test deployment" + + catalog_item_id = data.vra_catalog_item.this.id + project_id = data.vra_project.this.id + }`, projectName, catalogItemName, rInt) +} + +func testAccCheckVRADeploymentEmptyConfig(rInt int) string { + // Need valid details since this is creating a real deployment without a catalog item and blueprint + projectName := os.Getenv("VRA_PROJECT_NAME") + + return fmt.Sprintf(` + data "vra_project" "this" { + name = "%s" + } + + resource "vra_deployment" "this" { + name = "test-deployment-%d" + description = "terraform test deployment" + + project_id = data.vra_project.this.id + }`, projectName, rInt) +} + +func testAccCheckVRADeploymentDuplicateConfig(rInt int) string { + // Need valid details since this is creating a real deployment without a catalog item and blueprint + return testAccCheckVRADeploymentEmptyConfig(rInt) + fmt.Sprintf(` + resource "vra_deployment" "duplicate" { + name = "test-deployment-%d" + description = "terraform test deployment" + + project_id = data.vra_project.this.id + }`, rInt) +} + +func testAccCheckVRADeploymentBlueprintConfig(rInt int) string { + // Need valid details since this is creating a real deployment + blueprintId := os.Getenv("VRA_BLUEPRINT_ID") + blueprintVersion := os.Getenv("VRA_BLUEPRINT_VERSION") + projectName := os.Getenv("VRA_PROJECT_NAME") + + return fmt.Sprintf(` + data "vra_project" "this" { + name = "%s" + } + + resource "vra_deployment" "this" { + name = "test-deployment-%d" + description = "terraform test deployment" + + blueprint_id = "%s" + blueprint_version = "%s" + project_id = data.vra_project.this.id + }`, projectName, rInt, blueprintId, blueprintVersion) +} diff --git a/vra/resource_reference.go b/vra/resource_reference.go new file mode 100644 index 00000000..42989549 --- /dev/null +++ b/vra/resource_reference.go @@ -0,0 +1,61 @@ +package vra + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/vmware/vra-sdk-go/pkg/models" +) + +// resourceReferenceSchema returns the schema to use for the catalog item type property +func resourceReferenceSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + }, + "link": { + Type: schema.TypeString, + Optional: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } +} + +func flattenResourceReferences(resourceReferences []*models.ResourceReference) []map[string]interface{} { + if len(resourceReferences) == 0 { + return make([]map[string]interface{}, 0) + } + + resourceRefs := make([]map[string]interface{}, 0, len(resourceReferences)) + + for _, resourceRef := range resourceReferences { + helper := make(map[string]interface{}) + helper["id"] = resourceRef.ID + helper["link"] = resourceRef.Link + helper["name"] = resourceRef.Name + + resourceRefs = append(resourceRefs, helper) + } + + return resourceRefs +} + +func flattenResourceReference(resourceReference *models.ResourceReference) []interface{} { + if resourceReference == nil { + return make([]interface{}, 0) + } + helper := make(map[string]interface{}) + helper["id"] = resourceReference.ID + helper["link"] = resourceReference.Link + helper["name"] = resourceReference.Name + + return []interface{}{helper} +} diff --git a/vra/structure.go b/vra/structure.go index 7edef581..40b0ceac 100644 --- a/vra/structure.go +++ b/vra/structure.go @@ -38,7 +38,7 @@ func flattenStringList(list []*string) []interface{} { } */ -// compareUnique will determin if all of the items passed in are unique +// compareUnique will determine if all of the items passed in are unique func compareUnique(s []interface{}) bool { seen := make(map[string]struct{}, len(s)) j := 0 @@ -138,3 +138,16 @@ func flattenAndNormalizeCLoudAccountGcpRegionIds(regionOrder []string, cloudAcco } return m, nil } + +// expandInputs will convert the interface into a map of strings +func expandInputs(configInputs interface{}) map[string]string { + inputs := make(map[string]string) + + for key, value := range configInputs.(map[string]interface{}) { + if value != nil { + inputs[key] = value.(string) + } + } + + return inputs +}