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  }