From 52918b443ffedbf4902d23b4fa9c66e4a7ad18b9 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Tue, 23 May 2023 17:10:38 +0200 Subject: [PATCH 01/28] Init Signed-off-by: abarreiro --- go.mod | 4 +- go.sum | 4 +- vcd/datasource_vcd_ui_plugin.go | 72 ++++++++++++ vcd/provider.go | 2 + vcd/resource_vcd_ui_plugin.go | 195 ++++++++++++++++++++++++++++++++ 5 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 vcd/datasource_vcd_ui_plugin.go create mode 100644 vcd/resource_vcd_ui_plugin.go diff --git a/go.mod b/go.mod index ffa2dfe0e..4076beb48 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-hclog v1.4.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.4.8 // indirect @@ -60,3 +60,5 @@ require ( google.golang.org/grpc v1.51.0 // indirect google.golang.org/protobuf v1.28.1 // indirect ) + +replace github.com/vmware/go-vcloud-director/v2 => github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230523123449-3ae07bf954fa diff --git a/go.sum b/go.sum index 1d046c88a..bc72e3f95 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C6 github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230523123449-3ae07bf954fa h1:HyCFxgllkF3kQHBSgAJfjbQ4cOwzK59YTXTCIGTp1JA= +github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230523123449-3ae07bf954fa/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -194,8 +196,6 @@ github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvC github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= -github.com/vmware/go-vcloud-director/v2 v2.20.0 h1:vT1vmh6uxJPE5a4d2nSj8KmF3C0kb/1UF2hn6hp7vr0= -github.com/vmware/go-vcloud-director/v2 v2.20.0/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/vcd/datasource_vcd_ui_plugin.go b/vcd/datasource_vcd_ui_plugin.go new file mode 100644 index 000000000..04bca1588 --- /dev/null +++ b/vcd/datasource_vcd_ui_plugin.go @@ -0,0 +1,72 @@ +package vcd + +import ( + "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func datasourceVcdUIPlugin() *schema.Resource { + return &schema.Resource{ + ReadContext: datasourceVcdUIPluginRead, + Schema: map[string]*schema.Schema{ + "vendor": { + Type: schema.TypeString, + Required: true, + Description: "The UI Plugin vendor name. Combination of `vendor`, `name` and `version` must be unique", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The UI Plugin name. Combination of `vendor`, `name` and `version` must be unique", + }, + "version": { + Type: schema.TypeString, + Required: true, + Description: "The version of the UI Plugin. Combination of `vendor`, `name` and `version` must be unique", + }, + "license": { + Type: schema.TypeString, + Computed: true, + Description: "The license of the UI Plugin", + }, + "link": { + Type: schema.TypeString, + Computed: true, + Description: "The website of the UI Plugin", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "The description of the UI Plugin", + }, + "provider_scoped": { + Type: schema.TypeBool, + Computed: true, + Description: "'true' if the UI Plugin scope is the service provider. 'false' if not", + }, + "tenant_scoped": { + Type: schema.TypeBool, + Computed: true, + Description: "true if the UI Plugin scope is the tenants (organizations)", + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "true if the UI Plugin is enabled. 'false' if not", + }, + "published_tenant_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "Set of Organization IDs where the UI Plugin is published to", + }, + }, + } +} + +func datasourceVcdUIPluginRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return genericVcdUIPluginRead(ctx, d, meta, "datasource") +} diff --git a/vcd/provider.go b/vcd/provider.go index a527886e4..893433086 100644 --- a/vcd/provider.go +++ b/vcd/provider.go @@ -120,6 +120,7 @@ var globalDataSourceMap = map[string]*schema.Resource{ "vcd_nsxt_edgegateway_qos_profile": datasourceVcdNsxtEdgeGatewayQosProfile(), // 3.9 "vcd_nsxt_edgegateway_rate_limiting": datasourceVcdNsxtEdgegatewayRateLimiting(), // 3.9 "vcd_nsxt_network_dhcp_binding": datasourceVcdNsxtDhcpBinding(), // 3.9 + "vcd_ui_plugin": datasourceVcdUIPlugin(), // 3.10 } var globalResourceMap = map[string]*schema.Resource{ @@ -204,6 +205,7 @@ var globalResourceMap = map[string]*schema.Resource{ "vcd_rde": resourceVcdRde(), // 3.9 "vcd_nsxt_edgegateway_rate_limiting": resourceVcdNsxtEdgegatewayRateLimiting(), // 3.9 "vcd_nsxt_network_dhcp_binding": resourceVcdNsxtDhcpBinding(), // 3.9 + "vcd_ui_plugin": resourceVcdUIPlugin(), // 3.10 } // Provider returns a terraform.ResourceProvider. diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go new file mode 100644 index 000000000..5e6176b15 --- /dev/null +++ b/vcd/resource_vcd_ui_plugin.go @@ -0,0 +1,195 @@ +package vcd + +import ( + "context" + "github.com/hashicorp/go-cty/cty" + "github.com/vmware/go-vcloud-director/v2/govcd" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceVcdUIPlugin() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceVcdUIPluginCreate, + ReadContext: resourceVcdUIPluginRead, + UpdateContext: resourceVcdUIPluginUpdate, + DeleteContext: resourceVcdUIPluginDelete, + Schema: map[string]*schema.Schema{ + "plugin_path": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateDiagFunc: func(value interface{}, _ cty.Path) diag.Diagnostics { + valueString, ok := value.(string) + if !ok { + return diag.Errorf("expected type of %v to be string", value) + } + if !strings.HasSuffix(valueString, "zip") && !strings.HasSuffix(valueString, "ZIP") { + return diag.Errorf("the UI Plugin should be a ZIP bundle, but it is %s", valueString) + } + return nil + }, + Description: "Absolute or relative path to the ZIP file containing the UI Plugin", + }, + "enabled": { + Type: schema.TypeBool, + Required: true, + Description: "true to make the UI Plugin enabled. 'false' to make it disabled", + }, + "provider_scoped": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "This value is calculated automatically on create by reading the UI Plugin ZIP file contents. You can update" + + "it to `true` to make it provider scoped or `false` otherwise", + }, + "tenant_scoped": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "This value is calculated automatically on create by reading the UI Plugin ZIP file contents. You can update" + + "it to `true` to make it tenant scoped or `false` otherwise", + }, + "publish_to_all_tenants": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ConflictsWith: []string{"published_tenant_ids"}, + Description: "When `true`, publishes the UI Plugin to all tenants", + }, + "published_tenant_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Computed: true, + ConflictsWith: []string{"publish_to_all_tenants"}, + Description: "Set of organization IDs to which this UI Plugin is published", + }, + "vendor": { + Type: schema.TypeString, + Computed: true, + Description: "The UI Plugin vendor name", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The UI Plugin name", + }, + "version": { + Type: schema.TypeString, + Computed: true, + Description: "The version of the UI Plugin", + }, + "license": { + Type: schema.TypeString, + Computed: true, + Description: "The license of the UI Plugin", + }, + "link": { + Type: schema.TypeString, + Computed: true, + Description: "The website of the UI Plugin", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "The description of the UI Plugin", + }, + }, + } +} + +func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + uiPlugin, err := vcdClient.AddUIPlugin(d.Get("plugin_path").(string), d.Get("enabled").(bool)) + if err != nil { + return diag.Errorf("could not create the UI Plugin: %s", err) + } + + if d.Get("publish_to_all_tenants").(bool) { + err = uiPlugin.PublishAll() + if err != nil { + return diag.Errorf("could not publish the UI Plugin %s to all tenants: %s", uiPlugin.UIPluginMetadata.ID, err) + } + } + orgIdsRaw, isSet := d.GetOk("published_tenant_ids") + if isSet { + orgIds := orgIdsRaw.(*schema.Set).List() + var orgRefs = make(types.OpenApiReferences, len(orgIds)) + for i, orgId := range orgIds { + orgRefs[i] = types.OpenApiReference{ID: orgId.(string)} + } + err = uiPlugin.Publish(orgRefs) + if err != nil { + return diag.Errorf("could not publish the UI Plugin %s to tenants '%v': %s", uiPlugin.UIPluginMetadata.ID, orgIds, err) + } + } + + return resourceVcdUIPluginRead(ctx, d, meta) +} + +func resourceVcdUIPluginRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return genericVcdUIPluginRead(ctx, d, meta, "resource") +} + +func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta interface{}, origin string) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + var uiPlugin *govcd.UIPlugin + var err error + if d.Id() != "" { + uiPlugin, err = vcdClient.GetUIPluginById(d.Id()) + } else { + uiPlugin, err = vcdClient.GetUIPlugin(d.Get("vendor").(string), d.Get("name").(string), d.Get("version").(string)) + } + + if origin == "resource" && govcd.ContainsNotFound(err) { + log.Printf("[DEBUG] UI Plugin no longer exists. Removing from tfstate") + d.SetId("") + return nil + } + if err != nil { + return diag.FromErr(err) + } + + dSet(d, "name", uiPlugin.UIPluginMetadata.PluginName) + dSet(d, "vendor", uiPlugin.UIPluginMetadata.Vendor) + dSet(d, "version", uiPlugin.UIPluginMetadata.Version) + dSet(d, "license", uiPlugin.UIPluginMetadata.License) + dSet(d, "link", uiPlugin.UIPluginMetadata.Link) + dSet(d, "tenant_scoped", uiPlugin.UIPluginMetadata.TenantScoped) + dSet(d, "provider_scoped", uiPlugin.UIPluginMetadata.ProviderScoped) + dSet(d, "enabled", uiPlugin.UIPluginMetadata.Enabled) + dSet(d, "description", uiPlugin.UIPluginMetadata.Description) + + orgRefs, err := uiPlugin.GetPublishedTenants() + if err != nil { + return diag.Errorf("error retrieving the organizations where the plugin with ID '%s'", uiPlugin.UIPluginMetadata.ID, err) + } + var orgIds = make([]string, len(orgRefs)) + for i, orgRef := range orgRefs { + orgIds[i] = orgRef.ID + } + err = d.Set("published_tenant_ids", orgIds) + if err != nil { + return diag.FromErr(err) + } + d.SetId(uiPlugin.UIPluginMetadata.ID) + + return nil +} + +func resourceVcdUIPluginUpdate(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func resourceVcdUIPluginDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} From 3dfc1d289131ff80a90a3b1dd984919f2b73bf67 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Tue, 23 May 2023 17:10:55 +0200 Subject: [PATCH 02/28] Init Signed-off-by: abarreiro --- vcd/resource_vcd_ui_plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index 5e6176b15..67678638a 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -171,7 +171,7 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte orgRefs, err := uiPlugin.GetPublishedTenants() if err != nil { - return diag.Errorf("error retrieving the organizations where the plugin with ID '%s'", uiPlugin.UIPluginMetadata.ID, err) + return diag.Errorf("error retrieving the organizations where the plugin with ID '%s': %s", uiPlugin.UIPluginMetadata.ID, err) } var orgIds = make([]string, len(orgRefs)) for i, orgRef := range orgRefs { From 8c27b4faaa786b00723fe3094ecf22126a0b71e2 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Tue, 23 May 2023 17:13:06 +0200 Subject: [PATCH 03/28] changelog Signed-off-by: abarreiro --- .changes/v3.10.0/1059-features.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changes/v3.10.0/1059-features.md diff --git a/.changes/v3.10.0/1059-features.md b/.changes/v3.10.0/1059-features.md new file mode 100644 index 000000000..9c014b3ad --- /dev/null +++ b/.changes/v3.10.0/1059-features.md @@ -0,0 +1,2 @@ +* **New Resource:** `vcd_ui_plugin` to programmatically install and manage UI Plugins [GH-1059] +* **New Data Source:** `vcd_ui_plugin` to fetch existing UI Plugins [GH-1059] From 0ac45ddfb1ed3dc372d3e6f58b21ad459767cd5e Mon Sep 17 00:00:00 2001 From: abarreiro Date: Tue, 23 May 2023 17:14:36 +0200 Subject: [PATCH 04/28] Add delete Signed-off-by: abarreiro --- vcd/resource_vcd_ui_plugin.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index 67678638a..bc244fe60 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -191,5 +191,29 @@ func resourceVcdUIPluginUpdate(_ context.Context, d *schema.ResourceData, meta i } func resourceVcdUIPluginDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + var uiPlugin *govcd.UIPlugin + var err error + if d.Id() != "" { + uiPlugin, err = vcdClient.GetUIPluginById(d.Id()) + } else { + uiPlugin, err = vcdClient.GetUIPlugin(d.Get("vendor").(string), d.Get("name").(string), d.Get("version").(string)) + } + + if govcd.ContainsNotFound(err) { + log.Printf("[DEBUG] UI Plugin no longer exists. Removing from tfstate") + d.SetId("") + return nil + } + if err != nil { + return diag.FromErr(err) + } + + err = uiPlugin.Delete() + if err != nil { + return diag.FromErr(err) + } + return nil } From 3e61e46a82ee57e2ea598470a7635505162d04c3 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Wed, 24 May 2023 11:36:10 +0200 Subject: [PATCH 05/28] Fixes Signed-off-by: abarreiro --- go.mod | 2 +- go.sum | 4 ++-- vcd/resource_vcd_ui_plugin.go | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 4076beb48..23b7b7847 100644 --- a/go.mod +++ b/go.mod @@ -61,4 +61,4 @@ require ( google.golang.org/protobuf v1.28.1 // indirect ) -replace github.com/vmware/go-vcloud-director/v2 => github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230523123449-3ae07bf954fa +replace github.com/vmware/go-vcloud-director/v2 => github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230524093520-0c2043ce1b0a diff --git a/go.sum b/go.sum index bc72e3f95..7e0f9e4d5 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C6 github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= -github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230523123449-3ae07bf954fa h1:HyCFxgllkF3kQHBSgAJfjbQ4cOwzK59YTXTCIGTp1JA= -github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230523123449-3ae07bf954fa/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw= +github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230524093520-0c2043ce1b0a h1:6YDmeikL9Tp8r2KpgIt9MvaKlILlqs7cSCdwXFlR7vA= +github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230524093520-0c2043ce1b0a/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index bc244fe60..9de4db8d5 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -132,6 +132,9 @@ func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta } } + // We set the ID early so the read function can locate the plugin in VCD, as there's no identifying argument on Create, + // all identifying elements such as vendor, plugin name and version are inside the uploaded ZIP file. + d.SetId(uiPlugin.UIPluginMetadata.ID) return resourceVcdUIPluginRead(ctx, d, meta) } @@ -181,8 +184,8 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte if err != nil { return diag.FromErr(err) } - d.SetId(uiPlugin.UIPluginMetadata.ID) + d.SetId(uiPlugin.UIPluginMetadata.ID) return nil } From 9c2199ea3133a781cb9be6267e49808ca6270ef3 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Wed, 24 May 2023 12:56:11 +0200 Subject: [PATCH 06/28] Checkpoint Signed-off-by: abarreiro --- vcd/resource_vcd_ui_plugin.go | 103 ++++++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 23 deletions(-) diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index 9de4db8d5..be12cd19d 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -2,6 +2,7 @@ package vcd import ( "context" + "fmt" "github.com/hashicorp/go-cty/cty" "github.com/vmware/go-vcloud-director/v2/govcd" "github.com/vmware/go-vcloud-director/v2/types/v56" @@ -69,7 +70,7 @@ func resourceVcdUIPlugin() *schema.Resource { Optional: true, Computed: true, ConflictsWith: []string{"publish_to_all_tenants"}, - Description: "Set of organization IDs to which this UI Plugin is published", + Description: "Set of organization IDs to which this UI Plugin must be published", }, "vendor": { Type: schema.TypeString, @@ -113,38 +114,64 @@ func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta return diag.Errorf("could not create the UI Plugin: %s", err) } - if d.Get("publish_to_all_tenants").(bool) { - err = uiPlugin.PublishAll() + err = publishUIPluginToTenants(vcdClient, uiPlugin, d, "create") + if err != nil { + return diag.FromErr(err) + } + + // We set the ID early so the read function can locate the plugin in VCD, as there's no identifying argument on Create. + // All identifying elements such as vendor, plugin name and version are inside the uploaded ZIP file and populated + // in Terraform state after a Read. + d.SetId(uiPlugin.UIPluginMetadata.ID) + return resourceVcdUIPluginRead(ctx, d, meta) +} + +// publishUIPluginToTenants performs a publish/unpublish operation for the given UI plugin. +func publishUIPluginToTenants(vcdClient *VCDClient, uiPlugin *govcd.UIPlugin, d *schema.ResourceData, operation string) error { + if d.HasChange("publish_to_all_tenants") && d.Get("publish_to_all_tenants").(bool) { + err := uiPlugin.PublishAll() if err != nil { - return diag.Errorf("could not publish the UI Plugin %s to all tenants: %s", uiPlugin.UIPluginMetadata.ID, err) + return fmt.Errorf("could not publish the UI Plugin %s to all tenants: %s", uiPlugin.UIPluginMetadata.ID, err) } + return nil // This return is needed despite this field conflicts with `published_tenant_ids`, as the latter is also computed. } + orgIdsRaw, isSet := d.GetOk("published_tenant_ids") if isSet { orgIds := orgIdsRaw.(*schema.Set).List() - var orgRefs = make(types.OpenApiReferences, len(orgIds)) - for i, orgId := range orgIds { - orgRefs[i] = types.OpenApiReference{ID: orgId.(string)} + orgList, err := vcdClient.GetOrgList() + if err != nil { + return fmt.Errorf("could not publish the UI Plugin %s to tenants '%v': %s", uiPlugin.UIPluginMetadata.ID, orgIds, err) + } + var orgRefs types.OpenApiReferences + for _, org := range orgList.Org { + for _, orgId := range orgIds { + if orgId == org.ID { + orgRefs = append(orgRefs, types.OpenApiReference{ID: org.ID, Name: org.Name}) + } + } + } + if operation == "update" { + err = uiPlugin.UnpublishAll() // We need to clean up the already-published Orgs to put the new ones during an Update. + if err != nil { + return fmt.Errorf("could not publish the UI Plugin %s to tenants '%v': %s", uiPlugin.UIPluginMetadata.ID, orgIds, err) + } } err = uiPlugin.Publish(orgRefs) if err != nil { - return diag.Errorf("could not publish the UI Plugin %s to tenants '%v': %s", uiPlugin.UIPluginMetadata.ID, orgIds, err) + return fmt.Errorf("could not publish the UI Plugin %s to tenants '%v': %s", uiPlugin.UIPluginMetadata.ID, orgIds, err) } } - - // We set the ID early so the read function can locate the plugin in VCD, as there's no identifying argument on Create, - // all identifying elements such as vendor, plugin name and version are inside the uploaded ZIP file. - d.SetId(uiPlugin.UIPluginMetadata.ID) - return resourceVcdUIPluginRead(ctx, d, meta) + return nil } func resourceVcdUIPluginRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { return genericVcdUIPluginRead(ctx, d, meta, "resource") } -func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta interface{}, origin string) diag.Diagnostics { - vcdClient := meta.(*VCDClient) - +// getUIPlugin retrieves the UI Plugin from VCD using the resource/data source information. +// Returns a nil govcd.UIPlugin if it doesn't exist in VCD and origin is a "resource". +func getUIPlugin(vcdClient *VCDClient, d *schema.ResourceData, origin string) (*govcd.UIPlugin, error) { var uiPlugin *govcd.UIPlugin var err error if d.Id() != "" { @@ -156,11 +183,24 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte if origin == "resource" && govcd.ContainsNotFound(err) { log.Printf("[DEBUG] UI Plugin no longer exists. Removing from tfstate") d.SetId("") - return nil + return nil, nil + } + if err != nil { + return nil, err } + return uiPlugin, nil +} + +func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta interface{}, origin string) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + uiPlugin, err := getUIPlugin(vcdClient, d, origin) if err != nil { return diag.FromErr(err) } + if uiPlugin == nil { + return nil + } dSet(d, "name", uiPlugin.UIPluginMetadata.PluginName) dSet(d, "vendor", uiPlugin.UIPluginMetadata.Vendor) @@ -190,18 +230,35 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte } func resourceVcdUIPluginUpdate(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + uiPlugin, err := getUIPlugin(vcdClient, d, "resource") + if err != nil { + return diag.FromErr(err) + } + if uiPlugin == nil { + return nil + } + + err = uiPlugin.Update(d.Get("enabled").(bool), d.Get("provider_scoped").(bool), d.Get("tenant_scoped").(bool)) + if err != nil { + return diag.Errorf("could not update the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) + } + err = publishUIPluginToTenants(vcdClient, uiPlugin, d, "update") + if err != nil { + return diag.Errorf("could not update the published tenants of the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) + } return nil } func resourceVcdUIPluginDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { vcdClient := meta.(*VCDClient) - var uiPlugin *govcd.UIPlugin - var err error - if d.Id() != "" { - uiPlugin, err = vcdClient.GetUIPluginById(d.Id()) - } else { - uiPlugin, err = vcdClient.GetUIPlugin(d.Get("vendor").(string), d.Get("name").(string), d.Get("version").(string)) + uiPlugin, err := getUIPlugin(vcdClient, d, "resource") + if err != nil { + return diag.FromErr(err) + } + if uiPlugin == nil { + return nil } if govcd.ContainsNotFound(err) { From 6601025cdb0a6be6531d0e19ffd65b4d62f31be6 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Thu, 25 May 2023 12:59:41 +0200 Subject: [PATCH 07/28] Fix impl and init tests. Tests pass Signed-off-by: abarreiro --- test-resources/ui_plugin.zip | Bin 0 -> 62153 bytes vcd/resource_vcd_ui_plugin.go | 94 ++++++++++++----------- vcd/resource_vcd_ui_plugin_test.go | 116 +++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 43 deletions(-) create mode 100644 test-resources/ui_plugin.zip create mode 100644 vcd/resource_vcd_ui_plugin_test.go diff --git a/test-resources/ui_plugin.zip b/test-resources/ui_plugin.zip new file mode 100644 index 0000000000000000000000000000000000000000..c98a8eb644e17aa926de7711d55c0b49fa2d670b GIT binary patch literal 62153 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

