github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/ui_plugin.go (about) 1 package govcd 2 3 import ( 4 "archive/zip" 5 "crypto/sha256" 6 "encoding/json" 7 "fmt" 8 "github.com/vmware/go-vcloud-director/v2/types/v56" 9 "github.com/vmware/go-vcloud-director/v2/util" 10 "io" 11 "net/http" 12 "os" 13 "path/filepath" 14 "regexp" 15 "strings" 16 ) 17 18 type UIPlugin struct { 19 UIPluginMetadata *types.UIPluginMetadata 20 client *Client 21 } 22 23 // AddUIPlugin reads the plugin ZIP file located in the input path, obtains the inner metadata, sends it to 24 // VCD and performs the plugin upload. 25 func (vcdClient *VCDClient) AddUIPlugin(pluginPath string, enabled bool) (*UIPlugin, error) { 26 if strings.TrimSpace(pluginPath) == "" { 27 return nil, fmt.Errorf("plugin path must not be empty") 28 } 29 uiPluginMetadataPayload, err := getPluginMetadata(pluginPath) 30 if err != nil { 31 return nil, fmt.Errorf("error retrieving the metadata for the given plugin %s: %s", pluginPath, err) 32 } 33 uiPluginMetadataPayload.Enabled = enabled 34 uiPluginMetadata, err := createUIPlugin(&vcdClient.Client, uiPluginMetadataPayload) 35 if err != nil { 36 return nil, fmt.Errorf("error creating the UI plugin: %s", err) 37 } 38 err = uiPluginMetadata.upload(pluginPath) 39 if err != nil { 40 return nil, fmt.Errorf("error uploading the UI plugin: %s", err) 41 } 42 43 return uiPluginMetadata, nil 44 } 45 46 // GetAllUIPlugins retrieves a slice with all the available UIPlugin objects present in VCD. 47 func (vcdClient *VCDClient) GetAllUIPlugins() ([]*UIPlugin, error) { 48 endpoint := types.OpenApiEndpointExtensionsUi // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike 49 apiVersion, err := vcdClient.Client.getOpenApiHighestElevatedVersion(endpoint) 50 if err != nil { 51 return nil, err 52 } 53 54 urlRef, err := vcdClient.Client.OpenApiBuildEndpoint(endpoint) 55 if err != nil { 56 return nil, err 57 } 58 var typeResponses []*types.UIPluginMetadata 59 err = vcdClient.Client.OpenApiGetItem(apiVersion, urlRef, nil, &typeResponses, nil) 60 if err != nil { 61 return nil, err 62 } 63 64 // Wrap all typeResponses into UIPlugin types with client 65 uiPlugins := make([]*UIPlugin, len(typeResponses)) 66 for sliceIndex := range typeResponses { 67 uiPlugins[sliceIndex] = &UIPlugin{ 68 UIPluginMetadata: typeResponses[sliceIndex], 69 client: &vcdClient.Client, 70 } 71 } 72 73 return uiPlugins, nil 74 } 75 76 // GetUIPluginById obtains a unique UIPlugin identified by its URN. 77 func (vcdClient *VCDClient) GetUIPluginById(id string) (*UIPlugin, error) { 78 endpoint := types.OpenApiEndpointExtensionsUi // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike 79 apiVersion, err := vcdClient.Client.getOpenApiHighestElevatedVersion(endpoint) 80 if err != nil { 81 return nil, err 82 } 83 84 urlRef, err := vcdClient.Client.OpenApiBuildEndpoint(endpoint, id) 85 if err != nil { 86 return nil, err 87 } 88 89 result := &UIPlugin{ 90 UIPluginMetadata: &types.UIPluginMetadata{}, 91 client: &vcdClient.Client, 92 } 93 err = vcdClient.Client.OpenApiGetItem(apiVersion, urlRef, nil, result.UIPluginMetadata, nil) 94 if err != nil { 95 return nil, amendUIPluginGetByIdError(id, err) 96 } 97 98 return result, nil 99 } 100 101 // amendUIPluginGetByIdError is a workaround for a bug in VCD that causes the GET endpoint to return an ugly error 500 with a NullPointerException 102 // when the UI Plugin with given ID is not found 103 func amendUIPluginGetByIdError(id string, err error) error { 104 if err != nil && strings.Contains(err.Error(), "NullPointerException") { 105 return fmt.Errorf("could not find any UI plugin with ID '%s': %s", id, ErrorEntityNotFound) 106 } 107 return err 108 } 109 110 // GetUIPlugin obtains a unique UIPlugin identified by the combination of its vendor, plugin name and version. 111 func (vcdClient *VCDClient) GetUIPlugin(vendor, pluginName, version string) (*UIPlugin, error) { 112 allUIPlugins, err := vcdClient.GetAllUIPlugins() 113 if err != nil { 114 return nil, err 115 } 116 for _, plugin := range allUIPlugins { 117 if plugin.IsTheSameAs(&UIPlugin{UIPluginMetadata: &types.UIPluginMetadata{ 118 Vendor: vendor, 119 PluginName: pluginName, 120 Version: version, 121 }}) { 122 return plugin, nil 123 } 124 } 125 126 return nil, fmt.Errorf("could not find any UI plugin with vendor '%s', pluginName '%s' and version '%s': %s", vendor, pluginName, version, ErrorEntityNotFound) 127 } 128 129 // GetPublishedTenants gets all the Organization references where the receiver UIPlugin is published. 130 func (uiPlugin *UIPlugin) GetPublishedTenants() (types.OpenApiReferences, error) { 131 if strings.TrimSpace(uiPlugin.UIPluginMetadata.ID) == "" { 132 return nil, fmt.Errorf("plugin ID is required but it is empty") 133 } 134 135 endpoint := types.OpenApiEndpointExtensionsUiTenants // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike 136 apiVersion, err := uiPlugin.client.getOpenApiHighestElevatedVersion(endpoint) 137 if err != nil { 138 return nil, err 139 } 140 141 urlRef, err := uiPlugin.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, uiPlugin.UIPluginMetadata.ID)) 142 if err != nil { 143 return nil, err 144 } 145 146 var orgRefs types.OpenApiReferences 147 err = uiPlugin.client.OpenApiGetAllItems(apiVersion, urlRef, nil, &orgRefs, nil) 148 if err != nil { 149 return nil, err 150 } 151 return orgRefs, nil 152 } 153 154 // Publish publishes the receiver UIPlugin to the given Organizations. 155 // Does not modify the receiver UIPlugin. 156 func (uiPlugin *UIPlugin) Publish(orgs types.OpenApiReferences) error { 157 if len(orgs) == 0 { 158 return nil 159 } 160 return publishOrUnpublishFromOrgs(uiPlugin.client, uiPlugin.UIPluginMetadata.ID, orgs, types.OpenApiEndpointExtensionsUiTenantsPublish) 161 } 162 163 // Unpublish unpublishes the receiver UIPlugin from the given Organizations. 164 // Does not modify the receiver UIPlugin. 165 func (uiPlugin *UIPlugin) Unpublish(orgs types.OpenApiReferences) error { 166 if len(orgs) == 0 { 167 return nil 168 } 169 return publishOrUnpublishFromOrgs(uiPlugin.client, uiPlugin.UIPluginMetadata.ID, orgs, types.OpenApiEndpointExtensionsUiTenantsUnpublish) 170 } 171 172 // PublishAll publishes the receiver UIPlugin to all available Organizations. 173 // Does not modify the receiver UIPlugin. 174 func (uiPlugin *UIPlugin) PublishAll() error { 175 return publishOrUnpublishFromOrgs(uiPlugin.client, uiPlugin.UIPluginMetadata.ID, nil, types.OpenApiEndpointExtensionsUiTenantsPublishAll) 176 } 177 178 // UnpublishAll unpublishes the receiver UIPlugin from all available Organizations. 179 // Does not modify the receiver UIPlugin. 180 func (uiPlugin *UIPlugin) UnpublishAll() error { 181 return publishOrUnpublishFromOrgs(uiPlugin.client, uiPlugin.UIPluginMetadata.ID, nil, types.OpenApiEndpointExtensionsUiTenantsUnpublishAll) 182 } 183 184 // IsTheSameAs retruns true if the receiver UIPlugin has the same name, vendor and version as the input. 185 func (uiPlugin *UIPlugin) IsTheSameAs(otherUiPlugin *UIPlugin) bool { 186 if otherUiPlugin == nil { 187 return false 188 } 189 return uiPlugin.UIPluginMetadata.PluginName == otherUiPlugin.UIPluginMetadata.PluginName && 190 uiPlugin.UIPluginMetadata.Version == otherUiPlugin.UIPluginMetadata.Version && 191 uiPlugin.UIPluginMetadata.Vendor == otherUiPlugin.UIPluginMetadata.Vendor 192 } 193 194 // Update performs an update to several receiver plugin attributes 195 func (uiPlugin *UIPlugin) Update(enable, providerScoped, tenantScoped bool) error { 196 if strings.TrimSpace(uiPlugin.UIPluginMetadata.ID) == "" { 197 return fmt.Errorf("plugin ID is required but it is empty") 198 } 199 200 endpoint := types.OpenApiEndpointExtensionsUi // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike 201 apiVersion, err := uiPlugin.client.getOpenApiHighestElevatedVersion(endpoint) 202 if err != nil { 203 return err 204 } 205 206 urlRef, err := uiPlugin.client.OpenApiBuildEndpoint(endpoint, uiPlugin.UIPluginMetadata.ID) 207 if err != nil { 208 return err 209 } 210 211 payload := &types.UIPluginMetadata{ 212 Vendor: uiPlugin.UIPluginMetadata.Vendor, 213 License: uiPlugin.UIPluginMetadata.License, 214 Link: uiPlugin.UIPluginMetadata.Link, 215 PluginName: uiPlugin.UIPluginMetadata.PluginName, 216 Version: uiPlugin.UIPluginMetadata.Version, 217 Description: uiPlugin.UIPluginMetadata.Description, 218 ProviderScoped: providerScoped, 219 TenantScoped: tenantScoped, 220 Enabled: enable, 221 } 222 err = uiPlugin.client.OpenApiPutItem(apiVersion, urlRef, nil, payload, uiPlugin.UIPluginMetadata, nil) 223 if err != nil { 224 return err 225 } 226 return nil 227 } 228 229 // Delete deletes the receiver UIPlugin from VCD. 230 func (uiPlugin *UIPlugin) Delete() error { 231 if strings.TrimSpace(uiPlugin.UIPluginMetadata.ID) == "" { 232 return fmt.Errorf("plugin ID must not be empty") 233 } 234 235 endpoint := types.OpenApiEndpointExtensionsUi // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike 236 apiVersion, err := uiPlugin.client.getOpenApiHighestElevatedVersion(endpoint) 237 if err != nil { 238 return err 239 } 240 241 urlRef, err := uiPlugin.client.OpenApiBuildEndpoint(endpoint, uiPlugin.UIPluginMetadata.ID) 242 if err != nil { 243 return err 244 } 245 246 err = uiPlugin.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) 247 if err != nil { 248 return err 249 } 250 uiPlugin.UIPluginMetadata = &types.UIPluginMetadata{} 251 return nil 252 } 253 254 // getPluginMetadata retrieves the types.UIPluginMetadata information stored inside the given plugin file, that should 255 // be a ZIP file. 256 func getPluginMetadata(pluginPath string) (*types.UIPluginMetadata, error) { 257 archive, err := zip.OpenReader(filepath.Clean(pluginPath)) 258 if err != nil { 259 return nil, err 260 } 261 defer func() { 262 if err := archive.Close(); err != nil { 263 util.Logger.Printf("Error closing ZIP file: %s\n", err) 264 } 265 }() 266 267 var manifest *zip.File 268 for _, f := range archive.File { 269 if f.Name == "manifest.json" { 270 manifest = f 271 break 272 } 273 } 274 if manifest == nil { 275 return nil, fmt.Errorf("could not find manifest.json inside the file %s", pluginPath) 276 } 277 278 manifestContents, err := manifest.Open() 279 if err != nil { 280 return nil, err 281 } 282 defer func() { 283 if err := manifestContents.Close(); err != nil { 284 util.Logger.Printf("Error closing manifest file: %s\n", err) 285 } 286 }() 287 288 manifestBytes, err := io.ReadAll(manifestContents) 289 if err != nil { 290 return nil, err 291 } 292 293 var unmarshaledJson map[string]interface{} 294 err = json.Unmarshal(manifestBytes, &unmarshaledJson) 295 if err != nil { 296 return nil, err 297 } 298 299 result := &types.UIPluginMetadata{ 300 Vendor: unmarshaledJson["vendor"].(string), 301 License: unmarshaledJson["license"].(string), 302 Link: unmarshaledJson["link"].(string), 303 PluginName: unmarshaledJson["name"].(string), 304 Version: unmarshaledJson["version"].(string), 305 Description: unmarshaledJson["description"].(string), 306 } 307 308 for _, scope := range unmarshaledJson["scope"].([]interface{}) { 309 if strings.Contains(scope.(string), "provider") { 310 result.ProviderScoped = true 311 } else if strings.Contains(scope.(string), "tenant") { 312 result.TenantScoped = true 313 } 314 } 315 316 return result, nil 317 } 318 319 // createUIPlugin creates a new empty UIPlugin in VCD and sets the provided plugin metadata. 320 // The UI plugin contents should be uploaded afterwards. 321 func createUIPlugin(client *Client, uiPluginMetadata *types.UIPluginMetadata) (*UIPlugin, error) { 322 endpoint := types.OpenApiEndpointExtensionsUi // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike 323 apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) 324 if err != nil { 325 return nil, err 326 } 327 328 urlRef, err := client.OpenApiBuildEndpoint(endpoint) 329 if err != nil { 330 return nil, err 331 } 332 333 result := &UIPlugin{ 334 UIPluginMetadata: &types.UIPluginMetadata{}, 335 client: client, 336 } 337 338 err = client.OpenApiPostItem(apiVersion, urlRef, nil, uiPluginMetadata, result.UIPluginMetadata, nil) 339 if err != nil { 340 return nil, err 341 } 342 343 return result, nil 344 } 345 346 // This function uploads the given UI Plugin to VCD. Only the plugin path is required. 347 func (ui *UIPlugin) upload(pluginPath string) error { 348 fileContents, err := os.ReadFile(filepath.Clean(pluginPath)) 349 if err != nil { 350 return err 351 } 352 353 endpoint := types.OpenApiEndpointExtensionsUiPlugin // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike 354 apiVersion, err := ui.client.getOpenApiHighestElevatedVersion(endpoint) 355 if err != nil { 356 return err 357 } 358 359 urlRef, err := ui.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ui.UIPluginMetadata.ID)) 360 if err != nil { 361 return err 362 } 363 364 uploadSpec := types.UploadSpec{ 365 FileName: filepath.Base(pluginPath), 366 ChecksumAlgo: "sha256", 367 Checksum: fmt.Sprintf("%x", sha256.Sum256(fileContents)), 368 Size: int64(len(fileContents)), 369 } 370 371 headers, err := ui.client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, nil, uploadSpec, nil, nil) 372 if err != nil { 373 return err 374 } 375 376 transferId, err := getTransferIdFromHeader(headers) 377 if err != nil { 378 return err 379 } 380 381 transferEndpoint := fmt.Sprintf("%s://%s/transfer/%s", ui.client.VCDHREF.Scheme, ui.client.VCDHREF.Host, transferId) 382 request, err := newFileUploadRequest(ui.client, transferEndpoint, fileContents, 0, uploadSpec.Size, uploadSpec.Size) 383 if err != nil { 384 return err 385 } 386 387 response, err := ui.client.Http.Do(request) 388 if err != nil { 389 return err 390 } 391 return response.Body.Close() 392 } 393 394 // getTransferIdFromHeader retrieves a valid transfer ID from any given HTTP headers, that can be used to upload 395 // a UI Plugin to VCD. 396 func getTransferIdFromHeader(headers http.Header) (string, error) { 397 rawLinkContent := headers.Get("link") 398 if rawLinkContent == "" { 399 return "", fmt.Errorf("error during UI plugin upload, the POST call didn't return any transfer link") 400 } 401 linkRegex := regexp.MustCompile(`<\S+/transfer/(\S+)>`) 402 matches := linkRegex.FindStringSubmatch(rawLinkContent) 403 if len(matches) < 2 { 404 return "", fmt.Errorf("error during UI plugin upload, the POST call didn't return a valid transfer link: %s", rawLinkContent) 405 } 406 return matches[1], nil 407 } 408 409 // publishOrUnpublishFromOrgs publishes or unpublishes (depending on the input endpoint) the UI Plugin with given ID from all available 410 // organizations. 411 func publishOrUnpublishFromOrgs(client *Client, pluginId string, orgs types.OpenApiReferences, endpoint string) error { 412 if strings.TrimSpace(pluginId) == "" { 413 return fmt.Errorf("plugin ID is required but it is empty") 414 } 415 416 apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) 417 if err != nil { 418 return err 419 } 420 421 urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, pluginId)) 422 if err != nil { 423 return err 424 } 425 426 return client.OpenApiPostItem(apiVersion, urlRef, nil, orgs, nil, nil) 427 }