github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/webhook/service.go (about)

     1  package webhook
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/kyma-incubator/compass/components/director/pkg/graphql"
    10  
    11  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    12  
    13  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    14  
    15  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    16  
    17  	"github.com/kyma-incubator/compass/components/director/internal/domain/tenant"
    18  
    19  	"github.com/kyma-incubator/compass/components/director/internal/model"
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  // WebhookRepository missing godoc
    24  //
    25  //go:generate mockery --name=WebhookRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    26  type WebhookRepository interface {
    27  	GetByID(ctx context.Context, tenant, id string, objectType model.WebhookReferenceObjectType) (*model.Webhook, error)
    28  	GetByIDGlobal(ctx context.Context, id string) (*model.Webhook, error)
    29  	ListByReferenceObjectID(ctx context.Context, tenant, objID string, objType model.WebhookReferenceObjectType) ([]*model.Webhook, error)
    30  	ListByReferenceObjectIDGlobal(ctx context.Context, objID string, objType model.WebhookReferenceObjectType) ([]*model.Webhook, error)
    31  	ListByWebhookType(ctx context.Context, webhookType model.WebhookType) ([]*model.Webhook, error)
    32  	ListByApplicationTemplateID(ctx context.Context, applicationTemplateID string) ([]*model.Webhook, error)
    33  	Create(ctx context.Context, tenant string, item *model.Webhook) error
    34  	Update(ctx context.Context, tenant string, item *model.Webhook) error
    35  	Delete(ctx context.Context, id string) error
    36  }
    37  
    38  // ApplicationRepository missing godoc
    39  //
    40  //go:generate mockery --name=ApplicationRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    41  type ApplicationRepository interface {
    42  	GetGlobalByID(ctx context.Context, id string) (*model.Application, error)
    43  }
    44  
    45  // UIDService missing godoc
    46  //
    47  //go:generate mockery --name=UIDService --output=automock --outpkg=automock --case=underscore --disable-version-string
    48  type UIDService interface {
    49  	Generate() string
    50  }
    51  
    52  // TenantService is responsible for service-layer tenant operations
    53  //
    54  //go:generate mockery --name=TenantService --output=automock --outpkg=automock --case=underscore --disable-version-string
    55  type TenantService interface {
    56  	ExtractTenantIDForTenantScopedFormationTemplates(ctx context.Context) (string, error)
    57  }
    58  
    59  // OwningResource missing godoc
    60  type OwningResource string
    61  
    62  type service struct {
    63  	webhookRepo              WebhookRepository
    64  	appRepo                  ApplicationRepository
    65  	uidSvc                   UIDService
    66  	tenantSvc                TenantService
    67  	tenantMappingConfig      map[string]interface{}
    68  	tenantMappingCallbackURL string
    69  }
    70  
    71  // NewService missing godoc
    72  func NewService(repo WebhookRepository, appRepo ApplicationRepository, uidSvc UIDService, tenantSvc TenantService, tenantMappingConfig map[string]interface{}, tenantMappingCallbackURL string) *service {
    73  	return &service{
    74  		webhookRepo:              repo,
    75  		uidSvc:                   uidSvc,
    76  		appRepo:                  appRepo,
    77  		tenantSvc:                tenantSvc,
    78  		tenantMappingConfig:      tenantMappingConfig,
    79  		tenantMappingCallbackURL: tenantMappingCallbackURL,
    80  	}
    81  }
    82  
    83  // Get missing godoc
    84  func (s *service) Get(ctx context.Context, id string, objectType model.WebhookReferenceObjectType) (webhook *model.Webhook, err error) {
    85  	tnt, err := tenant.LoadFromContext(ctx)
    86  	if err != nil || tnt == "" {
    87  		log.C(ctx).Infof("tenant was not loaded while getting Webhook id %s", id)
    88  		webhook, err = s.webhookRepo.GetByIDGlobal(ctx, id)
    89  	} else {
    90  		webhook, err = s.webhookRepo.GetByID(ctx, tnt, id, objectType)
    91  	}
    92  	if err != nil {
    93  		return nil, errors.Wrapf(err, "while getting Webhook with ID %s", id)
    94  	}
    95  	return
    96  }
    97  
    98  // ListForApplication missing godoc
    99  func (s *service) ListForApplication(ctx context.Context, applicationID string) ([]*model.Webhook, error) {
   100  	tnt, err := tenant.LoadFromContext(ctx)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	return s.webhookRepo.ListByReferenceObjectID(ctx, tnt, applicationID, model.ApplicationWebhookReference)
   105  }
   106  
   107  // ListForApplicationGlobal lists all webhooks for application without tenant restrictions
   108  func (s *service) ListForApplicationGlobal(ctx context.Context, applicationID string) ([]*model.Webhook, error) {
   109  	return s.webhookRepo.ListByReferenceObjectIDGlobal(ctx, applicationID, model.ApplicationWebhookReference)
   110  }
   111  
   112  // ListByWebhookType lists all webhooks with given webhook type
   113  func (s *service) ListByWebhookType(ctx context.Context, webhookType model.WebhookType) ([]*model.Webhook, error) {
   114  	return s.webhookRepo.ListByWebhookType(ctx, webhookType)
   115  }
   116  
   117  // ListForApplicationTemplate missing godoc
   118  func (s *service) ListForApplicationTemplate(ctx context.Context, applicationTemplateID string) ([]*model.Webhook, error) {
   119  	return s.webhookRepo.ListByApplicationTemplateID(ctx, applicationTemplateID)
   120  }
   121  
   122  // ListAllApplicationWebhooks missing godoc
   123  func (s *service) ListAllApplicationWebhooks(ctx context.Context, applicationID string) ([]*model.Webhook, error) {
   124  	application, err := s.appRepo.GetGlobalByID(ctx, applicationID)
   125  	if err != nil {
   126  		return nil, errors.Wrapf(err, "while getting Application with ID %s", applicationID)
   127  	}
   128  
   129  	return s.retrieveWebhooks(ctx, application)
   130  }
   131  
   132  // ListForRuntime missing godoc
   133  func (s *service) ListForRuntime(ctx context.Context, runtimeID string) ([]*model.Webhook, error) {
   134  	tnt, err := tenant.LoadFromContext(ctx)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	return s.webhookRepo.ListByReferenceObjectID(ctx, tnt, runtimeID, model.RuntimeWebhookReference)
   139  }
   140  
   141  // ListForFormationTemplate lists all webhooks for a given formationTemplateID
   142  func (s *service) ListForFormationTemplate(ctx context.Context, tenant, formationTemplateID string) ([]*model.Webhook, error) {
   143  	if tenant == "" {
   144  		log.C(ctx).Infof("tenant was not loaded while getting webhooks for formation template with id %s", formationTemplateID)
   145  		return s.webhookRepo.ListByReferenceObjectIDGlobal(ctx, formationTemplateID, model.FormationTemplateWebhookReference)
   146  	}
   147  	return s.webhookRepo.ListByReferenceObjectID(ctx, tenant, formationTemplateID, model.FormationTemplateWebhookReference)
   148  }
   149  
   150  // Create creates a model.Webhook with generated ID and CreatedAt properties. Returns the ID of the webhook.
   151  func (s *service) Create(ctx context.Context, owningResourceID string, in model.WebhookInput, objectType model.WebhookReferenceObjectType) (string, error) {
   152  	tenantID, err := s.getTenantForWebhook(ctx, objectType.GetResourceType())
   153  	if apperrors.IsTenantRequired(err) {
   154  		log.C(ctx).Debugf("Creating Webhook with type: %q without tenant", in.Type)
   155  	} else if err != nil {
   156  		return "", err
   157  	}
   158  
   159  	id := s.uidSvc.Generate()
   160  
   161  	webhook := in.ToWebhook(id, owningResourceID, objectType)
   162  
   163  	if err = s.webhookRepo.Create(ctx, tenantID, webhook); err != nil {
   164  		return "", errors.Wrapf(err, "while creating %s with type: %q and ID: %q for: %q", objectType, webhook.Type, id, owningResourceID)
   165  	}
   166  	log.C(ctx).Infof("Successfully created %s with type: %q and ID: %q for: %q", objectType, webhook.Type, id, owningResourceID)
   167  
   168  	return webhook.ID, nil
   169  }
   170  
   171  // Update missing godoc
   172  func (s *service) Update(ctx context.Context, id string, in model.WebhookInput, objectType model.WebhookReferenceObjectType) error {
   173  	tnt, err := tenant.LoadFromContext(ctx)
   174  	if err != nil && objectType.GetResourceType() != resource.Webhook { // If the webhook is not global
   175  		return err
   176  	}
   177  	webhook, err := s.Get(ctx, id, objectType)
   178  	if err != nil {
   179  		return errors.Wrap(err, "while getting Webhook")
   180  	}
   181  
   182  	if len(webhook.ObjectID) == 0 || (webhook.ObjectType != model.ApplicationWebhookReference && webhook.ObjectType != model.ApplicationTemplateWebhookReference && webhook.ObjectType != model.RuntimeWebhookReference && webhook.ObjectType != model.FormationTemplateWebhookReference) {
   183  		return errors.New("while updating Webhook: webhook doesn't have neither of application_id, application_template_id, runtime_id and formation_template_id")
   184  	}
   185  
   186  	webhook = in.ToWebhook(id, webhook.ObjectID, webhook.ObjectType)
   187  
   188  	if err = s.webhookRepo.Update(ctx, tnt, webhook); err != nil {
   189  		return errors.Wrapf(err, "while updating Webhook")
   190  	}
   191  
   192  	return nil
   193  }
   194  
   195  // Delete missing godoc
   196  func (s *service) Delete(ctx context.Context, id string, objectType model.WebhookReferenceObjectType) error {
   197  	log.C(ctx).Infof("Deleting webhook with id %q", id)
   198  	webhook, err := s.Get(ctx, id, objectType)
   199  	if err != nil {
   200  		return errors.Wrap(err, "while getting Webhook")
   201  	}
   202  
   203  	return s.webhookRepo.Delete(ctx, webhook.ID)
   204  }
   205  
   206  // EnrichWebhooksWithTenantMappingWebhooks enriches webhook inputs with tenant mapping webhooks based on the tenant mapping
   207  // configuration. In order to be enriched, the input webhooks should contain Version, URL and Mode
   208  func (s *service) EnrichWebhooksWithTenantMappingWebhooks(in []*graphql.WebhookInput) ([]*graphql.WebhookInput, error) {
   209  	webhooks := make([]*graphql.WebhookInput, 0)
   210  	for _, w := range in {
   211  		if w.Version == nil {
   212  			webhooks = append(webhooks, w)
   213  			continue
   214  		}
   215  
   216  		if w.URL == nil || w.Mode == nil {
   217  			return nil, errors.New("url and mode are required fields when version is provided")
   218  		}
   219  		tenantMappingWebhooks, err := s.getTenantMappingWebhooks(w.Mode.String(), *w.Version)
   220  		if err != nil {
   221  			return nil, err
   222  		}
   223  		for _, tenantMappingWebhook := range tenantMappingWebhooks {
   224  			urlTemplate := *tenantMappingWebhook.URLTemplate
   225  			if strings.Contains(urlTemplate, "%s") {
   226  				urlTemplate = fmt.Sprintf(*tenantMappingWebhook.URLTemplate, *w.URL)
   227  			}
   228  
   229  			headerTemplate := *tenantMappingWebhook.HeaderTemplate
   230  			if *w.Mode == graphql.WebhookModeAsyncCallback && strings.Contains(headerTemplate, "%s") {
   231  				headerTemplate = fmt.Sprintf(*tenantMappingWebhook.HeaderTemplate, s.tenantMappingCallbackURL)
   232  			}
   233  			wh := &graphql.WebhookInput{
   234  				Type:           tenantMappingWebhook.Type,
   235  				Auth:           w.Auth,
   236  				Mode:           w.Mode,
   237  				URLTemplate:    &urlTemplate,
   238  				InputTemplate:  tenantMappingWebhook.InputTemplate,
   239  				HeaderTemplate: &headerTemplate,
   240  				OutputTemplate: tenantMappingWebhook.OutputTemplate,
   241  			}
   242  			webhooks = append(webhooks, wh)
   243  		}
   244  	}
   245  	return webhooks, nil
   246  }
   247  
   248  func (s *service) getTenantMappingWebhooks(mode, version string) ([]graphql.WebhookInput, error) {
   249  	modeObj, ok := s.tenantMappingConfig[mode]
   250  	if !ok {
   251  		return nil, errors.Errorf("missing tenant mapping configuration for mode %s", mode)
   252  	}
   253  	modeMap, ok := modeObj.(map[string]interface{})
   254  	if !ok {
   255  		return nil, errors.Errorf("unexpected mode type, should be a map, but was %T", mode)
   256  	}
   257  	webhooks, ok := modeMap[version]
   258  	if !ok {
   259  		return nil, errors.Errorf("missing tenant mapping configuration for mode %s and version %s", mode, version)
   260  	}
   261  
   262  	webhooksJSON, err := json.Marshal(webhooks)
   263  	if err != nil {
   264  		return nil, errors.Wrap(err, "while marshaling webhooks")
   265  	}
   266  
   267  	var tenantMappingWebhooks []graphql.WebhookInput
   268  	if err := json.Unmarshal(webhooksJSON, &tenantMappingWebhooks); err != nil {
   269  		return nil, errors.Wrap(err, "while unmarshaling webhooks")
   270  	}
   271  
   272  	return tenantMappingWebhooks, nil
   273  }
   274  
   275  func (s *service) retrieveWebhooks(ctx context.Context, application *model.Application) ([]*model.Webhook, error) {
   276  	appWebhooks, err := s.ListForApplication(ctx, application.ID)
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  
   281  	var appTemplateWebhooks []*model.Webhook
   282  	if application.ApplicationTemplateID != nil {
   283  		appTemplateWebhooks, err = s.ListForApplicationTemplate(ctx, *application.ApplicationTemplateID)
   284  		if err != nil {
   285  			return nil, err
   286  		}
   287  	}
   288  
   289  	webhooksMap := make(map[model.WebhookType]*model.Webhook)
   290  	for i, webhook := range appTemplateWebhooks {
   291  		webhooksMap[webhook.Type] = appTemplateWebhooks[i]
   292  	}
   293  	// Override values derived from template
   294  	for i, webhook := range appWebhooks {
   295  		webhooksMap[webhook.Type] = appWebhooks[i]
   296  	}
   297  
   298  	webhooks := make([]*model.Webhook, 0)
   299  	for key := range webhooksMap {
   300  		webhooks = append(webhooks, webhooksMap[key])
   301  	}
   302  
   303  	return webhooks, nil
   304  }
   305  
   306  func (s *service) getTenantForWebhook(ctx context.Context, whType resource.Type) (string, error) {
   307  	if whType == resource.FormationTemplateWebhook {
   308  		return s.tenantSvc.ExtractTenantIDForTenantScopedFormationTemplates(ctx)
   309  	}
   310  	return tenant.LoadFromContext(ctx)
   311  }