github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/metadata_openapi.go (about)

     1  /*
     2   * Copyright 2023 VMware, Inc.  All rights reserved.  Licensed under the Apache v2 License.
     3   */
     4  
     5  package govcd
     6  
     7  import (
     8  	"fmt"
     9  	"github.com/vmware/go-vcloud-director/v2/types/v56"
    10  	"net/url"
    11  	"strings"
    12  )
    13  
    14  // OpenApiMetadataEntry is a wrapper object for types.OpenApiMetadataEntry
    15  type OpenApiMetadataEntry struct {
    16  	MetadataEntry  *types.OpenApiMetadataEntry
    17  	client         *Client
    18  	Etag           string // Allows concurrent operations with metadata
    19  	href           string // This is the HREF of the given metadata entry
    20  	parentEndpoint string // This is the endpoint of the object that has the metadata entries
    21  }
    22  
    23  // ---------------------------------------------------------------------------------------------------------------------
    24  // Specific objects compatible with metadata
    25  // ---------------------------------------------------------------------------------------------------------------------
    26  
    27  // GetMetadata returns all the metadata from a DefinedEntity.
    28  // NOTE: The obtained metadata doesn't have ETags, use GetMetadataById or GetMetadataByKey to obtain a ETag for a specific entry.
    29  func (rde *DefinedEntity) GetMetadata() ([]*OpenApiMetadataEntry, error) {
    30  	endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities
    31  	return getAllOpenApiMetadata(rde.client, endpoint, rde.DefinedEntity.ID, rde.DefinedEntity.Name, "entity", nil)
    32  }
    33  
    34  // GetMetadataByKey returns a unique DefinedEntity metadata entry corresponding to the given domain, namespace and key.
    35  // The domain and namespace are only needed when there's more than one entry with the same key.
    36  // This is a more costly operation than GetMetadataById due to ETags, so use that preferred option whenever possible.
    37  func (rde *DefinedEntity) GetMetadataByKey(domain, namespace, key string) (*OpenApiMetadataEntry, error) {
    38  	endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities
    39  	return getOpenApiMetadataByKey(rde.client, endpoint, rde.DefinedEntity.ID, rde.DefinedEntity.Name, "entity", domain, namespace, key)
    40  }
    41  
    42  // GetMetadataById returns a unique DefinedEntity metadata entry corresponding to the given domain, namespace and key.
    43  // The domain and namespace are only needed when there's more than one entry with the same key.
    44  func (rde *DefinedEntity) GetMetadataById(id string) (*OpenApiMetadataEntry, error) {
    45  	endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities
    46  	return getOpenApiMetadataById(rde.client, endpoint, rde.DefinedEntity.ID, rde.DefinedEntity.Name, "entity", id)
    47  }
    48  
    49  // AddMetadata adds metadata to the receiver DefinedEntity.
    50  func (rde *DefinedEntity) AddMetadata(metadataEntry types.OpenApiMetadataEntry) (*OpenApiMetadataEntry, error) {
    51  	endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities
    52  	return addOpenApiMetadata(rde.client, endpoint, rde.DefinedEntity.ID, metadataEntry)
    53  }
    54  
    55  // ---------------------------------------------------------------------------------------------------------------------
    56  // Metadata Entry methods for OpenAPI metadata
    57  // ---------------------------------------------------------------------------------------------------------------------
    58  
    59  // Update updates the metadata value from the receiver entry.
    60  // Only the value and persistence of the entry can be updated. Re-create the entry in case you want to modify any of the other fields.
    61  func (entry *OpenApiMetadataEntry) Update(value interface{}, persistent bool) error {
    62  	if entry.MetadataEntry.ID == "" {
    63  		return fmt.Errorf("ID of the receiver metadata entry is empty")
    64  	}
    65  
    66  	payload := types.OpenApiMetadataEntry{
    67  		ID:           entry.MetadataEntry.ID,
    68  		IsPersistent: persistent,
    69  		IsReadOnly:   entry.MetadataEntry.IsReadOnly,
    70  		KeyValue: types.OpenApiMetadataKeyValue{
    71  			Domain: entry.MetadataEntry.KeyValue.Domain,
    72  			Key:    entry.MetadataEntry.KeyValue.Key,
    73  			Value: types.OpenApiMetadataTypedValue{
    74  				Value: value,
    75  				Type:  entry.MetadataEntry.KeyValue.Value.Type,
    76  			},
    77  			Namespace: entry.MetadataEntry.KeyValue.Namespace,
    78  		},
    79  	}
    80  
    81  	apiVersion, err := entry.client.getOpenApiHighestElevatedVersion(entry.parentEndpoint)
    82  	if err != nil {
    83  		return err
    84  	}
    85  
    86  	urlRef, err := url.ParseRequestURI(entry.href)
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	headers, err := entry.client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, nil, payload, entry.MetadataEntry, map[string]string{"If-Match": entry.Etag})
    92  	if err != nil {
    93  		return err
    94  	}
    95  	entry.Etag = headers.Get("Etag")
    96  	return nil
    97  }
    98  
    99  // Delete deletes the receiver metadata entry.
   100  func (entry *OpenApiMetadataEntry) Delete() error {
   101  	if entry.MetadataEntry.ID == "" {
   102  		return fmt.Errorf("ID of the receiver metadata entry is empty")
   103  	}
   104  
   105  	apiVersion, err := entry.client.getOpenApiHighestElevatedVersion(entry.parentEndpoint)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	urlRef, err := url.ParseRequestURI(entry.href)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	err = entry.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil)
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	entry.Etag = ""
   121  	entry.parentEndpoint = ""
   122  	entry.href = ""
   123  	entry.MetadataEntry = &types.OpenApiMetadataEntry{}
   124  	return nil
   125  }
   126  
   127  // ---------------------------------------------------------------------------------------------------------------------
   128  // OpenAPI Metadata private functions
   129  // ---------------------------------------------------------------------------------------------------------------------
   130  
   131  // getAllOpenApiMetadata is a generic function to retrieve all metadata from any VCD object using its ID and the given OpenAPI endpoint.
   132  // It supports query parameters to input, for example, filtering options.
   133  func getAllOpenApiMetadata(client *Client, endpoint, objectId, objectName, objectType string, queryParameters url.Values) ([]*OpenApiMetadataEntry, error) {
   134  	apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	urlRef, err := client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("%s/metadata", objectId))
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	allMetadata := []*types.OpenApiMetadataEntry{{}}
   145  	err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &allMetadata, nil)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	var filteredMetadata []*types.OpenApiMetadataEntry
   151  	for _, entry := range allMetadata {
   152  		_, err = filterSingleOpenApiMetadataEntry(objectType, objectName, entry, client.IgnoredMetadata)
   153  		if err != nil {
   154  			if strings.Contains(err.Error(), "is being ignored") {
   155  				continue
   156  			}
   157  			return nil, err
   158  		}
   159  		filteredMetadata = append(filteredMetadata, entry)
   160  	}
   161  
   162  	// Wrap all type.OpenApiMetadataEntry into OpenApiMetadataEntry types with client
   163  	results := make([]*OpenApiMetadataEntry, len(filteredMetadata))
   164  	for i := range filteredMetadata {
   165  		results[i] = &OpenApiMetadataEntry{
   166  			MetadataEntry:  filteredMetadata[i],
   167  			client:         client,
   168  			href:           fmt.Sprintf("%s/%s", urlRef.String(), filteredMetadata[i].ID),
   169  			parentEndpoint: endpoint,
   170  		}
   171  	}
   172  
   173  	return results, nil
   174  }
   175  
   176  // getOpenApiMetadataByKey is a generic function to retrieve a unique metadata entry from any VCD object using its domain, namespace and key.
   177  // The domain and namespace are only needed when there's more than one entry with the same key.
   178  func getOpenApiMetadataByKey(client *Client, endpoint, objectId, objectName, objectType string, domain, namespace, key string) (*OpenApiMetadataEntry, error) {
   179  	queryParameters := url.Values{}
   180  	// As for now, the filter only supports filtering by key
   181  	queryParameters.Add("filter", fmt.Sprintf("keyValue.key==%s", key))
   182  	metadata, err := getAllOpenApiMetadata(client, endpoint, objectId, objectName, objectType, queryParameters)
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  
   187  	if len(metadata) == 0 {
   188  		return nil, fmt.Errorf("%s could not find the metadata associated to object %s", ErrorEntityNotFound, objectId)
   189  	}
   190  
   191  	// There's more than one entry with same key, the namespace and domain need to be compared to be able to filter.
   192  	if len(metadata) > 1 {
   193  		var filteredMetadata []*OpenApiMetadataEntry
   194  		for _, entry := range metadata {
   195  			if entry.MetadataEntry.KeyValue.Namespace == namespace && entry.MetadataEntry.KeyValue.Domain == domain {
   196  				filteredMetadata = append(filteredMetadata, entry)
   197  			}
   198  		}
   199  		if len(filteredMetadata) > 1 {
   200  			return nil, fmt.Errorf("found %d metadata entries associated to object %s", len(filteredMetadata), objectId)
   201  		}
   202  		// Required to retrieve an ETag
   203  		return getOpenApiMetadataById(client, endpoint, objectId, objectName, objectType, filteredMetadata[0].MetadataEntry.ID)
   204  	}
   205  
   206  	// Required to retrieve an ETag
   207  	return getOpenApiMetadataById(client, endpoint, objectId, objectName, objectType, metadata[0].MetadataEntry.ID)
   208  }
   209  
   210  // getOpenApiMetadataById is a generic function to retrieve a unique metadata entry from any VCD object using its unique ID.
   211  func getOpenApiMetadataById(client *Client, endpoint, objectId, objectName, objectType, metadataId string) (*OpenApiMetadataEntry, error) {
   212  	if metadataId == "" {
   213  		return nil, fmt.Errorf("input metadata entry ID is empty")
   214  	}
   215  
   216  	apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	urlRef, err := client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("%s/metadata/%s", objectId, metadataId))
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	response := &OpenApiMetadataEntry{
   227  		MetadataEntry:  &types.OpenApiMetadataEntry{},
   228  		client:         client,
   229  		href:           urlRef.String(),
   230  		parentEndpoint: endpoint,
   231  	}
   232  
   233  	headers, err := client.OpenApiGetItemAndHeaders(apiVersion, urlRef, nil, response.MetadataEntry, nil)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	_, err = filterSingleOpenApiMetadataEntry(objectType, objectName, response.MetadataEntry, client.IgnoredMetadata)
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  
   243  	response.Etag = headers.Get("Etag")
   244  	return response, nil
   245  }
   246  
   247  // addOpenApiMetadata adds one metadata entry to the VCD object with given ID
   248  func addOpenApiMetadata(client *Client, endpoint, objectId string, metadataEntry types.OpenApiMetadataEntry) (*OpenApiMetadataEntry, error) {
   249  	apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  
   254  	urlRef, err := client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("%s/metadata", objectId))
   255  	if err != nil {
   256  		return nil, err
   257  	}
   258  
   259  	response := &OpenApiMetadataEntry{
   260  		client:         client,
   261  		MetadataEntry:  &types.OpenApiMetadataEntry{},
   262  		parentEndpoint: endpoint,
   263  	}
   264  	headers, err := client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, nil, metadataEntry, response.MetadataEntry, nil)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	response.Etag = headers.Get("Etag")
   269  	response.href = fmt.Sprintf("%s/%s", urlRef.String(), response.MetadataEntry.ID)
   270  	return response, nil
   271  }
   272  
   273  // ---------------------------------------------------------------------------------------------------------------------
   274  // Ignore OpenAPI Metadata feature
   275  // ---------------------------------------------------------------------------------------------------------------------
   276  
   277  // normaliseOpenApiMetadata transforms OpenAPI metadata into a normalised structure
   278  func normaliseOpenApiMetadata(objectType, name string, metadataEntry *types.OpenApiMetadataEntry) (*normalisedMetadata, error) {
   279  	return &normalisedMetadata{
   280  		ObjectType: objectType,
   281  		ObjectName: name,
   282  		Key:        metadataEntry.KeyValue.Key,
   283  		Value:      fmt.Sprintf("%v", metadataEntry.KeyValue.Value.Value),
   284  	}, nil
   285  }
   286  
   287  // filterSingleOpenApiMetadataEntry filters a single OpenAPI metadata entry depending on the contents of the input ignored metadata slice.
   288  func filterSingleOpenApiMetadataEntry(objectType, objectName string, metadataEntry *types.OpenApiMetadataEntry, metadataToIgnore []IgnoredMetadata) (*types.OpenApiMetadataEntry, error) {
   289  	normalisedEntry, err := normaliseOpenApiMetadata(objectType, objectName, metadataEntry)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  	isFiltered := filterSingleGenericMetadataEntry(normalisedEntry, metadataToIgnore)
   294  	if isFiltered {
   295  		return nil, fmt.Errorf("the metadata entry with key '%s' and value '%v' is being ignored", metadataEntry.KeyValue.Key, metadataEntry.KeyValue.Value.Value)
   296  	}
   297  	return metadataEntry, nil
   298  }