github.com/kyma-project/kyma-environment-broker@v0.0.1/internal/cis/client.go (about)

     1  package cis
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"strconv"
    10  	"time"
    11  
    12  	kebError "github.com/kyma-project/kyma-environment-broker/internal/error"
    13  	"github.com/sirupsen/logrus"
    14  	"golang.org/x/oauth2/clientcredentials"
    15  )
    16  
    17  const (
    18  	eventServicePath = "%s/events/v1/events/central"
    19  	eventType        = "Subaccount_Deletion"
    20  	defaultPageSize  = "150"
    21  )
    22  
    23  type Config struct {
    24  	ClientID             string
    25  	ClientSecret         string
    26  	AuthURL              string
    27  	EventServiceURL      string
    28  	PageSize             string        `envconfig:"optional"`
    29  	RateLimitingInterval time.Duration `envconfig:"default=2s,optional"`
    30  	MaxRequestRetries    int           `envconfig:"default=3,optional"`
    31  }
    32  
    33  type Client struct {
    34  	httpClient *http.Client
    35  	config     Config
    36  	log        logrus.FieldLogger
    37  }
    38  
    39  func NewClient(ctx context.Context, config Config, log logrus.FieldLogger) *Client {
    40  	cfg := clientcredentials.Config{
    41  		ClientID:     config.ClientID,
    42  		ClientSecret: config.ClientSecret,
    43  		TokenURL:     config.AuthURL,
    44  	}
    45  	httpClientOAuth := cfg.Client(ctx)
    46  
    47  	if config.PageSize == "" {
    48  		config.PageSize = defaultPageSize
    49  	}
    50  
    51  	return &Client{
    52  		httpClient: httpClientOAuth,
    53  		config:     config,
    54  		log:        log.WithField("client", "CIS-2.0"),
    55  	}
    56  }
    57  
    58  // SetHttpClient auxiliary method of testing to get rid of oAuth client wrapper
    59  func (c *Client) SetHttpClient(httpClient *http.Client) {
    60  	c.httpClient = httpClient
    61  }
    62  
    63  type subaccounts struct {
    64  	total int
    65  	ids   []string
    66  	from  time.Time
    67  	to    time.Time
    68  }
    69  
    70  func (c *Client) FetchSubaccountsToDelete() ([]string, error) {
    71  	subaccounts := subaccounts{}
    72  
    73  	err := c.fetchSubaccountsFromDeleteEvents(&subaccounts)
    74  	if err != nil {
    75  		return []string{}, fmt.Errorf("while fetching subaccounts from delete events: %w", err)
    76  	}
    77  
    78  	c.log.Infof("CIS returned total amount of delete events: %d, client fetched %d subaccounts to delete. "+
    79  		"The events includes a range of time from %s to %s",
    80  		subaccounts.total,
    81  		len(subaccounts.ids),
    82  		subaccounts.from,
    83  		subaccounts.to)
    84  
    85  	return subaccounts.ids, nil
    86  }
    87  
    88  func (c *Client) fetchSubaccountsFromDeleteEvents(subaccs *subaccounts) error {
    89  	var currentPage, totalPages, retries int
    90  	for currentPage <= totalPages {
    91  		cisResponse, err := c.fetchSubaccountDeleteEventsForGivenPageNum(currentPage)
    92  		if err != nil {
    93  			if kebError.IsTemporaryError(err) && retries < c.config.MaxRequestRetries {
    94  				time.Sleep(c.config.RateLimitingInterval)
    95  				retries++
    96  				continue
    97  			}
    98  			return fmt.Errorf("while fetching subaccount delete events for %d page: %w", currentPage, err)
    99  		}
   100  		totalPages = cisResponse.TotalPages
   101  		subaccs.total = cisResponse.Total
   102  		c.appendSubaccountsFromDeleteEvents(&cisResponse, subaccs)
   103  		retries = 0
   104  		currentPage++
   105  	}
   106  
   107  	return nil
   108  }
   109  
   110  func (c *Client) fetchSubaccountDeleteEventsForGivenPageNum(page int) (CisResponse, error) {
   111  	request, err := c.buildRequest(page)
   112  	if err != nil {
   113  		return CisResponse{}, fmt.Errorf("while building request for event service: %w", err)
   114  	}
   115  
   116  	response, err := c.httpClient.Do(request)
   117  	if err != nil {
   118  		return CisResponse{}, fmt.Errorf("while executing request to event service: %w", err)
   119  	}
   120  	defer response.Body.Close()
   121  
   122  	switch {
   123  	case response.StatusCode == http.StatusTooManyRequests:
   124  		return CisResponse{}, kebError.NewTemporaryError("rate limiting: %s", c.handleWrongStatusCode(response))
   125  	case response.StatusCode != http.StatusOK:
   126  		return CisResponse{}, fmt.Errorf("while processing response: %s", c.handleWrongStatusCode(response))
   127  	}
   128  
   129  	var cisResponse CisResponse
   130  	err = json.NewDecoder(response.Body).Decode(&cisResponse)
   131  	if err != nil {
   132  		return CisResponse{}, fmt.Errorf("while decoding CIS response: %w", err)
   133  	}
   134  
   135  	return cisResponse, nil
   136  }
   137  
   138  func (c *Client) buildRequest(page int) (*http.Request, error) {
   139  	request, err := http.NewRequest(http.MethodGet, fmt.Sprintf(eventServicePath, c.config.EventServiceURL), nil)
   140  	if err != nil {
   141  		return nil, fmt.Errorf("while creating request: %w", err)
   142  	}
   143  
   144  	q := request.URL.Query()
   145  	q.Add("eventType", eventType)
   146  	q.Add("pageSize", c.config.PageSize)
   147  	q.Add("pageNum", strconv.Itoa(page))
   148  	q.Add("sortField", "creationTime")
   149  	q.Add("sortOrder", "ASC")
   150  
   151  	request.URL.RawQuery = q.Encode()
   152  
   153  	return request, nil
   154  }
   155  
   156  func (c *Client) handleWrongStatusCode(response *http.Response) string {
   157  	body, err := io.ReadAll(response.Body)
   158  	if err != nil {
   159  		return fmt.Sprintf("server returned %d status code, response body is unreadable", response.StatusCode)
   160  	}
   161  
   162  	return fmt.Sprintf("server returned %d status code, body: %s", response.StatusCode, string(body))
   163  }
   164  
   165  func (c *Client) appendSubaccountsFromDeleteEvents(cisResp *CisResponse, subaccs *subaccounts) {
   166  	for _, event := range cisResp.Events {
   167  		if event.Type != eventType {
   168  			c.log.Warnf("event type %s is not equal to %s, skip event", event.Type, eventType)
   169  			continue
   170  		}
   171  		subaccs.ids = append(subaccs.ids, event.SubAccount)
   172  
   173  		if subaccs.from.IsZero() {
   174  			subaccs.from = time.Unix(0, event.CreationTime*int64(1000000))
   175  		}
   176  		if subaccs.total == len(subaccs.ids) {
   177  			subaccs.to = time.Unix(0, event.CreationTime*int64(1000000))
   178  		}
   179  	}
   180  }