github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/systemfetcher/client.go (about) 1 package systemfetcher 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "strings" 10 "sync" 11 "sync/atomic" 12 "time" 13 14 "github.com/avast/retry-go/v4" 15 16 "github.com/kyma-incubator/compass/components/director/pkg/log" 17 "github.com/kyma-incubator/compass/components/director/pkg/paging" 18 "github.com/pkg/errors" 19 ) 20 21 // APIClient missing godoc 22 // 23 //go:generate mockery --name=APIClient --output=automock --outpkg=automock --case=underscore --disable-version-string 24 type APIClient interface { 25 Do(*http.Request, string) (*http.Response, error) 26 } 27 28 // APIConfig missing godoc 29 type APIConfig struct { 30 Endpoint string `envconfig:"APP_SYSTEM_INFORMATION_ENDPOINT"` 31 FilterCriteria string `envconfig:"APP_SYSTEM_INFORMATION_FILTER_CRITERIA"` 32 Timeout time.Duration `envconfig:"APP_SYSTEM_INFORMATION_FETCH_TIMEOUT"` 33 PageSize uint64 `envconfig:"APP_SYSTEM_INFORMATION_PAGE_SIZE"` 34 PagingSkipParam string `envconfig:"APP_SYSTEM_INFORMATION_PAGE_SKIP_PARAM"` 35 PagingSizeParam string `envconfig:"APP_SYSTEM_INFORMATION_PAGE_SIZE_PARAM"` 36 SystemSourceKey string `envconfig:"APP_SYSTEM_INFORMATION_SOURCE_KEY"` 37 SystemRPSLimit uint64 `envconfig:"default=15,APP_SYSTEM_INFORMATION_RPS_LIMIT"` 38 } 39 40 // Client missing godoc 41 type Client struct { 42 apiConfig APIConfig 43 httpClient APIClient 44 } 45 46 // NewClient missing godoc 47 func NewClient(apiConfig APIConfig, client APIClient) *Client { 48 return &Client{ 49 apiConfig: apiConfig, 50 httpClient: client, 51 } 52 } 53 54 var currentRPS uint64 55 56 // FetchSystemsForTenant fetches systems from the service 57 func (c *Client) FetchSystemsForTenant(ctx context.Context, tenant string, mutex *sync.Mutex) ([]System, error) { 58 mutex.Lock() 59 qp := c.buildFilter() 60 mutex.Unlock() 61 log.C(ctx).Infof("Fetching systems for tenant %s with query: %s", tenant, qp) 62 63 var systems []System 64 65 systemsFunc := c.getSystemsPagingFunc(ctx, &systems, tenant) 66 pi := paging.NewPageIterator(c.apiConfig.Endpoint, c.apiConfig.PagingSkipParam, c.apiConfig.PagingSizeParam, qp, c.apiConfig.PageSize, systemsFunc) 67 if err := pi.FetchAll(); err != nil { 68 return nil, errors.Wrapf(err, "failed to fetch systems for tenant %s", tenant) 69 } 70 71 log.C(ctx).Infof("Fetched systems for tenant %s", tenant) 72 return systems, nil 73 } 74 75 func (c *Client) fetchSystemsForTenant(ctx context.Context, url, tenant string) ([]System, error) { 76 req, err := http.NewRequest("GET", url, nil) 77 if err != nil { 78 return nil, errors.Wrap(err, "failed to create new HTTP request") 79 } 80 81 resp, err := c.httpClient.Do(req, tenant) 82 if err != nil { 83 return nil, errors.Wrap(err, "failed to execute HTTP request") 84 } 85 defer func() { 86 err := resp.Body.Close() 87 if err != nil { 88 log.C(ctx).Println("Failed to close HTTP response body") 89 } 90 }() 91 if resp.StatusCode != http.StatusOK { 92 return nil, fmt.Errorf("unexpected status code: expected: %d, but got: %d", http.StatusOK, resp.StatusCode) 93 } 94 95 respBody, err := io.ReadAll(resp.Body) 96 if err != nil { 97 return nil, errors.Wrap(err, "failed to parse HTTP response body") 98 } 99 100 var systems []System 101 if err = json.Unmarshal(respBody, &systems); err != nil { 102 return nil, errors.Wrap(err, "failed to unmarshal systems response") 103 } 104 105 return systems, nil 106 } 107 108 func (c *Client) getSystemsPagingFunc(ctx context.Context, systems *[]System, tenant string) func(string) (uint64, error) { 109 return func(url string) (uint64, error) { 110 err := retry.Do( 111 func() error { 112 if atomic.LoadUint64(¤tRPS) >= c.apiConfig.SystemRPSLimit { 113 return errors.New("RPS limit reached") 114 } else { 115 atomic.AddUint64(¤tRPS, 1) 116 return nil 117 } 118 }, 119 retry.Attempts(0), 120 retry.Delay(time.Millisecond*100), 121 ) 122 123 if err != nil { 124 return 0, err 125 } 126 127 var currentSystems []System 128 err = retry.Do( 129 func() error { 130 currentSystems, err = c.fetchSystemsForTenant(ctx, url, tenant) 131 if err != nil && err.Error() == "unexpected status code: expected: 200, but got: 401" { 132 return retry.Unrecoverable(err) 133 } 134 return err 135 }, 136 retry.Attempts(3), 137 retry.Delay(time.Second), 138 retry.OnRetry(func(n uint, err error) { 139 log.C(ctx).Infof("Retrying request attempt (%d) after error %v", n, err) 140 }), 141 ) 142 143 atomic.AddUint64(¤tRPS, ^uint64(0)) 144 145 if err != nil { 146 return 0, err 147 } 148 log.C(ctx).Infof("Fetched page of systems for URL %s", url) 149 *systems = append(*systems, currentSystems...) 150 return uint64(len(currentSystems)), nil 151 } 152 } 153 154 func (c *Client) buildFilter() map[string]string { 155 var filterBuilder FilterBuilder 156 157 for _, at := range ApplicationTemplates { 158 lbl, ok := at.Labels[ApplicationTemplateLabelFilter] 159 if !ok { 160 continue 161 } 162 163 lblToString, ok := lbl.Value.(string) 164 if !ok { 165 lblToString = "" 166 } 167 expr1 := filterBuilder.NewExpression(SystemSourceKey, "eq", lblToString) 168 169 lblExists := false 170 minTime := time.Now() 171 172 for _, s := range SystemSynchronizationTimestamps { 173 v, ok := s[lblToString] 174 if ok { 175 lblExists = true 176 if v.LastSyncTimestamp.Before(minTime) { 177 minTime = v.LastSyncTimestamp 178 } 179 } 180 } 181 182 if lblExists { 183 expr2 := filterBuilder.NewExpression("lastChangeDateTime", "gt", minTime.String()) 184 filterBuilder.addFilter(expr1, expr2) 185 } else { 186 filterBuilder.addFilter(expr1) 187 } 188 } 189 result := map[string]string{"fetchAcrossZones": "true"} 190 191 if len(c.apiConfig.FilterCriteria) > 0 { 192 result["$filter"] = fmt.Sprintf(c.apiConfig.FilterCriteria, filterBuilder.buildFilterQuery()) 193 } 194 195 selectFilter := strings.Join(SelectFilter, ",") 196 if len(selectFilter) > 0 { 197 result["$select"] = selectFilter 198 } 199 return result 200 }