github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/fetchrequest/service.go (about) 1 package fetchrequest 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "time" 9 10 "github.com/kyma-incubator/compass/components/director/pkg/retry" 11 12 "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" 13 14 "github.com/kyma-incubator/compass/components/director/pkg/accessstrategy" 15 16 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 17 18 httputil "github.com/kyma-incubator/compass/components/director/pkg/http" 19 20 "github.com/kyma-incubator/compass/components/director/pkg/str" 21 22 "github.com/kyma-incubator/compass/components/director/internal/timestamp" 23 24 "github.com/kyma-incubator/compass/components/director/pkg/log" 25 26 "github.com/kyma-incubator/compass/components/director/internal/model" 27 ) 28 29 type service struct { 30 repo FetchRequestRepository 31 client *http.Client 32 timestampGen timestamp.Generator 33 accessStrategyExecutorProvider accessstrategy.ExecutorProvider 34 retryHTTPFuncExecutor *retry.HTTPExecutor 35 } 36 37 // FetchRequestRepository missing godoc 38 // 39 //go:generate mockery --name=FetchRequestRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 40 type FetchRequestRepository interface { 41 Update(ctx context.Context, tenant string, item *model.FetchRequest) error 42 UpdateGlobal(ctx context.Context, item *model.FetchRequest) error 43 } 44 45 // NewService missing godoc 46 func NewService(repo FetchRequestRepository, client *http.Client, executorProvider accessstrategy.ExecutorProvider) *service { 47 return &service{ 48 repo: repo, 49 client: client, 50 timestampGen: timestamp.DefaultGenerator, 51 accessStrategyExecutorProvider: executorProvider, 52 } 53 } 54 55 // NewServiceWithRetry creates a FetchRequest service which is able to retry failed HTTP requests 56 func NewServiceWithRetry(repo FetchRequestRepository, client *http.Client, executorProvider accessstrategy.ExecutorProvider, retryHTTPExecutor *retry.HTTPExecutor) *service { 57 return &service{ 58 repo: repo, 59 client: client, 60 timestampGen: timestamp.DefaultGenerator, 61 accessStrategyExecutorProvider: executorProvider, 62 retryHTTPFuncExecutor: retryHTTPExecutor, 63 } 64 } 65 66 // HandleSpec missing godoc 67 func (s *service) HandleSpec(ctx context.Context, fr *model.FetchRequest) *string { 68 tnt, err := tenant.LoadFromContext(ctx) 69 if err != nil { 70 log.C(ctx).WithError(err).Errorf("An error has occurred while getting tenant: %v", err) 71 return nil 72 } 73 74 var data *string 75 data, fr.Status = s.FetchSpec(ctx, fr) 76 77 if err := s.repo.Update(ctx, tnt, fr); err != nil { 78 log.C(ctx).WithError(err).Errorf("An error has occurred while updating fetch request status: %v", err) 79 return nil 80 } 81 82 return data 83 } 84 85 // Update is identical to HandleSpec with the difference that the fetch request is only updated in DB without being re-executed 86 func (s *service) Update(ctx context.Context, fr *model.FetchRequest) error { 87 tnt, err := tenant.LoadFromContext(ctx) 88 if err != nil { 89 log.C(ctx).WithError(err).Errorf("An error has occurred while getting tenant: %v", err) 90 return err 91 } 92 93 if err := s.repo.Update(ctx, tnt, fr); err != nil { 94 log.C(ctx).WithError(err).Errorf("An error has occurred while updating fetch request: %v", err) 95 return err 96 } 97 98 return nil 99 } 100 101 // UpdateGlobal is identical to HandleSpec with the difference that the fetch request is only updated in DB without being re-executed and there is no tenant isolation 102 func (s *service) UpdateGlobal(ctx context.Context, fr *model.FetchRequest) error { 103 if err := s.repo.UpdateGlobal(ctx, fr); err != nil { 104 log.C(ctx).WithError(err).Errorf("An error has occurred while updating fetch request: %v", err) 105 return err 106 } 107 108 return nil 109 } 110 111 func (s *service) FetchSpec(ctx context.Context, fr *model.FetchRequest) (*string, *model.FetchRequestStatus) { 112 err := s.validateFetchRequest(fr) 113 if err != nil { 114 log.C(ctx).WithError(err).Error() 115 return nil, FixStatus(model.FetchRequestStatusConditionInitial, str.Ptr(err.Error()), s.timestampGen()) 116 } 117 118 localTenantID, err := tenant.LoadLocalTenantIDFromContext(ctx) 119 if err != nil { 120 log.C(ctx).Warnf("An error has occurred while getting local tenant id: %v", err) 121 localTenantID = "" 122 } 123 var doRequest retry.ExecutableHTTPFunc 124 if fr.Auth != nil && fr.Auth.AccessStrategy != nil && len(*fr.Auth.AccessStrategy) > 0 { 125 log.C(ctx).Infof("Fetch Request with id %s is configured with %s access strategy.", fr.ID, *fr.Auth.AccessStrategy) 126 var executor accessstrategy.Executor 127 executor, err = s.accessStrategyExecutorProvider.Provide(accessstrategy.Type(*fr.Auth.AccessStrategy)) 128 if err != nil { 129 log.C(ctx).WithError(err).Errorf("Cannot find executor for access strategy %q as part of fetch request %s processing: %v", *fr.Auth.AccessStrategy, fr.ID, err) 130 return nil, FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr(fmt.Sprintf("While fetching Spec: %s", err.Error())), s.timestampGen()) 131 } 132 133 doRequest = func() (*http.Response, error) { 134 return executor.Execute(ctx, s.client, fr.URL, localTenantID) 135 } 136 } else if fr.Auth != nil { 137 doRequest = func() (*http.Response, error) { 138 return httputil.GetRequestWithCredentials(ctx, s.client, fr.URL, localTenantID, fr.Auth) 139 } 140 } else { 141 doRequest = func() (*http.Response, error) { 142 return httputil.GetRequestWithoutCredentials(s.client, fr.URL, localTenantID) 143 } 144 } 145 146 var resp *http.Response 147 if s.retryHTTPFuncExecutor != nil { 148 resp, err = s.retryHTTPFuncExecutor.Execute(doRequest) 149 } else { 150 resp, err = doRequest() 151 } 152 153 if err != nil { 154 log.C(ctx).WithError(err).Errorf("An error has occurred while fetching Spec: %v", err) 155 return nil, FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr(fmt.Sprintf("While fetching Spec: %s", err.Error())), s.timestampGen()) 156 } 157 158 defer func() { 159 if resp.Body != nil { 160 err := resp.Body.Close() 161 if err != nil { 162 log.C(ctx).WithError(err).Errorf("An error has occurred while closing response body: %v", err) 163 } 164 } 165 }() 166 167 body, err := io.ReadAll(resp.Body) 168 if err != nil { 169 log.C(ctx).WithError(err).Errorf("An error has occurred while reading Spec: %v", err) 170 return nil, FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr(fmt.Sprintf("While reading Spec: %s", err.Error())), s.timestampGen()) 171 } 172 173 if resp.StatusCode != http.StatusOK { 174 log.C(ctx).Errorf("Failed to execute fetch request for %s with id %q: status code: %d body: %s", fr.ObjectType, fr.ObjectID, resp.StatusCode, string(body)) 175 return nil, FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr(fmt.Sprintf("While fetching Spec status code: %d", resp.StatusCode)), s.timestampGen()) 176 } 177 178 spec := string(body) 179 return &spec, FixStatus(model.FetchRequestStatusConditionSucceeded, nil, s.timestampGen()) 180 } 181 182 func (s *service) validateFetchRequest(fr *model.FetchRequest) error { 183 if fr.Mode != model.FetchModeSingle { 184 return apperrors.NewInvalidDataError("Unsupported fetch mode: %s", fr.Mode) 185 } 186 187 if fr.Filter != nil { 188 return apperrors.NewInvalidDataError("Filter for Fetch Request was provided, currently it's unsupported") 189 } 190 191 return nil 192 } 193 194 // FixStatus missing godoc 195 func FixStatus(condition model.FetchRequestStatusCondition, message *string, timestamp time.Time) *model.FetchRequestStatus { 196 return &model.FetchRequestStatus{ 197 Condition: condition, 198 Message: message, 199 Timestamp: timestamp, 200 } 201 }