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 }