github.com/maxgio92/test-infra@v0.1.0/kubetest/boskos/client/client.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 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 package client 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "io" 26 "net" 27 "net/http" 28 "net/url" 29 "os" 30 "strings" 31 "sync" 32 "syscall" 33 "time" 34 35 "github.com/google/uuid" 36 "github.com/hashicorp/go-multierror" 37 "github.com/sirupsen/logrus" 38 utilerrors "k8s.io/apimachinery/pkg/util/errors" 39 40 "github.com/maxgio92/test-infra/kubetest/boskos/common" 41 "github.com/maxgio92/test-infra/kubetest/boskos/storage" 42 "github.com/maxgio92/test-infra/prow/config/secret" 43 ) 44 45 var ( 46 // ErrNotFound is returned by Acquire() when no resources are available. 47 ErrNotFound = errors.New("resources not found") 48 // ErrAlreadyInUse is returned by Acquire when resources are already being requested. 49 ErrAlreadyInUse = errors.New("resources already used by another user") 50 // ErrContextRequired is returned by AcquireWait and AcquireByStateWait when 51 // they are invoked with a nil context. 52 ErrContextRequired = errors.New("context required") 53 // ErrTypeNotFound is returned when the requested resource type (rtype) does not exist. 54 // For this error to be returned, you must set DistinguishNotFoundVsTypeNotFound to true. 55 ErrTypeNotFound = errors.New("resource type not found") 56 ) 57 58 // Client defines the public Boskos client object 59 type Client struct { 60 // Dialer is the net.Dialer used to establish connections to the remote 61 // boskos endpoint. 62 Dialer DialerWithRetry 63 // DistinguishNotFoundVsTypeNotFound, if set, will make it possible to distinguish between 64 // ErrNotFound and ErrTypeNotFound. For backwards-compatibility, this flag is off by 65 // default. 66 DistinguishNotFoundVsTypeNotFound bool 67 68 // http is the http.Client used to interact with the boskos REST API 69 http http.Client 70 71 owner string 72 url string 73 username string 74 getPassword func() []byte 75 lock sync.Mutex 76 77 storage storage.PersistenceLayer 78 } 79 80 // NewClient creates a Boskos client for the specified URL and resource owner. 81 // 82 // Clients created with this function default to retrying failed connection 83 // attempts three times with a ten second pause between each attempt. 84 func NewClient(owner string, urlString, username, passwordFile string) (*Client, error) { 85 86 if (username == "") != (passwordFile == "") { 87 return nil, fmt.Errorf("username and passwordFile must be specified together") 88 } 89 90 var getPassword func() []byte 91 if passwordFile != "" { 92 u, err := url.Parse(urlString) 93 if err != nil { 94 return nil, err 95 } 96 if u.Scheme != "https" { 97 // returning error here would make the tests hard 98 // we print out a warning message here instead 99 fmt.Printf("[WARNING] should NOT use password without enabling TLS: '%s'\n", urlString) 100 } 101 102 if err := secret.Add(passwordFile); err != nil { 103 logrus.WithError(err).Fatal("Failed to start secrets agent") 104 } 105 getPassword = secret.GetTokenGenerator(passwordFile) 106 } 107 108 return NewClientWithPasswordGetter(owner, urlString, username, getPassword) 109 } 110 111 // NewClientWithPasswordGetter creates a Boskos client for the specified URL and resource owner. 112 // 113 // Clients created with this function default to retrying failed connection 114 // attempts three times with a ten second pause between each attempt. 115 func NewClientWithPasswordGetter(owner string, urlString, username string, passwordGetter func() []byte) (*Client, error) { 116 client := &Client{ 117 url: urlString, 118 username: username, 119 getPassword: passwordGetter, 120 owner: owner, 121 storage: storage.NewMemoryStorage(), 122 } 123 124 // Configure the dialer to attempt three additional times to establish 125 // a connection after a failed dial attempt. The dialer should wait 10 126 // seconds between each attempt. 127 client.Dialer.RetryCount = 3 128 client.Dialer.RetrySleep = time.Second * 10 129 130 // Configure the dialer and HTTP client transport to mimic the configuration 131 // of the http.DefaultTransport with the exception that the Dialer's Dial 132 // and DialContext functions are assigned to the client transport. 133 // 134 // See https://golang.org/pkg/net/http/#RoundTripper for the values 135 // values used for the http.DefaultTransport. 136 client.Dialer.Timeout = 30 * time.Second 137 client.Dialer.KeepAlive = 30 * time.Second 138 client.Dialer.DualStack = true 139 client.http.Transport = &http.Transport{ 140 Proxy: http.ProxyFromEnvironment, 141 Dial: client.Dialer.Dial, 142 DialContext: client.Dialer.DialContext, 143 MaxIdleConns: 100, 144 IdleConnTimeout: 90 * time.Second, 145 TLSHandshakeTimeout: 10 * time.Second, 146 ExpectContinueTimeout: 1 * time.Second, 147 } 148 149 return client, nil 150 } 151 152 // public method 153 154 // Acquire asks boskos for a resource of certain type in certain state, and set the resource to dest state. 155 // Returns the resource on success. 156 func (c *Client) Acquire(rtype, state, dest string) (*common.Resource, error) { 157 return c.AcquireWithPriority(rtype, state, dest, "") 158 } 159 160 // AcquireWithPriority asks boskos for a resource of certain type in certain state, and set the resource to dest state. 161 // Returns the resource on success. 162 // Boskos Priority are FIFO. 163 func (c *Client) AcquireWithPriority(rtype, state, dest, requestID string) (*common.Resource, error) { 164 r, err := c.acquire(rtype, state, dest, requestID) 165 if err != nil { 166 return nil, err 167 } 168 c.lock.Lock() 169 defer c.lock.Unlock() 170 if r != nil { 171 c.storage.Add(*r) 172 } 173 174 return r, nil 175 } 176 177 // AcquireWait blocks until Acquire returns the specified resource or the 178 // provided context is cancelled or its deadline exceeded. 179 func (c *Client) AcquireWait(ctx context.Context, rtype, state, dest string) (*common.Resource, error) { 180 // request with FIFO priority 181 requestID := uuid.New().String() 182 return c.AcquireWaitWithPriority(ctx, rtype, state, dest, requestID) 183 } 184 185 // AcquireWaitWithPriority blocks until Acquire returns the specified resource or the 186 // provided context is cancelled or its deadline exceeded. This allows you to pass in a request priority. 187 // Boskos Priority are FIFO. 188 func (c *Client) AcquireWaitWithPriority(ctx context.Context, rtype, state, dest, requestID string) (*common.Resource, error) { 189 if ctx == nil { 190 return nil, ErrContextRequired 191 } 192 // Try to acquire the resource until available or the context is 193 // cancelled or its deadline exceeded. 194 for { 195 r, err := c.AcquireWithPriority(rtype, state, dest, requestID) 196 if err != nil { 197 if err == ErrAlreadyInUse || err == ErrNotFound { 198 select { 199 case <-ctx.Done(): 200 return nil, err 201 case <-time.After(3 * time.Second): 202 continue 203 } 204 } 205 return nil, err 206 } 207 return r, nil 208 } 209 } 210 211 // AcquireByState asks boskos for a resources of certain type, and set the resource to dest state. 212 // Returns a list of resources on success. 213 func (c *Client) AcquireByState(state, dest string, names []string) ([]common.Resource, error) { 214 resources, err := c.acquireByState(state, dest, names) 215 if err != nil { 216 return nil, err 217 } 218 c.lock.Lock() 219 defer c.lock.Unlock() 220 for _, r := range resources { 221 c.storage.Add(r) 222 } 223 return resources, nil 224 } 225 226 // AcquireByStateWait blocks until AcquireByState returns the specified 227 // resource(s) or the provided context is cancelled or its deadline 228 // exceeded. 229 func (c *Client) AcquireByStateWait(ctx context.Context, state, dest string, names []string) ([]common.Resource, error) { 230 if ctx == nil { 231 return nil, ErrContextRequired 232 } 233 // Try to acquire the resource(s) until available or the context is 234 // cancelled or its deadline exceeded. 235 for { 236 r, err := c.AcquireByState(state, dest, names) 237 if err != nil { 238 if err == ErrAlreadyInUse || err == ErrNotFound { 239 select { 240 case <-ctx.Done(): 241 return nil, err 242 case <-time.After(3 * time.Second): 243 continue 244 } 245 } 246 return nil, err 247 } 248 return r, nil 249 } 250 } 251 252 // ReleaseAll returns all resources hold by the client back to boskos and set them to dest state. 253 func (c *Client) ReleaseAll(dest string) error { 254 c.lock.Lock() 255 defer c.lock.Unlock() 256 resources, err := c.storage.List() 257 if err != nil { 258 return err 259 } 260 if len(resources) == 0 { 261 return fmt.Errorf("no holding resource") 262 } 263 var allErrors error 264 for _, r := range resources { 265 c.storage.Delete(r.Name) 266 err := c.Release(r.Name, dest) 267 if err != nil { 268 allErrors = multierror.Append(allErrors, err) 269 } 270 } 271 return allErrors 272 } 273 274 // ReleaseOne returns one of owned resources back to boskos and set it to dest state. 275 func (c *Client) ReleaseOne(name, dest string) error { 276 c.lock.Lock() 277 defer c.lock.Unlock() 278 279 if _, err := c.storage.Get(name); err != nil { 280 return fmt.Errorf("no resource name %v", name) 281 } 282 c.storage.Delete(name) 283 if err := c.Release(name, dest); err != nil { 284 return err 285 } 286 return nil 287 } 288 289 // UpdateAll signals update for all resources hold by the client. 290 func (c *Client) UpdateAll(state string) error { 291 c.lock.Lock() 292 defer c.lock.Unlock() 293 294 resources, err := c.storage.List() 295 if err != nil { 296 return err 297 } 298 if len(resources) == 0 { 299 return fmt.Errorf("no holding resource") 300 } 301 var allErrors error 302 for _, r := range resources { 303 if err := c.Update(r.Name, state, nil); err != nil { 304 allErrors = multierror.Append(allErrors, err) 305 continue 306 } 307 if err := c.updateLocalResource(r, state, nil); err != nil { 308 allErrors = multierror.Append(allErrors, err) 309 } 310 } 311 return allErrors 312 } 313 314 // SyncAll signals update for all resources hold by the client. 315 func (c *Client) SyncAll() error { 316 c.lock.Lock() 317 defer c.lock.Unlock() 318 319 resources, err := c.storage.List() 320 if err != nil { 321 return err 322 } 323 if len(resources) == 0 { 324 logrus.Info("no resource to sync") 325 return nil 326 } 327 var allErrors error 328 for _, r := range resources { 329 if err := c.Update(r.Name, r.State, nil); err != nil { 330 allErrors = multierror.Append(allErrors, err) 331 continue 332 } 333 if _, err := c.storage.Update(r); err != nil { 334 allErrors = multierror.Append(allErrors, err) 335 } 336 } 337 return allErrors 338 } 339 340 // UpdateOne signals update for one of the resources hold by the client. 341 func (c *Client) UpdateOne(name, state string, userData *common.UserData) error { 342 c.lock.Lock() 343 defer c.lock.Unlock() 344 345 r, err := c.storage.Get(name) 346 if err != nil { 347 return fmt.Errorf("no resource name %v", name) 348 } 349 if err := c.Update(r.Name, state, userData); err != nil { 350 return err 351 } 352 return c.updateLocalResource(r, state, userData) 353 } 354 355 // Reset will scan all boskos resources of type, in state, last updated before expire, and set them to dest state. 356 // Returns a map of {resourceName:owner} for further actions. 357 func (c *Client) Reset(rtype, state string, expire time.Duration, dest string) (map[string]string, error) { 358 return c.reset(rtype, state, expire, dest) 359 } 360 361 // Metric will query current metric for target resource type. 362 // Return a common.Metric object on success. 363 func (c *Client) Metric(rtype string) (common.Metric, error) { 364 return c.metric(rtype) 365 } 366 367 // HasResource tells if current client holds any resources 368 func (c *Client) HasResource() bool { 369 resources, _ := c.storage.List() 370 return len(resources) > 0 371 } 372 373 // private methods 374 375 func (c *Client) updateLocalResource(res common.Resource, state string, data *common.UserData) error { 376 res.State = state 377 if res.UserData == nil { 378 res.UserData = data 379 } else { 380 res.UserData.Update(data) 381 } 382 _, err := c.storage.Update(res) 383 return err 384 } 385 386 func (c *Client) acquire(rtype, state, dest, requestID string) (*common.Resource, error) { 387 values := url.Values{} 388 values.Set("type", rtype) 389 values.Set("state", state) 390 values.Set("owner", c.owner) 391 values.Set("dest", dest) 392 if requestID != "" { 393 values.Set("request_id", requestID) 394 } 395 396 res := common.Resource{} 397 398 work := func(retriedErrs *[]error) (bool, error) { 399 resp, err := c.httpPost("/acquire", values, "", nil) 400 if err != nil { 401 // Swallow the error so we can retry 402 *retriedErrs = append(*retriedErrs, err) 403 return false, nil 404 } 405 defer resp.Body.Close() 406 407 switch resp.StatusCode { 408 case http.StatusOK: 409 body, err := io.ReadAll(resp.Body) 410 if err != nil { 411 return false, err 412 } 413 414 err = json.Unmarshal(body, &res) 415 if err != nil { 416 return false, err 417 } 418 if res.Name == "" { 419 return false, fmt.Errorf("unable to parse resource") 420 } 421 return true, nil 422 case http.StatusUnauthorized: 423 return false, ErrAlreadyInUse 424 case http.StatusNotFound: 425 // The only way to distinguish between all reasources being busy and a request for a non-existent 426 // resource type is to check the text of the accompanying error message. 427 if c.DistinguishNotFoundVsTypeNotFound { 428 if bytes, err := io.ReadAll(resp.Body); err == nil { 429 errorMsg := string(bytes) 430 if strings.Contains(errorMsg, common.ResourceTypeNotFoundMessage(rtype)) { 431 return false, ErrTypeNotFound 432 } 433 } 434 } 435 return false, ErrNotFound 436 default: 437 *retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, status code %v", resp.Status, resp.StatusCode)) 438 // Swallow it so we can retry 439 return false, nil 440 } 441 } 442 443 return &res, retry(work) 444 } 445 446 func (c *Client) acquireByState(state, dest string, names []string) ([]common.Resource, error) { 447 values := url.Values{} 448 values.Set("state", state) 449 values.Set("dest", dest) 450 values.Set("names", strings.Join(names, ",")) 451 values.Set("owner", c.owner) 452 var resources []common.Resource 453 454 work := func(retriedErrs *[]error) (bool, error) { 455 resp, err := c.httpPost("/acquirebystate", values, "", nil) 456 if err != nil { 457 *retriedErrs = append(*retriedErrs, err) 458 return false, nil 459 } 460 defer resp.Body.Close() 461 462 switch resp.StatusCode { 463 case http.StatusOK: 464 if err := json.NewDecoder(resp.Body).Decode(&resources); err != nil { 465 return false, err 466 } 467 return true, nil 468 case http.StatusUnauthorized: 469 return false, ErrAlreadyInUse 470 case http.StatusNotFound: 471 return false, ErrNotFound 472 default: 473 *retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, status code %v", resp.Status, resp.StatusCode)) 474 return false, nil 475 } 476 } 477 478 return resources, retry(work) 479 } 480 481 // Release a lease for a resource and set its state to the destination state 482 func (c *Client) Release(name, dest string) error { 483 values := url.Values{} 484 values.Set("name", name) 485 values.Set("dest", dest) 486 values.Set("owner", c.owner) 487 488 work := func(retriedErrs *[]error) (bool, error) { 489 resp, err := c.httpPost("/release", values, "", nil) 490 if err != nil { 491 *retriedErrs = append(*retriedErrs, err) 492 return false, nil 493 } 494 defer resp.Body.Close() 495 496 if resp.StatusCode != http.StatusOK { 497 *retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, statusCode %v releasing %s", resp.Status, resp.StatusCode, name)) 498 return false, nil 499 } 500 return true, nil 501 } 502 503 return retry(work) 504 } 505 506 // Update a resource on the server, setting the state and user data 507 func (c *Client) Update(name, state string, userData *common.UserData) error { 508 var bodyData *bytes.Buffer 509 if userData != nil { 510 bodyData = new(bytes.Buffer) 511 err := json.NewEncoder(bodyData).Encode(userData) 512 if err != nil { 513 return err 514 } 515 } 516 values := url.Values{} 517 values.Set("name", name) 518 values.Set("owner", c.owner) 519 values.Set("state", state) 520 521 work := func(retriedErrs *[]error) (bool, error) { 522 // As the body is an io.Reader and hence its content 523 // can only be read once, we have to copy it for every request we make 524 var body io.Reader 525 if bodyData != nil { 526 body = bytes.NewReader(bodyData.Bytes()) 527 } 528 resp, err := c.httpPost("/update", values, "application/json", body) 529 if err != nil { 530 *retriedErrs = append(*retriedErrs, err) 531 return false, nil 532 } 533 defer resp.Body.Close() 534 535 if resp.StatusCode != http.StatusOK { 536 *retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, status code %v updating %s", resp.Status, resp.StatusCode, name)) 537 return false, nil 538 } 539 return true, nil 540 } 541 542 return retry(work) 543 } 544 545 func (c *Client) reset(rtype, state string, expire time.Duration, dest string) (map[string]string, error) { 546 rmap := make(map[string]string) 547 values := url.Values{} 548 values.Set("type", rtype) 549 values.Set("state", state) 550 values.Set("expire", expire.String()) 551 values.Set("dest", dest) 552 553 work := func(retriedErrs *[]error) (bool, error) { 554 resp, err := c.httpPost("/reset", values, "", nil) 555 if err != nil { 556 *retriedErrs = append(*retriedErrs, err) 557 return false, nil 558 } 559 defer resp.Body.Close() 560 561 if resp.StatusCode == http.StatusOK { 562 body, err := io.ReadAll(resp.Body) 563 if err != nil { 564 return false, err 565 } 566 567 err = json.Unmarshal(body, &rmap) 568 return true, err 569 } 570 *retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, status code %v", resp.Status, resp.StatusCode)) 571 return false, nil 572 573 } 574 575 return rmap, retry(work) 576 } 577 578 func (c *Client) metric(rtype string) (common.Metric, error) { 579 var metric common.Metric 580 values := url.Values{} 581 values.Set("type", rtype) 582 583 work := func(retriedErrs *[]error) (bool, error) { 584 resp, err := c.httpGet("/metric", values) 585 if err != nil { 586 *retriedErrs = append(*retriedErrs, err) 587 return false, nil 588 } 589 defer resp.Body.Close() 590 591 if resp.StatusCode != http.StatusOK { 592 *retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, status code %v", resp.Status, resp.StatusCode)) 593 return false, nil 594 } 595 596 body, err := io.ReadAll(resp.Body) 597 if err != nil { 598 return false, err 599 } 600 601 return true, json.Unmarshal(body, &metric) 602 } 603 604 return metric, retry(work) 605 } 606 607 func (c *Client) httpGet(action string, values url.Values) (*http.Response, error) { 608 u, _ := url.ParseRequestURI(c.url) 609 u.Path = action 610 u.RawQuery = values.Encode() 611 req, err := http.NewRequest(http.MethodGet, u.String(), nil) 612 if err != nil { 613 return nil, err 614 } 615 if c.username != "" && c.getPassword != nil { 616 req.SetBasicAuth(c.username, string(c.getPassword())) 617 } 618 return c.http.Do(req) 619 } 620 621 func (c *Client) httpPost(action string, values url.Values, contentType string, body io.Reader) (*http.Response, error) { 622 u, _ := url.ParseRequestURI(c.url) 623 u.Path = action 624 u.RawQuery = values.Encode() 625 req, err := http.NewRequest(http.MethodPost, u.String(), body) 626 if err != nil { 627 return nil, err 628 } 629 if c.username != "" && c.getPassword != nil { 630 req.SetBasicAuth(c.username, string(c.getPassword())) 631 } 632 if contentType != "" { 633 req.Header.Set("Content-Type", contentType) 634 } 635 return c.http.Do(req) 636 } 637 638 // DialerWithRetry is a composite version of the net.Dialer that retries 639 // connection attempts. 640 type DialerWithRetry struct { 641 net.Dialer 642 643 // RetryCount is the number of times to retry a connection attempt. 644 RetryCount uint 645 646 // RetrySleep is the length of time to pause between retry attempts. 647 RetrySleep time.Duration 648 } 649 650 // Dial connects to the address on the named network. 651 func (d *DialerWithRetry) Dial(network, address string) (net.Conn, error) { 652 return d.DialContext(context.Background(), network, address) 653 } 654 655 // DialContext connects to the address on the named network using the provided context. 656 func (d *DialerWithRetry) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 657 // Always bump the retry count by 1 in order to equal the actual number of 658 // attempts. For example, if a retry count of 2 is specified, the intent 659 // is for three attempts -- the initial attempt with two retries in case 660 // the initial attempt times out. 661 count := d.RetryCount + 1 662 sleep := d.RetrySleep 663 i := uint(0) 664 for { 665 conn, err := d.Dialer.DialContext(ctx, network, address) 666 if err != nil { 667 if isDialErrorRetriable(err) { 668 if i < count-1 { 669 select { 670 case <-time.After(sleep): 671 i++ 672 continue 673 case <-ctx.Done(): 674 return nil, err 675 } 676 } 677 } 678 return nil, err 679 } 680 return conn, nil 681 } 682 } 683 684 // isDialErrorRetriable determines whether or not a dialer should retry 685 // a failed connection attempt by examining the connection error to see 686 // if it is one of the following error types: 687 // - Timeout 688 // - Temporary 689 // - ECONNREFUSED 690 // - ECONNRESET 691 func isDialErrorRetriable(err error) bool { 692 opErr, isOpErr := err.(*net.OpError) 693 if !isOpErr { 694 return false 695 } 696 if opErr.Timeout() || opErr.Temporary() { 697 return true 698 } 699 sysErr, isSysErr := opErr.Err.(*os.SyscallError) 700 if !isSysErr { 701 return false 702 } 703 switch sysErr.Err { 704 case syscall.ECONNREFUSED, syscall.ECONNRESET: 705 return true 706 } 707 return false 708 } 709 710 // workFunc describes retrieable work. It should 711 // * Return an error for non-recoverable errors 712 // * Write retriable errors into `retriedErrs` and return with false, nil 713 // * Return with true, nil on success 714 type workFunc func(retriedErrs *[]error) (bool, error) 715 716 // SleepFunc is called when requests are retried. This may be replaced in tests. 717 var SleepFunc = time.Sleep 718 719 func retry(work workFunc) error { 720 var retriedErrs []error 721 722 maxAttempts := 4 723 for i := 1; i <= maxAttempts; i++ { 724 success, err := work(&retriedErrs) 725 if err != nil { 726 return err 727 } 728 if success { 729 return nil 730 } 731 if i == maxAttempts { 732 break 733 } 734 735 SleepFunc(time.Duration(i*i) * time.Second) 736 } 737 738 return utilerrors.NewAggregate(retriedErrs) 739 }