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(&currentRPS) >= c.apiConfig.SystemRPSLimit {
   113  					return errors.New("RPS limit reached")
   114  				} else {
   115  					atomic.AddUint64(&currentRPS, 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(&currentRPS, ^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  }