github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/api/service.go (about) 1 package api 2 3 import ( 4 "context" 5 "encoding/json" 6 7 "github.com/kyma-incubator/compass/components/director/pkg/log" 8 9 "github.com/kyma-incubator/compass/components/director/pkg/resource" 10 11 ord "github.com/kyma-incubator/compass/components/director/internal/open_resource_discovery" 12 "github.com/kyma-incubator/compass/components/director/pkg/str" 13 14 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 15 16 "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" 17 "github.com/kyma-incubator/compass/components/director/internal/model" 18 "github.com/kyma-incubator/compass/components/director/internal/timestamp" 19 "github.com/pkg/errors" 20 ) 21 22 // APIRepository is responsible for the repo-layer APIDefinition operations. 23 // 24 //go:generate mockery --name=APIRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 25 type APIRepository interface { 26 GetByID(ctx context.Context, tenantID, id string) (*model.APIDefinition, error) 27 GetByIDGlobal(ctx context.Context, id string) (*model.APIDefinition, error) 28 GetForBundle(ctx context.Context, tenant string, id string, bundleID string) (*model.APIDefinition, error) 29 Exists(ctx context.Context, tenant, id string) (bool, error) 30 ListByBundleIDs(ctx context.Context, tenantID string, bundleIDs []string, bundleRefs []*model.BundleReference, counts map[string]int, pageSize int, cursor string) ([]*model.APIDefinitionPage, error) 31 ListByApplicationIDPage(ctx context.Context, tenantID string, appID string, pageSize int, cursor string) (*model.APIDefinitionPage, error) 32 ListByResourceID(ctx context.Context, tenantID string, resourceType resource.Type, resourceID string) ([]*model.APIDefinition, error) 33 CreateMany(ctx context.Context, tenant string, item []*model.APIDefinition) error 34 Create(ctx context.Context, tenant string, item *model.APIDefinition) error 35 CreateGlobal(ctx context.Context, item *model.APIDefinition) error 36 Update(ctx context.Context, tenant string, item *model.APIDefinition) error 37 UpdateGlobal(ctx context.Context, item *model.APIDefinition) error 38 Delete(ctx context.Context, tenantID string, id string) error 39 DeleteAllByBundleID(ctx context.Context, tenantID, bundleID string) error 40 DeleteGlobal(ctx context.Context, id string) error 41 } 42 43 // UIDService is responsible for generating GUIDs, which will be used as internal apiDefinition IDs when they are created. 44 // 45 //go:generate mockery --name=UIDService --output=automock --outpkg=automock --case=underscore --disable-version-string 46 type UIDService interface { 47 Generate() string 48 } 49 50 // SpecService is responsible for the service-layer Specification operations. 51 // 52 //go:generate mockery --name=SpecService --output=automock --outpkg=automock --case=underscore --disable-version-string 53 type SpecService interface { 54 CreateByReferenceObjectID(ctx context.Context, in model.SpecInput, resourceType resource.Type, objectType model.SpecReferenceObjectType, objectID string) (string, error) 55 UpdateByReferenceObjectID(ctx context.Context, id string, in model.SpecInput, resourceType resource.Type, objectType model.SpecReferenceObjectType, objectID string) error 56 GetByReferenceObjectID(ctx context.Context, resourceType resource.Type, objectType model.SpecReferenceObjectType, objectID string) (*model.Spec, error) 57 RefetchSpec(ctx context.Context, id string, objectType model.SpecReferenceObjectType) (*model.Spec, error) 58 ListFetchRequestsByReferenceObjectIDs(ctx context.Context, tenant string, objectIDs []string, objectType model.SpecReferenceObjectType) ([]*model.FetchRequest, error) 59 } 60 61 // BundleReferenceService is responsible for the service-layer BundleReference operations. 62 // 63 //go:generate mockery --name=BundleReferenceService --output=automock --outpkg=automock --case=underscore --disable-version-string 64 type BundleReferenceService interface { 65 GetForBundle(ctx context.Context, objectType model.BundleReferenceObjectType, objectID, bundleID *string) (*model.BundleReference, error) 66 CreateByReferenceObjectID(ctx context.Context, in model.BundleReferenceInput, objectType model.BundleReferenceObjectType, objectID, bundleID *string) error 67 UpdateByReferenceObjectID(ctx context.Context, in model.BundleReferenceInput, objectType model.BundleReferenceObjectType, objectID, bundleID *string) error 68 DeleteByReferenceObjectID(ctx context.Context, objectType model.BundleReferenceObjectType, objectID, bundleID *string) error 69 ListByBundleIDs(ctx context.Context, objectType model.BundleReferenceObjectType, bundleIDs []string, pageSize int, cursor string) ([]*model.BundleReference, map[string]int, error) 70 } 71 72 type service struct { 73 repo APIRepository 74 uidService UIDService 75 specService SpecService 76 bundleReferenceService BundleReferenceService 77 timestampGen timestamp.Generator 78 } 79 80 // NewService returns a new object responsible for service-layer APIDefinition operations. 81 func NewService(repo APIRepository, uidService UIDService, specService SpecService, bundleReferenceService BundleReferenceService) *service { 82 return &service{ 83 repo: repo, 84 uidService: uidService, 85 specService: specService, 86 bundleReferenceService: bundleReferenceService, 87 timestampGen: timestamp.DefaultGenerator, 88 } 89 } 90 91 // ListByBundleIDs lists all APIDefinitions in pages for a given array of bundle IDs. 92 func (s *service) ListByBundleIDs(ctx context.Context, bundleIDs []string, pageSize int, cursor string) ([]*model.APIDefinitionPage, error) { 93 tnt, err := tenant.LoadFromContext(ctx) 94 if err != nil { 95 return nil, err 96 } 97 98 if pageSize < 1 || pageSize > 200 { 99 return nil, apperrors.NewInvalidDataError("page size must be between 1 and 200") 100 } 101 102 bundleRefs, counts, err := s.bundleReferenceService.ListByBundleIDs(ctx, model.BundleAPIReference, bundleIDs, pageSize, cursor) 103 if err != nil { 104 return nil, err 105 } 106 107 return s.repo.ListByBundleIDs(ctx, tnt, bundleIDs, bundleRefs, counts, pageSize, cursor) 108 } 109 110 // ListByApplicationID lists all APIDefinitions for a given application ID. 111 func (s *service) ListByApplicationID(ctx context.Context, appID string) ([]*model.APIDefinition, error) { 112 tnt, err := tenant.LoadFromContext(ctx) 113 if err != nil { 114 return nil, err 115 } 116 117 return s.repo.ListByResourceID(ctx, tnt, resource.Application, appID) 118 } 119 120 // ListByApplicationTemplateVersionID lists all APIDefinitions for a given application template version ID. 121 func (s *service) ListByApplicationTemplateVersionID(ctx context.Context, appTemplateVersionID string) ([]*model.APIDefinition, error) { 122 return s.repo.ListByResourceID(ctx, "", resource.ApplicationTemplateVersion, appTemplateVersionID) 123 } 124 125 // ListByApplicationIDPage lists all APIDefinitions for a given application ID with paging. 126 func (s *service) ListByApplicationIDPage(ctx context.Context, appID string, pageSize int, cursor string) (*model.APIDefinitionPage, error) { 127 tnt, err := tenant.LoadFromContext(ctx) 128 if err != nil { 129 return nil, err 130 } 131 132 if pageSize < 1 || pageSize > 200 { 133 return nil, apperrors.NewInvalidDataError("page size must be between 1 and 200") 134 } 135 136 return s.repo.ListByApplicationIDPage(ctx, tnt, appID, pageSize, cursor) 137 } 138 139 // Get returns the APIDefinition by its ID. 140 func (s *service) Get(ctx context.Context, id string) (*model.APIDefinition, error) { 141 tnt, err := tenant.LoadFromContext(ctx) 142 if err != nil { 143 return nil, err 144 } 145 146 api, err := s.repo.GetByID(ctx, tnt, id) 147 if err != nil { 148 return nil, err 149 } 150 151 return api, nil 152 } 153 154 // GetForBundle returns an APIDefinition by its ID and a bundle ID. 155 func (s *service) GetForBundle(ctx context.Context, id string, bundleID string) (*model.APIDefinition, error) { 156 tnt, err := tenant.LoadFromContext(ctx) 157 if err != nil { 158 return nil, err 159 } 160 161 apiDefinition, err := s.repo.GetForBundle(ctx, tnt, id, bundleID) 162 if err != nil { 163 return nil, errors.Wrapf(err, "while getting API definition with id %q", id) 164 } 165 166 return apiDefinition, nil 167 } 168 169 // CreateInBundle creates an APIDefinition. This function is used in the graphQL flow. 170 func (s *service) CreateInBundle(ctx context.Context, resourceType resource.Type, resourceID string, bundleID string, in model.APIDefinitionInput, spec *model.SpecInput) (string, error) { 171 return s.Create(ctx, resourceType, resourceID, &bundleID, nil, in, []*model.SpecInput{spec}, nil, 0, "") 172 } 173 174 // CreateInApplication creates an APIDefinition in the context of an Application without Bundle 175 func (s *service) CreateInApplication(ctx context.Context, appID string, in model.APIDefinitionInput, spec *model.SpecInput) (string, error) { 176 return s.Create(ctx, resource.Application, appID, nil, nil, in, []*model.SpecInput{spec}, nil, 0, "") 177 } 178 179 // Create creates APIDefinition/s. This function is used both in the ORD scenario and is re-used in CreateInBundle but with "null" ORD specific arguments. 180 func (s *service) Create(ctx context.Context, resourceType resource.Type, resourceID string, bundleID, packageID *string, in model.APIDefinitionInput, specs []*model.SpecInput, defaultTargetURLPerBundle map[string]string, apiHash uint64, defaultBundleID string) (string, error) { 181 id := s.uidService.Generate() 182 api := in.ToAPIDefinition(id, resourceType, resourceID, packageID, apiHash) 183 184 enrichAPIProtocol(api, specs) 185 186 if err := s.createAPI(ctx, api, resourceType); err != nil { 187 return "", errors.Wrap(err, "while creating api") 188 } 189 190 if err := s.processSpecs(ctx, api.ID, specs, resourceType); err != nil { 191 return "", errors.Wrap(err, "while processing specs") 192 } 193 194 if err := s.createBundleReferenceObject(ctx, api.ID, bundleID, defaultBundleID, api.TargetURLs, defaultTargetURLPerBundle); err != nil { 195 return "", errors.Wrap(err, "while creating bundle reference object") 196 } 197 198 return id, nil 199 } 200 201 // Update updates an APIDefinition. This function is used in the graphQL flow. 202 func (s *service) Update(ctx context.Context, resourceType resource.Type, id string, in model.APIDefinitionInput, specIn *model.SpecInput) error { 203 return s.UpdateInManyBundles(ctx, resourceType, id, in, specIn, nil, nil, nil, 0, "") 204 } 205 206 // UpdateInManyBundles updates APIDefinition/s. This function is used both in the ORD scenario and is re-used in Update but with "null" ORD specific arguments. 207 func (s *service) UpdateInManyBundles(ctx context.Context, resourceType resource.Type, id string, in model.APIDefinitionInput, specIn *model.SpecInput, defaultTargetURLPerBundleForUpdate map[string]string, defaultTargetURLPerBundleForCreation map[string]string, bundleIDsForDeletion []string, apiHash uint64, defaultBundleID string) error { 208 api, err := s.getAPI(ctx, id, resourceType) 209 if err != nil { 210 return errors.Wrapf(err, "while getting API with ID %s for %s", id, resourceType) 211 } 212 213 resourceID := getParentResourceID(api) 214 api = in.ToAPIDefinition(id, resourceType, resourceID, api.PackageID, apiHash) 215 216 err = s.updateAPI(ctx, api, resourceType) 217 if err != nil { 218 return errors.Wrapf(err, "while updating API with ID %s for %s", id, resourceType) 219 } 220 221 if err = s.updateReferences(ctx, api, in.TargetURLs, defaultTargetURLPerBundleForUpdate, defaultBundleID); err != nil { 222 return err 223 } 224 225 if err = s.createBundleReferences(ctx, api, defaultTargetURLPerBundleForCreation, defaultBundleID); err != nil { 226 return err 227 } 228 229 if err = s.deleteBundleIDs(ctx, &api.ID, bundleIDsForDeletion); err != nil { 230 return err 231 } 232 233 if specIn != nil { 234 return s.handleSpecsInAPI(ctx, api.ID, specIn, resourceType) 235 } 236 237 return nil 238 } 239 240 // UpdateForApplication updates an APIDefinition for Application without being in a Bundle 241 func (s *service) UpdateForApplication(ctx context.Context, id string, in model.APIDefinitionInput, specIn *model.SpecInput) error { 242 tnt, err := tenant.LoadFromContext(ctx) 243 if err != nil { 244 return err 245 } 246 247 api, err := s.Get(ctx, id) 248 if err != nil { 249 return err 250 } 251 252 api = in.ToAPIDefinition(id, resource.Application, str.PtrStrToStr(api.ApplicationID), api.PackageID, 0) 253 254 if err = s.repo.Update(ctx, tnt, api); err != nil { 255 return errors.Wrapf(err, "while updating APIDefinition with id %s", id) 256 } 257 258 if specIn != nil { 259 return s.handleSpecsInAPI(ctx, id, specIn, resource.Application) 260 } 261 262 return nil 263 } 264 265 // Delete deletes the APIDefinition by its ID. 266 func (s *service) Delete(ctx context.Context, resourceType resource.Type, id string) error { 267 if err := s.deleteAPI(ctx, id, resourceType); err != nil { 268 return errors.Wrapf(err, "while deleting APIDefinition with id %s", id) 269 } 270 271 log.C(ctx).Infof("Successfully deleted APIDefinition with id %s", id) 272 273 return nil 274 } 275 276 // DeleteAllByBundleID deletes all APIDefinitions for a given bundle ID 277 func (s *service) DeleteAllByBundleID(ctx context.Context, bundleID string) error { 278 tnt, err := tenant.LoadFromContext(ctx) 279 if err != nil { 280 return err 281 } 282 283 err = s.repo.DeleteAllByBundleID(ctx, tnt, bundleID) 284 if err != nil { 285 return errors.Wrapf(err, "while deleting APIDefinitions for Bundle with id %q", bundleID) 286 } 287 288 return nil 289 } 290 291 // ListFetchRequests lists all FetchRequests for given specification IDs 292 func (s *service) ListFetchRequests(ctx context.Context, specIDs []string) ([]*model.FetchRequest, error) { 293 tnt, err := tenant.LoadFromContext(ctx) 294 if err != nil { 295 return nil, err 296 } 297 298 fetchRequests, err := s.specService.ListFetchRequestsByReferenceObjectIDs(ctx, tnt, specIDs, model.APISpecReference) 299 if err != nil { 300 if apperrors.IsNotFoundError(err) { 301 return nil, nil 302 } 303 return nil, err 304 } 305 306 return fetchRequests, nil 307 } 308 309 func (s *service) updateBundleReferences(ctx context.Context, api *model.APIDefinition, defaultTargetURLPerBundleForUpdate map[string]string, defaultBundleID string) error { 310 for crrBndlID, defaultTargetURL := range defaultTargetURLPerBundleForUpdate { 311 bundleRefInput := &model.BundleReferenceInput{ 312 APIDefaultTargetURL: &defaultTargetURL, 313 } 314 if defaultBundleID != "" && defaultBundleID == crrBndlID { 315 isDefaultBundle := true 316 bundleRefInput.IsDefaultBundle = &isDefaultBundle 317 } 318 319 err := s.bundleReferenceService.UpdateByReferenceObjectID(ctx, *bundleRefInput, model.BundleAPIReference, &api.ID, &crrBndlID) 320 if err != nil { 321 return err 322 } 323 } 324 return nil 325 } 326 327 func (s *service) createBundleReferences(ctx context.Context, api *model.APIDefinition, defaultTargetURLPerBundleForCreation map[string]string, defaultBundleID string) error { 328 for crrBndlID, defaultTargetURL := range defaultTargetURLPerBundleForCreation { 329 bundleRefInput := &model.BundleReferenceInput{ 330 APIDefaultTargetURL: &defaultTargetURL, 331 } 332 if defaultBundleID != "" && crrBndlID == defaultBundleID { 333 isDefaultBundle := true 334 bundleRefInput.IsDefaultBundle = &isDefaultBundle 335 } 336 337 err := s.bundleReferenceService.CreateByReferenceObjectID(ctx, *bundleRefInput, model.BundleAPIReference, &api.ID, &crrBndlID) 338 if err != nil { 339 return err 340 } 341 } 342 return nil 343 } 344 345 func (s *service) deleteBundleIDs(ctx context.Context, apiID *string, bundleIDsForDeletion []string) error { 346 for _, bundleID := range bundleIDsForDeletion { 347 err := s.bundleReferenceService.DeleteByReferenceObjectID(ctx, model.BundleAPIReference, apiID, &bundleID) 348 if err != nil { 349 return err 350 } 351 } 352 return nil 353 } 354 355 func (s *service) handleSpecsInAPI(ctx context.Context, id string, specIn *model.SpecInput, resourceType resource.Type) error { 356 dbSpec, err := s.specService.GetByReferenceObjectID(ctx, resourceType, model.APISpecReference, id) 357 if err != nil { 358 return errors.Wrapf(err, "while getting spec for APIDefinition with id %q", id) 359 } 360 361 if dbSpec == nil { 362 _, err = s.specService.CreateByReferenceObjectID(ctx, *specIn, resourceType, model.APISpecReference, id) 363 return err 364 } 365 366 return s.specService.UpdateByReferenceObjectID(ctx, dbSpec.ID, *specIn, resourceType, model.APISpecReference, id) 367 } 368 369 func (s *service) updateReferences(ctx context.Context, api *model.APIDefinition, targetURLs json.RawMessage, defaultTargetURLPerBundleForUpdate map[string]string, defaultBundleID string) error { 370 // when defaultTargetURLPerBundle == nil we are in the graphQL flow 371 if defaultTargetURLPerBundleForUpdate == nil { 372 bundleRefInput := &model.BundleReferenceInput{ 373 APIDefaultTargetURL: str.Ptr(ExtractTargetURLFromJSONArray(targetURLs)), 374 } 375 return s.bundleReferenceService.UpdateByReferenceObjectID(ctx, *bundleRefInput, model.BundleAPIReference, &api.ID, nil) 376 } 377 378 return s.updateBundleReferences(ctx, api, defaultTargetURLPerBundleForUpdate, defaultBundleID) 379 } 380 381 func (s *service) processSpecs(ctx context.Context, apiID string, specs []*model.SpecInput, resourceType resource.Type) error { 382 for _, spec := range specs { 383 if spec == nil { 384 continue 385 } 386 387 if _, err := s.specService.CreateByReferenceObjectID(ctx, *spec, resourceType, model.APISpecReference, apiID); err != nil { 388 return err 389 } 390 } 391 392 return nil 393 } 394 395 func (s *service) createBundleReferenceObject(ctx context.Context, apiID string, bundleID *string, defaultBundleID string, targetURLs json.RawMessage, defaultTargetURLPerBundle map[string]string) error { 396 // when defaultTargetURLPerBundle == nil we are in the graphQL flow 397 if defaultTargetURLPerBundle == nil && bundleID != nil { 398 bundleRefInput := &model.BundleReferenceInput{ 399 APIDefaultTargetURL: str.Ptr(ExtractTargetURLFromJSONArray(targetURLs)), 400 } 401 return s.bundleReferenceService.CreateByReferenceObjectID(ctx, *bundleRefInput, model.BundleAPIReference, &apiID, bundleID) 402 } 403 404 for crrBndlID, defaultTargetURL := range defaultTargetURLPerBundle { 405 bundleRefInput := &model.BundleReferenceInput{ 406 APIDefaultTargetURL: &defaultTargetURL, 407 } 408 if defaultBundleID != "" && crrBndlID == defaultBundleID { 409 isDefaultBundle := true 410 bundleRefInput.IsDefaultBundle = &isDefaultBundle 411 } 412 if err := s.bundleReferenceService.CreateByReferenceObjectID(ctx, *bundleRefInput, model.BundleAPIReference, &apiID, &crrBndlID); err != nil { 413 return err 414 } 415 } 416 417 return nil 418 } 419 420 func (s *service) getAPI(ctx context.Context, id string, resourceType resource.Type) (*model.APIDefinition, error) { 421 if resourceType.IsTenantIgnorable() { 422 return s.repo.GetByIDGlobal(ctx, id) 423 } 424 return s.Get(ctx, id) 425 } 426 427 func (s *service) updateAPI(ctx context.Context, api *model.APIDefinition, resourceType resource.Type) error { 428 if resourceType.IsTenantIgnorable() { 429 return s.repo.UpdateGlobal(ctx, api) 430 } 431 432 tnt, err := tenant.LoadFromContext(ctx) 433 if err != nil { 434 return err 435 } 436 return s.repo.Update(ctx, tnt, api) 437 } 438 439 func (s *service) createAPI(ctx context.Context, api *model.APIDefinition, resourceType resource.Type) error { 440 if resourceType.IsTenantIgnorable() { 441 return s.repo.CreateGlobal(ctx, api) 442 } 443 444 tnt, err := tenant.LoadFromContext(ctx) 445 if err != nil { 446 return err 447 } 448 449 return s.repo.Create(ctx, tnt, api) 450 } 451 452 func (s *service) deleteAPI(ctx context.Context, apiID string, resourceType resource.Type) error { 453 if resourceType.IsTenantIgnorable() { 454 return s.repo.DeleteGlobal(ctx, apiID) 455 } 456 457 tnt, err := tenant.LoadFromContext(ctx) 458 if err != nil { 459 return err 460 } 461 return s.repo.Delete(ctx, tnt, apiID) 462 } 463 464 func getParentResourceID(api *model.APIDefinition) string { 465 if api.ApplicationTemplateVersionID != nil { 466 return *api.ApplicationTemplateVersionID 467 } else if api.ApplicationID != nil { 468 return *api.ApplicationID 469 } 470 471 return "" 472 } 473 474 func enrichAPIProtocol(api *model.APIDefinition, specs []*model.SpecInput) { 475 if len(specs) > 0 && specs[0] != nil && specs[0].APIType != nil { 476 switch *specs[0].APIType { 477 case model.APISpecTypeOdata: 478 protocol := ord.APIProtocolODataV2 479 api.APIProtocol = &protocol 480 case model.APISpecTypeOpenAPI: 481 protocol := ord.APIProtocolRest 482 api.APIProtocol = &protocol 483 } 484 } 485 }