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 }