github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/openapi.go (about) 1 package govcd 2 3 /* 4 * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. 5 */ 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "net/url" 14 "reflect" 15 "strconv" 16 "strings" 17 18 "github.com/peterhellberg/link" 19 20 "github.com/vmware/go-vcloud-director/v2/types/v56" 21 "github.com/vmware/go-vcloud-director/v2/util" 22 ) 23 24 // This file contains generalised low level methods to interact with VCD OpenAPI REST endpoints as documented in 25 // https://{VCD_HOST}/docs. In addition to this there are OpenAPI browser endpoints for tenant and provider 26 // respectively https://{VCD_HOST}/api-explorer/tenant/tenant-name and https://{VCD_HOST}/api-explorer/provider . 27 // OpenAPI has functions supporting below REST methods: 28 // GET /items (gets a slice of types like `[]types.OpenAPIEdgeGateway` or even `[]json.RawMessage` to process JSON as text. 29 // POST /items - creates an item 30 // PUT /items/URN - updates an item with specified URN 31 // GET /items/URN - retrieves an item with specified URN 32 // DELETE /items/URN - deletes an item with specified URN 33 // 34 // GET endpoints support FIQL for filtering in field `filter`. (FIQL IETF doc - https://tools.ietf.org/html/draft-nottingham-atompub-fiql-00) 35 // Not all API fields are supported for FIQL filtering and sometimes they return odd errors when filtering is 36 // unsupported. No exact documentation exists so far. 37 // 38 // Note. All functions accepting URL reference (*url.URL) will make a copy of URL because they may mutate URL reference. 39 // The parameter is kept as *url.URL for convenience because standard library provides pointer values. 40 // 41 // OpenAPI versioning. 42 // OpenAPI was introduced in VCD 9.5 (with API version 31.0). Endpoints are being added with each VCD iteration. 43 // Internally hosted documentation (https://HOSTNAME/docs/) can be used to check which endpoints where introduced in 44 // which VCD API version. 45 // Additionally each OpenAPI endpoint has a semantic version in its path (e.g. 46 // https://HOSTNAME/cloudapi/1.0.0/auditTrail). This versioned endpoint should ensure compatibility as VCD evolves. 47 48 // OpenApiIsSupported allows to check whether VCD supports OpenAPI. Each OpenAPI endpoint however is introduced with 49 // different VCD API versions so this is just a general check if OpenAPI is supported at all. Particular endpoint 50 // introduction version can be checked in self hosted docs (https://HOSTNAME/docs/) 51 func (client *Client) OpenApiIsSupported() bool { 52 // OpenAPI was introduced in VCD 9.5+ (API version 31.0+) 53 return client.APIVCDMaxVersionIs(">= 31") 54 } 55 56 // OpenApiBuildEndpoint helps to construct OpenAPI endpoint by using already configured VCD HREF while requiring only 57 // the last bit for endpoint. This is a variadic function and multiple pieces can be supplied for convenience. Leading 58 // '/' is added automatically. 59 // Sample URL construct: https://HOST/cloudapi/endpoint 60 func (client *Client) OpenApiBuildEndpoint(endpoint ...string) (*url.URL, error) { 61 endpointString := client.VCDHREF.Scheme + "://" + client.VCDHREF.Host + "/cloudapi/" + strings.Join(endpoint, "") 62 urlRef, err := url.ParseRequestURI(endpointString) 63 if err != nil { 64 return nil, fmt.Errorf("error formatting OpenAPI endpoint: %s", err) 65 } 66 return urlRef, nil 67 } 68 69 // OpenApiGetAllItems retrieves and accumulates all pages then parsing them to a single 'outType' object. It works by at 70 // first crawling pages and accumulating all responses into []json.RawMessage (as strings). Because there is no 71 // intermediate unmarshalling to exact `outType` for every page it unmarshals into response struct in one go. 'outType' 72 // must be a slice of object (e.g. []*types.OpenAPIEdgeGateway) because this response contains slice of structs. 73 // 74 // Note. Query parameter 'pageSize' is defaulted to 128 (maximum supported) unless it is specified in queryParams 75 func (client *Client) OpenApiGetAllItems(apiVersion string, urlRef *url.URL, queryParams url.Values, outType interface{}, additionalHeader map[string]string) error { 76 // copy passed in URL ref so that it is not mutated 77 urlRefCopy := copyUrlRef(urlRef) 78 79 util.Logger.Printf("[TRACE] Getting all items from endpoint %s for parsing into %s type\n", 80 urlRefCopy.String(), reflect.TypeOf(outType)) 81 82 if !client.OpenApiIsSupported() { 83 return fmt.Errorf("OpenAPI is not supported on this VCD version") 84 } 85 86 // Page size is defaulted to 128 (maximum supported number) to reduce HTTP calls and improve performance unless caller 87 // provides other value 88 newQueryParams := defaultPageSize(queryParams, "128") 89 util.Logger.Printf("[TRACE] Will use 'pageSize=%s'", newQueryParams.Get("pageSize")) 90 91 // Perform API call to initial endpoint. The function call recursively follows pages using Link headers "nextPage" 92 // until it crawls all results 93 responses, err := client.openApiGetAllPages(apiVersion, urlRefCopy, newQueryParams, outType, nil, additionalHeader) 94 if err != nil { 95 return fmt.Errorf("error getting all pages for endpoint %s: %s", urlRefCopy.String(), err) 96 } 97 98 // Create a slice of raw JSON messages in text so that they can be unmarshalled to specified `outType` after multiple 99 // calls are executed 100 var rawJsonBodies []string 101 for _, singleObject := range responses { 102 rawJsonBodies = append(rawJsonBodies, string(singleObject)) 103 } 104 105 // rawJsonBodies contains a slice of all response objects and they must be formatted as a JSON slice (wrapped 106 // into `[]`, separated with semicolons) so that unmarshalling to specified `outType` works in one go 107 allResponses := `[` + strings.Join(rawJsonBodies, ",") + `]` 108 109 // Unmarshal all accumulated responses into `outType` 110 if err = json.Unmarshal([]byte(allResponses), &outType); err != nil { 111 return fmt.Errorf("error decoding values into type: %s", err) 112 } 113 114 return nil 115 } 116 117 // OpenApiGetItem is a low level OpenAPI client function to perform GET request for any item. 118 // The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') 119 // It responds with HTTP 403: Forbidden - If the user is not authorized or the entity does not exist. When HTTP 403 is 120 // returned this function returns "ErrorEntityNotFound: API_ERROR" so that one can use ContainsNotFound(err) to 121 // differentiate when an object was not found from any other error. 122 func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params url.Values, outType interface{}, additionalHeader map[string]string) error { 123 _, err := client.OpenApiGetItemAndHeaders(apiVersion, urlRef, params, outType, additionalHeader) 124 return err 125 } 126 127 // OpenApiGetItemAndHeaders is a low level OpenAPI client function to perform GET request for any item and return all the headers. 128 // The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') 129 // It responds with HTTP 403: Forbidden - If the user is not authorized or the entity does not exist. When HTTP 403 is 130 // returned this function returns "ErrorEntityNotFound: API_ERROR" so that one can use ContainsNotFound(err) to 131 // differentiate when an object was not found from any other error. 132 func (client *Client) OpenApiGetItemAndHeaders(apiVersion string, urlRef *url.URL, params url.Values, outType interface{}, additionalHeader map[string]string) (http.Header, error) { 133 // copy passed in URL ref so that it is not mutated 134 urlRefCopy := copyUrlRef(urlRef) 135 136 util.Logger.Printf("[TRACE] Getting item from endpoint %s with expected response of type %s", 137 urlRefCopy.String(), reflect.TypeOf(outType)) 138 139 if !client.OpenApiIsSupported() { 140 return nil, fmt.Errorf("OpenAPI is not supported on this VCD version") 141 } 142 143 req := client.newOpenApiRequest(apiVersion, params, http.MethodGet, urlRefCopy, nil, additionalHeader) 144 resp, err := client.Http.Do(req) 145 if err != nil { 146 return nil, fmt.Errorf("error performing GET request to %s: %s", urlRefCopy.String(), err) 147 } 148 149 // Bypassing the regular path using function checkRespWithErrType and returning parsed error directly 150 // HTTP 403: Forbidden - is returned if the user is not authorized or the entity does not exist. 151 if resp.StatusCode == http.StatusForbidden { 152 err := ParseErr(types.BodyTypeJSON, resp, &types.OpenApiError{}) 153 closeErr := resp.Body.Close() 154 return nil, fmt.Errorf("%s: %s [body close error: %s]", ErrorEntityNotFound, err, closeErr) 155 } 156 157 // resp is ignored below because it is the same as above 158 _, err = checkRespWithErrType(types.BodyTypeJSON, resp, err, &types.OpenApiError{}) 159 160 // Any other error occurred 161 if err != nil { 162 return nil, fmt.Errorf("error in HTTP GET request: %s", err) 163 } 164 165 if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { 166 return nil, fmt.Errorf("error decoding JSON response after GET: %s", err) 167 } 168 169 err = resp.Body.Close() 170 if err != nil { 171 return nil, fmt.Errorf("error closing response body: %s", err) 172 } 173 174 return resp.Header, nil 175 } 176 177 // OpenApiPostItemSync is a low level OpenAPI client function to perform POST request for items that support synchronous 178 // requests. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways') that supports synchronous requests. It 179 // will return an error when endpoint does not support synchronous requests (HTTP response status code is not 200 or 201). 180 // Response will be unmarshalled into outType. 181 // 182 // Note. Even though it may return error if the item does not support synchronous request - the object may still be 183 // created. OpenApiPostItem would handle both cases and always return created item. 184 func (client *Client) OpenApiPostItemSync(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}) error { 185 // copy passed in URL ref so that it is not mutated 186 urlRefCopy := copyUrlRef(urlRef) 187 188 util.Logger.Printf("[TRACE] Posting %s item to endpoint %s with expected response of type %s", 189 reflect.TypeOf(payload), urlRefCopy.String(), reflect.TypeOf(outType)) 190 191 if !client.OpenApiIsSupported() { 192 return fmt.Errorf("OpenAPI is not supported on this VCD version") 193 } 194 195 resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload, nil) 196 if err != nil { 197 return err 198 } 199 200 if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { 201 util.Logger.Printf("[TRACE] Synchronous task expected (HTTP status code 200 or 201). Got %d", resp.StatusCode) 202 return fmt.Errorf("POST request expected sync task (HTTP response 200 or 201), got %d", resp.StatusCode) 203 204 } 205 206 if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { 207 return fmt.Errorf("error decoding JSON response after POST: %s", err) 208 } 209 210 err = resp.Body.Close() 211 if err != nil { 212 return fmt.Errorf("error closing response body: %s", err) 213 } 214 215 return nil 216 } 217 218 // OpenApiPostItemAsync is a low level OpenAPI client function to perform POST request for items that support 219 // asynchronous requests. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways') that supports asynchronous 220 // requests. It will return an error if item does not support asynchronous request (does not respond with HTTP 202). 221 // 222 // Note. Even though it may return error if the item does not support asynchronous request - the object may still be 223 // created. OpenApiPostItem would handle both cases and always return created item. 224 func (client *Client) OpenApiPostItemAsync(apiVersion string, urlRef *url.URL, params url.Values, payload interface{}) (Task, error) { 225 return client.OpenApiPostItemAsyncWithHeaders(apiVersion, urlRef, params, payload, nil) 226 } 227 228 // OpenApiPostItemAsyncWithHeaders is a low level OpenAPI client function to perform POST request for items that support 229 // asynchronous requests. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways') that supports asynchronous 230 // requests. It will return an error if item does not support asynchronous request (does not respond with HTTP 202). 231 // 232 // Note. Even though it may return error if the item does not support asynchronous request - the object may still be 233 // created. OpenApiPostItem would handle both cases and always return created item. 234 func (client *Client) OpenApiPostItemAsyncWithHeaders(apiVersion string, urlRef *url.URL, params url.Values, payload interface{}, additionalHeader map[string]string) (Task, error) { 235 // copy passed in URL ref so that it is not mutated 236 urlRefCopy := copyUrlRef(urlRef) 237 238 util.Logger.Printf("[TRACE] Posting async %s item to endpoint %s with expected task response", 239 reflect.TypeOf(payload), urlRefCopy.String()) 240 241 if !client.OpenApiIsSupported() { 242 return Task{}, fmt.Errorf("OpenAPI is not supported on this VCD version") 243 } 244 245 resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload, additionalHeader) 246 if err != nil { 247 return Task{}, err 248 } 249 250 if resp.StatusCode != http.StatusAccepted { 251 return Task{}, fmt.Errorf("POST request expected async task (HTTP response 202), got %d", resp.StatusCode) 252 } 253 254 err = resp.Body.Close() 255 if err != nil { 256 return Task{}, fmt.Errorf("error closing response body: %s", err) 257 } 258 259 // Asynchronous case returns "Location" header pointing to XML task 260 taskUrl := resp.Header.Get("Location") 261 if taskUrl == "" { 262 return Task{}, fmt.Errorf("unexpected empty task HREF") 263 } 264 task := NewTask(client) 265 task.Task.HREF = taskUrl 266 267 return *task, nil 268 } 269 270 // OpenApiPostItem is a low level OpenAPI client function to perform POST request for item supporting synchronous or 271 // asynchronous requests. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways'). When a task is 272 // synchronous - it will track task until it is finished and pick reference to marshal outType. 273 func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) error { 274 _, err := client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, params, payload, outType, additionalHeader) 275 return err 276 } 277 278 // OpenApiPostItemAndGetHeaders is a low level OpenAPI client function to perform POST request for item supporting synchronous or 279 // asynchronous requests, that returns also the response headers. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways'). When a task is 280 // synchronous - it will track task until it is finished and pick reference to marshal outType. 281 func (client *Client) OpenApiPostItemAndGetHeaders(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) (http.Header, error) { 282 // copy passed in URL ref so that it is not mutated 283 urlRefCopy := copyUrlRef(urlRef) 284 285 util.Logger.Printf("[TRACE] Posting %s item to endpoint %s with expected response of type %s", 286 reflect.TypeOf(payload), urlRefCopy.String(), reflect.TypeOf(outType)) 287 288 if !client.OpenApiIsSupported() { 289 return nil, fmt.Errorf("OpenAPI is not supported on this VCD version") 290 } 291 292 resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload, additionalHeader) 293 if err != nil { 294 return nil, err 295 } 296 297 // Handle two cases of API behaviour - synchronous (response status code is 200 or 201) and asynchronous (response status 298 // code 202) 299 switch resp.StatusCode { 300 // Asynchronous case - must track task and get item HREF from there 301 case http.StatusAccepted: 302 taskUrl := resp.Header.Get("Location") 303 util.Logger.Printf("[TRACE] Asynchronous task detected, tracking task with HREF: %s", taskUrl) 304 task := NewTask(client) 305 task.Task.HREF = taskUrl 306 err = task.WaitTaskCompletion() 307 if err != nil { 308 return nil, fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) 309 } 310 311 // Here we have to find the resource once more to return it populated. 312 // Task Owner ID is the ID of created object. ID must be used (although HREF exists in task) because HREF points to 313 // old XML API and here we need to pull data from OpenAPI. 314 315 newObjectUrl := urlParseRequestURI(urlRefCopy.String() + task.Task.Owner.ID) 316 err = client.OpenApiGetItem(apiVersion, newObjectUrl, nil, outType, additionalHeader) 317 if err != nil { 318 return nil, fmt.Errorf("error retrieving item after creation: %s", err) 319 } 320 321 // Synchronous task - new item body is returned in response of HTTP POST request 322 case http.StatusCreated, http.StatusOK: 323 util.Logger.Printf("[TRACE] Synchronous task detected (HTTP Status %d), marshalling outType '%s'", resp.StatusCode, reflect.TypeOf(outType)) 324 if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { 325 return nil, fmt.Errorf("error decoding JSON response after POST: %s", err) 326 } 327 } 328 329 err = resp.Body.Close() 330 if err != nil { 331 return nil, fmt.Errorf("error closing response body: %s", err) 332 } 333 334 return resp.Header, nil 335 } 336 337 // OpenApiPostUrlEncoded is a non-standard function used to send a POST request with `x-www-form-urlencoded` format. 338 // Accepts a map in format of key:value, marshals the response body in JSON format to outType. 339 // If additionalHeader contains a "Content-Type" header, it will be overwritten to "x-www-form-urlencoded" 340 func (client *Client) OpenApiPostUrlEncoded(apiVersion string, urlRef *url.URL, params url.Values, payloadMap map[string]string, outType interface{}, additionalHeaders map[string]string) error { 341 urlRefCopy := copyUrlRef(urlRef) 342 343 util.Logger.Printf("[TRACE] Sending a POST request with 'Content-Type: x-www-form-urlencoded' header to endpoint %s with expected response of type %s", urlRefCopy.String(), reflect.TypeOf(outType)) 344 345 // Add all values of the payloadMap to the actual payload 346 urlValues := url.Values{} 347 for key, value := range payloadMap { 348 urlValues.Add(key, value) 349 } 350 body := strings.NewReader(urlValues.Encode()) 351 352 // Create the header map if it's nil 353 if additionalHeaders == nil { 354 additionalHeaders = make(map[string]string) 355 } 356 // Overwrite the Content-Type header as this is a method only usable for x-www-form-urlencoded 357 additionalHeaders["Content-Type"] = "application/x-www-form-urlencoded" 358 359 req := client.newOpenApiRequest(apiVersion, params, http.MethodPost, urlRef, body, additionalHeaders) 360 resp, err := client.Http.Do(req) 361 if err != nil { 362 return err 363 } 364 365 // resp is ignored below because it is the same the one above 366 _, err = checkRespWithErrType(types.BodyTypeJSON, resp, err, &types.OpenApiError{}) 367 if err != nil { 368 return fmt.Errorf("error in HTTP %s request: %s", http.MethodPost, err) 369 } 370 371 if resp.StatusCode != http.StatusOK { 372 util.Logger.Printf("[TRACE] HTTP status code 200 expected. Got %d", resp.StatusCode) 373 } 374 375 if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { 376 return fmt.Errorf("error decoding JSON response after POST: %s", err) 377 } 378 379 err = resp.Body.Close() 380 if err != nil { 381 return fmt.Errorf("error closing response body: %s", err) 382 } 383 384 return nil 385 } 386 387 // OpenApiPutItemSync is a low level OpenAPI client function to perform PUT request for items that support synchronous 388 // requests. The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') and support synchronous 389 // requests. It will return an error when endpoint does not support synchronous requests (HTTP response status code is not 201). 390 // Response will be unmarshalled into outType. 391 // 392 // Note. Even though it may return error if the item does not support synchronous request - the object may still be 393 // updated. OpenApiPutItem would handle both cases and always return updated item. 394 func (client *Client) OpenApiPutItemSync(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) error { 395 // copy passed in URL ref so that it is not mutated 396 urlRefCopy := copyUrlRef(urlRef) 397 398 util.Logger.Printf("[TRACE] Putting %s item to endpoint %s with expected response of type %s", 399 reflect.TypeOf(payload), urlRefCopy.String(), reflect.TypeOf(outType)) 400 401 if !client.OpenApiIsSupported() { 402 return fmt.Errorf("OpenAPI is not supported on this VCD version") 403 } 404 405 resp, err := client.openApiPerformPostPut(http.MethodPut, apiVersion, urlRefCopy, params, payload, additionalHeader) 406 if err != nil { 407 return err 408 } 409 410 if resp.StatusCode != http.StatusCreated { 411 util.Logger.Printf("[TRACE] Synchronous task expected (HTTP status code 201). Got %d", resp.StatusCode) 412 } 413 414 if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { 415 return fmt.Errorf("error decoding JSON response after PUT: %s", err) 416 } 417 418 err = resp.Body.Close() 419 if err != nil { 420 return fmt.Errorf("error closing response body: %s", err) 421 } 422 423 return nil 424 } 425 426 // OpenApiPutItemAsync is a low level OpenAPI client function to perform PUT request for items that support asynchronous 427 // requests. The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') that supports asynchronous 428 // requests. It will return an error if item does not support asynchronous request (does not respond with HTTP 202). 429 // 430 // Note. Even though it may return error if the item does not support asynchronous request - the object may still be 431 // created. OpenApiPutItem would handle both cases and always return created item. 432 func (client *Client) OpenApiPutItemAsync(apiVersion string, urlRef *url.URL, params url.Values, payload interface{}, additionalHeader map[string]string) (Task, error) { 433 // copy passed in URL ref so that it is not mutated 434 urlRefCopy := copyUrlRef(urlRef) 435 436 util.Logger.Printf("[TRACE] Putting async %s item to endpoint %s with expected task response", 437 reflect.TypeOf(payload), urlRefCopy.String()) 438 439 if !client.OpenApiIsSupported() { 440 return Task{}, fmt.Errorf("OpenAPI is not supported on this VCD version") 441 } 442 resp, err := client.openApiPerformPostPut(http.MethodPut, apiVersion, urlRefCopy, params, payload, additionalHeader) 443 if err != nil { 444 return Task{}, err 445 } 446 447 if resp.StatusCode != http.StatusAccepted { 448 return Task{}, fmt.Errorf("PUT request expected async task (HTTP response 202), got %d", resp.StatusCode) 449 } 450 451 err = resp.Body.Close() 452 if err != nil { 453 return Task{}, fmt.Errorf("error closing response body: %s", err) 454 } 455 456 // Asynchronous case returns "Location" header pointing to XML task 457 taskUrl := resp.Header.Get("Location") 458 if taskUrl == "" { 459 return Task{}, fmt.Errorf("unexpected empty task HREF") 460 } 461 task := NewTask(client) 462 task.Task.HREF = taskUrl 463 464 return *task, nil 465 } 466 467 // OpenApiPutItem is a low level OpenAPI client function to perform PUT request for any item. 468 // The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') 469 // It handles synchronous and asynchronous tasks. When a task is synchronous - it will block until it is finished. 470 func (client *Client) OpenApiPutItem(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) error { 471 _, err := client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, params, payload, outType, additionalHeader) 472 return err 473 } 474 475 // OpenApiPutItemAndGetHeaders is a low level OpenAPI client function to perform PUT request for any item and return the response headers. 476 // The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') 477 // It handles synchronous and asynchronous tasks. When a task is synchronous - it will block until it is finished. 478 func (client *Client) OpenApiPutItemAndGetHeaders(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) (http.Header, error) { 479 // copy passed in URL ref so that it is not mutated 480 urlRefCopy := copyUrlRef(urlRef) 481 482 util.Logger.Printf("[TRACE] Putting %s item to endpoint %s with expected response of type %s", 483 reflect.TypeOf(payload), urlRefCopy.String(), reflect.TypeOf(outType)) 484 485 if !client.OpenApiIsSupported() { 486 return nil, fmt.Errorf("OpenAPI is not supported on this VCD version") 487 } 488 resp, err := client.openApiPerformPostPut(http.MethodPut, apiVersion, urlRefCopy, params, payload, additionalHeader) 489 490 if err != nil { 491 return nil, err 492 } 493 494 // Handle two cases of API behaviour - synchronous (response status code is 201) and asynchronous (response status 495 // code 202) 496 switch resp.StatusCode { 497 // Asynchronous case - must track task and get item HREF from there 498 case http.StatusAccepted: 499 taskUrl := resp.Header.Get("Location") 500 util.Logger.Printf("[TRACE] Asynchronous task detected, tracking task with HREF: %s", taskUrl) 501 task := NewTask(client) 502 task.Task.HREF = taskUrl 503 err = task.WaitTaskCompletion() 504 if err != nil { 505 return nil, fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) 506 } 507 508 // Here we have to find the resource once more to return it populated. Provided params ir ignored for retrieval. 509 err = client.OpenApiGetItem(apiVersion, urlRefCopy, nil, outType, additionalHeader) 510 if err != nil { 511 return nil, fmt.Errorf("error retrieving item after updating: %s", err) 512 } 513 514 // Synchronous task - new item body is returned in response of HTTP PUT request 515 case http.StatusOK: 516 util.Logger.Printf("[TRACE] Synchronous task detected, marshalling outType '%s'", reflect.TypeOf(outType)) 517 if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { 518 return nil, fmt.Errorf("error decoding JSON response after PUT: %s", err) 519 } 520 } 521 522 err = resp.Body.Close() 523 if err != nil { 524 return nil, fmt.Errorf("error closing HTTP PUT response body: %s", err) 525 } 526 527 return resp.Header, nil 528 } 529 530 // OpenApiDeleteItem is a low level OpenAPI client function to perform DELETE request for any item. 531 // The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') 532 // It handles synchronous and asynchronous tasks. When a task is synchronous - it will block until it is finished. 533 func (client *Client) OpenApiDeleteItem(apiVersion string, urlRef *url.URL, params url.Values, additionalHeader map[string]string) error { 534 // copy passed in URL ref so that it is not mutated 535 urlRefCopy := copyUrlRef(urlRef) 536 537 util.Logger.Printf("[TRACE] Deleting item at endpoint %s", urlRefCopy.String()) 538 539 if !client.OpenApiIsSupported() { 540 return fmt.Errorf("OpenAPI is not supported on this VCD version") 541 } 542 543 // Perform request 544 req := client.newOpenApiRequest(apiVersion, params, http.MethodDelete, urlRefCopy, nil, additionalHeader) 545 546 resp, err := client.Http.Do(req) 547 if err != nil { 548 return err 549 } 550 551 // resp is ignored below because it would be the same as above 552 _, err = checkRespWithErrType(types.BodyTypeJSON, resp, err, &types.OpenApiError{}) 553 if err != nil { 554 return fmt.Errorf("error in HTTP DELETE request: %s", err) 555 } 556 557 err = resp.Body.Close() 558 if err != nil { 559 return fmt.Errorf("error closing response body: %s", err) 560 } 561 562 // OpenAPI may work synchronously or asynchronously. When working asynchronously - it will return HTTP 202 and 563 // `Location` header will contain reference to task so that it can be tracked. In DELETE case we do not care about any 564 // ID so if DELETE operation is synchronous (returns HTTP 201) - the request has already succeeded. 565 if resp.StatusCode == http.StatusAccepted { 566 taskUrl := resp.Header.Get("Location") 567 task := NewTask(client) 568 task.Task.HREF = taskUrl 569 err = task.WaitTaskCompletion() 570 if err != nil { 571 return fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) 572 } 573 } 574 575 return nil 576 } 577 578 // openApiPerformPostPut is a shared function for all public PUT and POST function parts - OpenApiPostItemSync, 579 // OpenApiPostItemAsync, OpenApiPostItem, OpenApiPutItemSync, OpenApiPutItemAsync, OpenApiPutItem 580 func (client *Client) openApiPerformPostPut(httpMethod string, apiVersion string, urlRef *url.URL, params url.Values, payload interface{}, additionalHeader map[string]string) (*http.Response, error) { 581 // Marshal payload if we have one 582 body := new(bytes.Buffer) 583 if payload != nil { 584 marshaledJson, err := json.MarshalIndent(payload, "", " ") 585 if err != nil { 586 return nil, fmt.Errorf("error marshalling JSON data for %s request %s", httpMethod, err) 587 } 588 body = bytes.NewBuffer(marshaledJson) 589 } 590 591 req := client.newOpenApiRequest(apiVersion, params, httpMethod, urlRef, body, additionalHeader) 592 resp, err := client.Http.Do(req) 593 if err != nil { 594 return nil, err 595 } 596 597 // resp is ignored below because it is the same the one above 598 _, err = checkRespWithErrType(types.BodyTypeJSON, resp, err, &types.OpenApiError{}) 599 if err != nil { 600 return nil, fmt.Errorf("error in HTTP %s request: %s", httpMethod, err) 601 } 602 return resp, nil 603 } 604 605 // openApiGetAllPages is a recursive function that helps to accumulate responses from multiple pages for GET query. It 606 // works by at first crawling pages and accumulating all responses into []json.RawMessage (as strings). Because there is 607 // no intermediate unmarshalling to exact `outType` for every page it can unmarshal into direct `outType` supplied. 608 // outType must be a slice of object (e.g. []*types.OpenApiRole) because accumulated responses are in JSON list 609 // 610 // It follows pages in two ways: 611 // * Finds a 'nextPage' link and uses it to recursively crawl all pages (default for all, except for API bug) 612 // * Uses fields 'resultTotal', 'page', and 'pageSize' to calculate if it should crawl further on. It is only done 613 // because there is a BUG in API and in some endpoints it does not return 'nextPage' link as well as null 'pageCount' 614 // 615 // In general 'nextPage' header is preferred because some endpoints 616 // (like cloudapi/1.0.0/nsxTResources/importableTier0Routers) do not contain pagination details and nextPage header 617 // contains a base64 encoded data chunk via a supplied `cursor` field 618 // (e.g. ...importableTier0Routers?filter=_context==urn:vcloud:nsxtmanager:85aa2514-6a6f-4a32-8904-9695dc0f0298& 619 // cursor=eyJORVRXT1JLSU5HX0NVUlNPUl9PRkZTRVQiOiIwIiwicGFnZVNpemUiOjEsIk5FVFdPUktJTkdfQ1VSU09SIjoiMDAwMTMifQ==) 620 // The 'cursor' in example contains such values {"NETWORKING_CURSOR_OFFSET":"0","pageSize":1,"NETWORKING_CURSOR":"00013"} 621 func (client *Client) openApiGetAllPages(apiVersion string, urlRef *url.URL, queryParams url.Values, outType interface{}, responses []json.RawMessage, additionalHeader map[string]string) ([]json.RawMessage, error) { 622 // copy passed in URL ref so that it is not mutated 623 urlRefCopy := copyUrlRef(urlRef) 624 625 if responses == nil { 626 responses = []json.RawMessage{} 627 } 628 629 // Perform request 630 req := client.newOpenApiRequest(apiVersion, queryParams, http.MethodGet, urlRefCopy, nil, additionalHeader) 631 632 resp, err := client.Http.Do(req) 633 if err != nil { 634 return nil, err 635 } 636 637 // resp is ignored below because it is the same as above 638 _, err = checkRespWithErrType(types.BodyTypeJSON, resp, err, &types.OpenApiError{}) 639 if err != nil { 640 return nil, fmt.Errorf("error in HTTP GET request: %s", err) 641 } 642 643 // Pages will unwrap pagination and keep a slice of raw json message to marshal to specific types 644 pages := &types.OpenApiPages{} 645 646 if err = decodeBody(types.BodyTypeJSON, resp, pages); err != nil { 647 return nil, fmt.Errorf("error decoding JSON page response: %s", err) 648 } 649 650 err = resp.Body.Close() 651 if err != nil { 652 return nil, fmt.Errorf("error closing response body: %s", err) 653 } 654 655 // Accumulate all responses in a single page as JSON text using json.RawMessage 656 // After pages are unwrapped one can marshal response into specified type 657 // singleQueryResponses := &json.RawMessage{} 658 var singleQueryResponses []json.RawMessage 659 if err = json.Unmarshal(pages.Values, &singleQueryResponses); err != nil { 660 return nil, fmt.Errorf("error decoding values into accumulation type: %s", err) 661 } 662 responses = append(responses, singleQueryResponses...) 663 664 // Check if there is still 'nextPage' linked and continue accumulating responses if so 665 nextPageUrlRef, err := findRelLink("nextPage", resp.Header) 666 if err != nil && !IsNotFound(err) { 667 return nil, fmt.Errorf("error looking for 'nextPage' in 'Link' header: %s", err) 668 } 669 670 if nextPageUrlRef != nil { 671 responses, err = client.openApiGetAllPages(apiVersion, nextPageUrlRef, url.Values{}, outType, responses, additionalHeader) 672 if err != nil { 673 return nil, fmt.Errorf("got error on page %d: %s", pages.Page, err) 674 } 675 } 676 677 // If nextPage header was not found, but we are not at the last page - the query URL should be forged manually to 678 // overcome OpenAPI BUG when it does not return 'nextPage' header 679 // Some API calls do not return `OpenApiPages` results at all (just values) 680 // In some endpoints the page field is returned as `null` and this code block cannot handle it. 681 if nextPageUrlRef == nil && pages.PageSize != 0 && pages.Page != 0 { 682 // Next URL page ref was not found therefore one must double-check if it is not an API BUG. There are endpoints which 683 // return only Total results and pageSize (not 'pageCount' and not 'nextPage' header) 684 pageCount := pages.ResultTotal / pages.PageSize // This division returns number of "full pages" (containing 'pageSize' amount of results) 685 if pages.ResultTotal%pages.PageSize > 0 { // Check if is an incomplete page (containing less than 'pageSize' results) 686 pageCount++ // Total pageCount is "number of complete pages + 1 incomplete" if it exists) 687 } 688 if pages.Page < pageCount { 689 // Clone all originally supplied query parameters to avoid overwriting them 690 urlQueryString := queryParams.Encode() 691 urlQuery, err := url.ParseQuery(urlQueryString) 692 if err != nil { 693 return nil, fmt.Errorf("error cloning queryParams: %s", err) 694 } 695 696 // Increase page query by one to fetch "next" page 697 urlQuery.Set("page", strconv.Itoa(pages.Page+1)) 698 699 responses, err = client.openApiGetAllPages(apiVersion, urlRefCopy, urlQuery, outType, responses, additionalHeader) 700 if err != nil { 701 return nil, fmt.Errorf("got error on page %d: %s", pages.Page, err) 702 } 703 } 704 705 } 706 707 return responses, nil 708 } 709 710 // newOpenApiRequest is a low level function used in upstream OpenAPI functions which handles logging and 711 // authentication for each API request 712 func (client *Client) newOpenApiRequest(apiVersion string, params url.Values, method string, reqUrl *url.URL, body io.Reader, additionalHeader map[string]string) *http.Request { 713 // copy passed in URL ref so that it is not mutated 714 reqUrlCopy := copyUrlRef(reqUrl) 715 716 // Add the params to our URL 717 reqUrlCopy.RawQuery += params.Encode() 718 719 // If the body contains data - try to read all contents for logging and re-create another 720 // io.Reader with all contents to use it down the line 721 var readBody []byte 722 var err error 723 if body != nil { 724 readBody, err = io.ReadAll(body) 725 if err != nil { 726 util.Logger.Printf("[DEBUG - newOpenApiRequest] error reading body: %s", err) 727 } 728 body = bytes.NewReader(readBody) 729 } 730 731 req, err := http.NewRequest(method, reqUrlCopy.String(), body) 732 if err != nil { 733 util.Logger.Printf("[DEBUG - newOpenApiRequest] error getting new request: %s", err) 734 } 735 736 if client.VCDAuthHeader != "" && client.VCDToken != "" { 737 // Add the authorization header 738 req.Header.Add(client.VCDAuthHeader, client.VCDToken) 739 // The deprecated authorization token is 32 characters long 740 // The bearer token is 612 characters long 741 if len(client.VCDToken) > 32 { 742 req.Header.Add("Authorization", "bearer "+client.VCDToken) 743 req.Header.Add("X-Vmware-Vcloud-Token-Type", "Bearer") 744 } 745 // Add the Accept header for VCD 746 acceptMime := types.JSONMime + ";version=" + apiVersion 747 req.Header.Add("Accept", acceptMime) 748 } 749 750 for k, v := range client.customHeader { 751 for _, v1 := range v { 752 req.Header.Set(k, v1) 753 } 754 } 755 for k, v := range additionalHeader { 756 req.Header.Add(k, v) 757 } 758 759 // Inject JSON mime type if there are no overwrites 760 if req.Header.Get("Content-Type") == "" { 761 req.Header.Add("Content-Type", types.JSONMime) 762 } 763 764 setHttpUserAgent(client.UserAgent, req) 765 766 // Avoids passing data if the logging of requests is disabled 767 if util.LogHttpRequest { 768 payload := "" 769 if req.ContentLength > 0 { 770 payload = string(readBody) 771 } 772 util.ProcessRequestOutput(util.FuncNameCallStack(), method, reqUrlCopy.String(), payload, req) 773 debugShowRequest(req, payload) 774 } 775 776 return req 777 } 778 779 // findRelLink looks for link to "nextPage" in "Link" header. It will return when first occurrence is found. 780 // Sample Link header: 781 // Link: [<https://HOSTNAME/cloudapi/1.0.0/auditTrail?sortAsc=&pageSize=25&sortDesc=&page=7>;rel="lastPage"; 782 // type="application/json";model="AuditTrailEvents" <https://HOSTNAME/cloudapi/1.0.0/auditTrail?sortAsc=&pageSize=25&sortDesc=&page=2>; 783 // rel="nextPage";type="application/json";model="AuditTrailEvents"] 784 // Returns *url.Url or ErrorEntityNotFound 785 func findRelLink(relFieldName string, header http.Header) (*url.URL, error) { 786 headerLinks := link.ParseHeader(header) 787 788 for relKeyName, linkAddress := range headerLinks { 789 switch { 790 // When map key has more than one name (separated by space). In such cases it can have map key as 791 // "lastPage nextPage" when nextPage==lastPage or similar and one specific field needs to be matched. 792 case strings.Contains(relKeyName, " "): 793 relNameSlice := strings.Split(relKeyName, " ") 794 for _, oneRelName := range relNameSlice { 795 if oneRelName == relFieldName { 796 return url.Parse(linkAddress.String()) 797 } 798 } 799 case relKeyName == relFieldName: 800 return url.Parse(linkAddress.String()) 801 } 802 } 803 804 return nil, ErrorEntityNotFound 805 } 806 807 // jsonRawMessagesToStrings converts []*json.RawMessage to []string 808 func jsonRawMessagesToStrings(messages []json.RawMessage) []string { 809 resultString := make([]string, len(messages)) 810 for index, message := range messages { 811 resultString[index] = string(message) 812 } 813 814 return resultString 815 } 816 817 // copyOrNewUrlValues either creates a copy of parameters or instantiates a new url.Values if nil parameters are 818 // supplied. It helps to avoid mutating supplied parameter when additional values must be injected internally. 819 func copyOrNewUrlValues(parameters url.Values) url.Values { 820 parameterCopy := make(map[string][]string) 821 822 // if supplied parameters are nil - we just return new initialized 823 if parameters == nil { 824 return parameterCopy 825 } 826 827 // Copy URL values 828 for key, value := range parameters { 829 parameterCopy[key] = value 830 } 831 832 return parameterCopy 833 } 834 835 // queryParameterFilterAnd is a helper to append "AND" clause to FIQL filter by using ';' (semicolon) if any values are 836 // already set in 'filter' value of parameters. If none existed before then 'filter' value will be set. 837 // 838 // Note. It does a copy of supplied 'parameters' value and does not mutate supplied original parameters. 839 func queryParameterFilterAnd(filter string, parameters url.Values) url.Values { 840 newParameters := copyOrNewUrlValues(parameters) 841 842 existingFilter := newParameters.Get("filter") 843 if existingFilter == "" { 844 newParameters.Set("filter", filter) 845 return newParameters 846 } 847 848 newParameters.Set("filter", existingFilter+";"+filter) 849 return newParameters 850 } 851 852 // defaultPageSize allows to set 'pageSize' query parameter to defaultPageSize if one is not already specified in 853 // url.Values while preserving all other supplied url.Values 854 func defaultPageSize(queryParams url.Values, defaultPageSize string) url.Values { 855 newQueryParams := url.Values{} 856 if queryParams != nil { 857 newQueryParams = queryParams 858 } 859 860 if _, ok := newQueryParams["pageSize"]; !ok { 861 newQueryParams.Set("pageSize", defaultPageSize) 862 } 863 864 return newQueryParams 865 } 866 867 // copyUrlRef creates a copy of URL reference by re-parsing it 868 func copyUrlRef(in *url.URL) *url.URL { 869 // error is ignored because we expect to have correct URL supplied and this greatly simplifies code inside. 870 newUrlRef, err := url.Parse(in.String()) 871 if err != nil { 872 util.Logger.Printf("[DEBUG - copyUrlRef] error parsing URL: %s", err) 873 } 874 return newUrlRef 875 } 876 877 // shouldDoSlowSearch returns true and nil url.Values if the filter value contains commas, semicolons or asterisks, 878 // as the encoding is rejected by VCD with an error: QueryParseException: Cannot parse the supplied filter, so 879 // the caller knows that it needs to run a brute force search and NOT use filtering in any case. 880 // Also, url.QueryEscape as well as url.Values.Encode() both encode the space as a + character, so in this case 881 // it returns true and nil to specify a brute force search too. Reference to issue: 882 // https://github.com/golang/go/issues/4013 883 // https://github.com/czos/goamz/pull/11/files 884 // When this function returns false, it returns the url.Values that are not encoded, so make sure that the 885 // client encodes them before sending them. 886 func shouldDoSlowSearch(filterKey, filterValue string) (bool, url.Values) { 887 if strings.Contains(filterValue, ",") || strings.Contains(filterValue, ";") || 888 strings.Contains(filterValue, " ") || strings.Contains(filterValue, "+") || strings.Contains(filterValue, "*") { 889 return true, nil 890 } else { 891 params := url.Values{} 892 params.Set("filter", fmt.Sprintf(filterKey+"==%s", filterValue)) 893 params.Set("filterEncoded", "true") 894 return false, params 895 } 896 }