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 }