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 }