From 54ea7c8698d76f5fa11592943219762823c77920 Mon Sep 17 00:00:00 2001 From: abarreiro Date: Mon, 22 May 2023 12:11:41 +0200 Subject: [PATCH] Init Signed-off-by: abarreiro --- govcd/api_vcd_test.go | 2 +- govcd/openapi_endpoints.go | 7 ++ govcd/ui_plugin.go | 128 +++++++++++++++++++++++++++++++++++++ govcd/ui_plugin_test.go | 89 ++++++++++++++++++++++++++ types/v56/constants.go | 7 ++ types/v56/openapi.go | 21 ++++++ 6 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 govcd/ui_plugin.go create mode 100644 govcd/ui_plugin_test.go diff --git a/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index 3f105d119..fe8b96975 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -1,4 +1,4 @@ -//go:build api || openapi || functional || catalog || vapp || gateway || network || org || query || extnetwork || task || vm || vdc || system || disk || lb || lbAppRule || lbAppProfile || lbServerPool || lbServiceMonitor || lbVirtualServer || user || search || nsxv || nsxt || auth || affinity || role || alb || certificate || vdcGroup || metadata || providervdc || rde || ALL +//go:build api || openapi || functional || catalog || vapp || gateway || network || org || query || extnetwork || task || vm || vdc || system || disk || lb || lbAppRule || lbAppProfile || lbServerPool || lbServiceMonitor || lbVirtualServer || user || search || nsxv || nsxt || auth || affinity || role || alb || certificate || vdcGroup || metadata || providervdc || rde || plugin || ALL /* * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/openapi_endpoints.go b/govcd/openapi_endpoints.go index b8a6b0b12..b236b26a5 100644 --- a/govcd/openapi_endpoints.go +++ b/govcd/openapi_endpoints.go @@ -64,6 +64,13 @@ var endpointMinApiVersions = map[string]string{ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities: "35.0", // VCD 10.2+ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesTypes: "35.0", // VCD 10.2+ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesResolve: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExtensionsUi: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExtensionsUiPlugin: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExtensionsUiPublishAll: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExtensionsUiPublish: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExtensionsUiUnpublishAll: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExtensionsUiUnpublish: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTransfer: "35.0", // VCD 10.2+ // NSX-T ALB (Advanced/AVI Load Balancer) support was introduced in 10.2 types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController: "35.0", // VCD 10.2+ diff --git a/govcd/ui_plugin.go b/govcd/ui_plugin.go new file mode 100644 index 000000000..e1feb4464 --- /dev/null +++ b/govcd/ui_plugin.go @@ -0,0 +1,128 @@ +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" +) + +type UIPluginMetadata struct { + UIPluginMetadata *types.UIPluginMetadata + client *Client +} + +// CreateUIPlugin creates a new UI extension and sets the provided plugin metadata for it. +// Only System administrator can create a UI extension. +func (vcdClient *VCDClient) CreateUIPlugin(uiPluginMetadata *types.UIPluginMetadata) (*UIPluginMetadata, error) { + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExtensionsUi + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + result := &UIPluginMetadata{ + UIPluginMetadata: &types.UIPluginMetadata{}, + client: &vcdClient.Client, + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, uiPluginMetadata, result.UIPluginMetadata, nil) + if err != nil { + return nil, err + } + + return result, nil +} + +// Upload uploads the given UI Plugin to VCD. Only the file name in the input types.UploadSpec is required. +// The size is calculated automatically if not provided. +func (ui *UIPluginMetadata) Upload(uploadSpec *types.UploadSpec) error { + if strings.TrimSpace(uploadSpec.FileName) == "" { + return fmt.Errorf("file name to upload must not be empty") + } + fileContents, err := os.ReadFile(filepath.Clean(uploadSpec.FileName)) + if err != nil { + return err + } + + if uploadSpec.Size <= 0 { + uploadSpec.Size = len(fileContents) + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExtensionsUiPlugin + apiVersion, err := ui.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := ui.client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return err + } + + headers, err := ui.client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, nil, uploadSpec, nil, nil) + if err != nil { + return err + } + + transferId, err := getTransferIdFromHeader(headers) + if err != nil { + return err + } + + endpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTransfer + apiVersion, err = ui.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err = ui.client.OpenApiBuildEndpoint(endpoint, transferId) + if err != nil { + return err + } + + return ui.client.OpenApiPutItem(apiVersion, urlRef, nil, fileContents, nil, nil) +} + +// getTransferIdFromHeader retrieves a valid transfer ID from any given HTTP headers +func getTransferIdFromHeader(headers http.Header) (string, error) { + rawLinkContent := headers.Get("link") + if rawLinkContent == "" { + return "", fmt.Errorf("error during UI plugin upload, the POST call didn't return any transfer link") + } + linkRegex := regexp.MustCompile(`<\S+/transfer/(\S+)>`) + matches := linkRegex.FindStringSubmatch(rawLinkContent) + if len(matches) < 2 { + return "", fmt.Errorf("error during UI plugin upload, the POST call didn't return a valid transfer link: %s", rawLinkContent) + } + return matches[1], nil +} + +func (*UIPluginMetadata) Publish(orgs types.OpenApiReferences) (types.OpenApiReferences, error) { + return nil, nil +} + +func (*UIPluginMetadata) PublishAll() { + +} + +func (*UIPluginMetadata) Unpublish(orgs types.OpenApiReferences) (types.OpenApiReferences, error) { + return nil, nil +} + +func (*UIPluginMetadata) UnpublishAll() { + +} + +func (*UIPluginMetadata) Delete() { + +} diff --git a/govcd/ui_plugin_test.go b/govcd/ui_plugin_test.go new file mode 100644 index 000000000..1597d2e7e --- /dev/null +++ b/govcd/ui_plugin_test.go @@ -0,0 +1,89 @@ +//go:build functional || openapi || plugin || ALL + +package govcd + +import ( + "net/http" + "net/textproto" + "testing" +) + +// Test_getTransferIdFromHeader tests that getTransferIdFromHeader can retrieve correctly a transfer ID from the headers +// of any HTTP response. +func Test_getTransferIdFromHeader(t *testing.T) { + tests := []struct { + name string + headers http.Header + want string + wantErr bool + }{ + { + name: "valid link in header", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + ";rel=\"upload:default\";type=\"application/octet-stream\"", + }, + }, + want: "cb63b0f6-ba56-43a8-8fe3-a64f0b25e7e5/my-amazing-plugin1.0.zip", + wantErr: false, + }, + { + name: "valid link in header with special URI", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + ";rel=\"upload:default\";type=\"application/octet-stream\"", + }, + }, + want: "cb63b0f6-ba56-43a8-8fe3-a64f0b25e7e5/my-amazing-plugin1.1.zip", + wantErr: false, + }, + { + name: "empty header", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + "", + }, + }, + wantErr: true, + }, + { + name: "empty link in header", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + "<>;rel=\"upload:default\";type=\"application/octet-stream\"", + }, + }, + wantErr: true, + }, + { + name: "no link part in header", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + "rel=\"upload:default\";type=\"application/octet-stream\"", + }, + }, + wantErr: true, + }, + { + name: "invalid header", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + "Error", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getTransferIdFromHeader(tt.headers) + if (err != nil) != tt.wantErr { + t.Errorf("getTransferIdFromHeader() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("getTransferIdFromHeader() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/types/v56/constants.go b/types/v56/constants.go index b0dec9413..802b65e33 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -399,6 +399,13 @@ const ( OpenApiEndpointRdeEntities = "entities/" OpenApiEndpointRdeEntitiesTypes = "entities/types/" OpenApiEndpointRdeEntitiesResolve = "entities/%s/resolve" + OpenApiEndpointExtensionsUi = "extensions/ui" + OpenApiEndpointExtensionsUiPlugin = "extensions/ui/%s/plugin" + OpenApiEndpointExtensionsUiPublishAll = "extensions/ui/%s/tenants/publishAll" + OpenApiEndpointExtensionsUiPublish = "extensions/ui/%s/tenants/publish" + OpenApiEndpointExtensionsUiUnpublishAll = "extensions/ui/%s/tenants/unpublishAll" + OpenApiEndpointExtensionsUiUnpublish = "extensions/ui/%s/tenants/unpublish" + OpenApiEndpointTransfer = "transfer/" // NSX-T ALB related endpoints diff --git a/types/v56/openapi.go b/types/v56/openapi.go index e0ae7dfac..46f312778 100644 --- a/types/v56/openapi.go +++ b/types/v56/openapi.go @@ -465,3 +465,24 @@ type DefinedEntity struct { Owner *OpenApiReference `json:"owner,omitempty"` // The owner of the defined entity Org *OpenApiReference `json:"org,omitempty"` // The organization of the defined entity. } + +// UIPluginMetadata gives meta information about a UI Plugin +type UIPluginMetadata struct { + Vendor string `json:"vendor,omitempty"` + License string `json:"license,omitempty"` + Link string `json:"link,omitempty"` + PluginName string `json:"pluginName,omitempty"` + Version string `json:"version,omitempty"` + Description *string `json:"description,omitempty"` + ProviderScoped *bool `json:"provider_scoped,omitempty"` + TenantScoped *bool `json:"tenant_scoped,omitempty"` + Enabled *bool `json:"enabled,omitempty"` +} + +// UploadSpec gives information about an upload +type UploadSpec struct { + FileName string `json:"fileName,omitempty"` + Size int `json:"size,omitempty"` + Checksum *string `json:"checksum,omitempty"` + ChecksumAlgo *string `json:"checksumAlgo,omitempty"` +}