diff --git a/.changes/v3.13.0/1251-features.md b/.changes/v3.13.0/1251-features.md new file mode 100644 index 000000000..db6e9b46a --- /dev/null +++ b/.changes/v3.13.0/1251-features.md @@ -0,0 +1,2 @@ +* **New Resource:** `vcd_solution_landing_zone` to manage Solution Add-On Landing Zone [GH-1251] +* **New Data Source:** `vcd_solution_landing_zone` to read Solution Add-On Landing Zone [GH-1251] diff --git a/go.mod b/go.mod index 0d4a355f8..34610f389 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0 github.com/kr/pretty v0.2.1 - github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.2 + github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.3 ) require ( diff --git a/go.sum b/go.sum index 17495e129..e1b67331e 100644 --- a/go.sum +++ b/go.sum @@ -142,8 +142,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.2 h1:vNgH4Aare66pe/LZxuwHCxAUHsFmpGKN5fD0XvwgQ/0= -github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.2/go.mod h1:buylrFJrDJqZlqDQJrR5YS585pzYN+vPLY2a2k4XpLk= +github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.3 h1:aohdateSUX2kuExNh7QPIe//0sTkCrAXj/E9y7pSYKA= +github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.3/go.mod h1:NtFFjio08SkxTQH4j1U/SA8fNrh1tLGOhQC7lLNed+w= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/vcd/datasource_not_found_test.go b/vcd/datasource_not_found_test.go index b2c743102..cd430ad23 100644 --- a/vcd/datasource_not_found_test.go +++ b/vcd/datasource_not_found_test.go @@ -78,7 +78,11 @@ func testSpecificDataSourceNotFound(dataSourceName string, vcdClient *VCDClient) }, { skipVersionConstraint: "< 37.1", - datasourceName: "vcd_ip_space_custom_quota", + datasourceName: "vcd_ip_space", + }, + { + skipVersionConstraint: "< 37.1", + datasourceName: "vcd_solution_landing_zone", }, { skipVersionConstraint: "< 37.1", diff --git a/vcd/datasource_vcd_solution_landing_zone.go b/vcd/datasource_vcd_solution_landing_zone.go new file mode 100644 index 000000000..921a0954f --- /dev/null +++ b/vcd/datasource_vcd_solution_landing_zone.go @@ -0,0 +1,143 @@ +package vcd + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dsSlzChildComponent(title string) *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Description: fmt.Sprintf("Details of %s element", title), + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("ID of %s", title), + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Name of %s", title), + }, + "is_default": { + Type: schema.TypeBool, + Computed: true, + Description: fmt.Sprintf("Boolean value that marks if this %s should be default", title), + }, + "capabilities": { + Type: schema.TypeSet, + Computed: true, + Description: fmt.Sprintf("Set of capabilities for %s", title), + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + } +} + +func datasourceVcdSolutionLandingZone() *schema.Resource { + return &schema.Resource{ + ReadContext: datasourceVcdSolutionLandingZoneRead, + + Schema: map[string]*schema.Schema{ + "org": { + Type: schema.TypeString, + Optional: true, + Description: "The name of organization to use, optional if defined at provider " + + "level. Useful when connected as sysadmin working across different organizations", + }, + "state": { + Type: schema.TypeString, + Description: "State reports RDE state", + Computed: true, + }, + "catalog": { + Type: schema.TypeSet, + Computed: true, + Description: "Catalog definition for storing executable .ISO files", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of catalog", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "Catalog Name", + }, + "capabilities": { + Type: schema.TypeSet, + Computed: true, + Description: "Capability set for catalog", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + + "vdc": { + Type: schema.TypeSet, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "VDC ID", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "VDC Name", + }, + "is_default": { + Type: schema.TypeBool, + Computed: true, + Description: "Defines if this VDC should be treated as the default one", + }, + "capabilities": { + Type: schema.TypeSet, + Computed: true, + Description: "Set of capabilities of the VDC", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "org_vdc_network": dsSlzChildComponent("Org VDC Network"), + "storage_policy": dsSlzChildComponent("Storage Policy"), + "compute_policy": dsSlzChildComponent("Compute Policy"), + }, + }, + }, + }, + } +} + +func datasourceVcdSolutionLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + if vcdClient.Client.APIVCDMaxVersionIs("< 37.1") { + return diag.Errorf("Solution Landing Zones are supported in VCD 10.4.1+") + } + + slz, err := vcdClient.GetExactlyOneSolutionLandingZone() + if err != nil { + return diag.Errorf("error retrieving Solution Landing Zone: %s", err) + } + + err = setSlzData(d, slz) + if err != nil { + return diag.Errorf("error storing data to schema: %s", err) + } + + // The real ID of Solution Landing Zone is RDE ID + d.SetId(slz.RdeId()) + + return nil +} diff --git a/vcd/provider.go b/vcd/provider.go index 021595d28..9de1bb676 100644 --- a/vcd/provider.go +++ b/vcd/provider.go @@ -155,6 +155,7 @@ var globalDataSourceMap = map[string]*schema.Resource{ "vcd_vm_vgpu_policy": datasourceVcdVmVgpuPolicy(), // 3.11 "vcd_cse_kubernetes_cluster": datasourceVcdCseKubernetesCluster(), // 3.12 "vcd_version": datasourceVcdVersion(), // 3.12 + "vcd_solution_landing_zone": datasourceVcdSolutionLandingZone(), // 3.13 } var globalResourceMap = map[string]*schema.Resource{ @@ -265,6 +266,7 @@ var globalResourceMap = map[string]*schema.Resource{ "vcd_nsxt_edgegateway_dns": resourceVcdNsxtEdgeGatewayDns(), // 3.11 "vcd_vm_vgpu_policy": resourceVcdVmVgpuPolicy(), // 3.11 "vcd_cse_kubernetes_cluster": resourceVcdCseKubernetesCluster(), // 3.12 + "vcd_solution_landing_zone": resourceVcdSolutionLandingZone(), // 3.13 } // Provider returns a terraform.ResourceProvider. diff --git a/vcd/remove_leftovers_test.go b/vcd/remove_leftovers_test.go index 99835a69d..c1d36621d 100644 --- a/vcd/remove_leftovers_test.go +++ b/vcd/remove_leftovers_test.go @@ -84,6 +84,7 @@ var alsoDelete = entityList{ {Type: "vcd_vapp", Name: "Vapp-AC-2", Comment: "from vcd.TestAccVcdVappAccessControl-update.tf: Vapp-AC-2"}, {Type: "vcd_vapp", Name: "Vapp-AC-3", Comment: "from vcd.TestAccVcdVappAccessControl-update.tf: Vapp-AC-3"}, {Type: "vcd_org_vdc", Name: "ForInternalDiskTest", Comment: "from vcd.TestAccVcdVmInternalDisk-CreateALl.tf: ForInternalDiskTest"}, + {Type: "vcd_solution_landing_zone", Name: "urn:vcloud:type:vmware:solutions_organization:1.0.0", Comment: "Solution Landing Zone"}, } // isTest is a regular expression that tells if an entity needs to be deleted @@ -115,6 +116,23 @@ func removeLeftovers(govcdClient *govcd.VCDClient, verbose bool) error { } } + // -------------------------------------------------------------- + // Solution Landing Zone (SLZ) + // -------------------------------------------------------------- + if govcdClient.Client.IsSysAdmin { + allSlzs, err := govcdClient.GetAllSolutionLandingZones(nil) + if err != nil { + return fmt.Errorf("error retrieving all SLZs: %s", err) + } + for _, slz := range allSlzs { + _ = shouldDeleteEntity(alsoDelete, doNotDelete, slz.DefinedEntity.DefinedEntity.EntityType, "vcd_solution_landing_zone", 0, verbose) + err := slz.Delete() + if err != nil { + return fmt.Errorf("error removing SLZ: %s", err) + } + } + } + // -------------------------------------------------------------- // Provider VDCs // -------------------------------------------------------------- diff --git a/vcd/resource_vcd_solution_landing_zone.go b/vcd/resource_vcd_solution_landing_zone.go new file mode 100644 index 000000000..21c46fbf4 --- /dev/null +++ b/vcd/resource_vcd_solution_landing_zone.go @@ -0,0 +1,410 @@ +package vcd + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/vmware/go-vcloud-director/v2/govcd" + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +func slzChildComponent(title string) *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Description: fmt.Sprintf("Structure for %s", title), + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("ID of %s", title), + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Name of %s", title), + }, + "is_default": { + Type: schema.TypeBool, + Optional: true, + Description: fmt.Sprintf("Boolean value that marks if this %s should be default", title), + }, + // This is a future reserved field that is not effective at the moment + "capabilities": { + Type: schema.TypeSet, + Optional: true, + Description: fmt.Sprintf("Set of capabilities for %s", title), + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + } +} + +func resourceVcdSolutionLandingZone() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceVcdSolutionLandingZoneCreate, + ReadContext: resourceVcdSolutionLandingZoneRead, + UpdateContext: resourceVcdSolutionLandingZoneUpdate, + DeleteContext: resourceVcdSolutionLandingZoneDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceVcdSolutionLandingZoneImport, + }, + + Schema: map[string]*schema.Schema{ + "org": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of organization to use, optional if defined at provider " + + "level. Useful when connected as sysadmin working across different organizations", + }, + + "state": { + Type: schema.TypeString, + Description: "State reports RDE state", + Computed: true, + }, + "catalog": { + Type: schema.TypeSet, + Required: true, + Description: "Catalog definition for storing executable .ISO files", + MaxItems: 1, // Solution add-ons support only single element + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + Description: "ID of catalog", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "Catalog Name", + }, + // This is a future reserved field that is not effective at the moment + "capabilities": { + Type: schema.TypeSet, + Optional: true, + Description: "Capability set for catalog", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "vdc": { + Type: schema.TypeSet, + Required: true, + Description: "VDC definition ", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + Description: "ID of VDC", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "VDC Name", + }, + "is_default": { + Type: schema.TypeBool, + Required: true, + Description: "Defines if the entity should be considered as default", + }, + // This is a future reserved field that is not effective at the moment + "capabilities": { + Type: schema.TypeSet, + Optional: true, + Description: "Capability set for VDC", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "org_vdc_network": slzChildComponent("Org VDC Network"), + "storage_policy": slzChildComponent("Storage Policy"), + "compute_policy": slzChildComponent("Compute Policy"), + }, + }, + }, + }, + } +} + +func resourceVcdSolutionLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + if vcdClient.Client.APIVCDMaxVersionIs("< 37.1") { + return diag.Errorf("Solution Landing Zones are supported in VCD 10.4.1+") + } + + slzCfg, err := getSlzType(vcdClient, d) + if err != nil { + return diag.Errorf("error getting Solution Landing Zone configuration: %s", err) + } + + slz, err := vcdClient.CreateSolutionLandingZone(slzCfg) + if err != nil { + return diag.Errorf("error creating Solution Landing Zone: %s", err) + } + + // The real ID of Solution Landing Zone is RDE ID + d.SetId(slz.DefinedEntity.DefinedEntity.ID) + + return resourceVcdSolutionLandingZoneRead(ctx, d, meta) +} + +func resourceVcdSolutionLandingZoneUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + slz, err := vcdClient.GetSolutionLandingZoneById(d.Id()) + if err != nil { + return diag.Errorf("error retrieving ID: %s", err) + } + + slzCfg, err := getSlzType(vcdClient, d) + if err != nil { + return diag.Errorf("error getting Solution Landing Zone configuration: %s", err) + } + + _, err = slz.Update(slzCfg) + if err != nil { + return diag.Errorf("error updating Solution Landing Zone: %s", err) + } + + return resourceVcdSolutionLandingZoneRead(ctx, d, meta) +} + +func resourceVcdSolutionLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + slz, err := vcdClient.GetSolutionLandingZoneById(d.Id()) + if err != nil { + return diag.Errorf("error retrieving Solution Landing Zone: %s", err) + } + + err = setSlzData(d, slz) + if err != nil { + return diag.Errorf("error storing data to schema: %s", err) + } + + return nil +} + +func resourceVcdSolutionLandingZoneDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + slz, err := vcdClient.GetSolutionLandingZoneById(d.Id()) + if err != nil { + return diag.Errorf("error retrieving ID: %s", err) + } + + err = slz.Delete() + if err != nil { + return diag.Errorf("error deleting Solution Landing Zone RDE: %s", err) + } + + return nil +} + +func resourceVcdSolutionLandingZoneImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + vcdClient := meta.(*VCDClient) + if vcdClient.Client.APIVCDMaxVersionIs("< 37.1") { + return nil, fmt.Errorf("solution landing zones are supported in VCD 10.4.1+") + } + + slz, err := vcdClient.GetExactlyOneSolutionLandingZone() + if err != nil { + return nil, fmt.Errorf("error finding Solution Landing Zone: %s", err) + } + + if slz != nil && slz.SolutionLandingZoneType != nil { + dSet(d, "org", slz.SolutionLandingZoneType.Name) + } + d.SetId(slz.RdeId()) + + return []*schema.ResourceData{d}, nil +} + +func getSlzType(vcdClient *VCDClient, d *schema.ResourceData) (*types.SolutionLandingZoneType, error) { + org, err := vcdClient.GetOrgFromResource(d) + if err != nil { + return nil, fmt.Errorf("error retrieving Org: %s", err) + } + slzCfg := &types.SolutionLandingZoneType{ + Name: org.Org.Name, + ID: org.Org.ID, + Vdcs: make([]types.SolutionLandingZoneVdc, 1), + } + + vdcs := d.Get("vdc").(*schema.Set) + vdcsList := vdcs.List() + + // Construct all VDCs + for vdcIndex, vdc := range vdcsList { + vdcMap := vdc.(map[string]interface{}) + + vdcId := vdcMap["id"].(string) + vdc, err := org.GetVDCById(vdcId, false) + if err != nil { + return nil, fmt.Errorf("error retrieving VDC by ID: %s", err) + } + + slzCfg.Vdcs[vdcIndex] = types.SolutionLandingZoneVdc{ + ID: vdcId, + Name: vdc.Vdc.Name, + Capabilities: convertSchemaSetToSliceOfStrings(vdcMap["capabilities"].(*schema.Set)), + IsDefault: vdcMap["is_default"].(bool), + } + + // Org VDC Networks + orgNetworkNameRetriever := func(id string) (string, error) { + orgNetwork, err := vdc.GetOpenApiOrgVdcNetworkById(id) + if err != nil { + return "", fmt.Errorf("error retrieving Org VDC Network by name: %s", err) + } + return orgNetwork.OpenApiOrgVdcNetwork.Name, nil + } + networks, err := getSlzChildType(vdcMap["org_vdc_network"].(*schema.Set), orgNetworkNameRetriever) + if err != nil { + return nil, fmt.Errorf("error getting child entity type for Org VDC Networks: %s", err) + } + slzCfg.Vdcs[vdcIndex].Networks = networks + + // Storage Policies + storageProfileNameRetriever := func(id string) (string, error) { + storageProfile, err := vcdClient.GetStorageProfileById(id) + if err != nil { + return "", fmt.Errorf("error retrieving storage profile by ID: %s", err) + } + return storageProfile.Name, nil + } + storagePolicies, err := getSlzChildType(vdcMap["storage_policy"].(*schema.Set), storageProfileNameRetriever) + if err != nil { + return nil, fmt.Errorf("error getting child entity type for Storage Policies: %s", err) + } + slzCfg.Vdcs[vdcIndex].StoragePolicies = storagePolicies + + // Compute Policies + computePolicyNameRetriever := func(id string) (string, error) { + computePolicy, err := vcdClient.GetVdcComputePolicyV2ById(id) + if err != nil { + return "", fmt.Errorf("error retrieving compute policy by ID: %S", err) + } + return computePolicy.VdcComputePolicyV2.Name, nil + } + computePolicies, err := getSlzChildType(vdcMap["compute_policy"].(*schema.Set), computePolicyNameRetriever) + if err != nil { + return nil, fmt.Errorf("error getting child entity type for Compute Policies: %s", err) + } + slzCfg.Vdcs[vdcIndex].ComputePolicies = computePolicies + } + + // Construct Catalog list + catalogSet := d.Get("catalog").(*schema.Set) + catalogList := catalogSet.List() + + slzCfg.Catalogs = make([]types.SolutionLandingZoneCatalog, len(catalogList)) + for catalogIndex, catalog := range catalogList { + catalogMap := catalog.(map[string]interface{}) + + catalogId := catalogMap["id"].(string) + catalog, err := org.GetCatalogById(catalogId, false) + if err != nil { + return nil, fmt.Errorf("error retrieving catalog by ID: %s", err) + } + + slzCfg.Catalogs[catalogIndex] = types.SolutionLandingZoneCatalog{ + ID: catalogId, + Name: catalog.Catalog.Name, + Capabilities: convertSchemaSetToSliceOfStrings(catalogMap["capabilities"].(*schema.Set)), + } + + } + + return slzCfg, nil +} + +func getSlzChildType(entrySet *schema.Set, entryNameLookupFunc func(string) (string, error)) ([]types.SolutionLandingZoneVdcChild, error) { + entityList := entrySet.List() + + results := make([]types.SolutionLandingZoneVdcChild, len(entityList)) + for entityIndex, entity := range entityList { + entityMap := entity.(map[string]interface{}) + + childEntityId := entityMap["id"].(string) + childEntityName, err := entryNameLookupFunc(childEntityId) // API requires name to be present + if err != nil { + return nil, fmt.Errorf("error retrieving child entity '%s' name: %s", childEntityId, err) + } + + results[entityIndex] = types.SolutionLandingZoneVdcChild{ + ID: childEntityId, + Name: childEntityName, + Capabilities: convertSchemaSetToSliceOfStrings(entityMap["capabilities"].(*schema.Set)), + IsDefault: entityMap["is_default"].(bool), + } + } + return results, nil +} + +func setSlzData(d *schema.ResourceData, slz *govcd.SolutionLandingZone) error { + dSet(d, "state", slz.DefinedEntity.DefinedEntity.State) + + catalogSchema := make([]interface{}, len(slz.SolutionLandingZoneType.Catalogs)) + for catalogIndex, singleCatalog := range slz.SolutionLandingZoneType.Catalogs { + catalogEntry := make(map[string]interface{}) + + catalogEntry["id"] = singleCatalog.ID + catalogEntry["name"] = singleCatalog.Name + catalogEntry["capabilities"] = convertStringsToTypeSet(singleCatalog.Capabilities) + + catalogSchema[catalogIndex] = catalogEntry + } + + err := d.Set("catalog", catalogSchema) + if err != nil { + return fmt.Errorf("error storing 'catalog' to schema: %s", err) + } + + vdcSchema := make([]interface{}, len(slz.SolutionLandingZoneType.Vdcs)) + for vdcIndex, singleVdc := range slz.SolutionLandingZoneType.Vdcs { + vdcEntry := make(map[string]interface{}) + + vdcEntry["id"] = singleVdc.ID + vdcEntry["name"] = singleVdc.Name + vdcEntry["is_default"] = singleVdc.IsDefault + vdcEntry["capabilities"] = convertStringsToTypeSet(singleVdc.Capabilities) + + vdcEntry["org_vdc_network"] = setChildData(slz.SolutionLandingZoneType.Vdcs[vdcIndex].Networks) + vdcEntry["storage_policy"] = setChildData(slz.SolutionLandingZoneType.Vdcs[vdcIndex].StoragePolicies) + vdcEntry["compute_policy"] = setChildData(slz.SolutionLandingZoneType.Vdcs[vdcIndex].ComputePolicies) + + vdcSchema[vdcIndex] = vdcEntry + } + + err = d.Set("vdc", vdcSchema) + if err != nil { + return fmt.Errorf("error storing 'vdc' to schema: %s", err) + } + + return nil +} + +func setChildData(children []types.SolutionLandingZoneVdcChild) []interface{} { + allEntries := make([]interface{}, len(children)) + for entityIndex, singleEntity := range children { + singleEntry := make(map[string]interface{}) + + singleEntry["id"] = singleEntity.ID + singleEntry["name"] = singleEntity.Name + singleEntry["is_default"] = singleEntity.IsDefault + singleEntry["capabilities"] = convertStringsToTypeSet(singleEntity.Capabilities) + + allEntries[entityIndex] = singleEntry + } + + return allEntries +} diff --git a/vcd/resource_vcd_solution_landing_zone_test.go b/vcd/resource_vcd_solution_landing_zone_test.go new file mode 100644 index 000000000..77c91dc9f --- /dev/null +++ b/vcd/resource_vcd_solution_landing_zone_test.go @@ -0,0 +1,221 @@ +//go:build network || nsxt || vm || ALL || functional + +package vcd + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccVcdSolutionLandingZone(t *testing.T) { + preTestChecks(t) + if checkVersion(testConfig.Provider.ApiVersion, "< 37.1") { + t.Skipf("Solution Landing Zones are supported in VCD 10.4.1+. Skipping") + } + + var params = StringMap{ + "Org": testConfig.VCD.Org, + "VdcName": testConfig.Nsxt.Vdc, + + "TestName": t.Name(), + "CatalogName": testConfig.VCD.Catalog.NsxtBackedCatalogName, + "RoutedNetworkName": testConfig.Nsxt.RoutedNetwork, + "IsolatedNetworkName": testConfig.Nsxt.IsolatedNetwork, + } + testParamsNotEmpty(t, params) + + configTextStep1 := templateFill(testAccSolutionLandingZoneStep1, params) + + params["FuncName"] = t.Name() + "-step2" + configTextStep2 := templateFill(testAccSolutionLandingZoneStep2, params) + + params["FuncName"] = t.Name() + "-step3DS" + configTextStep3DS := templateFill(testAccSolutionLandingZoneStep3DS, params) + + debugPrintf("#[DEBUG] CONFIGURATION step 1: %s\n", configTextStep1) + debugPrintf("#[DEBUG] CONFIGURATION step 2: %s\n", configTextStep2) + debugPrintf("#[DEBUG] CONFIGURATION step 3: %s\n", configTextStep3DS) + if vcdShortTest { + t.Skip(acceptanceTestsSkipped) + return + } + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckSlzDestroy(), + Steps: []resource.TestStep{ + { + Config: configTextStep1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vcd_solution_landing_zone.slz", "org", testConfig.VCD.Org), + resource.TestCheckResourceAttrSet("vcd_solution_landing_zone.slz", "id"), + resource.TestCheckResourceAttr("vcd_solution_landing_zone.slz", "state", "RESOLVED"), + resource.TestCheckResourceAttr("vcd_solution_landing_zone.slz", "catalog.#", "1"), + + resource.TestCheckTypeSetElemNestedAttrs("vcd_solution_landing_zone.slz", "catalog.*", map[string]string{"name": testConfig.VCD.Catalog.NsxtBackedCatalogName}), + resource.TestCheckTypeSetElemNestedAttrs("vcd_solution_landing_zone.slz", "vdc.*.compute_policy.*", map[string]string{"name": "System Default", "is_default": "true"}), + resource.TestCheckTypeSetElemNestedAttrs("vcd_solution_landing_zone.slz", "vdc.*.org_vdc_network.*", map[string]string{"name": testConfig.Nsxt.RoutedNetwork, "is_default": "true"}), + resource.TestCheckTypeSetElemNestedAttrs("vcd_solution_landing_zone.slz", "vdc.*.storage_policy.*", map[string]string{"name": "*", "is_default": "true"}), + resource.TestCheckResourceAttr("vcd_solution_landing_zone.slz", "vdc.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs("vcd_solution_landing_zone.slz", "vdc.*", map[string]string{"is_default": "true", "name": testConfig.Nsxt.Vdc}), + ), + }, + { + Config: configTextStep2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vcd_solution_landing_zone.slz", "org", testConfig.VCD.Org), + resource.TestCheckResourceAttrSet("vcd_solution_landing_zone.slz", "id"), + resource.TestCheckResourceAttr("vcd_solution_landing_zone.slz", "state", "RESOLVED"), + resource.TestCheckResourceAttr("vcd_solution_landing_zone.slz", "catalog.#", "1"), + + resource.TestCheckTypeSetElemNestedAttrs("vcd_solution_landing_zone.slz", "catalog.*", map[string]string{"name": testConfig.VCD.Catalog.NsxtBackedCatalogName}), + resource.TestCheckTypeSetElemNestedAttrs("vcd_solution_landing_zone.slz", "vdc.*.compute_policy.*", map[string]string{"name": "System Default", "is_default": "false"}), + resource.TestCheckTypeSetElemNestedAttrs("vcd_solution_landing_zone.slz", "vdc.*.org_vdc_network.*", map[string]string{"name": testConfig.Nsxt.IsolatedNetwork, "is_default": "false"}), + resource.TestCheckTypeSetElemNestedAttrs("vcd_solution_landing_zone.slz", "vdc.*.storage_policy.*", map[string]string{"name": "*", "is_default": "false"}), + resource.TestCheckResourceAttr("vcd_solution_landing_zone.slz", "vdc.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs("vcd_solution_landing_zone.slz", "vdc.*", map[string]string{"is_default": "true", "name": testConfig.Nsxt.Vdc}), + ), + }, + { + Config: configTextStep3DS, + Check: resource.ComposeTestCheckFunc( + resourceFieldsEqual("data.vcd_solution_landing_zone.slz", "vcd_solution_landing_zone.slz", nil), + ), + }, + { + ResourceName: "vcd_solution_landing_zone.slz", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + postTestChecks(t) +} + +const testAccSolutionLandingZoneStep1 = ` +data "vcd_catalog" "nsxt" { + org = "{{.Org}}" + name = "{{.CatalogName}}" +} + +data "vcd_org_vdc" "vdc1" { + org = "{{.Org}}" + name = "{{.VdcName}}" +} + +data "vcd_network_routed_v2" "r1" { + org = "{{.Org}}" + vdc = "{{.VdcName}}" + name = "{{.RoutedNetworkName}}" +} + +data "vcd_storage_profile" "sp" { + org = "{{.Org}}" + vdc = "{{.VdcName}}" + name = "*" +} + +resource "vcd_solution_landing_zone" "slz" { + org = "{{.Org}}" + + catalog { + id = data.vcd_catalog.nsxt.id + } + + vdc { + id = data.vcd_org_vdc.vdc1.id + is_default = true + + org_vdc_network { + id = data.vcd_network_routed_v2.r1.id + is_default = true + } + + compute_policy { + id = data.vcd_org_vdc.vdc1.default_compute_policy_id + is_default = true + } + + storage_policy { + id = data.vcd_storage_profile.sp.id + is_default = true + } + } +} +` + +const testAccSolutionLandingZoneStep2 = ` +data "vcd_catalog" "nsxt" { + org = "{{.Org}}" + name = "{{.CatalogName}}" +} + +data "vcd_org_vdc" "vdc1" { + org = "{{.Org}}" + name = "{{.VdcName}}" +} + +data "vcd_network_routed_v2" "r1" { + org = "{{.Org}}" + vdc = "{{.VdcName}}" + name = "{{.RoutedNetworkName}}" +} + +data "vcd_network_isolated_v2" "i1" { + org = "{{.Org}}" + vdc = "{{.VdcName}}" + name = "{{.IsolatedNetworkName}}" +} + +data "vcd_storage_profile" "sp" { + org = "{{.Org}}" + vdc = "{{.VdcName}}" + name = "*" +} + +resource "vcd_solution_landing_zone" "slz" { + org = "{{.Org}}" + + catalog { + id = data.vcd_catalog.nsxt.id + } + + vdc { + id = data.vcd_org_vdc.vdc1.id + is_default = true + + org_vdc_network { + id = data.vcd_network_isolated_v2.i1.id + } + + compute_policy { + id = data.vcd_org_vdc.vdc1.default_compute_policy_id + } + + storage_policy { + id = data.vcd_storage_profile.sp.id + } + } +} +` + +const testAccSolutionLandingZoneStep3DS = testAccSolutionLandingZoneStep2 + ` +data "vcd_solution_landing_zone" "slz" { + depends_on = [vcd_solution_landing_zone.slz] +} +` + +func testAccCheckSlzDestroy() resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*VCDClient) + slz, err := conn.GetExactlyOneSolutionLandingZone() + if err == nil { + return fmt.Errorf("there is still an RDE for Solution Landing Zone: %s", slz.RdeId()) + } + + return nil + } +} diff --git a/website/docs/d/solution_landing_zone.html.markdown b/website/docs/d/solution_landing_zone.html.markdown new file mode 100644 index 000000000..7c0e49206 --- /dev/null +++ b/website/docs/d/solution_landing_zone.html.markdown @@ -0,0 +1,31 @@ +--- +layout: "vcd" +page_title: "VMware Cloud Director: vcd_solution_landing_zone" +sidebar_current: "docs-vcd-data-source-solution-landing-zone" +description: |- + Provides a data source to read VCD Solution Add-on Landing Zone. +--- + +# vcd\_solution\_landing\_zone + +Supported in provider *v3.13+* and VCD 10.4.1+. + +Provides a data source to read VCD Solution Add-on Landing Zone. + +~> Only `System Administrator` can read this configuration. + +## Example Usage + +```hcl +data "vcd_solution_landing_zone" "slz" {} +``` + +## Argument Reference + +No arguments are required because this is a global configuration for VCD + +## Attribute Reference + +All the attributes defined in +[`vcd_solution_landing_zone`](/providers/vmware/vcd/latest/docs/resources/solution_landing_zone) +resource are available. diff --git a/website/docs/r/solution_landing_zone.html.markdown b/website/docs/r/solution_landing_zone.html.markdown new file mode 100644 index 000000000..6241f9dff --- /dev/null +++ b/website/docs/r/solution_landing_zone.html.markdown @@ -0,0 +1,118 @@ +--- +layout: "vcd" +page_title: "VMware Cloud Director: vcd_solution_landing_zone" +sidebar_current: "docs-vcd-resource-solution-landing-zone" +description: |- + Provides a resource to configure VCD Solution Add-on Landing Zone +--- + +# vcd\_solution\_landing\_zone + +Supported in provider *v3.13+* and VCD 10.4.1+. + +Provides a resource to configure VCD Solution Add-on Landing Zone. + +~> Only `System Administrator` can create this resource and there can *be only one resource per VCD*. + +## Example Solution Landing Zone configuration + +```hcl +data "vcd_catalog" "nsxt" { + org = "datacloud" + name = "cat-datacloud-nsxt-backed" +} + +data "vcd_org_vdc" "vdc1" { + org = "datacloud" + name = "nsxt-vdc-datacloud" +} + +data "vcd_network_routed_v2" "r1" { + org = "datacloud" + vdc = "nsxt-vdc-datacloud" + name = "nsxt-net-datacloud-r" +} + +data "vcd_storage_profile" "sp" { + org = "datacloud" + vdc = "nsxt-vdc-datacloud" + name = "*" +} + +resource "vcd_solution_landing_zone" "slz" { + org = "datacloud" + + catalog { + id = data.vcd_catalog.nsxt.id + } + + vdc { + id = data.vcd_org_vdc.vdc1.id + is_default = true + + org_vdc_network { + id = data.vcd_network_routed_v2.r1.id + is_default = true + } + + compute_policy { + id = data.vcd_org_vdc.vdc1.default_compute_policy_id + is_default = true + } + + storage_policy { + id = data.vcd_storage_profile.sp.id + is_default = true + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `org` - (Required) Destination Organization name for Solution Add-ons +* `catalog` - (Required) This catalog stores all executable .ISO files for solution add-ons. There + can be a single `catalog` element and the required field is `id`. +* `vdc` - (Required) A single [vdc](#vdc) block that defines landing VDC configuration + + +## VDC configuration block + +* `id` - (Required) Destination VDC ID for Solution Add-ons +* `org_vdc_network` - (Required) At least one Org VDC Network is required. See [vdc + child](#vdc-child) block description for possible values. +* `compute_policy` - (Required) At least Compute Policy is required. See [vdc child](#vdc-child) + block description for possible values. +* `storage_policy` - (Required) At least Storage Policy is required. See [vdc child](#vdc-child) + block description for possible values. + + + +## VDC child configuration block + +* `id` - (Required) ID of child entity (Org VDC Network, Compute Policy, Storage Policy) +* `is_default` - (Optional) Defines which of the child entities is default (only one default is + possible) + +## Attribute Reference + +The following attributes are exported on this resource: + +* `state` - reports the state of parent [Runtime Defined + Entity](/providers/vmware/vcd/latest/docs/resources/rde) + +## Importing + +~> The current implementation of Terraform import can only import resources into the state. +It does not generate configuration. [More information.](https://www.terraform.io/docs/import/) + +A single configuration for Solution Landing Zone is present therefore it is imported directly as per +the example below: + +``` +terraform import vcd_solution_landing_zone.imported +``` + +[docs-import]: https://www.terraform.io/docs/import/ \ No newline at end of file diff --git a/website/vcd.erb b/website/vcd.erb index fd20b1a1a..7f02978eb 100644 --- a/website/vcd.erb +++ b/website/vcd.erb @@ -415,6 +415,8 @@