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  }