github.com/openshift-online/ocm-sdk-go@v0.1.473/connection.go (about) 1 /* 2 Copyright (c) 2018 Red Hat, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // This file contains the implementations of the Builder and Connection objects. 18 19 package sdk 20 21 import ( 22 "context" 23 "crypto/x509" 24 "fmt" 25 "net/http" 26 "net/url" 27 "regexp" 28 "sort" 29 "time" 30 31 "github.com/prometheus/client_golang/prometheus" 32 33 "github.com/openshift-online/ocm-sdk-go/accesstransparency" 34 "github.com/openshift-online/ocm-sdk-go/accountsmgmt" 35 "github.com/openshift-online/ocm-sdk-go/addonsmgmt" 36 "github.com/openshift-online/ocm-sdk-go/arohcp" 37 "github.com/openshift-online/ocm-sdk-go/authentication" 38 "github.com/openshift-online/ocm-sdk-go/authorizations" 39 "github.com/openshift-online/ocm-sdk-go/clustersmgmt" 40 "github.com/openshift-online/ocm-sdk-go/configuration" 41 "github.com/openshift-online/ocm-sdk-go/internal" 42 "github.com/openshift-online/ocm-sdk-go/jobqueue" 43 "github.com/openshift-online/ocm-sdk-go/logging" 44 "github.com/openshift-online/ocm-sdk-go/metrics" 45 "github.com/openshift-online/ocm-sdk-go/osdfleetmgmt" 46 "github.com/openshift-online/ocm-sdk-go/retry" 47 "github.com/openshift-online/ocm-sdk-go/servicelogs" 48 "github.com/openshift-online/ocm-sdk-go/servicemgmt" 49 "github.com/openshift-online/ocm-sdk-go/statusboard" 50 "github.com/openshift-online/ocm-sdk-go/webrca" 51 ) 52 53 // Default values: 54 const ( 55 // #nosec G101 56 DefaultTokenURL = authentication.DefaultTokenURL 57 DefaultClientID = authentication.DefaultClientID 58 DefaultClientSecret = authentication.DefaultClientSecret 59 DefaultURL = "https://api.openshift.com" 60 DefaultAgent = "OCM-SDK/" + Version 61 FedRAMPURL = "https://api.openshiftusgov.com" 62 ) 63 64 // DefaultScopes is the ser of scopes used by default: 65 var DefaultScopes = []string{ 66 "openid", 67 } 68 69 // ConnectionBuilder contains the configuration and logic needed to create connections to 70 // `api.openshift.com`. Don't create instances of this type directly, use the NewConnectionBuilder 71 // function instead. 72 type ConnectionBuilder struct { 73 // Basic attributes: 74 logger logging.Logger 75 trustedCAs []interface{} 76 insecure bool 77 disableKeepAlives bool 78 tokenURL string 79 clientID string 80 clientSecret string 81 urlTable map[string]string 82 agent string 83 user string 84 password string 85 tokens []string 86 scopes []string 87 retryLimit int 88 retryInterval time.Duration 89 retryJitter float64 90 transportWrappers []func(http.RoundTripper) http.RoundTripper 91 92 includeDefaultAuthnTransportWrapper bool 93 94 // Metrics: 95 metricsSubsystem string 96 metricsRegisterer prometheus.Registerer 97 98 // Error detected while populating the builder. Once set calls to methods to 99 // set other builder parameters will be ignored and the Build method will 100 // exit inmediately returning this error. 101 err error 102 } 103 104 // TransportWrapper is a wrapper for a transport of type http.RoundTripper. Creating a transport 105 // wrapper, enables to preform actions and manipulations on the transport request and response. 106 type TransportWrapper func(http.RoundTripper) http.RoundTripper 107 108 // Connection contains the data needed to connect to the `api.openshift.com`. Don't create instances 109 // of this type directly, use the builder instead. 110 type Connection struct { 111 // Basic attributes: 112 closed bool 113 logger logging.Logger 114 authnWrapper *authentication.TransportWrapper 115 retryWrapper *retry.TransportWrapper 116 clientSelector *internal.ClientSelector 117 urlTable []urlTableEntry 118 agent string 119 120 // Metrics: 121 metricsSubsystem string 122 metricsRegisterer prometheus.Registerer 123 } 124 125 // urlTableEntry is used to store one entry of the table that contains the correspondence between 126 // path prefixes and base URLs. 127 type urlTableEntry struct { 128 prefix string 129 re *regexp.Regexp 130 url *internal.ServerAddress 131 } 132 133 // NewConnectionBuilder creates an builder that knows how to create connections with the default 134 // configuration. 135 func NewConnectionBuilder() *ConnectionBuilder { 136 return &ConnectionBuilder{ 137 urlTable: map[string]string{ 138 "": DefaultURL, 139 }, 140 retryLimit: retry.DefaultLimit, 141 retryInterval: retry.DefaultInterval, 142 retryJitter: retry.DefaultJitter, 143 metricsRegisterer: prometheus.DefaultRegisterer, 144 includeDefaultAuthnTransportWrapper: true, 145 } 146 } 147 148 // NewConnectionBuilder creates a Builder that knows how to create connections 149 // without authentication 150 func NewUnauthenticatedConnectionBuilder() *ConnectionBuilder { 151 connectionBuilder := NewConnectionBuilder() 152 connectionBuilder.includeDefaultAuthnTransportWrapper = false 153 return connectionBuilder 154 } 155 156 // Logger sets the logger that will be used by the connection. By default it uses the Go `log` 157 // package, and with the debug level disabled and the rest enabled. If you need to change that you 158 // can create a logger and pass it to this method. For example: 159 // 160 // // Create a logger with the debug level enabled: 161 // logger, err := logging.NewGoLoggerBuilder(). 162 // Debug(true). 163 // Build() 164 // if err != nil { 165 // panic(err) 166 // } 167 // 168 // // Create the connection: 169 // cl, err := client.NewConnectionBuilder(). 170 // Logger(logger). 171 // Build() 172 // if err != nil { 173 // panic(err) 174 // } 175 // 176 // You can also build your own logger, implementing the Logger interface. 177 func (b *ConnectionBuilder) Logger(logger logging.Logger) *ConnectionBuilder { 178 if b.err != nil { 179 return b 180 } 181 b.logger = logger 182 return b 183 } 184 185 // TokenURL sets the URL that will be used to request OpenID access tokens. The default is 186 // `https://sso.redhat.com/auth/realms/cloud-services/protocol/openid-connect/token`. 187 func (b *ConnectionBuilder) TokenURL(url string) *ConnectionBuilder { 188 if b.err != nil { 189 return b 190 } 191 b.tokenURL = url 192 return b 193 } 194 195 // Client sets OpenID client identifier and secret that will be used to request OpenID tokens. The 196 // default identifier is `cloud-services`. The default secret is the empty string. When these two 197 // values are provided and no user name and password is provided, the connection will use the client 198 // credentials grant to obtain the token. For example, to create a connection using the client 199 // credentials grant do the following: 200 // 201 // // Use the client credentials grant: 202 // connection, err := sdk.NewConnectionBuilder(). 203 // Client("myclientid", "myclientsecret"). 204 // Build() 205 // 206 // Note that some OpenID providers (Keycloak, for example) require the client identifier also for 207 // the resource owner password grant. In that case use the set only the identifier, and let the 208 // secret blank. For example: 209 // 210 // // Use the resource owner password grant: 211 // connection, err := sdk.NewConnectionBuilder(). 212 // User("myuser", "mypassword"). 213 // Client("myclientid", ""). 214 // Build() 215 // 216 // Note the empty client secret. 217 func (b *ConnectionBuilder) Client(id string, secret string) *ConnectionBuilder { 218 if b.err != nil { 219 return b 220 } 221 b.clientID = id 222 b.clientSecret = secret 223 return b 224 } 225 226 // URL sets the base URL of the API gateway. The default is `https://api.openshift.com`. 227 // 228 // To connect using a Unix sockets and HTTP use the `unix` URL scheme and put the name of socket file 229 // in the URL path: 230 // 231 // connection, err := sdk.NewConnectionBuilder(). 232 // URL("unix://my.server.com/tmp/api.socket"). 233 // Build() 234 // 235 // To connect using Unix sockets and HTTPS use `unix+https://my.server.com/tmp/api.socket`. 236 // 237 // To force use of HTTP/2 without TLS use `h2c://...`. This can also be combined with Unix sockets, 238 // for example `unix+h2c://...`. 239 // 240 // Note that the host name is mandatory even when using Unix sockets because it is used to populate 241 // the `Host` header sent to the server. 242 func (b *ConnectionBuilder) URL(url string) *ConnectionBuilder { 243 if b.err != nil { 244 return b 245 } 246 return b.AlternativeURL("", url) 247 } 248 249 // AlternativeURL sets an alternative base URL for the given path prefix. For example, to configure 250 // the connection so that it sends the requests for the clusters management service to 251 // `https://my.server.com`: 252 // 253 // connection, err := client.NewConnectionBuilder(). 254 // URL("https://api.example.com"). 255 // AlternativeURL("/api/clusters_mgmt", "https://my.server.com"). 256 // Build() 257 // 258 // Requests for other paths that don't start with the given prefix will still be sent to the default 259 // base URL. 260 // 261 // This method can be called multiple times to set alternative URLs for multiple prefixes. 262 func (b *ConnectionBuilder) AlternativeURL(prefix, base string) *ConnectionBuilder { 263 if b.err != nil { 264 return b 265 } 266 b.urlTable[prefix] = base 267 return b 268 } 269 270 // AlternativeURLs sets an collection of alternative base URLs. For example, to configure the 271 // connection so that it sends the requests for the clusters management service to 272 // `https://my.server.com` and the requests for the accounts management service to 273 // `https://your.server.com`: 274 // 275 // connection, err := client.NewConnectionBuilder(). 276 // URL("https://api.example.com"). 277 // AlternativeURLs(map[string]string{ 278 // "/api/clusters_mgmt": "https://my.server.com", 279 // "/api/accounts_mgmt": "https://your.server.com", 280 // }). 281 // Build() 282 // 283 // The effect is the same as calling the AlternativeURL multiple times. 284 func (b *ConnectionBuilder) AlternativeURLs(entries map[string]string) *ConnectionBuilder { 285 if b.err != nil { 286 return b 287 } 288 for prefix, base := range entries { 289 b.urlTable[prefix] = base 290 } 291 return b 292 } 293 294 // Agent sets the `User-Agent` header that the client will use in all the HTTP requests. The default 295 // is `OCM` followed by an slash and the version of the client, for example `OCM/0.0.0`. 296 func (b *ConnectionBuilder) Agent(agent string) *ConnectionBuilder { 297 if b.err != nil { 298 return b 299 } 300 b.agent = agent 301 return b 302 } 303 304 // User sets the user name and password that will be used to request OpenID access tokens. When 305 // these two values are provided the connection will use the resource owner password grant type to 306 // obtain the token. For example: 307 // 308 // // Use the resource owner password grant: 309 // connection, err := sdk.NewConnectionBuilder(). 310 // User("myuser", "mypassword"). 311 // Build() 312 // 313 // Note that some OpenID providers (Keycloak, for example) require the client identifier also for 314 // the resource owner password grant. In that case use the set only the identifier, and let the 315 // secret blank. For example: 316 // 317 // // Use the resource owner password grant: 318 // connection, err := sdk.NewConnectionBuilder(). 319 // User("myuser", "mypassword"). 320 // Client("myclientid", ""). 321 // Build() 322 // 323 // Note the empty client secret. 324 func (b *ConnectionBuilder) User(name string, password string) *ConnectionBuilder { 325 if b.err != nil { 326 return b 327 } 328 b.user = name 329 b.password = password 330 return b 331 } 332 333 // Scopes sets the OpenID scopes that will be included in the token request. The default is to use 334 // the `openid` scope. If this method is used then that default will be completely replaced, so you 335 // will need to specify it explicitly if you want to use it. For example, if you want to add the 336 // scope 'myscope' without loosing the default you will have to do something like this: 337 // 338 // // Create a connection with the default 'openid' scope and some additional scopes: 339 // connection, err := sdk.NewConnectionBuilder(). 340 // User("myuser", "mypassword"). 341 // Scopes("openid", "myscope", "yourscope"). 342 // Build() 343 // 344 // If you just want to use the default 'openid' then there is no need to use this method. 345 func (b *ConnectionBuilder) Scopes(values ...string) *ConnectionBuilder { 346 if b.err != nil { 347 return b 348 } 349 b.scopes = make([]string, len(values)) 350 copy(b.scopes, values) 351 return b 352 } 353 354 // Tokens sets the OpenID tokens that will be used to authenticate. Multiple types of tokens are 355 // accepted, and used according to their type. For example, you can pass a single access token, or 356 // an access token and a refresh token, or just a refresh token. If no token is provided then the 357 // connection will the user name and password or the client identifier and client secret (see the 358 // User and Client methods) to request new ones. 359 // 360 // If the connection is created with these tokens and no user or client credentials, it will 361 // stop working when both tokens expire. That can happen, for example, if the connection isn't used 362 // for a period of time longer than the life of the refresh token. 363 func (b *ConnectionBuilder) Tokens(tokens ...string) *ConnectionBuilder { 364 if b.err != nil { 365 return b 366 } 367 b.tokens = append(b.tokens, tokens...) 368 return b 369 } 370 371 // TrustedCAs sets the certificate pool that contains the certificate authorities that will be 372 // trusted by the connection. If this isn't explicitly specified then the client will trust the 373 // certificate authorities trusted by default by the system. 374 func (b *ConnectionBuilder) TrustedCAs(value *x509.CertPool) *ConnectionBuilder { 375 if b.err != nil { 376 return b 377 } 378 b.trustedCAs = append(b.trustedCAs, value) 379 return b 380 } 381 382 // TrustedCAFile sets the name of a file that contains the certificate authorities that will be 383 // trusted by the connection. If this isn't explicitly specified then the client will trust the 384 // certificate authorities trusted by default by the system. 385 func (b *ConnectionBuilder) TrustedCAFile(value string) *ConnectionBuilder { 386 if b.err != nil { 387 return b 388 } 389 b.trustedCAs = append(b.trustedCAs, value) 390 return b 391 } 392 393 // Insecure enables insecure communication with the server. This disables verification of TLS 394 // certificates and host names and it isn't recommended for a production environment. 395 func (b *ConnectionBuilder) Insecure(flag bool) *ConnectionBuilder { 396 if b.err != nil { 397 return b 398 } 399 b.insecure = flag 400 return b 401 } 402 403 // DisableKeepAlives disables HTTP keep-alives with the server. This is unrelated to similarly 404 // named TCP keep-alives. 405 func (b *ConnectionBuilder) DisableKeepAlives(flag bool) *ConnectionBuilder { 406 if b.err != nil { 407 return b 408 } 409 b.disableKeepAlives = flag 410 return b 411 } 412 413 // RetryLimit sets the maximum number of retries for a request. When this is zero no retries will be 414 // performed. The default value is two. 415 func (b *ConnectionBuilder) RetryLimit(value int) *ConnectionBuilder { 416 if b.err != nil { 417 return b 418 } 419 b.retryLimit = value 420 return b 421 } 422 423 // RetryInterval sets the time to wait before the first retry. The interval time will be doubled for 424 // each retry. For example, if this is set to one second then the first retry will happen 425 // approximately one second after the failure of the initial request, the second retry will happen 426 // affer four seconds, the third will happen after eitght seconds, so on. 427 func (b *ConnectionBuilder) RetryInterval(value time.Duration) *ConnectionBuilder { 428 if b.err != nil { 429 return b 430 } 431 b.retryInterval = value 432 return b 433 } 434 435 // RetryJitter sets a factor that will be used to randomize the retry intervals. For example, if 436 // this is set to 0.1 then a random adjustment between -10% and +10% will be done to the interval 437 // for each retry. This is intended to reduce simultaneous retries by clients when a server starts 438 // failing. The default value is 0.2. 439 func (b *ConnectionBuilder) RetryJitter(value float64) *ConnectionBuilder { 440 if b.err != nil { 441 return b 442 } 443 b.retryJitter = value 444 return b 445 } 446 447 // TransportWrapper allows setting a transport layer into the connection for capturing and 448 // manipulating the request or response. 449 func (b *ConnectionBuilder) TransportWrapper(value TransportWrapper) *ConnectionBuilder { 450 if b.err != nil { 451 return b 452 } 453 b.transportWrappers = append(b.transportWrappers, value) 454 return b 455 } 456 457 // MetricsSubsystem sets the name of the subsystem that will be used by the connection to register 458 // metrics with Prometheus. If this isn't explicitly specified, or if it is an empty string, then no 459 // metrics will be registered. For example, if the value is `api_outbound` then the following 460 // metrics will be registered: 461 // 462 // api_outbound_request_count - Number of API requests sent. 463 // api_outbound_request_duration_sum - Total time to send API requests, in seconds. 464 // api_outbound_request_duration_count - Total number of API requests measured. 465 // api_outbound_request_duration_bucket - Number of API requests organized in buckets. 466 // api_outbound_token_request_count - Number of token requests sent. 467 // api_outbound_token_request_duration_sum - Total time to send token requests, in seconds. 468 // api_outbound_token_request_duration_count - Total number of token requests measured. 469 // api_outbound_token_request_duration_bucket - Number of token requests organized in buckets. 470 // 471 // The duration buckets metrics contain an `le` label that indicates the upper bound. For example if 472 // the `le` label is `1` then the value will be the number of requests that were processed in less 473 // than one second. 474 // 475 // The API request metrics have the following labels: 476 // 477 // method - Name of the HTTP method, for example GET or POST. 478 // path - Request path, for example /api/clusters_mgmt/v1/clusters. 479 // code - HTTP response code, for example 200 or 500. 480 // 481 // To calculate the average request duration during the last 10 minutes, for example, use a 482 // Prometheus expression like this: 483 // 484 // rate(api_outbound_request_duration_sum[10m]) / rate(api_outbound_request_duration_count[10m]) 485 // 486 // In order to reduce the cardinality of the metrics the path label is modified to remove the 487 // identifiers of the objects. For example, if the original path is .../clusters/123 then it will 488 // be replaced by .../clusters/-, and the values will be accumulated. The line returned by the 489 // metrics server will be like this: 490 // 491 // api_outbound_request_count{code="200",method="GET",path="/api/clusters_mgmt/v1/clusters/-"} 56 492 // 493 // The meaning of that is that there were a total of 56 requests to get specific clusters, 494 // independently of the specific identifier of the cluster. 495 // 496 // The token request metrics will contain the following labels: 497 // 498 // code - HTTP response code, for example 200 or 500. 499 // 500 // The value of the `code` label will be zero when sending the request failed without a response 501 // code, for example if it wasn't possible to open the connection, or if there was a timeout waiting 502 // for the response. 503 // 504 // Note that setting this attribute is not enough to have metrics published, you also need to 505 // create and start a metrics server, as described in the documentation of the Prometheus library. 506 func (b *ConnectionBuilder) MetricsSubsystem(value string) *ConnectionBuilder { 507 if b.err != nil { 508 return b 509 } 510 b.metricsSubsystem = value 511 return b 512 } 513 514 // MetricsRegisterer sets the Prometheus registerer that will be used to register the metrics. The 515 // default is to use the default Prometheus registerer and there is usually no need to change that. 516 // This is intended for unit tests, where it is convenient to have a registerer that doesn't 517 // interfere with the rest of the system. 518 func (b *ConnectionBuilder) MetricsRegisterer(value prometheus.Registerer) *ConnectionBuilder { 519 if b.err != nil { 520 return b 521 } 522 if value == nil { 523 value = prometheus.DefaultRegisterer 524 } 525 b.metricsRegisterer = value 526 return b 527 } 528 529 // Metrics sets the name of the subsystem that will be used by the connection to register metrics 530 // with Prometheus. 531 // 532 // Deprecated: has been replaced by MetricsSubsystem. 533 func (b *ConnectionBuilder) Metrics(value string) *ConnectionBuilder { 534 return b.MetricsSubsystem(value) 535 } 536 537 // Load loads the connection configuration from the given source. The source must be a YAML 538 // document with content similar to this: 539 // 540 // url: https://my.server.com 541 // alternative_urls: 542 // - /api/clusters_mgmt: https://your.server.com 543 // - /api/accounts_mgmt: https://her.server.com 544 // token_url: https://openid.server.com 545 // user: myuser 546 // password: mypassword 547 // client_id: myclient 548 // client_secret: mysecret 549 // tokens: 550 // - eY... 551 // - eY... 552 // scopes: 553 // - openid 554 // insecure: false 555 // trusted_cas: 556 // - /my/ca.pem 557 // - /your/ca.pem 558 // agent: myagent 559 // retry: true 560 // retry_limit: 1 561 // 562 // Setting any of these fields in the file has the same effect that calling the corresponding method 563 // of the builder. 564 // 565 // For details of the supported syntax see the documentation of the configuration package. 566 func (b *ConnectionBuilder) Load(source interface{}) *ConnectionBuilder { 567 if b.err != nil { 568 return b 569 } 570 571 // Load the configuration: 572 var config *configuration.Object 573 config, b.err = configuration.New(). 574 Load(source). 575 Build() 576 if b.err != nil { 577 return b 578 } 579 var view struct { 580 URL *string `yaml:"url"` 581 AlternativeURLs map[string]string `yaml:"alternative_urls"` 582 TokenURL *string `yaml:"token_url"` 583 User *string `yaml:"user"` 584 Password *string `yaml:"password"` 585 ClientID *string `yaml:"client_id"` 586 ClientSecret *string `yaml:"client_secret"` 587 Tokens []string `yaml:"tokens"` 588 Insecure *bool `yaml:"insecure"` 589 TrustedCAs []string `yaml:"trusted_cas"` 590 Scopes []string `yaml:"scopes"` 591 Agent *string `yaml:"agent"` 592 Retry *bool `yaml:"retry"` 593 RetryLimit *int `yaml:"retry_limit"` 594 MetricsSubsystem *string `yaml:"metrics_subsystem"` 595 } 596 b.err = config.Populate(&view) 597 if b.err != nil { 598 return b 599 } 600 601 // URL: 602 if view.URL != nil { 603 b.URL(*view.URL) 604 } 605 if view.TokenURL != nil { 606 b.TokenURL(*view.TokenURL) 607 } 608 609 // Alternative URLs: 610 if view.AlternativeURLs != nil { 611 for prefix, base := range view.AlternativeURLs { 612 b.AlternativeURL(prefix, base) 613 } 614 } 615 616 // User and password: 617 var user string 618 var password string 619 if view.User != nil { 620 user = *view.User 621 } 622 if view.Password != nil { 623 password = *view.Password 624 } 625 if user != "" || password != "" { 626 b.User(user, password) 627 } 628 629 // Client identifier and secret: 630 var clientID string 631 var clientSecret string 632 if view.ClientID != nil { 633 clientID = *view.ClientID 634 } 635 if view.ClientSecret != nil { 636 clientSecret = *view.ClientSecret 637 } 638 if clientID != "" || clientSecret != "" { 639 b.Client(clientID, clientSecret) 640 } 641 642 // Tokens: 643 if view.Tokens != nil { 644 b.Tokens(view.Tokens...) 645 } 646 647 // Scopes: 648 if view.Scopes != nil { 649 b.Scopes(view.Scopes...) 650 } 651 652 // Insecure: 653 if view.Insecure != nil { 654 b.Insecure(*view.Insecure) 655 } 656 657 // Trusted CAs: 658 for _, trustedCA := range view.TrustedCAs { 659 b.TrustedCAFile(trustedCA) 660 } 661 662 // Agent: 663 if view.Agent != nil { 664 b.Agent(*view.Agent) 665 } 666 667 // Retry: 668 if view.RetryLimit != nil { 669 b.RetryLimit(*view.RetryLimit) 670 } 671 672 // Metrics subsystem: 673 if view.MetricsSubsystem != nil { 674 b.MetricsSubsystem(*view.MetricsSubsystem) 675 } 676 677 return b 678 } 679 680 // Build uses the configuration stored in the builder to create a new connection. The builder can be 681 // reused to create multiple connections with the same configuration. It returns a pointer to the 682 // connection, and an error if something fails when trying to create it. 683 // 684 // This operation is potentially lengthy, as it may require network communications. Consider using a 685 // context and the BuildContext method. 686 func (b *ConnectionBuilder) Build() (connection *Connection, err error) { 687 return b.BuildContext(context.Background()) 688 } 689 690 // BuildContext uses the configuration stored in the builder to create a new connection. The builder 691 // can be reused to create multiple connections with the same configuration. It returns a pointer to 692 // the connection, and an error if something fails when trying to create it. 693 func (b *ConnectionBuilder) BuildContext(ctx context.Context) (connection *Connection, err error) { 694 // If an error has been detected while populating the builder then return it and finish: 695 if b.err != nil { 696 err = b.err 697 return 698 } 699 700 // Create the default logger, if needed: 701 if b.logger == nil { 702 b.logger, err = logging.NewGoLoggerBuilder(). 703 Debug(false). 704 Info(true). 705 Warn(true). 706 Error(true). 707 Build() 708 if err != nil { 709 err = fmt.Errorf("can't create default logger: %w", err) 710 return 711 } 712 b.logger.Debug(ctx, "Logger wasn't provided, will use Go log") 713 } 714 715 // Create the URL table: 716 urlTable, err := b.createURLTable(ctx) 717 if err != nil { 718 return 719 } 720 721 // Set the default agent, if needed: 722 agent := b.agent 723 if b.agent == "" { 724 agent = DefaultAgent 725 } 726 727 // Create the metrics wrapper: 728 var metricsWrapper func(http.RoundTripper) http.RoundTripper 729 if b.metricsSubsystem != "" { 730 var parsed *url.URL 731 parsed, err = url.Parse(b.tokenURL) 732 if err != nil { 733 return 734 } 735 var wrapper *metrics.TransportWrapper 736 wrapper, err = metrics.NewTransportWrapper(). 737 Path(parsed.Path). 738 Subsystem(b.metricsSubsystem). 739 Registerer(b.metricsRegisterer). 740 Build() 741 if err != nil { 742 return 743 } 744 metricsWrapper = wrapper.Wrap 745 } 746 747 // Create the logging wrapper: 748 var loggingWrapper func(http.RoundTripper) http.RoundTripper 749 if b.logger.DebugEnabled() { 750 wrapper := &dumpTransportWrapper{ 751 logger: b.logger, 752 } 753 loggingWrapper = wrapper.Wrap 754 } 755 756 // Initialize the client selector builder: 757 clientSelectorBuilder := internal.NewClientSelector(). 758 Logger(b.logger). 759 TrustedCAs(b.trustedCAs...). 760 Insecure(b.insecure) 761 762 var authnWrapper *authentication.TransportWrapper 763 if b.includeDefaultAuthnTransportWrapper { 764 // Create the authentication wrapper: 765 authnWrapper, err = authentication.NewTransportWrapper(). 766 Logger(b.logger). 767 TokenURL(b.tokenURL). 768 User(b.user, b.password). 769 Client(b.clientID, b.clientSecret). 770 Tokens(b.tokens...). 771 Scopes(b.scopes...). 772 TrustedCAs(b.trustedCAs...). 773 Insecure(b.insecure). 774 TransportWrapper(metricsWrapper). 775 TransportWrapper(loggingWrapper). 776 TransportWrappers(b.transportWrappers...). 777 MetricsSubsystem(b.metricsSubsystem). 778 MetricsRegisterer(b.metricsRegisterer). 779 Build(ctx) 780 if err != nil { 781 return 782 } 783 clientSelectorBuilder.TransportWrapper(authnWrapper.Wrap) 784 } 785 786 // Create the retry wrapper: 787 retryWrapper, err := retry.NewTransportWrapper(). 788 Logger(b.logger). 789 Limit(b.retryLimit). 790 Interval(b.retryInterval). 791 Jitter(b.retryJitter). 792 Build(ctx) 793 if err != nil { 794 return 795 } 796 797 // Create the client selector: 798 clientSelector, err := clientSelectorBuilder. 799 TransportWrapper(metricsWrapper). 800 TransportWrapper(retryWrapper.Wrap). 801 TransportWrapper(loggingWrapper). 802 TransportWrappers(b.transportWrappers...). 803 Build(ctx) 804 if err != nil { 805 return 806 } 807 808 // Allocate and populate the connection object: 809 connection = &Connection{ 810 logger: b.logger, 811 authnWrapper: authnWrapper, 812 retryWrapper: retryWrapper, 813 clientSelector: clientSelector, 814 urlTable: urlTable, 815 agent: agent, 816 metricsSubsystem: b.metricsSubsystem, 817 metricsRegisterer: b.metricsRegisterer, 818 } 819 820 return 821 } 822 823 func (b *ConnectionBuilder) createURLTable(ctx context.Context) (table []urlTableEntry, err error) { 824 // Check that all the prefixes are acceptable: 825 for prefix, base := range b.urlTable { 826 if !validPrefixRE.MatchString(prefix) { 827 err = fmt.Errorf( 828 "prefix '%s' for URL '%s' isn't valid; it must start with a "+ 829 "slash and be composed of slash separated segments "+ 830 "containing only digits, letters, dashes and undercores", 831 prefix, base, 832 ) 833 return 834 } 835 } 836 837 // Allocate space for the table: 838 table = make([]urlTableEntry, len(b.urlTable)) 839 840 // For each alternative URL create the regular expression that will be used to check if 841 // paths match it, and parse the base URL: 842 i := 0 843 for prefix, base := range b.urlTable { 844 entry := &table[i] 845 entry.prefix = prefix 846 pattern := fmt.Sprintf("^%s(/.*)?$", regexp.QuoteMeta(prefix)) 847 entry.re, err = regexp.Compile(pattern) 848 if err != nil { 849 err = fmt.Errorf( 850 "can't compile regular expression '%s' for URL with "+ 851 "prefix '%s' and URL '%s': %v", 852 pattern, prefix, base, err, 853 ) 854 return 855 } 856 entry.url, err = internal.ParseServerAddress(ctx, base) 857 if err != nil { 858 err = fmt.Errorf( 859 "can't parse URL '%s' for prefix '%s': %w", 860 base, prefix, err, 861 ) 862 return 863 } 864 i++ 865 } 866 867 // Sort the entries in descending order of the length of the prefix, so that later 868 // when matching it will be easier to select the longest prefix that matches: 869 sort.Slice(table, func(i, j int) bool { 870 lenI := len(table[i].prefix) 871 lenJ := len(table[j].prefix) 872 return lenI > lenJ 873 }) 874 875 // Write to the log the resulting table: 876 if b.logger.DebugEnabled() { 877 for _, entry := range table { 878 b.logger.Debug( 879 ctx, 880 "Added URL with prefix '%s', regular expression "+ 881 "'%s' and URL '%s'", 882 entry.prefix, entry.re, entry.url.Text, 883 ) 884 } 885 } 886 887 return 888 } 889 890 // Logger returns the logger that is used by the connection. 891 func (c *Connection) Logger() logging.Logger { 892 return c.logger 893 } 894 895 // TokenURL returns the URL that the connection is using request OpenID access tokens. 896 // An empty string is returned if the connection does not use authentication. 897 func (c *Connection) TokenURL() string { 898 if c.authnWrapper == nil { 899 return "" 900 } 901 return c.authnWrapper.TokenURL() 902 } 903 904 // Client returns OpenID client identifier and secret that the connection is using to request OpenID 905 // access tokens. 906 // Empty strings are returned if the connection does not use authentication. 907 func (c *Connection) Client() (id, secret string) { 908 if c.authnWrapper != nil { 909 id, secret = c.authnWrapper.Client() 910 } 911 return 912 } 913 914 // User returns the user name and password that the is using to request OpenID access tokens. 915 // Empty strings are returned if the connection does not use authentication. 916 func (c *Connection) User() (user, password string) { 917 if c.authnWrapper != nil { 918 user, password = c.authnWrapper.User() 919 } 920 return 921 } 922 923 // Scopes returns the OpenID scopes that the connection is using to request OpenID access tokens. 924 // An empty slice is returned if the connection does not use authentication. 925 func (c *Connection) Scopes() []string { 926 if c.authnWrapper == nil { 927 return []string{} 928 } 929 return c.authnWrapper.Scopes() 930 } 931 932 // URL returns the base URL of the API gateway. 933 func (c *Connection) URL() string { 934 // The base URL will most likely be the last in the URL table because it is sorted in 935 // descending order of the prefix length, so it is faster to traverse the table in 936 // reverse order. 937 for i := len(c.urlTable) - 1; i >= 0; i-- { 938 entry := &c.urlTable[i] 939 if entry.prefix == "" { 940 return entry.url.Text 941 } 942 } 943 return "" 944 } 945 946 // Agent returns the `User-Agent` header that the client is using for all HTTP requests. 947 func (c *Connection) Agent() string { 948 return c.agent 949 } 950 951 // TrustedCAs sets returns the certificate pool that contains the certificate authorities that are 952 // trusted by the connection. 953 func (c *Connection) TrustedCAs() *x509.CertPool { 954 return c.clientSelector.TrustedCAs() 955 } 956 957 // Insecure returns the flag that indicates if insecure communication with the server is enabled. 958 func (c *Connection) Insecure() bool { 959 return c.clientSelector.Insecure() 960 } 961 962 // DisableKeepAlives returns the flag that indicates if HTTP keep alive is disabled. 963 func (c *Connection) DisableKeepAlives() bool { 964 return c.clientSelector.DisableKeepAlives() 965 } 966 967 // RetryLimit gets the maximum number of retries for a request. 968 func (c *Connection) RetryLimit() int { 969 return c.retryWrapper.Limit() 970 } 971 972 // RetryInteval returns the initial retry interval. 973 func (c *Connection) RetryInterval() time.Duration { 974 return c.retryWrapper.Interval() 975 } 976 977 // RetryJitter returns the retry interval jitter factor. 978 func (c *Connection) RetryJitter() float64 { 979 return c.retryWrapper.Jitter() 980 } 981 982 // MetricsSubsystem returns the name of the subsystem that is used by the connection to register 983 // metrics with Prometheus. An empty string means that no metrics are registered. 984 func (c *Connection) MetricsSubsystem() string { 985 return c.metricsSubsystem 986 } 987 988 // AlternativeURLs returns the alternative URLs in use by the connection. Note that the map returned 989 // is a copy of the data used internally, so changing it will have no effect on the connection. 990 func (c *Connection) AlternativeURLs() map[string]string { 991 // Copy all the entries of the URL table except the one corresponding to the empty prefix, as 992 // that isn't usually set via the alternative URLs mechanism: 993 result := map[string]string{} 994 for _, entry := range c.urlTable { 995 if entry.prefix != "" { 996 result[entry.prefix] = entry.url.Text 997 } 998 } 999 return result 1000 } 1001 1002 // AccessTransparency returns the client for the access transparency service. 1003 func (c *Connection) AccessTransparency() *accesstransparency.Client { 1004 return accesstransparency.NewClient(c, "/api/access_transparency") 1005 } 1006 1007 // AccountsMgmt returns the client for the accounts management service. 1008 func (c *Connection) AccountsMgmt() *accountsmgmt.Client { 1009 return accountsmgmt.NewClient(c, "/api/accounts_mgmt") 1010 } 1011 1012 // AccountsMgmt returns the client for the accounts management service. 1013 func (c *Connection) AddonsMgmt() *addonsmgmt.Client { 1014 return addonsmgmt.NewClient(c, "/api/addons_mgmt") 1015 } 1016 1017 // ClustersMgmt returns the client for the clusters management service. 1018 func (c *Connection) ClustersMgmt() *clustersmgmt.Client { 1019 return clustersmgmt.NewClient(c, "/api/clusters_mgmt") 1020 } 1021 1022 // AroHCP returns the client for the ARO-HCP clusters management service. 1023 func (c *Connection) AroHCP() *arohcp.Client { 1024 return arohcp.NewClient(c, "/api/aro_hcp") 1025 } 1026 1027 // OSDFleetMgmt returns the client for the OSD management service. 1028 func (c *Connection) OSDFleetMgmt() *osdfleetmgmt.Client { 1029 1030 return osdfleetmgmt.NewClient(c, "/api/osd_fleet_mgmt") 1031 } 1032 1033 // Authorizations returns the client for the authorizations service. 1034 func (c *Connection) Authorizations() *authorizations.Client { 1035 return authorizations.NewClient(c, "/api/authorizations") 1036 } 1037 1038 // ServiceLogs returns the client for the logs service. 1039 func (c *Connection) ServiceLogs() *servicelogs.Client { 1040 return servicelogs.NewClient(c, "/api/service_logs") 1041 } 1042 1043 // JobQueue returns the client for the Job Queues service. 1044 func (c *Connection) JobQueue() *jobqueue.Client { 1045 return jobqueue.NewClient(c, "/api/job_queue") 1046 } 1047 1048 // Status board returns the client for the status board service. 1049 func (c *Connection) StatusBoard() *statusboard.Client { 1050 return statusboard.NewClient(c, "/api/status-board") 1051 } 1052 1053 // ServiceMgmt returns the client for the service management service. 1054 func (c *Connection) ServiceMgmt() *servicemgmt.Client { 1055 return servicemgmt.NewClient(c, "/api/service_mgmt") 1056 } 1057 1058 // WebRCA returns the client for the web RCA service. 1059 func (c *Connection) WebRCA() *webrca.Client { 1060 return webrca.NewClient(c, "/api/web-rca") 1061 } 1062 1063 // Close releases all the resources used by the connection. It is very important to always close it 1064 // once it is no longer needed, as otherwise those resources may be leaked. Trying to use a 1065 // connection that has been closed will result in a error. 1066 func (c *Connection) Close() error { 1067 var err error 1068 1069 // in case the connection is already closed, return instead of printing an error message 1070 if c.closed { 1071 return nil 1072 } 1073 1074 // Close the HTTP clients: 1075 err = c.clientSelector.Close() 1076 if err != nil { 1077 return err 1078 } 1079 1080 // If the default authentication wrapper is set close it 1081 if c.authnWrapper != nil { 1082 // Close the authentication wrapper: 1083 err = c.authnWrapper.Close() 1084 if err != nil { 1085 return err 1086 } 1087 } 1088 1089 // Mark the connection as closed, so that further attempts to use it will fail: 1090 c.closed = true 1091 return nil 1092 } 1093 1094 func (c *Connection) checkClosed() error { 1095 if c.closed { 1096 return fmt.Errorf("connection is closed") 1097 } 1098 return nil 1099 } 1100 1101 // validPrefixRE is the regular expression used to check patch prefixes. 1102 var validPrefixRE = regexp.MustCompile(`^((/\w+)*)?$`)