/
resource_vcd_ui_plugin.go
323 lines (297 loc) · 11.5 KB
/
resource_vcd_ui_plugin.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package vcd
import (
"context"
"fmt"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/vmware/go-vcloud-director/v2/govcd"
"github.com/vmware/go-vcloud-director/v2/types/v56"
"log"
"regexp"
"strings"
)
func resourceVcdUIPlugin() *schema.Resource {
return &schema.Resource{
CreateContext: resourceVcdUIPluginCreate,
ReadContext: resourceVcdUIPluginRead,
UpdateContext: resourceVcdUIPluginUpdate,
DeleteContext: resourceVcdUIPluginDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceVcdUIPluginImport,
},
Schema: map[string]*schema.Schema{
"plugin_path": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateDiagFunc: func(value interface{}, _ cty.Path) diag.Diagnostics {
ok, err := regexp.MatchString(`(?i)^.+\.zip$`, value.(string))
if err != nil {
return diag.Errorf("could not validate %s", value.(string))
}
if !ok {
return diag.Errorf("the UI Plugin should be a ZIP bundle, but it is %s", value.(string))
}
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",
},
"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,
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",
},
"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",
},
"status": {
Type: schema.TypeString,
Computed: true,
Description: "The status 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)
}
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("tenant_ids") {
// 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)
}
oldRaw, newRaw := d.GetChange("tenant_ids")
// Retrieve the Organization IDs that need to be unpublished
newOrgIds := newRaw.(*schema.Set)
var orgIdsToUnpublish []interface{}
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 {
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" {
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, orgsToUnpublish, err)
}
}
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)
}
}
return nil
}
func resourceVcdUIPluginRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return genericVcdUIPluginRead(ctx, d, meta, "resource")
}
// 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() != "" {
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, 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)
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)
dSet(d, "status", uiPlugin.UIPluginMetadata.PluginStatus)
err = setUIPluginTenantIds(uiPlugin, d)
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) 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)
}
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")
if err != nil {
return diag.FromErr(err)
}
if uiPlugin == nil {
return nil
}
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 Organizations of the UI Plugin '%s': %s", uiPlugin.UIPluginMetadata.ID, err)
}
return resourceVcdUIPluginRead(ctx, d, meta)
}
func resourceVcdUIPluginDelete(_ 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.Delete()
if err != nil {
return diag.FromErr(err)
}
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)
if err != nil {
return nil, err
}
d.SetId(uiPlugin.UIPluginMetadata.ID)
return []*schema.ResourceData{d}, nil
}