From 027477387d8360e99934b03384759b297fe1540e Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:07:11 +0200 Subject: [PATCH 01/17] init --- .../ApiClients/ApiModels/Roles.cs | 35 +++++ .../ApiClients/IOrgApiClient.cs | 5 +- .../ApiClients/IRolesApiClient.cs | 10 ++ .../ApiClients/ISummaryApiClient.cs | 17 ++- .../ApiClients/OrgApiClient.cs | 7 + .../ApiClients/RolesApiClient.cs | 45 +++++++ .../ApiClients/SummaryApiClient.cs | 10 ++ .../IServiceCollectionExtensions.cs | 3 + .../Extensions/ObjectExtensions.cs | 11 ++ .../Fusion.Resources.Functions.Common.csproj | 1 + .../Http/Handlers/RolesHttpHandler.cs | 25 ++++ .../Http/HttpClientFactoryBuilder.cs | 14 ++ .../Integration/Http/HttpClientNames.cs | 2 + .../ServiceDiscovery/ServiceEndpoint.cs | 1 + .../Functions/DepartmentResourceOwnerSync.cs | 31 +---- .../Functions/Helpers/QueueTimeHelper.cs | 37 ++++++ .../TaskOwnerReports/ProjectTaskOwnerSync.cs | 121 ++++++++++++++++++ .../Fusion.Summary.Functions.csproj | 1 + 18 files changed, 345 insertions(+), 31 deletions(-) create mode 100644 src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs create mode 100644 src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs create mode 100644 src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs create mode 100644 src/Fusion.Resources.Functions.Common/Extensions/ObjectExtensions.cs create mode 100644 src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/RolesHttpHandler.cs create mode 100644 src/Fusion.Summary.Functions/Functions/Helpers/QueueTimeHelper.cs create mode 100644 src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs new file mode 100644 index 000000000..8480eda8e --- /dev/null +++ b/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs @@ -0,0 +1,35 @@ +using Fusion.Services.LineOrg.ApiModels; +using Newtonsoft.Json; + +namespace Fusion.Resources.Functions.Common.ApiClients.ApiModels; + +public class ApiSinglePersonRole +{ + public Guid Id { get; init; } + + public string RoleName { get; init; } = null!; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? Identifier { get; set; } + + public string? Source { get; init; } + + public ApiSingleRoleScope Scope { get; init; } = null!; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public ApiPerson? Person { get; set; } + + public DateTimeOffset? ValidTo { get; init; } +} + +public class ApiSingleRoleScope +{ + public ApiSingleRoleScope(string type, string value) + { + Type = type; + Value = value; + } + + public string Type { get; set; } + public string Value { get; set; } +} \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/IOrgApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/IOrgApiClient.cs index 433397856..225564f5e 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/IOrgApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/IOrgApiClient.cs @@ -1,4 +1,5 @@ -using Fusion.ApiClients.Org; +using Fusion.Integration.Core.Http.OData; +using Fusion.Services.Org.ApiModels; using Newtonsoft.Json; namespace Fusion.Resources.Functions.Common.ApiClients; @@ -6,6 +7,8 @@ namespace Fusion.Resources.Functions.Common.ApiClients; public interface IOrgClient { Task GetChangeLog(string projectId, DateTime timestamp); + + Task> GetProjects(ODataQuery? query = null); } #region model diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs new file mode 100644 index 000000000..a6f9cada0 --- /dev/null +++ b/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs @@ -0,0 +1,10 @@ +using Fusion.Resources.Functions.Common.ApiClients.ApiModels; + +namespace Fusion.Resources.Functions.Common.ApiClients; + +public interface IRolesApiClient +{ + public Task>> GetAdminRolesForOrgProjects(IEnumerable projectIds); + + public Task>> GetExpiringAdminRolesForOrgProjects(IEnumerable projectIds, int monthsUntilExpiry); +} \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs index 83b5f5820..fd13eeb44 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs @@ -4,10 +4,13 @@ namespace Fusion.Resources.Functions.Common.ApiClients; public interface ISummaryApiClient { - /// + /// public Task PutDepartmentAsync(ApiResourceOwnerDepartment departments, CancellationToken cancellationToken = default); + /// + public Task PutProjectAsync(ApiProject project, CancellationToken cancellationToken = default); + /// public Task?> GetDepartmentsAsync( CancellationToken cancellationToken = default); @@ -84,4 +87,16 @@ public record ApiEndingPosition public DateTime EndDate { get; set; } } +public class ApiProject +{ + public required Guid Id { get; set; } + + public required string Name { get; set; } + public required Guid OrgProjectExternalId { get; set; } + + public Guid? DirectorAzureUniqueId { get; set; } + + public Guid[] AssignedAdminsAzureUniqueId { get; set; } = []; +} + #endregion \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/OrgApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/OrgApiClient.cs index b73f19968..d5606de92 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/OrgApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/OrgApiClient.cs @@ -1,5 +1,6 @@ #nullable enable using Fusion.Resources.Functions.Common.Integration.Http; +using Fusion.Services.Org.ApiModels; namespace Fusion.Resources.Functions.Common.ApiClients; @@ -22,4 +23,10 @@ await orgClient.GetAsJsonAsync( return data; } + + public Task> GetProjects(ODataQuery? query = null) + { + var url = ODataQuery.ApplyQueryString("/projects", query); + return orgClient.GetAsJsonAsync>(url); + } } \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs new file mode 100644 index 000000000..68a964552 --- /dev/null +++ b/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs @@ -0,0 +1,45 @@ +using Fusion.Resources.Functions.Common.ApiClients.ApiModels; +using Fusion.Resources.Functions.Common.Integration.Http; + +namespace Fusion.Resources.Functions.Common.ApiClients; + +public class RolesApiClient : IRolesApiClient +{ + private readonly HttpClient rolesClient; + + public RolesApiClient(IHttpClientFactory clientFactory) + { + rolesClient = clientFactory.CreateClient(HttpClientNames.Application.Roles); + } + + private static string GetActiveOrgAdminsOdataQuery() => "scope.type eq 'OrgChart' and roleName eq 'Fusion.OrgChart.Admin' and source eq 'Fusion.Roles' and " + + $"validTo gteq '{DateTime.UtcNow:O}'"; + + public async Task>> GetAdminRolesForOrgProjects(IEnumerable projectIds) + { + var odataQuery = new ODataQuery(); + + odataQuery.Filter = GetActiveOrgAdminsOdataQuery() + $" and scope.value in ({string.Join(',', projectIds.Select(p => $"'{p}'"))})"; + + var url = ODataQuery.ApplyQueryString("/roles", odataQuery); + var data = await rolesClient.GetAsJsonAsync>(url); + + return data.GroupBy(r => Guid.Parse(r.Scope.Value)) + .ToDictionary(g => g.Key, ICollection (g) => g.ToArray()); + } + + public async Task>> GetExpiringAdminRolesForOrgProjects(IEnumerable projectIds, int monthsUntilExpiry) + { + var odataQuery = new ODataQuery(); + + var expiryDate = DateTime.UtcNow.AddMonths(monthsUntilExpiry).ToString("O"); + + odataQuery.Filter = GetActiveOrgAdminsOdataQuery() + $" and validTo lteq '{expiryDate}' and scope.value in ({string.Join(',', projectIds.Select(p => $"'{p}'"))})"; + + var url = ODataQuery.ApplyQueryString("/roles", odataQuery); + var data = await rolesClient.GetAsJsonAsync>(url); + + return data.GroupBy(r => Guid.Parse(r.Scope.Value)) + .ToDictionary(g => g.Key, ICollection (g) => g.ToArray()); + } +} \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs index 946fa91a0..14f67c73a 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs @@ -32,6 +32,16 @@ public async Task PutDepartmentAsync(ApiResourceOwnerDepartment department, await ThrowIfUnsuccessfulAsync(response); } + public async Task PutProjectAsync(ApiProject project, CancellationToken cancellationToken = default) + { + using var body = new JsonContent(JsonSerializer.Serialize(project, jsonSerializerOptions)); + + // Error logging is handled by http middleware => FunctionHttpMessageHandler + using var response = await summaryClient.PutAsync($"projects/{project.OrgProjectExternalId}", body, cancellationToken); + + await ThrowIfUnsuccessfulAsync(response); + } + public async Task?> GetDepartmentsAsync( CancellationToken cancellationToken = default) { diff --git a/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs b/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs index ec54bd344..58adca253 100644 --- a/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs +++ b/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs @@ -58,6 +58,9 @@ public static IServiceCollection AddHttpClients(this IServiceCollection services builder.AddOrgClient(); services.AddScoped(); + builder.AddRolesClient(); + services.AddScoped(); + return services; } } diff --git a/src/Fusion.Resources.Functions.Common/Extensions/ObjectExtensions.cs b/src/Fusion.Resources.Functions.Common/Extensions/ObjectExtensions.cs new file mode 100644 index 000000000..b085a93f3 --- /dev/null +++ b/src/Fusion.Resources.Functions.Common/Extensions/ObjectExtensions.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace Fusion.Resources.Functions.Common.Extensions; + +public static class ObjectExtensions +{ + public static string ToJson(this object obj) + { + return JsonConvert.SerializeObject(obj, Formatting.Indented); + } +} \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/Fusion.Resources.Functions.Common.csproj b/src/Fusion.Resources.Functions.Common/Fusion.Resources.Functions.Common.csproj index 300ae241a..a4d6cd5d0 100644 --- a/src/Fusion.Resources.Functions.Common/Fusion.Resources.Functions.Common.csproj +++ b/src/Fusion.Resources.Functions.Common/Fusion.Resources.Functions.Common.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/RolesHttpHandler.cs b/src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/RolesHttpHandler.cs new file mode 100644 index 000000000..c3d929f96 --- /dev/null +++ b/src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/RolesHttpHandler.cs @@ -0,0 +1,25 @@ +using Fusion.Resources.Functions.Common.Integration.Authentication; +using Fusion.Resources.Functions.Common.Integration.ServiceDiscovery; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Fusion.Resources.Functions.Common.Integration.Http.Handlers; + +public class RolesHttpHandler : FunctionHttpMessageHandler +{ + private readonly IOptions options; + + public RolesHttpHandler(ILoggerFactory loggerFactory, ITokenProvider tokenProvider, IServiceDiscovery serviceDiscovery, IOptions options) + : base(loggerFactory.CreateLogger(), tokenProvider, serviceDiscovery) + { + this.options = options; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + await SetEndpointUriForRequestAsync(request, ServiceEndpoint.Roles); + await AddAuthHeaderForRequestAsync(request, options.Value.Fusion); + + return await base.SendAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs index b467b9ff9..7c54e7ee7 100644 --- a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs +++ b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs @@ -108,6 +108,20 @@ public HttpClientFactoryBuilder AddLineOrgClient() return this; } + public HttpClientFactoryBuilder AddRolesClient() + { + services.AddTransient(); + services.AddHttpClient(HttpClientNames.Application.Roles, client => + { + client.BaseAddress = new Uri("https://fusion-notifications"); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + }) + .AddHttpMessageHandler() + .AddTransientHttpErrorPolicy(DefaultRetryPolicy()); + + return this; + } + private readonly TimeSpan[] DefaultSleepDurations = new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10) }; private Func, IAsyncPolicy> DefaultRetryPolicy(TimeSpan[] sleepDurations = null) => diff --git a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientNames.cs b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientNames.cs index dc5139d5f..2217901c9 100644 --- a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientNames.cs +++ b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientNames.cs @@ -11,7 +11,9 @@ public static class Application public const string Notifications = "App.Notifications"; public const string Context = "App.Context"; public const string LineOrg = "App.LineOrg"; + public const string Roles = "App.Roles"; } + } } diff --git a/src/Fusion.Resources.Functions.Common/Integration/ServiceDiscovery/ServiceEndpoint.cs b/src/Fusion.Resources.Functions.Common/Integration/ServiceDiscovery/ServiceEndpoint.cs index dc80ac870..a2893251d 100644 --- a/src/Fusion.Resources.Functions.Common/Integration/ServiceDiscovery/ServiceEndpoint.cs +++ b/src/Fusion.Resources.Functions.Common/Integration/ServiceDiscovery/ServiceEndpoint.cs @@ -11,5 +11,6 @@ public sealed class ServiceEndpoint public static ServiceEndpoint Notifications = new ServiceEndpoint { Key = "notifications" }; public static ServiceEndpoint Context = new ServiceEndpoint { Key = "context" }; public static ServiceEndpoint LineOrg = new ServiceEndpoint { Key = "lineorg" }; + public static ServiceEndpoint Roles = new ServiceEndpoint { Key = "roles" }; } } diff --git a/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs index 7f3deff67..124649c92 100644 --- a/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/DepartmentResourceOwnerSync.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Fusion.Resources.Functions.Common.ApiClients; +using Fusion.Summary.Functions.Functions.Helpers; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -115,7 +116,7 @@ public async Task RunAsync( }); } - var enqueueTimeForDepartmentMapping = CalculateDepartmentEnqueueTime(apiDepartments); + var enqueueTimeForDepartmentMapping = QueueTimeHelper.CalculateEnqueueTime(apiDepartments, _totalBatchTime, logger); logger.LogInformation("Syncing departments {Departments}", JsonConvert.SerializeObject(enqueueTimeForDepartmentMapping, Formatting.Indented)); @@ -160,32 +161,4 @@ private async Task SendDepartmentToQueue(ServiceBusSender sender, ApiResourceOwn await sender.SendMessageAsync(message); } - - /// - /// Calculate the enqueue time for each department based on the total batch time and amount of departments. This should spread - /// the work over the total batch time. - /// - private Dictionary CalculateDepartmentEnqueueTime(List apiDepartments) - { - var currentTime = DateTimeOffset.UtcNow; - var minutesPerReportSlice = _totalBatchTime.TotalMinutes / apiDepartments.Count; - - logger.LogInformation("Minutes allocated for each worker: {MinutesPerReportSlice}", minutesPerReportSlice); - - var departmentDelayMapping = new Dictionary(); - foreach (var department in apiDepartments) - { - // First department has no delay - if (departmentDelayMapping.Count == 0) - { - departmentDelayMapping.Add(department, currentTime); - continue; - } - - var enqueueTime = departmentDelayMapping.Last().Value.AddMinutes(minutesPerReportSlice); - departmentDelayMapping.Add(department, enqueueTime); - } - - return departmentDelayMapping; - } } \ No newline at end of file diff --git a/src/Fusion.Summary.Functions/Functions/Helpers/QueueTimeHelper.cs b/src/Fusion.Summary.Functions/Functions/Helpers/QueueTimeHelper.cs new file mode 100644 index 000000000..0937c320a --- /dev/null +++ b/src/Fusion.Summary.Functions/Functions/Helpers/QueueTimeHelper.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; + +namespace Fusion.Summary.Functions.Functions.Helpers; + +public static class QueueTimeHelper +{ + /// + /// Calculate the enqueue time for each value based on the total batch time and amount of values. This should + /// spread the work over the total batch time. + /// + public static Dictionary CalculateEnqueueTime(List values, TimeSpan totalBatchTime, ILogger? logger = null) where T : notnull + { + var currentTime = DateTimeOffset.UtcNow; + var minutesPerReportSlice = totalBatchTime.TotalMinutes / values.Count; + + logger?.LogInformation("Minutes allocated for each worker: {MinutesPerReportSlice}", minutesPerReportSlice); + + var delayMapping = new Dictionary(); + foreach (var value in values) + { + // First values has no delay + if (delayMapping.Count == 0) + { + delayMapping.Add(value, currentTime); + continue; + } + + var enqueueTime = delayMapping.Last().Value.AddMinutes(minutesPerReportSlice); + delayMapping.Add(value, enqueueTime); + } + + return delayMapping; + } +} \ No newline at end of file diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs new file mode 100644 index 000000000..ff2a35e9e --- /dev/null +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -0,0 +1,121 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Fusion.Resources.Functions.Common.ApiClients; +using Fusion.Resources.Functions.Common.Extensions; +using Fusion.Summary.Functions.Functions.Helpers; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Fusion.Summary.Functions.Functions.TaskOwnerReports; + +public class ProjectTaskOwnerSync +{ + private const string FunctionName = "weekly-project-recipients-sync"; + private readonly ILogger logger; + private readonly IOrgClient orgClient; + private readonly IRolesApiClient rolesApiClient; + private readonly ISummaryApiClient summaryApiClient; + + private string _serviceBusConnectionString; + private string _weeklySummaryQueueName; + private string[]? _projectTypeFilter; + private TimeSpan _totalBatchTime; + + public ProjectTaskOwnerSync(ILogger logger, IConfiguration configuration, IOrgClient orgClient, IRolesApiClient rolesApiClient, ISummaryApiClient summaryApiClient) + { + this.logger = logger; + this.orgClient = orgClient; + this.rolesApiClient = rolesApiClient; + this.summaryApiClient = summaryApiClient; + + _serviceBusConnectionString = configuration["AzureWebJobsServiceBus"]!; + _weeklySummaryQueueName = configuration["project_summary_weekly_queue"]!; + // TODO: Should there be a default value for projectTypeFilter? + _projectTypeFilter = configuration["projectTypeFilter"]?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? ["PRD"]; + + var totalBatchTimeInMinutesStr = configuration["total_batch_time_in_minutes"]; + + if (!string.IsNullOrWhiteSpace(totalBatchTimeInMinutesStr)) + { + _totalBatchTime = TimeSpan.FromMinutes(double.Parse(totalBatchTimeInMinutesStr)); + this.logger.LogInformation("Batching messages over {BatchTime}", _totalBatchTime); + } + else + { + _totalBatchTime = TimeSpan.FromHours(4.5); + + this.logger.LogInformation("Configuration variable 'total_batch_time_in_minutes' not found, batching messages over {BatchTime}", _totalBatchTime); + } + } + + + [FunctionName(FunctionName)] + public async Task RunAsync( + [TimerTrigger("0 5 0 * * MON", RunOnStartup = false)] + TimerInfo myTimer) + { + // TODO: Should gather all relevant projects + + logger.LogInformation("{FunctionName} triggered with projectTypeFilter {ProjectTypeFilter}", FunctionName, _projectTypeFilter?.ToJson()); + + var queryFilter = new ODataQuery(); + + if (_projectTypeFilter != null) + { + queryFilter.Filter = $"projectType in ({string.Join(',', _projectTypeFilter)})"; + // TODO: Filter on active projects when odata support is added in org + // + " and state eq 'ACTIVE'"; + } + + // TODO: Should projects be retrieved from context api? + // Can we used project id from the org api as identifier? + // Context api does not have project type + var projects = await orgClient.GetProjects(queryFilter); + + var activeProjects = projects.Where(p => p.State.Equals("ACTIVE", StringComparison.OrdinalIgnoreCase)).ToList(); + + if (_projectTypeFilter is not null) + activeProjects = activeProjects.Where(p => _projectTypeFilter.Contains(p.ProjectType, StringComparer.OrdinalIgnoreCase)).ToList(); + + logger.LogInformation("Found {ProjectCount} active projects {Projects}", activeProjects.Count, activeProjects.Select(p => new { p.ProjectId, p.Name, p.DomainId }).ToJson()); + var admins = await rolesApiClient.GetAdminRolesForOrgProjects(activeProjects.Select(p => p.ProjectId)); + + + var projectToEnqueueTime = QueueTimeHelper.CalculateEnqueueTime(activeProjects, _totalBatchTime, logger); + + logger.LogInformation("Syncing projects and admins"); + + foreach (var (project, queueTime) in projectToEnqueueTime) + { + try + { + var projectAdmins = admins.TryGetValue(project.ProjectId, out var values) ? values : []; + var projectDirector = project.Director.Instances + .FirstOrDefault(i => i.AssignedPerson is not null && i.AppliesFrom <= DateTime.UtcNow && i.AppliesTo >= DateTime.UtcNow)?.AssignedPerson; + + var putRequest = new ApiProject() + { + Id = Guid.Empty, // Ignored + OrgProjectExternalId = project.ProjectId, + Name = project.Name, + DirectorAzureUniqueId = projectDirector?.AzureUniqueId, + AssignedAdminsAzureUniqueId = projectAdmins + .Where(p => p.Person?.AzureUniqueId is not null) + .Select(p => p.Person!.AzureUniqueId) + .ToArray() + }; + + await summaryApiClient.PutProjectAsync(putRequest); + } + catch (SummaryApiError e) + { + logger.LogCritical(e, "Failed to PUT project {Project}", project.ToJson()); + continue; + } + + // TODO: Should send project and recipients on to the bus + } + } +} \ No newline at end of file diff --git a/src/Fusion.Summary.Functions/Fusion.Summary.Functions.csproj b/src/Fusion.Summary.Functions/Fusion.Summary.Functions.csproj index ef350f835..779362bd8 100644 --- a/src/Fusion.Summary.Functions/Fusion.Summary.Functions.csproj +++ b/src/Fusion.Summary.Functions/Fusion.Summary.Functions.csproj @@ -2,6 +2,7 @@ net8.0 v4 + enable From 341e442c41e79e23f12a95d817f1e97336cfb302 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:48:45 +0200 Subject: [PATCH 02/17] Add to queue --- .../Integration/Errors/ApiError.cs | 5 +- .../TaskOwnerReports/ProjectTaskOwnerSync.cs | 72 ++++++++++++------- .../local.settings.template.json | 2 + 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/Fusion.Resources.Functions.Common/Integration/Errors/ApiError.cs b/src/Fusion.Resources.Functions.Common/Integration/Errors/ApiError.cs index de147d775..24bc82a35 100644 --- a/src/Fusion.Resources.Functions.Common/Integration/Errors/ApiError.cs +++ b/src/Fusion.Resources.Functions.Common/Integration/Errors/ApiError.cs @@ -4,7 +4,8 @@ namespace Fusion.Resources.Functions.Common.Integration.Errors { public class ApiError : Exception { - public ApiError(string url, HttpStatusCode statusCode, string body, string message) : base($"{message}. Status code: {statusCode}, Body: {body.Substring(0, 500)}") + public ApiError(string url, HttpStatusCode statusCode, string body, string message) : + base($"{message}. Status code: {statusCode}, Body: {(body.Length > 500 ? body[..500] : body)}") { Url = url; StatusCode = statusCode; @@ -17,4 +18,4 @@ public ApiError(string url, HttpStatusCode statusCode, string body, string messa public string Body { get; } } -} +} \ No newline at end of file diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index ff2a35e9e..34ef837d1 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -1,12 +1,15 @@ using System; using System.Linq; +using System.Text; using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; using Fusion.Resources.Functions.Common.ApiClients; using Fusion.Resources.Functions.Common.Extensions; using Fusion.Summary.Functions.Functions.Helpers; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; namespace Fusion.Summary.Functions.Functions.TaskOwnerReports; @@ -56,7 +59,8 @@ public async Task RunAsync( [TimerTrigger("0 5 0 * * MON", RunOnStartup = false)] TimerInfo myTimer) { - // TODO: Should gather all relevant projects + var client = new ServiceBusClient(_serviceBusConnectionString); + var sender = client.CreateSender(_weeklySummaryQueueName); logger.LogInformation("{FunctionName} triggered with projectTypeFilter {ProjectTypeFilter}", FunctionName, _projectTypeFilter?.ToJson()); @@ -64,7 +68,7 @@ public async Task RunAsync( if (_projectTypeFilter != null) { - queryFilter.Filter = $"projectType in ({string.Join(',', _projectTypeFilter)})"; + queryFilter.Filter = $"projectType in ({string.Join(',', _projectTypeFilter.Select(s => $"'{s}'"))})"; // TODO: Filter on active projects when odata support is added in org // + " and state eq 'ACTIVE'"; } @@ -76,38 +80,35 @@ public async Task RunAsync( var activeProjects = projects.Where(p => p.State.Equals("ACTIVE", StringComparison.OrdinalIgnoreCase)).ToList(); - if (_projectTypeFilter is not null) - activeProjects = activeProjects.Where(p => _projectTypeFilter.Contains(p.ProjectType, StringComparer.OrdinalIgnoreCase)).ToList(); - logger.LogInformation("Found {ProjectCount} active projects {Projects}", activeProjects.Count, activeProjects.Select(p => new { p.ProjectId, p.Name, p.DomainId }).ToJson()); - var admins = await rolesApiClient.GetAdminRolesForOrgProjects(activeProjects.Select(p => p.ProjectId)); + var projectAdminsMapping = await rolesApiClient.GetAdminRolesForOrgProjects(activeProjects.Select(p => p.ProjectId)); - var projectToEnqueueTime = QueueTimeHelper.CalculateEnqueueTime(activeProjects, _totalBatchTime, logger); + var projectToEnqueueTimeMapping = QueueTimeHelper.CalculateEnqueueTime(activeProjects, _totalBatchTime, logger); logger.LogInformation("Syncing projects and admins"); - foreach (var (project, queueTime) in projectToEnqueueTime) + foreach (var (project, queueTime) in projectToEnqueueTimeMapping) { + var projectAdmins = projectAdminsMapping.TryGetValue(project.ProjectId, out var values) ? values : []; + var projectDirector = project.Director.Instances + .FirstOrDefault(i => i.AssignedPerson is not null && i.AppliesFrom <= DateTime.UtcNow && i.AppliesTo >= DateTime.UtcNow)?.AssignedPerson; + + var apiProject = new ApiProject() + { + Id = Guid.Empty, // Ignored + OrgProjectExternalId = project.ProjectId, + Name = project.Name, + DirectorAzureUniqueId = projectDirector?.AzureUniqueId, + AssignedAdminsAzureUniqueId = projectAdmins + .Where(p => p.Person?.AzureUniqueId is not null) + .Select(p => p.Person!.AzureUniqueId) + .ToArray() + }; + try { - var projectAdmins = admins.TryGetValue(project.ProjectId, out var values) ? values : []; - var projectDirector = project.Director.Instances - .FirstOrDefault(i => i.AssignedPerson is not null && i.AppliesFrom <= DateTime.UtcNow && i.AppliesTo >= DateTime.UtcNow)?.AssignedPerson; - - var putRequest = new ApiProject() - { - Id = Guid.Empty, // Ignored - OrgProjectExternalId = project.ProjectId, - Name = project.Name, - DirectorAzureUniqueId = projectDirector?.AzureUniqueId, - AssignedAdminsAzureUniqueId = projectAdmins - .Where(p => p.Person?.AzureUniqueId is not null) - .Select(p => p.Person!.AzureUniqueId) - .ToArray() - }; - - await summaryApiClient.PutProjectAsync(putRequest); + await summaryApiClient.PutProjectAsync(apiProject); } catch (SummaryApiError e) { @@ -115,7 +116,26 @@ public async Task RunAsync( continue; } - // TODO: Should send project and recipients on to the bus + try + { + await SendProjectToQueue(sender, apiProject, queueTime); + } + catch (Exception e) + { + logger.LogCritical(e, "Failed to send project to queue {Project}", apiProject.ToJson()); + } } } + + private async Task SendProjectToQueue(ServiceBusSender sender, ApiProject project, DateTimeOffset enqueueTime) + { + var serializedDto = JsonConvert.SerializeObject(project); + + var message = new ServiceBusMessage(Encoding.UTF8.GetBytes(serializedDto)) + { + ScheduledEnqueueTime = enqueueTime + }; + + await sender.SendMessageAsync(message); + } } \ No newline at end of file diff --git a/src/Fusion.Summary.Functions/local.settings.template.json b/src/Fusion.Summary.Functions/local.settings.template.json index 6c208be30..2e1516aaf 100644 --- a/src/Fusion.Summary.Functions/local.settings.template.json +++ b/src/Fusion.Summary.Functions/local.settings.template.json @@ -9,6 +9,7 @@ "AzureAd_ClientId": "5a842df8-3238-415d-b168-9f16a6a6031b", "AzureAd_Secret": "[REPLACE WITH SECRET]", "department_summary_weekly_queue": "department-summary-weekly-queue-[REPLACE WITH DEV QUEUE]", + "project_summary_weekly_queue": "project-summary-weekly-queue-[REPLACE WITH DEV QUEUE]", "AzureWebJobsServiceBus": "[REPLACE WITH SB CONNECTION STRING]", "departmentFilter": "PRD", "Endpoints_lineorg": "https://fusion-s-lineorg-ci.azurewebsites.net", @@ -18,6 +19,7 @@ "Endpoints_summary": "https://summary-api.ci.fusion-dev.net/", "Endpoints_context": "https://fusion-s-context-ci.azurewebsites.net", "Endpoints_notifications": "https://fusion-s-notification-ci.azurewebsites.net", + "Endpoints_roles": "https://roles.ci.api.fusion-dev.net", "Endpoints_Resources_Fusion": "5a842df8-3238-415d-b168-9f16a6a6031b" } } \ No newline at end of file From ba4b123fb01bbb8a215b5227d144fd8b80ce6d60 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:14:03 +0100 Subject: [PATCH 03/17] Cleanup --- .../ApiClients/ISummaryApiClient.cs | 2 +- .../ApiClients/SummaryApiClient.cs | 6 +++++- .../TaskOwnerReports/ProjectTaskOwnerSync.cs | 20 +++++++++---------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs index fd13eeb44..bac611522 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs @@ -9,7 +9,7 @@ public Task PutDepartmentAsync(ApiResourceOwnerDepartment departments, CancellationToken cancellationToken = default); /// - public Task PutProjectAsync(ApiProject project, CancellationToken cancellationToken = default); + public Task PutProjectAsync(ApiProject project, CancellationToken cancellationToken = default); /// public Task?> GetDepartmentsAsync( diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs index 14f67c73a..2f4a4d9ca 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs @@ -32,7 +32,7 @@ public async Task PutDepartmentAsync(ApiResourceOwnerDepartment department, await ThrowIfUnsuccessfulAsync(response); } - public async Task PutProjectAsync(ApiProject project, CancellationToken cancellationToken = default) + public async Task PutProjectAsync(ApiProject project, CancellationToken cancellationToken = default) { using var body = new JsonContent(JsonSerializer.Serialize(project, jsonSerializerOptions)); @@ -40,6 +40,10 @@ public async Task PutProjectAsync(ApiProject project, CancellationToken cancella using var response = await summaryClient.PutAsync($"projects/{project.OrgProjectExternalId}", body, cancellationToken); await ThrowIfUnsuccessfulAsync(response); + + await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); + + return (await JsonSerializer.DeserializeAsync(contentStream, jsonSerializerOptions, cancellationToken: cancellationToken))!; } public async Task?> GetDepartmentsAsync( diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index 34ef837d1..02fd06462 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -23,7 +23,7 @@ public class ProjectTaskOwnerSync private string _serviceBusConnectionString; private string _weeklySummaryQueueName; - private string[]? _projectTypeFilter; + private string[] _projectTypeFilter; private TimeSpan _totalBatchTime; public ProjectTaskOwnerSync(ILogger logger, IConfiguration configuration, IOrgClient orgClient, IRolesApiClient rolesApiClient, ISummaryApiClient summaryApiClient) @@ -35,7 +35,6 @@ public ProjectTaskOwnerSync(ILogger logger, IConfiguration _serviceBusConnectionString = configuration["AzureWebJobsServiceBus"]!; _weeklySummaryQueueName = configuration["project_summary_weekly_queue"]!; - // TODO: Should there be a default value for projectTypeFilter? _projectTypeFilter = configuration["projectTypeFilter"]?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? ["PRD"]; var totalBatchTimeInMinutesStr = configuration["total_batch_time_in_minutes"]; @@ -62,23 +61,22 @@ public async Task RunAsync( var client = new ServiceBusClient(_serviceBusConnectionString); var sender = client.CreateSender(_weeklySummaryQueueName); - logger.LogInformation("{FunctionName} triggered with projectTypeFilter {ProjectTypeFilter}", FunctionName, _projectTypeFilter?.ToJson()); + logger.LogInformation("{FunctionName} triggered with projectTypeFilter {ProjectTypeFilter}", FunctionName, _projectTypeFilter.ToJson()); var queryFilter = new ODataQuery(); - if (_projectTypeFilter != null) + if (_projectTypeFilter.Length != 0) { queryFilter.Filter = $"projectType in ({string.Join(',', _projectTypeFilter.Select(s => $"'{s}'"))})"; // TODO: Filter on active projects when odata support is added in org // + " and state eq 'ACTIVE'"; } - // TODO: Should projects be retrieved from context api? - // Can we used project id from the org api as identifier? - // Context api does not have project type var projects = await orgClient.GetProjects(queryFilter); - var activeProjects = projects.Where(p => p.State.Equals("ACTIVE", StringComparison.OrdinalIgnoreCase)).ToList(); + var activeProjects = projects + .Where(p => p.State.Equals("ACTIVE", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(p.State)) + .ToList(); logger.LogInformation("Found {ProjectCount} active projects {Projects}", activeProjects.Count, activeProjects.Select(p => new { p.ProjectId, p.Name, p.DomainId }).ToJson()); var projectAdminsMapping = await rolesApiClient.GetAdminRolesForOrgProjects(activeProjects.Select(p => p.ProjectId)); @@ -96,7 +94,7 @@ public async Task RunAsync( var apiProject = new ApiProject() { - Id = Guid.Empty, // Ignored + Id = Guid.NewGuid(), // Ignored OrgProjectExternalId = project.ProjectId, Name = project.Name, DirectorAzureUniqueId = projectDirector?.AzureUniqueId, @@ -108,7 +106,7 @@ public async Task RunAsync( try { - await summaryApiClient.PutProjectAsync(apiProject); + apiProject = await summaryApiClient.PutProjectAsync(apiProject); } catch (SummaryApiError e) { @@ -124,6 +122,8 @@ public async Task RunAsync( { logger.LogCritical(e, "Failed to send project to queue {Project}", apiProject.ToJson()); } + + logger.LogInformation("{FunctionName} completed", FunctionName); } } From 6b7dd9e0b0025e750ff07537280a649ad40e3871 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:43:18 +0100 Subject: [PATCH 04/17] Updated PUT logic --- .../ApiClients/ISummaryApiClient.cs | 10 +++++++--- .../ApiClients/SummaryApiClient.cs | 14 +++++++++++++- .../TaskOwnerReports/ProjectTaskOwnerSync.cs | 8 +++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs index bac611522..e04ffab99 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs @@ -8,9 +8,6 @@ public interface ISummaryApiClient public Task PutDepartmentAsync(ApiResourceOwnerDepartment departments, CancellationToken cancellationToken = default); - /// - public Task PutProjectAsync(ApiProject project, CancellationToken cancellationToken = default); - /// public Task?> GetDepartmentsAsync( CancellationToken cancellationToken = default); @@ -26,6 +23,12 @@ public Task PutDepartmentAsync(ApiResourceOwnerDepartment departments, /// public Task PutWeeklySummaryReportAsync(string departmentSapId, ApiWeeklySummaryReport report, CancellationToken cancellationToken = default); + + /// + public Task> GetProjectsAsync(CancellationToken cancellationToken = default); + + /// + public Task PutProjectAsync(ApiProject project, CancellationToken cancellationToken = default); } #region Models @@ -87,6 +90,7 @@ public record ApiEndingPosition public DateTime EndDate { get; set; } } +[DebuggerDisplay("{Id} - {Name}")] public class ApiProject { public required Guid Id { get; set; } diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs index 2f4a4d9ca..e5391da21 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs @@ -37,7 +37,7 @@ public async Task PutProjectAsync(ApiProject project, CancellationTo using var body = new JsonContent(JsonSerializer.Serialize(project, jsonSerializerOptions)); // Error logging is handled by http middleware => FunctionHttpMessageHandler - using var response = await summaryClient.PutAsync($"projects/{project.OrgProjectExternalId}", body, cancellationToken); + using var response = await summaryClient.PutAsync($"projects/{project.Id}", body, cancellationToken); await ThrowIfUnsuccessfulAsync(response); @@ -92,6 +92,18 @@ public async Task PutWeeklySummaryReportAsync(string departmentSapId, ApiWeeklyS await ThrowIfUnsuccessfulAsync(response); } + public async Task> GetProjectsAsync(CancellationToken cancellationToken = default) + { + using var response = await summaryClient.GetAsync("projects", cancellationToken); + + await ThrowIfUnsuccessfulAsync(response); + + await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); + + return await JsonSerializer.DeserializeAsync>(contentStream, + jsonSerializerOptions, cancellationToken: cancellationToken) ?? []; + } + private async Task ThrowIfUnsuccessfulAsync(HttpResponseMessage response) => await response.ThrowIfUnsuccessfulAsync((responseBody) => new SummaryApiError(response, responseBody)); } diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index 02fd06462..8e21832b2 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -73,6 +73,7 @@ public async Task RunAsync( } var projects = await orgClient.GetProjects(queryFilter); + var existingSummaryProjects = await summaryApiClient.GetProjectsAsync(); var activeProjects = projects .Where(p => p.State.Equals("ACTIVE", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(p.State)) @@ -82,6 +83,7 @@ public async Task RunAsync( var projectAdminsMapping = await rolesApiClient.GetAdminRolesForOrgProjects(activeProjects.Select(p => p.ProjectId)); + var projectToEnqueueTimeMapping = QueueTimeHelper.CalculateEnqueueTime(activeProjects, _totalBatchTime, logger); logger.LogInformation("Syncing projects and admins"); @@ -92,9 +94,13 @@ public async Task RunAsync( var projectDirector = project.Director.Instances .FirstOrDefault(i => i.AssignedPerson is not null && i.AppliesFrom <= DateTime.UtcNow && i.AppliesTo >= DateTime.UtcNow)?.AssignedPerson; + // OrgProjectExternalId is the common key between the two systems, org api and summary api + // We use this to see if we're updating or creating a new project entity + var existingProjectId = existingSummaryProjects.FirstOrDefault(p => p.OrgProjectExternalId == project.ProjectId)?.Id; + var apiProject = new ApiProject() { - Id = Guid.NewGuid(), // Ignored + Id = existingProjectId ?? Guid.NewGuid(), OrgProjectExternalId = project.ProjectId, Name = project.Name, DirectorAzureUniqueId = projectDirector?.AzureUniqueId, From d1972f5d54690802d788e1140aaaa2b845fa1fe1 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:20:12 +0100 Subject: [PATCH 05/17] Update IAC --- infrastructure/arm/environment.template.json | 16 ++++++++++++++++ .../deploy-summary-function-pr-template.yml | 3 +++ .../deploy-summary-function-template.yml | 3 +++ .../Deployment/function.template.json | 12 ++++++++++++ 4 files changed, 34 insertions(+) diff --git a/infrastructure/arm/environment.template.json b/infrastructure/arm/environment.template.json index 3e088788e..cc8edda9b 100644 --- a/infrastructure/arm/environment.template.json +++ b/infrastructure/arm/environment.template.json @@ -232,6 +232,22 @@ "maxDeliveryCount": 2 } }, + { + "type": "Microsoft.ServiceBus/namespaces/queues", + "apiVersion": "2017-04-01", + "name": "[concat(variables('sb-name'), '/scheduled-weekly-project-report')]", + "location": "North Europe", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', variables('sb-name'))]" + ], + "properties": { + "maxSizeInMegabytes": 1024, + "duplicateDetectionHistoryTimeWindow": "P1D", + "defaultMessageTimeToLive": "PT1H", + "deadLetteringOnMessageExpiration": true, + "maxDeliveryCount": 2 + } + }, /* SECRETS */ { diff --git a/pipelines/templates/deploy-summary-function-pr-template.yml b/pipelines/templates/deploy-summary-function-pr-template.yml index 55de5ca6b..72afcec93 100644 --- a/pipelines/templates/deploy-summary-function-pr-template.yml +++ b/pipelines/templates/deploy-summary-function-pr-template.yml @@ -50,6 +50,7 @@ steps: $settings = @{ clientId = "${{ parameters.clientId }}" departmentFilter = "PRD" + projectTypeFilter = "PRD" secretIds = @{ clientSecret = "https://$envVaultName.vault.azure.net:443/secrets/AzureAd--ClientSecret" serviceBus = "https://$envVaultName.vault.azure.net:443/secrets/Connectionstrings--ServiceBus" @@ -63,12 +64,14 @@ steps: notifications = "https://fusion-s-notification-$fusionEnvironment.azurewebsites.net" context = "https://fusion-s-context-$fusionEnvironment.azurewebsites.net" portal = "https://fusion-s-portal-$fusionEnvironment.azurewebsites.net" + roles = "https://fusion-s-roles-$fusionEnvironment.azurewebsites.net" } resources = @{ fusion = "${{ parameters.fusionResource }}" } queues = @{ departmentSummaryWeeklyQueue = "scheduled-weekly-department-report" + projectSummaryWeeklyQueue = "scheduled-weekly-project-report" } } diff --git a/pipelines/templates/deploy-summary-function-template.yml b/pipelines/templates/deploy-summary-function-template.yml index f3b5d6448..f03d00709 100644 --- a/pipelines/templates/deploy-summary-function-template.yml +++ b/pipelines/templates/deploy-summary-function-template.yml @@ -48,6 +48,7 @@ steps: $settings = @{ clientId = "${{ parameters.clientId }}" departmentFilter = "PRD" + projectTypeFilter = "PRD" secretIds = @{ clientSecret = Get-SecretsId -secret AzureAd--ClientSecret serviceBus = Get-SecretsId -secret Connectionstrings--ServiceBus @@ -61,12 +62,14 @@ steps: notifications = "https://fusion-s-notification-$fusionEnvironment.azurewebsites.net" context = "https://fusion-s-context-$fusionEnvironment.azurewebsites.net" portal = "https://fusion-s-portal-$fusionEnvironment.azurewebsites.net" + roles = "https://fusion-s-roles-$fusionEnvironment.azurewebsites.net" } resources = @{ fusion = "${{ parameters.fusionResource }}" } queues = @{ departmentSummaryWeeklyQueue = "scheduled-weekly-department-report" + projectSummaryWeeklyQueue = "scheduled-weekly-project-report" } } diff --git a/src/Fusion.Summary.Functions/Deployment/function.template.json b/src/Fusion.Summary.Functions/Deployment/function.template.json index f74b9a60f..8701b01e0 100644 --- a/src/Fusion.Summary.Functions/Deployment/function.template.json +++ b/src/Fusion.Summary.Functions/Deployment/function.template.json @@ -162,6 +162,10 @@ "name": "Endpoints_summary", "value": "[parameters('settings').endpoints.summary]" }, + { + "name": "Endpoints_roles", + "value": "[parameters('settings').endpoints.roles]" + }, { "name": "Endpoints_Resources_Fusion", "value": "[parameters('settings').resources.fusion]" @@ -170,9 +174,17 @@ "name": "department_summary_weekly_queue", "value": "[parameters('settings').queues.departmentSummaryWeeklyQueue]" }, + { + "name": "project_summary_weekly_queue", + "value": "[parameters('settings').queues.projectSummaryWeeklyQueue]" + }, { "name": "departmentFilter", "value": "[parameters('settings').departmentFilter]" + }, + { + "name": "projectTypeFilter", + "value": "[parameters('settings').projectTypeFilter]" } ] } From 5db3dc72425ed24a388738d5c7345bb8fecab550 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:12:00 +0100 Subject: [PATCH 06/17] Send custom queue message with project admins --- .../TaskOwnerReports/ProjectTaskOwnerSync.cs | 43 +++++++++++++------ .../Models/WeeklyTaskOwnerReportMessage.cs | 25 +++++++++++ 2 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index 8e21832b2..ec5622d0b 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -6,6 +6,7 @@ using Fusion.Resources.Functions.Common.ApiClients; using Fusion.Resources.Functions.Common.Extensions; using Fusion.Summary.Functions.Functions.Helpers; +using Fusion.Summary.Functions.Models; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -83,26 +84,25 @@ public async Task RunAsync( var projectAdminsMapping = await rolesApiClient.GetAdminRolesForOrgProjects(activeProjects.Select(p => p.ProjectId)); - var projectToEnqueueTimeMapping = QueueTimeHelper.CalculateEnqueueTime(activeProjects, _totalBatchTime, logger); logger.LogInformation("Syncing projects and admins"); - foreach (var (project, queueTime) in projectToEnqueueTimeMapping) + foreach (var (orgproject, queueTime) in projectToEnqueueTimeMapping) { - var projectAdmins = projectAdminsMapping.TryGetValue(project.ProjectId, out var values) ? values : []; - var projectDirector = project.Director.Instances + var projectAdmins = projectAdminsMapping.TryGetValue(orgproject.ProjectId, out var values) ? values : []; + var projectDirector = orgproject.Director.Instances .FirstOrDefault(i => i.AssignedPerson is not null && i.AppliesFrom <= DateTime.UtcNow && i.AppliesTo >= DateTime.UtcNow)?.AssignedPerson; // OrgProjectExternalId is the common key between the two systems, org api and summary api // We use this to see if we're updating or creating a new project entity - var existingProjectId = existingSummaryProjects.FirstOrDefault(p => p.OrgProjectExternalId == project.ProjectId)?.Id; + var existingProjectId = existingSummaryProjects.FirstOrDefault(p => p.OrgProjectExternalId == orgproject.ProjectId)?.Id; var apiProject = new ApiProject() { Id = existingProjectId ?? Guid.NewGuid(), - OrgProjectExternalId = project.ProjectId, - Name = project.Name, + OrgProjectExternalId = orgproject.ProjectId, + Name = orgproject.Name, DirectorAzureUniqueId = projectDirector?.AzureUniqueId, AssignedAdminsAzureUniqueId = projectAdmins .Where(p => p.Person?.AzureUniqueId is not null) @@ -116,26 +116,43 @@ public async Task RunAsync( } catch (SummaryApiError e) { - logger.LogCritical(e, "Failed to PUT project {Project}", project.ToJson()); + logger.LogCritical(e, "Failed to PUT project {Project}", orgproject.ToJson()); continue; } + + var message = new WeeklyTaskOwnerReportMessage() + { + ProjectId = apiProject.Id, + OrgProjectExternalId = apiProject.OrgProjectExternalId, + ProjectName = apiProject.Name, + ProjectAdmins = projectAdmins + .Where(p => p.Person?.AzureUniqueId is not null) + .Select(p => new WeeklyTaskOwnerReportMessage.ProjectAdmin() + { + AzureUniqueId = p.Person!.AzureUniqueId, + UPN = p.Person!.Upn, + ValidTo = p.ValidTo + }) + .ToArray() + }; + try { - await SendProjectToQueue(sender, apiProject, queueTime); + await SendProjectToQueue(sender, message, queueTime); } catch (Exception e) { logger.LogCritical(e, "Failed to send project to queue {Project}", apiProject.ToJson()); } - - logger.LogInformation("{FunctionName} completed", FunctionName); } + + logger.LogInformation("{FunctionName} completed", FunctionName); } - private async Task SendProjectToQueue(ServiceBusSender sender, ApiProject project, DateTimeOffset enqueueTime) + private async Task SendProjectToQueue(ServiceBusSender sender, WeeklyTaskOwnerReportMessage projectMessage, DateTimeOffset enqueueTime) { - var serializedDto = JsonConvert.SerializeObject(project); + var serializedDto = JsonConvert.SerializeObject(projectMessage); var message = new ServiceBusMessage(Encoding.UTF8.GetBytes(serializedDto)) { diff --git a/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs b/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs new file mode 100644 index 000000000..d21277401 --- /dev/null +++ b/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs @@ -0,0 +1,25 @@ +using System; + +namespace Fusion.Summary.Functions.Models; + +/// Message sent between weekly task owner report sync and worker +public class WeeklyTaskOwnerReportMessage +{ + public required Guid ProjectId { get; set; } + public required Guid OrgProjectExternalId { get; set; } + + /// Mainly for debugging purposes + public required string ProjectName { get; set; } + + public required ProjectAdmin[] ProjectAdmins { get; set; } + + public class ProjectAdmin + { + public required Guid AzureUniqueId { get; set; } + + public string? UPN { get; set; } + + /// Can be null due to being nullable from the roles api + public required DateTimeOffset? ValidTo { get; set; } + } +} \ No newline at end of file From b4a6ace7b4626012ac466c82829d806fdce84824 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:35:07 +0100 Subject: [PATCH 07/17] Adjustments --- .../ApiClients/IRolesApiClient.cs | 2 -- .../ApiClients/RolesApiClient.cs | 29 ++++++------------- .../TaskOwnerReports/ProjectTaskOwnerSync.cs | 19 ++++++------ .../Models/WeeklyTaskOwnerReportMessage.cs | 2 ++ 4 files changed, 21 insertions(+), 31 deletions(-) diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs index a6f9cada0..2637a079e 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs @@ -5,6 +5,4 @@ namespace Fusion.Resources.Functions.Common.ApiClients; public interface IRolesApiClient { public Task>> GetAdminRolesForOrgProjects(IEnumerable projectIds); - - public Task>> GetExpiringAdminRolesForOrgProjects(IEnumerable projectIds, int monthsUntilExpiry); } \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs index 68a964552..f3dd5ad28 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs @@ -1,4 +1,5 @@ -using Fusion.Resources.Functions.Common.ApiClients.ApiModels; +using System.Net.Http.Json; +using Fusion.Resources.Functions.Common.ApiClients.ApiModels; using Fusion.Resources.Functions.Common.Integration.Http; namespace Fusion.Resources.Functions.Common.ApiClients; @@ -13,33 +14,21 @@ public RolesApiClient(IHttpClientFactory clientFactory) } private static string GetActiveOrgAdminsOdataQuery() => "scope.type eq 'OrgChart' and roleName eq 'Fusion.OrgChart.Admin' and source eq 'Fusion.Roles' and " + - $"validTo gteq '{DateTime.UtcNow:O}'"; + $"validTo gt '{DateTime.UtcNow:O}'"; public async Task>> GetAdminRolesForOrgProjects(IEnumerable projectIds) { var odataQuery = new ODataQuery(); - odataQuery.Filter = GetActiveOrgAdminsOdataQuery() + $" and scope.value in ({string.Join(',', projectIds.Select(p => $"'{p}'"))})"; + odataQuery.Filter = GetActiveOrgAdminsOdataQuery(); var url = ODataQuery.ApplyQueryString("/roles", odataQuery); - var data = await rolesClient.GetAsJsonAsync>(url); + var data = await rolesClient.GetFromJsonAsync>(url); - return data.GroupBy(r => Guid.Parse(r.Scope.Value)) - .ToDictionary(g => g.Key, ICollection (g) => g.ToArray()); - } - - public async Task>> GetExpiringAdminRolesForOrgProjects(IEnumerable projectIds, int monthsUntilExpiry) - { - var odataQuery = new ODataQuery(); - - var expiryDate = DateTime.UtcNow.AddMonths(monthsUntilExpiry).ToString("O"); - - odataQuery.Filter = GetActiveOrgAdminsOdataQuery() + $" and validTo lteq '{expiryDate}' and scope.value in ({string.Join(',', projectIds.Select(p => $"'{p}'"))})"; - - var url = ODataQuery.ApplyQueryString("/roles", odataQuery); - var data = await rolesClient.GetAsJsonAsync>(url); - - return data.GroupBy(r => Guid.Parse(r.Scope.Value)) + return data + // Filter roles to projects in memory to avoid a very lage OData query (url) + .Where(r => Guid.TryParse(r.Scope.Value, out var projectId) && projectIds.Contains(projectId)) + .GroupBy(r => Guid.Parse(r.Scope.Value)) .ToDictionary(g => g.Key, ICollection (g) => g.ToArray()); } } \ No newline at end of file diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index ec5622d0b..cdbc93126 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -70,14 +70,15 @@ public async Task RunAsync( { queryFilter.Filter = $"projectType in ({string.Join(',', _projectTypeFilter.Select(s => $"'{s}'"))})"; // TODO: Filter on active projects when odata support is added in org - // + " and state eq 'ACTIVE'"; + // + " and state in ('ACTIVE', 'null', '')"; } var projects = await orgClient.GetProjects(queryFilter); var existingSummaryProjects = await summaryApiClient.GetProjectsAsync(); + // TODO: Remove this when odata state support is added in org var activeProjects = projects - .Where(p => p.State.Equals("ACTIVE", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(p.State)) + .Where(p => p.State.Equals("ACTIVE", StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(p.State)) .ToList(); logger.LogInformation("Found {ProjectCount} active projects {Projects}", activeProjects.Count, activeProjects.Select(p => new { p.ProjectId, p.Name, p.DomainId }).ToJson()); @@ -88,21 +89,21 @@ public async Task RunAsync( logger.LogInformation("Syncing projects and admins"); - foreach (var (orgproject, queueTime) in projectToEnqueueTimeMapping) + foreach (var (orgProject, queueTime) in projectToEnqueueTimeMapping) { - var projectAdmins = projectAdminsMapping.TryGetValue(orgproject.ProjectId, out var values) ? values : []; - var projectDirector = orgproject.Director.Instances + var projectAdmins = projectAdminsMapping.TryGetValue(orgProject.ProjectId, out var values) ? values : []; + var projectDirector = orgProject.Director.Instances .FirstOrDefault(i => i.AssignedPerson is not null && i.AppliesFrom <= DateTime.UtcNow && i.AppliesTo >= DateTime.UtcNow)?.AssignedPerson; // OrgProjectExternalId is the common key between the two systems, org api and summary api // We use this to see if we're updating or creating a new project entity - var existingProjectId = existingSummaryProjects.FirstOrDefault(p => p.OrgProjectExternalId == orgproject.ProjectId)?.Id; + var existingProjectId = existingSummaryProjects.FirstOrDefault(p => p.OrgProjectExternalId == orgProject.ProjectId)?.Id; var apiProject = new ApiProject() { Id = existingProjectId ?? Guid.NewGuid(), - OrgProjectExternalId = orgproject.ProjectId, - Name = orgproject.Name, + OrgProjectExternalId = orgProject.ProjectId, + Name = orgProject.Name, DirectorAzureUniqueId = projectDirector?.AzureUniqueId, AssignedAdminsAzureUniqueId = projectAdmins .Where(p => p.Person?.AzureUniqueId is not null) @@ -116,7 +117,7 @@ public async Task RunAsync( } catch (SummaryApiError e) { - logger.LogCritical(e, "Failed to PUT project {Project}", orgproject.ToJson()); + logger.LogCritical(e, "Failed to PUT project {Project}", orgProject.ToJson()); continue; } diff --git a/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs b/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs index d21277401..834f7b4df 100644 --- a/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs +++ b/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics; namespace Fusion.Summary.Functions.Models; /// Message sent between weekly task owner report sync and worker +[DebuggerDisplay("ProjectName: {ProjectName} - ProjectAdmins count: {ProjectAdmins.Length}")] public class WeeklyTaskOwnerReportMessage { public required Guid ProjectId { get; set; } From 2989d7f2f619d70a39551046b57493ff480af9b0 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:44:25 +0100 Subject: [PATCH 08/17] Added cancellation support --- .../ApiClients/ApiModels/Roles.cs | 11 ----------- .../ApiClients/IOrgApiClient.cs | 2 +- .../ApiClients/IRolesApiClient.cs | 2 +- .../ApiClients/OrgApiClient.cs | 7 +++---- .../ApiClients/RolesApiClient.cs | 6 ++++-- .../Integration/Http/HttpClientExtensions.cs | 10 +++++----- .../TaskOwnerReports/ProjectTaskOwnerSync.cs | 15 ++++++++++----- 7 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs index 8480eda8e..8581a2cd9 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs @@ -1,22 +1,11 @@ using Fusion.Services.LineOrg.ApiModels; -using Newtonsoft.Json; namespace Fusion.Resources.Functions.Common.ApiClients.ApiModels; public class ApiSinglePersonRole { - public Guid Id { get; init; } - - public string RoleName { get; init; } = null!; - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string? Identifier { get; set; } - - public string? Source { get; init; } - public ApiSingleRoleScope Scope { get; init; } = null!; - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ApiPerson? Person { get; set; } public DateTimeOffset? ValidTo { get; init; } diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/IOrgApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/IOrgApiClient.cs index 225564f5e..c151e7a4d 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/IOrgApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/IOrgApiClient.cs @@ -8,7 +8,7 @@ public interface IOrgClient { Task GetChangeLog(string projectId, DateTime timestamp); - Task> GetProjects(ODataQuery? query = null); + Task> GetProjectsAsync(ODataQuery? query = null, CancellationToken cancellationToken = default); } #region model diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs index 2637a079e..337ce3c12 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs @@ -4,5 +4,5 @@ namespace Fusion.Resources.Functions.Common.ApiClients; public interface IRolesApiClient { - public Task>> GetAdminRolesForOrgProjects(IEnumerable projectIds); + public Task>> GetAdminRolesForOrgProjects(IEnumerable projectIds, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/OrgApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/OrgApiClient.cs index d5606de92..cacf141d7 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/OrgApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/OrgApiClient.cs @@ -1,5 +1,4 @@ -#nullable enable -using Fusion.Resources.Functions.Common.Integration.Http; +using Fusion.Resources.Functions.Common.Integration.Http; using Fusion.Services.Org.ApiModels; namespace Fusion.Resources.Functions.Common.ApiClients; @@ -24,9 +23,9 @@ await orgClient.GetAsJsonAsync( return data; } - public Task> GetProjects(ODataQuery? query = null) + public Task> GetProjectsAsync(ODataQuery? query = null, CancellationToken cancellationToken = default) { var url = ODataQuery.ApplyQueryString("/projects", query); - return orgClient.GetAsJsonAsync>(url); + return orgClient.GetAsJsonAsync>(url, cancellationToken: cancellationToken); } } \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs index f3dd5ad28..69675fd34 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs @@ -1,4 +1,5 @@ using System.Net.Http.Json; +using System.Text.Json; using Fusion.Resources.Functions.Common.ApiClients.ApiModels; using Fusion.Resources.Functions.Common.Integration.Http; @@ -16,14 +17,15 @@ public RolesApiClient(IHttpClientFactory clientFactory) private static string GetActiveOrgAdminsOdataQuery() => "scope.type eq 'OrgChart' and roleName eq 'Fusion.OrgChart.Admin' and source eq 'Fusion.Roles' and " + $"validTo gt '{DateTime.UtcNow:O}'"; - public async Task>> GetAdminRolesForOrgProjects(IEnumerable projectIds) + public async Task>> GetAdminRolesForOrgProjects(IEnumerable projectIds, CancellationToken cancellationToken = default) { var odataQuery = new ODataQuery(); odataQuery.Filter = GetActiveOrgAdminsOdataQuery(); var url = ODataQuery.ApplyQueryString("/roles", odataQuery); - var data = await rolesClient.GetFromJsonAsync>(url); + var data = await rolesClient.GetFromJsonAsync>(url, cancellationToken: cancellationToken) + ?? throw new InvalidOperationException("Roles response was null"); return data // Filter roles to projects in memory to avoid a very lage OData query (url) diff --git a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientExtensions.cs b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientExtensions.cs index 730c7de96..26e561a79 100644 --- a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientExtensions.cs +++ b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientExtensions.cs @@ -5,10 +5,10 @@ namespace Fusion.Resources.Functions.Common.Integration.Http { public static class HttpClientExtensions { - public static async Task GetAsJsonAsync(this HttpClient client, string url) where T : class + public static async Task GetAsJsonAsync(this HttpClient client, string url, CancellationToken cancellationToken = default) where T : class { - var response = await client.GetAsync(url); - var body = await response.Content.ReadAsStringAsync(); + var response = await client.GetAsync(url, cancellationToken); + var body = await response.Content.ReadAsStringAsync(cancellationToken); if (!response.IsSuccessStatusCode) throw new ApiError(response.RequestMessage!.RequestUri!.ToString(), response.StatusCode, body, "Response from API call indicates error"); @@ -17,10 +17,10 @@ public static async Task GetAsJsonAsync(this HttpClient client, string url return deserialized; } - public static async Task> OptionsAsync(this HttpClient client, string url) + public static async Task> OptionsAsync(this HttpClient client, string url, CancellationToken cancellationToken = default) { var message = new HttpRequestMessage(HttpMethod.Options, url); - var resp = await client.SendAsync(message); + var resp = await client.SendAsync(message, cancellationToken); resp.Content.Headers.TryGetValues("Allow", out var allowHeaders); diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index cdbc93126..a0bb13866 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Fusion.Resources.Functions.Common.ApiClients; @@ -57,13 +58,15 @@ public ProjectTaskOwnerSync(ILogger logger, IConfiguration [FunctionName(FunctionName)] public async Task RunAsync( [TimerTrigger("0 5 0 * * MON", RunOnStartup = false)] - TimerInfo myTimer) + TimerInfo myTimer, CancellationToken cancellationToken) { var client = new ServiceBusClient(_serviceBusConnectionString); var sender = client.CreateSender(_weeklySummaryQueueName); logger.LogInformation("{FunctionName} triggered with projectTypeFilter {ProjectTypeFilter}", FunctionName, _projectTypeFilter.ToJson()); + #region Retrieve projects and admins + var queryFilter = new ODataQuery(); if (_projectTypeFilter.Length != 0) @@ -73,8 +76,8 @@ public async Task RunAsync( // + " and state in ('ACTIVE', 'null', '')"; } - var projects = await orgClient.GetProjects(queryFilter); - var existingSummaryProjects = await summaryApiClient.GetProjectsAsync(); + var projects = await orgClient.GetProjectsAsync(queryFilter, cancellationToken); + var existingSummaryProjects = await summaryApiClient.GetProjectsAsync(cancellationToken); // TODO: Remove this when odata state support is added in org var activeProjects = projects @@ -82,11 +85,13 @@ public async Task RunAsync( .ToList(); logger.LogInformation("Found {ProjectCount} active projects {Projects}", activeProjects.Count, activeProjects.Select(p => new { p.ProjectId, p.Name, p.DomainId }).ToJson()); - var projectAdminsMapping = await rolesApiClient.GetAdminRolesForOrgProjects(activeProjects.Select(p => p.ProjectId)); + var projectAdminsMapping = await rolesApiClient.GetAdminRolesForOrgProjects(activeProjects.Select(p => p.ProjectId), cancellationToken); var projectToEnqueueTimeMapping = QueueTimeHelper.CalculateEnqueueTime(activeProjects, _totalBatchTime, logger); + #endregion + logger.LogInformation("Syncing projects and admins"); foreach (var (orgProject, queueTime) in projectToEnqueueTimeMapping) @@ -113,7 +118,7 @@ public async Task RunAsync( try { - apiProject = await summaryApiClient.PutProjectAsync(apiProject); + apiProject = await summaryApiClient.PutProjectAsync(apiProject, CancellationToken.None); } catch (SummaryApiError e) { From f11f5e658092af7b425cf7abc6a1918d4cce67d0 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:47:38 +0100 Subject: [PATCH 09/17] Updated logging --- .../Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index a0bb13866..35ff21e4c 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -149,7 +149,7 @@ public async Task RunAsync( } catch (Exception e) { - logger.LogCritical(e, "Failed to send project to queue {Project}", apiProject.ToJson()); + logger.LogCritical(e, "Failed to send project to queue {Project}", message.ToJson()); } } From 09e94c112955aa71a2e7a7da21e4007ee536ba1b Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:50:05 +0100 Subject: [PATCH 10/17] Filter director instances to normal and rotation --- .../Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index 35ff21e4c..3e6405fad 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -6,6 +6,7 @@ using Azure.Messaging.ServiceBus; using Fusion.Resources.Functions.Common.ApiClients; using Fusion.Resources.Functions.Common.Extensions; +using Fusion.Services.Org.ApiModels; using Fusion.Summary.Functions.Functions.Helpers; using Fusion.Summary.Functions.Models; using Microsoft.Azure.WebJobs; @@ -98,6 +99,7 @@ public async Task RunAsync( { var projectAdmins = projectAdminsMapping.TryGetValue(orgProject.ProjectId, out var values) ? values : []; var projectDirector = orgProject.Director.Instances + .Where(i => i.Type == ApiPositionInstanceV2.ApiInstanceType.Normal.ToString() || i.Type == ApiPositionInstanceV2.ApiInstanceType.Rotation.ToString()) .FirstOrDefault(i => i.AssignedPerson is not null && i.AppliesFrom <= DateTime.UtcNow && i.AppliesTo >= DateTime.UtcNow)?.AssignedPerson; // OrgProjectExternalId is the common key between the two systems, org api and summary api From b6200f65f63bcd5b9f56d3946e7a09e618f5b56a Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:02:46 +0100 Subject: [PATCH 11/17] Fixed incorrect roles apimodel --- .../ApiClients/ApiModels/Roles.cs | 15 ++++++++++----- .../TaskOwnerReports/ProjectTaskOwnerSync.cs | 14 +++++++------- .../Models/WeeklyTaskOwnerReportMessage.cs | 15 ++++++++------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs index 8581a2cd9..9074aea4b 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Roles.cs @@ -1,12 +1,11 @@ -using Fusion.Services.LineOrg.ApiModels; - + namespace Fusion.Resources.Functions.Common.ApiClients.ApiModels; public class ApiSinglePersonRole { public ApiSingleRoleScope Scope { get; init; } = null!; - public ApiPerson? Person { get; set; } + public ApiPerson? Person { get; init; } public DateTimeOffset? ValidTo { get; init; } } @@ -19,6 +18,12 @@ public ApiSingleRoleScope(string type, string value) Value = value; } - public string Type { get; set; } - public string Value { get; set; } + public string Type { get; init; } + public string Value { get; init; } +} + +public class ApiPerson +{ + public Guid Id { get; init; } + public string? Mail { get; init; } } \ No newline at end of file diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index 3e6405fad..aa20e449b 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -61,8 +61,8 @@ public async Task RunAsync( [TimerTrigger("0 5 0 * * MON", RunOnStartup = false)] TimerInfo myTimer, CancellationToken cancellationToken) { - var client = new ServiceBusClient(_serviceBusConnectionString); - var sender = client.CreateSender(_weeklySummaryQueueName); + await using var client = new ServiceBusClient(_serviceBusConnectionString); + await using var sender = client.CreateSender(_weeklySummaryQueueName); logger.LogInformation("{FunctionName} triggered with projectTypeFilter {ProjectTypeFilter}", FunctionName, _projectTypeFilter.ToJson()); @@ -113,8 +113,8 @@ public async Task RunAsync( Name = orgProject.Name, DirectorAzureUniqueId = projectDirector?.AzureUniqueId, AssignedAdminsAzureUniqueId = projectAdmins - .Where(p => p.Person?.AzureUniqueId is not null) - .Select(p => p.Person!.AzureUniqueId) + .Where(p => p.Person is not null && p.Person.Id != Guid.Empty) + .Select(p => p.Person!.Id) .ToArray() }; @@ -135,11 +135,11 @@ public async Task RunAsync( OrgProjectExternalId = apiProject.OrgProjectExternalId, ProjectName = apiProject.Name, ProjectAdmins = projectAdmins - .Where(p => p.Person?.AzureUniqueId is not null) + .Where(p => p.Person is not null && p.Person.Id != Guid.Empty) .Select(p => new WeeklyTaskOwnerReportMessage.ProjectAdmin() { - AzureUniqueId = p.Person!.AzureUniqueId, - UPN = p.Person!.Upn, + AzureUniqueId = p.Person!.Id, + Mail = p.Person!.Mail, ValidTo = p.ValidTo }) .ToArray() diff --git a/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs b/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs index 834f7b4df..f544f508f 100644 --- a/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs +++ b/src/Fusion.Summary.Functions/Models/WeeklyTaskOwnerReportMessage.cs @@ -7,21 +7,22 @@ namespace Fusion.Summary.Functions.Models; [DebuggerDisplay("ProjectName: {ProjectName} - ProjectAdmins count: {ProjectAdmins.Length}")] public class WeeklyTaskOwnerReportMessage { - public required Guid ProjectId { get; set; } - public required Guid OrgProjectExternalId { get; set; } + public required Guid ProjectId { get; init; } + public required Guid OrgProjectExternalId { get; init; } /// Mainly for debugging purposes - public required string ProjectName { get; set; } + public required string ProjectName { get; init; } - public required ProjectAdmin[] ProjectAdmins { get; set; } + public required ProjectAdmin[] ProjectAdmins { get; init; } + [DebuggerDisplay("AzureUniqueId: {AzureUniqueId} - Mail: {Mail} - ValidTo: {ValidTo}")] public class ProjectAdmin { - public required Guid AzureUniqueId { get; set; } + public required Guid AzureUniqueId { get; init; } - public string? UPN { get; set; } + public string? Mail { get; init; } /// Can be null due to being nullable from the roles api - public required DateTimeOffset? ValidTo { get; set; } + public required DateTimeOffset? ValidTo { get; init; } } } \ No newline at end of file From 91eee9c21fbdc1c68a37cce76b8f28b1f3ac90b0 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:09:07 +0100 Subject: [PATCH 12/17] Don't create or send reports for projects with no admins/director --- .../TaskOwnerReports/ProjectTaskOwnerSync.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index aa20e449b..b706838fc 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -18,12 +18,12 @@ namespace Fusion.Summary.Functions.Functions.TaskOwnerReports; public class ProjectTaskOwnerSync { - private const string FunctionName = "weekly-project-recipients-sync"; private readonly ILogger logger; private readonly IOrgClient orgClient; private readonly IRolesApiClient rolesApiClient; private readonly ISummaryApiClient summaryApiClient; + // Configuration variables private string _serviceBusConnectionString; private string _weeklySummaryQueueName; private string[] _projectTypeFilter; @@ -55,7 +55,7 @@ public ProjectTaskOwnerSync(ILogger logger, IConfiguration } } - + private const string FunctionName = "weekly-project-recipients-sync"; [FunctionName(FunctionName)] public async Task RunAsync( [TimerTrigger("0 5 0 * * MON", RunOnStartup = false)] @@ -97,11 +97,22 @@ public async Task RunAsync( foreach (var (orgProject, queueTime) in projectToEnqueueTimeMapping) { - var projectAdmins = projectAdminsMapping.TryGetValue(orgProject.ProjectId, out var values) ? values : []; + // Get recipients + var projectAdmins = projectAdminsMapping.TryGetValue(orgProject.ProjectId, out var values) + ? values.Where(p => p.Person is not null && p.Person.Id != Guid.Empty).ToArray() + : []; + var projectDirector = orgProject.Director.Instances .Where(i => i.Type == ApiPositionInstanceV2.ApiInstanceType.Normal.ToString() || i.Type == ApiPositionInstanceV2.ApiInstanceType.Rotation.ToString()) .FirstOrDefault(i => i.AssignedPerson is not null && i.AppliesFrom <= DateTime.UtcNow && i.AppliesTo >= DateTime.UtcNow)?.AssignedPerson; + // No point in storing project and creating a project if there are no recipients + if (projectDirector is null && projectAdmins.Length == 0) + { + logger.LogInformation("Project {Name} ({ProjectId}) has no director or admins, skipping", orgProject.Name, orgProject.ProjectId); + continue; + } + // OrgProjectExternalId is the common key between the two systems, org api and summary api // We use this to see if we're updating or creating a new project entity var existingProjectId = existingSummaryProjects.FirstOrDefault(p => p.OrgProjectExternalId == orgProject.ProjectId)?.Id; @@ -113,7 +124,6 @@ public async Task RunAsync( Name = orgProject.Name, DirectorAzureUniqueId = projectDirector?.AzureUniqueId, AssignedAdminsAzureUniqueId = projectAdmins - .Where(p => p.Person is not null && p.Person.Id != Guid.Empty) .Select(p => p.Person!.Id) .ToArray() }; @@ -135,7 +145,6 @@ public async Task RunAsync( OrgProjectExternalId = apiProject.OrgProjectExternalId, ProjectName = apiProject.Name, ProjectAdmins = projectAdmins - .Where(p => p.Person is not null && p.Person.Id != Guid.Empty) .Select(p => new WeeklyTaskOwnerReportMessage.ProjectAdmin() { AzureUniqueId = p.Person!.Id, From b7c981d83f9b4a6ffd077ecf1d5de3458f2d2790 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:12:49 +0100 Subject: [PATCH 13/17] Use ODATA filter to retrieve active projects --- .../TaskOwnerReports/ProjectTaskOwnerSync.cs | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index b706838fc..af6918d71 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; @@ -56,6 +57,7 @@ public ProjectTaskOwnerSync(ILogger logger, IConfiguration } private const string FunctionName = "weekly-project-recipients-sync"; + [FunctionName(FunctionName)] public async Task RunAsync( [TimerTrigger("0 5 0 * * MON", RunOnStartup = false)] @@ -68,28 +70,14 @@ public async Task RunAsync( #region Retrieve projects and admins - var queryFilter = new ODataQuery(); - - if (_projectTypeFilter.Length != 0) - { - queryFilter.Filter = $"projectType in ({string.Join(',', _projectTypeFilter.Select(s => $"'{s}'"))})"; - // TODO: Filter on active projects when odata support is added in org - // + " and state in ('ACTIVE', 'null', '')"; - } - - var projects = await orgClient.GetProjectsAsync(queryFilter, cancellationToken); + var projects = await GetActiveOrgProjectsAsync(cancellationToken); var existingSummaryProjects = await summaryApiClient.GetProjectsAsync(cancellationToken); - // TODO: Remove this when odata state support is added in org - var activeProjects = projects - .Where(p => p.State.Equals("ACTIVE", StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(p.State)) - .ToList(); + logger.LogInformation("Found {ProjectCount} active projects {Projects}", projects.Count, projects.Select(p => new { p.ProjectId, p.Name, p.DomainId }).ToJson()); - logger.LogInformation("Found {ProjectCount} active projects {Projects}", activeProjects.Count, activeProjects.Select(p => new { p.ProjectId, p.Name, p.DomainId }).ToJson()); - var projectAdminsMapping = await rolesApiClient.GetAdminRolesForOrgProjects(activeProjects.Select(p => p.ProjectId), cancellationToken); + var projectAdminsMapping = await rolesApiClient.GetAdminRolesForOrgProjects(projects.Select(p => p.ProjectId), cancellationToken); - - var projectToEnqueueTimeMapping = QueueTimeHelper.CalculateEnqueueTime(activeProjects, _totalBatchTime, logger); + var projectToEnqueueTimeMapping = QueueTimeHelper.CalculateEnqueueTime(projects, _totalBatchTime, logger); #endregion @@ -178,4 +166,21 @@ private async Task SendProjectToQueue(ServiceBusSender sender, WeeklyTaskOwnerRe await sender.SendMessageAsync(message); } + + private async Task> GetActiveOrgProjectsAsync(CancellationToken cancellationToken) + { + var queryFilter = new ODataQuery(); + + if (_projectTypeFilter.Length != 0) + { + queryFilter.Filter = $"projectType in ({string.Join(',', _projectTypeFilter.Select(s => $"'{s}'"))})"; + } + + const string stateFilter = "state in ('ACTIVE', 'null')"; + + queryFilter.Filter = queryFilter.Filter is null ? stateFilter : $"{queryFilter.Filter} and {stateFilter}"; + + var projects = await orgClient.GetProjectsAsync(queryFilter, cancellationToken); + return projects; + } } \ No newline at end of file From 065c5695e5a1df1537036d194974a211f7b081fc Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:25:06 +0100 Subject: [PATCH 14/17] Minor adjustments --- .../ApiClients/IRolesApiClient.cs | 2 +- .../ApiClients/RolesApiClient.cs | 2 +- .../Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs index 337ce3c12..82b5c13af 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/IRolesApiClient.cs @@ -4,5 +4,5 @@ namespace Fusion.Resources.Functions.Common.ApiClients; public interface IRolesApiClient { - public Task>> GetAdminRolesForOrgProjects(IEnumerable projectIds, CancellationToken cancellationToken = default); + public Task>> GetAdminRolesForOrgProjects(ICollection projectIds, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs index 69675fd34..7768940ec 100644 --- a/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs +++ b/src/Fusion.Resources.Functions.Common/ApiClients/RolesApiClient.cs @@ -17,7 +17,7 @@ public RolesApiClient(IHttpClientFactory clientFactory) private static string GetActiveOrgAdminsOdataQuery() => "scope.type eq 'OrgChart' and roleName eq 'Fusion.OrgChart.Admin' and source eq 'Fusion.Roles' and " + $"validTo gt '{DateTime.UtcNow:O}'"; - public async Task>> GetAdminRolesForOrgProjects(IEnumerable projectIds, CancellationToken cancellationToken = default) + public async Task>> GetAdminRolesForOrgProjects(ICollection projectIds, CancellationToken cancellationToken = default) { var odataQuery = new ODataQuery(); diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs index af6918d71..2d66b8a54 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/ProjectTaskOwnerSync.cs @@ -68,19 +68,15 @@ public async Task RunAsync( logger.LogInformation("{FunctionName} triggered with projectTypeFilter {ProjectTypeFilter}", FunctionName, _projectTypeFilter.ToJson()); - #region Retrieve projects and admins - var projects = await GetActiveOrgProjectsAsync(cancellationToken); var existingSummaryProjects = await summaryApiClient.GetProjectsAsync(cancellationToken); logger.LogInformation("Found {ProjectCount} active projects {Projects}", projects.Count, projects.Select(p => new { p.ProjectId, p.Name, p.DomainId }).ToJson()); - var projectAdminsMapping = await rolesApiClient.GetAdminRolesForOrgProjects(projects.Select(p => p.ProjectId), cancellationToken); + var projectAdminsMapping = await rolesApiClient.GetAdminRolesForOrgProjects(projects.Select(p => p.ProjectId).ToArray(), cancellationToken); var projectToEnqueueTimeMapping = QueueTimeHelper.CalculateEnqueueTime(projects, _totalBatchTime, logger); - #endregion - logger.LogInformation("Syncing projects and admins"); foreach (var (orgProject, queueTime) in projectToEnqueueTimeMapping) From 6ddd406ef07c1a8dcbd8b9e7b73bdd36eeb2e1c2 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Wed, 6 Nov 2024 09:56:45 +0100 Subject: [PATCH 15/17] Disable az function --- src/Fusion.Summary.Functions/Deployment/disabled-functions.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Fusion.Summary.Functions/Deployment/disabled-functions.json b/src/Fusion.Summary.Functions/Deployment/disabled-functions.json index dc1a983a6..fea774561 100644 --- a/src/Fusion.Summary.Functions/Deployment/disabled-functions.json +++ b/src/Fusion.Summary.Functions/Deployment/disabled-functions.json @@ -11,11 +11,13 @@ { "environment": "fqa", "disabledFunctions": [ + "weekly-project-recipients-sync" ] }, { "environment": "fprd", "disabledFunctions": [ + "weekly-project-recipients-sync" ] } ] From a865642d343e78047dd407d5f430ee1c01d7bd1a Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:03:48 +0100 Subject: [PATCH 16/17] Trigger summary api pr env --- src/Fusion.Summary.Api/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fusion.Summary.Api/README.md b/src/Fusion.Summary.Api/README.md index 6a26de51b..35b8fee32 100644 --- a/src/Fusion.Summary.Api/README.md +++ b/src/Fusion.Summary.Api/README.md @@ -12,4 +12,4 @@ Migrations are by default added to the `Migrations` folder. ### Apply migration on a local environment -1. Run `dotnet ef database update --context SummaryDbContext` \ No newline at end of file +1. Run `dotnet ef database update --context SummaryDbContext` From afab2dec88c333b79f7623f610428a556bdf1d36 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:21:51 +0100 Subject: [PATCH 17/17] Update summary api pr env url for summary az function --- pipelines/templates/deploy-summary-function-pr-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipelines/templates/deploy-summary-function-pr-template.yml b/pipelines/templates/deploy-summary-function-pr-template.yml index 72afcec93..a1e2cec21 100644 --- a/pipelines/templates/deploy-summary-function-pr-template.yml +++ b/pipelines/templates/deploy-summary-function-pr-template.yml @@ -45,7 +45,7 @@ steps: # # Sett correct resources URI based on environment $resourcesFunctionUri = "https://resources-api-pr-$pullRequestNumber.fusion-dev.net/" - $summaryFunctionUri = "https://summary-api-pr-$pullRequestNumber.fusion-dev.net/" + $summaryFunctionUri = "https://fra-summary-$pullRequestNumber.pr.api.fusion-dev.net/" $settings = @{ clientId = "${{ parameters.clientId }}"