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

     1  /*
     2   * Copyright 2021 VMware, Inc.  All rights reserved.  Licensed under the Apache v2 License.
     3   */
     4  
     5  // Package govcd provides a simple binding for VMware Cloud Director REST APIs.
     6  package govcd
     7  
     8  import (
     9  	"bytes"
    10  	"encoding/json"
    11  	"encoding/xml"
    12  	"fmt"
    13  	"io"
    14  	"net/http"
    15  	"net/url"
    16  	"os"
    17  	"regexp"
    18  	"strconv"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/vmware/go-vcloud-director/v2/types/v56"
    23  	"github.com/vmware/go-vcloud-director/v2/util"
    24  )
    25  
    26  // Client provides a client to VMware Cloud Director, values can be populated automatically using the Authenticate method.
    27  type Client struct {
    28  	APIVersion       string      // The API version required
    29  	VCDToken         string      // Access Token (authorization header)
    30  	VCDAuthHeader    string      // Authorization header
    31  	VCDHREF          url.URL     // VCD API ENDPOINT
    32  	Http             http.Client // HttpClient is the client to use. Default will be used if not provided.
    33  	IsSysAdmin       bool        // flag if client is connected as system administrator
    34  	UsingBearerToken bool        // flag if client is using a bearer token
    35  	UsingAccessToken bool        // flag if client is using an API token
    36  
    37  	// MaxRetryTimeout specifies a time limit (in seconds) for retrying requests made by the SDK
    38  	// where VMware Cloud Director may take time to respond and retry mechanism is needed.
    39  	// This must be >0 to avoid instant timeout errors.
    40  	MaxRetryTimeout int
    41  
    42  	// UseSamlAdfs specifies if SAML auth is used for authenticating vCD instead of local login.
    43  	// The following conditions must be met so that authentication SAML authentication works:
    44  	// * SAML IdP (Identity Provider) is Active Directory Federation Service (ADFS)
    45  	// * Authentication endpoint "/adfs/services/trust/13/usernamemixed" must be enabled on ADFS
    46  	// server
    47  	UseSamlAdfs bool
    48  	// CustomAdfsRptId allows to set custom Relaying Party Trust identifier. By default vCD Entity
    49  	// ID is used as Relaying Party Trust identifier.
    50  	CustomAdfsRptId string
    51  
    52  	// UserAgent to send for API queries. Standard format is described as:
    53  	// "User-Agent: <product> / <product-version> <comment>"
    54  	UserAgent string
    55  
    56  	// IgnoredMetadata allows to ignore metadata entries when using the methods defined in metadata_v2.go
    57  	IgnoredMetadata []IgnoredMetadata
    58  
    59  	supportedVersions SupportedVersions // Versions from /api/versions endpoint
    60  	customHeader      http.Header
    61  }
    62  
    63  // AuthorizationHeader header key used by default to set the authorization token.
    64  const AuthorizationHeader = "X-Vcloud-Authorization"
    65  
    66  // BearerTokenHeader is the header key containing a bearer token
    67  // #nosec G101 -- This is not a credential, it's just the header key
    68  const BearerTokenHeader = "X-Vmware-Vcloud-Access-Token"
    69  
    70  const ApiTokenHeader = "API-token"
    71  
    72  // General purpose error to be used whenever an entity is not found from a "GET" request
    73  // Allows a simpler checking of the call result
    74  // such as
    75  //
    76  //	if err == ErrorEntityNotFound {
    77  //	   // do what is needed in case of not found
    78  //	}
    79  var errorEntityNotFoundMessage = "[ENF] entity not found"
    80  var ErrorEntityNotFound = fmt.Errorf(errorEntityNotFoundMessage)
    81  
    82  // Triggers for debugging functions that show requests and responses
    83  var debugShowRequestEnabled = os.Getenv("GOVCD_SHOW_REQ") != ""
    84  var debugShowResponseEnabled = os.Getenv("GOVCD_SHOW_RESP") != ""
    85  
    86  // Enables the debugging hook to show requests as they are processed.
    87  //
    88  //lint:ignore U1000 this function is used on request for debugging purposes
    89  func enableDebugShowRequest() {
    90  	debugShowRequestEnabled = true
    91  }
    92  
    93  // Disables the debugging hook to show requests as they are processed.
    94  //
    95  //lint:ignore U1000 this function is used on request for debugging purposes
    96  func disableDebugShowRequest() {
    97  	debugShowRequestEnabled = false
    98  	err := os.Setenv("GOVCD_SHOW_REQ", "")
    99  	if err != nil {
   100  		util.Logger.Printf("[DEBUG - disableDebugShowRequest] error setting environment variable: %s", err)
   101  	}
   102  }
   103  
   104  // Enables the debugging hook to show responses as they are processed.
   105  //
   106  //lint:ignore U1000 this function is used on request for debugging purposes
   107  func enableDebugShowResponse() {
   108  	debugShowResponseEnabled = true
   109  }
   110  
   111  // Disables the debugging hook to show responses as they are processed.
   112  //
   113  //lint:ignore U1000 this function is used on request for debugging purposes
   114  func disableDebugShowResponse() {
   115  	debugShowResponseEnabled = false
   116  	err := os.Setenv("GOVCD_SHOW_RESP", "")
   117  	if err != nil {
   118  		util.Logger.Printf("[DEBUG - disableDebugShowResponse] error setting environment variable: %s", err)
   119  	}
   120  }
   121  
   122  // On-the-fly debug hook. If either debugShowRequestEnabled or the environment
   123  // variable "GOVCD_SHOW_REQ" are enabled, this function will show the contents
   124  // of the request as it is being processed.
   125  func debugShowRequest(req *http.Request, payload string) {
   126  	if debugShowRequestEnabled {
   127  		header := "[\n"
   128  		for key, value := range util.SanitizedHeader(req.Header) {
   129  			header += fmt.Sprintf("\t%s => %s\n", key, value)
   130  		}
   131  		header += "]\n"
   132  		fmt.Println("** REQUEST **")
   133  		fmt.Printf("time:    %s\n", time.Now().Format("2006-01-02T15:04:05.000Z"))
   134  		fmt.Printf("method:  %s\n", req.Method)
   135  		fmt.Printf("host:    %s\n", req.Host)
   136  		fmt.Printf("length:  %d\n", req.ContentLength)
   137  		fmt.Printf("URL:     %s\n", req.URL.String())
   138  		fmt.Printf("header:  %s\n", header)
   139  		fmt.Printf("payload: %s\n", payload)
   140  		fmt.Println()
   141  	}
   142  }
   143  
   144  // On-the-fly debug hook. If either debugShowResponseEnabled or the environment
   145  // variable "GOVCD_SHOW_RESP" are enabled, this function will show the contents
   146  // of the response as it is being processed.
   147  func debugShowResponse(resp *http.Response, body []byte) {
   148  	if debugShowResponseEnabled {
   149  		fmt.Println("## RESPONSE ##")
   150  		fmt.Printf("time:   %s\n", time.Now().Format("2006-01-02T15:04:05.000Z"))
   151  		fmt.Printf("status: %d - %s \n", resp.StatusCode, resp.Status)
   152  		fmt.Printf("length: %d\n", resp.ContentLength)
   153  		fmt.Printf("header: %v\n", util.SanitizedHeader(resp.Header))
   154  		fmt.Printf("body:   %s\n", body)
   155  		fmt.Println()
   156  	}
   157  }
   158  
   159  // IsNotFound is a convenience function, similar to os.IsNotExist that checks whether a given error
   160  // is a "Not found" error, such as
   161  //
   162  //	if isNotFound(err) {
   163  //	   // do what is needed in case of not found
   164  //	}
   165  func IsNotFound(err error) bool {
   166  	return err != nil && err == ErrorEntityNotFound
   167  }
   168  
   169  // ContainsNotFound is a convenience function, similar to os.IsNotExist that checks whether a given error
   170  // contains a "Not found" error. It is almost the same as `IsNotFound` but checks if an error contains substring
   171  // ErrorEntityNotFound
   172  func ContainsNotFound(err error) bool {
   173  	return err != nil && strings.Contains(err.Error(), ErrorEntityNotFound.Error())
   174  }
   175  
   176  // NewRequestWitNotEncodedParams allows passing complex values params that shouldn't be encoded like for queries. e.g. /query?filter=name=foo
   177  func (client *Client) NewRequestWitNotEncodedParams(params map[string]string, notEncodedParams map[string]string, method string, reqUrl url.URL, body io.Reader) *http.Request {
   178  	return client.NewRequestWitNotEncodedParamsWithApiVersion(params, notEncodedParams, method, reqUrl, body, client.APIVersion)
   179  }
   180  
   181  // NewRequestWitNotEncodedParamsWithApiVersion allows passing complex values params that shouldn't be encoded like for queries. e.g. /query?filter=name=foo
   182  // * params - request parameters
   183  // * notEncodedParams - request parameters which will be added not encoded
   184  // * method - request type
   185  // * reqUrl - request url
   186  // * body - request body
   187  // * apiVersion - provided Api version overrides default Api version value used in request parameter
   188  func (client *Client) NewRequestWitNotEncodedParamsWithApiVersion(params map[string]string, notEncodedParams map[string]string, method string, reqUrl url.URL, body io.Reader, apiVersion string) *http.Request {
   189  	return client.newRequest(params, notEncodedParams, method, reqUrl, body, apiVersion, nil)
   190  }
   191  
   192  // newRequest is the parent of many "specific" "NewRequest" functions.
   193  // Note. It is kept private to avoid breaking public API on every new field addition.
   194  func (client *Client) newRequest(params map[string]string, notEncodedParams map[string]string, method string, reqUrl url.URL, body io.Reader, apiVersion string, additionalHeader http.Header) *http.Request {
   195  	reqValues := url.Values{}
   196  
   197  	// Build up our request parameters
   198  	for key, value := range params {
   199  		reqValues.Add(key, value)
   200  	}
   201  
   202  	// Add the params to our URL
   203  	reqUrl.RawQuery = reqValues.Encode()
   204  
   205  	for key, value := range notEncodedParams {
   206  		if key != "" && value != "" {
   207  			reqUrl.RawQuery += "&" + key + "=" + value
   208  		}
   209  	}
   210  
   211  	// If the body contains data - try to read all contents for logging and re-create another
   212  	// io.Reader with all contents to use it down the line
   213  	var readBody []byte
   214  	var err error
   215  	if body != nil {
   216  		readBody, err = io.ReadAll(body)
   217  		if err != nil {
   218  			util.Logger.Printf("[DEBUG - newRequest] error reading body: %s", err)
   219  		}
   220  		body = bytes.NewReader(readBody)
   221  	}
   222  
   223  	req, err := http.NewRequest(method, reqUrl.String(), body)
   224  	if err != nil {
   225  		util.Logger.Printf("[DEBUG - newRequest] error getting new request: %s", err)
   226  	}
   227  
   228  	if client.VCDAuthHeader != "" && client.VCDToken != "" {
   229  		// Add the authorization header
   230  		req.Header.Add(client.VCDAuthHeader, client.VCDToken)
   231  	}
   232  	if (client.VCDAuthHeader != "" && client.VCDToken != "") ||
   233  		(additionalHeader != nil && additionalHeader.Get("Authorization") != "") {
   234  		// Add the Accept header for VCD
   235  		req.Header.Add("Accept", "application/*+xml;version="+apiVersion)
   236  	}
   237  	// The deprecated authorization token is 32 characters long
   238  	// The bearer token is 612 characters long
   239  	if len(client.VCDToken) > 32 {
   240  		req.Header.Add("X-Vmware-Vcloud-Token-Type", "Bearer")
   241  		req.Header.Add("Authorization", "bearer "+client.VCDToken)
   242  	}
   243  
   244  	// Merge in additional headers before logging if anywhere specified in additionalHeader
   245  	// parameter
   246  	if len(additionalHeader) > 0 {
   247  		for headerName, headerValueSlice := range additionalHeader {
   248  			for _, singleHeaderValue := range headerValueSlice {
   249  				req.Header.Set(headerName, singleHeaderValue)
   250  			}
   251  		}
   252  	}
   253  	if client.customHeader != nil {
   254  		for k, v := range client.customHeader {
   255  			for _, v1 := range v {
   256  				req.Header.Add(k, v1)
   257  			}
   258  		}
   259  	}
   260  
   261  	setHttpUserAgent(client.UserAgent, req)
   262  
   263  	// Avoids passing data if the logging of requests is disabled
   264  	if util.LogHttpRequest {
   265  		payload := ""
   266  		if req.ContentLength > 0 {
   267  			payload = string(readBody)
   268  		}
   269  		util.ProcessRequestOutput(util.FuncNameCallStack(), method, reqUrl.String(), payload, req)
   270  		debugShowRequest(req, payload)
   271  	}
   272  
   273  	return req
   274  
   275  }
   276  
   277  // NewRequest creates a new HTTP request and applies necessary auth headers if set.
   278  func (client *Client) NewRequest(params map[string]string, method string, reqUrl url.URL, body io.Reader) *http.Request {
   279  	return client.NewRequestWitNotEncodedParams(params, nil, method, reqUrl, body)
   280  }
   281  
   282  // NewRequestWithApiVersion creates a new HTTP request and applies necessary auth headers if set.
   283  // Allows to override default request API Version
   284  func (client *Client) NewRequestWithApiVersion(params map[string]string, method string, reqUrl url.URL, body io.Reader, apiVersion string) *http.Request {
   285  	return client.NewRequestWitNotEncodedParamsWithApiVersion(params, nil, method, reqUrl, body, apiVersion)
   286  }
   287  
   288  // ParseErr takes an error XML resp, error interface for unmarshalling and returns a single string for
   289  // use in error messages.
   290  func ParseErr(bodyType types.BodyType, resp *http.Response, errType error) error {
   291  	// if there was an error decoding the body, just return that
   292  	if err := decodeBody(bodyType, resp, errType); err != nil {
   293  		util.Logger.Printf("[ParseErr]: unhandled response <--\n%+v\n-->\n", resp)
   294  		return fmt.Errorf("[ParseErr]: error parsing error body for non-200 request: %s (%+v)", err, resp)
   295  	}
   296  
   297  	// response body maybe empty for some error, such like 416, 400
   298  	if errType.Error() == "API Error: 0: " {
   299  		errType = fmt.Errorf(resp.Status)
   300  	}
   301  
   302  	return errType
   303  }
   304  
   305  // decodeBody is used to decode a response body of types.BodyType
   306  func decodeBody(bodyType types.BodyType, resp *http.Response, out interface{}) error {
   307  	body, err := io.ReadAll(resp.Body)
   308  
   309  	// In case of JSON, body does not have indents in response therefore it must be indented
   310  	if bodyType == types.BodyTypeJSON {
   311  		body, err = indentJsonBody(body)
   312  		if err != nil {
   313  			return err
   314  		}
   315  	}
   316  
   317  	util.ProcessResponseOutput(util.FuncNameCallStack(), resp, string(body))
   318  	if err != nil {
   319  		return err
   320  	}
   321  
   322  	debugShowResponse(resp, body)
   323  
   324  	// only attempt to unmarshal if body is not empty
   325  	if len(body) > 0 {
   326  		switch bodyType {
   327  		case types.BodyTypeXML:
   328  			if err = xml.Unmarshal(body, &out); err != nil {
   329  				return err
   330  			}
   331  		case types.BodyTypeJSON:
   332  			if err = json.Unmarshal(body, &out); err != nil {
   333  				return err
   334  			}
   335  
   336  		default:
   337  			panic(fmt.Sprintf("unknown body type: %d", bodyType))
   338  		}
   339  	}
   340  
   341  	return nil
   342  }
   343  
   344  // indentJsonBody indents raw JSON body for easier readability
   345  func indentJsonBody(body []byte) ([]byte, error) {
   346  	var prettyJSON bytes.Buffer
   347  	err := json.Indent(&prettyJSON, body, "", "  ")
   348  	if err != nil {
   349  		return nil, fmt.Errorf("error indenting response JSON: %s", err)
   350  	}
   351  	body = prettyJSON.Bytes()
   352  	return body, nil
   353  }
   354  
   355  // checkResp wraps http.Client.Do() and verifies the request, if status code
   356  // is 2XX it passes back the response, if it's a known invalid status code it
   357  // parses the resultant XML error and returns a descriptive error, if the
   358  // status code is not handled it returns a generic error with the status code.
   359  func checkResp(resp *http.Response, err error) (*http.Response, error) {
   360  	return checkRespWithErrType(types.BodyTypeXML, resp, err, &types.Error{})
   361  }
   362  
   363  // checkRespWithErrType allows to specify custom error errType for checkResp unmarshaling
   364  // the error.
   365  func checkRespWithErrType(bodyType types.BodyType, resp *http.Response, err, errType error) (*http.Response, error) {
   366  	if err != nil {
   367  		return resp, err
   368  	}
   369  
   370  	switch resp.StatusCode {
   371  	// Valid request, return the response.
   372  	case
   373  		http.StatusOK,        // 200
   374  		http.StatusCreated,   // 201
   375  		http.StatusAccepted,  // 202
   376  		http.StatusNoContent, // 204
   377  		http.StatusFound:     // 302
   378  		return resp, nil
   379  	// Invalid request, parse the XML error returned and return it.
   380  	case
   381  		http.StatusBadRequest,                   // 400
   382  		http.StatusUnauthorized,                 // 401
   383  		http.StatusForbidden,                    // 403
   384  		http.StatusNotFound,                     // 404
   385  		http.StatusMethodNotAllowed,             // 405
   386  		http.StatusNotAcceptable,                // 406
   387  		http.StatusProxyAuthRequired,            // 407
   388  		http.StatusRequestTimeout,               // 408
   389  		http.StatusConflict,                     // 409
   390  		http.StatusGone,                         // 410
   391  		http.StatusLengthRequired,               // 411
   392  		http.StatusPreconditionFailed,           // 412
   393  		http.StatusRequestEntityTooLarge,        // 413
   394  		http.StatusRequestURITooLong,            // 414
   395  		http.StatusUnsupportedMediaType,         // 415
   396  		http.StatusRequestedRangeNotSatisfiable, // 416
   397  		http.StatusLocked,                       // 423
   398  		http.StatusFailedDependency,             // 424
   399  		http.StatusUpgradeRequired,              // 426
   400  		http.StatusPreconditionRequired,         // 428
   401  		http.StatusTooManyRequests,              // 429
   402  		http.StatusRequestHeaderFieldsTooLarge,  // 431
   403  		http.StatusUnavailableForLegalReasons,   // 451
   404  		http.StatusInternalServerError,          // 500
   405  		http.StatusServiceUnavailable,           // 503
   406  		http.StatusGatewayTimeout:               // 504
   407  		return nil, ParseErr(bodyType, resp, errType)
   408  	// Unhandled response.
   409  	default:
   410  		return nil, fmt.Errorf("unhandled API response, please report this issue, status code: %s", resp.Status)
   411  	}
   412  }
   413  
   414  // ExecuteTaskRequest helper function creates request, runs it, checks response and parses task from response.
   415  // pathURL - request URL
   416  // requestType - HTTP method type
   417  // contentType - value to set for "Content-Type"
   418  // errorMessage - error message to return when error happens
   419  // payload - XML struct which will be marshalled and added as body/payload
   420  // E.g. client.ExecuteTaskRequest(updateDiskLink.HREF, http.MethodPut, updateDiskLink.Type, "error updating disk: %s", xmlPayload)
   421  func (client *Client) ExecuteTaskRequest(pathURL, requestType, contentType, errorMessage string, payload interface{}) (Task, error) {
   422  	return client.executeTaskRequest(pathURL, requestType, contentType, errorMessage, payload, client.APIVersion)
   423  }
   424  
   425  // ExecuteTaskRequestWithApiVersion helper function creates request, runs it, checks response and parses task from response.
   426  // pathURL - request URL
   427  // requestType - HTTP method type
   428  // contentType - value to set for "Content-Type"
   429  // errorMessage - error message to return when error happens
   430  // payload - XML struct which will be marshalled and added as body/payload
   431  // apiVersion - api version which will be used in request
   432  // E.g. client.ExecuteTaskRequest(updateDiskLink.HREF, http.MethodPut, updateDiskLink.Type, "error updating disk: %s", xmlPayload)
   433  func (client *Client) ExecuteTaskRequestWithApiVersion(pathURL, requestType, contentType, errorMessage string, payload interface{}, apiVersion string) (Task, error) {
   434  	return client.executeTaskRequest(pathURL, requestType, contentType, errorMessage, payload, apiVersion)
   435  }
   436  
   437  // Helper function creates request, runs it, checks response and parses task from response.
   438  // pathURL - request URL
   439  // requestType - HTTP method type
   440  // contentType - value to set for "Content-Type"
   441  // errorMessage - error message to return when error happens
   442  // payload - XML struct which will be marshalled and added as body/payload
   443  // apiVersion - api version which will be used in request
   444  // E.g. client.ExecuteTaskRequest(updateDiskLink.HREF, http.MethodPut, updateDiskLink.Type, "error updating disk: %s", xmlPayload)
   445  func (client *Client) executeTaskRequest(pathURL, requestType, contentType, errorMessage string, payload interface{}, apiVersion string) (Task, error) {
   446  
   447  	if !isMessageWithPlaceHolder(errorMessage) {
   448  		return Task{}, fmt.Errorf("error message has to include place holder for error")
   449  	}
   450  
   451  	resp, err := executeRequestWithApiVersion(pathURL, requestType, contentType, payload, client, apiVersion)
   452  	if err != nil {
   453  		return Task{}, fmt.Errorf(errorMessage, err)
   454  	}
   455  
   456  	task := NewTask(client)
   457  
   458  	if err = decodeBody(types.BodyTypeXML, resp, task.Task); err != nil {
   459  		return Task{}, fmt.Errorf("error decoding Task response: %s", err)
   460  	}
   461  
   462  	err = resp.Body.Close()
   463  	if err != nil {
   464  		return Task{}, fmt.Errorf(errorMessage, err)
   465  	}
   466  
   467  	// The request was successful
   468  	return *task, nil
   469  }
   470  
   471  // ExecuteRequestWithoutResponse helper function creates request, runs it, checks response and do not expect any values from it.
   472  // pathURL - request URL
   473  // requestType - HTTP method type
   474  // contentType - value to set for "Content-Type"
   475  // errorMessage - error message to return when error happens
   476  // payload - XML struct which will be marshalled and added as body/payload
   477  // E.g. client.ExecuteRequestWithoutResponse(catalogItemHREF.String(), http.MethodDelete, "", "error deleting Catalog item: %s", nil)
   478  func (client *Client) ExecuteRequestWithoutResponse(pathURL, requestType, contentType, errorMessage string, payload interface{}) error {
   479  	return client.executeRequestWithoutResponse(pathURL, requestType, contentType, errorMessage, payload, client.APIVersion)
   480  }
   481  
   482  // ExecuteRequestWithoutResponseWithApiVersion helper function creates request, runs it, checks response and do not expect any values from it.
   483  // pathURL - request URL
   484  // requestType - HTTP method type
   485  // contentType - value to set for "Content-Type"
   486  // errorMessage - error message to return when error happens
   487  // payload - XML struct which will be marshalled and added as body/payload
   488  // apiVersion - api version which will be used in request
   489  // E.g. client.ExecuteRequestWithoutResponse(catalogItemHREF.String(), http.MethodDelete, "", "error deleting Catalog item: %s", nil)
   490  func (client *Client) ExecuteRequestWithoutResponseWithApiVersion(pathURL, requestType, contentType, errorMessage string, payload interface{}, apiVersion string) error {
   491  	return client.executeRequestWithoutResponse(pathURL, requestType, contentType, errorMessage, payload, apiVersion)
   492  }
   493  
   494  // Helper function creates request, runs it, checks response and do not expect any values from it.
   495  // pathURL - request URL
   496  // requestType - HTTP method type
   497  // contentType - value to set for "Content-Type"
   498  // errorMessage - error message to return when error happens
   499  // payload - XML struct which will be marshalled and added as body/payload
   500  // apiVersion - api version which will be used in request
   501  // E.g. client.ExecuteRequestWithoutResponse(catalogItemHREF.String(), http.MethodDelete, "", "error deleting Catalog item: %s", nil)
   502  func (client *Client) executeRequestWithoutResponse(pathURL, requestType, contentType, errorMessage string, payload interface{}, apiVersion string) error {
   503  
   504  	if !isMessageWithPlaceHolder(errorMessage) {
   505  		return fmt.Errorf("error message has to include place holder for error")
   506  	}
   507  
   508  	resp, err := executeRequestWithApiVersion(pathURL, requestType, contentType, payload, client, apiVersion)
   509  	if err != nil {
   510  		return fmt.Errorf(errorMessage, err)
   511  	}
   512  
   513  	// log response explicitly because decodeBody() was not triggered
   514  	util.ProcessResponseOutput(util.FuncNameCallStack(), resp, fmt.Sprintf("%s", resp.Body))
   515  
   516  	debugShowResponse(resp, []byte("SKIPPED RESPONSE"))
   517  	err = resp.Body.Close()
   518  	if err != nil {
   519  		return fmt.Errorf("error closing response body: %s", err)
   520  	}
   521  
   522  	// The request was successful
   523  	return nil
   524  }
   525  
   526  // ExecuteRequest helper function creates request, runs it, check responses and parses out interface from response.
   527  // pathURL - request URL
   528  // requestType - HTTP method type
   529  // contentType - value to set for "Content-Type"
   530  // errorMessage - error message to return when error happens
   531  // payload - XML struct which will be marshalled and added as body/payload
   532  // out - structure to be used for unmarshalling xml
   533  // E.g. 	unmarshalledAdminOrg := &types.AdminOrg{}
   534  // client.ExecuteRequest(adminOrg.AdminOrg.HREF, http.MethodGet, "", "error refreshing organization: %s", nil, unmarshalledAdminOrg)
   535  func (client *Client) ExecuteRequest(pathURL, requestType, contentType, errorMessage string, payload, out interface{}) (*http.Response, error) {
   536  	return client.executeRequest(pathURL, requestType, contentType, errorMessage, payload, out, client.APIVersion)
   537  }
   538  
   539  // ExecuteRequestWithApiVersion helper function creates request, runs it, check responses and parses out interface from response.
   540  // pathURL - request URL
   541  // requestType - HTTP method type
   542  // contentType - value to set for "Content-Type"
   543  // errorMessage - error message to return when error happens
   544  // payload - XML struct which will be marshalled and added as body/payload
   545  // out - structure to be used for unmarshalling xml
   546  // apiVersion - api version which will be used in request
   547  // E.g. 	unmarshalledAdminOrg := &types.AdminOrg{}
   548  // client.ExecuteRequest(adminOrg.AdminOrg.HREF, http.MethodGet, "", "error refreshing organization: %s", nil, unmarshalledAdminOrg)
   549  func (client *Client) ExecuteRequestWithApiVersion(pathURL, requestType, contentType, errorMessage string, payload, out interface{}, apiVersion string) (*http.Response, error) {
   550  	return client.executeRequest(pathURL, requestType, contentType, errorMessage, payload, out, apiVersion)
   551  }
   552  
   553  // Helper function creates request, runs it, check responses and parses out interface from response.
   554  // pathURL - request URL
   555  // requestType - HTTP method type
   556  // contentType - value to set for "Content-Type"
   557  // errorMessage - error message to return when error happens
   558  // payload - XML struct which will be marshalled and added as body/payload
   559  // out - structure to be used for unmarshalling xml
   560  // apiVersion - api version which will be used in request
   561  // E.g. 	unmarshalledAdminOrg := &types.AdminOrg{}
   562  // client.ExecuteRequest(adminOrg.AdminOrg.HREF, http.MethodGet, "", "error refreshing organization: %s", nil, unmarshalledAdminOrg)
   563  func (client *Client) executeRequest(pathURL, requestType, contentType, errorMessage string, payload, out interface{}, apiVersion string) (*http.Response, error) {
   564  
   565  	if !isMessageWithPlaceHolder(errorMessage) {
   566  		return &http.Response{}, fmt.Errorf("error message has to include place holder for error")
   567  	}
   568  
   569  	resp, err := executeRequestWithApiVersion(pathURL, requestType, contentType, payload, client, apiVersion)
   570  	if err != nil {
   571  		return resp, fmt.Errorf(errorMessage, err)
   572  	}
   573  
   574  	if err = decodeBody(types.BodyTypeXML, resp, out); err != nil {
   575  		return resp, fmt.Errorf("error decoding response: %s", err)
   576  	}
   577  
   578  	err = resp.Body.Close()
   579  	if err != nil {
   580  		return resp, fmt.Errorf("error closing response body: %s", err)
   581  	}
   582  
   583  	// The request was successful
   584  	return resp, nil
   585  }
   586  
   587  // ExecuteRequestWithCustomError sends the request and checks for 2xx response. If the returned status code
   588  // was not as expected - the returned error will be unmarshalled to `errType` which implements Go's standard `error`
   589  // interface.
   590  func (client *Client) ExecuteRequestWithCustomError(pathURL, requestType, contentType, errorMessage string,
   591  	payload interface{}, errType error) (*http.Response, error) {
   592  	return client.ExecuteParamRequestWithCustomError(pathURL, map[string]string{}, requestType, contentType,
   593  		errorMessage, payload, errType)
   594  }
   595  
   596  // ExecuteParamRequestWithCustomError behaves exactly like ExecuteRequestWithCustomError but accepts
   597  // query parameter specification
   598  func (client *Client) ExecuteParamRequestWithCustomError(pathURL string, params map[string]string,
   599  	requestType, contentType, errorMessage string, payload interface{}, errType error) (*http.Response, error) {
   600  	if !isMessageWithPlaceHolder(errorMessage) {
   601  		return &http.Response{}, fmt.Errorf("error message has to include place holder for error")
   602  	}
   603  
   604  	resp, err := executeRequestCustomErr(pathURL, params, requestType, contentType, payload, client, errType, client.APIVersion)
   605  	if err != nil {
   606  		return &http.Response{}, fmt.Errorf(errorMessage, err)
   607  	}
   608  
   609  	// read from resp.Body io.Reader for debug output if it has body
   610  	var bodyBytes []byte
   611  	if resp.Body != nil {
   612  		bodyBytes, err = io.ReadAll(resp.Body)
   613  		if err != nil {
   614  			return &http.Response{}, fmt.Errorf("could not read response body: %s", err)
   615  		}
   616  		// Restore the io.ReadCloser to its original state with no-op closer
   617  		resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
   618  	}
   619  
   620  	util.ProcessResponseOutput(util.FuncNameCallStack(), resp, string(bodyBytes))
   621  	debugShowResponse(resp, bodyBytes)
   622  
   623  	return resp, nil
   624  }
   625  
   626  // executeRequest does executeRequestCustomErr and checks for vCD errors in API response
   627  func executeRequestWithApiVersion(pathURL, requestType, contentType string, payload interface{}, client *Client, apiVersion string) (*http.Response, error) {
   628  	return executeRequestCustomErr(pathURL, map[string]string{}, requestType, contentType, payload, client, &types.Error{}, apiVersion)
   629  }
   630  
   631  // executeRequestCustomErr performs request and unmarshals API error to errType if not 2xx status was returned
   632  func executeRequestCustomErr(pathURL string, params map[string]string, requestType, contentType string, payload interface{}, client *Client, errType error, apiVersion string) (*http.Response, error) {
   633  	requestURI, err := url.ParseRequestURI(pathURL)
   634  	if err != nil {
   635  		return nil, fmt.Errorf("couldn't parse path request URI '%s': %s", pathURL, err)
   636  	}
   637  
   638  	var req *http.Request
   639  	switch {
   640  	// Only send data (and xml.Header) if the payload is actually provided to avoid sending empty body with XML header
   641  	// (some Web Application Firewalls block requests when empty XML header is set but not body provided)
   642  	case payload != nil:
   643  		marshaledXml, err := xml.MarshalIndent(payload, "  ", "    ")
   644  		if err != nil {
   645  			return &http.Response{}, fmt.Errorf("error marshalling xml data %s", err)
   646  		}
   647  		body := bytes.NewBufferString(xml.Header + string(marshaledXml))
   648  
   649  		req = client.NewRequestWithApiVersion(params, requestType, *requestURI, body, apiVersion)
   650  
   651  	default:
   652  		req = client.NewRequestWithApiVersion(params, requestType, *requestURI, nil, apiVersion)
   653  	}
   654  
   655  	if contentType != "" {
   656  		req.Header.Add("Content-Type", contentType)
   657  	}
   658  
   659  	setHttpUserAgent(client.UserAgent, req)
   660  
   661  	resp, err := client.Http.Do(req)
   662  	if err != nil {
   663  		return resp, err
   664  	}
   665  
   666  	return checkRespWithErrType(types.BodyTypeXML, resp, err, errType)
   667  }
   668  
   669  // setHttpUserAgent adds User-Agent string to HTTP request. When supplied string is empty - header will not be set
   670  func setHttpUserAgent(userAgent string, req *http.Request) {
   671  	if userAgent != "" {
   672  		req.Header.Set("User-Agent", userAgent)
   673  	}
   674  }
   675  
   676  func isMessageWithPlaceHolder(message string) bool {
   677  	err := fmt.Errorf(message, "test error")
   678  	return !strings.Contains(err.Error(), "%!(EXTRA")
   679  }
   680  
   681  // combinedTaskErrorMessage is a general purpose function
   682  // that returns the contents of the operation error and, if found, the error
   683  // returned by the associated task
   684  func combinedTaskErrorMessage(task *types.Task, err error) string {
   685  	extendedError := err.Error()
   686  	if task.Error != nil {
   687  		extendedError = fmt.Sprintf("operation error: %s - task error: [%d - %s] %s",
   688  			err, task.Error.MajorErrorCode, task.Error.MinorErrorCode, task.Error.Message)
   689  	}
   690  	return extendedError
   691  }
   692  
   693  // addrOf is a generic function to return the address of a variable
   694  // Note. It is mainly meant for converting literal values to pointers (e.g. `addrOf(true)`)
   695  // and not getting the address of a variable (e.g. `addrOf(variable)`)
   696  func addrOf[T any](variable T) *T {
   697  	return &variable
   698  }
   699  
   700  // IsUuid returns true if the identifier is a bare UUID
   701  func IsUuid(identifier string) bool {
   702  	reUuid := regexp.MustCompile(`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`)
   703  	return reUuid.MatchString(identifier)
   704  }
   705  
   706  // isUrn validates if supplied identifier is of URN format (e.g. urn:vcloud:nsxtmanager:09722307-aee0-4623-af95-7f8e577c9ebc)
   707  // it checks for the following criteria:
   708  // 1. idenfifier is not empty
   709  // 2. identifier has 4 elements separated by ':'
   710  // 3. element 1 is 'urn' and element 4 is valid UUID
   711  func isUrn(identifier string) bool {
   712  	if identifier == "" {
   713  		return false
   714  	}
   715  
   716  	ss := strings.Split(identifier, ":")
   717  	if len(ss) != 4 {
   718  		return false
   719  	}
   720  
   721  	if ss[0] != "urn" && !IsUuid(ss[3]) {
   722  		return false
   723  	}
   724  
   725  	return true
   726  }
   727  
   728  // BuildUrnWithUuid helps to build valid URNs where APIs require URN format, but other API responds with UUID (or
   729  // extracted from HREF)
   730  func BuildUrnWithUuid(urnPrefix, uuid string) (string, error) {
   731  	if !IsUuid(uuid) {
   732  		return "", fmt.Errorf("supplied uuid '%s' is not valid UUID", uuid)
   733  	}
   734  
   735  	urn := urnPrefix + uuid
   736  	if !isUrn(urn) {
   737  		return "", fmt.Errorf("failed building valid URN '%s'", urn)
   738  	}
   739  
   740  	return urn, nil
   741  }
   742  
   743  // takeFloatAddress is a helper that returns the address of an `float64`
   744  func takeFloatAddress(x float64) *float64 {
   745  	return &x
   746  }
   747  
   748  // SetCustomHeader adds custom HTTP header values to a client
   749  func (client *Client) SetCustomHeader(values map[string]string) {
   750  	if len(client.customHeader) == 0 {
   751  		client.customHeader = make(http.Header)
   752  	}
   753  	for k, v := range values {
   754  		client.customHeader.Add(k, v)
   755  	}
   756  }
   757  
   758  // RemoveCustomHeader remove custom header values from the client
   759  func (client *Client) RemoveCustomHeader() {
   760  	if client.customHeader != nil {
   761  		client.customHeader = nil
   762  	}
   763  }
   764  
   765  // RemoveProvidedCustomHeaders removes custom header values from the client
   766  func (client *Client) RemoveProvidedCustomHeaders(values map[string]string) {
   767  	if client.customHeader != nil {
   768  		for k := range values {
   769  			client.customHeader.Del(k)
   770  		}
   771  	}
   772  }
   773  
   774  // Retrieves the administrator URL of a given HREF
   775  func getAdminURL(href string) string {
   776  	adminApi := "/api/admin/"
   777  	if strings.Contains(href, adminApi) {
   778  		return href
   779  	}
   780  	return strings.ReplaceAll(href, "/api/", adminApi)
   781  }
   782  
   783  // Retrieves the admin extension URL of a given HREF
   784  func getAdminExtensionURL(href string) string {
   785  	adminExtensionApi := "/api/admin/extension/"
   786  	if strings.Contains(href, adminExtensionApi) {
   787  		return href
   788  	}
   789  	return strings.ReplaceAll(getAdminURL(href), "/api/admin/", adminExtensionApi)
   790  }
   791  
   792  // TestConnection calls API to test a connection against a VCD, including SSL handshake and hostname verification.
   793  func (client *Client) TestConnection(testConnection types.TestConnection) (*types.TestConnectionResult, error) {
   794  	endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTestConnection
   795  
   796  	apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint)
   797  	if err != nil {
   798  		return nil, err
   799  	}
   800  
   801  	urlRef, err := client.OpenApiBuildEndpoint(endpoint)
   802  	if err != nil {
   803  		return nil, err
   804  	}
   805  
   806  	returnTestConnectionResult := &types.TestConnectionResult{
   807  		TargetProbe: &types.ProbeResult{},
   808  		ProxyProbe:  &types.ProbeResult{},
   809  	}
   810  
   811  	err = client.OpenApiPostItem(apiVersion, urlRef, nil, testConnection, returnTestConnectionResult, nil)
   812  	if err != nil {
   813  		return nil, fmt.Errorf("error performing test connection: %s", err)
   814  	}
   815  
   816  	return returnTestConnectionResult, nil
   817  }
   818  
   819  // TestConnectionWithDefaults calls TestConnection given a subscriptionURL. The rest of parameters are set as default.
   820  // It returns whether it could reach the server and establish SSL connection or not.
   821  func (client *Client) TestConnectionWithDefaults(subscriptionURL string) (bool, error) {
   822  	if subscriptionURL == "" {
   823  		return false, fmt.Errorf("TestConnectionWithDefaults needs to be passed a host. i.e. my-host.vmware.com")
   824  	}
   825  
   826  	url, err := url.Parse(subscriptionURL)
   827  	if err != nil {
   828  		return false, fmt.Errorf("unable to parse URL - %s", err)
   829  	}
   830  
   831  	// Get port
   832  	var port int
   833  	if v := url.Port(); v != "" {
   834  		port, err = strconv.Atoi(v)
   835  		if err != nil {
   836  			return false, fmt.Errorf("couldn't parse port provided - %s", err)
   837  		}
   838  	} else {
   839  		switch url.Scheme {
   840  		case "http":
   841  			port = 80
   842  		case "https":
   843  			port = 443
   844  		}
   845  	}
   846  
   847  	testConnectionConfig := types.TestConnection{
   848  		Host:    url.Hostname(),
   849  		Port:    port,
   850  		Secure:  addrOf(true), // Default value used by VCD UI
   851  		Timeout: 30,           // Default value used by VCD UI
   852  	}
   853  
   854  	testConnectionResult, err := client.TestConnection(testConnectionConfig)
   855  	if err != nil {
   856  		return false, err
   857  	}
   858  
   859  	if !testConnectionResult.TargetProbe.CanConnect {
   860  		return false, fmt.Errorf("the remote host is not reachable")
   861  	}
   862  
   863  	if !testConnectionResult.TargetProbe.SSLHandshake {
   864  		return true, fmt.Errorf("unsupported or unrecognized SSL message")
   865  	}
   866  
   867  	return true, nil
   868  }
   869  
   870  // buildUrl uses the Client base URL to create a customised URL
   871  func (client *Client) buildUrl(elements ...string) (string, error) {
   872  	baseUrl := client.VCDHREF.String()
   873  	if !IsValidUrl(baseUrl) {
   874  		return "", fmt.Errorf("incorrect URL %s", client.VCDHREF.String())
   875  	}
   876  	if strings.HasSuffix(baseUrl, "/") {
   877  		baseUrl = strings.TrimRight(baseUrl, "/")
   878  	}
   879  	if strings.HasSuffix(baseUrl, "/api") {
   880  		baseUrl = strings.TrimRight(baseUrl, "/api")
   881  	}
   882  	return url.JoinPath(baseUrl, elements...)
   883  }
   884  
   885  // ---------------------------------------------------------------------
   886  // The following functions are needed to avoid strict Coverity warnings
   887  // ---------------------------------------------------------------------
   888  
   889  // urlParseRequestURI returns a URL, discarding the error
   890  func urlParseRequestURI(href string) *url.URL {
   891  	apiEndpoint, err := url.ParseRequestURI(href)
   892  	if err != nil {
   893  		util.Logger.Printf("[DEBUG - urlParseRequestURI] error parsing request URI: %s", err)
   894  	}
   895  	return apiEndpoint
   896  }
   897  
   898  // safeClose closes a file and logs the error, if any. This can be used instead of file.Close()
   899  func safeClose(file *os.File) {
   900  	if err := file.Close(); err != nil {
   901  		util.Logger.Printf("Error closing file: %s\n", err)
   902  	}
   903  }
   904  
   905  // isSuccessStatus returns true if the given status code is between 200 and 300
   906  func isSuccessStatus(statusCode int) bool {
   907  	if statusCode >= http.StatusOK && // 200
   908  		statusCode < http.StatusMultipleChoices { // 300
   909  		return true
   910  	}
   911  	return false
   912  }