diff --git a/src/AeFinder.Application.Contracts/AeFinderApplicationConsts.cs b/src/AeFinder.Application.Contracts/AeFinderApplicationConsts.cs index 9547286ff..5e53c0e96 100644 --- a/src/AeFinder.Application.Contracts/AeFinderApplicationConsts.cs +++ b/src/AeFinder.Application.Contracts/AeFinderApplicationConsts.cs @@ -16,6 +16,7 @@ public class AeFinderApplicationConsts public const int DefaultAssetExpiration = 100; // 100 years public const string RegisterEmailTemplate = "Register"; public const string AdminRoleName = "admin"; + public const string AppStorageResourceName = "Storage"; public static readonly HashSet AppInterestedExtraPropertiesKey = new HashSet { "RefBlockNumber", "RefBlockPrefix", "ReturnValue", "Error", "TransactionFee", "ResourceFee" }; diff --git a/src/AeFinder.Application.Contracts/AppResources/Dto/AppFullPodResourceUsageDto.cs b/src/AeFinder.Application.Contracts/AppResources/Dto/AppFullPodResourceUsageDto.cs index 866483dc3..4d52ce9c5 100644 --- a/src/AeFinder.Application.Contracts/AppResources/Dto/AppFullPodResourceUsageDto.cs +++ b/src/AeFinder.Application.Contracts/AppResources/Dto/AppFullPodResourceUsageDto.cs @@ -4,10 +4,10 @@ public class AppFullPodResourceUsageDto { public string AppId { get; set; } public string AppVersion { get; set; } - public string ContainerName { get; set; } + // public string ContainerName { get; set; } public string CurrentState { get; set; } - public string RequestCpu { get; set; } - public string RequestMemory { get; set; } + // public string RequestCpu { get; set; } + // public string RequestMemory { get; set; } public string LimitCpu { get; set; } public string LimitMemory { get; set; } public long UsageTimestamp { get; set; } diff --git a/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs b/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs new file mode 100644 index 000000000..edc6a1a5c --- /dev/null +++ b/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using AeFinder.Apps; + +namespace AeFinder.AppResources.Dto; + +public class AppResourceUsageDto +{ + public AppInfoImmutable AppInfo { get; set; } + public Guid OrganizationId { get; set; } + public Dictionary> ResourceUsages { get; set; } = new(); +} + +public class ResourceUsageDto +{ + public string Name { get; set; } + public string Limit { get; set; } + public string Usage { get; set; } +} \ No newline at end of file diff --git a/src/AeFinder.Application.Contracts/AppResources/Dto/GetAppResourceUsageInput.cs b/src/AeFinder.Application.Contracts/AppResources/Dto/GetAppResourceUsageInput.cs new file mode 100644 index 000000000..1b2b803af --- /dev/null +++ b/src/AeFinder.Application.Contracts/AppResources/Dto/GetAppResourceUsageInput.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Application.Dtos; + +namespace AeFinder.AppResources.Dto; + +public class GetAppResourceUsageInput: PagedResultRequestDto +{ + public string AppId { get; set; } +} \ No newline at end of file diff --git a/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs b/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs new file mode 100644 index 000000000..1dfbcf2a4 --- /dev/null +++ b/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AeFinder.AppResources.Dto; +using Volo.Abp.Application.Dtos; + +namespace AeFinder.AppResources; + +public interface IAppResourceUsageService +{ + Task AddOrUpdateAsync(List input); + Task DeleteAsync(string appId); + Task GetAsync(Guid? organizationId, string appId); + Task> GetListAsync(Guid? organizationId, GetAppResourceUsageInput input); +} \ No newline at end of file diff --git a/src/AeFinder.Application/AeFinderApplicationAutoMapperProfile.cs b/src/AeFinder.Application/AeFinderApplicationAutoMapperProfile.cs index 663c10f19..956a9b222 100644 --- a/src/AeFinder.Application/AeFinderApplicationAutoMapperProfile.cs +++ b/src/AeFinder.Application/AeFinderApplicationAutoMapperProfile.cs @@ -250,5 +250,11 @@ public AeFinderApplicationAutoMapperProfile() CreateMap(); CreateMap(); + + // AppResourceUsage + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); } } \ No newline at end of file diff --git a/src/AeFinder.Application/AppResources/AppResourceUsageService.cs b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs new file mode 100644 index 000000000..16755f50d --- /dev/null +++ b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AeFinder.App.Es; +using AeFinder.AppResources.Dto; +using AElf.EntityMapping.Repositories; +using Volo.Abp; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Auditing; +using System.Linq; + +namespace AeFinder.AppResources; + +[RemoteService(IsEnabled = false)] +[DisableAuditing] +public class AppResourceUsageService : AeFinderAppService, IAppResourceUsageService +{ + private readonly IEntityMappingRepository _entityMappingRepository; + + public AppResourceUsageService(IEntityMappingRepository entityMappingRepository) + { + _entityMappingRepository = entityMappingRepository; + } + + public async Task AddOrUpdateAsync(List input) + { + var index = ObjectMapper.Map, List>(input); + await _entityMappingRepository.AddOrUpdateManyAsync(index); + } + + public async Task DeleteAsync(string appId) + { + await _entityMappingRepository.DeleteAsync(appId); + } + + public async Task> GetListAsync(Guid? organizationId, + GetAppResourceUsageInput input) + { + var queryable = await _entityMappingRepository.GetQueryableAsync(); + if (organizationId.HasValue) + { + queryable = queryable.Where(o => o.OrganizationId == organizationId.Value); + } + + if (!input.AppId.IsNullOrWhiteSpace()) + { + queryable = queryable.Where(o => o.AppInfo.AppId == input.AppId); + } + + var count = queryable.Count(); + var list = queryable.OrderBy(o => o.AppInfo.AppId).Skip(input.SkipCount).Take(input.MaxResultCount).ToList(); + + return new PagedResultDto + { + TotalCount = count, + Items = ObjectMapper.Map, List>(list) + }; + } + + public async Task GetAsync(Guid? organizationId, string appId) + { + var index = await _entityMappingRepository.GetAsync(appId); + if (index != null && organizationId.HasValue && index.OrganizationId != organizationId.Value) + { + throw new UserFriendlyException("No permission."); + } + + return ObjectMapper.Map(index); + } +} \ No newline at end of file diff --git a/src/AeFinder.Application/Apps/AppDeployService.cs b/src/AeFinder.Application/Apps/AppDeployService.cs index 67c8db62d..dc5b6bafc 100644 --- a/src/AeFinder.Application/Apps/AppDeployService.cs +++ b/src/AeFinder.Application/Apps/AppDeployService.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using AeFinder.ApiKeys; using AeFinder.App.Deploy; +using AeFinder.AppResources; +using AeFinder.AppResources.Dto; using AeFinder.Apps.Dto; using AeFinder.Assets; using AeFinder.BlockScan; @@ -40,6 +42,7 @@ public class AppDeployService : AeFinderAppService, IAppDeployService private readonly IAppEmailSender _appEmailSender; private readonly IApiKeyService _apiKeyService; private readonly PodResourceOptions _podResourceOptions; + private readonly IAppResourceUsageService _appResourceUsageService; public AppDeployService(IClusterClient clusterClient, IBlockScanAppService blockScanAppService, IAppDeployManager appDeployManager, @@ -50,12 +53,13 @@ public AppDeployService(IClusterClient clusterClient, IAssetService assetService, IApiKeyService apiKeyService, IUserAppService userAppService, IAppEmailSender appEmailSender, - IAppResourceLimitProvider appResourceLimitProvider) + IAppResourceLimitProvider appResourceLimitProvider, IAppResourceUsageService appResourceUsageService) { _clusterClient = clusterClient; _blockScanAppService = blockScanAppService; _appDeployManager = appDeployManager; _appResourceLimitProvider = appResourceLimitProvider; + _appResourceUsageService = appResourceUsageService; _organizationAppService = organizationAppService; _appDeployOptions = appDeployOptions.Value; _customOrganizationOptions = customOrganizationOptions.Value; @@ -388,6 +392,23 @@ await appResourceLimitGrain.SetAsync(new SetAppResourceLimitDto() throw new UserFriendlyException("Please purchase storage capacity before proceeding with deployment."); } } + else + { + var appResourceUsage = await _appResourceUsageService.GetAsync(null, appId); + + if (appResourceUsage != null) + { + foreach (var resourceUsage in appResourceUsage.ResourceUsages.Values + .SelectMany(resourceUsages => resourceUsages.Where(resourceUsage => + resourceUsage.Name == AeFinderApplicationConsts.AppStorageResourceName))) + { + if (Convert.ToDecimal(resourceUsage.Usage) > storageAsset.Replicas) + { + throw new UserFriendlyException("Storage is insufficient. Please purchase more storage."); + } + } + } + } } private async Task SetFirstDeployTimeAsync(string appId) diff --git a/src/AeFinder.BackgroundWorker.Core/EventHandler/AppDeleteHandler.cs b/src/AeFinder.BackgroundWorker.Core/EventHandler/AppDeleteHandler.cs index 621613669..111a84548 100644 --- a/src/AeFinder.BackgroundWorker.Core/EventHandler/AppDeleteHandler.cs +++ b/src/AeFinder.BackgroundWorker.Core/EventHandler/AppDeleteHandler.cs @@ -24,20 +24,23 @@ public class AppDeleteHandler : IDistributedEventHandler, ITransie private readonly IBlockScanAppService _blockScanAppService; private readonly IEntityMappingRepository _appInfoEntityMappingRepository; private readonly IAssetService _assetService; + private readonly IEntityMappingRepository _organizationEntityMappingRepository; public AppDeleteHandler(ILogger logger, IClusterClient clusterClient, IBlockScanAppService blockScanAppService, IAssetService assetService, - IEntityMappingRepository appInfoEntityMappingRepository) + IEntityMappingRepository appInfoEntityMappingRepository, + IEntityMappingRepository organizationEntityMappingRepository) { _logger = logger; _clusterClient = clusterClient; _blockScanAppService = blockScanAppService; _assetService = assetService; _appInfoEntityMappingRepository = appInfoEntityMappingRepository; + _organizationEntityMappingRepository = organizationEntityMappingRepository; } - + public async Task HandleEventAsync(AppDeleteEto eventData) { var appId = eventData.AppId; @@ -82,6 +85,11 @@ public async Task HandleEventAsync(AppDeleteEto eventData) appInfoIndex.Status = eventData.Status; appInfoIndex.DeleteTime = eventData.DeleteTime; await _appInfoEntityMappingRepository.AddOrUpdateAsync(appInfoIndex); + + var organizationIndex = await _organizationEntityMappingRepository.GetAsync(organizationGuid.ToString()); + organizationIndex.AppIds.Remove(eventData.AppId); + await _organizationEntityMappingRepository.UpdateAsync(organizationIndex); + _logger.LogInformation($"[AppDeleteHandler] App {eventData.AppId} is deleted."); } diff --git a/src/AeFinder.BackgroundWorker.Core/Options/ScheduledTaskOptions.cs b/src/AeFinder.BackgroundWorker.Core/Options/ScheduledTaskOptions.cs index de808fdb1..d0c8bf8c0 100644 --- a/src/AeFinder.BackgroundWorker.Core/Options/ScheduledTaskOptions.cs +++ b/src/AeFinder.BackgroundWorker.Core/Options/ScheduledTaskOptions.cs @@ -20,4 +20,5 @@ public class ScheduledTaskOptions public int PayFailedOrderTimeoutHours { get; set; } = 48; public int ConfirmingOrderTimeoutMinutes { get; set; } = 60; public int BillingPaymentTaskPeriodMilliSeconds { get; set; } = 300000; + public int AppResourceUsageTaskPeriodMilliSeconds { get; set; } = 600000; } \ No newline at end of file diff --git a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs new file mode 100644 index 000000000..40a1ac3e0 --- /dev/null +++ b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs @@ -0,0 +1,157 @@ +using AeFinder.App.Es; +using AeFinder.AppResources; +using AeFinder.AppResources.Dto; +using AeFinder.Apps; +using AeFinder.Assets; +using AeFinder.BackgroundWorker.Options; +using AeFinder.Merchandises; +using AElf.EntityMapping.Elasticsearch; +using Elasticsearch.Net; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Nest; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; + +namespace AeFinder.BackgroundWorker.ScheduledTask; + +public class AppResourceUsageWorker : AsyncPeriodicBackgroundWorkerBase, ISingletonDependency +{ + private readonly IAppResourceUsageService _appResourceUsageService; + private readonly IElasticsearchClientProvider _elasticsearchClientProvider; + private readonly IAppService _appService; + private readonly ScheduledTaskOptions _scheduledTaskOptions; + private readonly IAssetService _assetService; + + public AppResourceUsageWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory, + IAppResourceUsageService appResourceUsageService, IElasticsearchClientProvider elasticsearchClientProvider, + IAppService appService, + IOptionsSnapshot scheduledTaskOptions, IAssetService assetService) + : base(timer, serviceScopeFactory) + { + _appResourceUsageService = appResourceUsageService; + _elasticsearchClientProvider = elasticsearchClientProvider; + _appService = appService; + _assetService = assetService; + _scheduledTaskOptions = scheduledTaskOptions.Value; + + Timer.Period = _scheduledTaskOptions.AppResourceUsageTaskPeriodMilliSeconds; + } + + protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) + { + var indices = await GetIndicesRecordsAsync(); + + var skipCount = 0; + var maxResultCount = 100; + var apps = await _appService.GetIndexListAsync(new GetAppInput + { + SkipCount = skipCount, + MaxResultCount = maxResultCount + }); + + while (apps.Items.Count > 0) + { + var toAddAppResourceUsage = new List(); + foreach (var app in apps.Items) + { + if (!indices.TryGetValue(app.AppId, out var records)) + { + await _appResourceUsageService.DeleteAsync(app.AppId); + } + else + { + var appResourceUsage = new AppResourceUsageDto + { + AppInfo = new AppInfoImmutable + { + AppId = app.AppId, + AppName = app.AppName + }, + OrganizationId = Guid.Parse(app.OrganizationId), + ResourceUsages = new Dictionary>() + }; + + var storeSizes = new Dictionary(); + foreach (var record in records) + { + var version = record.Index.Split('.')[0].Split('-')[1]; + if (!storeSizes.TryGetValue(version, out var usage)) + { + usage = 0; + } + + usage += Convert.ToDecimal(record.PrimaryStoreSize) / 1024; + storeSizes[version] = usage; + } + + var storageLimit = await GetStorageLimitAsync(Guid.Parse(app.OrganizationId), app.AppId); + + foreach (var storeSize in storeSizes) + { + if (!appResourceUsage.ResourceUsages.TryGetValue(storeSize.Key, out var resourceUsage)) + { + resourceUsage = new List(); + } + + resourceUsage.Add(new ResourceUsageDto + { + Name = AeFinderApplicationConsts.AppStorageResourceName, + Limit = storageLimit.ToString(), + Usage = storeSize.Value.ToString("F2") + }); + appResourceUsage.ResourceUsages[storeSize.Key] = resourceUsage; + } + + toAddAppResourceUsage.Add(appResourceUsage); + } + + if (toAddAppResourceUsage.Count > 0) + { + await _appResourceUsageService.AddOrUpdateAsync(toAddAppResourceUsage); + } + } + + skipCount += maxResultCount; + apps = await _appService.GetIndexListAsync(new GetAppInput + { + SkipCount = skipCount, + MaxResultCount = maxResultCount + }); + } + } + + private async Task>> GetIndicesRecordsAsync() + { + var client = _elasticsearchClientProvider.GetClient(); + var catResponse = await client.Cat.IndicesAsync(r => r.Bytes(Bytes.Mb)); + + var indices = new Dictionary>(); + foreach (var record in catResponse.Records) + { + var appId = record.Index.Split('-')[0]; + if (!indices.TryGetValue(appId, out var records)) + { + records = new List(); + } + + records.Add(record); + indices[appId] = records; + } + + return indices; + } + + private async Task GetStorageLimitAsync(Guid organizationId, string appId) + { + var storageAssets = await _assetService.GetListAsync(organizationId, new GetAssetInput() + { + Type = MerchandiseType.Storage, + AppId = appId, + IsDeploy = true + }); + var storageAsset = storageAssets.Items.FirstOrDefault(); + return storageAsset?.Replicas ?? 0; + } +} \ No newline at end of file diff --git a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs index a8c15773f..f45f3341b 100644 --- a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs +++ b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs @@ -1,3 +1,5 @@ +using AeFinder.AppResources; +using AeFinder.AppResources.Dto; using AeFinder.Apps; using AeFinder.Assets; using AeFinder.BackgroundWorker.Options; @@ -31,6 +33,7 @@ public class CleanExpiredAssetWorker: AsyncPeriodicBackgroundWorkerBase, ISingle private readonly IUserAppService _userAppService; private readonly CustomOrganizationOptions _customOrganizationOptions; private readonly IBillingService _billingService; + private readonly IAppResourceUsageService _appResourceUsageService; public CleanExpiredAssetWorker(AbpAsyncTimer timer, ILogger logger, @@ -44,7 +47,7 @@ public CleanExpiredAssetWorker(AbpAsyncTimer timer, IUserAppService userAppService, IOptionsSnapshot customOrganizationOptions, IBillingService billingService, - IServiceScopeFactory serviceScopeFactory) : base(timer, serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, IAppResourceUsageService appResourceUsageService) : base(timer, serviceScopeFactory) { _logger = logger; _scheduledTaskOptions = scheduledTaskOptions.Value; @@ -57,6 +60,7 @@ public CleanExpiredAssetWorker(AbpAsyncTimer timer, _userAppService = userAppService; _customOrganizationOptions = customOrganizationOptions.Value; _billingService = billingService; + _appResourceUsageService = appResourceUsageService; // Timer.Period = 1 * 60 * 60 * 1000; // 3600000 milliseconds = 1 hours Timer.Period = _scheduledTaskOptions.CleanExpiredAssetTaskPeriodMilliSeconds; } @@ -130,6 +134,8 @@ private async Task ProcessAssetCleanAsync() await _appDeployService.DestroyAppAllSubscriptionAsync(appId); continue; } + + await CheckAppStorageAsync(appId, appAssets); } } @@ -216,4 +222,30 @@ private async Task> GetAeIndexerAssetListAsync(Guid organizationG return resultList; } + private async Task CheckAppStorageAsync(string appId, List appAssets) + { + var appResourceUsage = await _appResourceUsageService.GetAsync(null, appId); + + if (appResourceUsage!= null) + { + var storageAsset = + appAssets.First(o => o.Status == AssetStatus.Using && o.Merchandise.Type == MerchandiseType.Storage); + + foreach (var resourceUsage in appResourceUsage.ResourceUsages.Values + .SelectMany(resourceUsages => resourceUsages.Where(resourceUsage => + resourceUsage.Name == AeFinderApplicationConsts.AppStorageResourceName))) + { + if (Convert.ToDecimal(resourceUsage.Usage) <= storageAsset.Replicas) + { + continue; + } + + _logger.LogInformation( + "App {App}: storage usage ({StorageUsage} GB) exceeds the purchased amount ({StorageAsset} GB).", + appId, resourceUsage.Usage, storageAsset.Replicas); + await _appDeployService.DestroyAppAllSubscriptionAsync(appId); + return; + } + } + } } \ No newline at end of file diff --git a/src/AeFinder.BackgroundWorker/AeFinderBackGroundModule.cs b/src/AeFinder.BackgroundWorker/AeFinderBackGroundModule.cs index bd3a71a8a..f7350c5d9 100644 --- a/src/AeFinder.BackgroundWorker/AeFinderBackGroundModule.cs +++ b/src/AeFinder.BackgroundWorker/AeFinderBackGroundModule.cs @@ -114,6 +114,7 @@ public override void OnApplicationInitialization(ApplicationInitializationContex AsyncHelper.RunSync(() => context.AddBackgroundWorkerAsync()); AsyncHelper.RunSync(() => context.AddBackgroundWorkerAsync()); AsyncHelper.RunSync(() => context.AddBackgroundWorkerAsync()); + AsyncHelper.RunSync(() => context.AddBackgroundWorkerAsync()); } public override void OnApplicationShutdown(ApplicationShutdownContext context) diff --git a/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs b/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs new file mode 100644 index 000000000..9d1ae8ca7 --- /dev/null +++ b/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using AeFinder.Entities; +using AElf.EntityMapping.Entities; +using Nest; + +namespace AeFinder.App.Es; + +public class AppResourceUsageIndex : AeFinderDomainEntity, IEntityMappingEntity +{ + [Keyword] + public override string Id => AppInfo.AppId; + + public AppInfoImmutableIndex AppInfo { get; set; } + + [Keyword] + public Guid OrganizationId { get; set; } + + // Version -> ResourceUsage + public Dictionary> ResourceUsages { get; set; } +} + +public class ResourceUsageIndex +{ + public string Name { get; set; } + public string Limit { get; set; } + public string Usage { get; set; } +} \ No newline at end of file diff --git a/src/AeFinder.Grains/Grain/Apps/AppGrain.cs b/src/AeFinder.Grains/Grain/Apps/AppGrain.cs index 93859c17e..0b04397e0 100644 --- a/src/AeFinder.Grains/Grain/Apps/AppGrain.cs +++ b/src/AeFinder.Grains/Grain/Apps/AppGrain.cs @@ -119,6 +119,12 @@ public async Task UnFreezeAppAsync() public async Task DeleteAppAsync() { await ReadStateAsync(); + + var organizationAppGain = + GrainFactory.GetGrain( + GrainIdHelper.GenerateOrganizationAppGrainId(State.OrganizationId)); + await organizationAppGain.DeleteAppAsync(State.AppId); + State.Status = AppStatus.Deleted; State.DeleteTime = DateTime.UtcNow; await WriteStateAsync(); diff --git a/src/AeFinder.Grains/Grain/Apps/IOrganizationAppGrain.cs b/src/AeFinder.Grains/Grain/Apps/IOrganizationAppGrain.cs index fc04c76a6..4d5319f46 100644 --- a/src/AeFinder.Grains/Grain/Apps/IOrganizationAppGrain.cs +++ b/src/AeFinder.Grains/Grain/Apps/IOrganizationAppGrain.cs @@ -6,6 +6,7 @@ public interface IOrganizationAppGrain : IGrainWithStringKey { Task AddOrganizationAsync(string organizationName); Task AddAppAsync(string appId); + Task DeleteAppAsync(string appId); Task> GetAppsAsync(); Task GetMaxAppCountAsync(); Task SetMaxAppCountAsync(int maxAppCount); diff --git a/src/AeFinder.Grains/Grain/Apps/OrganizationAppGrain.cs b/src/AeFinder.Grains/Grain/Apps/OrganizationAppGrain.cs index 17d1c2ce2..4311ab232 100644 --- a/src/AeFinder.Grains/Grain/Apps/OrganizationAppGrain.cs +++ b/src/AeFinder.Grains/Grain/Apps/OrganizationAppGrain.cs @@ -48,6 +48,13 @@ public async Task AddAppAsync(string appId) await WriteStateAsync(); } + public async Task DeleteAppAsync(string appId) + { + await ReadStateAsync(); + State.AppIds.Remove(appId); + await WriteStateAsync(); + } + public async Task> GetAppsAsync() { await ReadStateAsync(); diff --git a/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs b/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs new file mode 100644 index 000000000..e2e1570c3 --- /dev/null +++ b/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AeFinder.AppResources; +using AeFinder.AppResources.Dto; +using AeFinder.User; +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp; +using Volo.Abp.Application.Dtos; + +namespace AeFinder.Controllers; + +[RemoteService] +[ControllerName("AppResource")] +[Route("api/apps/resource-usages")] +public class AppResourceUsageController : AeFinderController +{ + private readonly IAppResourceUsageService _appResourceUsageService; + private readonly IOrganizationAppService _organizationAppService; + + public AppResourceUsageController(IAppResourceUsageService appResourceUsageService, + IOrganizationAppService organizationAppService) + { + _appResourceUsageService = appResourceUsageService; + _organizationAppService = organizationAppService; + } + + [HttpGet] + [Route("{appId}")] + [Authorize] + public async Task GetAsync(string appId) + { + Guid? orgId = null; + if (!CurrentUser.IsInRole(AeFinderApplicationConsts.AdminRoleName)) + { + orgId = await GetOrganizationIdAsync(); + } + + return await _appResourceUsageService.GetAsync(orgId, appId); + } + + [HttpGet] + [Authorize] + public async Task> GetListAsync(GetAppResourceUsageInput input) + { + Guid? orgId = null; + if (!CurrentUser.IsInRole(AeFinderApplicationConsts.AdminRoleName)) + { + orgId = await GetOrganizationIdAsync(); + } + + return await _appResourceUsageService.GetListAsync(orgId, input); + } + + private async Task GetOrganizationIdAsync() + { + var organizationIds = await _organizationAppService.GetOrganizationUnitsByUserIdAsync(CurrentUser.Id.Value); + return organizationIds.First().Id; + } +} \ No newline at end of file diff --git a/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs b/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs new file mode 100644 index 000000000..e5b7d61db --- /dev/null +++ b/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AeFinder.App.Es; +using AeFinder.AppResources.Dto; +using AeFinder.Apps; +using AElf.EntityMapping.Elasticsearch; +using AElf.EntityMapping.Repositories; +using Elasticsearch.Net; +using Elasticsearch.Net.Specification.CatApi; +using Nest; +using Shouldly; +using Volo.Abp; +using Xunit; + +namespace AeFinder.AppResources; + +public class AppResourceUsageServiceTests : AeFinderApplicationAppTestBase +{ + private readonly IEntityMappingRepository _entityMappingRepository; + private readonly IAppResourceUsageService _appResourceUsageService; + + public AppResourceUsageServiceTests() + { + _appResourceUsageService = GetRequiredService(); + _entityMappingRepository = GetRequiredService>(); + } + + [Fact] + public async Task GetTest() + { + var appUsage1 = new AppResourceUsageDto + { + AppInfo = new AppInfoImmutable + { + AppId = "app1", + AppName = "app1" + }, + OrganizationId = AeFinderApplicationTestConsts.OrganizationId, + ResourceUsages = new Dictionary> + { + { + "version1", new List + { + new ResourceUsageDto + { + Name = AeFinderApplicationConsts.AppStorageResourceName, + Limit = "200", + Usage = "100" + } + } + }, + { + "version2", new List + { + new ResourceUsageDto + { + Name = AeFinderApplicationConsts.AppStorageResourceName, + Limit = "400", + Usage = "200" + } + } + } + } + }; + + var appUsage2 = new AppResourceUsageDto + { + AppInfo = new AppInfoImmutable + { + AppId = "app2", + AppName = "app2" + }, + OrganizationId = AeFinderApplicationTestConsts.OrganizationId, + ResourceUsages = new Dictionary> + { + { + "version1", new List + { + new ResourceUsageDto + { + Name = AeFinderApplicationConsts.AppStorageResourceName, + Limit = "2200", + Usage = "2100" + } + } + }, + { + "version2", new List + { + new ResourceUsageDto + { + Name = AeFinderApplicationConsts.AppStorageResourceName, + Limit = "600", + Usage = "300" + } + } + } + } + }; + await _appResourceUsageService.AddOrUpdateAsync(new List + { + appUsage1, + appUsage2 + }); + + var list = await _appResourceUsageService.GetListAsync(AeFinderApplicationTestConsts.OrganizationId, + new GetAppResourceUsageInput()); + list.Items.Count.ShouldBe(2); + list.Items.ShouldContain(o => o.AppInfo.AppId == "app1"); + list.Items.ShouldContain(o => o.AppInfo.AppId == "app2"); + + list = await _appResourceUsageService.GetListAsync(null, + new GetAppResourceUsageInput()); + list.Items.Count.ShouldBe(2); + list.Items.ShouldContain(o => o.AppInfo.AppId == "app1"); + list.Items.ShouldContain(o => o.AppInfo.AppId == "app2"); + + list = await _appResourceUsageService.GetListAsync(AeFinderApplicationTestConsts.OrganizationId, + new GetAppResourceUsageInput + { + AppId = "app1" + }); + list.Items.Count.ShouldBe(1); + list.Items[0].AppInfo.AppId.ShouldBe("app1"); + list.Items[0].ResourceUsages["version1"].Count().ShouldBe(1); + list.Items[0].ResourceUsages["version1"][0].Name.ShouldBe(AeFinderApplicationConsts.AppStorageResourceName); + list.Items[0].ResourceUsages["version1"][0].Limit.ShouldBe("200"); + list.Items[0].ResourceUsages["version1"][0].Usage.ShouldBe("100"); + list.Items[0].ResourceUsages["version2"].Count().ShouldBe(1); + list.Items[0].ResourceUsages["version2"][0].Name.ShouldBe(AeFinderApplicationConsts.AppStorageResourceName); + list.Items[0].ResourceUsages["version2"][0].Limit.ShouldBe("400"); + list.Items[0].ResourceUsages["version2"][0].Usage.ShouldBe("200"); + + await _appResourceUsageService.DeleteAsync("app1"); + + list = await _appResourceUsageService.GetListAsync(AeFinderApplicationTestConsts.OrganizationId, + new GetAppResourceUsageInput + { + }); + list.Items.Count.ShouldBe(1); + list.Items[0].AppInfo.AppId.ShouldBe("app2"); + + var usage = await _appResourceUsageService.GetAsync(AeFinderApplicationTestConsts.OrganizationId, "app1"); + usage.ShouldBeNull(); + + usage = await _appResourceUsageService.GetAsync(AeFinderApplicationTestConsts.OrganizationId, "app2"); + usage.AppInfo.AppId.ShouldBe("app2"); + + await Assert.ThrowsAsync(async () => + await _appResourceUsageService.GetAsync(Guid.NewGuid(), "app2")); + } +} \ No newline at end of file diff --git a/test/AeFinder.Grains.Tests/Apps/AppGrainTests.cs b/test/AeFinder.Grains.Tests/Apps/AppGrainTests.cs new file mode 100644 index 000000000..8f1e0b2b2 --- /dev/null +++ b/test/AeFinder.Grains.Tests/Apps/AppGrainTests.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using AeFinder.Apps; +using AeFinder.Grains.Grain.Apps; +using Shouldly; +using Xunit; + +namespace AeFinder.Grains.Apps; + +[Collection(ClusterCollection.Name)] +public class AppGrainTests : AeFinderGrainTestBase +{ + [Fact] + public async Task DeleteApp_Test() + { + var orgId = Guid.NewGuid(); + var createInput = new CreateAppDto + { + AppId = "appid", + OrganizationId = orgId.ToString("N"), + AppName = "app" + }; + + var appGrain = Cluster.Client.GetGrain(GrainIdHelper.GenerateAppGrainId(createInput.AppId)); + var app = await appGrain.CreateAsync(createInput); + app.AppId.ShouldBe(createInput.AppId); + + var organizationAppGain = + Cluster.Client.GetGrain( + GrainIdHelper.GenerateOrganizationAppGrainId(createInput.OrganizationId)); + var appIds = await organizationAppGain.GetAppsAsync(); + appIds.Count.ShouldBe(1); + appIds.ShouldContain(createInput.AppId); + + await appGrain.DeleteAppAsync(); + app = await appGrain.GetAsync(); + app.Status.ShouldBe(AppStatus.Deleted); + + appIds = await organizationAppGain.GetAppsAsync(); + appIds.Count.ShouldBe(0); + } +} \ No newline at end of file