6wu*-eTHT+%L))@{%A$YUN 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

VF4|~V1R`NKS0~6#dutM zq^+HLaBmy@2#bkHU(JZS_Cv%0f&x?x3i+S@Z?5V%9G_`H9SobD{`dX#b)rIgj;a)O zyMxq;+8&cb4%!U|2-s1^b4#&4+#V+AKu_QM9kFeg8/A5Jc>qXpaeZAtR1kApqp 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 0 { + var orgIds = make([]string, len(orgRefs)) + for i, orgRef := range orgRefs { + orgIds[i] = orgRef.ID + } + err = d.Set("published_tenant_ids", convertStringsToTypeSet(orgIds)) + if err != nil { + return diag.FromErr(err) + } } d.SetId(uiPlugin.UIPluginMetadata.ID) @@ -261,15 +278,6 @@ func resourceVcdUIPluginDelete(_ context.Context, d *schema.ResourceData, meta i return nil } - if govcd.ContainsNotFound(err) { - log.Printf("[DEBUG] UI Plugin no longer exists. Removing from tfstate") - d.SetId("") - return nil - } - if err != nil { - return diag.FromErr(err) - } - err = uiPlugin.Delete() if err != nil { return diag.FromErr(err) diff --git a/vcd/resource_vcd_ui_plugin_test.go b/vcd/resource_vcd_ui_plugin_test.go new file mode 100644 index 000000000..8976532c4 --- /dev/null +++ b/vcd/resource_vcd_ui_plugin_test.go @@ -0,0 +1,116 @@ +//go:build plugin || ALL || functional + +package vcd + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "regexp" + "testing" +) + +func init() { + testingTags["plugin"] = "resource_vcd_ui_plugin_test.go" +} + +func TestAccVcdUiPlugin(t *testing.T) { + + var params = StringMap{ + "Org1": testConfig.VCD.Org, + "Org2": testConfig.Provider.SysOrg, + "Enabled": "true", + "PluginPath": "../test-resources/ui_plugin.zip", + "PublishToAllTenants": "true", + "PublishedTenantIds": " ", + "FuncName": t.Name() + "Step1", + } + testParamsNotEmpty(t, params) + + step1Config := templateFill(testAccVcdUiPluginStep1, params) + params["FuncName"] = t.Name() + "Step2" + params["PublishToAllTenants"] = "false" + params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" + step2Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPluginStep1, params) + + plugin1 := "vcd_ui_plugin.plugin1" + + debugPrintf("#[DEBUG] CONFIGURATION Step 1: %s\n", step1Config) + if vcdShortTest { + t.Skip(acceptanceTestsSkipped) + return + } + + cachedId := &testCachedFieldValue{} + + testCheckResourceCommonUIPluginAsserts := func(resourcePath string) resource.TestCheckFunc { + return resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourcePath, "vendor", "VMware"), + resource.TestCheckResourceAttr(resourcePath, "name", "Test Plugin"), + resource.TestCheckResourceAttr(resourcePath, "version", "1.2.3"), + resource.TestCheckResourceAttr(resourcePath, "license", "BSD-2-Clause"), + resource.TestCheckResourceAttr(resourcePath, "description", "Test Plugin description"), + resource.TestCheckResourceAttr(resourcePath, "link", "http://www.vmware.com"), + ) + } + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckUIPluginDestroy(cachedId.fieldValue), + Steps: []resource.TestStep{ + // Test UI Plugin creation with publish to all tenants and enabled + { + Config: step1Config, + Check: resource.ComposeAggregateTestCheckFunc( + testCheckResourceCommonUIPluginAsserts(plugin1), + resource.TestCheckResourceAttr(plugin1, "enabled", "true"), + resource.TestCheckResourceAttr(plugin1, "publish_to_all_tenants", "true"), + resource.TestMatchResourceAttr(plugin1, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), + ), + }, + // Test UI Plugin creation (we taint it for that) with publish to only specific tenants and enabled + { + Config: step2Config, + Taint: []string{plugin1}, + Check: resource.ComposeAggregateTestCheckFunc( + testCheckResourceCommonUIPluginAsserts(plugin1), + resource.TestCheckResourceAttr(plugin1, "enabled", "true"), + resource.TestCheckResourceAttr(plugin1, "publish_to_all_tenants", "false"), + resource.TestMatchResourceAttr(plugin1, "published_tenant_ids.#", regexp.MustCompile("2")), + ), + }, + }, + }) + postTestChecks(t) +} + +// Test UI Plugin creation with publish to all tenants +const testAccVcdUiPluginStep1 = ` +resource "vcd_ui_plugin" "plugin1" { + plugin_path = "{{.PluginPath}}" + enabled = {{.Enabled}} + publish_to_all_tenants = {{.PublishToAllTenants}} + {{.PublishedTenantIds}} +} +` + +const testAccVcdUiPluginStepOrgs = ` +data "vcd_org" "org1" { + name = "{{.Org1}}" +} + +data "vcd_org" "org2" { + name = "{{.Org2}}" +} +` + +func testAccCheckUIPluginDestroy(id string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*VCDClient) + _, err := conn.GetUIPluginById(id) + if err == nil { + return fmt.Errorf("UI Plugin %s still exists", id) + } + return nil + } +} From 56e22e44b1045ae2553e1ebc6a101fe887c5646c Mon Sep 17 00:00:00 2001 From: abarreiro Date: Thu, 25 May 2023 14:09:17 +0200 Subject: [PATCH 08/28] Add tests Signed-off-by: abarreiro --- vcd/resource_vcd_ui_plugin.go | 34 ++++-- vcd/resource_vcd_ui_plugin_test.go | 168 +++++++++++++++++++++++++---- 2 files changed, 177 insertions(+), 25 deletions(-) diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index 0ffcd24bd..3dc410c32 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -103,7 +103,29 @@ func resourceVcdUIPlugin() *schema.Resource { } } +// validateUIPluginAttributes is an auxiliary validation function that is similar to what ValidateDiagFunc would do, +// but in this case we need to validate one attribute that depends on another. +func validateUIPluginAttributes(d *schema.ResourceData) error { + publishToAllTenants := d.Get("publish_to_all_tenants").(bool) + rawConfig := d.GetRawConfig() + if rawConfig.IsNull() { + return nil + } + attr := rawConfig.GetAttr("published_tenant_ids") + if attr.IsNull() { + return nil + } + if !publishToAllTenants { + return nil + } + return fmt.Errorf("`publish_to_all_tenants` can't be true if `published_tenant_ids` is also set") +} + func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + err := validateUIPluginAttributes(d) + if err != nil { + return diag.FromErr(err) + } vcdClient := meta.(*VCDClient) uiPlugin, err := vcdClient.AddUIPlugin(d.Get("plugin_path").(string), d.Get("enabled").(bool)) @@ -125,12 +147,7 @@ func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta // publishUIPluginToTenants performs a publish/unpublish operation for the given UI plugin. func publishUIPluginToTenants(vcdClient *VCDClient, uiPlugin *govcd.UIPlugin, d *schema.ResourceData, operation string) error { - publishToAllTenants := d.Get("publish_to_all_tenants").(bool) - publishedOrgIds, isPublishedOrgIdsSet := d.GetOk("published_tenant_ids") - - if publishToAllTenants && isPublishedOrgIdsSet { - return fmt.Errorf("`publish_to_all_tenants` can't be true if `published_tenant_ids` is also set") - } + publishedOrgIds := d.Get("published_tenant_ids") if d.HasChange("publish_to_all_tenants") { if d.Get("publish_to_all_tenants").(bool) { @@ -247,6 +264,11 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte } func resourceVcdUIPluginUpdate(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + err := validateUIPluginAttributes(d) + if err != nil { + return diag.FromErr(err) + } + vcdClient := meta.(*VCDClient) uiPlugin, err := getUIPlugin(vcdClient, d, "resource") if err != nil { diff --git a/vcd/resource_vcd_ui_plugin_test.go b/vcd/resource_vcd_ui_plugin_test.go index 8976532c4..faad636c2 100644 --- a/vcd/resource_vcd_ui_plugin_test.go +++ b/vcd/resource_vcd_ui_plugin_test.go @@ -15,6 +15,8 @@ func init() { } func TestAccVcdUiPlugin(t *testing.T) { + preTestChecks(t) + skipIfNotSysAdmin(t) var params = StringMap{ "Org1": testConfig.VCD.Org, @@ -22,20 +24,61 @@ func TestAccVcdUiPlugin(t *testing.T) { "Enabled": "true", "PluginPath": "../test-resources/ui_plugin.zip", "PublishToAllTenants": "true", - "PublishedTenantIds": " ", - "FuncName": t.Name() + "Step1", + "PublishedTenantIds": "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]", + "ProviderScoped": " ", + "TenantScoped": " ", + "SkipBinary": "# skip-binary-test", + "FuncName": t.Name() + "Fail", } testParamsNotEmpty(t, params) - step1Config := templateFill(testAccVcdUiPluginStep1, params) + step1Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step2" + params["PublishedTenantIds"] = " " + params["SkipBinary"] = " " + step2Config := templateFill(testAccVcdUiPlugin, params) + params["FuncName"] = t.Name() + "Step3" params["PublishToAllTenants"] = "false" params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" - step2Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPluginStep1, params) + step3Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) + params["FuncName"] = t.Name() + "Step4" + params["Enabled"] = "false" + params["PublishToAllTenants"] = "true" + params["PublishedTenantIds"] = " " + step4Config := templateFill(testAccVcdUiPlugin, params) + params["FuncName"] = t.Name() + "Step5" + params["Enabled"] = "false" + params["PublishToAllTenants"] = "false" + params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" + step5Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) + params["FuncName"] = t.Name() + "Step6" + params["Enabled"] = "true" + params["PublishToAllTenants"] = "true" + params["PublishedTenantIds"] = " " + params["ProviderScoped"] = "provider_scoped = false" + params["TenantScoped"] = "tenant_scoped = false" + step6Config := templateFill(testAccVcdUiPlugin, params) + params["FuncName"] = t.Name() + "Step7" + params["Enabled"] = "false" + params["PublishToAllTenants"] = "false" + params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" + params["ProviderScoped"] = "provider_scoped = true" + params["TenantScoped"] = "tenant_scoped = true" + step7Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) + params["FuncName"] = t.Name() + "Step8" + params["SkipBinary"] = "# skip-binary-test" + step8Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin+testAccVcdUiPluginDS, params) - plugin1 := "vcd_ui_plugin.plugin1" + resourceName := "vcd_ui_plugin.plugin" + dsName := "vcd_ui_plugin.pluginDS" debugPrintf("#[DEBUG] CONFIGURATION Step 1: %s\n", step1Config) + debugPrintf("#[DEBUG] CONFIGURATION Step 2: %s\n", step2Config) + debugPrintf("#[DEBUG] CONFIGURATION Step 3: %s\n", step3Config) + debugPrintf("#[DEBUG] CONFIGURATION Step 4: %s\n", step4Config) + debugPrintf("#[DEBUG] CONFIGURATION Step 5: %s\n", step5Config) + debugPrintf("#[DEBUG] CONFIGURATION Step 6: %s\n", step6Config) + debugPrintf("#[DEBUG] CONFIGURATION Step 7: %s\n", step7Config) if vcdShortTest { t.Skip(acceptanceTestsSkipped) return @@ -58,25 +101,102 @@ func TestAccVcdUiPlugin(t *testing.T) { ProviderFactories: testAccProviders, CheckDestroy: testAccCheckUIPluginDestroy(cachedId.fieldValue), Steps: []resource.TestStep{ + // Test UI Plugin error in configuration + { + Config: step1Config, + ExpectError: regexp.MustCompile("`publish_to_all_tenants` can't be true if `published_tenant_ids` is also set"), + }, // Test UI Plugin creation with publish to all tenants and enabled { - Config: step1Config, + Config: step2Config, Check: resource.ComposeAggregateTestCheckFunc( - testCheckResourceCommonUIPluginAsserts(plugin1), - resource.TestCheckResourceAttr(plugin1, "enabled", "true"), - resource.TestCheckResourceAttr(plugin1, "publish_to_all_tenants", "true"), - resource.TestMatchResourceAttr(plugin1, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), + testCheckResourceCommonUIPluginAsserts(resourceName), + resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), + resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), ), }, // Test UI Plugin creation (we taint it for that) with publish to only specific tenants and enabled { - Config: step2Config, - Taint: []string{plugin1}, + Config: step3Config, + Taint: []string{resourceName}, + Check: resource.ComposeAggregateTestCheckFunc( + testCheckResourceCommonUIPluginAsserts(resourceName), + resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), + resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), + ), + }, + // Test UI Plugin creation (we taint it for that) with publish to all tenants and disabled + { + Config: step4Config, + Taint: []string{resourceName}, + Check: resource.ComposeAggregateTestCheckFunc( + testCheckResourceCommonUIPluginAsserts(resourceName), + resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), + resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), + ), + }, + // Test UI Plugin creation (we taint it for that) with publish only specific tenants and disabled + { + Config: step5Config, + Taint: []string{resourceName}, Check: resource.ComposeAggregateTestCheckFunc( - testCheckResourceCommonUIPluginAsserts(plugin1), - resource.TestCheckResourceAttr(plugin1, "enabled", "true"), - resource.TestCheckResourceAttr(plugin1, "publish_to_all_tenants", "false"), - resource.TestMatchResourceAttr(plugin1, "published_tenant_ids.#", regexp.MustCompile("2")), + testCheckResourceCommonUIPluginAsserts(resourceName), + resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), + resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), + ), + }, + // Test UI Plugin update + { + Config: step6Config, + Check: resource.ComposeAggregateTestCheckFunc( + testCheckResourceCommonUIPluginAsserts(resourceName), + resource.TestCheckResourceAttr(resourceName, "provider_scoped", "false"), + resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "false"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), + resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), + ), + }, + // Test UI Plugin update + { + Config: step7Config, + Check: resource.ComposeAggregateTestCheckFunc( + testCheckResourceCommonUIPluginAsserts(resourceName), + resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), + resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), + ), + }, + // Test UI Plugin data source + { + Config: step8Config, + Check: resource.ComposeAggregateTestCheckFunc( + testCheckResourceCommonUIPluginAsserts(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "vendor", dsName, "vendor"), + resource.TestCheckResourceAttrPair(resourceName, "name", dsName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "version", dsName, "version"), + resource.TestCheckResourceAttrPair(resourceName, "license", dsName, "license"), + resource.TestCheckResourceAttrPair(resourceName, "description", dsName, "description"), + resource.TestCheckResourceAttrPair(resourceName, "link", dsName, "link"), + resource.TestCheckResourceAttrPair(resourceName, "provider_scoped", dsName, "provider_scoped"), + resource.TestCheckResourceAttrPair(resourceName, "tenant_scoped", dsName, "tenant_scoped"), + resource.TestCheckResourceAttrPair(resourceName, "enabled", dsName, "enabled"), + resource.TestCheckResourceAttrPair(resourceName, "link", dsName, "link"), + resource.TestCheckResourceAttrPair(resourceName, "published_tenant_ids.#", dsName, "published_tenant_ids.#"), ), }, }, @@ -84,12 +204,14 @@ func TestAccVcdUiPlugin(t *testing.T) { postTestChecks(t) } -// Test UI Plugin creation with publish to all tenants -const testAccVcdUiPluginStep1 = ` -resource "vcd_ui_plugin" "plugin1" { +const testAccVcdUiPlugin = ` +{{.SkipBinary}} +resource "vcd_ui_plugin" "plugin" { plugin_path = "{{.PluginPath}}" enabled = {{.Enabled}} publish_to_all_tenants = {{.PublishToAllTenants}} + {{.ProviderScoped}} + {{.TenantScoped}} {{.PublishedTenantIds}} } ` @@ -104,6 +226,14 @@ data "vcd_org" "org2" { } ` +const testAccVcdUiPluginDS = ` +data "vcd_ui_plugin" "pluginDS" { + vendor = "VMware" + name = "Test Plugin" + version = "1.2.3" +} +` + func testAccCheckUIPluginDestroy(id string) resource.TestCheckFunc { return func(s *terraform.State) error { conn := testAccProvider.Meta().(*VCDClient) From 680bff0ef3f64ce047507fac3a386d515d8f9cb0 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Thu, 25 May 2023 15:29:50 +0200 Subject: [PATCH 09/28] Checkpoint Signed-off-by: abarreiro --- vcd/remove_leftovers_test.go | 26 +++++++++ vcd/resource_vcd_ui_plugin.go | 75 ++++++++++--------------- vcd/resource_vcd_ui_plugin_test.go | 90 +++++++++++++----------------- 3 files changed, 94 insertions(+), 97 deletions(-) diff --git a/vcd/remove_leftovers_test.go b/vcd/remove_leftovers_test.go index 2b5b84653..52db2259f 100644 --- a/vcd/remove_leftovers_test.go +++ b/vcd/remove_leftovers_test.go @@ -301,6 +301,23 @@ func removeLeftovers(govcdClient *govcd.VCDClient, verbose bool) error { } } } + // -------------------------------------------------------------- + // Plugins + // -------------------------------------------------------------- + uiPlugins, err := govcdClient.GetAllUIPlugins() + if err != nil { + return fmt.Errorf("error retrieving UI Plugins: %s", err) + } + for _, uiPlugin := range uiPlugins { + toBeDeleted := shouldDeleteEntity(alsoDelete, doNotDelete, uiPlugin.UIPluginMetadata.PluginName, "vcd_ui_plugin", 1, verbose) + if toBeDeleted { + err = deleteUIPlugin(uiPlugin) + if err != nil { + return fmt.Errorf("error deleting UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) + } + } + } + return nil } @@ -627,3 +644,12 @@ func deleteRdeType(rdeType *govcd.DefinedEntityType) error { } return nil } + +func deleteUIPlugin(uiPlugin *govcd.UIPlugin) error { + fmt.Printf("\t\t REMOVING UI PLUGIN %s\n", uiPlugin.UIPluginMetadata.ID) + err := uiPlugin.Delete() + if err != nil { + return fmt.Errorf("error deleting UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) + } + return nil +} diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index 3dc410c32..b26e2b6d9 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -55,19 +55,22 @@ func resourceVcdUIPlugin() *schema.Resource { Description: "This value is calculated automatically on create by reading the UI Plugin ZIP file contents. You can update" + "it to `true` to make it tenant scoped or `false` otherwise", }, - "publish_to_all_tenants": { - Type: schema.TypeBool, - Required: true, - Description: "When `true`, publishes the UI Plugin to all tenants", + "published_to_all_tenants": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "When `true`, the UI plugin is published in all tenants", + ExactlyOneOf: []string{"published_to_all_tenants", "published_tenant_ids"}, }, "published_tenant_ids": { Type: schema.TypeSet, Elem: &schema.Schema{ Type: schema.TypeString, }, - Optional: true, - Computed: true, - Description: "Set of organization IDs to which this UI Plugin must be published", + Optional: true, + Computed: true, + Description: "Set of organization IDs to which this UI Plugin must be published", + ExactlyOneOf: []string{"published_to_all_tenants", "published_tenant_ids"}, }, "vendor": { Type: schema.TypeString, @@ -103,29 +106,7 @@ func resourceVcdUIPlugin() *schema.Resource { } } -// validateUIPluginAttributes is an auxiliary validation function that is similar to what ValidateDiagFunc would do, -// but in this case we need to validate one attribute that depends on another. -func validateUIPluginAttributes(d *schema.ResourceData) error { - publishToAllTenants := d.Get("publish_to_all_tenants").(bool) - rawConfig := d.GetRawConfig() - if rawConfig.IsNull() { - return nil - } - attr := rawConfig.GetAttr("published_tenant_ids") - if attr.IsNull() { - return nil - } - if !publishToAllTenants { - return nil - } - return fmt.Errorf("`publish_to_all_tenants` can't be true if `published_tenant_ids` is also set") -} - func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - err := validateUIPluginAttributes(d) - if err != nil { - return diag.FromErr(err) - } vcdClient := meta.(*VCDClient) uiPlugin, err := vcdClient.AddUIPlugin(d.Get("plugin_path").(string), d.Get("enabled").(bool)) @@ -147,10 +128,8 @@ func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta // publishUIPluginToTenants performs a publish/unpublish operation for the given UI plugin. func publishUIPluginToTenants(vcdClient *VCDClient, uiPlugin *govcd.UIPlugin, d *schema.ResourceData, operation string) error { - publishedOrgIds := d.Get("published_tenant_ids") - - if d.HasChange("publish_to_all_tenants") { - if d.Get("publish_to_all_tenants").(bool) { + if d.HasChange("published_to_all_tenants") { + if d.Get("published_to_all_tenants").(bool) { err := uiPlugin.PublishAll() if err != nil { return fmt.Errorf("could not publish the UI Plugin %s to all tenants: %s", uiPlugin.UIPluginMetadata.ID, err) @@ -165,6 +144,7 @@ func publishUIPluginToTenants(vcdClient *VCDClient, uiPlugin *govcd.UIPlugin, d } if d.HasChange("published_tenant_ids") { + publishedOrgIds := d.Get("published_tenant_ids") orgIds := publishedOrgIds.(*schema.Set).List() if len(orgIds) == 0 { return nil @@ -248,15 +228,21 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte if err != nil { return diag.Errorf("error retrieving the organizations where the plugin with ID '%s': %s", uiPlugin.UIPluginMetadata.ID, err) } - if len(orgRefs) > 0 { - var orgIds = make([]string, len(orgRefs)) - for i, orgRef := range orgRefs { - orgIds[i] = orgRef.ID - } - err = d.Set("published_tenant_ids", convertStringsToTypeSet(orgIds)) - if err != nil { - return diag.FromErr(err) - } + + var orgIds = make([]string, len(orgRefs)) + for i, orgRef := range orgRefs { + orgIds[i] = orgRef.ID + } + err = d.Set("published_tenant_ids", convertStringsToTypeSet(orgIds)) + if err != nil { + return diag.FromErr(err) + } + + orgs, err := vcdClient.GetOrgList() + if len(orgs.Org) == len(orgRefs) { + dSet(d, "published_to_all_tenants", true) + } else { + dSet(d, "published_to_all_tenants", false) } d.SetId(uiPlugin.UIPluginMetadata.ID) @@ -264,11 +250,6 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte } func resourceVcdUIPluginUpdate(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - err := validateUIPluginAttributes(d) - if err != nil { - return diag.FromErr(err) - } - vcdClient := meta.(*VCDClient) uiPlugin, err := getUIPlugin(vcdClient, d, "resource") if err != nil { diff --git a/vcd/resource_vcd_ui_plugin_test.go b/vcd/resource_vcd_ui_plugin_test.go index faad636c2..a28078471 100644 --- a/vcd/resource_vcd_ui_plugin_test.go +++ b/vcd/resource_vcd_ui_plugin_test.go @@ -19,58 +19,53 @@ func TestAccVcdUiPlugin(t *testing.T) { skipIfNotSysAdmin(t) var params = StringMap{ - "Org1": testConfig.VCD.Org, - "Org2": testConfig.Provider.SysOrg, - "Enabled": "true", - "PluginPath": "../test-resources/ui_plugin.zip", - "PublishToAllTenants": "true", - "PublishedTenantIds": "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]", - "ProviderScoped": " ", - "TenantScoped": " ", - "SkipBinary": "# skip-binary-test", - "FuncName": t.Name() + "Fail", + "Org1": testConfig.VCD.Org, + "Org2": testConfig.Provider.SysOrg, + "Enabled": "true", + "PluginPath": "../test-resources/ui_plugin.zip", + "PublishedToAllTenants": "true", + "PublishedTenantIds": " ", + "ProviderScoped": " ", + "TenantScoped": " ", + "FuncName": t.Name(), } testParamsNotEmpty(t, params) - step1Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) + step1Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step2" - params["PublishedTenantIds"] = " " - params["SkipBinary"] = " " + params["PublishedToAllTenants"] = "false" + params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" step2Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step3" - params["PublishToAllTenants"] = "false" - params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" + params["Enabled"] = "false" + params["PublishedToAllTenants"] = "true" + params["PublishedTenantIds"] = " " step3Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step4" params["Enabled"] = "false" - params["PublishToAllTenants"] = "true" - params["PublishedTenantIds"] = " " + params["PublishedToAllTenants"] = "false" + params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" step4Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step5" - params["Enabled"] = "false" - params["PublishToAllTenants"] = "false" - params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" - step5Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) - params["FuncName"] = t.Name() + "Step6" params["Enabled"] = "true" - params["PublishToAllTenants"] = "true" + params["PublishedToAllTenants"] = "true" params["PublishedTenantIds"] = " " params["ProviderScoped"] = "provider_scoped = false" params["TenantScoped"] = "tenant_scoped = false" - step6Config := templateFill(testAccVcdUiPlugin, params) - params["FuncName"] = t.Name() + "Step7" + step5Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) + params["FuncName"] = t.Name() + "Step6" params["Enabled"] = "false" - params["PublishToAllTenants"] = "false" + params["PublishedToAllTenants"] = "false" params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" params["ProviderScoped"] = "provider_scoped = true" params["TenantScoped"] = "tenant_scoped = true" - step7Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) - params["FuncName"] = t.Name() + "Step8" + step6Config := templateFill(testAccVcdUiPlugin, params) + params["FuncName"] = t.Name() + "Step7" params["SkipBinary"] = "# skip-binary-test" - step8Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin+testAccVcdUiPluginDS, params) + step7Config := templateFill(testAccVcdUiPluginDS+testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) resourceName := "vcd_ui_plugin.plugin" - dsName := "vcd_ui_plugin.pluginDS" + dsName := "data.vcd_ui_plugin.pluginDS" debugPrintf("#[DEBUG] CONFIGURATION Step 1: %s\n", step1Config) debugPrintf("#[DEBUG] CONFIGURATION Step 2: %s\n", step2Config) @@ -101,89 +96,84 @@ func TestAccVcdUiPlugin(t *testing.T) { ProviderFactories: testAccProviders, CheckDestroy: testAccCheckUIPluginDestroy(cachedId.fieldValue), Steps: []resource.TestStep{ - // Test UI Plugin error in configuration - { - Config: step1Config, - ExpectError: regexp.MustCompile("`publish_to_all_tenants` can't be true if `published_tenant_ids` is also set"), - }, // Test UI Plugin creation with publish to all tenants and enabled { - Config: step2Config, + Config: step1Config, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), + resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "true"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), ), }, // Test UI Plugin creation (we taint it for that) with publish to only specific tenants and enabled { - Config: step3Config, + Config: step2Config, Taint: []string{resourceName}, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), + resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "false"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), ), }, // Test UI Plugin creation (we taint it for that) with publish to all tenants and disabled { - Config: step4Config, + Config: step3Config, Taint: []string{resourceName}, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), + resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "true"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), ), }, // Test UI Plugin creation (we taint it for that) with publish only specific tenants and disabled { - Config: step5Config, + Config: step4Config, Taint: []string{resourceName}, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), + resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "false"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), ), }, // Test UI Plugin update { - Config: step6Config, + Config: step5Config, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttr(resourceName, "provider_scoped", "false"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "false"), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), + resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "true"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), ), }, // Test UI Plugin update { - Config: step7Config, + Config: step6Config, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), + resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "false"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), ), }, // Test UI Plugin data source { - Config: step8Config, + Config: step7Config, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttrPair(resourceName, "vendor", dsName, "vendor"), @@ -205,11 +195,10 @@ func TestAccVcdUiPlugin(t *testing.T) { } const testAccVcdUiPlugin = ` -{{.SkipBinary}} resource "vcd_ui_plugin" "plugin" { plugin_path = "{{.PluginPath}}" enabled = {{.Enabled}} - publish_to_all_tenants = {{.PublishToAllTenants}} + published_to_all_tenants = {{.PublishedToAllTenants}} {{.ProviderScoped}} {{.TenantScoped}} {{.PublishedTenantIds}} @@ -227,6 +216,7 @@ data "vcd_org" "org2" { ` const testAccVcdUiPluginDS = ` +# skip-binary-test - Data source referencing the same resource data "vcd_ui_plugin" "pluginDS" { vendor = "VMware" name = "Test Plugin" From 925bde99cc475fa33566ec0a11f0c42e75e60ea8 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Thu, 25 May 2023 16:26:25 +0200 Subject: [PATCH 10/28] # Signed-off-by: abarreiro --- vcd/resource_vcd_ui_plugin.go | 39 +++++++--------- vcd/resource_vcd_ui_plugin_test.go | 71 ++++++++++++++++++------------ 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index b26e2b6d9..51a0277ee 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -55,12 +55,11 @@ func resourceVcdUIPlugin() *schema.Resource { Description: "This value is calculated automatically on create by reading the UI Plugin ZIP file contents. You can update" + "it to `true` to make it tenant scoped or `false` otherwise", }, - "published_to_all_tenants": { + "publish_to_all_tenants": { Type: schema.TypeBool, Optional: true, - Computed: true, - Description: "When `true`, the UI plugin is published in all tenants", - ExactlyOneOf: []string{"published_to_all_tenants", "published_tenant_ids"}, + Description: "When `true`, publishes the UI Plugin to all tenants. When `false`, it unpublishes from all tenants", + ExactlyOneOf: []string{"publish_to_all_tenants", "published_tenant_ids"}, }, "published_tenant_ids": { Type: schema.TypeSet, @@ -70,7 +69,7 @@ func resourceVcdUIPlugin() *schema.Resource { Optional: true, Computed: true, Description: "Set of organization IDs to which this UI Plugin must be published", - ExactlyOneOf: []string{"published_to_all_tenants", "published_tenant_ids"}, + ExactlyOneOf: []string{"publish_to_all_tenants", "published_tenant_ids"}, }, "vendor": { Type: schema.TypeString, @@ -128,8 +127,8 @@ func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta // publishUIPluginToTenants performs a publish/unpublish operation for the given UI plugin. func publishUIPluginToTenants(vcdClient *VCDClient, uiPlugin *govcd.UIPlugin, d *schema.ResourceData, operation string) error { - if d.HasChange("published_to_all_tenants") { - if d.Get("published_to_all_tenants").(bool) { + if d.HasChange("publish_to_all_tenants") { + if d.Get("publish_to_all_tenants").(bool) { err := uiPlugin.PublishAll() if err != nil { return fmt.Errorf("could not publish the UI Plugin %s to all tenants: %s", uiPlugin.UIPluginMetadata.ID, err) @@ -140,10 +139,7 @@ func publishUIPluginToTenants(vcdClient *VCDClient, uiPlugin *govcd.UIPlugin, d return fmt.Errorf("could not unpublish the UI Plugin %s from all tenants: %s", uiPlugin.UIPluginMetadata.ID, err) } } - return nil - } - - if d.HasChange("published_tenant_ids") { + } else if d.HasChange("published_tenant_ids") { publishedOrgIds := d.Get("published_tenant_ids") orgIds := publishedOrgIds.(*schema.Set).List() if len(orgIds) == 0 { @@ -238,18 +234,11 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte return diag.FromErr(err) } - orgs, err := vcdClient.GetOrgList() - if len(orgs.Org) == len(orgRefs) { - dSet(d, "published_to_all_tenants", true) - } else { - dSet(d, "published_to_all_tenants", false) - } - d.SetId(uiPlugin.UIPluginMetadata.ID) return nil } -func resourceVcdUIPluginUpdate(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func resourceVcdUIPluginUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { vcdClient := meta.(*VCDClient) uiPlugin, err := getUIPlugin(vcdClient, d, "resource") if err != nil { @@ -258,16 +247,18 @@ func resourceVcdUIPluginUpdate(_ context.Context, d *schema.ResourceData, meta i if uiPlugin == nil { return nil } - - err = uiPlugin.Update(d.Get("enabled").(bool), d.Get("provider_scoped").(bool), d.Get("tenant_scoped").(bool)) - if err != nil { - return diag.Errorf("could not update the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) + if d.HasChange("enabled") || d.HasChange("provider_scoped") || d.HasChange("tenant_scoped") { + err = uiPlugin.Update(d.Get("enabled").(bool), d.Get("provider_scoped").(bool), d.Get("tenant_scoped").(bool)) + if err != nil { + return diag.Errorf("could not update the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) + } } + err = publishUIPluginToTenants(vcdClient, uiPlugin, d, "update") if err != nil { return diag.Errorf("could not update the published tenants of the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) } - return nil + return resourceVcdUIPluginRead(ctx, d, meta) } func resourceVcdUIPluginDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { diff --git a/vcd/resource_vcd_ui_plugin_test.go b/vcd/resource_vcd_ui_plugin_test.go index a28078471..344296b49 100644 --- a/vcd/resource_vcd_ui_plugin_test.go +++ b/vcd/resource_vcd_ui_plugin_test.go @@ -23,7 +23,7 @@ func TestAccVcdUiPlugin(t *testing.T) { "Org2": testConfig.Provider.SysOrg, "Enabled": "true", "PluginPath": "../test-resources/ui_plugin.zip", - "PublishedToAllTenants": "true", + "PublishedToAllTenants": "publish_to_all_tenants = true", "PublishedTenantIds": " ", "ProviderScoped": " ", "TenantScoped": " ", @@ -33,36 +33,39 @@ func TestAccVcdUiPlugin(t *testing.T) { step1Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step2" - params["PublishedToAllTenants"] = "false" + params["PublishedToAllTenants"] = " " params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" step2Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step3" params["Enabled"] = "false" - params["PublishedToAllTenants"] = "true" + params["PublishedToAllTenants"] = "publish_to_all_tenants = true" params["PublishedTenantIds"] = " " - step3Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) + step3Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step4" params["Enabled"] = "false" - params["PublishedToAllTenants"] = "false" + params["PublishedToAllTenants"] = " " params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" step4Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step5" params["Enabled"] = "true" - params["PublishedToAllTenants"] = "true" + params["PublishedToAllTenants"] = "publish_to_all_tenants = true" params["PublishedTenantIds"] = " " params["ProviderScoped"] = "provider_scoped = false" params["TenantScoped"] = "tenant_scoped = false" - step5Config := templateFill(testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) + step5Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step6" params["Enabled"] = "false" - params["PublishedToAllTenants"] = "false" + params["PublishedToAllTenants"] = " " params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" params["ProviderScoped"] = "provider_scoped = true" params["TenantScoped"] = "tenant_scoped = true" step6Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step7" + params["PublishedToAllTenants"] = "publish_to_all_tenants = false" + step7Config := templateFill(testAccVcdUiPlugin, params) + params["FuncName"] = t.Name() + "Step8" params["SkipBinary"] = "# skip-binary-test" - step7Config := templateFill(testAccVcdUiPluginDS+testAccVcdUiPluginStepOrgs+testAccVcdUiPlugin, params) + step8Config := templateFill(testAccVcdUiPluginDS+testAccVcdUiPlugin, params) resourceName := "vcd_ui_plugin.plugin" dsName := "data.vcd_ui_plugin.pluginDS" @@ -74,6 +77,7 @@ func TestAccVcdUiPlugin(t *testing.T) { debugPrintf("#[DEBUG] CONFIGURATION Step 5: %s\n", step5Config) debugPrintf("#[DEBUG] CONFIGURATION Step 6: %s\n", step6Config) debugPrintf("#[DEBUG] CONFIGURATION Step 7: %s\n", step7Config) + debugPrintf("#[DEBUG] CONFIGURATION Step 8: %s\n", step8Config) if vcdShortTest { t.Skip(acceptanceTestsSkipped) return @@ -104,7 +108,7 @@ func TestAccVcdUiPlugin(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "true"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), ), }, @@ -117,7 +121,7 @@ func TestAccVcdUiPlugin(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "false"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), ), }, @@ -130,7 +134,7 @@ func TestAccVcdUiPlugin(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "true"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), ), }, @@ -143,7 +147,7 @@ func TestAccVcdUiPlugin(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "false"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), ), }, @@ -155,25 +159,40 @@ func TestAccVcdUiPlugin(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "provider_scoped", "false"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "false"), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "true"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), ), }, // Test UI Plugin update { Config: step6Config, + PreConfig: func() { + fmt.Printf("a") + }, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "published_to_all_tenants", "false"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), ), }, + // Test UI Plugin update + { + Config: step6Config, + Check: resource.ComposeAggregateTestCheckFunc( + testCheckResourceCommonUIPluginAsserts(resourceName), + resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), + resource.TestCheckResourceAttr(resourceName, "enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), + resource.TestCheckResourceAttr(resourceName, "published_tenant_ids.#", "0"), + ), + }, // Test UI Plugin data source { - Config: step7Config, + Config: step8Config, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttrPair(resourceName, "vendor", dsName, "vendor"), @@ -195,17 +214,6 @@ func TestAccVcdUiPlugin(t *testing.T) { } const testAccVcdUiPlugin = ` -resource "vcd_ui_plugin" "plugin" { - plugin_path = "{{.PluginPath}}" - enabled = {{.Enabled}} - published_to_all_tenants = {{.PublishedToAllTenants}} - {{.ProviderScoped}} - {{.TenantScoped}} - {{.PublishedTenantIds}} -} -` - -const testAccVcdUiPluginStepOrgs = ` data "vcd_org" "org1" { name = "{{.Org1}}" } @@ -213,6 +221,15 @@ data "vcd_org" "org1" { data "vcd_org" "org2" { name = "{{.Org2}}" } + +resource "vcd_ui_plugin" "plugin" { + plugin_path = "{{.PluginPath}}" + enabled = {{.Enabled}} + {{.PublishedToAllTenants}} + {{.ProviderScoped}} + {{.TenantScoped}} + {{.PublishedTenantIds}} +} ` const testAccVcdUiPluginDS = ` From 5bb310114e12759def50291a1e4fbbb930c80f18 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Fri, 26 May 2023 11:14:45 +0200 Subject: [PATCH 11/28] Tests pass Signed-off-by: abarreiro --- go.mod | 2 +- go.sum | 4 +- vcd/datasource_vcd_ui_plugin.go | 7 +- vcd/resource_vcd_ui_plugin.go | 99 ++++++++++-------------- vcd/resource_vcd_ui_plugin_test.go | 119 ++++++----------------------- 5 files changed, 74 insertions(+), 157 deletions(-) diff --git a/go.mod b/go.mod index 23b7b7847..f7cfbcba3 100644 --- a/go.mod +++ b/go.mod @@ -61,4 +61,4 @@ require ( google.golang.org/protobuf v1.28.1 // indirect ) -replace github.com/vmware/go-vcloud-director/v2 => github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230524093520-0c2043ce1b0a +replace github.com/vmware/go-vcloud-director/v2 => github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230526090050-e6e6ef3844ee diff --git a/go.sum b/go.sum index 7e0f9e4d5..ae83c6c5a 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C6 github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= -github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230524093520-0c2043ce1b0a h1:6YDmeikL9Tp8r2KpgIt9MvaKlILlqs7cSCdwXFlR7vA= -github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230524093520-0c2043ce1b0a/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw= +github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230526090050-e6e6ef3844ee h1:ghaFIj7spkheFoIOFhnfNP5k/jyInidf7K9gf6v4c7g= +github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230526090050-e6e6ef3844ee/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= diff --git a/vcd/datasource_vcd_ui_plugin.go b/vcd/datasource_vcd_ui_plugin.go index 04bca1588..cdc7195d1 100644 --- a/vcd/datasource_vcd_ui_plugin.go +++ b/vcd/datasource_vcd_ui_plugin.go @@ -55,7 +55,12 @@ func datasourceVcdUIPlugin() *schema.Resource { Computed: true, Description: "true if the UI Plugin is enabled. 'false' if not", }, - "published_tenant_ids": { + "status": { + Type: schema.TypeString, + Computed: true, + Description: "Status of the UI Plugin", + }, + "tenant_ids": { Type: schema.TypeSet, Elem: &schema.Schema{ Type: schema.TypeString, diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index 51a0277ee..96b2c759e 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -41,6 +41,14 @@ func resourceVcdUIPlugin() *schema.Resource { Required: true, Description: "true to make the UI Plugin enabled. 'false' to make it disabled", }, + "tenant_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Description: "Set of organization IDs to which this UI Plugin must be published", + }, "provider_scoped": { Type: schema.TypeBool, Optional: true, @@ -55,22 +63,6 @@ func resourceVcdUIPlugin() *schema.Resource { Description: "This value is calculated automatically on create by reading the UI Plugin ZIP file contents. You can update" + "it to `true` to make it tenant scoped or `false` otherwise", }, - "publish_to_all_tenants": { - Type: schema.TypeBool, - Optional: true, - Description: "When `true`, publishes the UI Plugin to all tenants. When `false`, it unpublishes from all tenants", - ExactlyOneOf: []string{"publish_to_all_tenants", "published_tenant_ids"}, - }, - "published_tenant_ids": { - Type: schema.TypeSet, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - Computed: true, - Description: "Set of organization IDs to which this UI Plugin must be published", - ExactlyOneOf: []string{"publish_to_all_tenants", "published_tenant_ids"}, - }, "vendor": { Type: schema.TypeString, Computed: true, @@ -101,6 +93,11 @@ func resourceVcdUIPlugin() *schema.Resource { Computed: true, Description: "The description of the UI Plugin", }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The status of the UI Plugin", + }, }, } } @@ -127,47 +124,31 @@ func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta // publishUIPluginToTenants performs a publish/unpublish operation for the given UI plugin. func publishUIPluginToTenants(vcdClient *VCDClient, uiPlugin *govcd.UIPlugin, d *schema.ResourceData, operation string) error { - if d.HasChange("publish_to_all_tenants") { - if d.Get("publish_to_all_tenants").(bool) { - err := uiPlugin.PublishAll() - if err != nil { - return fmt.Errorf("could not publish the UI Plugin %s to all tenants: %s", uiPlugin.UIPluginMetadata.ID, err) - } - } else { - err := uiPlugin.UnpublishAll() - if err != nil { - return fmt.Errorf("could not unpublish the UI Plugin %s from all tenants: %s", uiPlugin.UIPluginMetadata.ID, err) - } - } - } else if d.HasChange("published_tenant_ids") { - publishedOrgIds := d.Get("published_tenant_ids") - orgIds := publishedOrgIds.(*schema.Set).List() - if len(orgIds) == 0 { - return nil - } - orgList, err := vcdClient.GetOrgList() + if d.HasChange("tenant_ids") { + orgsToPublish := d.Get("tenant_ids").(*schema.Set).List() + existingOrgs, err := vcdClient.GetOrgList() if err != nil { - return fmt.Errorf("could not publish the UI Plugin %s to tenants '%v': %s", uiPlugin.UIPluginMetadata.ID, orgIds, err) + return fmt.Errorf("could not publish the UI Plugin %s to Organizations '%v': %s", uiPlugin.UIPluginMetadata.ID, orgsToPublish, err) } - var orgRefs types.OpenApiReferences - for _, org := range orgList.Org { - for _, orgId := range orgIds { - // We do this as org.ID is empty, so we need to re-build the URN with the HREF + var orgsToPubRefs types.OpenApiReferences + for _, org := range existingOrgs.Org { + for _, orgId := range orgsToPublish { + // We do this as org.ID is empty, so we need to reconstruct the URN with the HREF uuid := extractUuid(orgId.(string)) if strings.Contains(org.HREF, uuid) { - orgRefs = append(orgRefs, types.OpenApiReference{ID: "urn:cloud:org:" + uuid, Name: org.Name}) + orgsToPubRefs = append(orgsToPubRefs, types.OpenApiReference{ID: "urn:cloud:org:" + uuid, Name: org.Name}) } } } if operation == "update" { err = uiPlugin.UnpublishAll() // We need to clean up the already-published Orgs to put the new ones during an Update. if err != nil { - return fmt.Errorf("could not publish the UI Plugin %s to tenants '%v': %s", uiPlugin.UIPluginMetadata.ID, orgIds, err) + return fmt.Errorf("could not publish the UI Plugin %s to Organizations '%v': %s", uiPlugin.UIPluginMetadata.ID, orgsToPublish, err) } } - err = uiPlugin.Publish(orgRefs) + err = uiPlugin.Publish(orgsToPubRefs) if err != nil { - return fmt.Errorf("could not publish the UI Plugin %s to tenants '%v': %s", uiPlugin.UIPluginMetadata.ID, orgIds, err) + return fmt.Errorf("could not publish the UI Plugin %s to Organizations '%v': %s", uiPlugin.UIPluginMetadata.ID, orgsToPublish, err) } } return nil @@ -219,21 +200,21 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte dSet(d, "provider_scoped", uiPlugin.UIPluginMetadata.ProviderScoped) dSet(d, "enabled", uiPlugin.UIPluginMetadata.Enabled) dSet(d, "description", uiPlugin.UIPluginMetadata.Description) - - orgRefs, err := uiPlugin.GetPublishedTenants() - if err != nil { - return diag.Errorf("error retrieving the organizations where the plugin with ID '%s': %s", uiPlugin.UIPluginMetadata.ID, err) - } - - var orgIds = make([]string, len(orgRefs)) - for i, orgRef := range orgRefs { - orgIds[i] = orgRef.ID - } - err = d.Set("published_tenant_ids", convertStringsToTypeSet(orgIds)) - if err != nil { - return diag.FromErr(err) + dSet(d, "status", uiPlugin.UIPluginMetadata.PluginStatus) + if origin == "datasource" { + orgRefs, err := uiPlugin.GetPublishedTenants() + if err != nil { + return diag.Errorf("could not update the published Organizations of the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) + } + var orgIds = make([]string, len(orgRefs)) + for i, orgRef := range orgRefs { + orgIds[i] = orgRef.ID + } + err = d.Set("tenant_ids", convertStringsToTypeSet(orgIds)) + if err != nil { + return diag.FromErr(err) + } } - d.SetId(uiPlugin.UIPluginMetadata.ID) return nil } @@ -256,7 +237,7 @@ func resourceVcdUIPluginUpdate(ctx context.Context, d *schema.ResourceData, meta err = publishUIPluginToTenants(vcdClient, uiPlugin, d, "update") if err != nil { - return diag.Errorf("could not update the published tenants of the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) + return diag.Errorf("could not update the published Organizations of the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) } return resourceVcdUIPluginRead(ctx, d, meta) } diff --git a/vcd/resource_vcd_ui_plugin_test.go b/vcd/resource_vcd_ui_plugin_test.go index 344296b49..6c5700f1c 100644 --- a/vcd/resource_vcd_ui_plugin_test.go +++ b/vcd/resource_vcd_ui_plugin_test.go @@ -19,53 +19,34 @@ func TestAccVcdUiPlugin(t *testing.T) { skipIfNotSysAdmin(t) var params = StringMap{ - "Org1": testConfig.VCD.Org, - "Org2": testConfig.Provider.SysOrg, - "Enabled": "true", - "PluginPath": "../test-resources/ui_plugin.zip", - "PublishedToAllTenants": "publish_to_all_tenants = true", - "PublishedTenantIds": " ", - "ProviderScoped": " ", - "TenantScoped": " ", - "FuncName": t.Name(), + "Org1": testConfig.VCD.Org, + "Org2": testConfig.Provider.SysOrg, + "Enabled": "true", + "PluginPath": "../test-resources/ui_plugin.zip", + "TenantIds": "tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]", + "ProviderScoped": " ", + "TenantScoped": " ", + "FuncName": t.Name(), } testParamsNotEmpty(t, params) step1Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step2" - params["PublishedToAllTenants"] = " " - params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" + params["TenantIds"] = "tenant_ids = [data.vcd_org.org1.id]" step2Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step3" params["Enabled"] = "false" - params["PublishedToAllTenants"] = "publish_to_all_tenants = true" - params["PublishedTenantIds"] = " " + params["TenantIds"] = " " step3Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step4" - params["Enabled"] = "false" - params["PublishedToAllTenants"] = " " - params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" - step4Config := templateFill(testAccVcdUiPlugin, params) - params["FuncName"] = t.Name() + "Step5" params["Enabled"] = "true" - params["PublishedToAllTenants"] = "publish_to_all_tenants = true" - params["PublishedTenantIds"] = " " + params["TenantIds"] = "tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" params["ProviderScoped"] = "provider_scoped = false" params["TenantScoped"] = "tenant_scoped = false" - step5Config := templateFill(testAccVcdUiPlugin, params) - params["FuncName"] = t.Name() + "Step6" - params["Enabled"] = "false" - params["PublishedToAllTenants"] = " " - params["PublishedTenantIds"] = "published_tenant_ids = [data.vcd_org.org1.id, data.vcd_org.org2.id]" - params["ProviderScoped"] = "provider_scoped = true" - params["TenantScoped"] = "tenant_scoped = true" - step6Config := templateFill(testAccVcdUiPlugin, params) + step4Config := templateFill(testAccVcdUiPlugin, params) params["FuncName"] = t.Name() + "Step7" - params["PublishedToAllTenants"] = "publish_to_all_tenants = false" - step7Config := templateFill(testAccVcdUiPlugin, params) - params["FuncName"] = t.Name() + "Step8" params["SkipBinary"] = "# skip-binary-test" - step8Config := templateFill(testAccVcdUiPluginDS+testAccVcdUiPlugin, params) + step5Config := templateFill(testAccVcdUiPluginDS+testAccVcdUiPlugin, params) resourceName := "vcd_ui_plugin.plugin" dsName := "data.vcd_ui_plugin.pluginDS" @@ -74,10 +55,6 @@ func TestAccVcdUiPlugin(t *testing.T) { debugPrintf("#[DEBUG] CONFIGURATION Step 2: %s\n", step2Config) debugPrintf("#[DEBUG] CONFIGURATION Step 3: %s\n", step3Config) debugPrintf("#[DEBUG] CONFIGURATION Step 4: %s\n", step4Config) - debugPrintf("#[DEBUG] CONFIGURATION Step 5: %s\n", step5Config) - debugPrintf("#[DEBUG] CONFIGURATION Step 6: %s\n", step6Config) - debugPrintf("#[DEBUG] CONFIGURATION Step 7: %s\n", step7Config) - debugPrintf("#[DEBUG] CONFIGURATION Step 8: %s\n", step8Config) if vcdShortTest { t.Skip(acceptanceTestsSkipped) return @@ -93,6 +70,7 @@ func TestAccVcdUiPlugin(t *testing.T) { resource.TestCheckResourceAttr(resourcePath, "license", "BSD-2-Clause"), resource.TestCheckResourceAttr(resourcePath, "description", "Test Plugin description"), resource.TestCheckResourceAttr(resourcePath, "link", "http://www.vmware.com"), + resource.TestMatchResourceAttr(resourcePath, "status", regexp.MustCompile("^ready|unavailable$")), ) } @@ -100,7 +78,7 @@ func TestAccVcdUiPlugin(t *testing.T) { ProviderFactories: testAccProviders, CheckDestroy: testAccCheckUIPluginDestroy(cachedId.fieldValue), Steps: []resource.TestStep{ - // Test UI Plugin creation with publish to all tenants and enabled + // Test UI Plugin creation with 2 tenants and enabled { Config: step1Config, Check: resource.ComposeAggregateTestCheckFunc( @@ -108,91 +86,45 @@ func TestAccVcdUiPlugin(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), - resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), + resource.TestCheckResourceAttr(resourceName, "tenant_ids.#", "2"), ), }, - // Test UI Plugin creation (we taint it for that) with publish to only specific tenants and enabled + // Test UI Plugin update to unpublish one tenant { Config: step2Config, - Taint: []string{resourceName}, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), - resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), + resource.TestCheckResourceAttr(resourceName, "tenant_ids.#", "1"), ), }, - // Test UI Plugin creation (we taint it for that) with publish to all tenants and disabled + // Test UI Plugin update to unpublish all tenants { Config: step3Config, - Taint: []string{resourceName}, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), - resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), - ), - }, - // Test UI Plugin creation (we taint it for that) with publish only specific tenants and disabled - { - Config: step4Config, - Taint: []string{resourceName}, - Check: resource.ComposeAggregateTestCheckFunc( - testCheckResourceCommonUIPluginAsserts(resourceName), - resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), - resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), - resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), - resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), + resource.TestCheckResourceAttr(resourceName, "tenant_ids.#", "0"), ), }, // Test UI Plugin update { - Config: step5Config, + Config: step4Config, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttr(resourceName, "provider_scoped", "false"), resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "false"), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "true"), - resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile(`^[1-9]+$`)), - ), - }, - // Test UI Plugin update - { - Config: step6Config, - PreConfig: func() { - fmt.Printf("a") - }, - Check: resource.ComposeAggregateTestCheckFunc( - testCheckResourceCommonUIPluginAsserts(resourceName), - resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), - resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), - resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), - resource.TestMatchResourceAttr(resourceName, "published_tenant_ids.#", regexp.MustCompile("2")), - ), - }, - // Test UI Plugin update - { - Config: step6Config, - Check: resource.ComposeAggregateTestCheckFunc( - testCheckResourceCommonUIPluginAsserts(resourceName), - resource.TestCheckResourceAttr(resourceName, "provider_scoped", "true"), - resource.TestCheckResourceAttr(resourceName, "tenant_scoped", "true"), - resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "publish_to_all_tenants", "false"), - resource.TestCheckResourceAttr(resourceName, "published_tenant_ids.#", "0"), + resource.TestCheckResourceAttr(resourceName, "tenant_ids.#", "2"), ), }, // Test UI Plugin data source { - Config: step8Config, + Config: step5Config, Check: resource.ComposeAggregateTestCheckFunc( testCheckResourceCommonUIPluginAsserts(resourceName), resource.TestCheckResourceAttrPair(resourceName, "vendor", dsName, "vendor"), @@ -205,7 +137,7 @@ func TestAccVcdUiPlugin(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "tenant_scoped", dsName, "tenant_scoped"), resource.TestCheckResourceAttrPair(resourceName, "enabled", dsName, "enabled"), resource.TestCheckResourceAttrPair(resourceName, "link", dsName, "link"), - resource.TestCheckResourceAttrPair(resourceName, "published_tenant_ids.#", dsName, "published_tenant_ids.#"), + resource.TestCheckResourceAttrPair(resourceName, "tenant_ids.#", dsName, "tenant_ids.#"), ), }, }, @@ -225,10 +157,9 @@ data "vcd_org" "org2" { resource "vcd_ui_plugin" "plugin" { plugin_path = "{{.PluginPath}}" enabled = {{.Enabled}} - {{.PublishedToAllTenants}} + {{.TenantIds}} {{.ProviderScoped}} {{.TenantScoped}} - {{.PublishedTenantIds}} } ` From 76b7358cfadfbfa3347c5d959c53e1f6576f4e5d Mon Sep 17 00:00:00 2001 From: abarreiro Date: Fri, 26 May 2023 13:41:02 +0200 Subject: [PATCH 12/28] Docs Signed-off-by: abarreiro --- website/docs/d/ui_plugin.markdown | 46 ++++++++++++++++++ website/docs/r/ui_plugin.markdown | 81 +++++++++++++++++++++++++++++++ website/vcd.erb | 6 +++ 3 files changed, 133 insertions(+) create mode 100644 website/docs/d/ui_plugin.markdown create mode 100644 website/docs/r/ui_plugin.markdown diff --git a/website/docs/d/ui_plugin.markdown b/website/docs/d/ui_plugin.markdown new file mode 100644 index 000000000..d65313fc5 --- /dev/null +++ b/website/docs/d/ui_plugin.markdown @@ -0,0 +1,46 @@ +--- +layout: "vcd" +page_title: "VMware Cloud Director: vcd_ui_plugin" +sidebar_current: "docs-vcd-datasource-ui-plugin" +description: |- + Provides a VMware Cloud Director UI Plugin data source. This can be used to fetch and read an existing UI Plugin. +--- + +# vcd\_ui\_plugin + +Provides a VMware Cloud Director UI Plugin data source. This can be used to fetch and read an existing UI Plugin. + +-> Reading UI Plugins requires System administrator privileges. + +Supported in provider *v3.10+* and requires VCD 10.2+ + +## Example Usage + +```hcl +data "vcd_ui_plugin" "existing_ui_plugin" { + vendor = "VMware" + name = "Customize Portal" + version = "3.1.4" +} +output "license" { + value = data.vcd_ui_plugin.existing_ui_plugin.license +} +output "tenants" { + value = data.vcd_ui_plugin.existing_ui_plugin.tenant_ids +} +``` + +## Argument Reference + +The following arguments are supported: + +* `vendor` - (Required) The vendor of the UI Plugin +* `name` - (Required) The name of the UI Plugin +* `version` - (Required) The version of the UI Plugin + +## Attribute Reference + +All **attributes** defined in [`vcd_ui_plugin`](/providers/vmware/vcd/latest/docs/resources/ui_plugin#attribute-reference) are supported. + +Also, the arguments `enabled`, `tenant_ids`, `provider_scoped` and `tenant_scoped` that are defined in the same resource +are available as read-only attributes. \ No newline at end of file diff --git a/website/docs/r/ui_plugin.markdown b/website/docs/r/ui_plugin.markdown new file mode 100644 index 000000000..e47c27d4c --- /dev/null +++ b/website/docs/r/ui_plugin.markdown @@ -0,0 +1,81 @@ +--- +layout: "vcd" +page_title: "VMware Cloud Director: vcd_ui_plugin" +sidebar_current: "docs-vcd-resource-ui-plugin" +description: |- + Provides a VMware Cloud Director UI Plugin resource. This can be used to manage UI Plugins. +--- + +# vcd\_ui\_plugin + +Provides a VMware Cloud Director UI Plugin resource. This can be used to manage UI Plugins in VCD, for example to add a new +plugin from an local ZIP file, to publish/unpublish a UI Plugin to different Organizations, etc. + +-> Creating, updating and deleting UI Plugins requires System administrator privileges. + +Supported in provider *v3.10+* and requires VCD 10.2+ + +## Example Usage with specific Organizations to publish + +```hcl +locals { + my_plugin_orgs = [ + "myOrg1", + "myOrg2" + ] +} + +data "vcd_org" "my_plugin_orgs" { + count = length(local.my_plugin_orgs) + name = local.my_plugin_orgs[count.index] +} + +resource "vcd_ui_plugin" "my_plugin" { + plugin_path = "./container-ui-plugin-4.0.zip" + enabled = true + tenant_ids = data.vcd_org.my_plugin_orgs[*].id +} +``` + +## Example Usage publishing to all Organizations available + +```hcl +data "vcd_resource_list" "list_of_orgs" { + name = "list_of_orgs" + resource_type = "vcd_org" + list_mode = "name" +} + +data "vcd_org" "all_orgs" { + count = length(data.vcd_resource_list.list_of_orgs.list) + name = data.vcd_resource_list.list_of_orgs.list[count.index] +} + +resource "vcd_ui_plugin" "my_plugin" { + plugin_path = "./container-ui-plugin-4.0.zip" + enabled = true + tenant_ids = data.vcd_org.all_orgs[*].id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `plugin_path` - (Required) Path to a .zip file that contains the bundled UI Plugin +* `enabled` - (Required) Whether the UI Plugin will be enabled (`true`) or not (`false`) +* `tenant_ids` - (Optional) The identifiers of the [Organizations](/providers/vmware/vcd/latest/docs/data-sources/org) + that will be able to use the UI Plugin if enabled. If not set, it doesn't publish to any Organization. +* `provider_scoped` - (Optional) **Can only be set on updates**. Changes the scope of the UI Plugin for System providers. +* `tenant_scoped` - (Optional) **Can only be set on updates**. Changes the scope of the UI Plugin for Organization users. + +## Attribute Reference + +* `vendor` - The vendor of the UI Plugin +* `name` - The name of the UI Plugin +* `version` - The version of the UI Plugin +* `license` - The license of the UI Plugin +* `link` - The website or custom URL of the UI Plugin +* `description` - The description of the UI Plugin +* `status` - The status of the UI Plugin (for example, `ready`, `unavailable`, etc) + diff --git a/website/vcd.erb b/website/vcd.erb index 8626b4226..27a9fdda6 100644 --- a/website/vcd.erb +++ b/website/vcd.erb @@ -305,6 +305,9 @@ > vcd_nsxt_edgegateway_rate_limiting + > + vcd_ui_plugin + > @@ -544,6 +547,9 @@ > vcd_nsxt_edgegateway_rate_limiting + > + vcd_ui_plugin + From 0968d498692e6447ff520ad023e64696286fb0e0 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Fri, 26 May 2023 14:07:03 +0200 Subject: [PATCH 13/28] Add import Signed-off-by: abarreiro --- vcd/resource_vcd_ui_plugin.go | 70 ++++++++++++++++++++++++------ vcd/resource_vcd_ui_plugin_test.go | 45 ++++++++++++++++--- website/docs/r/ui_plugin.markdown | 28 ++++++++++++ 3 files changed, 124 insertions(+), 19 deletions(-) diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index 96b2c759e..d95cd80db 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -19,6 +19,9 @@ func resourceVcdUIPlugin() *schema.Resource { ReadContext: resourceVcdUIPluginRead, UpdateContext: resourceVcdUIPluginUpdate, DeleteContext: resourceVcdUIPluginDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceVcdUIPluginImport, + }, Schema: map[string]*schema.Schema{ "plugin_path": { Type: schema.TypeString, @@ -201,24 +204,31 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte dSet(d, "enabled", uiPlugin.UIPluginMetadata.Enabled) dSet(d, "description", uiPlugin.UIPluginMetadata.Description) dSet(d, "status", uiPlugin.UIPluginMetadata.PluginStatus) - if origin == "datasource" { - orgRefs, err := uiPlugin.GetPublishedTenants() - if err != nil { - return diag.Errorf("could not update the published Organizations of the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) - } - var orgIds = make([]string, len(orgRefs)) - for i, orgRef := range orgRefs { - orgIds[i] = orgRef.ID - } - err = d.Set("tenant_ids", convertStringsToTypeSet(orgIds)) - if err != nil { - return diag.FromErr(err) - } + err = setUIPluginTenantIds(uiPlugin, d, origin) + if err != nil { + return diag.FromErr(err) } d.SetId(uiPlugin.UIPluginMetadata.ID) return nil } +// setUIPluginTenantIds reads the published tenants for a given UI Plugin. +func setUIPluginTenantIds(uiPlugin *govcd.UIPlugin, d *schema.ResourceData, origin string) error { + // tenant_ids is only Computed in a data source or during an import + if origin != "datasource" && origin != "import" { + return nil + } + orgRefs, err := uiPlugin.GetPublishedTenants() + if err != nil { + return fmt.Errorf("could not update the published Organizations of the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) + } + var orgIds = make([]string, len(orgRefs)) + for i, orgRef := range orgRefs { + orgIds[i] = orgRef.ID + } + return d.Set("tenant_ids", convertStringsToTypeSet(orgIds)) +} + func resourceVcdUIPluginUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { vcdClient := meta.(*VCDClient) uiPlugin, err := getUIPlugin(vcdClient, d, "resource") @@ -260,3 +270,37 @@ func resourceVcdUIPluginDelete(_ context.Context, d *schema.ResourceData, meta i return nil } + +// resourceVcdUIPluginImport is responsible for importing the resource. +// The following steps happen as part of import +// 1. The user supplies `terraform import _resource_name_ _the_id_string_` command +// 2. `_the_id_string_` contains a dot formatted path to resource as in the example below +// 3. The functions splits the dot-formatted path and tries to lookup the object +// 4. If the lookup succeeds it sets the ID field for `_resource_name_` resource in state file +// (the resource must be already defined in .tf config otherwise `terraform import` will complain) +// 5. `terraform refresh` is being implicitly launched. The Read method looks up all other fields +// based on the known ID of object. +// +// Example resource name (_resource_name_): vcd_ui_plugin.existing_ui_plugin +// Example import path (_the_id_string_): VMware."Customize Portal".3.1.4 +// Note: the separator can be changed using Provider.import_separator or variable VCD_IMPORT_SEPARATOR +func resourceVcdUIPluginImport(_ context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + resourceURI := strings.Split(d.Id(), ImportSeparator) + if len(resourceURI) < 3 { + return nil, fmt.Errorf("resource identifier must be specified as vendor.pluginName.version") + } + vendor, name, version := resourceURI[0], resourceURI[1], strings.Join(resourceURI[2:], ".") + + vcdClient := meta.(*VCDClient) + uiPlugin, err := vcdClient.GetUIPlugin(vendor, name, version) + if err != nil { + return nil, fmt.Errorf("error finding UI Plugin with vendor %s, nss %s and version %s: %s", vendor, name, version, err) + } + + err = setUIPluginTenantIds(uiPlugin, d, "import") + if err != nil { + return nil, err + } + d.SetId(uiPlugin.UIPluginMetadata.ID) + return []*schema.ResourceData{d}, nil +} diff --git a/vcd/resource_vcd_ui_plugin_test.go b/vcd/resource_vcd_ui_plugin_test.go index 6c5700f1c..4e27cb3be 100644 --- a/vcd/resource_vcd_ui_plugin_test.go +++ b/vcd/resource_vcd_ui_plugin_test.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/vmware/go-vcloud-director/v2/types/v56" "regexp" "testing" ) @@ -14,6 +15,18 @@ func init() { testingTags["plugin"] = "resource_vcd_ui_plugin_test.go" } +// This object is equivalent to the manifest.json that is inside the ../test-resources/ui_plugin.zip file +var testUIPluginMetadata = &types.UIPluginMetadata{ + Vendor: "VMware", + License: "BSD-2-Clause", + Link: "http://www.vmware.com", + PluginName: "Test Plugin", + Version: "1.2.3", + Description: "Test Plugin description", + ProviderScoped: true, + TenantScoped: true, +} + func TestAccVcdUiPlugin(t *testing.T) { preTestChecks(t) skipIfNotSysAdmin(t) @@ -64,12 +77,12 @@ func TestAccVcdUiPlugin(t *testing.T) { testCheckResourceCommonUIPluginAsserts := func(resourcePath string) resource.TestCheckFunc { return resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(resourcePath, "vendor", "VMware"), - resource.TestCheckResourceAttr(resourcePath, "name", "Test Plugin"), - resource.TestCheckResourceAttr(resourcePath, "version", "1.2.3"), - resource.TestCheckResourceAttr(resourcePath, "license", "BSD-2-Clause"), - resource.TestCheckResourceAttr(resourcePath, "description", "Test Plugin description"), - resource.TestCheckResourceAttr(resourcePath, "link", "http://www.vmware.com"), + resource.TestCheckResourceAttr(resourcePath, "vendor", testUIPluginMetadata.Vendor), + resource.TestCheckResourceAttr(resourcePath, "name", testUIPluginMetadata.PluginName), + resource.TestCheckResourceAttr(resourcePath, "version", testUIPluginMetadata.Version), + resource.TestCheckResourceAttr(resourcePath, "license", testUIPluginMetadata.License), + resource.TestCheckResourceAttr(resourcePath, "description", testUIPluginMetadata.Description), + resource.TestCheckResourceAttr(resourcePath, "link", testUIPluginMetadata.Link), resource.TestMatchResourceAttr(resourcePath, "status", regexp.MustCompile("^ready|unavailable$")), ) } @@ -138,8 +151,18 @@ func TestAccVcdUiPlugin(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "enabled", dsName, "enabled"), resource.TestCheckResourceAttrPair(resourceName, "link", dsName, "link"), resource.TestCheckResourceAttrPair(resourceName, "tenant_ids.#", dsName, "tenant_ids.#"), + func(state *terraform.State) error { + return nil + }, ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: importStateIdUIPlugin(testUIPluginMetadata.Vendor, testUIPluginMetadata.PluginName, testUIPluginMetadata.Version), + ImportStateVerifyIgnore: []string{"plugin_path"}, + }, }, }) postTestChecks(t) @@ -182,3 +205,13 @@ func testAccCheckUIPluginDestroy(id string) resource.TestCheckFunc { return nil } } + +func importStateIdUIPlugin(vendor, name, version string) resource.ImportStateIdFunc { + return func(*terraform.State) (string, error) { + return vendor + + ImportSeparator + + name + + ImportSeparator + + version, nil + } +} diff --git a/website/docs/r/ui_plugin.markdown b/website/docs/r/ui_plugin.markdown index e47c27d4c..b5d8126f3 100644 --- a/website/docs/r/ui_plugin.markdown +++ b/website/docs/r/ui_plugin.markdown @@ -79,3 +79,31 @@ The following arguments are supported: * `description` - The description of the UI Plugin * `status` - The status of the UI Plugin (for example, `ready`, `unavailable`, etc) +## Importing + +~> **Note:** The current implementation of Terraform import can only import resources into the state. It does not generate +configuration. [More information.][docs-import] + +An existing UI Plugin can be [imported][docs-import] into this resource via supplying its vendor, name and version, which +unequivocally identifies it. +For example, using this structure, representing an existing UI Plugin that was **not** created using Terraform: + +```hcl +resource "vcd_ui_plugin" "my_existing_plugin" { + # `plugin_path` is not needed as it was already created + enabled = true +} +``` + +For example, you can import the "Customize Portal" UI Plugin into Terraform state using this command + +``` +terraform import vcd_ui_plugin.my_plugin VMware."Customize Portal".3.1.4 +``` + +NOTE: the default separator (.) can be changed using Provider.import_separator or variable VCD_IMPORT_SEPARATOR + +[docs-import]:https://www.terraform.io/docs/import/ + +After that, you can expand the configuration file and either update or delete the UI Plugin as needed. Running `terraform plan` +at this stage will show the difference between the minimal configuration file and the UI Plugin's stored properties. From 5c1ed736cb1e80bf6ff7232050ed9cbac47754bd Mon Sep 17 00:00:00 2001 From: abarreiro Date: Fri, 26 May 2023 14:26:19 +0200 Subject: [PATCH 14/28] Update CSE guide Signed-off-by: abarreiro --- ...step1.tf => 3.10-cse-4.0-install-step1.tf} | 4 +-- ...step2.tf => 3.10-cse-4.0-install-step2.tf} | 25 ++++++++++++++---- .../install/step2/terraform.tfvars.example | 8 ++++++ .../install/step2/variables.tf | 9 +++++++ ...tainer_service_extension_4_0.html.markdown | 26 ++++++++++++------- 5 files changed, 56 insertions(+), 16 deletions(-) rename examples/container-service-extension-4.0/install/step1/{3.9-cse-4.0-install-step1.tf => 3.10-cse-4.0-install-step1.tf} (97%) rename examples/container-service-extension-4.0/install/step2/{3.9-cse-4.0-install-step2.tf => 3.10-cse-4.0-install-step2.tf} (97%) diff --git a/examples/container-service-extension-4.0/install/step1/3.9-cse-4.0-install-step1.tf b/examples/container-service-extension-4.0/install/step1/3.10-cse-4.0-install-step1.tf similarity index 97% rename from examples/container-service-extension-4.0/install/step1/3.9-cse-4.0-install-step1.tf rename to examples/container-service-extension-4.0/install/step1/3.10-cse-4.0-install-step1.tf index 8896b424d..6248d1f70 100644 --- a/examples/container-service-extension-4.0/install/step1/3.9-cse-4.0-install-step1.tf +++ b/examples/container-service-extension-4.0/install/step1/3.10-cse-4.0-install-step1.tf @@ -15,12 +15,12 @@ # You can check the comments on each resource/data source for more help and context. # ------------------------------------------------------------------------------------------------------------ -# VCD Provider configuration. It must be at least v3.9.0 and configured with a System administrator account. +# VCD Provider configuration. It must be at least v3.10.0 and configured with a System administrator account. terraform { required_providers { vcd = { source = "vmware/vcd" - version = ">= 3.9" + version = ">= 3.10" } } } diff --git a/examples/container-service-extension-4.0/install/step2/3.9-cse-4.0-install-step2.tf b/examples/container-service-extension-4.0/install/step2/3.10-cse-4.0-install-step2.tf similarity index 97% rename from examples/container-service-extension-4.0/install/step2/3.9-cse-4.0-install-step2.tf rename to examples/container-service-extension-4.0/install/step2/3.10-cse-4.0-install-step2.tf index 3275bdcf8..e3a1d5eac 100644 --- a/examples/container-service-extension-4.0/install/step2/3.9-cse-4.0-install-step2.tf +++ b/examples/container-service-extension-4.0/install/step2/3.10-cse-4.0-install-step2.tf @@ -4,7 +4,7 @@ # * Please read the guide present at https://registry.terraform.io/providers/vmware/vcd/latest/docs/guides/container_service_extension_4_0 # before applying this configuration. # -# * Please apply "3.9-cse-4.0-install-step1.tf" first, located at +# * Please apply "3.10-cse-4.0-install-step1.tf" first, located at # https://github.com/vmware/terraform-provider-vcd/tree/main/examples/container-service-extension-4.0/install/step1 # # * Please review this HCL configuration before applying, to change the settings to the ones that fit best with your organization. @@ -15,13 +15,13 @@ # You can check the comments on each resource/data source for more help and context. # ------------------------------------------------------------------------------------------------------------ -# VCD Provider configuration. It must be at least v3.9.0 and configured with a System administrator account. +# VCD Provider configuration. It must be at least v3.10.0 and configured with a System administrator account. # This is needed to build the minimum setup for CSE v4.0 to work, like Organizations, VDCs, Provider Gateways, etc. terraform { required_providers { vcd = { source = "vmware/vcd" - version = ">= 3.9" + version = ">= 3.10" } } } @@ -293,7 +293,7 @@ resource "vcd_catalog_vapp_template" "cse_ova" { ova_path = format("%s/%s", var.cse_ova_folder, var.cse_ova_file) } -# Fetch the RDE Type created in 3.9-cse-4.0-install-step1.tf. This is required to be able to create the following +# Fetch the RDE Type created in 3.10-cse-4.0-install-step1.tf. This is required to be able to create the following # Rights Bundle. data "vcd_rde_type" "existing_capvcdcluster_type" { vendor = "vmware" @@ -675,7 +675,7 @@ resource "vcd_nsxt_firewall" "tenant_firewall" { } } -# Fetch the RDE Type created in 3.9-cse-4.0-install-step1.tf, as we need to create the configuration instance. +# Fetch the RDE Type created in 3.10-cse-4.0-install-step1.tf, as we need to create the configuration instance. data "vcd_rde_type" "existing_vcdkeconfig_type" { vendor = "vmware" nss = "VCDKEConfig" @@ -769,6 +769,21 @@ resource "vcd_vapp_vm" "cse_server_vm" { ] } +data "vcd_org" "system_org" { + name = var.administrator_org +} + +resource vcd_ui_plugin "k8s_container_clusters_ui_plugin" { + count = var.k8s_container_clusters_ui_plugin_path == "" ? 0 : 1 + plugin_path = var.k8s_container_clusters_ui_plugin_path + enabled = true + tenant_ids = [ + data.vcd_org.system_org.id, + vcd_org.solutions_organization.id, + vcd_org.tenant_organization.id, + ] +} + output "publish_ui_plugin" { value = "When CSE Server '${vcd_vapp_vm.cse_server_vm.name}' is ready, please install the Kubernetes Container Clusters UI plug-in 4.0 for VCD that you can download from https://docs.vmware.com/en/VMware-Cloud-Director-Container-Service-Extension/index.html" } diff --git a/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example b/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example index c9172b000..050dd09cc 100644 --- a/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example +++ b/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example @@ -109,3 +109,11 @@ github_personal_access_token = "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # This user was created in Step 1. You need to provide a valid API token for it cse_admin_user = "cse-admin" cse_admin_api_token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# ------------------------------------------------ +# Other configuration +# ------------------------------------------------ +# This path points to the .zip file that contains the bundled Kubernetes Container Clusters UI Plugin. +# It is optional, if not informed it won't be installed. +# Remember to remove older CSE UI plugins if present (for example 3.x plugins) after installing this one. +k8s_container_clusters_ui_plugin_path = "/home/change-me/container-ui-plugin 4.0.zip" \ No newline at end of file diff --git a/examples/container-service-extension-4.0/install/step2/variables.tf b/examples/container-service-extension-4.0/install/step2/variables.tf index 262e09bfb..4c713d5f3 100644 --- a/examples/container-service-extension-4.0/install/step2/variables.tf +++ b/examples/container-service-extension-4.0/install/step2/variables.tf @@ -313,3 +313,12 @@ variable "syslog_port" { description = "VCDKEConfig: Port for system logs" default = "" } + +# ------------------------------------------------ +# Other configuration +# ------------------------------------------------ +variable "k8s_container_clusters_ui_plugin_path" { + type = string + description = "Path to the Kubernetes Container Clusters UI Plugin zip file" + default = "" +} \ No newline at end of file diff --git a/website/docs/guides/container_service_extension_4_0.html.markdown b/website/docs/guides/container_service_extension_4_0.html.markdown index 253729d0f..30d32eb3e 100644 --- a/website/docs/guides/container_service_extension_4_0.html.markdown +++ b/website/docs/guides/container_service_extension_4_0.html.markdown @@ -22,7 +22,7 @@ To know more about CSE v4.0, you can visit [the documentation][cse_docs]. In order to complete the steps described in this guide, please be aware: * CSE v4.0 is supported from VCD v10.4.0 or above, make sure your VCD appliance matches the criteria. -* Terraform provider needs to be v3.9.0 or above. +* Terraform provider needs to be v3.10.0 or above. * Both CSE Server and the Bootstrap clusters require outbound Internet connectivity. * CSE v4.0 makes use of [ALB](/providers/vmware/vcd/latest/docs/guides/nsxt_alb) capabilities. @@ -235,7 +235,7 @@ If you wish to have a different networking setup, please modify the [proposed co ### CSE Server -The final set of resources created by the [proposed configuration][step2] correspond to the CSE Server vApp. +There is also a set of resources created by the [proposed configuration][step2] that correspond to the CSE Server vApp. The generated VM makes use of the uploaded CSE OVA and some required guest properties. In order to do so, the [configuration][step2] asks for the following variables that you can customise in `terraform.tfvars`: @@ -254,6 +254,20 @@ In order to do so, the [configuration][step2] asks for the following variables t - `cse_admin_user`: This should reference the CSE Administrator [User][user] that was created in Step 1. - `cse_admin_api_token`: This should be the API token that you created for the CSE Administrator after Step 1. +### UI plugin installation + +The final resource created by the [proposed configuration][step2] is the [`vcd_ui_plugin`][ui_plugin] resource. + +This resource is optional, it will be only created if the variable `k8s_container_clusters_ui_plugin_path` is not empty, +so you can leverage whether your tenant users or system administrators will need it or not. It can be useful for troubleshooting, +or if your tenant users are not familiar with Terraform, they will be still able to create and manage their clusters +with the UI. + +If you decide to install it, `k8s_container_clusters_ui_plugin_path` should point to the +[Kubernetes Container Clusters UI plug-in v4.0][cse_docs] ZIP file that you can download in the [CSE documentation][cse_docs]. + +-> If the old CSE 3.x plugin is installed, you will need to remove it also. + ### Final considerations #### Verifying that the setup works @@ -283,13 +297,6 @@ resource "vcd_nsxt_nat_rule" "solutions_nat" { Once you gain access to the CSE Server, you can check the `cse.log` file, the configuration file or check Internet connectivity. If something does not work, please check the **Troubleshooting** section below. -#### Install the UI plugin - -To manage CSE clusters with the UI, you can [download the Kubernetes Container Clusters UI plug-in 4.0][cse_docs] -and install it in your VCD appliance. If the old CSE 3.x plugin is installed, you will need to remove it first. The plugin -will allow tenants to create Kubernetes clusters with the UI wizard. Providers should still use the proposed Terraform configuration -to perform updates on the CSE Server (see sections below). - #### Troubleshooting To evaluate the correctness of the setup, you can check the _"Verifying that the setup works"_ section above. @@ -407,6 +414,7 @@ Once all clusters are removed in the background by CSE Server, you may destroy t [step2]: https://github.com/vmware/terraform-provider-vcd/tree/main/examples/container-service-extension-4.0/install/step2 [tkgm_docs]: https://docs.vmware.com/en/VMware-Tanzu-Kubernetes-Grid/index.html [user]: /providers/vmware/vcd/latest/docs/resources/org_user +[ui_plugin]: /providers/vmware/vcd/latest/docs/resources/ui_plugin [catalog_vapp_template]: /providers/vmware/vcd/latest/docs/resources/catalog_vapp_template [vdc]: /providers/vmware/vcd/latest/docs/resources/org_vdc [vm]: /providers/vmware/vcd/latest/docs/resources/vapp_vm From 55aa6de3d874d692a5a456c197a9902aaba7c510 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Fri, 26 May 2023 14:48:41 +0200 Subject: [PATCH 15/28] nit Signed-off-by: abarreiro --- .../install/step2/terraform.tfvars.example | 2 +- .../install/step2/variables.tf | 2 +- vcd/resource_vcd_ui_plugin_test.go | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example b/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example index 050dd09cc..584fd5b7d 100644 --- a/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example +++ b/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example @@ -116,4 +116,4 @@ cse_admin_api_token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # This path points to the .zip file that contains the bundled Kubernetes Container Clusters UI Plugin. # It is optional, if not informed it won't be installed. # Remember to remove older CSE UI plugins if present (for example 3.x plugins) after installing this one. -k8s_container_clusters_ui_plugin_path = "/home/change-me/container-ui-plugin 4.0.zip" \ No newline at end of file +k8s_container_clusters_ui_plugin_path = "/home/change-me/container-ui-plugin 4.0.zip" diff --git a/examples/container-service-extension-4.0/install/step2/variables.tf b/examples/container-service-extension-4.0/install/step2/variables.tf index 4c713d5f3..f4b64e4cc 100644 --- a/examples/container-service-extension-4.0/install/step2/variables.tf +++ b/examples/container-service-extension-4.0/install/step2/variables.tf @@ -321,4 +321,4 @@ variable "k8s_container_clusters_ui_plugin_path" { type = string description = "Path to the Kubernetes Container Clusters UI Plugin zip file" default = "" -} \ No newline at end of file +} diff --git a/vcd/resource_vcd_ui_plugin_test.go b/vcd/resource_vcd_ui_plugin_test.go index 4e27cb3be..cf9aca5af 100644 --- a/vcd/resource_vcd_ui_plugin_test.go +++ b/vcd/resource_vcd_ui_plugin_test.go @@ -68,6 +68,7 @@ func TestAccVcdUiPlugin(t *testing.T) { debugPrintf("#[DEBUG] CONFIGURATION Step 2: %s\n", step2Config) debugPrintf("#[DEBUG] CONFIGURATION Step 3: %s\n", step3Config) debugPrintf("#[DEBUG] CONFIGURATION Step 4: %s\n", step4Config) + debugPrintf("#[DEBUG] CONFIGURATION Step 5: %s\n", step5Config) if vcdShortTest { t.Skip(acceptanceTestsSkipped) return @@ -178,8 +179,9 @@ data "vcd_org" "org2" { } resource "vcd_ui_plugin" "plugin" { - plugin_path = "{{.PluginPath}}" - enabled = {{.Enabled}} + plugin_path = "{{.PluginPath}}" + enabled = {{.Enabled}} + {{.TenantIds}} {{.ProviderScoped}} {{.TenantScoped}} From ccbec6fe9235a152a3d3833fb7b45bbfe6fd0bae Mon Sep 17 00:00:00 2001 From: abarreiro Date: Fri, 26 May 2023 14:51:53 +0200 Subject: [PATCH 16/28] Add changelog Signed-off-by: abarreiro --- .changes/v3.10.0/1059-improvements.md | 1 + .../install/step2/3.10-cse-4.0-install-step2.tf | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) create mode 100644 .changes/v3.10.0/1059-improvements.md diff --git a/.changes/v3.10.0/1059-improvements.md b/.changes/v3.10.0/1059-improvements.md new file mode 100644 index 000000000..2f0f71029 --- /dev/null +++ b/.changes/v3.10.0/1059-improvements.md @@ -0,0 +1 @@ +* The guide to install Container Service Extension v4.0 now additionally installs the Kubernetes Container Clusters UI Plugin [GH-1059] diff --git a/examples/container-service-extension-4.0/install/step2/3.10-cse-4.0-install-step2.tf b/examples/container-service-extension-4.0/install/step2/3.10-cse-4.0-install-step2.tf index e3a1d5eac..d0a850084 100644 --- a/examples/container-service-extension-4.0/install/step2/3.10-cse-4.0-install-step2.tf +++ b/examples/container-service-extension-4.0/install/step2/3.10-cse-4.0-install-step2.tf @@ -783,7 +783,3 @@ resource vcd_ui_plugin "k8s_container_clusters_ui_plugin" { vcd_org.tenant_organization.id, ] } - -output "publish_ui_plugin" { - value = "When CSE Server '${vcd_vapp_vm.cse_server_vm.name}' is ready, please install the Kubernetes Container Clusters UI plug-in 4.0 for VCD that you can download from https://docs.vmware.com/en/VMware-Cloud-Director-Container-Service-Extension/index.html" -} From cc56b8a7c15f50094d84fe7f04048423389961dd Mon Sep 17 00:00:00 2001 From: abarreiro Date: Fri, 26 May 2023 20:33:39 +0200 Subject: [PATCH 17/28] Fix regex Signed-off-by: abarreiro --- vcd/resource_vcd_ui_plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index d95cd80db..8a302cfe7 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -28,7 +28,7 @@ func resourceVcdUIPlugin() *schema.Resource { Optional: true, ForceNew: true, ValidateDiagFunc: func(value interface{}, _ cty.Path) diag.Diagnostics { - ok, err := regexp.MatchString(`^.+\.[z|Z][i|I][p|P]$`, value.(string)) + ok, err := regexp.MatchString(`(?i)^.+\.zip$`, value.(string)) if err != nil { return diag.Errorf("could not validate %s", value.(string)) } From 3711ae0931793ef7adf0541d1014ffd6d0bd624a Mon Sep 17 00:00:00 2001 From: abarreiro Date: Tue, 30 May 2023 10:44:19 +0200 Subject: [PATCH 18/28] Fix docs extensions Signed-off-by: abarreiro --- website/docs/d/{ui_plugin.markdown => ui_plugin.html.markdown} | 0 website/docs/r/{ui_plugin.markdown => ui_plugin.html.markdown} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename website/docs/d/{ui_plugin.markdown => ui_plugin.html.markdown} (100%) rename website/docs/r/{ui_plugin.markdown => ui_plugin.html.markdown} (100%) diff --git a/website/docs/d/ui_plugin.markdown b/website/docs/d/ui_plugin.html.markdown similarity index 100% rename from website/docs/d/ui_plugin.markdown rename to website/docs/d/ui_plugin.html.markdown diff --git a/website/docs/r/ui_plugin.markdown b/website/docs/r/ui_plugin.html.markdown similarity index 100% rename from website/docs/r/ui_plugin.markdown rename to website/docs/r/ui_plugin.html.markdown From 4d3b15928e079889a6318a6755f323690833cde9 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Tue, 30 May 2023 10:52:01 +0200 Subject: [PATCH 19/28] fmt hcl Signed-off-by: abarreiro --- website/docs/d/ui_plugin.html.markdown | 2 ++ website/docs/r/ui_plugin.html.markdown | 18 +++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/website/docs/d/ui_plugin.html.markdown b/website/docs/d/ui_plugin.html.markdown index d65313fc5..87d0087b2 100644 --- a/website/docs/d/ui_plugin.html.markdown +++ b/website/docs/d/ui_plugin.html.markdown @@ -22,9 +22,11 @@ data "vcd_ui_plugin" "existing_ui_plugin" { name = "Customize Portal" version = "3.1.4" } + output "license" { value = data.vcd_ui_plugin.existing_ui_plugin.license } + output "tenants" { value = data.vcd_ui_plugin.existing_ui_plugin.tenant_ids } diff --git a/website/docs/r/ui_plugin.html.markdown b/website/docs/r/ui_plugin.html.markdown index b5d8126f3..5ea754c85 100644 --- a/website/docs/r/ui_plugin.html.markdown +++ b/website/docs/r/ui_plugin.html.markdown @@ -19,21 +19,21 @@ Supported in provider *v3.10+* and requires VCD 10.2+ ```hcl locals { - my_plugin_orgs = [ - "myOrg1", - "myOrg2" - ] + my_plugin_orgs = [ + "myOrg1", + "myOrg2" + ] } data "vcd_org" "my_plugin_orgs" { - count = length(local.my_plugin_orgs) - name = local.my_plugin_orgs[count.index] + count = length(local.my_plugin_orgs) + name = local.my_plugin_orgs[count.index] } resource "vcd_ui_plugin" "my_plugin" { - plugin_path = "./container-ui-plugin-4.0.zip" - enabled = true - tenant_ids = data.vcd_org.my_plugin_orgs[*].id + plugin_path = "./container-ui-plugin-4.0.zip" + enabled = true + tenant_ids = data.vcd_org.my_plugin_orgs[*].id } ``` From 0e082e23012189ea1189c16523cb01bcb2be5aa6 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Wed, 7 Jun 2023 09:23:16 +0200 Subject: [PATCH 20/28] Simplify example Signed-off-by: abarreiro --- website/docs/r/ui_plugin.html.markdown | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/website/docs/r/ui_plugin.html.markdown b/website/docs/r/ui_plugin.html.markdown index 5ea754c85..ccad22b8f 100644 --- a/website/docs/r/ui_plugin.html.markdown +++ b/website/docs/r/ui_plugin.html.markdown @@ -40,21 +40,16 @@ resource "vcd_ui_plugin" "my_plugin" { ## Example Usage publishing to all Organizations available ```hcl -data "vcd_resource_list" "list_of_orgs" { - name = "list_of_orgs" +data "vcd_resource_list" "all_orgs" { + name = "all_orgs" resource_type = "vcd_org" - list_mode = "name" -} - -data "vcd_org" "all_orgs" { - count = length(data.vcd_resource_list.list_of_orgs.list) - name = data.vcd_resource_list.list_of_orgs.list[count.index] + list_mode = "id" } resource "vcd_ui_plugin" "my_plugin" { plugin_path = "./container-ui-plugin-4.0.zip" enabled = true - tenant_ids = data.vcd_org.all_orgs[*].id + tenant_ids = data.vcd_resource_list.all_orgs.list } ``` From 7c4114d07d7e01a87b9796f093c1bfe4d7bcf4a9 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Wed, 7 Jun 2023 09:33:03 +0200 Subject: [PATCH 21/28] nit Signed-off-by: abarreiro --- website/docs/d/ui_plugin.html.markdown | 2 +- website/docs/r/ui_plugin.html.markdown | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/d/ui_plugin.html.markdown b/website/docs/d/ui_plugin.html.markdown index 87d0087b2..1933e06da 100644 --- a/website/docs/d/ui_plugin.html.markdown +++ b/website/docs/d/ui_plugin.html.markdown @@ -10,7 +10,7 @@ description: |- Provides a VMware Cloud Director UI Plugin data source. This can be used to fetch and read an existing UI Plugin. --> Reading UI Plugins requires System administrator privileges. +-> Reading UI Plugins requires System Administrator privileges. Supported in provider *v3.10+* and requires VCD 10.2+ diff --git a/website/docs/r/ui_plugin.html.markdown b/website/docs/r/ui_plugin.html.markdown index ccad22b8f..a68844092 100644 --- a/website/docs/r/ui_plugin.html.markdown +++ b/website/docs/r/ui_plugin.html.markdown @@ -11,7 +11,7 @@ description: |- Provides a VMware Cloud Director UI Plugin resource. This can be used to manage UI Plugins in VCD, for example to add a new plugin from an local ZIP file, to publish/unpublish a UI Plugin to different Organizations, etc. --> Creating, updating and deleting UI Plugins requires System administrator privileges. +-> Creating, updating and deleting UI Plugins requires System Administrator privileges. Supported in provider *v3.10+* and requires VCD 10.2+ From 631d20c9e7e2fac9cf0017326a6b9b777b05ed03 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Tue, 20 Jun 2023 10:46:31 +0200 Subject: [PATCH 22/28] Checkpoint Signed-off-by: abarreiro --- vcd/remove_leftovers_test.go | 3 +- vcd/resource_vcd_ui_plugin.go | 41 ++++++++++++++++++-------- website/docs/r/ui_plugin.html.markdown | 2 +- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/vcd/remove_leftovers_test.go b/vcd/remove_leftovers_test.go index 52db2259f..a5d92cef1 100644 --- a/vcd/remove_leftovers_test.go +++ b/vcd/remove_leftovers_test.go @@ -309,11 +309,12 @@ func removeLeftovers(govcdClient *govcd.VCDClient, verbose bool) error { return fmt.Errorf("error retrieving UI Plugins: %s", err) } for _, uiPlugin := range uiPlugins { + // This will delete all UI Plugins that match the `isTest` regex. toBeDeleted := shouldDeleteEntity(alsoDelete, doNotDelete, uiPlugin.UIPluginMetadata.PluginName, "vcd_ui_plugin", 1, verbose) if toBeDeleted { err = deleteUIPlugin(uiPlugin) if err != nil { - return fmt.Errorf("error deleting UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) + return err } } } diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index 8a302cfe7..28c45dd23 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -128,28 +128,45 @@ func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta // publishUIPluginToTenants performs a publish/unpublish operation for the given UI plugin. func publishUIPluginToTenants(vcdClient *VCDClient, uiPlugin *govcd.UIPlugin, d *schema.ResourceData, operation string) error { if d.HasChange("tenant_ids") { - orgsToPublish := d.Get("tenant_ids").(*schema.Set).List() + // We get all the Organizations instead of one by one, as there could be thousands. existingOrgs, err := vcdClient.GetOrgList() if err != nil { - return fmt.Errorf("could not publish the UI Plugin %s to Organizations '%v': %s", uiPlugin.UIPluginMetadata.ID, orgsToPublish, err) + return fmt.Errorf("UI Plugin '%s' update failed, could not retrieve all the Organizations: %s", uiPlugin.UIPluginMetadata.ID, err) + } + oldRaw, newRaw := d.GetChange("tenant_ids") + + // Retrieve the Organization IDs that need to be unpublished + newOrgIds := oldRaw.(*schema.Set) + var orgIdsToUnpublish []interface{} + for _, oldOrgId := range newRaw.(*schema.Set).List() { + if !newOrgIds.Contains(oldOrgId) { + orgIdsToUnpublish = append(orgIdsToUnpublish, oldOrgId) + } } - var orgsToPubRefs types.OpenApiReferences - for _, org := range existingOrgs.Org { - for _, orgId := range orgsToPublish { - // We do this as org.ID is empty, so we need to reconstruct the URN with the HREF - uuid := extractUuid(orgId.(string)) - if strings.Contains(org.HREF, uuid) { - orgsToPubRefs = append(orgsToPubRefs, types.OpenApiReference{ID: "urn:cloud:org:" + uuid, Name: org.Name}) + + getOrgReferences := func(orgIds []interface{}, allOrgs *types.OrgList) types.OpenApiReferences { + var orgRefs types.OpenApiReferences + for _, org := range allOrgs.Org { + for _, orgId := range orgIds { + // We do this as org.ID is empty, so we need to reconstruct the URN with the HREF + uuid := extractUuid(orgId.(string)) + if strings.Contains(org.HREF, uuid) { + orgRefs = append(orgRefs, types.OpenApiReference{ID: "urn:cloud:org:" + uuid, Name: org.Name}) + } } } + return orgRefs } + if operation == "update" { - err = uiPlugin.UnpublishAll() // We need to clean up the already-published Orgs to put the new ones during an Update. + orgsToUnpublish := getOrgReferences(orgIdsToUnpublish, existingOrgs) + err = uiPlugin.Unpublish(orgsToUnpublish) if err != nil { - return fmt.Errorf("could not publish the UI Plugin %s to Organizations '%v': %s", uiPlugin.UIPluginMetadata.ID, orgsToPublish, err) + return fmt.Errorf("could not publish the UI Plugin %s to Organizations '%v': %s", uiPlugin.UIPluginMetadata.ID, orgsToUnpublish, err) } } - err = uiPlugin.Publish(orgsToPubRefs) + orgsToPublish := getOrgReferences(newOrgIds.List(), existingOrgs) + err = uiPlugin.Publish(orgsToPublish) if err != nil { return fmt.Errorf("could not publish the UI Plugin %s to Organizations '%v': %s", uiPlugin.UIPluginMetadata.ID, orgsToPublish, err) } diff --git a/website/docs/r/ui_plugin.html.markdown b/website/docs/r/ui_plugin.html.markdown index a68844092..aba971500 100644 --- a/website/docs/r/ui_plugin.html.markdown +++ b/website/docs/r/ui_plugin.html.markdown @@ -13,7 +13,7 @@ plugin from an local ZIP file, to publish/unpublish a UI Plugin to different Org -> Creating, updating and deleting UI Plugins requires System Administrator privileges. -Supported in provider *v3.10+* and requires VCD 10.2+ +Supported in provider *v3.10+* and requires VCD 10.3+ ## Example Usage with specific Organizations to publish From 6dd4bbf0899010183683cbe2d9b25eb4635830f8 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Tue, 20 Jun 2023 11:06:48 +0200 Subject: [PATCH 23/28] Apply suggestion Signed-off-by: abarreiro --- vcd/resource_vcd_ui_plugin.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index 28c45dd23..5b2320aee 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -128,7 +128,9 @@ func resourceVcdUIPluginCreate(ctx context.Context, d *schema.ResourceData, meta // publishUIPluginToTenants performs a publish/unpublish operation for the given UI plugin. func publishUIPluginToTenants(vcdClient *VCDClient, uiPlugin *govcd.UIPlugin, d *schema.ResourceData, operation string) error { if d.HasChange("tenant_ids") { - // We get all the Organizations instead of one by one, as there could be thousands. + // We get all the Organizations because we need to retrieve the Organization names, in order to build + // OpenApiReference objects. Publish/Unpublish doesn't work without an Organization name in the OpenApiReferences payload, + // so we can't use convertSliceOfStringsToOpenApiReferenceIds. existingOrgs, err := vcdClient.GetOrgList() if err != nil { return fmt.Errorf("UI Plugin '%s' update failed, could not retrieve all the Organizations: %s", uiPlugin.UIPluginMetadata.ID, err) @@ -136,14 +138,16 @@ func publishUIPluginToTenants(vcdClient *VCDClient, uiPlugin *govcd.UIPlugin, d oldRaw, newRaw := d.GetChange("tenant_ids") // Retrieve the Organization IDs that need to be unpublished - newOrgIds := oldRaw.(*schema.Set) + newOrgIds := newRaw.(*schema.Set) var orgIdsToUnpublish []interface{} - for _, oldOrgId := range newRaw.(*schema.Set).List() { + for _, oldOrgId := range oldRaw.(*schema.Set).List() { if !newOrgIds.Contains(oldOrgId) { orgIdsToUnpublish = append(orgIdsToUnpublish, oldOrgId) } } + // This function is similar to convertSliceOfStringsToOpenApiReferenceIds, but here we need the + // Organization names, otherwise Publish/Unpublish don't work as expected. getOrgReferences := func(orgIds []interface{}, allOrgs *types.OrgList) types.OpenApiReferences { var orgRefs types.OpenApiReferences for _, org := range allOrgs.Org { From e66ea42d6386c2efb33c0e183a3ea3f83ca69de4 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Tue, 20 Jun 2023 11:14:10 +0200 Subject: [PATCH 24/28] Apply suggestion Signed-off-by: abarreiro --- website/docs/d/ui_plugin.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/d/ui_plugin.html.markdown b/website/docs/d/ui_plugin.html.markdown index 1933e06da..d2a4125df 100644 --- a/website/docs/d/ui_plugin.html.markdown +++ b/website/docs/d/ui_plugin.html.markdown @@ -12,7 +12,7 @@ Provides a VMware Cloud Director UI Plugin data source. This can be used to fetc -> Reading UI Plugins requires System Administrator privileges. -Supported in provider *v3.10+* and requires VCD 10.2+ +Supported in provider *v3.10+* and requires VCD 10.3+ ## Example Usage From 11b6d7bf05550e909ce349bf5834b05512d194d9 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Tue, 20 Jun 2023 14:32:41 +0200 Subject: [PATCH 25/28] Bump Signed-off-by: abarreiro --- go.mod | 2 +- go.sum | 4 ++-- vcd/resource_vcd_ui_plugin.go | 10 +++------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index f7cfbcba3..01205d69b 100644 --- a/go.mod +++ b/go.mod @@ -61,4 +61,4 @@ require ( google.golang.org/protobuf v1.28.1 // indirect ) -replace github.com/vmware/go-vcloud-director/v2 => github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230526090050-e6e6ef3844ee +replace github.com/vmware/go-vcloud-director/v2 => github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230620122415-d8024db008ea diff --git a/go.sum b/go.sum index ae83c6c5a..c2ecd8280 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C6 github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= -github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230526090050-e6e6ef3844ee h1:ghaFIj7spkheFoIOFhnfNP5k/jyInidf7K9gf6v4c7g= -github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230526090050-e6e6ef3844ee/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw= +github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230620122415-d8024db008ea h1:gffjEzHqzXcQ2KO7XocDUdKgFDyGMF5wo4Y5CGbO0aY= +github.com/adambarreiro/go-vcloud-director/v2 v2.17.0-alpha.1.0.20230620122415-d8024db008ea/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= diff --git a/vcd/resource_vcd_ui_plugin.go b/vcd/resource_vcd_ui_plugin.go index 5b2320aee..d75fee85a 100644 --- a/vcd/resource_vcd_ui_plugin.go +++ b/vcd/resource_vcd_ui_plugin.go @@ -225,7 +225,7 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte dSet(d, "enabled", uiPlugin.UIPluginMetadata.Enabled) dSet(d, "description", uiPlugin.UIPluginMetadata.Description) dSet(d, "status", uiPlugin.UIPluginMetadata.PluginStatus) - err = setUIPluginTenantIds(uiPlugin, d, origin) + err = setUIPluginTenantIds(uiPlugin, d) if err != nil { return diag.FromErr(err) } @@ -234,11 +234,7 @@ func genericVcdUIPluginRead(_ context.Context, d *schema.ResourceData, meta inte } // setUIPluginTenantIds reads the published tenants for a given UI Plugin. -func setUIPluginTenantIds(uiPlugin *govcd.UIPlugin, d *schema.ResourceData, origin string) error { - // tenant_ids is only Computed in a data source or during an import - if origin != "datasource" && origin != "import" { - return nil - } +func setUIPluginTenantIds(uiPlugin *govcd.UIPlugin, d *schema.ResourceData) error { orgRefs, err := uiPlugin.GetPublishedTenants() if err != nil { return fmt.Errorf("could not update the published Organizations of the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err) @@ -318,7 +314,7 @@ func resourceVcdUIPluginImport(_ context.Context, d *schema.ResourceData, meta i return nil, fmt.Errorf("error finding UI Plugin with vendor %s, nss %s and version %s: %s", vendor, name, version, err) } - err = setUIPluginTenantIds(uiPlugin, d, "import") + err = setUIPluginTenantIds(uiPlugin, d) if err != nil { return nil, err } From b0764bb30e238f3056807b62798ced952881dff5 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Wed, 21 Jun 2023 16:26:38 +0200 Subject: [PATCH 26/28] Apply suggestions Signed-off-by: abarreiro --- .changes/v3.10.0/1059-improvements.md | 3 ++- .../install/step2/terraform.tfvars.example | 4 ++-- website/docs/r/ui_plugin.html.markdown | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.changes/v3.10.0/1059-improvements.md b/.changes/v3.10.0/1059-improvements.md index 2f0f71029..2da8f2a1c 100644 --- a/.changes/v3.10.0/1059-improvements.md +++ b/.changes/v3.10.0/1059-improvements.md @@ -1 +1,2 @@ -* The guide to install Container Service Extension v4.0 now additionally installs the Kubernetes Container Clusters UI Plugin [GH-1059] +* The guide to install the Container Service Extension v4.0 now additionally explains how to install the + Kubernetes Container Clusters UI Plugin [GH-1059] diff --git a/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example b/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example index 584fd5b7d..58d112542 100644 --- a/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example +++ b/examples/container-service-extension-4.0/install/step2/terraform.tfvars.example @@ -114,6 +114,6 @@ cse_admin_api_token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Other configuration # ------------------------------------------------ # This path points to the .zip file that contains the bundled Kubernetes Container Clusters UI Plugin. -# It is optional, if not informed it won't be installed. -# Remember to remove older CSE UI plugins if present (for example 3.x plugins) after installing this one. +# It is optional: if not set, it won't be installed. +# Remember to remove older CSE UI plugins if present (for example 3.x plugins) before installing this one. k8s_container_clusters_ui_plugin_path = "/home/change-me/container-ui-plugin 4.0.zip" diff --git a/website/docs/r/ui_plugin.html.markdown b/website/docs/r/ui_plugin.html.markdown index aba971500..21cc3bef6 100644 --- a/website/docs/r/ui_plugin.html.markdown +++ b/website/docs/r/ui_plugin.html.markdown @@ -9,9 +9,9 @@ description: |- # vcd\_ui\_plugin Provides a VMware Cloud Director UI Plugin resource. This can be used to manage UI Plugins in VCD, for example to add a new -plugin from an local ZIP file, to publish/unpublish a UI Plugin to different Organizations, etc. +plugin from a local ZIP file, to publish/unpublish a UI Plugin to different Organizations, etc. --> Creating, updating and deleting UI Plugins requires System Administrator privileges. +-> Reading UI Plugins requires System Administrator privileges. Supported in provider *v3.10+* and requires VCD 10.3+ From d6b93222583a6e39582d68b850a2df5a26cbb379 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Wed, 21 Jun 2023 16:41:21 +0200 Subject: [PATCH 27/28] Apply suggestions Signed-off-by: abarreiro --- website/docs/r/ui_plugin.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/ui_plugin.html.markdown b/website/docs/r/ui_plugin.html.markdown index 21cc3bef6..c600803d3 100644 --- a/website/docs/r/ui_plugin.html.markdown +++ b/website/docs/r/ui_plugin.html.markdown @@ -11,7 +11,7 @@ description: |- Provides a VMware Cloud Director UI Plugin resource. This can be used to manage UI Plugins in VCD, for example to add a new plugin from a local ZIP file, to publish/unpublish a UI Plugin to different Organizations, etc. --> Reading UI Plugins requires System Administrator privileges. +-> Managing UI Plugins requires System Administrator privileges. Supported in provider *v3.10+* and requires VCD 10.3+ From ed6440d084858ceadb3a0494b9c588949a300299 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Wed, 12 Jul 2023 13:52:20 +0200 Subject: [PATCH 28/28] Fix testing tags Signed-off-by: abarreiro --- vcd/config_test.go | 2 +- vcd/provider_test.go | 2 +- vcd/resource_vcd_ui_plugin_test.go | 4 ++-- vcd/testcheck_funcs_test.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vcd/config_test.go b/vcd/config_test.go index c013c2c34..0972da957 100644 --- a/vcd/config_test.go +++ b/vcd/config_test.go @@ -1,4 +1,4 @@ -//go:build api || functional || catalog || vapp || network || extnetwork || org || query || vm || vdc || gateway || disk || binary || lb || lbServiceMonitor || lbServerPool || lbAppProfile || lbAppRule || lbVirtualServer || access_control || user || standaloneVm || search || auth || nsxt || role || alb || certificate || vdcGroup || ldap || rde || ALL +//go:build api || functional || catalog || vapp || network || extnetwork || org || query || vm || vdc || gateway || disk || binary || lb || lbServiceMonitor || lbServerPool || lbAppProfile || lbAppRule || lbVirtualServer || access_control || user || standaloneVm || search || auth || nsxt || role || alb || certificate || vdcGroup || ldap || rde || uiPlugin || ALL package vcd diff --git a/vcd/provider_test.go b/vcd/provider_test.go index 183456343..d2538b1a0 100644 --- a/vcd/provider_test.go +++ b/vcd/provider_test.go @@ -1,4 +1,4 @@ -//go:build api || functional || catalog || vapp || network || extnetwork || org || query || vm || vdc || gateway || disk || binary || lb || lbAppProfile || lbAppRule || lbServiceMonitor || lbServerPool || lbVirtualServer || user || access_control || standaloneVm || search || auth || nsxt || role || alb || certificate || vdcGroup || ldap || rde || ALL +//go:build api || functional || catalog || vapp || network || extnetwork || org || query || vm || vdc || gateway || disk || binary || lb || lbAppProfile || lbAppRule || lbServiceMonitor || lbServerPool || lbVirtualServer || user || access_control || standaloneVm || search || auth || nsxt || role || alb || certificate || vdcGroup || ldap || rde || uiPlugin || ALL package vcd diff --git a/vcd/resource_vcd_ui_plugin_test.go b/vcd/resource_vcd_ui_plugin_test.go index cf9aca5af..8752872dc 100644 --- a/vcd/resource_vcd_ui_plugin_test.go +++ b/vcd/resource_vcd_ui_plugin_test.go @@ -1,4 +1,4 @@ -//go:build plugin || ALL || functional +//go:build uiPlugin || ALL || functional package vcd @@ -12,7 +12,7 @@ import ( ) func init() { - testingTags["plugin"] = "resource_vcd_ui_plugin_test.go" + testingTags["uiPlugin"] = "resource_vcd_ui_plugin_test.go" } // This object is equivalent to the manifest.json that is inside the ../test-resources/ui_plugin.zip file diff --git a/vcd/testcheck_funcs_test.go b/vcd/testcheck_funcs_test.go index 9a9bfd722..86e757b08 100644 --- a/vcd/testcheck_funcs_test.go +++ b/vcd/testcheck_funcs_test.go @@ -1,4 +1,4 @@ -//go:build vapp || vm || user || nsxt || extnetwork || network || gateway || catalog || standaloneVm || alb || vdcGroup || ldap || vdc || access_control || rde || ALL || functional +//go:build vapp || vm || user || nsxt || extnetwork || network || gateway || catalog || standaloneVm || alb || vdcGroup || ldap || vdc || access_control || rde || uiPlugin || ALL || functional package vcd