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  }