From 369d178c45e5a6f5f8430e5b8f20e63c3d59e97d Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 24 Jun 2024 12:12:40 -0700 Subject: [PATCH 0001/1479] set dropdown options for bit type attribute --- UI/web-app/src/store/sqlMembershipSources.api.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/UI/web-app/src/store/sqlMembershipSources.api.tsx b/UI/web-app/src/store/sqlMembershipSources.api.tsx index d2a749c34..f6be8eb3e 100644 --- a/UI/web-app/src/store/sqlMembershipSources.api.tsx +++ b/UI/web-app/src/store/sqlMembershipSources.api.tsx @@ -43,6 +43,9 @@ export const fetchAttributeValues = createAsyncThunk Date: Mon, 24 Jun 2024 12:13:46 -0700 Subject: [PATCH 0002/1479] fetch attribute values --- .../HRQuerySource/HRQuerySource.base.tsx | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 63d038c17..328e13842 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -101,20 +101,17 @@ export const HRQuerySourceBase: React.FunctionComponent = (p }, [children]); useEffect(() => { + let mappedItems = items.map((item) => ({ attribute: item.attribute, equalityOperator: item.equalityOperator, value: item.value, andOr: item.andOr })); + let att = mappedItems.map(item => item.attribute); + let distinctAttributes = [...new Set(att)]; + for (let i = 0; i < distinctAttributes.length; i++) { + if (attributeValues && attributeValues[distinctAttributes[i]] === undefined) { + const selectedAttribute = attributes?.find(({ hasMapping, name }) => ((hasMapping && `${name}_Code` === distinctAttributes[i]) || (!hasMapping && name === distinctAttributes[i]))); + dispatch(fetchAttributeValues({attribute: distinctAttributes[i] as string, type: selectedAttribute?.type, hasMapping: selectedAttribute?.hasMapping })); + } + } if (!groupingEnabled) { - const newGroups = [ - { - name: "", - items: items.map((item) => ({ - attribute: item.attribute, - equalityOperator: item.equalityOperator, - value: item.value, - andOr: item.andOr, - })), - children: [], - andOr: "" - }, - ]; + const newGroups = [{ name: "", items: mappedItems, children: [], andOr: "" }]; setGroups(newGroups); } }, [items]); @@ -215,15 +212,6 @@ const checkType = (value: string, type: string | undefined): string => { return valueOptions; }; - const getAttributeValues = (attribute: string, attributeValue: string) => { - const selectedAttribute = attributes?.find(({ hasMapping, name }) => ((hasMapping && `${name}_Code` === attribute) || (!hasMapping && name === attribute))); - dispatch(fetchAttributeValues({attribute: attribute, type: selectedAttribute?.type, hasMapping: selectedAttribute?.hasMapping })).then(results => { - var payload = results.payload as GetAttributeValuesResponse; - dispatch(setAttributeValues({ attribute: attribute, values: payload.values, type: selectedAttribute?.type })); - }).catch(error => {}); - return attributeValue; - } - useEffect(() => { if (props.source.filter && !groupingEnabled && (props.source.filter.includes("(") || props.source.filter.includes(")"))) { const groups = parseGroup(props.source.filter); @@ -707,7 +695,9 @@ const checkType = (value: string, type: string | undefined): string => { const handleAttributeChange = (event: React.FormEvent, item?: IComboBoxOption, index?: number, groupIndex?: number): void => { if (item) { const selectedAttribute = attributes?.find(({ hasMapping, name }) => ((hasMapping && `${name}_Code` === item.key) || (!hasMapping && name === item.key))); - dispatch(fetchAttributeValues({attribute: item.key as string, type: selectedAttribute?.type, hasMapping: selectedAttribute?.hasMapping })); + if (attributeValues && attributeValues[item.key] === undefined) { + dispatch(fetchAttributeValues({attribute: item.key as string, type: selectedAttribute?.type, hasMapping: selectedAttribute?.hasMapping })); + } const updatedItems = items.map((it, idx) => { if (idx === index) { return { ...it, attribute: item.text }; @@ -1258,7 +1248,7 @@ const checkType = (value: string, type: string | undefined): string => { /> } else { return handleTAttributeValueChange(item.attribute, event, newValue!, index)} onBlur={(event) => handleBlur(item.attribute, event, index)} styles={{ fieldGroup: classNames.textField }} From c398032e94241f2d5418cf13fc86cde8212d42d5 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 12 Jun 2024 13:05:13 -0700 Subject: [PATCH 0003/1479] Database migration for enum --- .../Models/NotificationType.cs | 3 +- .../GMMContext.cs | 24 + ...240612183023_notification_type.Designer.cs | 505 ++++++++++++++++++ .../20240612183023_notification_type.cs | 100 ++++ .../Migrations/GMMContextModelSnapshot.cs | 63 ++- 5 files changed, 691 insertions(+), 4 deletions(-) create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240612183023_notification_type.Designer.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240612183023_notification_type.cs diff --git a/Service/GroupMembershipManagement/Models/NotificationType.cs b/Service/GroupMembershipManagement/Models/NotificationType.cs index 00f712739..3e96129c2 100644 --- a/Service/GroupMembershipManagement/Models/NotificationType.cs +++ b/Service/GroupMembershipManagement/Models/NotificationType.cs @@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Models.CustomAttributes; +using Models.Notifications; namespace Models { @@ -15,7 +16,7 @@ public class NotificationType public int Id { get; set; } [Required] - public string Name { get; set; } + public NotificationMessageType Name { get; set; } public bool Disabled { get; set; } } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs index 01e8c1c4d..c5917afcf 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Models; +using Models.Notifications; using System.Text.Json; namespace Repositories.EntityFramework.Contexts @@ -140,7 +141,30 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(s => s.ChangeReason).IsRequired(); entity.Property(s => s.ChangeDetails).IsRequired(); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.Property(e => e.Name) + .HasConversion( + v => v.ToString(), + v => (NotificationMessageType)Enum.Parse(typeof(NotificationMessageType), v)) + .IsUnicode(false); + }); + SeedNotificationTypes(modelBuilder); + } + private void SeedNotificationTypes(ModelBuilder modelBuilder) + { + var notificationTypes = Enum.GetValues(typeof(NotificationMessageType)) + .Cast() + .Select((value, index) => new NotificationType + { + Id = index + 1, + Name = value, + Disabled = false + }); + modelBuilder.Entity().HasData(notificationTypes); } public GMMContext(DbContextOptions options) diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240612183023_notification_type.Designer.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240612183023_notification_type.Designer.cs new file mode 100644 index 000000000..fd6e1ee7a --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240612183023_notification_type.Designer.cs @@ -0,0 +1,505 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Repositories.EntityFramework.Contexts; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + [DbContext(typeof(GMMContext))] + [Migration("20240612183023_notification_type")] + partial class notification_type + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.22") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.Property("DestinationOwnersId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("DestinationOwnersId", "SyncJobsId"); + + b.HasIndex("SyncJobsId"); + + b.ToTable("DestinationOwnerSyncJob"); + }); + + modelBuilder.Entity("Entities.SqlMembershipSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Attributes") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomLabel") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SqlMembershipSources"); + + b.HasData( + new + { + Id = new Guid("907ecc0b-60b9-4a9e-b536-c14993d64e46"), + Name = "SqlMembership" + }); + }); + + modelBuilder.Entity("Entities.SyncJobChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("ChangeDetails") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeSource") + .HasColumnType("int"); + + b.Property("ChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValue(new DateTime(2024, 6, 12, 18, 30, 23, 125, DateTimeKind.Utc).AddTicks(6706)); + + b.Property("ChangedByDisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedByObjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ChangeSource"); + + b.HasIndex("ChangeTime"); + + b.HasIndex("ChangedByObjectId"); + + b.HasIndex("SyncJobId"); + + b.ToTable("SyncJobChanges"); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("DestinationNames"); + }); + + modelBuilder.Entity("Models.DestinationOwner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("ObjectId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.ToTable("DestinationOwners"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("NotificationTypeID") + .HasColumnType("int"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("NotificationTypeID"); + + b.HasIndex("SyncJobId", "NotificationTypeID") + .IsUnique(); + + b.ToTable("JobNotifications"); + }); + + modelBuilder.Entity("Models.NotificationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .IsUnicode(false) + .HasColumnType("varchar(max)"); + + b.HasKey("Id"); + + b.ToTable("NotificationTypes"); + + b.HasData( + new + { + Id = 1, + Disabled = false, + Name = "ThresholdNotification" + }, + new + { + Id = 2, + Disabled = false, + Name = "SyncStartedNotification" + }, + new + { + Id = 3, + Disabled = false, + Name = "SyncCompletedNotification" + }, + new + { + Id = 4, + Disabled = false, + Name = "DestinationNotExistNotification" + }, + new + { + Id = 5, + Disabled = false, + Name = "SourceNotExistNotification" + }, + new + { + Id = 6, + Disabled = false, + Name = "NotOwnerNotification" + }, + new + { + Id = 7, + Disabled = false, + Name = "NotValidSourceNotification" + }, + new + { + Id = 8, + Disabled = false, + Name = "NoDataNotification" + }, + new + { + Id = 9, + Disabled = false, + Name = "NormalThresholdNotification" + }); + }); + + modelBuilder.Entity("Models.PurgedSyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("PurgedAt") + .HasColumnType("datetime2"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PurgedSyncJobs"); + }); + + modelBuilder.Entity("Models.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("SettingKey") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SettingValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SettingKey") + .IsUnique(); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("Models.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SortPriority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Statuses", (string)null); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(450)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Status") + .IsUnique() + .HasFilter("[Status] IS NOT NULL"); + + b.ToTable("SyncJobs"); + }); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.HasOne("Models.DestinationOwner", null) + .WithMany() + .HasForeignKey("DestinationOwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.HasOne("Models.SyncJob", "SyncJob") + .WithOne("DestinationName") + .HasForeignKey("Models.DestinationName", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.HasOne("Models.NotificationType", "NotificationType") + .WithMany() + .HasForeignKey("NotificationTypeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", "SyncJob") + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationType"); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.HasOne("Models.Status", "StatusDetails") + .WithOne() + .HasForeignKey("Models.SyncJob", "Status") + .HasPrincipalKey("Models.Status", "Name") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("StatusDetails"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Navigation("DestinationName"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240612183023_notification_type.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240612183023_notification_type.cs new file mode 100644 index 000000000..28c14feb3 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240612183023_notification_type.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + public partial class notification_type : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + + migrationBuilder.AlterColumn( + name: "Name", + table: "NotificationTypes", + type: "varchar(max)", + unicode: false, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.Sql("DELETE FROM NotificationTypes"); + + migrationBuilder.InsertData( + table: "NotificationTypes", + columns: new[] { "Id", "Disabled", "Name" }, + values: new object[,] + { + { 1, false, "ThresholdNotification" }, + { 2, false, "SyncStartedNotification" }, + { 3, false, "SyncCompletedNotification" }, + { 4, false, "DestinationNotExistNotification" }, + { 5, false, "SourceNotExistNotification" }, + { 6, false, "NotOwnerNotification" }, + { 7, false, "NotValidSourceNotification" }, + { 8, false, "NoDataNotification" }, + { 9, false, "NormalThresholdNotification" } + }); + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 1); + + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 2); + + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 3); + + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 4); + + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 5); + + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 6); + + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 7); + + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 8); + + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 9); + + migrationBuilder.AlterColumn( + name: "Name", + table: "NotificationTypes", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(max)", + oldUnicode: false); + + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs index d742d9cef..c4840831d 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs @@ -64,7 +64,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasData( new { - Id = new Guid("e10dbc23-84d2-4843-b020-d5e2698a0f7a"), + Id = new Guid("907ecc0b-60b9-4a9e-b536-c14993d64e46"), Name = "SqlMembership" }); }); @@ -90,7 +90,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ChangeTime") .ValueGeneratedOnAdd() .HasColumnType("datetime2") - .HasDefaultValue(new DateTime(2024, 4, 8, 19, 7, 33, 472, DateTimeKind.Utc).AddTicks(8183)); + .HasDefaultValue(new DateTime(2024, 6, 12, 18, 30, 23, 125, DateTimeKind.Utc).AddTicks(6706)); b.Property("ChangedByDisplayName") .IsRequired() @@ -192,11 +192,68 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Name") .IsRequired() - .HasColumnType("nvarchar(max)"); + .IsUnicode(false) + .HasColumnType("varchar(max)"); b.HasKey("Id"); b.ToTable("NotificationTypes"); + + b.HasData( + new + { + Id = 1, + Disabled = false, + Name = "ThresholdNotification" + }, + new + { + Id = 2, + Disabled = false, + Name = "SyncStartedNotification" + }, + new + { + Id = 3, + Disabled = false, + Name = "SyncCompletedNotification" + }, + new + { + Id = 4, + Disabled = false, + Name = "DestinationNotExistNotification" + }, + new + { + Id = 5, + Disabled = false, + Name = "SourceNotExistNotification" + }, + new + { + Id = 6, + Disabled = false, + Name = "NotOwnerNotification" + }, + new + { + Id = 7, + Disabled = false, + Name = "NotValidSourceNotification" + }, + new + { + Id = 8, + Disabled = false, + Name = "NoDataNotification" + }, + new + { + Id = 9, + Disabled = false, + Name = "NormalThresholdNotification" + }); }); modelBuilder.Entity("Models.PurgedSyncJob", b => From efc6831983556004a4f15d3e750a5e6d2c49bc46 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 12 Jun 2024 15:16:06 -0700 Subject: [PATCH 0004/1479] Change Notifier that be able to disable notification --- .../SendThresholdNotificationAsync.cs | 5 +- .../Orchestrator/OrchestratorFunction.cs | 11 ++- .../INotifierService.cs | 2 +- .../NotifierServiceTests.cs | 22 ++--- .../OrchestratorTests.cs | 4 +- .../Services.Notifier/NotifierService.cs | 87 +++++++++++++++++-- .../INotificationTypesRepository.cs | 3 +- .../NotificationTypesRepository.cs | 6 +- .../MockNotificationTypesRepository.cs | 19 ++-- 9 files changed, 119 insertions(+), 40 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotificationAsync.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotificationAsync.cs index c8fe03d63..6e8ebfe3e 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotificationAsync.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotificationAsync.cs @@ -24,11 +24,12 @@ public SendThresholdNotification(ILoggingRepository loggingRepository, INotifier } [FunctionName(nameof(SendThresholdNotification))] - public async Task SendThresholdNotificationAsync([ActivityTrigger] ThresholdNotification notification) + public async Task SendThresholdNotificationAsync([ActivityTrigger] ThresholdNotification notification) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SendThresholdNotification)} function started at: {DateTime.UtcNow}" }); - await _notifierService.SendThresholdEmailAsync(notification); + var thresholdDiabled = await _notifierService.SendThresholdEmailAsync(notification); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SendThresholdNotification)} function completed at: {DateTime.UtcNow}" }); + return thresholdDiabled; } } } diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs index 40a1e2d17..ec576641f 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs @@ -44,8 +44,15 @@ await context.CallActivityAsync(nameof(LoggerFunction), { case nameof(NotificationMessageType.ThresholdNotification): var notification = await context.CallActivityAsync(nameof(CreateThresholdNotificationFunction), message); - await context.CallActivityAsync(nameof(SendThresholdNotification), notification); - await context.CallActivityAsync(nameof(UpdateNotificationStatusFunction), new UpdateNotificationStatusRequest { Notification = notification, Status = ThresholdNotificationStatus.AwaitingResponse }); + var thresholdDisabled = await context.CallActivityAsync(nameof(SendThresholdNotification), notification); + if (!thresholdDisabled) + { + await context.CallActivityAsync(nameof(UpdateNotificationStatusFunction), new UpdateNotificationStatusRequest + { + Notification = notification, + Status = ThresholdNotificationStatus.AwaitingResponse + }); + } break; case nameof(NotificationMessageType.NormalThresholdNotification): diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs index 609250dde..4847cad0f 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs @@ -12,7 +12,7 @@ namespace Services.Notifier.Contracts { public interface INotifierService { - public Task SendThresholdEmailAsync(ThresholdNotification notification); + public Task SendThresholdEmailAsync(ThresholdNotification notification); public Task> RetrieveQueuedNotificationsAsync(); public Task UpdateNotificationStatusAsync(ThresholdNotification notification, ThresholdNotificationStatus status); public Task CreateActionableNotificationFromContentAsync(string messageBody); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs index 189c93728..72c5d2090 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs @@ -218,8 +218,8 @@ public async Task TestSendEmailAsync() }; string subjectTemplate = "DisabledJobEmailSubject"; string contentTemplate = "SyncDisabledNoGroupEmailBody"; - _notificationTypesRepository.Setup(repo => repo.GetNotificationTypeByNotificationTypeNameAsync(contentTemplate)) - .ReturnsAsync(new NotificationType { Id = notificationTypeId, Name = contentTemplate, Disabled = false }); + _notificationTypesRepository.Setup(repo => repo.GetNotificationTypeByNotificationTypeNameAsync(NotificationMessageType.SyncStartedNotification)) + .ReturnsAsync(new NotificationType { Id = notificationTypeId, Name = NotificationMessageType.SyncStartedNotification, Disabled = false }); _jobNotificationRepository.Setup(repo => repo.IsNotificationDisabledForJobAsync(job.Id, notificationTypeId)) .ReturnsAsync(false); @@ -330,13 +330,13 @@ public async Task VerifyEmailNotSentIfDisabled() var notificationTypeId = 1; var notificationName = "SyncStartedEmailBody"; - _notificationTypesRepository.Setup(repo => repo.GetNotificationTypeByNotificationTypeNameAsync(notificationName)) - .ReturnsAsync(new NotificationType { Id = notificationTypeId, Name = notificationName, Disabled = false }); + _notificationTypesRepository.Setup(repo => repo.GetNotificationTypeByNotificationTypeNameAsync(NotificationMessageType.SyncStartedNotification)) + .ReturnsAsync(new NotificationType { Id = notificationTypeId, Name = NotificationMessageType.SyncStartedNotification, Disabled = false }); _jobNotificationRepository.Setup(repo => repo.IsNotificationDisabledForJobAsync(job.Id, notificationTypeId)) .ReturnsAsync(true); - bool result = await _notifierService.IsNotificationDisabledAsync(job.Id, notificationName); + bool result = await _notifierService.IsNotificationDisabledAsync(job.Id, NotificationMessageType.SyncStartedNotification); Assert.IsTrue(result); } @@ -348,13 +348,13 @@ public async Task VerifyEmailNotSentIfGloballyDisabled() var notificationTypeId = 1; var notificationName = "SyncStartedEmailBody"; - _notificationTypesRepository.Setup(repo => repo.GetNotificationTypeByNotificationTypeNameAsync(notificationName)) - .ReturnsAsync(new NotificationType { Id = notificationTypeId, Name = notificationName, Disabled = true }); + _notificationTypesRepository.Setup(repo => repo.GetNotificationTypeByNotificationTypeNameAsync(NotificationMessageType.SyncStartedNotification)) + .ReturnsAsync(new NotificationType { Id = notificationTypeId, Name = NotificationMessageType.SyncStartedNotification, Disabled = true }); _jobNotificationRepository.Setup(repo => repo.IsNotificationDisabledForJobAsync(job.Id, notificationTypeId)) .ReturnsAsync(false); - bool result = await _notifierService.IsNotificationDisabledAsync(job.Id, notificationName); + bool result = await _notifierService.IsNotificationDisabledAsync(job.Id, NotificationMessageType.SyncStartedNotification); Assert.IsTrue(result); } @@ -363,7 +363,7 @@ public async Task SendEmailAsync_ShouldHandleNonOKResponse() { var job = new SyncJob { Id = Guid.NewGuid(), RunId = Guid.NewGuid(), Requestor = "requestor@example.com", TargetOfficeGroupId = Guid.NewGuid() }; var messageBody = JsonSerializer.Serialize(new { SyncJob = job }); - var messageType = "TestMessageType"; + var messageType = NotificationMessageType.SyncStartedNotification.ToString(); var subjectTemplate = "TestSubjectTemplate"; var contentTemplate = "SyncDisabledNoGroupEmailBody"; @@ -443,8 +443,8 @@ public async Task SendEmailWhenEmailsHaveBeenDisabled() }; string subjectTemplate = "DisabledJobEmailSubject"; string contentTemplate = "SyncDisabledNoGroupEmailBody"; - _notificationTypesRepository.Setup(repo => repo.GetNotificationTypeByNotificationTypeNameAsync(contentTemplate)) - .ReturnsAsync(new NotificationType { Id = notificationTypeId, Name = contentTemplate, Disabled = false }); + _notificationTypesRepository.Setup(repo => repo.GetNotificationTypeByNotificationTypeNameAsync(NotificationMessageType.SyncStartedNotification)) + .ReturnsAsync(new NotificationType { Id = notificationTypeId, Name = NotificationMessageType.SyncStartedNotification, Disabled = false }); _jobNotificationRepository.Setup(repo => repo.IsNotificationDisabledForJobAsync(job.Id, notificationTypeId)) .ReturnsAsync(false); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs index 659e44a89..ec3adb1af 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs @@ -67,8 +67,8 @@ public async Task RunOrchestratorAsync_ThresholdNotification_CallsAppropriateFun _durableContext.Verify(x => x.CallActivityAsync( nameof(CreateThresholdNotificationFunction), It.IsAny()), Times.Once); - _durableContext.Verify(x => x.CallActivityAsync( - nameof(SendThresholdNotification), It.IsAny()), Times.Once); + _durableContext.Setup(x => x.CallActivityAsync(nameof(SendThresholdNotification), It.IsAny())) + .ReturnsAsync(false); _durableContext.Verify(x => x.CallActivityAsync( nameof(UpdateNotificationStatusFunction), It.IsAny()), Times.Once); _durableContext.Verify(x => x.CallActivityAsync( diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index d1de247be..8fb52f902 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -73,8 +73,19 @@ public NotifierService( _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - public async Task SendThresholdEmailAsync(ThresholdNotification notification) + public async Task SendThresholdEmailAsync(ThresholdNotification notification) { + bool isNotificationDisabled = await IsNotificationDisabledAsync(notification.SyncJobId, NotificationMessageType.ThresholdNotification); + + if (isNotificationDisabled) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = notification.SyncJobId, + Message = $"Notification '{NotificationMessageType.ThresholdNotification}' is disabled for job {notification.Id} with destination group {notification.TargetOfficeGroupId}." + }); + return true; + } await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Sending email to recipient addresses." }); var groupName = await _graphGroupRepository.GetGroupNameAsync(notification.TargetOfficeGroupId); @@ -103,10 +114,30 @@ public async Task SendThresholdEmailAsync(ThresholdNotification notification) CcEmailAddresses = _emailSenderAndRecipients.SupportEmailAddresses, IsHTML = true }; + + var response = await _mailRepository.SendMailAsync(message, null); - await _mailRepository.SendMailAsync(message, null); - TrackSentNotificationEvent(notification.TargetOfficeGroupId); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Sent email to recipient addresses." }); + + if (response != null && response.StatusCode != HttpStatusCode.Accepted) + { + var messageContent = new Dictionary + { + { "MessageBody", message.Content }, + { "MessageType", NotificationMessageType.ThresholdNotification.ToString() }, + { "HttpStatusCode", response.StatusCode.ToString() }, + { "ReasonPhrase", response.ReasonPhrase.ToString() } + }; + var body = System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(messageContent)); + var failedMessage = new ServiceBusMessage + { + MessageId = $"{notification.Id}_{notification.SyncJobId}_{NotificationMessageType.ThresholdNotification}", + Body = body + }; + await _serviceBusQueueRepository.SendMessageAsync(failedMessage); + } + TrackSentNotificationEvent(notification.TargetOfficeGroupId); + return false; } public async Task> RetrieveQueuedNotificationsAsync() @@ -166,7 +197,16 @@ public async Task SendEmailAsync(string messageType, string messageBody, string { var (job, additionalContentParameters) = await ParseMessageContentAsync(messageBody); - bool isNotificationDisabled = await IsNotificationDisabledAsync(job.Id, contentTemplate); + if (!Enum.TryParse(messageType, true, out var messageTypeEnum)) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = job.RunId, + Message = $"Notification type '{messageType}' do not exist." + }); + return; + } + bool isNotificationDisabled = await IsNotificationDisabledAsync(job.Id, messageTypeEnum); if (isNotificationDisabled) { @@ -225,16 +265,16 @@ await _loggingRepository.LogMessageAsync(new LogMessage } } - public async Task IsNotificationDisabledAsync(Guid jobId, string contentTemplate) + public async Task IsNotificationDisabledAsync(Guid jobId, NotificationMessageType messageType) { - var notificationType = await _notificationTypesRepository.GetNotificationTypeByNotificationTypeNameAsync(contentTemplate); + var notificationType = await _notificationTypesRepository.GetNotificationTypeByNotificationTypeNameAsync(messageType); if (notificationType == null) { await _loggingRepository.LogMessageAsync(new LogMessage { RunId = jobId, - Message = $"No notification type ID found for notification type name '{contentTemplate}'." + Message = $"No notification type ID found for notification type name '{messageType}'." }); return false; } @@ -244,7 +284,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage await _loggingRepository.LogMessageAsync(new LogMessage { RunId = jobId, - Message = $"Notifications of type '{contentTemplate}' have been globally disabled." + Message = $"Notifications of type '{messageType}' have been globally disabled." }); return true; } @@ -357,6 +397,17 @@ public async Task SendNormalThresholdEmailAsync(string messageBody) { var (job, threshold, sendDisableJobNotification, groupName) = ParseNormalThresholdMessageContent(messageBody); + bool isNotificationDisabled = await IsNotificationDisabledAsync(job.Id, NotificationMessageType.NormalThresholdNotification); + + if (isNotificationDisabled) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = job.RunId, + Message = $"Notification '{NotificationMessageType.NormalThresholdNotification}' is disabled for job {job.Id} with destination group {job.TargetOfficeGroupId}." + }); + return; + } var emailSubject = NotificationConstants.SyncThresholdEmailSubject; string contentTemplate; @@ -399,7 +450,25 @@ public async Task SendNormalThresholdEmailAsync(string messageBody) AdditionalContentParams = additionalContent, AdditionalSubjectParams = additionalSubjectContent }; - await _mailRepository.SendMailAsync(message, job.RunId); + var response = await _mailRepository.SendMailAsync(message, job.RunId); + + if (response != null && response.StatusCode != HttpStatusCode.Accepted) + { + var messageContent = new Dictionary + { + { "MessageBody", messageBody }, + { "MessageType", NotificationMessageType.NormalThresholdNotification }, + { "HttpStatusCode", response.StatusCode.ToString() }, + { "ReasonPhrase", response.ReasonPhrase.ToString() } + }; + var body = System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(messageContent)); + var failedMessage = new ServiceBusMessage + { + MessageId = $"{job.Id}_{job.RunId}_{NotificationMessageType.NormalThresholdNotification}", + Body = body + }; + await _serviceBusQueueRepository.SendMessageAsync(failedMessage); + } } private (string ContentTemplate, string[] AdditionalContent) GetNormalThresholdEmail(string groupName, ThresholdResult threshold, SyncJob job) { diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/INotificationTypesRepository.cs b/Service/GroupMembershipManagement/Repositories.Contracts/INotificationTypesRepository.cs index dc30278ab..3f06746af 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts/INotificationTypesRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Contracts/INotificationTypesRepository.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Models; +using Models.Notifications; using System.Collections.Generic; using System.Threading.Tasks; @@ -9,7 +10,7 @@ namespace Repositories.Contracts { public interface INotificationTypesRepository { - Task GetNotificationTypeByNotificationTypeNameAsync(string notificationTypeName); + Task GetNotificationTypeByNotificationTypeNameAsync(NotificationMessageType notificationTypeName); } } diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationTypesRepository.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationTypesRepository.cs index 4040d93df..b5982dc6f 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationTypesRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationTypesRepository.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Models; +using Models.Notifications; using Repositories.Contracts; using Repositories.EntityFramework.Contexts; @@ -19,10 +20,9 @@ public NotificationTypesRepository(GMMContext writeContext, GMMReadContext readC _readContext = readContext ?? throw new ArgumentNullException(nameof(readContext)); } - public async Task GetNotificationTypeByNotificationTypeNameAsync(string notificationName) + public async Task GetNotificationTypeByNotificationTypeNameAsync(NotificationMessageType notificationName) { - return await _readContext.NotificationTypes - .FirstOrDefaultAsync(e => e.Name == notificationName); + return await _readContext.NotificationTypes.FirstOrDefaultAsync(e => e.Name == notificationName); } } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/MockNotificationTypesRepository.cs b/Service/GroupMembershipManagement/Repositories.Mocks/MockNotificationTypesRepository.cs index d753137a4..13728db83 100644 --- a/Service/GroupMembershipManagement/Repositories.Mocks/MockNotificationTypesRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Mocks/MockNotificationTypesRepository.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Models; +using Models.Notifications; using Polly; using Repositories.Contracts; using System; @@ -20,13 +21,13 @@ public MockNotificationTypesRepository(Dictionary noti _notificationNameToTypeMapping = notificationNameToTypeMapping ?? new Dictionary(); } - public async Task GetNotificationTypeByNotificationTypeNameAsync(string notificationName) - { - if (_notificationNameToTypeMapping.TryGetValue(notificationName, out NotificationType notificationType)) - { - return await Task.FromResult(notificationType); - } - return null; - } - } + public async Task GetNotificationTypeByNotificationTypeNameAsync(NotificationMessageType notificationName) + { + if (_notificationNameToTypeMapping.TryGetValue(notificationName.ToString(), out NotificationType notificationType)) + { + return await Task.FromResult(notificationType); + } + return null; + } + } } \ No newline at end of file From f438fe02745e05d23859d13f1cc2b403ab886c22 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 13 Jun 2024 14:47:54 -0700 Subject: [PATCH 0005/1479] Change wording of the logs --- .../Hosts/Notifier/Services.Notifier/NotifierService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index 8fb52f902..cda1cfd35 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -213,7 +213,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage await _loggingRepository.LogMessageAsync(new LogMessage { RunId = job.RunId, - Message = $"Notification template '{contentTemplate}' is disabled for job {job.Id} with destination group {job.TargetOfficeGroupId}." + Message = $"Notification '{messageType}' is disabled for job {job.Id} with destination group {job.TargetOfficeGroupId}." }); return; } From 043ae6827754f421b06bb83e4ff4bc363ef04cc7 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 13 Jun 2024 15:13:13 -0700 Subject: [PATCH 0006/1479] Revert threshold logic --- .../SendThresholdNotificationAsync.cs | 5 ++--- .../Function/Orchestrator/OrchestratorFunction.cs | 11 ++--------- .../Services.Notifier.Contracts/INotifierService.cs | 2 +- .../Notifier/Services.Notifier/NotifierService.cs | 4 +--- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotificationAsync.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotificationAsync.cs index 6e8ebfe3e..c8fe03d63 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotificationAsync.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotificationAsync.cs @@ -24,12 +24,11 @@ public SendThresholdNotification(ILoggingRepository loggingRepository, INotifier } [FunctionName(nameof(SendThresholdNotification))] - public async Task SendThresholdNotificationAsync([ActivityTrigger] ThresholdNotification notification) + public async Task SendThresholdNotificationAsync([ActivityTrigger] ThresholdNotification notification) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SendThresholdNotification)} function started at: {DateTime.UtcNow}" }); - var thresholdDiabled = await _notifierService.SendThresholdEmailAsync(notification); + await _notifierService.SendThresholdEmailAsync(notification); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SendThresholdNotification)} function completed at: {DateTime.UtcNow}" }); - return thresholdDiabled; } } } diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs index ec576641f..40a1e2d17 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs @@ -44,15 +44,8 @@ await context.CallActivityAsync(nameof(LoggerFunction), { case nameof(NotificationMessageType.ThresholdNotification): var notification = await context.CallActivityAsync(nameof(CreateThresholdNotificationFunction), message); - var thresholdDisabled = await context.CallActivityAsync(nameof(SendThresholdNotification), notification); - if (!thresholdDisabled) - { - await context.CallActivityAsync(nameof(UpdateNotificationStatusFunction), new UpdateNotificationStatusRequest - { - Notification = notification, - Status = ThresholdNotificationStatus.AwaitingResponse - }); - } + await context.CallActivityAsync(nameof(SendThresholdNotification), notification); + await context.CallActivityAsync(nameof(UpdateNotificationStatusFunction), new UpdateNotificationStatusRequest { Notification = notification, Status = ThresholdNotificationStatus.AwaitingResponse }); break; case nameof(NotificationMessageType.NormalThresholdNotification): diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs index 4847cad0f..609250dde 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs @@ -12,7 +12,7 @@ namespace Services.Notifier.Contracts { public interface INotifierService { - public Task SendThresholdEmailAsync(ThresholdNotification notification); + public Task SendThresholdEmailAsync(ThresholdNotification notification); public Task> RetrieveQueuedNotificationsAsync(); public Task UpdateNotificationStatusAsync(ThresholdNotification notification, ThresholdNotificationStatus status); public Task CreateActionableNotificationFromContentAsync(string messageBody); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index cda1cfd35..50cf7bf2c 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -73,7 +73,7 @@ public NotifierService( _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - public async Task SendThresholdEmailAsync(ThresholdNotification notification) + public async Task SendThresholdEmailAsync(ThresholdNotification notification) { bool isNotificationDisabled = await IsNotificationDisabledAsync(notification.SyncJobId, NotificationMessageType.ThresholdNotification); @@ -84,7 +84,6 @@ await _loggingRepository.LogMessageAsync(new LogMessage RunId = notification.SyncJobId, Message = $"Notification '{NotificationMessageType.ThresholdNotification}' is disabled for job {notification.Id} with destination group {notification.TargetOfficeGroupId}." }); - return true; } await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Sending email to recipient addresses." }); @@ -137,7 +136,6 @@ await _loggingRepository.LogMessageAsync(new LogMessage await _serviceBusQueueRepository.SendMessageAsync(failedMessage); } TrackSentNotificationEvent(notification.TargetOfficeGroupId); - return false; } public async Task> RetrieveQueuedNotificationsAsync() From 1773ba31bac343b95b36d00ad4eb3d68cf7f2acc Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 25 Jun 2024 15:31:56 -0700 Subject: [PATCH 0007/1479] Add return --- .../Hosts/Notifier/Services.Notifier/NotifierService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index 50cf7bf2c..3205f6d2d 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -84,6 +84,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage RunId = notification.SyncJobId, Message = $"Notification '{NotificationMessageType.ThresholdNotification}' is disabled for job {notification.Id} with destination group {notification.TargetOfficeGroupId}." }); + return; } await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Sending email to recipient addresses." }); From 576420c6e92079a64aec5e9b0696a2d0098bf00b Mon Sep 17 00:00:00 2001 From: abgonz Date: Mon, 24 Jun 2024 11:48:38 -0700 Subject: [PATCH 0008/1479] Fix confirmation screen query display and placeholder query --- .../src/components/AdvancedQuery/AdvancedQuery.base.tsx | 2 +- .../src/components/Confirmation/Confirmation.base.tsx | 4 ++-- .../src/components/Confirmation/Confirmation.styles.ts | 8 +++++++- .../src/components/Confirmation/Confirmation.types.ts | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx b/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx index a03ae117e..197e768ff 100644 --- a/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx +++ b/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx @@ -55,7 +55,7 @@ export const AdvancedQueryBase: React.FunctionComponent = ( "id": 0, "depth": 0 }, - filter: "" + "filter": "" }, }, { diff --git a/UI/web-app/src/components/Confirmation/Confirmation.base.tsx b/UI/web-app/src/components/Confirmation/Confirmation.base.tsx index a5a405513..755900271 100644 --- a/UI/web-app/src/components/Confirmation/Confirmation.base.tsx +++ b/UI/web-app/src/components/Confirmation/Confirmation.base.tsx @@ -256,9 +256,9 @@ export const ConfirmationBase: React.FunctionComponent = (pr - +
{displayQuery}
-
+
diff --git a/UI/web-app/src/components/Confirmation/Confirmation.styles.ts b/UI/web-app/src/components/Confirmation/Confirmation.styles.ts index 72fc47f1d..90fb86fb8 100644 --- a/UI/web-app/src/components/Confirmation/Confirmation.styles.ts +++ b/UI/web-app/src/components/Confirmation/Confirmation.styles.ts @@ -48,7 +48,13 @@ export const getStyles = (props: IConfirmationStyleProps): IConfirmationStyles = endpointsContainer: { display: 'flex', flexDirection: 'column', - gap: 23 + gap: 23, + }, + queryContainer: { + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + overflow: 'auto', + maxWidth: '100%', }, }; }; diff --git a/UI/web-app/src/components/Confirmation/Confirmation.types.ts b/UI/web-app/src/components/Confirmation/Confirmation.types.ts index a6869946a..e5fc98cd3 100644 --- a/UI/web-app/src/components/Confirmation/Confirmation.types.ts +++ b/UI/web-app/src/components/Confirmation/Confirmation.types.ts @@ -16,6 +16,7 @@ import { itemTitle: IStyle; itemData: IStyle; endpointsContainer: IStyle; + queryContainer: IStyle; } export interface IConfirmationStyleProps { From b117fe16d0aede9bd892fb367523f33eca95ac5f Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 24 Jun 2024 13:19:17 -0700 Subject: [PATCH 0009/1479] Change wording for email --- .../LocalizationRepository.en-US.resx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 303861718..315de2f44 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -1039,7 +1039,7 @@ The group **{1}** with Id: {0} **has completed its initial sync**. Management Alert - You received this notification because you are listed as an owner of this group. + You received this notification because you are listed as an owner of **{1}**. Membership syncs for group with object Id {0}, previously named **{1}**, are now **disabled** because the group **no longer exists**. @@ -1049,7 +1049,7 @@ Otherwise, contact support for further help. - You received this notification because you are listed as an owner of this group, **{1}**. + You received this notification because you are listed as an owner of **{1}**. Membership syncs for group **{1}**, are now **disabled** because its pre-defined source group with object Id: {2} **no longer exists**. @@ -1057,7 +1057,7 @@ Please reach out to support to update the source definition and re-enable the me - You received this notification because you are listed as an owner of this group, **{2}**. + You received this notification because you are listed as an owner of **{2}**. Membership syncs are **disabled** because **GMM is not an owner** of **{2}** with object Id {0}. @@ -1066,15 +1066,16 @@ Please reach out to support to update the source definition and re-enable the me You received this notification because you are listed as the owner of **{0}**. -Your group **{0}** is currently disabled from Group Membership Management (GMM) due to one of the following reasons: + +Your group's membership synchronizations are currently disabled due to one of the following reasons: • Source and/or destination group(s) no longer exist. • Group owner did not take action on a drastic membership change alert. -• Group owner paused syncs for more than 30 days. +• Membership syncs were paused by owner for more than 30 days. -If you believe this group should still be managed by Group Membership Management (GMM), please take the necessary actions via GMM UI or reach out to support to request the assistance before {2}. +If you believe this group should still be managed by Group Membership Management (GMM), please take the necessary actions via the GMM UI or reach out to support to request assistance before{2}. -If not remediated by {2}, your group's affiliation with GMM (including its membership definition) will be removed. You will need to re-onboard if you still want GMM to manage your group after that date. +If not remediated by {2}, your group's membership will still remain, but its affiliation with GMM (including its membership definition) will be removed. You will need to re-onboard if you still want GMM to manage your group after that date. Group Membership Management (GMM) update @@ -1088,9 +1089,9 @@ If not remediated by {2}, your group's affiliation with GMM (including its membe You received this notification because you are listed as a GMM onboarding requestor or as owner of **{1}**. -Membership syncs for **{1}** with Id {0} have been paused since the membership definition provided currently indicates that there should be no users in the destination.. +The membership definition provided for **{1}** currently does not return any users. This is not common, so out of an abundance of caution, no users have been removed and subsequent membership syncs have been paused. -Please reach out to support to get this issue resolved. +Please review your membership definition by visiting the GMM UI. @@ -1099,11 +1100,12 @@ Please reach out to support to get this issue resolved. You received this notification because you are listed as an owner of **{0}**. -Membership syncs for **{0}** with Id {1} have been disabled because its following source group object ID(s) are not valid: + + Membership syncs for **{0}** with Id {1} have been disabled because its following source group object ID(s) are not valid: {2} - You received this notification because you are listed as a GMM onboarding requestor or as owner of this group. Please visit {3} to learn more about GMM. + You received this notification because you are listed as a GMM onboarding requestor or as owner of **{0}**. Please visit {3} to learn more about GMM. The synchronization job for group {0} with Id: {1} has been disabled. @@ -1113,7 +1115,7 @@ The group sync will be PAUSED until your response is received. - You received this notification because you are listed as a GMM onboarding requestor or as owner of this group. Please visit {3} to learn more about GMM. + You received this notification because you are listed as a GMM onboarding requestor or as owner of **{1}**. Please visit {3} to learn more about GMM. The group {1} with Id: {0} has exceeded the specified thresholds. From 827bac40732a5cf08dbd804e47c7086bbdcb449e Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 26 Jun 2024 13:33:08 -0700 Subject: [PATCH 0010/1479] Add a space --- .../Resources/LocalizationRepository.en-US.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 315de2f44..25446fc9e 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -1073,7 +1073,7 @@ Your group's membership synchronizations are currently disabled due to one of th • Group owner did not take action on a drastic membership change alert. • Membership syncs were paused by owner for more than 30 days. -If you believe this group should still be managed by Group Membership Management (GMM), please take the necessary actions via the GMM UI or reach out to support to request assistance before{2}. +If you believe this group should still be managed by Group Membership Management (GMM), please take the necessary actions via the GMM UI or reach out to support to request assistance before {2}. If not remediated by {2}, your group's membership will still remain, but its affiliation with GMM (including its membership definition) will be removed. You will need to re-onboard if you still want GMM to manage your group after that date. From ac5fdd88c196bcccaee02a28ffab1f19998c75df Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 3 Jun 2024 13:56:47 -0700 Subject: [PATCH 0011/1479] Updated AzureMaintenance bicep to have serviceBusNotificationsQueue environment variable --- .../AzureMaintenance/Infrastructure/compute/template.bicep | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep index 82728e1a9..79e096758 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep @@ -89,6 +89,7 @@ var notificationsTableName = resourceId(subscription().subscriptionId, dataKeyVa var azureMaintenanceStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'azureMaintenanceStorageAccountProd') var azureMaintenanceStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'azureMaintenanceStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') +var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') module servicePlanTemplate 'servicePlan.bicep' = { name: 'servicePlanTemplate-AzureMaintenance' @@ -130,6 +131,7 @@ var appSettings = { senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' + serviceBusNotificationsQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusNotificationsQueue, '2019-09-01').secretUriWithVersion})' } var stagingSettings = { From ca1b70cebca3dd4c34e457cb82935b345a18a416 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 3 Jun 2024 13:59:42 -0700 Subject: [PATCH 0012/1479] Updated AzureMaintenance to send email information over to Notifier --- .../EmailSenderFunction.cs} | 19 +++--- .../EmailSender/EmailSenderRequest.cs | 16 +++++ .../Orchestrator/OrchestratorFunction.cs | 23 ++++++- .../AzureMaintenance/Function/Startup.cs | 16 +++-- .../Function/local.settings.json | 5 +- .../Contracts/IAzureMaintenanceService.cs | 3 +- .../Services/AzureMaintenanceService.cs | 64 +++++++++---------- .../Models/Notifications/NotificationType.cs | 1 + .../LocalizationRepository.en-US.resx | 4 +- 9 files changed, 96 insertions(+), 55 deletions(-) rename Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/{SendEmail/SendEmailFunction.cs => EmailSender/EmailSenderFunction.cs} (54%) create mode 100644 Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderRequest.cs diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/SendEmail/SendEmailFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs similarity index 54% rename from Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/SendEmail/SendEmailFunction.cs rename to Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs index 2a6c8369d..d5ae7b315 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/SendEmail/SendEmailFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs @@ -10,26 +10,25 @@ namespace Hosts.AzureMaintenance { - public class SendEmailFunction + public class EmailSenderFunction { private readonly ILoggingRepository _loggingRepository = null; private readonly IAzureMaintenanceService _azureMaintenanceService = null; - public SendEmailFunction(ILoggingRepository loggingRepository, IAzureMaintenanceService azureMaintenanceService) + public EmailSenderFunction(ILoggingRepository loggingRepository, IAzureMaintenanceService azureMaintenanceService) { _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); _azureMaintenanceService = azureMaintenanceService ?? throw new ArgumentNullException(nameof(azureMaintenanceService)); } - [FunctionName(nameof(SendEmailFunction))] - public async Task SendEmailAsync([ActivityTrigger] SyncJob job) + [FunctionName(nameof(EmailSenderFunction))] + public async Task SendEmailAsync([ActivityTrigger] EmailSenderRequest request) { - if (job != null) + if (request.SyncJob != null) { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SendEmailFunction)} function started", RunId = job.RunId }, VerbosityLevel.DEBUG); - var groupName = await _azureMaintenanceService.GetGroupNameAsync(job.TargetOfficeGroupId); - if (string.IsNullOrEmpty(groupName)) return; - await _azureMaintenanceService.SendEmailAsync(job, groupName); - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SendEmailFunction)} function completed", RunId = job.RunId }, VerbosityLevel.DEBUG); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(EmailSenderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); + + await _azureMaintenanceService.SendEmailAsync(request.SyncJob, request.NotificationType, request.AdditionalContentParams); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(EmailSenderFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); } } } diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderRequest.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderRequest.cs new file mode 100644 index 000000000..256928bd0 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderRequest.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Models; +using Models.Notifications; +using System; + +namespace Hosts.AzureMaintenance +{ + public class EmailSenderRequest + { + public Guid RunId { get; set; } + public SyncJob SyncJob { get; set; } + public NotificationMessageType NotificationType { get; set; } + public string[] AdditionalContentParams { get; set; } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs index 2b5bdd732..ac1d03c1a 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs @@ -8,19 +8,23 @@ using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using Models; +using Services.Contracts; namespace Hosts.AzureMaintenance { public class OrchestratorFunction { private readonly IHandleInactiveJobsConfig _handleInactiveJobsConfig = null; - private readonly IThresholdNotificationConfig _thresholdNotificationConfig; + private readonly IThresholdNotificationConfig _thresholdNotificationConfig = null; + private readonly IAzureMaintenanceService _azureMaintenanceService = null; public OrchestratorFunction(IHandleInactiveJobsConfig handleInactiveJobsConfig, - IThresholdNotificationConfig thresholdNotificationConfig) + IThresholdNotificationConfig thresholdNotificationConfig, + IAzureMaintenanceService azureMaintenanceService) { _handleInactiveJobsConfig = handleInactiveJobsConfig; _thresholdNotificationConfig = thresholdNotificationConfig; + _azureMaintenanceService = azureMaintenanceService; } [FunctionName(nameof(OrchestratorFunction))] @@ -54,7 +58,20 @@ await context.CallActivityAsync( var processingTasks = new List(); foreach (var inactiveSyncJob in inactiveSyncJobs) { - var processTask = context.CallActivityAsync(nameof(SendEmailFunction), inactiveSyncJob); + var groupName = await _azureMaintenanceService.GetGroupNameAsync(inactiveSyncJob.TargetOfficeGroupId); + var additionalContentParams = new[] + { + groupName, + inactiveSyncJob.TargetOfficeGroupId.ToString(), + context.CurrentUtcDateTime.AddDays(_handleInactiveJobsConfig.NumberOfDaysBeforeDeletion-5).ToString() + }; + var processTask = context.CallActivityAsync(nameof(EmailSenderFunction), new EmailSenderRequest + { + RunId = runId, + SyncJob = inactiveSyncJob, + NotificationType = Models.Notifications.NotificationMessageType.InactiveSyncJobNotification, + AdditionalContentParams = additionalContentParams + }); processingTasks.Add(processTask); } await Task.WhenAll(processingTasks); diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs index 5a7faab2d..933220b9d 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Azure.Messaging.ServiceBus; using Common.DependencyInjection; using DIConcreteTypes; using Hosts.AzureMaintenance; @@ -14,6 +15,7 @@ using Repositories.EntityFramework; using Repositories.GraphGroups; using Repositories.NotificationsRepository; +using Repositories.ServiceBusQueue; using Services; using Services.Contracts; @@ -68,14 +70,20 @@ public override void Configure(IFunctionsHostBuilder builder) builder.Services.AddScoped(services => { + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + var notificationsQueueRepository = new ServiceBusQueueRepository(sender); + return new AzureMaintenanceService(services.GetService(), services.GetService(), services.GetService(), - services.GetService(), - services.GetService(), services.GetService(), - services.GetService()); - }); + services.GetService(), + notificationsQueueRepository, + services.GetService()); + }); } private bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/local.settings.json index e1f48a8c4..882440d38 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/local.settings.json @@ -21,7 +21,8 @@ "senderPassword": "", "supportEmailAddresses": "", "AzureMaintenance:NumberOfDaysBeforeDeletion": 30, - "AzureMaintenance:HandleInactiveJobsEnabled": true, - "ThresholdNotification:IsThresholdNotificationEnabled": false + "AzureMaintenance:HandleInactiveJobsEnabled": true, + "ThresholdNotification:IsThresholdNotificationEnabled": false, + "serviceBusNotificationsQueue": "" } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Contracts/IAzureMaintenanceService.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Contracts/IAzureMaintenanceService.cs index c2bb206d2..568a9a952 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Contracts/IAzureMaintenanceService.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Contracts/IAzureMaintenanceService.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; +using Models.Notifications; using Services.Entities.Contracts; using System; using System.Collections.Generic; @@ -16,6 +17,6 @@ public interface IAzureMaintenanceService Task RemoveBackupsAsync(); Task ExpireNotificationsAsync(IEnumerable jobs); Task GetGroupNameAsync(Guid groupId); - Task SendEmailAsync(SyncJob job, string groupName); + Task SendEmailAsync(SyncJob job, NotificationMessageType notificationType, string[] additionalContentParams); } } diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs index 5b1f2682c..e244a3f51 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs @@ -3,6 +3,8 @@ using Models; using Models.AzureMaintenance; +using Models.Notifications; +using Models.ServiceBus; using Models.ThresholdNotifications; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; @@ -10,39 +12,37 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; namespace Services { public class AzureMaintenanceService : IAzureMaintenanceService { - private const string CustomerPausedJobEmailSubject = "CustomerPausedJobEmailSubject"; - private const string CustomerPausedJobEmailBody = "CustomerPausedJobEmailBody"; - private readonly IDatabaseSyncJobsRepository _syncJobRepository = null; private readonly IDatabasePurgedSyncJobsRepository _purgedSyncJobRepository = null; private readonly IGraphGroupRepository _graphGroupRepository = null; - private readonly IEmailSenderRecipient _emailSenderAndRecipients = null; - private readonly IMailRepository _mailRepository = null; private readonly IHandleInactiveJobsConfig _handleInactiveJobsConfig = null; - private readonly INotificationRepository _notificationRepository = null; + private readonly INotificationRepository _notificationRepository = null; + private readonly IServiceBusQueueRepository _notificationsQueueRepository; + private readonly ILoggingRepository _loggingRepository; public AzureMaintenanceService( IDatabaseSyncJobsRepository syncJobRepository, IDatabasePurgedSyncJobsRepository purgedSyncJobRepository, IGraphGroupRepository graphGroupRepository, - IEmailSenderRecipient emailSenderAndRecipients, - IMailRepository mailRepository, IHandleInactiveJobsConfig handleInactiveJobsConfig, - INotificationRepository notificationRepository) + INotificationRepository notificationRepository, + IServiceBusQueueRepository notificationQueueRepository, + ILoggingRepository loggingRepository) { _syncJobRepository = syncJobRepository ?? throw new ArgumentNullException(nameof(syncJobRepository)); _purgedSyncJobRepository = purgedSyncJobRepository ?? throw new ArgumentNullException(nameof(purgedSyncJobRepository)); _graphGroupRepository = graphGroupRepository ?? throw new ArgumentNullException(nameof(graphGroupRepository)); - _emailSenderAndRecipients = emailSenderAndRecipients ?? throw new ArgumentNullException(nameof(emailSenderAndRecipients)); - _mailRepository = mailRepository ?? throw new ArgumentNullException(nameof(mailRepository)); _handleInactiveJobsConfig = handleInactiveJobsConfig ?? throw new ArgumentNullException(nameof(handleInactiveJobsConfig)); _notificationRepository = notificationRepository ?? throw new ArgumentNullException(nameof(notificationRepository)); + _notificationsQueueRepository = notificationQueueRepository ?? throw new ArgumentNullException(nameof(notificationQueueRepository)); + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } public async Task> GetSyncJobsAsync() @@ -69,30 +69,28 @@ public async Task GetGroupNameAsync(Guid groupId) return await _graphGroupRepository.GetGroupNameAsync(groupId); } - public async Task SendEmailAsync(SyncJob job, string groupName) + public async Task SendEmailAsync(SyncJob job, NotificationMessageType notificationType, string[] additionalContentParams) { - if (job != null) + var messageContent = new Dictionary { - var owners = await _graphGroupRepository.GetGroupOwnersAsync(job.TargetOfficeGroupId); - var ownerEmails = string.Join(";", owners.Where(x => !string.IsNullOrWhiteSpace(x.Mail)).Select(x => x.Mail)); - var message = new EmailMessage - { - Subject = CustomerPausedJobEmailSubject, - Content = CustomerPausedJobEmailBody, - SenderAddress = _emailSenderAndRecipients.SenderAddress, - SenderPassword = _emailSenderAndRecipients.SenderPassword, - ToEmailAddresses = ownerEmails, - CcEmailAddresses = _emailSenderAndRecipients.SupportEmailAddresses, - AdditionalContentParams = new[] - { - groupName, - job.TargetOfficeGroupId.ToString(), - DateTime.UtcNow.AddDays(_handleInactiveJobsConfig.NumberOfDaysBeforeDeletion-5).ToString() - } - }; - - await _mailRepository.SendMailAsync(message, job.RunId); - } + { "SyncJob", job }, + { "AdditionalContentParameters", additionalContentParams } + }; + var body = Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(messageContent)); + var message = new ServiceBusMessage + { + MessageId = $"{job.Id}_{job.RunId}_{notificationType}", + Body = body + }; + message.ApplicationProperties.Add("MessageType", notificationType.ToString()); + await _notificationsQueueRepository.SendMessageAsync(message); + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = job.RunId, + Message = $"Sent message {message.MessageId} to service bus notifications queue " + }); + + } public async Task BackupInactiveJobsAsync(List syncJobs) diff --git a/Service/GroupMembershipManagement/Models/Notifications/NotificationType.cs b/Service/GroupMembershipManagement/Models/Notifications/NotificationType.cs index 7bcd02ce6..96d35746d 100644 --- a/Service/GroupMembershipManagement/Models/Notifications/NotificationType.cs +++ b/Service/GroupMembershipManagement/Models/Notifications/NotificationType.cs @@ -11,5 +11,6 @@ public enum NotificationMessageType NotValidSourceNotification= 6, NoDataNotification = 7, NormalThresholdNotification = 8, + InactiveSyncJobNotification = 9, } } diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 25446fc9e..5289c2876 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -1064,7 +1064,7 @@ Please reach out to support to update the source definition and re-enable the me Contact support if you would like to re-enable GMM. - + You received this notification because you are listed as the owner of **{0}**. Your group's membership synchronizations are currently disabled due to one of the following reasons: @@ -1077,7 +1077,7 @@ If you believe this group should still be managed by Group Membership Management If not remediated by {2}, your group's membership will still remain, but its affiliation with GMM (including its membership definition) will be removed. You will need to re-onboard if you still want GMM to manage your group after that date. - + Group Membership Management (GMM) update From 446a2eb72b4737d56dd201847e611d6c76afd9a3 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 3 Jun 2024 14:20:06 -0700 Subject: [PATCH 0013/1479] Added NotificationMessageType for InactiveSyncJobNotification to Notifier --- ...oldNotificationAsync.cs => SendThresholdNotification.cs} | 0 .../Notifier/Function/Orchestrator/OrchestratorFunction.cs | 6 ++++++ .../Hosts/Notifier/Services.Notifier/NotifierService.cs | 4 ++-- .../Services.Contracts/NotificationConstants.cs | 2 ++ 4 files changed, 10 insertions(+), 2 deletions(-) rename Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/{SendThresholdNotificationAsync.cs => SendThresholdNotification.cs} (100%) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotificationAsync.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotification.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotificationAsync.cs rename to Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotification.cs diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs index 40a1e2d17..ab9d257ce 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs @@ -94,6 +94,12 @@ await context.CallActivityAsync(nameof(LoggerFunction), await context.CallActivityAsync(nameof(SendNotification), message); break; + case nameof(NotificationMessageType.InactiveSyncJobNotification): + message.SubjectTemplate = NotificationConstants.SyncDisabledInactivityEmailSubject; + message.ContentTemplate = NotificationConstants.SyncDisabledInactivityEmailBody; + await context.CallActivityAsync(nameof(SendNotification), message); + break; + default: await context.CallActivityAsync(nameof(LoggerFunction), new LoggerRequest diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index 3205f6d2d..a48f9a405 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -115,8 +115,8 @@ await _loggingRepository.LogMessageAsync(new LogMessage IsHTML = true }; - var response = await _mailRepository.SendMailAsync(message, null); - + await _mailRepository.SendMailAsync(message, null); + TrackSentNotificationEvent(notification.TargetOfficeGroupId); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Sent email to recipient addresses." }); if (response != null && response.StatusCode != HttpStatusCode.Accepted) diff --git a/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs b/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs index cf327a84e..a0d245d16 100644 --- a/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs +++ b/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs @@ -24,5 +24,7 @@ public static class NotificationConstants public const string SyncThresholdEmailSubject = "SyncThresholdEmailSubject"; public const string SyncThresholdBothEmailBody = "SyncThresholdBothEmailBody"; public const string SyncThresholdDisablingJobEmailSubject = "SyncThresholdDisablingJobEmailSubject"; + public const string SyncDisabledInactivityEmailBody = "SyncDisabledInactivityEmailBody"; + public const string SyncDisabledInactivityEmailSubject = "SyncDisabledInactivityEmailSubject"; } } From 58951c11e807cac1e67626f21787d67d2720f5b7 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 4 Jun 2024 10:19:06 -0700 Subject: [PATCH 0014/1479] Updated testing for AzureMaintenance --- .../AzureMaintenanceServiceTests.cs | 69 +++++++++---------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs index b37a76a07..bb8c2edcd 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Models.AzureMaintenance; +using Models.ServiceBus; using Models.ThresholdNotifications; using Moq; using Repositories.Contracts; @@ -68,20 +69,19 @@ public async Task TestBackupInactiveJobs() var syncJobRepository = new Mock(); var purgedSyncJobRepository = new Mock(); var graphGroupRepository = new Mock(); - var mailAddresses = new Mock(); - var mailRepository = new Mock(); var handleInactiveJobsConfig = new Mock(); var notificationRepository = new Mock(); + var notificationQueueRepository = new Mock(); purgedSyncJobRepository.Setup(x => x.InsertPurgedSyncJobsAsync(It.IsAny>())).ReturnsAsync(2); var azureMaintenanceService = new AzureMaintenanceService(syncJobRepository.Object, purgedSyncJobRepository.Object, graphGroupRepository.Object, - mailAddresses.Object, - mailRepository.Object, handleInactiveJobsConfig.Object, - notificationRepository.Object); + notificationRepository.Object, + notificationQueueRepository.Object, + loggerMock.Object); var countOfBackedUpJobs = await azureMaintenanceService.BackupInactiveJobsAsync(jobs); Assert.AreEqual(countOfBackedUpJobs, jobs.Count); @@ -122,10 +122,9 @@ public async Task TestRemoveBackups() var syncJobRepository = new Mock(); var purgedSyncJobRepository = new Mock(); var graphGroupRepository = new Mock(); - var mailAddresses = new Mock(); - var mailRepository = new Mock(); var handleInactiveJobsConfig = new Mock(); var notificationRepository = new Mock(); + var notificationQueueRepository = new Mock(); purgedSyncJobRepository.Setup(x => x.GetPurgedSyncJobsAsync(It.IsAny())).ReturnsAsync(tables); purgedSyncJobRepository.Setup(x => x.DeletePurgedSyncJobsAsync(It.IsAny>())).ReturnsAsync(2); @@ -133,10 +132,10 @@ public async Task TestRemoveBackups() var azureMaintenanceService = new AzureMaintenanceService(syncJobRepository.Object, purgedSyncJobRepository.Object, graphGroupRepository.Object, - mailAddresses.Object, - mailRepository.Object, handleInactiveJobsConfig.Object, - notificationRepository.Object); + notificationRepository.Object, + notificationQueueRepository.Object, + loggerMock.Object); var countOfRemovedBackUps = await azureMaintenanceService.RemoveBackupsAsync(); Assert.AreEqual(countOfRemovedBackUps, tables.Count); @@ -175,18 +174,17 @@ public async Task TestRemoveInactiveJobs() var syncJobRepository = new Mock(); var purgedSyncJobRepository = new Mock(); var graphGroupRepository = new Mock(); - var mailAddresses = new Mock(); - var mailRepository = new Mock(); var handleInactiveJobsConfig = new Mock(); var notificationRepository = new Mock(); + var notificationQueueRepository = new Mock(); var azureMaintenanceService = new AzureMaintenanceService(syncJobRepository.Object, purgedSyncJobRepository.Object, graphGroupRepository.Object, - mailAddresses.Object, - mailRepository.Object, handleInactiveJobsConfig.Object, - notificationRepository.Object); + notificationRepository.Object, + notificationQueueRepository.Object, + loggerMock.Object); await azureMaintenanceService.RemoveInactiveJobsAsync(j); syncJobRepository.Verify(x => x.DeleteSyncJobsAsync(It.IsAny>()), Times.Once()); @@ -224,10 +222,10 @@ public async Task TestExpireNotifications() var syncJobRepository = new Mock(); var purgedSyncJobRepository = new Mock(); var graphGroupRepository = new Mock(); - var mailAddresses = new Mock(); - var mailRepository = new Mock(); var handleInactiveJobsConfig = new Mock(); var notificationRepository = new Mock(); + var notificationQueueRepository = new Mock(); + var notification = new ThresholdNotification { ChangePercentageForAdditions = Random.Shared.Next(51, 100), @@ -250,10 +248,10 @@ public async Task TestExpireNotifications() var azureMaintenanceService = new AzureMaintenanceService(syncJobRepository.Object, purgedSyncJobRepository.Object, graphGroupRepository.Object, - mailAddresses.Object, - mailRepository.Object, handleInactiveJobsConfig.Object, - notificationRepository.Object); + notificationRepository.Object, + notificationQueueRepository.Object, + loggerMock.Object); notificationRepository.Setup(x => x.GetThresholdNotificationBySyncJobIdAsync(It.IsAny())).Returns(() => Task.FromResult(notification)); await azureMaintenanceService.ExpireNotificationsAsync(j); @@ -269,20 +267,19 @@ public async Task TestGetGroupName() var syncJobRepository = new Mock(); var purgedSyncJobRepository = new Mock(); var graphGroupRepository = new Mock(); - var mailAddresses = new Mock(); - var mailRepository = new Mock(); var handleInactiveJobsConfig = new Mock(); var notificationRepository = new Mock(); + var notificationQueueRepository = new Mock(); graphGroupRepository.Setup(x => x.GetGroupNameAsync(It.IsAny())).ReturnsAsync(() => "Test Group"); var azureMaintenanceService = new AzureMaintenanceService(syncJobRepository.Object, purgedSyncJobRepository.Object, graphGroupRepository.Object, - mailAddresses.Object, - mailRepository.Object, handleInactiveJobsConfig.Object, - notificationRepository.Object); + notificationRepository.Object, + notificationQueueRepository.Object, + loggerMock.Object); await azureMaintenanceService.GetGroupNameAsync(Guid.NewGuid()); graphGroupRepository.Verify(x => x.GetGroupNameAsync(It.IsAny()), Times.Once()); @@ -312,18 +309,17 @@ public async Task TestGetSyncJobs() var syncJobRepository = new Mock(); var purgedSyncJobRepository = new Mock(); var graphGroupRepository = new Mock(); - var mailAddresses = new Mock(); - var mailRepository = new Mock(); var handleInactiveJobsConfig = new Mock(); var notificationRepository = new Mock(); + var notificationQueueRepository = new Mock(); var azureMaintenanceService = new AzureMaintenanceService(syncJobRepository.Object, purgedSyncJobRepository.Object, graphGroupRepository.Object, - mailAddresses.Object, - mailRepository.Object, handleInactiveJobsConfig.Object, - notificationRepository.Object); + notificationRepository.Object, + notificationQueueRepository.Object, + loggerMock.Object); var jobs = await azureMaintenanceService.GetSyncJobsAsync(); Assert.AreEqual(jobs.Count, 0); @@ -369,23 +365,22 @@ public async Task TestSendEmail() var syncJobRepository = new Mock(); var purgedSyncJobRepository = new Mock(); var graphGroupRepository = new Mock(); - var mailAddresses = new Mock(); - var mailRepository = new Mock(); var handleInactiveJobsConfig = new Mock(); var notificationRepository = new Mock(); + var notificationQueueRepository = new Mock(); _ = graphGroupRepository.Setup(x => x.GetGroupOwnersAsync(job.TargetOfficeGroupId, 0)).ReturnsAsync(users); var azureMaintenanceService = new AzureMaintenanceService(syncJobRepository.Object, purgedSyncJobRepository.Object, graphGroupRepository.Object, - mailAddresses.Object, - mailRepository.Object, handleInactiveJobsConfig.Object, - notificationRepository.Object); + notificationRepository.Object, + notificationQueueRepository.Object, + loggerMock.Object); - await azureMaintenanceService.SendEmailAsync(job, "Test Group"); - mailRepository.Verify(x => x.SendMailAsync(It.IsAny(), It.IsAny()), Times.Once()); + await azureMaintenanceService.SendEmailAsync(job, Models.Notifications.NotificationMessageType.InactiveSyncJobNotification); + notificationQueueRepository.Verify(x => x.SendMessageAsync(It.IsAny()), Times.Once()); } public IEnumerable GetJobs(List jobs) From 8674d5b43b74ee21cd4061e59961eb44b037c14b Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 5 Jun 2024 16:36:31 -0700 Subject: [PATCH 0015/1479] Added FQN to AzureMaintenance --- .../AzureMaintenance/Infrastructure/compute/template.bicep | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep index 79e096758..57cf7c39a 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep @@ -74,6 +74,7 @@ param setRBACPermissions bool = false var logAnalyticsCustomerId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsCustomerId') var logAnalyticsPrimarySharedKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsPrimarySharedKey') +var serviceBusFQN = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusFQN') var maintenanceJobs = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'maintenanceJobs') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var graphAppClientId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppClientId') @@ -127,6 +128,7 @@ var appSettings = { 'graphCredentials:KeyVaultTenantId': tenantId 'ConnectionStrings:JobsContext': '@Microsoft.KeyVault(SecretUri=${reference(jobsMSIConnectionString, '2019-09-01').secretUriWithVersion})' 'ConnectionStrings:JobsContextReadOnly': '@Microsoft.KeyVault(SecretUri=${reference(replicaJobsMSIConnectionString, '2019-09-01').secretUriWithVersion})' + gmmServiceBus__fullyQualifiedNamespace: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusFQN, '2019-09-01').secretUriWithVersion})' senderAddress: '@Microsoft.KeyVault(SecretUri=${reference(senderUsername, '2019-09-01').secretUriWithVersion})' senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' From cbc2662dd1889bad3a7f7d548e8d3688c20853dc Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 19 Jun 2024 08:33:56 -0700 Subject: [PATCH 0016/1479] Fixed to prevent multithreading in Orchestrator --- .../Activity/EmailSender/EmailSenderFunction.cs | 7 +++++-- .../Activity/EmailSender/EmailSenderRequest.cs | 1 - .../Function/Orchestrator/OrchestratorFunction.cs | 14 +------------- .../Contracts/IAzureMaintenanceService.cs | 2 +- .../Services/AzureMaintenanceService.cs | 10 +++++++++- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs index d5ae7b315..cbcc5652d 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs @@ -4,6 +4,7 @@ using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; using Services.Contracts; using System; using System.Threading.Tasks; @@ -14,7 +15,8 @@ public class EmailSenderFunction { private readonly ILoggingRepository _loggingRepository = null; private readonly IAzureMaintenanceService _azureMaintenanceService = null; - public EmailSenderFunction(ILoggingRepository loggingRepository, IAzureMaintenanceService azureMaintenanceService) + public EmailSenderFunction(ILoggingRepository loggingRepository, + IAzureMaintenanceService azureMaintenanceService) { _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); _azureMaintenanceService = azureMaintenanceService ?? throw new ArgumentNullException(nameof(azureMaintenanceService)); @@ -27,7 +29,8 @@ public async Task SendEmailAsync([ActivityTrigger] EmailSenderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(EmailSenderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); - await _azureMaintenanceService.SendEmailAsync(request.SyncJob, request.NotificationType, request.AdditionalContentParams); + await _azureMaintenanceService.SendEmailAsync(request.SyncJob, request.NotificationType); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(EmailSenderFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); } } diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderRequest.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderRequest.cs index 256928bd0..e78f669f0 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderRequest.cs @@ -11,6 +11,5 @@ public class EmailSenderRequest public Guid RunId { get; set; } public SyncJob SyncJob { get; set; } public NotificationMessageType NotificationType { get; set; } - public string[] AdditionalContentParams { get; set; } } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs index ac1d03c1a..9c30f0d19 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs @@ -49,28 +49,16 @@ await context.CallActivityAsync( if (inactiveSyncJobs != null && inactiveSyncJobs.Count > 0 && inactiveSyncJobs.Count == countOfBackUpJobs) { - if (_thresholdNotificationConfig.IsThresholdNotificationEnabled) - { - await context.CallActivityAsync(nameof(ExpireNotificationsFunction), inactiveSyncJobs); - } await context.CallActivityAsync(nameof(RemoveInactiveJobsFunction), inactiveSyncJobs); var processingTasks = new List(); foreach (var inactiveSyncJob in inactiveSyncJobs) { - var groupName = await _azureMaintenanceService.GetGroupNameAsync(inactiveSyncJob.TargetOfficeGroupId); - var additionalContentParams = new[] - { - groupName, - inactiveSyncJob.TargetOfficeGroupId.ToString(), - context.CurrentUtcDateTime.AddDays(_handleInactiveJobsConfig.NumberOfDaysBeforeDeletion-5).ToString() - }; var processTask = context.CallActivityAsync(nameof(EmailSenderFunction), new EmailSenderRequest { RunId = runId, SyncJob = inactiveSyncJob, - NotificationType = Models.Notifications.NotificationMessageType.InactiveSyncJobNotification, - AdditionalContentParams = additionalContentParams + NotificationType = Models.Notifications.NotificationMessageType.InactiveSyncJobNotification }); processingTasks.Add(processTask); } diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Contracts/IAzureMaintenanceService.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Contracts/IAzureMaintenanceService.cs index 568a9a952..22a946dd7 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Contracts/IAzureMaintenanceService.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Contracts/IAzureMaintenanceService.cs @@ -17,6 +17,6 @@ public interface IAzureMaintenanceService Task RemoveBackupsAsync(); Task ExpireNotificationsAsync(IEnumerable jobs); Task GetGroupNameAsync(Guid groupId); - Task SendEmailAsync(SyncJob job, NotificationMessageType notificationType, string[] additionalContentParams); + Task SendEmailAsync(SyncJob job, NotificationMessageType notificationType); } } diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs index e244a3f51..8043ee151 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs @@ -69,8 +69,16 @@ public async Task GetGroupNameAsync(Guid groupId) return await _graphGroupRepository.GetGroupNameAsync(groupId); } - public async Task SendEmailAsync(SyncJob job, NotificationMessageType notificationType, string[] additionalContentParams) + public async Task SendEmailAsync(SyncJob job, NotificationMessageType notificationType) { + var groupName = await GetGroupNameAsync(job.TargetOfficeGroupId); + var additionalContentParams = new[] + { + groupName, + job.TargetOfficeGroupId.ToString(), + DateTime.UtcNow.AddDays(_handleInactiveJobsConfig.NumberOfDaysBeforeDeletion).ToString() + }; + var messageContent = new Dictionary { { "SyncJob", job }, From d69c940ec5115c3f988173f754298c794f3cdacd Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Thu, 13 Jun 2024 11:30:17 -0700 Subject: [PATCH 0017/1479] TCU Notification refactor --- .../EmailSender/EmailSenderFunction.cs | 8 ++-- .../EmailSender/EmailSenderRequest.cs | 9 ++-- .../GroupOwnersReaderFunction.cs | 36 --------------- .../GroupOwnersReaderRequest.cs | 12 ----- .../Orchestrator/OrchestratorFunction.cs | 20 +++------ .../TeamsChannelUpdater/Function/Startup.cs | 13 +++++- .../ITeamsChannelUpdaterService.cs | 3 +- .../TeamsChannelUpdaterService.cs | 45 +++++++++++-------- .../Services.Tests/OrchestratorTests.cs | 23 +++------- .../TeamsChannelUpdaterServiceTests.cs | 8 ++-- 10 files changed, 65 insertions(+), 112 deletions(-) delete mode 100644 Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderFunction.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderRequest.cs diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs index dedc8dd00..74df3955e 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs @@ -25,9 +25,11 @@ public EmailSenderFunction(ILoggingRepository loggingRepository, ITeamsChannelUp [FunctionName(nameof(EmailSenderFunction))] public async Task SendEmailAsync([ActivityTrigger] EmailSenderRequest request) { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(EmailSenderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); - await _teamsChannelUpdaterService.SendEmailAsync(request.ToEmail, request.ContentTemplate, request.AdditionalContentParams, request.RunId, request.CcEmail, null, null); - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(EmailSenderFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(EmailSenderFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); + + await _teamsChannelUpdaterService.SendEmailAsync(request.SyncJob, request.NotificationType, request.AdditionalContentParams); + + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(EmailSenderFunction)} function completed", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); } } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderRequest.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderRequest.cs index 4f1ef1f5e..56b8a0361 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderRequest.cs @@ -1,15 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System; +using Models; +using Models.Notifications; namespace Hosts.TeamsChannelUpdater { public class EmailSenderRequest { - public Guid RunId { get; set; } - public string ToEmail { get; set; } - public string ContentTemplate { get; set; } + public SyncJob SyncJob { get; set; } + public NotificationMessageType NotificationType { get; set; } public string[] AdditionalContentParams { get; set; } - public string CcEmail { get; set; } } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderFunction.cs deleted file mode 100644 index ebeb85b63..000000000 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderFunction.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Models; -using Repositories.Contracts; -using Services.TeamsChannelUpdater.Contracts; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Hosts.TeamsChannelUpdater -{ - public class GroupOwnersReaderFunction - { - private readonly ILoggingRepository _loggingRepository; - private readonly ITeamsChannelUpdaterService _teamsChannelUpdaterService; - - public GroupOwnersReaderFunction(ILoggingRepository loggingRepository, ITeamsChannelUpdaterService teamsChannelUpdaterService) - { - _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); - _teamsChannelUpdaterService = teamsChannelUpdaterService ?? throw new ArgumentNullException(nameof(teamsChannelUpdaterService)); - } - - [FunctionName(nameof(GroupOwnersReaderFunction))] - public async Task> GetGroupOwnersAsync([ActivityTrigger] GroupOwnersReaderRequest request) - { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupOwnersReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); - _teamsChannelUpdaterService.RunId = request.RunId; - var owners = await _teamsChannelUpdaterService.GetGroupOwnersAsync(request.GroupId, request.RunId); - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupOwnersReaderFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); - - return owners; - } - } -} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderRequest.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderRequest.cs deleted file mode 100644 index 064491d66..000000000 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using System; - -namespace Hosts.TeamsChannelUpdater -{ - public class GroupOwnersReaderRequest - { - public Guid RunId { get; set; } - public Guid GroupId { get; set; } - } -} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Orchestrator/OrchestratorFunction.cs index d80e8b365..f70b3d5ff 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Orchestrator/OrchestratorFunction.cs @@ -18,6 +18,7 @@ using Models.Entities; using System.Text.Json; using Services.TeamsChannelUpdater.Contracts; +using Models.Notifications; namespace Hosts.TeamsChannelUpdater { @@ -125,11 +126,6 @@ public async Task RunOrchestratorAsync( var groupName = await context.CallActivityAsync(nameof(GroupNameReaderFunction), new GroupNameReaderRequest { RunId = groupMembership.RunId, GroupId = groupMembership.Destination.ObjectId }); - var groupOwners = await context.CallActivityAsync>(nameof(GroupOwnersReaderFunction), - new GroupOwnersReaderRequest { RunId = groupMembership.RunId, GroupId = groupMembership.Destination.ObjectId }); - - var ownerEmails = string.Join(";", groupOwners.Where(x => !string.IsNullOrWhiteSpace(x.Mail)).Select(x => x.Mail)); - var additionalContent = new[] { groupMembership.Destination.ObjectId.ToString(), @@ -142,14 +138,12 @@ public async Task RunOrchestratorAsync( }; await context.CallActivityAsync(nameof(EmailSenderFunction), - new EmailSenderRequest - { - ToEmail = ownerEmails, - CcEmail = _emailSenderAndRecipients.SyncCompletedCCAddresses, - ContentTemplate = SyncCompletedEmailBody, - AdditionalContentParams = additionalContent, - RunId = groupMembership.RunId - }); + new EmailSenderRequest + { + SyncJob = syncJob, + NotificationType = NotificationMessageType.SyncCompletedNotification, + AdditionalContentParams = additionalContent, + }); } diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs index 41e0c74f5..9da030e70 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs @@ -11,6 +11,7 @@ using Microsoft.Graph; using Repositories.BlobStorage; using Repositories.Contracts; +using Repositories.ServiceBusQueue; using Repositories.TeamsChannel; using Services.TeamsChannelUpdater; using Services.TeamsChannelUpdater.Contracts; @@ -44,7 +45,6 @@ public override void Configure(IFunctionsHostBuilder builder) return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); }) - .AddTransient() .AddTransient() .AddSingleton(services => { @@ -52,7 +52,16 @@ public override void Configure(IFunctionsHostBuilder builder) var serviceBusMembershipUpdatersTopic = GetValueOrThrow("serviceBusMembershipUpdatersTopic"); var receiver = client.CreateReceiver(serviceBusMembershipUpdatersTopic, "TeamsChannelUpdater"); return receiver; - }); + }) + .AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + return new ServiceBusQueueRepository(sender); + }) + .AddTransient(); } } } diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater.Contracts/ITeamsChannelUpdaterService.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater.Contracts/ITeamsChannelUpdaterService.cs index 7be9884c3..ed1c25671 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater.Contracts/ITeamsChannelUpdaterService.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater.Contracts/ITeamsChannelUpdaterService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Models; using Models.Entities; +using Models.Notifications; namespace Services.TeamsChannelUpdater.Contracts { @@ -15,6 +16,6 @@ public interface ITeamsChannelUpdaterService public Task<(int SuccessCount, List UserRemovesFailed)> RemoveUsersFromChannelAsync(AzureADTeamsChannel azureADTeamsChannel, List members); public Task GetGroupNameAsync(Guid groupId, Guid runId); public Task> GetGroupOwnersAsync(Guid groupObjectId, Guid runId, int top = 0); - public Task SendEmailAsync(string toEmail, string contentTemplate, string[] additionalContentParams, Guid runId, string ccEmail = null, string emailSubject = null, string[] additionalSubjectParams = null); + public Task SendEmailAsync(SyncJob job, NotificationMessageType notificationType, string[] additionalContentParams); } } diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater/TeamsChannelUpdaterService.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater/TeamsChannelUpdaterService.cs index f15edbacd..03a4cfe5e 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater/TeamsChannelUpdaterService.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater/TeamsChannelUpdaterService.cs @@ -4,7 +4,10 @@ using Models.Entities; using Repositories.Contracts; using Services.TeamsChannelUpdater.Contracts; -using Repositories.Contracts.InjectConfig; +using Models.Notifications; +using Models.ServiceBus; +using System.Text.Json; +using System.Text; namespace Services.TeamsChannelUpdater { @@ -16,8 +19,7 @@ public class TeamsChannelUpdaterService : ITeamsChannelUpdaterService private readonly ITeamsChannelRepository _teamsChannelRepository; private readonly IDatabaseSyncJobsRepository _syncJobRepository; private readonly ILoggingRepository _loggingRepository; - private readonly IMailRepository _mailRepository; - private readonly IEmailSenderRecipient _emailSenderAndRecipients; + private readonly IServiceBusQueueRepository _serviceBusQueueRepository; private Guid _runId; public Guid RunId @@ -33,14 +35,12 @@ public Guid RunId public TeamsChannelUpdaterService(ITeamsChannelRepository teamsChannelRepository, IDatabaseSyncJobsRepository syncJobRepository, ILoggingRepository loggingRepository, - IMailRepository mailRepository, - IEmailSenderRecipient emailSenderAndRecipients) + IServiceBusQueueRepository serviceBusQueueRepository) { _teamsChannelRepository = teamsChannelRepository ?? throw new ArgumentNullException(nameof(teamsChannelRepository)); _syncJobRepository = syncJobRepository ?? throw new ArgumentNullException(nameof(syncJobRepository)); _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); - _mailRepository = mailRepository ?? throw new ArgumentNullException(nameof(mailRepository)); - _emailSenderAndRecipients = emailSenderAndRecipients ?? throw new ArgumentNullException(nameof(emailSenderAndRecipients)); + _serviceBusQueueRepository = serviceBusQueueRepository ?? throw new ArgumentNullException(nameof(serviceBusQueueRepository)); } public async Task GetSyncJobAsync(Guid syncJobId) @@ -109,19 +109,28 @@ public async Task> GetGroupOwnersAsync(Guid groupObjectId, Gui return await _teamsChannelRepository.GetGroupOwnersAsync(groupObjectId, runId, top); } - public async Task SendEmailAsync(string toEmail, string contentTemplate, string[] additionalContentParams, Guid runId, string ccEmail = null, string emailSubject = null, string[] additionalSubjectParams = null) + public async Task SendEmailAsync(SyncJob job, NotificationMessageType notificationType, string[] additionalContentParams) { - await _mailRepository.SendMailAsync(new EmailMessage + var messageContent = new Dictionary { - Subject = emailSubject ?? EmailSubject, - Content = contentTemplate, - SenderAddress = _emailSenderAndRecipients.SenderAddress, - SenderPassword = _emailSenderAndRecipients.SenderPassword, - ToEmailAddresses = toEmail, - CcEmailAddresses = ccEmail, - AdditionalContentParams = additionalContentParams, - AdditionalSubjectParams = additionalSubjectParams - }, runId); + { "SyncJob", job }, + { "AdditionalContentParameters", additionalContentParams } + }; + + var body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(messageContent)); + var message = new ServiceBusMessage + { + MessageId = $"{job.Id}_{job.RunId}_{notificationType}", + Body = body + }; + message.ApplicationProperties.Add("MessageType", notificationType.ToString()); + + await _serviceBusQueueRepository.SendMessageAsync(message); + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = job.RunId, + Message = $"Sent message {message.MessageId} to service bus notifications queue " + }); } } diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/OrchestratorTests.cs index 92e37e210..802c0f2a9 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/OrchestratorTests.cs @@ -14,6 +14,8 @@ using Models.ServiceBus; using System.Text.Json; using Repositories.Contracts.InjectConfig; +using Microsoft.Graph.Models; +using Models.Notifications; namespace Services.Tests { @@ -81,13 +83,6 @@ public void SetUp() groupName = await CallGroupNameReaderFunctionAsync(request as GroupNameReaderRequest); }) .ReturnsAsync(() => groupName); - List owners = new List(); - _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync>(nameof(GroupOwnersReaderFunction), It.IsAny())) - .Callback(async (name, request) => - { - owners = await CallGroupOwnersReaderFunctionAsync(request as GroupOwnersReaderRequest); - }) - .ReturnsAsync(() => owners); _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(EmailSenderFunction), It.IsAny())) .Callback(async (name, request) => { @@ -143,8 +138,9 @@ public async Task TestOrchestratorOngoingSync() ), Times.Once); _mockLoggingRepository.Verify(x => x.RemoveSyncJobProperties(It.IsAny()), Times.Once()); - _mockTeamsChannelUpdaterService.Verify(x => x.UpdateSyncJobStatusAsync(It.IsAny(), SyncStatus.Idle, false, It.IsAny())); _mockTeamsChannelUpdaterService.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _mockTeamsChannelUpdaterService.Verify(x => x.UpdateSyncJobStatusAsync(It.IsAny(), SyncStatus.Idle, false, It.IsAny())); + + _mockTeamsChannelUpdaterService.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] @@ -182,8 +178,7 @@ public async Task TestOrchestratorInitialSync() _mockLoggingRepository.Verify(x => x.RemoveSyncJobProperties(It.IsAny()), Times.Once()); _mockTeamsChannelUpdaterService.Verify(x => x.UpdateSyncJobStatusAsync(It.IsAny(), SyncStatus.Idle, false, It.IsAny())); - _mockTeamsChannelUpdaterService.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _mockTeamsChannelUpdaterService.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny())); } [TestMethod] @@ -267,12 +262,6 @@ private async Task CallGroupNameReaderFunctionAsync(GroupNameReaderReque return await groupNameReaderFunction.GetGroupNameAsync(request); } - private async Task> CallGroupOwnersReaderFunctionAsync(GroupOwnersReaderRequest request) - { - var groupOwnersReaderFunction = new GroupOwnersReaderFunction(_mockLoggingRepository.Object, _mockTeamsChannelUpdaterService.Object); - return await groupOwnersReaderFunction.GetGroupOwnersAsync(request); - } - private async Task CallEmailSenderFunctionAsync(EmailSenderRequest request) { var emailSenderFunction = new EmailSenderFunction(_mockLoggingRepository.Object, _mockTeamsChannelUpdaterService.Object); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/TeamsChannelUpdaterServiceTests.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/TeamsChannelUpdaterServiceTests.cs index decf272dc..dd9e48d0f 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/TeamsChannelUpdaterServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/TeamsChannelUpdaterServiceTests.cs @@ -20,8 +20,7 @@ public class TeamsChannelUpdaterServiceTests private Mock _mockTeamsChannelRepository = null!; private Mock _mockSyncJobRepository = null!; private Mock _mockLoggingRepository = null!; - private Mock _mockMailRepository = null!; - private Mock _mockEmailSenderRecipient = null!; + private Mock _mockServiceBusQueueRepository = null!; private string _groupName = "Group 1 Display Name"; @@ -84,11 +83,10 @@ public void SetUp() _mockLoggingRepository = new Mock(); - _mockMailRepository = new Mock(); - _mockEmailSenderRecipient = new Mock(); + _mockServiceBusQueueRepository = new Mock(); _teamsChannelUpdaterService = new TeamsChannelUpdaterService(_mockTeamsChannelRepository.Object, _mockSyncJobRepository.Object, - _mockLoggingRepository.Object, _mockMailRepository.Object, _mockEmailSenderRecipient.Object); + _mockLoggingRepository.Object, _mockServiceBusQueueRepository.Object); } From fe5c63ae586c2176b9aa2d984f88c81af46ced35 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 18 Jun 2024 08:50:31 -0700 Subject: [PATCH 0018/1479] Updated JobTrigger Startup to support channelReadWriteApplicationPermissionGranted --- .../Hosts/JobTrigger/Function/Startup.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs index c59c2c632..c6e21e20a 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Azure.Core; using Azure.Messaging.ServiceBus; using Common.DependencyInjection; using DIConcreteTypes; @@ -66,9 +67,23 @@ public override void Configure(IFunctionsHostBuilder builder) var configuration = services.GetService(); var graphCredentials = services.GetService>().Value; - graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; - graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; - var graphServiceClient = new GraphServiceClient(FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials)); + + var channelReadWriteApplicationPermissionGranted = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); + + TokenCredential graphTokenCredential; + + if (channelReadWriteApplicationPermissionGranted) + { + graphTokenCredential = FunctionAppDI.CreateAuthProviderFromSecret(graphCredentials); + } + else + { + graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; + graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; + + graphTokenCredential = FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials); + } + var graphServiceClient = new GraphServiceClient(graphTokenCredential); return new TeamsChannelRepository(loggingRepository, graphServiceClient, telemetryClient); }); From a27556a322e6199353095fe6276028a41efe1c71 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 18 Jun 2024 08:55:38 -0700 Subject: [PATCH 0019/1479] Updated TeamsChannelUpdater Startup to support channelReadWriteApplicationPermissionGranted --- .../TeamsChannelUpdater/Function/Startup.cs | 26 ++++++++++++++++--- .../Function/TeamsChannelUpdater.sln | 6 +++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs index 9da030e70..1e641350a 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Azure.Core; using Azure.Messaging.ServiceBus; using Common.DependencyInjection; using Hosts.FunctionBase; @@ -33,9 +34,22 @@ public override void Configure(IFunctionsHostBuilder builder) { var configuration = services.GetService(); var graphCredentials = services.GetService>().Value; - graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; - graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; - return new GraphServiceClient(FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials)); + + var channelReadWriteApplicationPermissionGranted = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); + + TokenCredential graphTokenCredential; + if (channelReadWriteApplicationPermissionGranted) + { + graphTokenCredential = FunctionAppDI.CreateAuthProviderFromSecret(graphCredentials); + } + else + { + graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; + graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; + + graphTokenCredential = FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials); + } + return new GraphServiceClient(graphTokenCredential); }) .AddSingleton((s) => { @@ -63,5 +77,11 @@ public override void Configure(IFunctionsHostBuilder builder) }) .AddTransient(); } + + private bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) + { + var checkParse = bool.TryParse(configuration[settingName], out bool value); + return checkParse ? value : defaultValue; + } } } diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln index e0b978875..bcae45cb8 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln @@ -45,6 +45,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Mail", "..\..\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Localization", "..\..\..\Repositories.Localization\Repositories.Localization.csproj", "{8C5D2583-A04B-45E1-A4F5-40B5E5DEC042}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.RetryPolicyProvider", "..\..\..\Repositories.RetryPolicyProvider\Repositories.RetryPolicyProvider.csproj", "{FCF83599-33C3-4C6F-BCA4-0EAC2D7D0ABF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -131,6 +133,10 @@ Global {8C5D2583-A04B-45E1-A4F5-40B5E5DEC042}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C5D2583-A04B-45E1-A4F5-40B5E5DEC042}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C5D2583-A04B-45E1-A4F5-40B5E5DEC042}.Release|Any CPU.Build.0 = Release|Any CPU + {FCF83599-33C3-4C6F-BCA4-0EAC2D7D0ABF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCF83599-33C3-4C6F-BCA4-0EAC2D7D0ABF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCF83599-33C3-4C6F-BCA4-0EAC2D7D0ABF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCF83599-33C3-4C6F-BCA4-0EAC2D7D0ABF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 9bb213a26266fa15737a5dcbe3a50d1b89f854f0 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 18 Jun 2024 08:57:35 -0700 Subject: [PATCH 0020/1479] Updated TeamsChannelMembershipObtainer Startup to support channelReadWriteApplicationPermissionGranted --- .../Function/Startup.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/Startup.cs index 490c25d32..99d7b5731 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/Startup.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Azure.Core; using Azure.Messaging.ServiceBus; using Common.DependencyInjection; using Hosts.FunctionBase; @@ -39,9 +40,22 @@ public override void Configure(IFunctionsHostBuilder builder) { var configuration = services.GetService(); var graphCredentials = services.GetService>().Value; - graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; - graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; - return new GraphServiceClient(FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials)); + + var channelReadWriteApplicationPermissionGranted = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); + + TokenCredential graphTokenCredential; + if (channelReadWriteApplicationPermissionGranted) + { + graphTokenCredential = FunctionAppDI.CreateAuthProviderFromSecret(graphCredentials); + } + else + { + graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; + graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; + + graphTokenCredential = FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials); + } + return new GraphServiceClient(graphTokenCredential); }) .AddSingleton((s) => { @@ -71,5 +85,11 @@ static IAsyncPolicy GetRetryPolicy() .OrResult(msg => msg.StatusCode != System.Net.HttpStatusCode.NoContent) .WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); } + + private bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) + { + var checkParse = bool.TryParse(configuration[settingName], out bool value); + return checkParse ? value : defaultValue; + } } } From 7360f67de30a6df9c388759fdfe7f1c1c5babb2e Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 18 Jun 2024 09:24:10 -0700 Subject: [PATCH 0021/1479] Updated JobTrigger to skip ownership checks for TeamsChannel when application permission is granted --- .../DIConcreteTypes/JobTriggerConfig.cs | 4 +++- .../Hosts/JobTrigger/Function/Startup.cs | 1 + .../Hosts/JobTrigger/Services/JobTriggerService.cs | 6 ++++-- .../InjectConfig/IJobTriggerConfig.cs | 3 ++- .../Repositories.Mocks/MockJobTriggerConfig.cs | 4 +++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Service/GroupMembershipManagement/DIConcreteTypes/JobTriggerConfig.cs b/Service/GroupMembershipManagement/DIConcreteTypes/JobTriggerConfig.cs index fcc58732c..f27ca477e 100644 --- a/Service/GroupMembershipManagement/DIConcreteTypes/JobTriggerConfig.cs +++ b/Service/GroupMembershipManagement/DIConcreteTypes/JobTriggerConfig.cs @@ -8,14 +8,16 @@ public class JobTriggerConfig : IJobTriggerConfig { public JobTriggerConfig() {} - public JobTriggerConfig(bool gmmHasGroupReadWriteAllPermissions, int jobCountThreshold, int jobPerMilleThreshold) + public JobTriggerConfig(bool gmmHasGroupReadWriteAllPermissions, bool gmmHasChannelReadWriteAllPermissions, int jobCountThreshold, int jobPerMilleThreshold) { GMMHasGroupReadWriteAllPermissions = gmmHasGroupReadWriteAllPermissions; + GMMHasChannelReadWriteAllPermissions = gmmHasChannelReadWriteAllPermissions; JobCountThreshold = jobCountThreshold; JobPerMilleThreshold = jobPerMilleThreshold; } public bool GMMHasGroupReadWriteAllPermissions { get; set; } + public bool GMMHasChannelReadWriteAllPermissions { get; set; } public int JobCountThreshold { get; set; } public int JobPerMilleThreshold { get; set; } } diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs index c6e21e20a..fbddcba45 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs @@ -40,6 +40,7 @@ public override void Configure(IFunctionsHostBuilder builder) builder.Services.AddOptions().Configure((settings, configuration) => { settings.GMMHasGroupReadWriteAllPermissions = GetBoolSetting(configuration, "JobTrigger:IsGroupReadWriteAllGranted", false); + settings.GMMHasChannelReadWriteAllPermissions = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); settings.JobCountThreshold = GetIntSetting(configuration, "JobTrigger:JobCountThreshold", 10); settings.JobPerMilleThreshold = GetIntSetting(configuration, "JobTrigger:JobPerMilleThreshold", 10); }); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs index 360f5b9ec..88d4a44f5 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs @@ -267,12 +267,14 @@ private async Task TeamsChannelExistsAndGMMCanWriteTo if (!await CheckTeamExists(job, channel)) return DestinationVerifierResult.NotFound; - if (!await CheckGMMIsTeamOwner(job, channel)) + if (!_jobTriggerConfig.GMMHasChannelReadWriteAllPermissions && !await CheckGMMIsTeamOwner(job, channel)) return DestinationVerifierResult.NotOwnedByGMM; + if (!await CheckChannelExists(job, channel)) return DestinationVerifierResult.NotFound; - if (!await CheckGMMIsChannelOwner(job, channel)) + if(!_jobTriggerConfig.GMMHasChannelReadWriteAllPermissions && !await CheckGMMIsChannelOwner(job, channel)) return DestinationVerifierResult.NotOwnedByGMM; + return DestinationVerifierResult.Success; } diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/InjectConfig/IJobTriggerConfig.cs b/Service/GroupMembershipManagement/Repositories.Contracts/InjectConfig/IJobTriggerConfig.cs index f15e4124c..580b4805e 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts/InjectConfig/IJobTriggerConfig.cs +++ b/Service/GroupMembershipManagement/Repositories.Contracts/InjectConfig/IJobTriggerConfig.cs @@ -5,7 +5,8 @@ namespace Repositories.Contracts.InjectConfig public interface IJobTriggerConfig { public bool GMMHasGroupReadWriteAllPermissions { get; } - public int JobCountThreshold { get; } + public bool GMMHasChannelReadWriteAllPermissions { get; } + public int JobCountThreshold { get; } public int JobPerMilleThreshold { get; } } diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/MockJobTriggerConfig.cs b/Service/GroupMembershipManagement/Repositories.Mocks/MockJobTriggerConfig.cs index 850f0a7d3..3b4b9defc 100644 --- a/Service/GroupMembershipManagement/Repositories.Mocks/MockJobTriggerConfig.cs +++ b/Service/GroupMembershipManagement/Repositories.Mocks/MockJobTriggerConfig.cs @@ -7,12 +7,14 @@ namespace Repositories.Mocks public class MockJobTriggerConfig : IJobTriggerConfig { public bool GMMHasGroupReadWriteAllPermissions { get; set; } - public int JobCountThreshold { get; set; } + public bool GMMHasChannelReadWriteAllPermissions { get; set; } + public int JobCountThreshold { get; set; } public int JobPerMilleThreshold { get; set; } public MockJobTriggerConfig() { GMMHasGroupReadWriteAllPermissions = false; + GMMHasChannelReadWriteAllPermissions = false; JobCountThreshold = 4; JobPerMilleThreshold = 250; From 62564df2f58c302d54e8a7b454f176155ed22bd4 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 25 Jun 2024 15:33:20 -0700 Subject: [PATCH 0022/1479] Added app config setting TeamsChannel:IsChannelReadWriteApplicationPermissionGranted with default setting false --- Infrastructure/data/template.bicep | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Infrastructure/data/template.bicep b/Infrastructure/data/template.bicep index f5d488a9a..a742adb2a 100644 --- a/Infrastructure/data/template.bicep +++ b/Infrastructure/data/template.bicep @@ -150,6 +150,7 @@ param logAnalyticsSku string = 'PerGB2018' param authenticationType string = 'ClientSecret' param skipMailNotifications bool = false param isMailApplicationPermissionGranted bool = false +param isTeamsChannelApplicationPermissionGranted bool = false @description('Enter app configuration name.') @minLength(1) @@ -352,6 +353,14 @@ param appConfigurationKeyData array = [ tag1: 'Mail' } } + { + key: 'TeamsChannel:IsChannelReadWriteApplicationPermissionGranted' + value: isTeamsChannelApplicationPermissionGranted + contentType: 'bool' + tag: { + tag1: 'TeamsChannel' + } + } ] @description('Array of feature flags objects. {id:"value", description:"description", enabled:true }') From c646ac5e7f4fc4e69dfde542b6e3fd62c23ebd78 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Thu, 27 Jun 2024 16:42:43 -0700 Subject: [PATCH 0023/1479] Fix for NotifierService to add back the send email response --- .../Hosts/Notifier/Services.Notifier/NotifierService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index a48f9a405..1ac5846e1 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -115,7 +115,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage IsHTML = true }; - await _mailRepository.SendMailAsync(message, null); + var response = await _mailRepository.SendMailAsync(message, null); TrackSentNotificationEvent(notification.TargetOfficeGroupId); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Sent email to recipient addresses." }); From 20afd78bbc5814824af630a0bdc82158da329d32 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 8 Jul 2024 15:42:17 -0700 Subject: [PATCH 0024/1479] Updated AzureMaintenance new email to pass in info in correct order for Notifier --- .../Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs | 2 +- .../Resources/LocalizationRepository.en-US.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs index 8043ee151..1594094ef 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/AzureMaintenanceService.cs @@ -74,8 +74,8 @@ public async Task SendEmailAsync(SyncJob job, NotificationMessageType notificati var groupName = await GetGroupNameAsync(job.TargetOfficeGroupId); var additionalContentParams = new[] { - groupName, job.TargetOfficeGroupId.ToString(), + groupName, DateTime.UtcNow.AddDays(_handleInactiveJobsConfig.NumberOfDaysBeforeDeletion).ToString() }; diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 5289c2876..2a019995b 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -1065,7 +1065,7 @@ Please reach out to support to update the source definition and re-enable the me - You received this notification because you are listed as the owner of **{0}**. + You received this notification because you are listed as the owner of **{1}**. Your group's membership synchronizations are currently disabled due to one of the following reasons: From 4b6129fda44c74f93685fd54ee78dc32ecdb80f7 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 8 Jul 2024 15:57:47 -0700 Subject: [PATCH 0025/1479] Added Sql Migration for new NotificationType: InactiveSyncJobNotification --- ...notification_type_add_inactive.Designer.cs | 511 ++++++++++++++++++ ...08224924_notification_type_add_inactive.cs | 26 + .../Migrations/GMMContextModelSnapshot.cs | 10 +- 3 files changed, 545 insertions(+), 2 deletions(-) create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240708224924_notification_type_add_inactive.Designer.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240708224924_notification_type_add_inactive.cs diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240708224924_notification_type_add_inactive.Designer.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240708224924_notification_type_add_inactive.Designer.cs new file mode 100644 index 000000000..4c5b5ca4b --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240708224924_notification_type_add_inactive.Designer.cs @@ -0,0 +1,511 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Repositories.EntityFramework.Contexts; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + [DbContext(typeof(GMMContext))] + [Migration("20240708224924_notification_type_add_inactive")] + partial class notification_type_add_inactive + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.22") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.Property("DestinationOwnersId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("DestinationOwnersId", "SyncJobsId"); + + b.HasIndex("SyncJobsId"); + + b.ToTable("DestinationOwnerSyncJob"); + }); + + modelBuilder.Entity("Entities.SqlMembershipSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Attributes") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomLabel") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SqlMembershipSources"); + + b.HasData( + new + { + Id = new Guid("bb0eff76-405d-43c0-8a65-e8ccc322511e"), + Name = "SqlMembership" + }); + }); + + modelBuilder.Entity("Entities.SyncJobChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("ChangeDetails") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeSource") + .HasColumnType("int"); + + b.Property("ChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValue(new DateTime(2024, 7, 8, 22, 49, 23, 577, DateTimeKind.Utc).AddTicks(7304)); + + b.Property("ChangedByDisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedByObjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ChangeSource"); + + b.HasIndex("ChangeTime"); + + b.HasIndex("ChangedByObjectId"); + + b.HasIndex("SyncJobId"); + + b.ToTable("SyncJobChanges"); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("DestinationNames"); + }); + + modelBuilder.Entity("Models.DestinationOwner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("ObjectId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.ToTable("DestinationOwners"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("NotificationTypeID") + .HasColumnType("int"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("NotificationTypeID"); + + b.HasIndex("SyncJobId", "NotificationTypeID") + .IsUnique(); + + b.ToTable("JobNotifications"); + }); + + modelBuilder.Entity("Models.NotificationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .IsUnicode(false) + .HasColumnType("varchar(max)"); + + b.HasKey("Id"); + + b.ToTable("NotificationTypes"); + + b.HasData( + new + { + Id = 1, + Disabled = false, + Name = "ThresholdNotification" + }, + new + { + Id = 2, + Disabled = false, + Name = "SyncStartedNotification" + }, + new + { + Id = 3, + Disabled = false, + Name = "SyncCompletedNotification" + }, + new + { + Id = 4, + Disabled = false, + Name = "DestinationNotExistNotification" + }, + new + { + Id = 5, + Disabled = false, + Name = "SourceNotExistNotification" + }, + new + { + Id = 6, + Disabled = false, + Name = "NotOwnerNotification" + }, + new + { + Id = 7, + Disabled = false, + Name = "NotValidSourceNotification" + }, + new + { + Id = 8, + Disabled = false, + Name = "NoDataNotification" + }, + new + { + Id = 9, + Disabled = false, + Name = "NormalThresholdNotification" + }, + new + { + Id = 10, + Disabled = false, + Name = "InactiveSyncJobNotification" + }); + }); + + modelBuilder.Entity("Models.PurgedSyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("PurgedAt") + .HasColumnType("datetime2"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PurgedSyncJobs"); + }); + + modelBuilder.Entity("Models.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("SettingKey") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SettingValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SettingKey") + .IsUnique(); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("Models.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SortPriority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Statuses", (string)null); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(450)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Status") + .IsUnique() + .HasFilter("[Status] IS NOT NULL"); + + b.ToTable("SyncJobs"); + }); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.HasOne("Models.DestinationOwner", null) + .WithMany() + .HasForeignKey("DestinationOwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.HasOne("Models.SyncJob", "SyncJob") + .WithOne("DestinationName") + .HasForeignKey("Models.DestinationName", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.HasOne("Models.NotificationType", "NotificationType") + .WithMany() + .HasForeignKey("NotificationTypeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", "SyncJob") + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationType"); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.HasOne("Models.Status", "StatusDetails") + .WithOne() + .HasForeignKey("Models.SyncJob", "Status") + .HasPrincipalKey("Models.Status", "Name") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("StatusDetails"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Navigation("DestinationName"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240708224924_notification_type_add_inactive.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240708224924_notification_type_add_inactive.cs new file mode 100644 index 000000000..078643761 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240708224924_notification_type_add_inactive.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + public partial class notification_type_add_inactive : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "NotificationTypes", + columns: new[] { "Id", "Disabled", "Name" }, + values: new object[] { 10, false, "InactiveSyncJobNotification" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 10); + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs index c4840831d..959d37489 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs @@ -64,7 +64,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasData( new { - Id = new Guid("907ecc0b-60b9-4a9e-b536-c14993d64e46"), + Id = new Guid("bb0eff76-405d-43c0-8a65-e8ccc322511e"), Name = "SqlMembership" }); }); @@ -90,7 +90,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ChangeTime") .ValueGeneratedOnAdd() .HasColumnType("datetime2") - .HasDefaultValue(new DateTime(2024, 6, 12, 18, 30, 23, 125, DateTimeKind.Utc).AddTicks(6706)); + .HasDefaultValue(new DateTime(2024, 7, 8, 22, 49, 23, 577, DateTimeKind.Utc).AddTicks(7304)); b.Property("ChangedByDisplayName") .IsRequired() @@ -253,6 +253,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) Id = 9, Disabled = false, Name = "NormalThresholdNotification" + }, + new + { + Id = 10, + Disabled = false, + Name = "InactiveSyncJobNotification" }); }); From e2eb51a97773cbb9393b1fe9551b67220b9a80cd Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 10 Jul 2024 14:18:25 -0700 Subject: [PATCH 0026/1479] Prevent non-job writers from editing jobs --- UI/web-app/src/pages/JobDetails/JobDetails.base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx index 22b02e48b..9b6b192cc 100644 --- a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx +++ b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx @@ -213,7 +213,7 @@ export const JobDetailsBase: React.FunctionComponent = ( children={} removeButton={isJobWriter} editButton={canEditJob} - actionText={canEditJob ? strings.JobDetails.editButton : strings.JobDetails.viewDetails} + actionText={canEditJob ? strings.JobDetails.editButton : ''} useLinkButton={true} actionOnClick={openMembershipConfiguration} /> From 4f4fc0d47577e0d6ccb8821970aa2b87af32c846 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 25 Jun 2024 12:21:01 -0700 Subject: [PATCH 0027/1479] reorder items --- .../HRQuerySource/HRQuerySource.base.tsx | 75 +++---------------- 1 file changed, 12 insertions(+), 63 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 328e13842..311416ecc 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -1064,17 +1064,18 @@ const checkType = (value: string, type: string | undefined): string => { } ]; - function onUpClick(index: number, items: IFilterPart[]) { + function reorderItems(index: number, newIndex: number, items: IFilterPart[]) { let newItems = [...items]; - const insertIndex = index - 1; - if (insertIndex < 0) { return; } + const insertIndex = newIndex; + if (insertIndex < 0 || insertIndex >= items.length || index === newIndex) { return; } let sourceItems: IFilterPart[] = []; sourceItems.push(items[index]); sourceItems.push(items[insertIndex]); - let transformedItems: ChildType[] = sourceItems.map((item) => ({ - filter: `${item.attribute} ${item.equalityOperator} ${item.value} ${item.andOr}`, - })); + let transformedItems: ChildType[] = sourceItems.map((item) => { + const filter = `${item.attribute} ${item.equalityOperator} ${item.value}`; + return { filter: item.andOr ? `${filter} ${item.andOr}` : "undefined" }; + }); const hasUndefined = transformedItems.some((item) => item.filter.includes("undefined")); if (hasUndefined) { return; } @@ -1122,65 +1123,13 @@ const checkType = (value: string, type: string | undefined): string => { } } - function onDownClick(index: number, items: IFilterPart[]) { - let newItems = [...items]; - const insertIndex = index + 1; - - if (insertIndex >= items.length) { return; } - - let sourceItems: IFilterPart[] = []; - sourceItems.push(items[index]); - sourceItems.push(items[insertIndex]); - let transformedItems: ChildType[] = sourceItems.map((item) => ({ - filter: `${item.attribute} ${item.equalityOperator} ${item.value} ${item.andOr}`, - })); - const hasUndefined = transformedItems.some((item) => item.filter.includes("undefined")); - if (hasUndefined) { return; } - - setIsDragAndDropEnabled(true); - newItems = newItems.filter((_, i) => i !== index); - newItems.splice(insertIndex, 0, { ...items[index] }); - let newChildren: ChildType[] = newItems.map((item) => ({ - filter: `${item.attribute} ${item.equalityOperator} ${item.value} ${item.andOr}`, - })); - - if (groupingEnabled) { - const groupIndex = groups.findIndex(group => - group.children?.some(child => - child.items.some(item => - JSON.stringify(item) === JSON.stringify(items[index]) - ) - ) || group.items?.some(item => - JSON.stringify(item) === JSON.stringify(items[index]) - ) - ); - const childIndex = groupIndex !== -1 ? groups[groupIndex].children.findIndex(child => - child.items.some(item => - JSON.stringify(item) === JSON.stringify(items[index]) - ) - ) : -1; - - if (groupIndex !== -1 && childIndex === -1) { - groups[groupIndex].items = newItems; - setGroups(groups); - getGroupLabels(groups); - setItemsBasedOnGroups(groups); - } - else if (groupIndex !== -1 && childIndex !== -1) { - groups[groupIndex].children[childIndex].items = newItems; - setGroups(groups); - getGroupLabels(groups); - setItemsBasedOnGroups(groups); - } - } - else { - groups[0].items = newItems; - setGroups(groups); - setChildren(newChildren); - setItems(newItems); - } + function onUpClick(index: number, items: IFilterPart[]) { + reorderItems(index, index - 1, items); } + function onDownClick(index: number, items: IFilterPart[]) { + reorderItems(index, index + 1, items); + } function onGroupUpClick(index: number) { let newGroups = [...groups]; From cf8d3d439f07166aaeb457131929748f950090b9 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Thu, 27 Jun 2024 16:00:45 -0700 Subject: [PATCH 0028/1479] code cleanup --- UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 311416ecc..131fc2037 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -26,7 +26,6 @@ import { SqlMembershipAttribute, SqlMembershipAttributeValue } from '../../model import { IFilterPart } from '../../models/IFilterPart'; import { Group } from '../../models/Group'; import { parseGroup, stringifyGroups } from './QuerySerializer'; -import { GetAttributeValuesResponse } from '../../models/GetAttributeValuesResponse'; export const getClassNames = classNamesFunction(); @@ -988,6 +987,7 @@ const checkType = (value: string, type: string | undefined): string => { const onAttributeChange = (text: string, index: number) => { let newFilteredOptions = { ...filteredOptions }; + if (groupingEnabled && groups.length > 1) return; if (attributes && attributes.length > 0) { if (!text) { newFilteredOptions[index] = getOptions(attributes); @@ -1001,6 +1001,7 @@ const checkType = (value: string, type: string | undefined): string => { const onAttributeValueChange = (text: string, index: number) => { let newFilteredValueOptions = { ...filteredValueOptions }; + if (groupingEnabled && groups.length > 1) return; const currentAttributeValues = attributeValues[items[index].attribute].values || []; if (currentAttributeValues.length > 0) { if (!text) { From 8f9795670bfe108bbd2dc979a442188aea394458 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Thu, 27 Jun 2024 13:03:28 -0700 Subject: [PATCH 0029/1479] Added features/int branch to trigger branches for public yaml --- vsts-cicd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/vsts-cicd.yml b/vsts-cicd.yml index 37f4b726e..f7ad48b30 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -5,6 +5,7 @@ name: $(major).$(minor).$(Date:yyMM).$(Rev:r) trigger: +- features/int - develop - main From db5235555e0b7b069a2dc77e5ecdcae4b5a95941 Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 19 Jun 2024 14:58:42 -0700 Subject: [PATCH 0030/1479] Add new roles, Job.Delete.OwnedBy, Job.Enable.OwnedBy, Job.EditConfiguration.OwnedBy --- .../Hosts/WebApi/Documentation/WebApiSetup.md | 10 ++++++ .../WebApi/Scripts/Set-AppRolesIfNeeded.ps1 | 24 +++++++++++++ .../WebApi/WebApi.Models/DTOs/RolesObject.cs | 3 ++ .../Hosts/WebApi/WebApi.Models/Roles.cs | 3 ++ .../WebApi.Tests/JobDetailsControllerTests.cs | 2 +- .../WebApi.Tests/RolesControllerTests.cs | 7 +++- .../v1/Jobs/JobDetailsController.cs | 4 +-- .../Controllers/v1/Roles/RolesController.cs | 36 +++++++++++++++---- UI/web-app/src/apis/roles/IRolesApi.ts | 3 ++ .../src/pages/JobDetails/JobDetails.base.tsx | 12 ++++--- UI/web-app/src/store/roles.slice.tsx | 7 +++- 11 files changed, 95 insertions(+), 16 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md b/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md index cdb8ae3f0..9a7f5fcc4 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md @@ -38,10 +38,20 @@ The roles are: - Users with this role have **read** access to Membership Management page. - They can view onboarded destinations that they own. +- Job Owner Enabler + - Users with this role can use the UI to enable or disable destinations that they own. + +- Job Owner Deleter + - Users with this role can use the UI to delete destinations that they own. + +- Job Owner Configuration Editor + - Users with this role can edit the configuration of destinations that they own. + - Job Owner Writer - Users with this role have **read write** access to groups that they own in the Membership Management page. - They can view onboarded destinations that they own. - They can submit updates or onboarding requests for destinations that they own. + - They can delete destinations they own. - Job Tenant Reader - Users with this role have access to Membership Management page. diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 b/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 index 37de9c540..87921b331 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 @@ -56,6 +56,30 @@ function Set-AppRolesIfNeeded { IsEnabled = $True AllowedMemberTypes = @($memberTypes) }, + @{ + DisplayName = "Job Owner Enabler/Disabler" + Description = "Can enable or disable owned destinations in the tenant." + Value = "Job.Enable.OwnedBy" + Id = [Guid]::NewGuid().ToString() + IsEnabled = $True + AllowedMemberTypes = @($memberTypes) + }, + @{ + DisplayName = "Job Owner Deleter" + Description = "Can delete the job from GMM (disable GMM sync)." + Value = "Job.Delete.OwnedBy" + Id = [Guid]::NewGuid().ToString() + IsEnabled = $True + AllowedMemberTypes = @($memberTypes) + }, + @{ + DisplayName = "Job Owner Configuration Editor" + Description = "Can update owned destinations' configuration." + Value = "Job.EditConfiguration.OwnedBy" + Id = [Guid]::NewGuid().ToString() + IsEnabled = $True + AllowedMemberTypes = @($memberTypes) + }, @{ DisplayName = "Job Writer" Description = "Can create, view, and update owned destinations in the tenant." diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs index dbaec7616..76ca210fa 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs @@ -10,6 +10,9 @@ public RolesObject() } public bool IsJobOwnerReader { get; set; } + public bool IsJobOwnerEnabler { get; set; } + public bool IsJobOwnerDeleter { get; set; } + public bool IsJobOwnerConfigurationEditor { get; set; } public bool IsJobOwnerWriter { get; set; } public bool IsJobTenantReader { get; set; } public bool IsJobTenantWriter { get; set; } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs index 10f624835..c3fef15f9 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs @@ -6,6 +6,9 @@ namespace WebApi.Models public class Roles { public const string JOB_OWNER_READER = "Job.Read.OwnedBy"; + public const string JOB_OWNER_ENABLER = "Job.Enable.OwnedBy"; + public const string JOB_OWNER_DELETER = "Job.Delete.OwnedBy"; + public const string JOB_OWNER_CONFIGURATION_EDITOR = "Job.EditConfiguration.OwnedBy"; public const string JOB_OWNER_WRITER = "Job.ReadWrite.OwnedBy"; public const string JOB_TENANT_READER = "Job.Read.All"; public const string JOB_TENANT_WRITER = "Job.ReadWrite.All"; diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs index 53178b91a..a14389b56 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs @@ -355,7 +355,7 @@ public async Task PatchJobWhenIsAnOwner(string role) } [TestMethod] - [DataRow(Roles.JOB_OWNER_WRITER)] + [DataRow(Roles.JOB_OWNER_DELETER)] public async Task RemoveGMMAsyncWhenIsAnAuthorizedUser(string role) { var syncJobId = Guid.NewGuid(); diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs index 57d4561a0..f7d02483a 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs @@ -27,6 +27,9 @@ public void GetAllRolesStatus_ForVariousUsers() var claims = new List { new Claim(ClaimTypes.Role, Roles.JOB_OWNER_WRITER), + new Claim(ClaimTypes.Role, Roles.JOB_OWNER_ENABLER), + new Claim(ClaimTypes.Role, Roles.JOB_OWNER_DELETER), + new Claim(ClaimTypes.Role, Roles.JOB_OWNER_CONFIGURATION_EDITOR), new Claim(ClaimTypes.Role, Roles.JOB_OWNER_READER), new Claim(ClaimTypes.Role, Roles.JOB_TENANT_READER), new Claim(ClaimTypes.Role, Roles.JOB_TENANT_WRITER), @@ -44,8 +47,10 @@ public void GetAllRolesStatus_ForVariousUsers() Assert.IsNotNull(okResult); var rolesStatuses = okResult.Value as RolesObject; Assert.IsNotNull(rolesStatuses); - Assert.IsTrue(rolesStatuses.IsJobOwnerWriter); Assert.IsTrue(rolesStatuses.IsJobOwnerReader); + Assert.IsTrue(rolesStatuses.IsJobOwnerEnabler); + Assert.IsTrue(rolesStatuses.IsJobOwnerDeleter); + Assert.IsTrue(rolesStatuses.IsJobOwnerWriter); Assert.IsTrue(rolesStatuses.IsJobTenantReader); Assert.IsTrue(rolesStatuses.IsJobTenantWriter); Assert.IsTrue(rolesStatuses.IsSubmissionReviewer); diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Jobs/JobDetailsController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Jobs/JobDetailsController.cs index 22df37384..be9edcb3a 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Jobs/JobDetailsController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Jobs/JobDetailsController.cs @@ -44,7 +44,7 @@ public async Task>> GetJobDetailsAsync(Guid sy }; } - [Authorize(Roles = Models.Roles.JOB_TENANT_WRITER + "," + Models.Roles.SUBMISSION_REVIEWER)] + [Authorize(Roles = $"{Models.Roles.JOB_OWNER_ENABLER}, {Models.Roles.JOB_TENANT_WRITER}, {Models.Roles.JOB_OWNER_CONFIGURATION_EDITOR}, {Models.Roles.SUBMISSION_REVIEWER}")] [HttpPatch("{syncJobId}")] [Consumes("application/json-patch+json")] public async Task UpdateSyncJobAsync(Guid syncJobId, [FromBody] JsonPatchDocument patchDocument) @@ -81,7 +81,7 @@ public async Task UpdateSyncJobAsync(Guid syncJobId, [FromBody] Js } } - [Authorize(Roles = Models.Roles.JOB_OWNER_WRITER + "," + Models.Roles.JOB_TENANT_WRITER)] + [Authorize(Roles = $"{Models.Roles.JOB_OWNER_DELETER},{Models.Roles.JOB_OWNER_WRITER},{Models.Roles.JOB_TENANT_WRITER}")] [HttpPost("{syncJobId}/removeGMM")] public async Task RemoveGMMAsync(Guid syncJobId) { diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs index e142729ed..47280f8f4 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs @@ -19,15 +19,37 @@ public RolesController() [HttpGet("getAllRoles")] public ActionResult GetAllRoles() { + var isJobOwnerReader = User.IsInRole(Models.Roles.JOB_OWNER_READER); + var isJobOwnerEnabler = User.IsInRole(Models.Roles.JOB_OWNER_ENABLER); + var isJobOwnerDeleter = User.IsInRole(Models.Roles.JOB_OWNER_DELETER); + var isJobOwnerConfigurationEditor = User.IsInRole(Models.Roles.JOB_OWNER_CONFIGURATION_EDITOR); + var isJobOwnerWriter = User.IsInRole(Models.Roles.JOB_OWNER_WRITER); + + if (User.IsInRole(Models.Roles.JOB_OWNER_WRITER)) + { + isJobOwnerReader = true; + isJobOwnerEnabler = true; + isJobOwnerConfigurationEditor = true; + isJobOwnerDeleter = true; + }; + + var isJobTenantReader = User.IsInRole(Models.Roles.JOB_TENANT_READER); + var isJobTenantWriter = User.IsInRole(Models.Roles.JOB_TENANT_WRITER); + var isSubmissionReviewer = User.IsInRole(Models.Roles.SUBMISSION_REVIEWER); + var isHyperlinkAdministrator = User.IsInRole(Models.Roles.HYPERLINK_ADMINISTRATOR); + var isCustomMembershipProviderAdministrator = User.IsInRole(Models.Roles.CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR); + var roleStatus = new Models.DTOs.RolesObject { - IsJobOwnerReader = User.IsInRole(Models.Roles.JOB_OWNER_READER), - IsJobOwnerWriter = User.IsInRole(Models.Roles.JOB_OWNER_WRITER), - IsJobTenantReader = User.IsInRole(Models.Roles.JOB_TENANT_READER), - IsJobTenantWriter = User.IsInRole(Models.Roles.JOB_TENANT_WRITER), - IsSubmissionReviewer = User.IsInRole(Models.Roles.SUBMISSION_REVIEWER), - IsHyperlinkAdministrator = User.IsInRole(Models.Roles.HYPERLINK_ADMINISTRATOR), - IsCustomMembershipProviderAdministrator = User.IsInRole(Models.Roles.CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR) + IsJobOwnerReader = isJobOwnerReader, + IsJobOwnerEnabler = isJobOwnerEnabler, + IsJobOwnerDeleter = isJobOwnerDeleter, + IsJobOwnerWriter = isJobOwnerWriter, + IsJobTenantReader = isJobTenantReader, + IsJobTenantWriter = isJobTenantWriter, + IsSubmissionReviewer = isSubmissionReviewer, + IsHyperlinkAdministrator = isHyperlinkAdministrator, + IsCustomMembershipProviderAdministrator = isCustomMembershipProviderAdministrator }; return Ok(roleStatus); diff --git a/UI/web-app/src/apis/roles/IRolesApi.ts b/UI/web-app/src/apis/roles/IRolesApi.ts index 9ec0cfbe5..6f9a9863c 100644 --- a/UI/web-app/src/apis/roles/IRolesApi.ts +++ b/UI/web-app/src/apis/roles/IRolesApi.ts @@ -3,6 +3,9 @@ export interface Roles { isJobOwnerReader(): boolean; + isJobOwnerEnabler(): boolean; + isJobOwnerDeleter(): boolean; + isJobOwnerConfigurationEditor(): boolean; isJobOwnerWriter(): boolean; isJobTenantReader(): boolean; isJobTenantWriter(): boolean; diff --git a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx index 9b6b192cc..6f0cdf61e 100644 --- a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx +++ b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx @@ -54,7 +54,7 @@ import { import { JobDetails } from '../../models/JobDetails'; import { useStrings } from '../../store/hooks'; import { setPagingBarVisible } from '../../store/pagingBar.slice'; -import { selectIsJobWriter, selectIsSubmissionReviewer } from '../../store/roles.slice'; +import { selectIsJobOwnerDeleter, selectIsJobOwnerEnabler, selectIsJobWriter, selectIsSubmissionReviewer } from '../../store/roles.slice'; import { SyncStatus } from '../../models'; import { OnboardingSteps } from '../../models/OnboardingSteps'; import { fetchJobs } from '../../store/jobs.api'; @@ -98,8 +98,10 @@ export const JobDetailsBase: React.FunctionComponent = ( const jobsLoading = useSelector(selectJobsLoading); const removeGMMPending = useSelector(selectRemoveGMMLoading); const isJobWriter = useSelector(selectIsJobWriter); - const showLoader: boolean = jobsLoading || removeGMMPending; + const isJobOwnerDeleter: boolean = useSelector(selectIsJobOwnerDeleter); + const canDeleteJob: boolean = isJobWriter || isJobOwnerDeleter; const canEditJob: boolean = isJobWriter && (job.status !== SyncStatus.PendingReview && job.status !== SyncStatus.SubmissionRejected); + const showLoader: boolean = jobsLoading || removeGMMPending; const OpenInNewWindowIcon: IIconProps = { iconName: 'OpenInNewWindow' }; @@ -218,7 +220,7 @@ export const JobDetailsBase: React.FunctionComponent = ( actionOnClick={openMembershipConfiguration} />
- {isJobWriter && + {canDeleteJob && = ( const patchResponse = useSelector(selectPatchJobDetailsResponse); const [jobStatus, setJobStatus] = useState(job.status); const [isJobEnabled, setIsJobEnabled] = useState(job.enabledOrNot); + const isJobEnabler = useSelector(selectIsJobOwnerEnabler); const isJobWriter = useSelector(selectIsJobWriter); + const canEnableJob = isJobEnabler || isJobWriter; useEffect(() => { setJobStatus(job.status); @@ -351,7 +355,7 @@ const MembershipStatusContent: React.FunctionComponent = ( inlineLabel={true} checked={isJobEnabled} onChange={handleStatusChange} - disabled={!isJobWriter || jobStatus === SyncStatus.PendingReview || jobStatus === SyncStatus.SubmissionRejected} + disabled={!canEnableJob || jobStatus === SyncStatus.PendingReview || jobStatus === SyncStatus.SubmissionRejected} />
diff --git a/UI/web-app/src/store/roles.slice.tsx b/UI/web-app/src/store/roles.slice.tsx index 1f8cb4129..8e9ff9fb2 100644 --- a/UI/web-app/src/store/roles.slice.tsx +++ b/UI/web-app/src/store/roles.slice.tsx @@ -8,6 +8,8 @@ import { getAllRoles } from './roles.api'; // Define a type for the slice stat export type Roles = { isJobOwnerReader: boolean; + isJobOwnerEnabler: boolean; + isJobOwnerDeleter: boolean; isJobOwnerWriter: boolean; isJobTenantReader: boolean; isJobTenantWriter: boolean; @@ -20,6 +22,8 @@ export type Roles = { // Define the initial state using that ty const initialState: Roles = { isJobOwnerReader: false, + isJobOwnerEnabler: false, + isJobOwnerDeleter: false, isJobOwnerWriter: false, isJobTenantReader: false, isJobTenantWriter: false, @@ -49,7 +53,8 @@ export const rolesSlice = createSlice({ export const selectIsFetchingRoles = (state: RootState) => state.roles.isFetchingRoles; export const selectIsJobOwnerReader = (state: RootState) => state.roles.isJobOwnerReader; -export const selectIsJobOwnerWriter = (state: RootState) => state.roles.isJobOwnerWriter; +export const selectIsJobOwnerEnabler = (state: RootState) => state.roles.isJobOwnerEnabler; +export const selectIsJobOwnerDeleter = (state: RootState) => state.roles.isJobOwnerDeleter; export const selectIsJobTenantReader = (state: RootState) => state.roles.isJobTenantReader; export const selectIsJobTenantWriter = (state: RootState) => state.roles.isJobTenantWriter; export const selectIsSubmissionReviewer = (state: RootState) => state.roles.isSubmissionReviewer; From 3d38bcd89289ef86f39a5e25dc8fe7ca817fa1d1 Mon Sep 17 00:00:00 2001 From: abgonz Date: Tue, 25 Jun 2024 15:47:14 -0700 Subject: [PATCH 0031/1479] Add not found page --- .../src/components/JobsList/JobsList.base.tsx | 6 +- UI/web-app/src/index.tsx | 3 +- .../src/pages/NotFound/NotFound.base.tsx | 62 +++++++++++++++++++ .../src/pages/NotFound/NotFound.styles.ts | 29 +++++++++ UI/web-app/src/pages/NotFound/NotFound.ts | 21 +++++++ .../src/pages/NotFound/NotFound.types.ts | 32 ++++++++++ UI/web-app/src/pages/NotFound/index.ts | 5 ++ UI/web-app/src/pages/index.ts | 1 + .../src/services/localization/IStrings.ts | 1 + .../i18n/locales/en/translations.ts | 1 + .../i18n/locales/es/translations.ts | 1 + 11 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 UI/web-app/src/pages/NotFound/NotFound.base.tsx create mode 100644 UI/web-app/src/pages/NotFound/NotFound.styles.ts create mode 100644 UI/web-app/src/pages/NotFound/NotFound.ts create mode 100644 UI/web-app/src/pages/NotFound/NotFound.types.ts create mode 100644 UI/web-app/src/pages/NotFound/index.ts diff --git a/UI/web-app/src/components/JobsList/JobsList.base.tsx b/UI/web-app/src/components/JobsList/JobsList.base.tsx index d4406e3ce..885d32299 100644 --- a/UI/web-app/src/components/JobsList/JobsList.base.tsx +++ b/UI/web-app/src/components/JobsList/JobsList.base.tsx @@ -250,7 +250,11 @@ export const JobsListBase: React.FunctionComponent = ( index?: number, ev?: React.FocusEvent ): void => { - navigate('/JobDetails', { replace: false, state: { item: item } }); + if(item.targetGroupName === null){ + navigate('/NotFound', { replace: true, state: { item: item} }); + } else { + navigate('/JobDetails', { replace: false, state: { item: item } }); + } }; const onRefreshClicked = ( diff --git a/UI/web-app/src/index.tsx b/UI/web-app/src/index.tsx index 5b495914e..ed2e2547e 100644 --- a/UI/web-app/src/index.tsx +++ b/UI/web-app/src/index.tsx @@ -10,7 +10,7 @@ import { Provider } from 'react-redux'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { App } from './App'; -import { AdminConfig, JobsPage, JobDetails, OwnerPage, ManageMembership } from './pages'; +import { AdminConfig, JobsPage, JobDetails, OwnerPage, ManageMembership, NotFound } from './pages'; import { store } from './store'; const connectionString = process.env.REACT_APP_APPINSIGHTS_CONNECTIONSTRING; @@ -55,6 +55,7 @@ ReactDOM.render( } /> } /> } /> + } /> diff --git a/UI/web-app/src/pages/NotFound/NotFound.base.tsx b/UI/web-app/src/pages/NotFound/NotFound.base.tsx new file mode 100644 index 000000000..e68775372 --- /dev/null +++ b/UI/web-app/src/pages/NotFound/NotFound.base.tsx @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React, { useEffect } from 'react'; +import { + IProcessedStyleSet, + classNamesFunction, + format, + useTheme, +} from '@fluentui/react'; +import { Page } from '../../components/Page'; +import { PageHeader } from '../../components/PageHeader'; +import { INotFoundProps, INotFoundStyleProps, INotFoundStyles } from './NotFound.types'; +import { useStrings } from '../../store/hooks'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { setPagingBarVisible } from '../../store/pagingBar.slice'; +import { useDispatch } from 'react-redux'; +import { AppDispatch } from '../../store'; +import { Job } from '../../models/Job'; + +const getClassNames = classNamesFunction< + INotFoundStyleProps, + INotFoundStyles +>(); + +export const NotFoundBase: React.FunctionComponent = ( + props: INotFoundProps +) => { + const { className, styles } = props; + + const classNames: IProcessedStyleSet = getClassNames( + styles, + { + className, + theme: useTheme(), + } + ); + const dispatch = useDispatch(); + const location = useLocation(); + const job: Job = location.state.item; + const strings = useStrings(); + const navigate = useNavigate(); + + const handleBackToDashboardButtonClick = () => { + navigate('/'); + }; + + useEffect(() => { + dispatch(setPagingBarVisible(false)); + }, [dispatch]); + + return ( + + +
+
+ {format(strings.JobDetails.notFound, job.targetGroupId)} +
+
+
+ ) +}; \ No newline at end of file diff --git a/UI/web-app/src/pages/NotFound/NotFound.styles.ts b/UI/web-app/src/pages/NotFound/NotFound.styles.ts new file mode 100644 index 000000000..eb8bb77c0 --- /dev/null +++ b/UI/web-app/src/pages/NotFound/NotFound.styles.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + type INotFoundStyleProps, + type INotFoundStyles, +} from './NotFound.types'; + +export const getStyles = (props: INotFoundStyleProps): INotFoundStyles => { + const { className } = props; + + return { + root: [{ + padding: '9px 36px 0px 36px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '20vh', + }, className], + container: { + maxWidth: '600px', + alignItems: 'center', + textAlign: 'center', + fontSize: '20px', + } + }; +}; + + diff --git a/UI/web-app/src/pages/NotFound/NotFound.ts b/UI/web-app/src/pages/NotFound/NotFound.ts new file mode 100644 index 000000000..6d07a7e39 --- /dev/null +++ b/UI/web-app/src/pages/NotFound/NotFound.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { styled } from '@fluentui/react'; +import type * as React from 'react'; + +import { NotFoundBase } from './NotFound.base'; +import { getStyles } from './NotFound.styles'; +import { + type INotFoundProps, + type INotFoundStyleProps, + type INotFoundStyles, +} from './NotFound.types'; + +export const NotFound: React.FunctionComponent = styled< + INotFoundProps, + INotFoundStyleProps, + INotFoundStyles +>(NotFoundBase, getStyles, undefined, { + scope: 'NotFound', +}); diff --git a/UI/web-app/src/pages/NotFound/NotFound.types.ts b/UI/web-app/src/pages/NotFound/NotFound.types.ts new file mode 100644 index 000000000..aa0ca8eb7 --- /dev/null +++ b/UI/web-app/src/pages/NotFound/NotFound.types.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + type IStyle, + type IStyleFunctionOrObject, + type ITheme, +} from '@fluentui/react'; +import type React from 'react'; + +export interface INotFoundStyles { + root: IStyle; + container: IStyle; +} + +export interface INotFoundStyleProps { + className?: string; + theme: ITheme; +} + +export interface INotFoundProps + extends React.AllHTMLAttributes { + /** + * Optional className to apply to the root of the component. + */ + className?: string; + + /** + * Call to provide customized styling that will layer on top of the variant rules. + */ + styles?: IStyleFunctionOrObject; +} diff --git a/UI/web-app/src/pages/NotFound/index.ts b/UI/web-app/src/pages/NotFound/index.ts new file mode 100644 index 000000000..a29ff6298 --- /dev/null +++ b/UI/web-app/src/pages/NotFound/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export * from './NotFound'; +export * from './NotFound.types'; \ No newline at end of file diff --git a/UI/web-app/src/pages/index.ts b/UI/web-app/src/pages/index.ts index bde6a90c0..6f6a44d3a 100644 --- a/UI/web-app/src/pages/index.ts +++ b/UI/web-app/src/pages/index.ts @@ -6,3 +6,4 @@ export * from './JobDetails'; export * from './ManageMembership'; export * from './JobsPage'; export * from './OwnerPage'; +export * from './NotFound'; \ No newline at end of file diff --git a/UI/web-app/src/services/localization/IStrings.ts b/UI/web-app/src/services/localization/IStrings.ts index cde281654..72374cf94 100644 --- a/UI/web-app/src/services/localization/IStrings.ts +++ b/UI/web-app/src/services/localization/IStrings.ts @@ -175,6 +175,7 @@ export type IStrings = { internalError: string; removeGMMError: string; } + notFound: string; openInAzure: string; viewDetails: string; editButton: string; diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index 943039e62..50df83ac2 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -183,6 +183,7 @@ export const strings: IStrings = { openInAzure: 'Open in Azure', viewDetails: 'View Details', editButton: 'Edit', + notFound: 'Membership syncs for group with object Id {0} are now disabled because the group no longer exists.', }, JobsList: { listOfMemberships: 'Managed groups', diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index de9593b34..8f90380af 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -183,6 +183,7 @@ export const strings: IStrings = { internalError: 'No podemos procesar su solicitud en este momento. Por favor, inténtelo de nuevo más tarde.', removeGMMError: 'Error al dejar de administrar con GMM.', }, + notFound: 'La sincronización de membresía para el grupo con ID {0} ahora está deshabilitada porque el grupo ya no existe.', openInAzure: 'Abrir en Azure', viewDetails: 'Ver Detalles', editButton: 'Editar', From 206627f72fc94f20ff5c3faa95c752725a05bc9e Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Fri, 5 Jul 2024 11:13:05 -0700 Subject: [PATCH 0032/1479] fix 'jobs in error' metric --- Infrastructure/data/dashboard.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Infrastructure/data/dashboard.bicep b/Infrastructure/data/dashboard.bicep index 26d0577c8..ec2a89235 100644 --- a/Infrastructure/data/dashboard.bicep +++ b/Infrastructure/data/dashboard.bicep @@ -4557,7 +4557,7 @@ resource name_resource 'Microsoft.Portal/dashboards@2015-08-01-preview' = { GridColumnsWidth: { Message: '379px' } - Query: 'ApplicationLog_CL\n| where ((location_s != "ProfileSync") and (Message has "exception" or Message has "error") and Message !has "Response" and Message !has "Regex Expression:") or (Message has "Setting job status to" and Message !has "Idle" and Message !has "InProgress" and Message !has "StuckInProgress")\n| distinct TimeGenerated, location_s, Message, RunId_g\n| order by TimeGenerated desc\n' + Query: 'ApplicationLog_CL\n| where ((location_s != "ProfileSync") and (Message has "exception" or Message has "error") and Message !has "Response" and Message !has "ErrorInvalidRecipients" and Message !has "Regex Expression:") or (Message has "Setting job status to" and Message !has "Idle" and Message !has "InProgress" and Message !has "StuckInProgress")\n| distinct TimeGenerated, location_s, Message, RunId_g\n| order by TimeGenerated desc\n' PartTitle: 'Jobs marked as Error' } } From 33120e04923925089e09bf687be29803f5a1e8c7 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 8 Jul 2024 13:15:28 -0700 Subject: [PATCH 0033/1479] Updated public build yaml to buildSourceCode by default --- vsts-cicd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsts-cicd.yml b/vsts-cicd.yml index f7ad48b30..4b23271c4 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -31,7 +31,7 @@ parameters: - name: buildSourceCode displayName: Build source code? type: boolean - default: false + default: true stages: From 51d44f72edf633b725ddf54cceaf663cd4887a3c Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 2 Jul 2024 12:29:53 -0700 Subject: [PATCH 0034/1479] enable editing sqlmembership source part --- UI/web-app/src/components/SourcePart/SourcePart.base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/web-app/src/components/SourcePart/SourcePart.base.tsx b/UI/web-app/src/components/SourcePart/SourcePart.base.tsx index 4c0e8cc5e..1a896ac19 100644 --- a/UI/web-app/src/components/SourcePart/SourcePart.base.tsx +++ b/UI/web-app/src/components/SourcePart/SourcePart.base.tsx @@ -204,7 +204,7 @@ export const SourcePartBase: React.FunctionComponent = (props: {part.query.type === SourcePartType.HR && (
- { } : handleSourceChange} /> +
)} {part.query.type === SourcePartType.GroupMembership && ( From cb72303713d8bc5ffb22295a7f2cdebcf614758a Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 8 Jul 2024 16:08:02 -0700 Subject: [PATCH 0035/1479] add sql membership schema file --- .../Models/Schemas/SqlMembershipSchema.json | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 Service/GroupMembershipManagement/Models/Schemas/SqlMembershipSchema.json diff --git a/Service/GroupMembershipManagement/Models/Schemas/SqlMembershipSchema.json b/Service/GroupMembershipManagement/Models/Schemas/SqlMembershipSchema.json new file mode 100644 index 000000000..e4c94c138 --- /dev/null +++ b/Service/GroupMembershipManagement/Models/Schemas/SqlMembershipSchema.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["SqlMembership"] + }, + "source": { + "type": "object", + "anyOf": [ + { + "required": ["manager"] + }, + { + "required": ["filter"] + } + ], + "properties": { + "manager": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "depth": { + "type": "integer", + "minimum": 1 + } + }, + "required": ["id"], + "additionalProperties": false + }, + "filter": { + "type": "string" + } + }, + "additionalProperties": false + }, + "exclusionary": { + "type": "boolean" + } + }, + "required": ["type", "source"], + "additionalProperties": false + } +} From 63cd63e661b9a438a9d6bf85add6a9be10fa053b Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 8 Jul 2024 17:00:49 -0700 Subject: [PATCH 0036/1479] add group membership schema file --- .../Models/Schemas/GroupMembershipSchema.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Service/GroupMembershipManagement/Models/Schemas/GroupMembershipSchema.json diff --git a/Service/GroupMembershipManagement/Models/Schemas/GroupMembershipSchema.json b/Service/GroupMembershipManagement/Models/Schemas/GroupMembershipSchema.json new file mode 100644 index 000000000..8751e7d22 --- /dev/null +++ b/Service/GroupMembershipManagement/Models/Schemas/GroupMembershipSchema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["GroupMembership"] + }, + "source": { + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "minLength": 36, + "maxLength": 36 + }, + "exclusionary": { + "type": "boolean" + } + }, + "required": ["type", "source"], + "additionalProperties": false + } +} From 49181e807d76f71d9b052e654e87b3acc04ec469 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 8 Jul 2024 17:01:10 -0700 Subject: [PATCH 0037/1479] add group ownership schema file --- .../Models/Schemas/GroupOwnershipSchema.json | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 Service/GroupMembershipManagement/Models/Schemas/GroupOwnershipSchema.json diff --git a/Service/GroupMembershipManagement/Models/Schemas/GroupOwnershipSchema.json b/Service/GroupMembershipManagement/Models/Schemas/GroupOwnershipSchema.json new file mode 100644 index 000000000..029c95888 --- /dev/null +++ b/Service/GroupMembershipManagement/Models/Schemas/GroupOwnershipSchema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["GroupOwnership"] + }, + "source": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": ["All", "Hybrid", "GroupMembership"] + }, + "oneOf": [ + { + "contains": { "const": "All" }, + "not": { "contains": { "const": "Hybrid" } } + }, + { + "contains": { "const": "Hybrid" }, + "not": { "contains": { "const": "All" } } + }, + { + "not": { + "anyOf": [ + { "contains": { "const": "All" } }, + { "contains": { "const": "Hybrid" } } + ] + } + } + ] + }, + "exclusionary": { + "type": "boolean" + } + }, + "required": ["type", "source"], + "additionalProperties": false +} + \ No newline at end of file From 99f52f36b47ce69d3a5e700a0eea5303b741d909 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 8 Jul 2024 17:01:21 -0700 Subject: [PATCH 0038/1479] add place membership schema file --- .../Models/Schemas/PlaceMembershipSchema.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Service/GroupMembershipManagement/Models/Schemas/PlaceMembershipSchema.json diff --git a/Service/GroupMembershipManagement/Models/Schemas/PlaceMembershipSchema.json b/Service/GroupMembershipManagement/Models/Schemas/PlaceMembershipSchema.json new file mode 100644 index 000000000..629e505be --- /dev/null +++ b/Service/GroupMembershipManagement/Models/Schemas/PlaceMembershipSchema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["PlaceMembership"] + }, + "source": { + "type": "string", + "minLength": 1 + }, + "exclusionary": { + "type": "boolean" + } + }, + "required": ["type", "source"], + "additionalProperties": false +} From 8179d2e8ff1de59313446295af8b44fd6cc53ec8 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 9 Jul 2024 12:19:37 -0700 Subject: [PATCH 0039/1479] add teams channel membership schema file --- .../Schemas/TeamsChannelMembershipSchema.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Service/GroupMembershipManagement/Models/Schemas/TeamsChannelMembershipSchema.json diff --git a/Service/GroupMembershipManagement/Models/Schemas/TeamsChannelMembershipSchema.json b/Service/GroupMembershipManagement/Models/Schemas/TeamsChannelMembershipSchema.json new file mode 100644 index 000000000..8751e7d22 --- /dev/null +++ b/Service/GroupMembershipManagement/Models/Schemas/TeamsChannelMembershipSchema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["GroupMembership"] + }, + "source": { + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "minLength": 36, + "maxLength": 36 + }, + "exclusionary": { + "type": "boolean" + } + }, + "required": ["type", "source"], + "additionalProperties": false + } +} From e6e6df5be00edba7ecae2353529e45104e0b42f7 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 8 Jul 2024 13:48:19 -0700 Subject: [PATCH 0040/1479] Expand new source part --- .../MembershipConfiguration.base.tsx | 3 ++- UI/web-app/src/components/SourcePart/SourcePart.base.tsx | 9 +++++++-- UI/web-app/src/components/SourcePart/SourcePart.types.ts | 1 + UI/web-app/src/models/ISourcePart.ts | 1 + UI/web-app/src/store/manageMembership.slice.tsx | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/UI/web-app/src/components/MembershipConfiguration/MembershipConfiguration.base.tsx b/UI/web-app/src/components/MembershipConfiguration/MembershipConfiguration.base.tsx index 38606a75c..ebfbdc887 100644 --- a/UI/web-app/src/components/MembershipConfiguration/MembershipConfiguration.base.tsx +++ b/UI/web-app/src/components/MembershipConfiguration/MembershipConfiguration.base.tsx @@ -64,7 +64,8 @@ export const MembershipConfigurationBase: React.FunctionComponent = (props: const [isExclusionary, setIsExclusionary] = useState(query.exclusionary); const [errorMessage, setErrorMessage] = useState(''); const isEditingExistingJob = useSelector(manageMembershipIsEditingExistingJob); - const [expanded, setExpanded] = useState(isEditingExistingJob); + const [expanded, setExpanded] = useState(part.isNew ||isEditingExistingJob); const hrSource = useSelector(selectSource); - + + useEffect(() => { + if (part.isNew) { + dispatch(updateSourcePart({ ...part, isNew: false })); + } + }, [dispatch, part]); const handleSourceTypeChanged = (event: React.FormEvent, item: IDropdownOption | undefined): void => { if (!item) return; diff --git a/UI/web-app/src/components/SourcePart/SourcePart.types.ts b/UI/web-app/src/components/SourcePart/SourcePart.types.ts index c7071c4f3..8d4adec37 100644 --- a/UI/web-app/src/components/SourcePart/SourcePart.types.ts +++ b/UI/web-app/src/components/SourcePart/SourcePart.types.ts @@ -36,6 +36,7 @@ export type SourcePartProps = React.AllHTMLAttributes & { totalSourceParts: number; query: SourcePartQuery; part: ISourcePart; + isNew?: boolean; /** * Call to provide customized styling that will layer on top of the variant rules. diff --git a/UI/web-app/src/models/ISourcePart.ts b/UI/web-app/src/models/ISourcePart.ts index eba6cf53e..8de8d18b4 100644 --- a/UI/web-app/src/models/ISourcePart.ts +++ b/UI/web-app/src/models/ISourcePart.ts @@ -6,4 +6,5 @@ import { SourcePartQuery } from "./SourcePartQuery"; export type ISourcePart = { id: number; query: SourcePartQuery; + isNew?: boolean; }; diff --git a/UI/web-app/src/store/manageMembership.slice.tsx b/UI/web-app/src/store/manageMembership.slice.tsx index 49a84446a..8460a7724 100644 --- a/UI/web-app/src/store/manageMembership.slice.tsx +++ b/UI/web-app/src/store/manageMembership.slice.tsx @@ -231,7 +231,7 @@ const manageMembershipSlice = createSlice({ if (index !== -1) { state.sourceParts[index] = { ...state.sourceParts[index], - query: action.payload.query + ...action.payload, }; } }, From d30fa1b4888b6d06a340abc5bdff6ceb04070791 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 25 Jun 2024 12:21:01 -0700 Subject: [PATCH 0041/1479] reorder items From ccd04f46044e0b3a9af2285dc2ef1414ddf28114 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Thu, 27 Jun 2024 16:00:45 -0700 Subject: [PATCH 0042/1479] code cleanup From 3c91dad49ec7010618c23c4727164db4d9a76bc8 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Thu, 27 Jun 2024 13:03:28 -0700 Subject: [PATCH 0043/1479] Added features/int branch to trigger branches for public yaml From 648605ed45eddd421076acec392eff2cf743abff Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 19 Jun 2024 14:58:42 -0700 Subject: [PATCH 0044/1479] Add new roles, Job.Delete.OwnedBy, Job.Enable.OwnedBy, Job.EditConfiguration.OwnedBy From 1602763d134f2a36c636fa19e8ba7f004df4c8ea Mon Sep 17 00:00:00 2001 From: abgonz Date: Tue, 25 Jun 2024 15:47:14 -0700 Subject: [PATCH 0045/1479] Add not found page From e4f4452b7f1cff988c5d6dbb2679dc68d77cf4fc Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Fri, 5 Jul 2024 11:13:05 -0700 Subject: [PATCH 0046/1479] fix 'jobs in error' metric From 741c35f362d574946deeab6467a3fe5b9a1a314a Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 8 Jul 2024 13:15:28 -0700 Subject: [PATCH 0047/1479] Updated public build yaml to buildSourceCode by default From 6f361eb304dcc81f89c42b0da5ab1ac1c7548876 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 2 Jul 2024 12:29:53 -0700 Subject: [PATCH 0048/1479] enable editing sqlmembership source part From 0536f6bd09d579e762c0010a25e827e432bd80d7 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 9 Jul 2024 14:32:44 -0700 Subject: [PATCH 0049/1479] Update migration script --- Scripts/Set-MigrateStorageTable.ps1 | 97 +++++++++++++++++++++++++++++ breaking_changes.md | 13 ++++ 2 files changed, 110 insertions(+) create mode 100644 Scripts/Set-MigrateStorageTable.ps1 diff --git a/Scripts/Set-MigrateStorageTable.ps1 b/Scripts/Set-MigrateStorageTable.ps1 new file mode 100644 index 000000000..01e6857ed --- /dev/null +++ b/Scripts/Set-MigrateStorageTable.ps1 @@ -0,0 +1,97 @@ +$ErrorActionPreference = "Stop" +<# +.SYNOPSIS +Migrate the storage table into a sql table + +.DESCRIPTION +Long description + +.PARAMETER connectionString +connectionString +-notificationsCsvPath + +.EXAMPLE +Set-MigrateStorageTable -connectionString "" ` + -notificationsCsvPath "" ` + -Verbose +#> + +function Set-MigrateStorageTable { + [CmdletBinding()] + param( + [Parameter(Mandatory=$True)] + [string] $connectionString, + [Parameter(Mandatory=$True)] + [string] $notificationsCsvPath + ) + + Write-Host "Start Set-MigrateStorageTable" + + # Connect to the SQL Server instance + $context = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile.DefaultContext + $sqlToken = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate($context.Account, $context.Environment, $context.Tenant.Id.ToString(), $null, [Microsoft.Azure.Commands.Common.Authentication.ShowDialog]::Never, $null, "https://database.windows.net").AccessToken + $connection = New-Object System.Data.SqlClient.SqlConnection + $connection.ConnectionString = $connectionString + $connection.AccessToken = $sqlToken + $connection.Open() + + # Read the notifications CSV file + $notifications = Import-Csv -Path $notificationsCsvPath + + # Function to convert timestamp format if needed + function Convert-Timestamp { + param ( + [string]$timestamp + ) + return [DateTimeOffset]::Parse($timestamp).ToString('yyyy-MM-dd HH:mm:ss') + } + + # Iterate over the CSV rows and insert into SQL table + foreach ($row in $notifications) { + $lastUpdateTime = Convert-Timestamp -timestamp $row.Timestamp + $query = @" + INSERT INTO ThresholdNotifications ( + Id, + TargetOfficeGroupId, + SyncJobId, + StatusName, + ThresholdPercentageForAdditions, + ThresholdPercentageForRemovals, + ChangePercentageForAdditions, + ChangePercentageForRemovals, + ChangeQuantityForAdditions, + ChangeQuantityForRemovals, + CreatedTime, + ResolvedTime, + ResolvedBy, + LastUpdatedTime, + ResolutionName, + CardStateName + ) + VALUES ( + '$($row.Id)', + '$($row.TargetOfficeGroupId)', + '$($row.SyncJobId)', + '$($row.StatusName)', + $($row.ThresholdPercentageForAdditions), + $($row.ThresholdPercentageForRemovals), + $($row.ChangePercentageForAdditions), + $($row.ChangePercentageForRemovals), + $($row.ChangeQuantityForAdditions), + $($row.ChangeQuantityForRemovals), + '$($row.CreatedTime)', + '$($row.ResolvedTime)', + '$($row.ResolvedBy)', + '$lastUpdateTime', + '$($row.ResolutionName)', + '$($row.CardStateName)' + ) +"@ + + $command = $connection.CreateCommand() + $command.CommandText = $query + $command.ExecuteNonQuery() + } + $connection.Close() + Write-Output "Data migration completed successfully." +} diff --git a/breaking_changes.md b/breaking_changes.md index 66183da7f..0ff187557 100644 --- a/breaking_changes.md +++ b/breaking_changes.md @@ -1,5 +1,18 @@ # Breaking Changes +## 7/9/2024 + +The storage of threshold notifications will be migrated from the current storage table to a SQL table. To accommodate this update, users will need to run the following script to migrate existing records. + +1. Export the existing storage table to a CSV file. This can be done using Azure Storage Explorer or any preferred tool for accessing Azure Storage. +2. Run the Set-MigrateStorageTable.ps1 script to migrate existing records from the storage table to the SQL table. + +``` + . .\Set-MigrateStorageTable.ps1 + Set-MigrateStorageTable -connectionString "" ` + -notificationsCsvPath "" ` +``` + ## 5/1/2024 Local auth has been disabled for App Configuration resource. From 4a970241d9917c358ba13b55f6fb9a9b72fb5ea5 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 27 Jun 2024 16:09:38 -0700 Subject: [PATCH 0050/1479] Add DB migration --- .../Entities/ThresholdNotification.cs | 75 ++++++++++++------- .../Services.Notifier/NotifierService.cs | 10 +-- .../Notifications/ThresholdNotification.cs | 14 +--- .../GMMContext.cs | 35 +++++++++ .../NotificationRepository.cs | 2 - 5 files changed, 89 insertions(+), 47 deletions(-) diff --git a/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs b/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs index 0b0165001..0023eb1a7 100644 --- a/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs +++ b/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs @@ -1,31 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Azure; -using Azure.Data.Tables; -using Newtonsoft.Json; using System; -using System.Diagnostics.CodeAnalysis; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using Models.ThresholdNotifications; -using System.Runtime.Serialization; namespace Entities { - [ExcludeFromCodeCoverage] - public class ThresholdNotification : ITableEntity + public class ThresholdNotification { - public ThresholdNotification() - { - } - - public ThresholdNotification(string partitionKey, string rowKey) - { - PartitionKey = partitionKey; - RowKey = rowKey; - } - public string PartitionKey { get; set; } - public string RowKey { get; set; } - public Guid JobId { get; set; } = Guid.Empty; + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] /// /// The threshold notification id. @@ -37,12 +23,17 @@ public ThresholdNotification(string partitionKey, string rowKey) /// public Guid TargetOfficeGroupId { get; set; } + /// + /// The threshold notification sync job's Id. + /// + public Guid SyncJobId { get; set; } + /// /// Gets or sets the notification status name to persist in the azure table store. /// public string StatusName { get; set; } - [IgnoreDataMember] + [NotMapped] public ThresholdNotificationStatus? Status { get @@ -75,12 +66,22 @@ public ThresholdNotificationStatus? Status /// /// The percentage of users to be added as a percentage of the current group size. /// - public int ChangePercentageForAdditions { get; set; } = 0; + public double ChangePercentageForAdditions { get; set; } = 0; /// /// The percentage of users to be removed as a percentage of the current group size. /// - public int ChangePercentageForRemovals { get; set; } = 0; + public double ChangePercentageForRemovals { get; set; } = 0; + + /// + /// The number of users to be added to the current group; + /// + public int ChangeQuantityForAdditions { get; set; } = 0; + + /// + /// The number of users to be removed from the current group. + /// + public int ChangeQuantityForRemovals { get; set; } = 0; /// /// The time the notification was created. @@ -96,13 +97,16 @@ public ThresholdNotificationStatus? Status /// The UPN of the person who resolved the notification. /// public string ResolvedByUPN { get; set; } = string.Empty; + + [DatabaseGenerated(DatabaseGeneratedOption.Computed)] + public DateTime LastUpdatedTime { get; set; } /// /// The action taken to resolve the notification. /// public string ResolutionName { get; set; } - [IgnoreDataMember] + [NotMapped] public ThresholdNotificationResolution? Resolution { get @@ -114,12 +118,29 @@ public ThresholdNotificationResolution? Resolution return (ThresholdNotificationResolution)Enum.Parse(typeof(ThresholdNotificationResolution), this.ResolutionName); } + set + { + this.ResolutionName = value.HasValue ? value.ToString() : null; + } } + public string CardStateName { get; set; } - [JsonIgnore] - public DateTimeOffset? Timestamp { get; set; } + [NotMapped] + public ThresholdNotificationCardState? CardState + { + get + { + if (string.IsNullOrEmpty(this.CardStateName)) + { + return null; + } - [JsonIgnore] - public ETag ETag { get; set; } + return (ThresholdNotificationCardState)Enum.Parse(typeof(ThresholdNotificationCardState), this.CardStateName); + } + set + { + this.ResolutionName = value.HasValue ? value.ToString() : null; + } + } } } diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index 1ac5846e1..e7db1d6be 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -345,11 +345,9 @@ public async Task ParseDestinationAsync(SyncJob syncJob) thresholdNotification = new ThresholdNotification { Id = Guid.NewGuid(), - SyncJobPartitionKey = job.Id.ToString(), - SyncJobRowKey = job.Id.ToString(), SyncJobId = job.Id, - ChangePercentageForAdditions = (int)threshold.IncreaseThresholdPercentage, - ChangePercentageForRemovals = (int)threshold.DecreaseThresholdPercentage, + ChangePercentageForAdditions = threshold.IncreaseThresholdPercentage, + ChangePercentageForRemovals = threshold.DecreaseThresholdPercentage, ChangeQuantityForAdditions = threshold.DeltaToAddCount, ChangeQuantityForRemovals = threshold.DeltaToRemoveCount, CreatedTime = DateTime.UtcNow, @@ -365,8 +363,8 @@ public async Task ParseDestinationAsync(SyncJob syncJob) } else { - thresholdNotification.ChangePercentageForAdditions = (int)threshold.IncreaseThresholdPercentage; - thresholdNotification.ChangePercentageForRemovals = (int)threshold.DecreaseThresholdPercentage; + thresholdNotification.ChangePercentageForAdditions = threshold.IncreaseThresholdPercentage; + thresholdNotification.ChangePercentageForRemovals = threshold.DecreaseThresholdPercentage; thresholdNotification.ChangeQuantityForAdditions = threshold.DeltaToAddCount; thresholdNotification.ChangeQuantityForRemovals = threshold.DeltaToRemoveCount; thresholdNotification.ThresholdPercentageForAdditions = job.ThresholdPercentageForAdditions; diff --git a/Service/GroupMembershipManagement/Models/Notifications/ThresholdNotification.cs b/Service/GroupMembershipManagement/Models/Notifications/ThresholdNotification.cs index e13716178..b1f4180b7 100644 --- a/Service/GroupMembershipManagement/Models/Notifications/ThresholdNotification.cs +++ b/Service/GroupMembershipManagement/Models/Notifications/ThresholdNotification.cs @@ -17,16 +17,6 @@ public class ThresholdNotification /// public Guid SyncJobId { get; set; } = Guid.Empty; - /// - /// The threshold notification sync job's PartitionKey. - /// - public string SyncJobPartitionKey { get; set; } = string.Empty; - - /// - /// The threshold notification sync job's RowKey. - /// - public string SyncJobRowKey { get; set; } = string.Empty; - /// /// The id of the group associated with the notification. /// @@ -50,12 +40,12 @@ public class ThresholdNotification /// /// The percentage of users to be added as a percentage of the current group size. /// - public int ChangePercentageForAdditions { get; set; } = 0; + public double ChangePercentageForAdditions { get; set; } = 0; /// /// The percentage of users to be removed as a percentage of the current group size. /// - public int ChangePercentageForRemovals { get; set; } = 0; + public double ChangePercentageForRemovals { get; set; } = 0; /// /// The number of users to be added to the current group; diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs index c5917afcf..3e78f0ac7 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs @@ -22,6 +22,8 @@ public class GMMContext : DbContext public DbSet DestinationNames { get; set; } public DbSet DestinationOwners { get; set; } public DbSet SyncJobChanges { get; set; } = null!; + public DbSet ThresholdNotifications { get; set; } = null!; + protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().Property(t => t.Id) @@ -141,6 +143,39 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(s => s.ChangeReason).IsRequired(); entity.Property(s => s.ChangeDetails).IsRequired(); }); + modelBuilder.Entity(entity => + { + entity.HasKey(t => t.Id); + + entity.Property(t => t.Id) + .ValueGeneratedOnAdd() + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + entity.Property(t => t.TargetOfficeGroupId).IsRequired(); + entity.Property(t => t.SyncJobId).IsRequired(); + entity.Property(t => t.StatusName).HasMaxLength(50); + entity.Property(t => t.ResolvedByUPN).HasMaxLength(255); + entity.Property(t => t.ResolutionName).HasMaxLength(50); + entity.Property(t => t.CardStateName).HasMaxLength(50); + + entity.Property(t => t.ThresholdPercentageForAdditions).HasDefaultValue(100); + entity.Property(t => t.ThresholdPercentageForRemovals).HasDefaultValue(20); + entity.Property(t => t.ChangePercentageForAdditions).HasDefaultValue(0); + entity.Property(t => t.ChangePercentageForRemovals).HasDefaultValue(0); + entity.Property(t => t.ChangeQuantityForAdditions).HasDefaultValue(0); + entity.Property(t => t.ChangeQuantityForRemovals).HasDefaultValue(0); + entity.Property(t => t.CreatedTime).HasDefaultValueSql("GETUTCDATE()"); + entity.Property(t => t.ResolvedTime).HasDefaultValueSql("GETUTCDATE()"); + entity.Property(t => t.LastUpdatedTime) + .ValueGeneratedOnAddOrUpdate() + .HasDefaultValueSql("GETUTCDATE()"); + + entity.HasOne() + .WithMany() + .HasForeignKey(t => t.SyncJobId) + .OnDelete(DeleteBehavior.Cascade) + .HasPrincipalKey(s => s.Id); + }); modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); diff --git a/Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs b/Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs index 7d6412759..1066138d4 100644 --- a/Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs @@ -96,8 +96,6 @@ private ThresholdNotification ToModel(ThresholdNotificationEntity entity) return new ThresholdNotification { Id = entity.Id, - SyncJobPartitionKey = entity.SyncJobId.ToString(), - SyncJobRowKey = entity.SyncJobId.ToString(), SyncJobId = entity.SyncJobId, ChangePercentageForAdditions = entity.ChangePercentageForAdditions, ChangePercentageForRemovals = entity.ChangePercentageForRemovals, From ac0dcb31807bd602f681c3831eff524e3a3b25de Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 9 Jul 2024 11:34:56 -0700 Subject: [PATCH 0051/1479] Rebase migration --- Documentation/thresholdNotificationSample.csv | 2 +- .../Entities/ThresholdNotification.cs | 2 +- .../Services/DeltaCalculatorService.cs | 2 +- .../NotifierServiceTests.cs | 2 +- .../Services.Notifier/NotifierService.cs | 2 +- .../ResolveNotificationHandler.cs | 2 +- .../NotificationsControllerTests.cs | 13 +- .../ThesholdNotificationResolvedCardData.cs | 6 +- .../ThresholdNotificationCardData.cs | 4 +- .../Notifications/ThresholdNotification.cs | 2 +- .../GMMContext.cs | 2 +- ...9181547_threshold_notification.Designer.cs | 601 ++++++++++++++++++ .../20240709181547_threshold_notification.cs | 57 ++ .../Migrations/GMMContextModelSnapshot.cs | 94 ++- .../NotificationRepository.cs | 4 +- .../ThresholdNotificationEntity.cs | 6 +- .../ThresholdNotificationService.cs | 2 +- 17 files changed, 776 insertions(+), 27 deletions(-) create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240709181547_threshold_notification.Designer.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240709181547_threshold_notification.cs diff --git a/Documentation/thresholdNotificationSample.csv b/Documentation/thresholdNotificationSample.csv index 08f082050..1404952e4 100644 --- a/Documentation/thresholdNotificationSample.csv +++ b/Documentation/thresholdNotificationSample.csv @@ -1,2 +1,2 @@ -PartitionKey,RowKey,Timestamp,Id,Id@type,TargetOfficeGroupId,TargetOfficeGroupId@type,StatusName,StatusName@type,ThresholdPercentageForAdditions,ThresholdPercentageForAdditions@type,ThresholdPercentageForRemovals,ThresholdPercentageForRemovals@type,ChangePercentageForAdditions,ChangePercentageForAdditions@type,ChangePercentageForRemovals,ChangePercentageForRemovals@type,CreatedTime,CreatedTime@type,ResolvedTime,ResolvedTime@type,ResolvedByUPN,ResolvedByUPN@type,ResolutionName,ResolutionName@type,ChangeQuantityForAdditions,ChangeQuantityForAdditions@type,ChangeQuantityForRemovals,ChangeQuantityForRemovals@type,SyncJobPartitionKey,SyncJobPartitionKey@type,SyncJobRowKey,SyncJobRowKey@type +PartitionKey,RowKey,Timestamp,Id,Id@type,TargetOfficeGroupId,TargetOfficeGroupId@type,StatusName,StatusName@type,ThresholdPercentageForAdditions,ThresholdPercentageForAdditions@type,ThresholdPercentageForRemovals,ThresholdPercentageForRemovals@type,ChangePercentageForAdditions,ChangePercentageForAdditions@type,ChangePercentageForRemovals,ChangePercentageForRemovals@type,CreatedTime,CreatedTime@type,ResolvedTime,ResolvedTime@type,ResolvedBy,ResolvedBy@type,ResolutionName,ResolutionName@type,ChangeQuantityForAdditions,ChangeQuantityForAdditions@type,ChangeQuantityForRemovals,ChangeQuantityForRemovals@type,SyncJobPartitionKey,SyncJobPartitionKey@type,SyncJobRowKey,SyncJobRowKey@type ThresholdNotification,00000000-0000-0000-0000-000000000000,0000-00-00T00:00:00Z,00000000-0000-0000-0000-000000000000,Guid,00000000-0000-0000-0000-000000000000,Guid,Unknown,String,100,Int32,20,Int32,0,Int32,0,Int32,1601-01-01T00:00:00Z,DateTime,1601-01-01T00:00:00Z,DateTime,test@test.com,String,Unresolved,String,0,Int32,0,Int32, 0000-00-00,String,00000000-0000-0000-0000-000000000000,String \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs b/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs index 0023eb1a7..40917cac2 100644 --- a/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs +++ b/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs @@ -96,7 +96,7 @@ public ThresholdNotificationStatus? Status /// /// The UPN of the person who resolved the notification. /// - public string ResolvedByUPN { get; set; } = string.Empty; + public string ResolvedBy { get; set; } = string.Empty; [DatabaseGenerated(DatabaseGeneratedOption.Computed)] public DateTime LastUpdatedTime { get; set; } diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/DeltaCalculatorService.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/DeltaCalculatorService.cs index e913a5a14..91888fb3a 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/DeltaCalculatorService.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/DeltaCalculatorService.cs @@ -335,7 +335,7 @@ private async Task CloseUnresolvedThresholdNotificationAsync(SyncJob job) if (thresholdNotification != null && thresholdNotification.Status != ThresholdNotificationStatus.Resolved) { thresholdNotification.Resolution = ThresholdNotificationResolution.SelfCorrected; - thresholdNotification.ResolvedByUPN = "N/A"; + thresholdNotification.ResolvedBy = "N/A"; thresholdNotification.ResolvedTime = DateTime.UtcNow; thresholdNotification.Status = ThresholdNotificationStatus.Resolved; diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs index 72c5d2090..4c2f704f0 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs @@ -140,7 +140,7 @@ public void SetupTest() ChangePercentageForRemovals = 1, CreatedTime = DateTime.UtcNow, Resolution = ThresholdNotificationResolution.Unresolved, - ResolvedByUPN = string.Empty, + ResolvedBy = string.Empty, ResolvedTime = DateTime.UtcNow, Status = ThresholdNotificationStatus.Unknown, CardState = ThresholdNotificationCardState.DefaultCard, diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index e7db1d6be..4417af7e2 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -352,7 +352,7 @@ public async Task ParseDestinationAsync(SyncJob syncJob) ChangeQuantityForRemovals = threshold.DeltaToRemoveCount, CreatedTime = DateTime.UtcNow, Resolution = ThresholdNotificationResolution.Unresolved, - ResolvedByUPN = string.Empty, + ResolvedBy = string.Empty, ResolvedTime = DateTime.FromFileTimeUtc(0), Status = ThresholdNotificationStatus.Triggered, CardState = ThresholdNotificationCardState.DefaultCard, diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs index 5d52ec55c..2e3e08f3e 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs @@ -83,7 +83,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage thresholdNotification.Status = ThresholdNotificationStatus.Resolved; thresholdNotification.CardState = ThresholdNotificationCardState.NoCard; thresholdNotification.Resolution = resolution; - thresholdNotification.ResolvedByUPN = resolvedByMail; + thresholdNotification.ResolvedBy = resolvedByMail; thresholdNotification.ResolvedTime = DateTime.UtcNow; await handleSyncJobResolution(thresholdNotification); diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs index 11b4bf764..8fac2eeed 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs @@ -24,6 +24,7 @@ using Microsoft.O365.ActionableMessages.Utilities; using WebApi.Models; using WebApi.Configuration; +using System; namespace Services.Tests { @@ -124,15 +125,15 @@ public void Initialize() var notification = new ThresholdNotification { - ChangePercentageForAdditions = Random.Shared.Next(51, 100), - ChangePercentageForRemovals = Random.Shared.Next(51, 100), + ChangePercentageForAdditions = Random.Shared.NextDouble() * (100 - 51) + 51, + ChangePercentageForRemovals = Random.Shared.NextDouble() * (100 - 51) + 51, ChangeQuantityForAdditions = Random.Shared.Next(50, 1000), ChangeQuantityForRemovals = Random.Shared.Next(50, 1000), CreatedTime = DateTime.UtcNow, Resolution = ThresholdNotificationResolution.Unresolved, Id = Guid.NewGuid(), SyncJobId = Guid.NewGuid(), - ResolvedByUPN = string.Empty, + ResolvedBy = string.Empty, ResolvedTime = DateTime.UtcNow, Status = ThresholdNotificationStatus.AwaitingResponse, TargetOfficeGroupId = group.ObjectId, @@ -153,7 +154,7 @@ public void Initialize() CreatedTime = DateTime.UtcNow, Resolution = ThresholdNotificationResolution.Paused, Id = Guid.NewGuid(), - ResolvedByUPN = _userUPN, + ResolvedBy = _userUPN, ResolvedTime = DateTime.UtcNow, Status = ThresholdNotificationStatus.AwaitingResponse }; @@ -296,7 +297,7 @@ public async Task ResolveNotification_HandleIsAlreadyResolvedTestAsync() var resolvedTime = DateTime.UtcNow.AddDays(Random.Shared.Next(-30, -1)); _thresholdNotification.Status = ThresholdNotificationStatus.Resolved; _thresholdNotification.Resolution = ThresholdNotificationResolution.IgnoreOnce; - _thresholdNotification.ResolvedByUPN = _userUPN; + _thresholdNotification.ResolvedBy = _userUPN; _thresholdNotification.ResolvedTime = resolvedTime; var response = await _notificationsController.ResolveNotificationAsync(_thresholdNotification.Id, _resolveNotificationModel); @@ -426,7 +427,7 @@ public async Task GetNotificationCard_HandleResolvedTestAsync() var resolvedTime = DateTime.UtcNow.AddDays(Random.Shared.Next(-30, -1)); _thresholdNotification.Status = ThresholdNotificationStatus.Resolved; _thresholdNotification.Resolution = ThresholdNotificationResolution.IgnoreOnce; - _thresholdNotification.ResolvedByUPN = _userUPN; + _thresholdNotification.ResolvedBy = _userUPN; _thresholdNotification.ResolvedTime = resolvedTime; var response = await _notificationsController.GetCardAsync(_thresholdNotification.Id); diff --git a/Service/GroupMembershipManagement/Models/AdaptiveCards/ThesholdNotificationResolvedCardData.cs b/Service/GroupMembershipManagement/Models/AdaptiveCards/ThesholdNotificationResolvedCardData.cs index d37f486d5..857577227 100644 --- a/Service/GroupMembershipManagement/Models/AdaptiveCards/ThesholdNotificationResolvedCardData.cs +++ b/Service/GroupMembershipManagement/Models/AdaptiveCards/ThesholdNotificationResolvedCardData.cs @@ -10,12 +10,12 @@ public class ThesholdNotificationResolvedCardData public string GroupName { get; set; } public int ChangeQuantityForAdditions { get; set; } public int ChangeQuantityForRemovals { get; set; } - public int ChangePercentageForAdditions { get; set; } - public int ChangePercentageForRemovals { get; set; } + public double ChangePercentageForAdditions { get; set; } + public double ChangePercentageForRemovals { get; set; } public int ThresholdPercentageForAdditions { get; set; } public int ThresholdPercentageForRemovals { get; set; } public string NotificationId { get; set; } - public string ResolvedByUPN { get; set; } + public string ResolvedBy { get; set; } public string ResolvedTime { get; set; } public string Resolution { get; set; } public string ProviderId { get; set; } diff --git a/Service/GroupMembershipManagement/Models/AdaptiveCards/ThresholdNotificationCardData.cs b/Service/GroupMembershipManagement/Models/AdaptiveCards/ThresholdNotificationCardData.cs index c40626c4d..d59e8b45b 100644 --- a/Service/GroupMembershipManagement/Models/AdaptiveCards/ThresholdNotificationCardData.cs +++ b/Service/GroupMembershipManagement/Models/AdaptiveCards/ThresholdNotificationCardData.cs @@ -10,8 +10,8 @@ public class ThresholdNotificationCardData public string GroupName { get; set; } public int ChangeQuantityForAdditions { get; set; } public int ChangeQuantityForRemovals { get; set; } - public int ChangePercentageForAdditions { get; set; } - public int ChangePercentageForRemovals { get; set; } + public double ChangePercentageForAdditions { get; set; } + public double ChangePercentageForRemovals { get; set; } public int ThresholdPercentageForAdditions { get; set; } public int ThresholdPercentageForRemovals { get; set; } public string NotificationId { get; set; } diff --git a/Service/GroupMembershipManagement/Models/Notifications/ThresholdNotification.cs b/Service/GroupMembershipManagement/Models/Notifications/ThresholdNotification.cs index b1f4180b7..0294e30a0 100644 --- a/Service/GroupMembershipManagement/Models/Notifications/ThresholdNotification.cs +++ b/Service/GroupMembershipManagement/Models/Notifications/ThresholdNotification.cs @@ -75,7 +75,7 @@ public class ThresholdNotification /// /// The UPN of the person who resolved the notification. /// - public string ResolvedByUPN { get; set; } = string.Empty; + public string ResolvedBy { get; set; } = string.Empty; /// /// The action taken to resolve the notification. diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs index 3e78f0ac7..9fffbb202 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs @@ -154,7 +154,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(t => t.TargetOfficeGroupId).IsRequired(); entity.Property(t => t.SyncJobId).IsRequired(); entity.Property(t => t.StatusName).HasMaxLength(50); - entity.Property(t => t.ResolvedByUPN).HasMaxLength(255); + entity.Property(t => t.ResolvedBy).HasMaxLength(255); entity.Property(t => t.ResolutionName).HasMaxLength(50); entity.Property(t => t.CardStateName).HasMaxLength(50); diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240709181547_threshold_notification.Designer.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240709181547_threshold_notification.Designer.cs new file mode 100644 index 000000000..859461057 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240709181547_threshold_notification.Designer.cs @@ -0,0 +1,601 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Repositories.EntityFramework.Contexts; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + [DbContext(typeof(GMMContext))] + [Migration("20240709181547_threshold_notification")] + partial class threshold_notification + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.22") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.Property("DestinationOwnersId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("DestinationOwnersId", "SyncJobsId"); + + b.HasIndex("SyncJobsId"); + + b.ToTable("DestinationOwnerSyncJob"); + }); + + modelBuilder.Entity("Entities.SqlMembershipSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Attributes") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomLabel") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SqlMembershipSources"); + + b.HasData( + new + { + Id = new Guid("dee1f40f-f1f1-4887-9d57-bd5d6cf7669c"), + Name = "SqlMembership" + }); + }); + + modelBuilder.Entity("Entities.SyncJobChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("ChangeDetails") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeSource") + .HasColumnType("int"); + + b.Property("ChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValue(new DateTime(2024, 7, 9, 18, 15, 47, 251, DateTimeKind.Utc).AddTicks(4032)); + + b.Property("ChangedByDisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedByObjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ChangeSource"); + + b.HasIndex("ChangeTime"); + + b.HasIndex("ChangedByObjectId"); + + b.HasIndex("SyncJobId"); + + b.ToTable("SyncJobChanges"); + }); + + modelBuilder.Entity("Entities.ThresholdNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("CardStateName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ChangePercentageForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("float") + .HasDefaultValue(0.0); + + b.Property("ChangePercentageForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("float") + .HasDefaultValue(0.0); + + b.Property("ChangeQuantityForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ChangeQuantityForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("CreatedTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("LastUpdatedTime") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("ResolutionName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ResolvedBy") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ResolvedTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("StatusName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(100); + + b.Property("ThresholdPercentageForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(20); + + b.HasKey("Id"); + + b.HasIndex("SyncJobId"); + + b.ToTable("ThresholdNotifications"); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("DestinationNames"); + }); + + modelBuilder.Entity("Models.DestinationOwner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("ObjectId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.ToTable("DestinationOwners"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("NotificationTypeID") + .HasColumnType("int"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("NotificationTypeID"); + + b.HasIndex("SyncJobId", "NotificationTypeID") + .IsUnique(); + + b.ToTable("JobNotifications"); + }); + + modelBuilder.Entity("Models.NotificationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .IsUnicode(false) + .HasColumnType("varchar(max)"); + + b.HasKey("Id"); + + b.ToTable("NotificationTypes"); + + b.HasData( + new + { + Id = 1, + Disabled = false, + Name = "ThresholdNotification" + }, + new + { + Id = 2, + Disabled = false, + Name = "SyncStartedNotification" + }, + new + { + Id = 3, + Disabled = false, + Name = "SyncCompletedNotification" + }, + new + { + Id = 4, + Disabled = false, + Name = "DestinationNotExistNotification" + }, + new + { + Id = 5, + Disabled = false, + Name = "SourceNotExistNotification" + }, + new + { + Id = 6, + Disabled = false, + Name = "NotOwnerNotification" + }, + new + { + Id = 7, + Disabled = false, + Name = "NotValidSourceNotification" + }, + new + { + Id = 8, + Disabled = false, + Name = "NoDataNotification" + }, + new + { + Id = 9, + Disabled = false, + Name = "NormalThresholdNotification" + }, + new + { + Id = 10, + Disabled = false, + Name = "InactiveSyncJobNotification" + }); + }); + + modelBuilder.Entity("Models.PurgedSyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("PurgedAt") + .HasColumnType("datetime2"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PurgedSyncJobs"); + }); + + modelBuilder.Entity("Models.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("SettingKey") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SettingValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SettingKey") + .IsUnique(); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("Models.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SortPriority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Statuses", (string)null); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(450)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Status") + .IsUnique() + .HasFilter("[Status] IS NOT NULL"); + + b.ToTable("SyncJobs"); + }); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.HasOne("Models.DestinationOwner", null) + .WithMany() + .HasForeignKey("DestinationOwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Entities.ThresholdNotification", b => + { + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.HasOne("Models.SyncJob", "SyncJob") + .WithOne("DestinationName") + .HasForeignKey("Models.DestinationName", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.HasOne("Models.NotificationType", "NotificationType") + .WithMany() + .HasForeignKey("NotificationTypeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", "SyncJob") + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationType"); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.HasOne("Models.Status", "StatusDetails") + .WithOne() + .HasForeignKey("Models.SyncJob", "Status") + .HasPrincipalKey("Models.Status", "Name") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("StatusDetails"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Navigation("DestinationName"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240709181547_threshold_notification.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240709181547_threshold_notification.cs new file mode 100644 index 000000000..fccc5f04b --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240709181547_threshold_notification.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + public partial class threshold_notification : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ThresholdNotifications", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"), + TargetOfficeGroupId = table.Column(type: "uniqueidentifier", nullable: false), + SyncJobId = table.Column(type: "uniqueidentifier", nullable: false), + StatusName = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + ThresholdPercentageForAdditions = table.Column(type: "int", nullable: false, defaultValue: 100), + ThresholdPercentageForRemovals = table.Column(type: "int", nullable: false, defaultValue: 20), + ChangePercentageForAdditions = table.Column(type: "float", nullable: false, defaultValue: 0.0), + ChangePercentageForRemovals = table.Column(type: "float", nullable: false, defaultValue: 0.0), + ChangeQuantityForAdditions = table.Column(type: "int", nullable: false, defaultValue: 0), + ChangeQuantityForRemovals = table.Column(type: "int", nullable: false, defaultValue: 0), + CreatedTime = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + ResolvedTime = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + ResolvedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: true), + LastUpdatedTime = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + ResolutionName = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + CardStateName = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ThresholdNotifications", x => x.Id); + table.ForeignKey( + name: "FK_ThresholdNotifications_SyncJobs_SyncJobId", + column: x => x.SyncJobId, + principalTable: "SyncJobs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ThresholdNotifications_SyncJobId", + table: "ThresholdNotifications", + column: "SyncJobId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ThresholdNotifications"); + + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs index 959d37489..059044d68 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs @@ -64,7 +64,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasData( new { - Id = new Guid("bb0eff76-405d-43c0-8a65-e8ccc322511e"), + Id = new Guid("dee1f40f-f1f1-4887-9d57-bd5d6cf7669c"), Name = "SqlMembership" }); }); @@ -90,7 +90,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ChangeTime") .ValueGeneratedOnAdd() .HasColumnType("datetime2") - .HasDefaultValue(new DateTime(2024, 7, 8, 22, 49, 23, 577, DateTimeKind.Utc).AddTicks(7304)); + .HasDefaultValue(new DateTime(2024, 7, 9, 18, 15, 47, 251, DateTimeKind.Utc).AddTicks(4032)); b.Property("ChangedByDisplayName") .IsRequired() @@ -115,6 +115,87 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SyncJobChanges"); }); + modelBuilder.Entity("Entities.ThresholdNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("CardStateName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ChangePercentageForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("float") + .HasDefaultValue(0.0); + + b.Property("ChangePercentageForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("float") + .HasDefaultValue(0.0); + + b.Property("ChangeQuantityForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ChangeQuantityForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("CreatedTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("LastUpdatedTime") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("ResolutionName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ResolvedBy") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ResolvedTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("StatusName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(100); + + b.Property("ThresholdPercentageForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(20); + + b.HasKey("Id"); + + b.HasIndex("SyncJobId"); + + b.ToTable("ThresholdNotifications"); + }); + modelBuilder.Entity("Models.DestinationName", b => { b.Property("Id") @@ -458,6 +539,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Entities.ThresholdNotification", b => + { + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Models.DestinationName", b => { b.HasOne("Models.SyncJob", "SyncJob") diff --git a/Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs b/Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs index 1066138d4..128ae3b47 100644 --- a/Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs @@ -103,7 +103,7 @@ private ThresholdNotification ToModel(ThresholdNotificationEntity entity) ChangeQuantityForRemovals = entity.ChangeQuantityForRemovals, CreatedTime = entity.CreatedTime, Resolution = entity.Resolution.GetValueOrDefault(), - ResolvedByUPN = entity.ResolvedByUPN, + ResolvedBy = entity.ResolvedBy, ResolvedTime = entity.ResolvedTime, Status = entity.Status.GetValueOrDefault(), CardState = entity.CardState.GetValueOrDefault(), @@ -130,7 +130,7 @@ private ThresholdNotificationEntity ToEntity(ThresholdNotification entity) ChangeQuantityForRemovals = entity.ChangeQuantityForRemovals, CreatedTime = entity.CreatedTime, Resolution = entity.Resolution, - ResolvedByUPN = entity.ResolvedByUPN, + ResolvedBy = entity.ResolvedBy, ResolvedTime = entity.ResolvedTime, Status = entity.Status, CardState = entity.CardState, diff --git a/Service/GroupMembershipManagement/Repositories.Notifications/ThresholdNotificationEntity.cs b/Service/GroupMembershipManagement/Repositories.Notifications/ThresholdNotificationEntity.cs index 1024eb249..9a33ea7b8 100644 --- a/Service/GroupMembershipManagement/Repositories.Notifications/ThresholdNotificationEntity.cs +++ b/Service/GroupMembershipManagement/Repositories.Notifications/ThresholdNotificationEntity.cs @@ -91,12 +91,12 @@ public ThresholdNotificationStatus? Status /// /// The percentage of users to be added as a percentage of the current group size. /// - public int ChangePercentageForAdditions { get; set; } = 0; + public double ChangePercentageForAdditions { get; set; } = 0; /// /// The percentage of users to be removed as a percentage of the current group size. /// - public int ChangePercentageForRemovals { get; set; } = 0; + public double ChangePercentageForRemovals { get; set; } = 0; /// /// The number of users to be added to the current group; @@ -121,7 +121,7 @@ public ThresholdNotificationStatus? Status /// /// The UPN of the person who resolved the notification. /// - public string ResolvedByUPN { get; set; } = string.Empty; + public string ResolvedBy { get; set; } = string.Empty; /// /// The action taken to resolve the notification. diff --git a/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs b/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs index a5af0a27c..756f8bc81 100644 --- a/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs +++ b/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs @@ -111,7 +111,7 @@ public async Task CreateResolvedNotificationCardAsync(ThresholdNotificat ChangePercentageForRemovals = notification.ChangePercentageForRemovals, ThresholdPercentageForAdditions = notification.ThresholdPercentageForAdditions, ThresholdPercentageForRemovals = notification.ThresholdPercentageForRemovals, - ResolvedByUPN = notification.ResolvedByUPN, + ResolvedBy = notification.ResolvedBy, ResolvedTime = notification.ResolvedTime.ToString("U"), Resolution = resolution, NotificationId = $"{notification.Id}", From b8e100ea450ad0817a65ca3c49e83eca999f7d61 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 9 Jul 2024 13:02:53 -0700 Subject: [PATCH 0052/1479] Fix test --- .../Services.Tests/AzureMaintenanceServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs index bb8c2edcd..02f55cd3c 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs @@ -237,7 +237,7 @@ public async Task TestExpireNotifications() Id = Guid.NewGuid(), SyncJobPartitionKey = Guid.NewGuid().ToString(), SyncJobRowKey = Guid.NewGuid().ToString(), - ResolvedByUPN = string.Empty, + ResolvedBy = string.Empty, ResolvedTime = DateTime.UtcNow, Status = ThresholdNotificationStatus.AwaitingResponse, TargetOfficeGroupId = Guid.NewGuid(), From 49d9bd4d2f5ca9fac80eada487e6a5719077c0c1 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 10 Jul 2024 10:52:57 -0700 Subject: [PATCH 0053/1479] Change the email template for UPN --- .../Resources/LocalizationRepository.en-US.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 2a019995b..dfde50971 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -631,7 +631,7 @@ }, { "type": "TextBlock", - "text": "This notification was resolved by **${$root.resolvedByUPN}** on **${$root.resolvedTime}** UTC.", + "text": "This notification was resolved by **${$root.resolvedBy}** on **${$root.resolvedTime}** UTC.", "wrap": true, "spacing": "Small", "color": "Good" From b3de7bf972df7ac85ae74ff2336d0ab62c638aa2 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 27 Jun 2024 12:21:09 -0700 Subject: [PATCH 0054/1479] Change Notification repo --- .../Hosts.FunctionBase.csproj | 1 - .../Hosts/Notifier/Function/Notifier.sln | 12 --- .../NotificationRepository.cs | 94 ++++++++++--------- .../MockNotificationsRepository.cs | 0 4 files changed, 48 insertions(+), 59 deletions(-) rename Service/GroupMembershipManagement/{Repositories.Notifications => Repositories.EntityFramework}/NotificationRepository.cs (50%) rename Service/GroupMembershipManagement/{Repositories.Notifications.Tests => Repositories.Mocks}/MockNotificationsRepository.cs (100%) diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj b/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj index 00b72458f..913d3b06d 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj @@ -28,7 +28,6 @@ - diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.sln b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.sln index 7cff73a30..83b7dc72c 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.sln +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.sln @@ -31,16 +31,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Mail", "..\..\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\Models.csproj", "{6A1A20D0-25FB-41E5-AA43-19D3DDA9A9F1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{4EEB09A8-A1CF-4A50-ACD7-FC4EB406E59B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.GraphAzureADGroups", "..\..\..\Repositories.GraphGroups\Repositories.GraphAzureADGroups.csproj", "{0C9234E0-1E3A-46C9-A4C2-3766F0992AAE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Contracts", "..\..\..\Services.Contracts\Services.Contracts.csproj", "{B7AAC356-FF19-4BF7-8EE0-4FEFED95ADB3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Notifications", "..\..\..\Services.Notifications\Services.Notifications.csproj", "{D631037A-CD3E-44A7-B0FA-6E8949E0E84C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Notifications.Tests", "..\..\..\Repositories.Notifications.Tests\Repositories.Notifications.Tests.csproj", "{4E5DD232-118A-4010-8A07-5100929C07FE}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Mocks", "..\..\..\Repositories.Mocks\Repositories.Mocks.csproj", "{731CA9A5-9E74-4478-A6AC-D0A4353BC425}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework", "..\..\..\Repositories.EntityFramework\Repositories.EntityFramework.csproj", "{236881E9-0396-4F3E-9F6E-2CD69EE57816}" @@ -109,10 +105,6 @@ Global {6A1A20D0-25FB-41E5-AA43-19D3DDA9A9F1}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A1A20D0-25FB-41E5-AA43-19D3DDA9A9F1}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A1A20D0-25FB-41E5-AA43-19D3DDA9A9F1}.Release|Any CPU.Build.0 = Release|Any CPU - {4EEB09A8-A1CF-4A50-ACD7-FC4EB406E59B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4EEB09A8-A1CF-4A50-ACD7-FC4EB406E59B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4EEB09A8-A1CF-4A50-ACD7-FC4EB406E59B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4EEB09A8-A1CF-4A50-ACD7-FC4EB406E59B}.Release|Any CPU.Build.0 = Release|Any CPU {0C9234E0-1E3A-46C9-A4C2-3766F0992AAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0C9234E0-1E3A-46C9-A4C2-3766F0992AAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C9234E0-1E3A-46C9-A4C2-3766F0992AAE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -125,10 +117,6 @@ Global {D631037A-CD3E-44A7-B0FA-6E8949E0E84C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D631037A-CD3E-44A7-B0FA-6E8949E0E84C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D631037A-CD3E-44A7-B0FA-6E8949E0E84C}.Release|Any CPU.Build.0 = Release|Any CPU - {4E5DD232-118A-4010-8A07-5100929C07FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4E5DD232-118A-4010-8A07-5100929C07FE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4E5DD232-118A-4010-8A07-5100929C07FE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4E5DD232-118A-4010-8A07-5100929C07FE}.Release|Any CPU.Build.0 = Release|Any CPU {731CA9A5-9E74-4478-A6AC-D0A4353BC425}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {731CA9A5-9E74-4478-A6AC-D0A4353BC425}.Debug|Any CPU.Build.0 = Debug|Any CPU {731CA9A5-9E74-4478-A6AC-D0A4353BC425}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationRepository.cs similarity index 50% rename from Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs rename to Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationRepository.cs index 128ae3b47..2f3b49fba 100644 --- a/Service/GroupMembershipManagement/Repositories.Notifications/NotificationRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationRepository.cs @@ -2,37 +2,32 @@ // Licensed under the MIT license. using Azure; -using Azure.Data.Tables; -using DIConcreteTypes; -using Microsoft.Extensions.Options; -using Models; using Models.ThresholdNotifications; using Repositories.Contracts; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using ModelNotification = Models.ThresholdNotifications.ThresholdNotification; +using EntityNotification = Entities.ThresholdNotification; +using Repositories.EntityFramework.Contexts; namespace Repositories.NotificationsRepository { public class NotificationRepository : INotificationRepository { - private readonly string _thresholdNotificationPartitionKey = "ThresholdNotification"; - private readonly TableClient _tableClient = null; - private readonly ILoggingRepository _log; + private readonly GMMContext _writeContext; + private readonly GMMReadContext _readContext; - public NotificationRepository(IOptions> notificationRepoCredentials, ILoggingRepository logger) + public NotificationRepository(GMMContext writeContext, GMMReadContext readContext) { - _log = logger ?? throw new ArgumentNullException(nameof(logger)); - _tableClient = new TableClient(notificationRepoCredentials.Value.ConnectionString, notificationRepoCredentials.Value.TableName); + _writeContext = writeContext ?? throw new ArgumentNullException(nameof(writeContext)); + _readContext = readContext ?? throw new ArgumentNullException(nameof(readContext)); } - public async Task GetThresholdNotificationByIdAsync(Guid notificationId) + public async Task GetThresholdNotificationByIdAsync(Guid notificationId) { try { - var result = await _tableClient.GetEntityAsync(_thresholdNotificationPartitionKey, notificationId.ToString()); - return ToModel(result.Value); + var result = await _readContext.ThresholdNotifications.FirstOrDefaultAsync(n => n.Id == notificationId); + return result != null ? ToModel(result) : null; } catch (RequestFailedException ex) { @@ -44,56 +39,67 @@ public async Task GetThresholdNotificationByIdAsync(Guid return null; } - public async Task GetThresholdNotificationBySyncJobIdAsync(Guid syncJobId) + public async Task GetThresholdNotificationBySyncJobIdAsync(Guid syncJobId) { var resolutionNameString = ThresholdNotificationResolution.Unresolved.ToString(); - var queryResult = _tableClient.QueryAsync(x => - x.SyncJobId == syncJobId && x.ResolutionName == resolutionNameString); - await foreach (var segmentResult in queryResult.AsPages()) + var queryResult = await _readContext.ThresholdNotifications + .Where(n => n.SyncJobId == syncJobId && + n.ResolutionName == resolutionNameString) + .FirstOrDefaultAsync(); + + if (queryResult != null) { - var results = segmentResult.Values; - if (results.Count() > 0) - { - return ToModel(results.ElementAt(0)); - } + return ToModel(queryResult); } return null; } - public async Task SaveNotificationAsync(ThresholdNotification notification) + public async Task SaveNotificationAsync(ModelNotification notification) { - var entity = ToEntity(notification); - await _tableClient.UpsertEntityAsync(entity); + var entityNotification = ToEntity(notification); + + var existingNotification = await _readContext.ThresholdNotifications + .FirstOrDefaultAsync(n => n.Id == entityNotification.Id); + + if (existingNotification == null) + { + _writeContext.ThresholdNotifications.Add(entityNotification); + } + else + { + _writeContext.Entry(existingNotification).CurrentValues.SetValues(entityNotification); + } + await _writeContext.SaveChangesAsync(); } - public async IAsyncEnumerable GetQueuedNotificationsAsync() + public async IAsyncEnumerable GetQueuedNotificationsAsync() { - var notifications = new List(); + var notifications = new List(); - var queryResult = _tableClient.QueryAsync(x => x.StatusName == ThresholdNotificationStatus.Queued.ToString()); - - await foreach (var segmentResult in queryResult.AsPages()) { - var results = segmentResult.Values.Where(x => x.ResolutionName == ThresholdNotificationResolution.Unresolved.ToString()); - foreach (var notification in results) + var query = _readContext.ThresholdNotifications + .Where(n => n.StatusName == ThresholdNotificationStatus.Queued.ToString() && + n.ResolutionName == ThresholdNotificationResolution.Unresolved.ToString()); + + await foreach (var notification in query.AsAsyncEnumerable()) { yield return ToModel(notification); } } } - public async Task UpdateNotificationStatusAsync(ThresholdNotification notification, ThresholdNotificationStatus status) + public async Task UpdateNotificationStatusAsync(ModelNotification notification, ThresholdNotificationStatus status) { var updatedNotification = ToEntity(notification); updatedNotification.Status = status; await SaveNotificationAsync(ToModel(updatedNotification)); } - private ThresholdNotification ToModel(ThresholdNotificationEntity entity) + private ModelNotification ToModel(EntityNotification entity) { - return new ThresholdNotification + return new ModelNotification { Id = entity.Id, SyncJobId = entity.SyncJobId, @@ -110,19 +116,15 @@ private ThresholdNotification ToModel(ThresholdNotificationEntity entity) TargetOfficeGroupId = entity.TargetOfficeGroupId, ThresholdPercentageForAdditions = entity.ThresholdPercentageForAdditions, ThresholdPercentageForRemovals = entity.ThresholdPercentageForRemovals, - LastUpdatedTime = entity.Timestamp?.UtcDateTime ?? DateTime.MinValue + LastUpdatedTime = entity.LastUpdatedTime }; } - private ThresholdNotificationEntity ToEntity(ThresholdNotification entity) + private EntityNotification ToEntity(ModelNotification entity) { - return new ThresholdNotificationEntity + return new EntityNotification { - PartitionKey = _thresholdNotificationPartitionKey, - RowKey = entity.Id.ToString(), Id = entity.Id, - SyncJobPartitionKey = entity.SyncJobId.ToString(), - SyncJobRowKey = entity.SyncJobId.ToString(), SyncJobId = entity.SyncJobId, ChangePercentageForAdditions = entity.ChangePercentageForAdditions, ChangePercentageForRemovals = entity.ChangePercentageForRemovals, diff --git a/Service/GroupMembershipManagement/Repositories.Notifications.Tests/MockNotificationsRepository.cs b/Service/GroupMembershipManagement/Repositories.Mocks/MockNotificationsRepository.cs similarity index 100% rename from Service/GroupMembershipManagement/Repositories.Notifications.Tests/MockNotificationsRepository.cs rename to Service/GroupMembershipManagement/Repositories.Mocks/MockNotificationsRepository.cs From e54e925ab02c1ac747b43c558e52a370141fcd25 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 27 Jun 2024 12:58:49 -0700 Subject: [PATCH 0055/1479] Change AM test --- .../Services.Tests/AzureMaintenanceServiceTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs index 02f55cd3c..e6f0db121 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/AzureMaintenanceServiceTests.cs @@ -235,8 +235,6 @@ public async Task TestExpireNotifications() CreatedTime = DateTime.UtcNow, Resolution = ThresholdNotificationResolution.Unresolved, Id = Guid.NewGuid(), - SyncJobPartitionKey = Guid.NewGuid().ToString(), - SyncJobRowKey = Guid.NewGuid().ToString(), ResolvedBy = string.Empty, ResolvedTime = DateTime.UtcNow, Status = ThresholdNotificationStatus.AwaitingResponse, From 15c230b878e5c336a5783460493c2d4730348f9d Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 27 Jun 2024 15:10:38 -0700 Subject: [PATCH 0056/1479] Data clearning --- .../data/parameters/parameters.int.json | 3 --- .../data/parameters/parameters.prodv2.json | 3 --- .../data/parameters/parameters.ua.json | 3 --- Infrastructure/data/template.bicep | 8 -------- .../GroupMembershipManagement.sln | 2 -- .../Hosts.FunctionBase/CommonStartup.cs | 6 ------ .../Function/AzureMaintenance.sln | 6 ------ .../AzureMaintenance/Function/Startup.cs | 5 ----- .../Function/local.settings.json | 1 - .../Infrastructure/compute/template.bicep | 2 -- .../Hosts/JobTrigger/Function/JobTrigger.sln | 2 -- .../Function/MembershipAggregator.sln | 6 ------ .../Function/local.settings.json | 1 - .../Infrastructure/compute/template.bicep | 2 -- .../Notifier/Function/local.settings.json | 1 - .../Infrastructure/compute/template.bicep | 2 -- .../Function/SqlMembershipObtainer.sln | 1 - .../TeamsChannelMembershipObtainer.sln | 6 ------ .../Function/TeamsChannelUpdater.sln | 6 ------ .../Services.WebApi/Services.WebApi.csproj | 1 - .../Hosts/WebApi/WebApi/Program.cs | 5 ----- .../Hosts/WebApi/WebApi/WebApi.csproj | 1 - .../Hosts/WebApi/WebApi/WebApi.sln | 2 -- .../Hosts/WebApi/WebApi/appsettings.json | 1 - ...Repositories.NotificationRepository.csproj | 20 ------------------- 25 files changed, 96 deletions(-) delete mode 100644 Service/GroupMembershipManagement/Repositories.Notifications/Repositories.NotificationRepository.csproj diff --git a/Infrastructure/data/parameters/parameters.int.json b/Infrastructure/data/parameters/parameters.int.json index 367fd3ecc..e890ae990 100644 --- a/Infrastructure/data/parameters/parameters.int.json +++ b/Infrastructure/data/parameters/parameters.int.json @@ -2,9 +2,6 @@ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { - "notificationsTableName": { - "value": "notifications" - }, "serviceBusTopicSubscriptions": { "value": [ { diff --git a/Infrastructure/data/parameters/parameters.prodv2.json b/Infrastructure/data/parameters/parameters.prodv2.json index 1197d19d8..e2b4c63de 100644 --- a/Infrastructure/data/parameters/parameters.prodv2.json +++ b/Infrastructure/data/parameters/parameters.prodv2.json @@ -5,9 +5,6 @@ "isProduction": { "value": true }, - "notificationsTableName": { - "value": "notifications" - }, "serviceBusTopicSubscriptions": { "value": [ { diff --git a/Infrastructure/data/parameters/parameters.ua.json b/Infrastructure/data/parameters/parameters.ua.json index 367fd3ecc..e890ae990 100644 --- a/Infrastructure/data/parameters/parameters.ua.json +++ b/Infrastructure/data/parameters/parameters.ua.json @@ -2,9 +2,6 @@ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { - "notificationsTableName": { - "value": "notifications" - }, "serviceBusTopicSubscriptions": { "value": [ { diff --git a/Infrastructure/data/template.bicep b/Infrastructure/data/template.bicep index a742adb2a..5d1ab10bf 100644 --- a/Infrastructure/data/template.bicep +++ b/Infrastructure/data/template.bicep @@ -126,10 +126,6 @@ param jobsStorageAccountName string = 'jobs${environmentAbbreviation}${uniqueStr @minLength(1) param membershipContainerName string = 'membership' -@description('Enter notifications table name.') -@minLength(1) -param notificationsTableName string = 'notifications' - param logAnalyticsName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}' @allowed([ @@ -654,10 +650,6 @@ module secretsTemplate 'keyVaultSecrets.bicep' = { name: 'membershipContainerName' value: membershipContainerName } - { - name: 'notificationsTableName' - value: notificationsTableName - } { name: 'appInsightsAppId' value: appInsightsTemplate.outputs.appId diff --git a/Service/GroupMembershipManagement/GroupMembershipManagement.sln b/Service/GroupMembershipManagement/GroupMembershipManagement.sln index 177ccb91a..93dea4eaa 100644 --- a/Service/GroupMembershipManagement/GroupMembershipManagement.sln +++ b/Service/GroupMembershipManagement/GroupMembershipManagement.sln @@ -51,8 +51,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "Models\Models.csp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models.Tests", "Models.Tests\Models.Tests.csproj", "{A96E6F5F-0022-47A0-A4DE-B915DF4746D6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "Repositories.Notifications\Repositories.NotificationRepository.csproj", "{684F33BF-821D-41F8-A9D2-EBA0AD1A64E7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Notifications.Tests", "Repositories.Notifications.Tests\Repositories.Notifications.Tests.csproj", "{31948D74-32A0-45D4-8732-C63D2E1A1622}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework.Contexts", "Repositories.EntityFramework.Contexts\Repositories.EntityFramework.Contexts.csproj", "{9A389F97-A10B-433F-91CF-DC89A7B6F5C6}" diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs index a26503330..8e7fff6d2 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs @@ -210,12 +210,6 @@ public override void Configure(IFunctionsHostBuilder builder) ); }); - builder.Services.AddOptions>().Configure((settings, configuration) => - { - settings.ConnectionString = configuration.GetValue("jobsStorageAccountConnectionString"); - settings.TableName = configuration.GetValue("notificationsTableName"); - }); - builder.Services.AddSingleton(); builder.Services.AddOptions().Configure((settings, configuration) => diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.sln b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.sln index b5ed02213..426722c03 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.sln +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.sln @@ -33,8 +33,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\M EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework", "..\..\..\Repositories.EntityFramework\Repositories.EntityFramework.csproj", "{DB73FE21-9E74-4C60-BCE0-24482A67BCB8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{2CD0093E-9C17-4B34-B0F2-C6E882A07DCC}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.DependencyInjection", "..\..\..\Common.DependencyInjection\Common.DependencyInjection.csproj", "{B6AAED09-41E0-4718-8396-48C243EB54F9}" EndProject Global @@ -99,10 +97,6 @@ Global {DB73FE21-9E74-4C60-BCE0-24482A67BCB8}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB73FE21-9E74-4C60-BCE0-24482A67BCB8}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB73FE21-9E74-4C60-BCE0-24482A67BCB8}.Release|Any CPU.Build.0 = Release|Any CPU - {2CD0093E-9C17-4B34-B0F2-C6E882A07DCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2CD0093E-9C17-4B34-B0F2-C6E882A07DCC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2CD0093E-9C17-4B34-B0F2-C6E882A07DCC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2CD0093E-9C17-4B34-B0F2-C6E882A07DCC}.Release|Any CPU.Build.0 = Release|Any CPU {B6AAED09-41E0-4718-8396-48C243EB54F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B6AAED09-41E0-4718-8396-48C243EB54F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {B6AAED09-41E0-4718-8396-48C243EB54F9}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs index 933220b9d..959598f79 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs @@ -56,11 +56,6 @@ public override void Configure(IFunctionsHostBuilder builder) services.GetService>().Value.IsThresholdNotificationEnabled); }); - builder.Services.AddOptions>().Configure((settings, configuration) => - { - settings.ConnectionString = configuration.GetValue("jobsStorageAccountConnectionString"); - settings.TableName = configuration.GetValue("notificationsTableName"); - }); builder.Services.AddSingleton(); diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/local.settings.json index 882440d38..2372bcdb2 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/local.settings.json @@ -11,7 +11,6 @@ "jobsStorageAccountConnectionString": "", "ConnectionStrings:JobsContext": "", "ConnectionStrings:JobsContextReadOnly": "", - "notificationsTableName": "", "graphCredentials:ClientId": "", "graphCredentials:ClientSecret": "", "graphCredentials:KeyVaultName": "", diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep index 57cf7c39a..8087e0f70 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep @@ -86,7 +86,6 @@ var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKey var jobsStorageAccountConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountConnectionString') var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') -var notificationsTableName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notificationsTableName') var azureMaintenanceStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'azureMaintenanceStorageAccountProd') var azureMaintenanceStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'azureMaintenanceStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') @@ -120,7 +119,6 @@ var appSettings = { maintenanceJobs: '@Microsoft.KeyVault(SecretUri=${reference(maintenanceJobs, '2019-09-01').secretUriWithVersion})' appConfigurationEndpoint: appConfigurationEndpoint jobsStorageAccountConnectionString: '@Microsoft.KeyVault(SecretUri=${reference(jobsStorageAccountConnectionString, '2019-09-01').secretUriWithVersion})' - notificationsTableName: '@Microsoft.KeyVault(SecretUri=${reference(notificationsTableName, '2019-09-01').secretUriWithVersion})' 'graphCredentials:ClientSecret': '@Microsoft.KeyVault(SecretUri=${reference(graphAppClientSecret, '2019-09-01').secretUriWithVersion})' 'graphCredentials:ClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphAppClientId, '2019-09-01').secretUriWithVersion})' 'graphCredentials:TenantId': '@Microsoft.KeyVault(SecretUri=${reference(graphAppTenantId, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.sln b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.sln index 1c1497dd6..ea9c1392b 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.sln +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.sln @@ -42,8 +42,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\M EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework", "..\..\..\Repositories.EntityFramework\Repositories.EntityFramework.csproj", "{D371B88F-01CB-4D14-984A-128D5F910125}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{7FBBA476-521E-4F27-9FD1-F29743D4DBA7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework.Contexts", "..\..\..\Repositories.EntityFramework.Contexts\Repositories.EntityFramework.Contexts.csproj", "{4DC3AB68-6A3E-4E02-BDEC-4C84BB2E90AC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.TeamsChannel", "..\..\..\Repositories.TeamsChannel\Repositories.TeamsChannel.csproj", "{8AE7D933-70DA-487D-BF41-4DC0C5B3D668}" diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.sln b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.sln index 546476106..cfa253095 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.sln +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.sln @@ -29,8 +29,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\M EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DIConcreteTypes", "..\..\..\DIConcreteTypes\DIConcreteTypes.csproj", "{B56BC7BA-4F7E-44EC-BE72-7C8DD8F23B4D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{63C840D2-CE0C-4308-BEB6-35B49AF7B276}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.DependencyInjection", "..\..\..\Common.DependencyInjection\Common.DependencyInjection.csproj", "{44D425F3-7883-4014-A3A7-108807522C51}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.ServiceBusTopics", "..\..\..\Repositories.Topics\Repositories.ServiceBusTopics.csproj", "{855E5414-FF0C-4097-9CAC-BFD91F2D4B08}" @@ -105,10 +103,6 @@ Global {B56BC7BA-4F7E-44EC-BE72-7C8DD8F23B4D}.Debug|Any CPU.Build.0 = Debug|Any CPU {B56BC7BA-4F7E-44EC-BE72-7C8DD8F23B4D}.Release|Any CPU.ActiveCfg = Release|Any CPU {B56BC7BA-4F7E-44EC-BE72-7C8DD8F23B4D}.Release|Any CPU.Build.0 = Release|Any CPU - {63C840D2-CE0C-4308-BEB6-35B49AF7B276}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {63C840D2-CE0C-4308-BEB6-35B49AF7B276}.Debug|Any CPU.Build.0 = Debug|Any CPU - {63C840D2-CE0C-4308-BEB6-35B49AF7B276}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63C840D2-CE0C-4308-BEB6-35B49AF7B276}.Release|Any CPU.Build.0 = Release|Any CPU {44D425F3-7883-4014-A3A7-108807522C51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {44D425F3-7883-4014-A3A7-108807522C51}.Debug|Any CPU.Build.0 = Debug|Any CPU {44D425F3-7883-4014-A3A7-108807522C51}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json index 04d5db25e..689fd3e0f 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json @@ -9,7 +9,6 @@ "jobsStorageAccountConnectionString": "", "ConnectionStrings:JobsContext": "", "ConnectionStrings:JobsContextReadOnly": "", - "notificationsTableName": "notifications", "graphUpdaterUrl": "", "graphUpdaterFunctionKey": "", "membershipStorageAccountName": "", diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep index 7c82e1f19..c021eb5f6 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep @@ -95,7 +95,6 @@ param setRBACPermissions bool = false var logAnalyticsCustomerId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsCustomerId') var logAnalyticsPrimarySharedKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsPrimarySharedKey') var jobsStorageAccountConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountConnectionString') -var notificationsTableName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notificationsTableName') var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') var functionAppFullName = '${functionAppName}-MembershipAggregator' @@ -145,7 +144,6 @@ var appSettings = { logAnalyticsCustomerId: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsCustomerId, '2019-09-01').secretUriWithVersion})' logAnalyticsPrimarySharedKey: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsPrimarySharedKey, '2019-09-01').secretUriWithVersion})' jobsStorageAccountConnectionString: '@Microsoft.KeyVault(SecretUri=${reference(jobsStorageAccountConnectionString, '2019-09-01').secretUriWithVersion})' - notificationsTableName: '@Microsoft.KeyVault(SecretUri=${reference(notificationsTableName, '2019-09-01').secretUriWithVersion})' membershipStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(membershipStorageAccountName, '2019-09-01').secretUriWithVersion})' membershipContainerName: '@Microsoft.KeyVault(SecretUri=${reference(membershipContainerName, '2019-09-01').secretUriWithVersion})' appConfigurationEndpoint: appConfigurationEndpoint diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/Notifier/Function/local.settings.json index 0e36559e5..4ed34551f 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/local.settings.json @@ -9,7 +9,6 @@ "APPINSIGHTS_INSTRUMENTATIONKEY": "", "jobsStorageAccountConnectionString": "", "notifierTriggerSchedule": "0 */5 * * * *", - "notificationsTableName": "notifications", "graphCredentials:ClientId": "", "graphCredentials:ClientSecret": "", "graphCredentials:KeyVaultName": "", diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep index 884e08b35..5a3f5dd23 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep @@ -110,7 +110,6 @@ var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultRe var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierProviderId') var jobsStorageAccountConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountConnectionString') -var notificationsTableName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notificationsTableName') var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var notifierStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierStorageAccountProd') @@ -146,7 +145,6 @@ var appSettings = { logAnalyticsCustomerId: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsCustomerId, '2019-09-01').secretUriWithVersion})' logAnalyticsPrimarySharedKey: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsPrimarySharedKey, '2019-09-01').secretUriWithVersion})' jobsStorageAccountConnectionString: '@Microsoft.KeyVault(SecretUri=${reference(jobsStorageAccountConnectionString, '2019-09-01').secretUriWithVersion})' - notificationsTableName: '@Microsoft.KeyVault(SecretUri=${reference(notificationsTableName, '2019-09-01').secretUriWithVersion})' appConfigurationEndpoint: appConfigurationEndpoint 'graphCredentials:ClientSecret': '@Microsoft.KeyVault(SecretUri=${reference(graphAppClientSecret, '2019-09-01').secretUriWithVersion})' 'graphCredentials:ClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphAppClientId, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln index fe3fe7afc..76dedabd1 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln @@ -37,7 +37,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Entities", "..\..\..\Entiti EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework", "..\..\..\Repositories.EntityFramework\Repositories.EntityFramework.csproj", "{279D7C62-303B-4C72-A80E-8A3B4EE34E8D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{524EFA1D-B572-41E1-9D76-A4328C98B789}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Mail", "..\..\..\Repositories.Mail\Repositories.Mail.csproj", "{454EEE07-02C1-48A9-B751-88E94EC162C1}" EndProject Global diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/TeamsChannelMembershipObtainer.sln b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/TeamsChannelMembershipObtainer.sln index 2ebefc23d..fc10faf1a 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/TeamsChannelMembershipObtainer.sln +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/TeamsChannelMembershipObtainer.sln @@ -27,8 +27,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Logging", "..\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.DependencyInjection", "..\..\..\Common.DependencyInjection\Common.DependencyInjection.csproj", "{5C683B9D-E546-4FA0-87F8-242342EC5BA8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{9AD0DD4C-01FB-40A1-9A9E-BBDC2DD74EB7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.FeatureFlag", "..\..\..\Repositories.FeatureFlag\Repositories.FeatureFlag.csproj", "{8F655591-5955-4647-BF87-B39F043F82FC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.GraphAzureADGroups", "..\..\..\Repositories.GraphGroups\Repositories.GraphAzureADGroups.csproj", "{9C2C998D-EE72-466F-9CF5-319FE1E311DD}" @@ -85,10 +83,6 @@ Global {5C683B9D-E546-4FA0-87F8-242342EC5BA8}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C683B9D-E546-4FA0-87F8-242342EC5BA8}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C683B9D-E546-4FA0-87F8-242342EC5BA8}.Release|Any CPU.Build.0 = Release|Any CPU - {9AD0DD4C-01FB-40A1-9A9E-BBDC2DD74EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9AD0DD4C-01FB-40A1-9A9E-BBDC2DD74EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9AD0DD4C-01FB-40A1-9A9E-BBDC2DD74EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9AD0DD4C-01FB-40A1-9A9E-BBDC2DD74EB7}.Release|Any CPU.Build.0 = Release|Any CPU {8F655591-5955-4647-BF87-B39F043F82FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8F655591-5955-4647-BF87-B39F043F82FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F655591-5955-4647-BF87-B39F043F82FC}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln index bcae45cb8..fef84a671 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln @@ -23,8 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Logging", "..\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.DependencyInjection", "..\..\..\Common.DependencyInjection\Common.DependencyInjection.csproj", "{5C683B9D-E546-4FA0-87F8-242342EC5BA8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{9AD0DD4C-01FB-40A1-9A9E-BBDC2DD74EB7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.TeamsChannelUpdater", "..\Services.TeamsChannelUpdater\Services.TeamsChannelUpdater.csproj", "{FC7E66B0-A134-4041-867E-370834F2C0D1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.TeamsChannelUpdater.Contracts", "..\Services.TeamsChannelUpdater.Contracts\Services.TeamsChannelUpdater.Contracts.csproj", "{46E07EED-A68C-4850-9AA5-89BEB144B028}" @@ -89,10 +87,6 @@ Global {5C683B9D-E546-4FA0-87F8-242342EC5BA8}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C683B9D-E546-4FA0-87F8-242342EC5BA8}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C683B9D-E546-4FA0-87F8-242342EC5BA8}.Release|Any CPU.Build.0 = Release|Any CPU - {9AD0DD4C-01FB-40A1-9A9E-BBDC2DD74EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9AD0DD4C-01FB-40A1-9A9E-BBDC2DD74EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9AD0DD4C-01FB-40A1-9A9E-BBDC2DD74EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9AD0DD4C-01FB-40A1-9A9E-BBDC2DD74EB7}.Release|Any CPU.Build.0 = Release|Any CPU {FC7E66B0-A134-4041-867E-370834F2C0D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FC7E66B0-A134-4041-867E-370834F2C0D1}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC7E66B0-A134-4041-867E-370834F2C0D1}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj index 2decb78ce..6094e1773 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj @@ -20,7 +20,6 @@ - diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs index 72d1829e4..80af91ea8 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs @@ -241,11 +241,6 @@ public static void Main(string[] args) builder.Services.AddSingleton>(services => new KeyVaultSecret(services.GetService().GetValue("Settings:SqlServerConnectionString"))); builder.Services.AddSingleton(); - builder.Services.AddOptions>().Configure((settings, configuration) => - { - settings.ConnectionString = configuration.GetValue("Settings:jobsStorageAccountConnectionString"); - settings.TableName = configuration.GetValue("Settings:notificationsTableName"); - }); builder.Services.AddSingleton(); builder.Services.Configure(builder.Configuration.GetSection("Settings:GraphCredentials")) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj index fd0599dba..37e4abbf6 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj @@ -34,7 +34,6 @@ - diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.sln b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.sln index e99926345..78eae6206 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.sln +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.sln @@ -35,8 +35,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Contracts", "..\.. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Notifications", "..\..\..\Services.Notifications\Services.Notifications.csproj", "{83648C3D-303F-4292-9228-84598B84B334}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{B0614A69-D5D3-494C-8DD0-BFA2E9E75469}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Localization", "..\..\..\Repositories.Localization\Repositories.Localization.csproj", "{EF7436BD-26A1-49D8-AFCB-1803EB24C4CB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework.Contexts", "..\..\..\Repositories.EntityFramework.Contexts\Repositories.EntityFramework.Contexts.csproj", "{D4E9D493-2DD5-499D-B011-80B386FDB759}" diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json index 0a44e2ae7..169d38875 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json @@ -10,7 +10,6 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform "logAnalyticsCustomerId": "", "logAnalyticsPrimarySharedKey": "", "jobsStorageAccountConnectionString": "", - "notificationsTableName": "notifications", "ActionableEmailProviderId": "00000000-0000-0000-0000-000000000000", "ApiHostname": "api..gmm.microsoft.com", "SqlServerConnectionString": "", diff --git a/Service/GroupMembershipManagement/Repositories.Notifications/Repositories.NotificationRepository.csproj b/Service/GroupMembershipManagement/Repositories.Notifications/Repositories.NotificationRepository.csproj deleted file mode 100644 index de53ca8a8..000000000 --- a/Service/GroupMembershipManagement/Repositories.Notifications/Repositories.NotificationRepository.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net6.0 - Repositories.NotificationRepository - Repositories.NotificationsRepository - - - - - - - - - - - - - - From 55fc30df10e9f82daf0b8bcb6122df9b0a95b31f Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Fri, 28 Jun 2024 14:39:17 -0700 Subject: [PATCH 0057/1479] Fix the webapi --- .../GroupMembershipManagement/Entities/ThresholdNotification.cs | 2 +- .../Hosts.FunctionBase/CommonStartup.cs | 2 +- .../GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs | 2 +- .../Repositories.EntityFramework/NotificationRepository.cs | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs b/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs index 40917cac2..fb87fa0ea 100644 --- a/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs +++ b/Service/GroupMembershipManagement/Entities/ThresholdNotification.cs @@ -139,7 +139,7 @@ public ThresholdNotificationCardState? CardState } set { - this.ResolutionName = value.HasValue ? value.ToString() : null; + this.CardStateName = value.HasValue ? value.ToString() : null; } } } diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs index 8e7fff6d2..4347c5cb4 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs @@ -210,7 +210,7 @@ public override void Configure(IFunctionsHostBuilder builder) ); }); - builder.Services.AddSingleton(); + builder.Services.AddScoped(); builder.Services.AddOptions().Configure((settings, configuration) => { diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs index 80af91ea8..5aedab2d9 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs @@ -241,7 +241,7 @@ public static void Main(string[] args) builder.Services.AddSingleton>(services => new KeyVaultSecret(services.GetService().GetValue("Settings:SqlServerConnectionString"))); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddScoped(); builder.Services.Configure(builder.Configuration.GetSection("Settings:GraphCredentials")) .AddGraphAPIClient() diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationRepository.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationRepository.cs index 2f3b49fba..b453a2b9e 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework/NotificationRepository.cs @@ -70,6 +70,7 @@ public async Task SaveNotificationAsync(ModelNotification notification) else { _writeContext.Entry(existingNotification).CurrentValues.SetValues(entityNotification); + _writeContext.Entry(existingNotification).State = EntityState.Modified; } await _writeContext.SaveChangesAsync(); } From a6e9990c34a861de9ba9f31841682230c920f669 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 9 Jul 2024 12:15:14 -0700 Subject: [PATCH 0058/1479] Data clearning --- .../GroupMembershipManagement.sln | 2 - .../Repositories.Notifications.Tests.csproj | 13 -- .../ThresholdNotificationEntity.cs | 181 ------------------ 3 files changed, 196 deletions(-) delete mode 100644 Service/GroupMembershipManagement/Repositories.Notifications.Tests/Repositories.Notifications.Tests.csproj delete mode 100644 Service/GroupMembershipManagement/Repositories.Notifications/ThresholdNotificationEntity.cs diff --git a/Service/GroupMembershipManagement/GroupMembershipManagement.sln b/Service/GroupMembershipManagement/GroupMembershipManagement.sln index 93dea4eaa..bfe7abf07 100644 --- a/Service/GroupMembershipManagement/GroupMembershipManagement.sln +++ b/Service/GroupMembershipManagement/GroupMembershipManagement.sln @@ -51,8 +51,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "Models\Models.csp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models.Tests", "Models.Tests\Models.Tests.csproj", "{A96E6F5F-0022-47A0-A4DE-B915DF4746D6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Notifications.Tests", "Repositories.Notifications.Tests\Repositories.Notifications.Tests.csproj", "{31948D74-32A0-45D4-8732-C63D2E1A1622}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework.Contexts", "Repositories.EntityFramework.Contexts\Repositories.EntityFramework.Contexts.csproj", "{9A389F97-A10B-433F-91CF-DC89A7B6F5C6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework", "Repositories.EntityFramework\Repositories.EntityFramework.csproj", "{D14FF6B2-92A0-4A00-9A34-A3E9554444BF}" diff --git a/Service/GroupMembershipManagement/Repositories.Notifications.Tests/Repositories.Notifications.Tests.csproj b/Service/GroupMembershipManagement/Repositories.Notifications.Tests/Repositories.Notifications.Tests.csproj deleted file mode 100644 index a0a601c40..000000000 --- a/Service/GroupMembershipManagement/Repositories.Notifications.Tests/Repositories.Notifications.Tests.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net6.0 - - false - - - - - - - diff --git a/Service/GroupMembershipManagement/Repositories.Notifications/ThresholdNotificationEntity.cs b/Service/GroupMembershipManagement/Repositories.Notifications/ThresholdNotificationEntity.cs deleted file mode 100644 index 9a33ea7b8..000000000 --- a/Service/GroupMembershipManagement/Repositories.Notifications/ThresholdNotificationEntity.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using Azure; -using Azure.Data.Tables; -using Models.CustomAttributes; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using Models; -using System.Diagnostics.CodeAnalysis; -using Models.ThresholdNotifications; -using System.Runtime.Serialization; - -namespace Repositories.NotificationsRepository -{ - [ExcludeFromCodeCoverage] - internal class ThresholdNotificationEntity : ITableEntity - { - public ThresholdNotificationEntity() - { - } - - public ThresholdNotificationEntity(string partitionKey, string rowKey) - { - PartitionKey = partitionKey; - RowKey = rowKey; - } - public Guid SyncJobId { get; set; } - public string PartitionKey { get; set; } - public string RowKey { get; set; } - - /// - /// The threshold notification id. - /// - public Guid Id { get; set; } = Guid.Empty; - - /// - /// The threshold notification sync job's PartitionKey. - /// - public string SyncJobPartitionKey { get; set; } = string.Empty; - - /// - /// The threshold notification sync job's RowKey. - /// - public string SyncJobRowKey { get; set; } = string.Empty; - - /// - /// The id of the group associated with the notification. - /// - public Guid TargetOfficeGroupId { get; set; } - - /// - /// Gets or sets the notification status name to persist in the azure table store. - /// - public string StatusName { get; set; } - - ///// - ///// The notification status. - ///// - [IgnoreDataMember] - public ThresholdNotificationStatus? Status - { - get - { - if (string.IsNullOrEmpty(StatusName)) - { - return null; - } - - return (ThresholdNotificationStatus)Enum.Parse(typeof(ThresholdNotificationStatus), StatusName); - } - - set - { - StatusName = value?.ToString(); - } - } - - - /// - /// The allowed change size of users to be added to the group as a percentage of the current group size. - /// - public int ThresholdPercentageForAdditions { get; set; } = 0; - - /// - /// The allowed change size of users to be removed from the group as a percentage of the current group size. - /// - public int ThresholdPercentageForRemovals { get; set; } = 0; - - /// - /// The percentage of users to be added as a percentage of the current group size. - /// - public double ChangePercentageForAdditions { get; set; } = 0; - - /// - /// The percentage of users to be removed as a percentage of the current group size. - /// - public double ChangePercentageForRemovals { get; set; } = 0; - - /// - /// The number of users to be added to the current group; - /// - public int ChangeQuantityForAdditions { get; set; } = 0; - - /// - /// The number of users to be removed from the current group. - /// - public int ChangeQuantityForRemovals { get; set; } = 0; - - /// - /// The time the notification was created. - /// - public DateTime CreatedTime { get; set; } = DateTime.FromFileTimeUtc(0); - - /// - /// The time the notification was resolved. - /// - public DateTime ResolvedTime { get; set; } = DateTime.FromFileTimeUtc(0); - - /// - /// The UPN of the person who resolved the notification. - /// - public string ResolvedBy { get; set; } = string.Empty; - - /// - /// The action taken to resolve the notification. - /// - public string ResolutionName { get; set; } - - [IgnoreDataMember] - public ThresholdNotificationResolution? Resolution - { - get - { - if (string.IsNullOrEmpty(ResolutionName)) - { - return null; - } - - return (ThresholdNotificationResolution)Enum.Parse(typeof(ThresholdNotificationResolution), ResolutionName); - } - - set - { - ResolutionName = value?.ToString(); - } - } - - /// - /// The state of the notification card and what type of card should be sent out in the next email - /// - public string CardStateName { get; set; } - - [IgnoreDataMember] - public ThresholdNotificationCardState? CardState - { - get - { - if (string.IsNullOrEmpty(CardStateName)) - { - return null; - } - - return (ThresholdNotificationCardState)Enum.Parse(typeof(ThresholdNotificationCardState), CardStateName); - } - - set - { - CardStateName = value?.ToString(); - } - } - - [JsonIgnore] - public DateTimeOffset? Timestamp { get; set; } - - [JsonIgnore] - public ETag ETag { get; set; } - - } -} From d3c160d4c03d1ee61d83af148758f5551c2e1d33 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 25 Jun 2024 12:21:01 -0700 Subject: [PATCH 0059/1479] reorder items From 22772cc5fdcf6ad02664a0268045f2c574900c8c Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Thu, 27 Jun 2024 16:00:45 -0700 Subject: [PATCH 0060/1479] code cleanup From 79a7c5ce133ae3bd9c2848ea86c957dd6126fad1 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Thu, 27 Jun 2024 13:03:28 -0700 Subject: [PATCH 0061/1479] Added features/int branch to trigger branches for public yaml From 98e503464bf632b5145b3b23d23602a9db5d433a Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 19 Jun 2024 14:58:42 -0700 Subject: [PATCH 0062/1479] Add new roles, Job.Delete.OwnedBy, Job.Enable.OwnedBy, Job.EditConfiguration.OwnedBy From 33217bfdb3017edb213a81ed0aeff47151179780 Mon Sep 17 00:00:00 2001 From: abgonz Date: Tue, 25 Jun 2024 15:47:14 -0700 Subject: [PATCH 0063/1479] Add not found page From 4cc6b8d2634f0eeae26df6ee799c6c56a6a5eef9 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Fri, 5 Jul 2024 11:13:05 -0700 Subject: [PATCH 0064/1479] fix 'jobs in error' metric From fbbd3eb68b26b0d28d1becea53d69b40ae9da495 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 8 Jul 2024 13:15:28 -0700 Subject: [PATCH 0065/1479] Updated public build yaml to buildSourceCode by default From ddfe324cd753bc605ab1466f36f59ad3bbc884d3 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 2 Jul 2024 12:29:53 -0700 Subject: [PATCH 0066/1479] enable editing sqlmembership source part From 78b707a3bb05a7a2230d14caf084594c2309a664 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 3 Jul 2024 11:47:21 -0700 Subject: [PATCH 0067/1479] Change threshold warning --- .../RunConfiguration/RunConfiguration.base.tsx | 14 ++++++++++++-- .../RunConfiguration/RunConfiguration.styles.ts | 5 +++++ .../RunConfiguration/RunConfiguration.types.ts | 1 + UI/web-app/src/services/localization/IStrings.ts | 2 ++ .../localization/i18n/locales/en/translations.ts | 2 ++ .../localization/i18n/locales/es/translations.ts | 2 ++ 6 files changed, 24 insertions(+), 2 deletions(-) diff --git a/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx b/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx index 4875bed72..9abe14f05 100644 --- a/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx +++ b/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx @@ -7,7 +7,8 @@ import { classNamesFunction, useTheme, ChoiceGroup, IChoiceGroupOption, DatePicker, Dropdown, Checkbox, - TextField + TextField, + MessageBar, MessageBarType, } from '@fluentui/react'; import { IRunConfigurationProps, @@ -162,7 +163,7 @@ export const RunConfigurationBase: React.FunctionComponent + {useThresholdLimits === 'No' && ( + + {strings.ManageMembership.labels.preventAutomaticSyncWarning} + + )}
{useThresholdLimits === 'Yes' && (
diff --git a/UI/web-app/src/components/RunConfiguration/RunConfiguration.styles.ts b/UI/web-app/src/components/RunConfiguration/RunConfiguration.styles.ts index 61f3194a7..f0b8bc747 100644 --- a/UI/web-app/src/components/RunConfiguration/RunConfiguration.styles.ts +++ b/UI/web-app/src/components/RunConfiguration/RunConfiguration.styles.ts @@ -71,6 +71,11 @@ import { backgroud: theme.palette.white, width: 300 }, + thresholdWarning: { + width: 'fit-content', + display: 'flex', + alignItems: 'center' + }, }; }; \ No newline at end of file diff --git a/UI/web-app/src/components/RunConfiguration/RunConfiguration.types.ts b/UI/web-app/src/components/RunConfiguration/RunConfiguration.types.ts index 83706b8e6..d753a8bac 100644 --- a/UI/web-app/src/components/RunConfiguration/RunConfiguration.types.ts +++ b/UI/web-app/src/components/RunConfiguration/RunConfiguration.types.ts @@ -19,6 +19,7 @@ import { thresholdDropdown: IStyle; dropdownTitle: IStyle; textFieldFieldGroup: IStyle; + thresholdWarning: IStyle; } export interface IRunConfigurationStyleProps { diff --git a/UI/web-app/src/services/localization/IStrings.ts b/UI/web-app/src/services/localization/IStrings.ts index 72374cf94..8009499af 100644 --- a/UI/web-app/src/services/localization/IStrings.ts +++ b/UI/web-app/src/services/localization/IStrings.ts @@ -304,6 +304,8 @@ export type IStrings = { hrs: string; frequency: string; preventAutomaticSync: string; + preventAutomaticSyncInfo: string; + preventAutomaticSyncWarning: string; increase: string; decrease: string; step4title: string; diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index 50df83ac2..cc4a94426 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -309,6 +309,8 @@ export const strings: IStrings = { frequency: 'Frequency', hrs: 'hrs', preventAutomaticSync: 'Prevent automatic synchronization if membership change exceeds increase and/or decrease threshold?', + preventAutomaticSyncInfo: 'Enable this setting by selecting \'Yes\' to manually approve synchronization when membership changes exceed specified thresholds. For example, with a 20% increase threshold and a 15% decrease threshold on 100 members, synchronization will pause above 120 members or below 85 members. Select \'No\' to allow continuous automatic synchronization.', + preventAutomaticSyncWarning: 'Warning: Selecting \'No\' will allow automatic synchronization without manual review, regardless of how significantly the membership changes', increase: 'Increase', decrease: 'Decrease', step4title: 'Step 4: Confirmation', diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index 8f90380af..33c16358f 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -313,6 +313,8 @@ export const strings: IStrings = { frequency: 'frecuencia', hrs: 'hrs', preventAutomaticSync: '¿Prevenir sincronización automática si los cambios en la membresía exceden los límites de incremento y/o decremento?', + preventAutomaticSyncInfo: 'Active esta configuración seleccionando \'Sí\' para aprobar manualmente la sincronización cuando los cambios en la membresía superen los umbrales especificados. Por ejemplo, con un umbral de aumento del 20% y un umbral de disminución del 15% en 100 miembros, la sincronización se pausará si los miembros superan los 120 o si disminuyen a menos de 85. Seleccione \'No\' para permitir la sincronización automática continua.', + preventAutomaticSyncWarning: 'Advertencia: Seleccionar \'No\' permitirá la sincronización automática sin revisión manual, sin importar cuán significativos sean los cambios en la membresía', increase: 'Incremento', decrease: 'Decremento', step4title: 'Paso 4: Confirmación', From 218cd47b0024b0b188d1c256eb66b889e8f2ee44 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 9 Jul 2024 13:17:51 -0700 Subject: [PATCH 0068/1479] Change the wording of the messages --- .../src/services/localization/i18n/locales/en/translations.ts | 4 ++-- .../src/services/localization/i18n/locales/es/translations.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index cc4a94426..832a6b219 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -309,8 +309,8 @@ export const strings: IStrings = { frequency: 'Frequency', hrs: 'hrs', preventAutomaticSync: 'Prevent automatic synchronization if membership change exceeds increase and/or decrease threshold?', - preventAutomaticSyncInfo: 'Enable this setting by selecting \'Yes\' to manually approve synchronization when membership changes exceed specified thresholds. For example, with a 20% increase threshold and a 15% decrease threshold on 100 members, synchronization will pause above 120 members or below 85 members. Select \'No\' to allow continuous automatic synchronization.', - preventAutomaticSyncWarning: 'Warning: Selecting \'No\' will allow automatic synchronization without manual review, regardless of how significantly the membership changes', + preventAutomaticSyncInfo: 'Selecting \'Yes\' means extra protection and visibility. If a drastic membership change is detected, group owners will receive a detailed alert notification to approve or pause the synchronization attempts. For example, with a 20% increase threshold and a 15% decrease threshold on 100 members, synchronization will pause above 120 members or below 85 members.', + preventAutomaticSyncWarning: 'Warning: Selecting \'No\' means membership synchronizations will occur without notification, regardless of how drastic of an increase or decrease in membership GMM projects. This can be painful in situations where a misconfigured change in membership definition (ex: by a group owner) or an errant change in the source information (ex: by someone in HR) occurs.', increase: 'Increase', decrease: 'Decrease', step4title: 'Step 4: Confirmation', diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index 33c16358f..cfeeff48c 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -313,8 +313,8 @@ export const strings: IStrings = { frequency: 'frecuencia', hrs: 'hrs', preventAutomaticSync: '¿Prevenir sincronización automática si los cambios en la membresía exceden los límites de incremento y/o decremento?', - preventAutomaticSyncInfo: 'Active esta configuración seleccionando \'Sí\' para aprobar manualmente la sincronización cuando los cambios en la membresía superen los umbrales especificados. Por ejemplo, con un umbral de aumento del 20% y un umbral de disminución del 15% en 100 miembros, la sincronización se pausará si los miembros superan los 120 o si disminuyen a menos de 85. Seleccione \'No\' para permitir la sincronización automática continua.', - preventAutomaticSyncWarning: 'Advertencia: Seleccionar \'No\' permitirá la sincronización automática sin revisión manual, sin importar cuán significativos sean los cambios en la membresía', + preventAutomaticSyncInfo: 'Seleccionar \'Sí\' significa protección y visibilidad adicionales. Si se detecta un cambio drástico en la membresía, los propietarios del grupo recibirán una notificación de alerta detallada para aprobar o pausar los intentos de sincronización. Por ejemplo, con un umbral de aumento del 20% y un umbral de disminución del 15% en 100 miembros, la sincronización se detendrá por encima de 120 miembros o por debajo de 85 miembros.', + preventAutomaticSyncWarning: 'Advertencia: Seleccionar \'No\' significa que las sincronizaciones de membresía se producirán sin notificación, independientemente de cuán drástico sea el aumento o la disminución de la membresía en los proyectos de GMM. Esto puede ser perjudificial en situaciones en las que se produce un cambio mal configurado en la definición de pertenencia (por ejemplo, por parte del propietario de un grupo) o un cambio erróneo en la información de origen (por ejemplo, por alguien de RR.HH.).', increase: 'Incremento', decrease: 'Decremento', step4title: 'Paso 4: Confirmación', From 5c45ebac99a0a9b8c228aece53aa6ef225ce9d5c Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 10 Jul 2024 10:23:52 -0700 Subject: [PATCH 0069/1479] Add multiline warning --- .../src/components/RunConfiguration/RunConfiguration.base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx b/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx index 9abe14f05..15e2629f4 100644 --- a/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx +++ b/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx @@ -193,7 +193,7 @@ export const RunConfigurationBase: React.FunctionComponent {strings.ManageMembership.labels.preventAutomaticSyncWarning} From 9988990ccaf722c8976adbf451eba48ee73e2c88 Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 10 Jul 2024 10:16:06 -0700 Subject: [PATCH 0070/1479] Add ThresholdPercentages to DistributionSyncJob and CompareTo --- .../Services/JobSchedulingService.cs | 2 +- .../Models/DistributionSyncJob.cs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/JobSchedulingService.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/JobSchedulingService.cs index cbf2fd2b3..5d4c92a85 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/JobSchedulingService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/JobSchedulingService.cs @@ -142,7 +142,7 @@ private async Task> DistributeJobStartTimesForPeriod( HashSet groupDestinationsForPeriod = new HashSet(jobsToDistribute.ConvertAll(job => job.Destination)); runtimeMap = new Dictionary(runtimeMap.Where(entry => groupDestinationsForPeriod.Contains(entry.Key) || entry.Key == "Default")); - // Sort sync jobs by Status, LastRunTime + // Sort sync jobs by Status, LastRunTime and ThresholdPercentages jobsToDistribute.Sort(); double totalTimeInSeconds = runtimeMap.Values.Sum() + (jobsToDistribute.Count - runtimeMap.Count) * runtimeMap["Default"]; diff --git a/Service/GroupMembershipManagement/Models/DistributionSyncJob.cs b/Service/GroupMembershipManagement/Models/DistributionSyncJob.cs index b114c698f..df93a5031 100644 --- a/Service/GroupMembershipManagement/Models/DistributionSyncJob.cs +++ b/Service/GroupMembershipManagement/Models/DistributionSyncJob.cs @@ -13,6 +13,8 @@ public class DistributionSyncJob : UpdateMergeSyncJob, IComparable Date: Thu, 11 Jul 2024 16:39:11 -0700 Subject: [PATCH 0071/1479] schema validation check in SqlMembershipObtainer function --- .../Hosts.FunctionBase/CommonStartup.cs | 19 +++++ .../SchemaValidatorFunction.cs | 68 +++++++++++++++ .../SchemaValidator/SchemaValidatorRequest.cs | 12 +++ .../Orchestrator/OrchestratorFunction.cs | 25 ++++++ .../Function/SqlMembershipObtainer.csproj | 1 + .../Function/SqlMembershipObtainer.sln | 8 ++ .../Models/Models.csproj | 18 ++++ .../Models/Schema.cs | 14 ++++ .../Models/SchemaProvider.cs | 12 +++ .../Models/Schemas/SqlMembershipSchema.json | 83 +++++++++---------- 10 files changed, 217 insertions(+), 43 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs create mode 100644 Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs create mode 100644 Service/GroupMembershipManagement/Models/Schema.cs create mode 100644 Service/GroupMembershipManagement/Models/SchemaProvider.cs diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs index 4347c5cb4..e841f2d6e 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs @@ -30,6 +30,9 @@ using Repositories.EntityFramework; using Repositories.FeatureFlag; using Azure.Core; +using System.IO; +using Models; +using System.Data; namespace Hosts.FunctionBase { @@ -38,6 +41,8 @@ public abstract class CommonStartup : FunctionsStartup protected abstract string FunctionName { get; } protected abstract string DryRunSettingName { get; } + private const string SCHEMA_DIRECTORY = "Schemas"; + public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder) { builder.ConfigurationBuilder.AddAzureAppConfiguration(options => @@ -241,6 +246,20 @@ public override void Configure(IFunctionsHostBuilder builder) return new ServiceBusClient(serviceBusFQN, new DefaultAzureCredential()); }); + + var rootPath = builder.GetContext().ApplicationRootPath; + var jsonSchemasPath = Path.Combine(rootPath, SCHEMA_DIRECTORY); + var schemaProvider = new SchemaProvider(); + if (Directory.Exists(jsonSchemasPath)) + { + var files = Directory.EnumerateFiles(jsonSchemasPath); + foreach (var file in files) + { + var fileName = Path.GetFileNameWithoutExtension(file); + schemaProvider.Schemas.Add((Schema)Enum.Parse(typeof(Schema), fileName), File.ReadAllText(file)); + } + } + builder.Services.AddSingleton(schemaProvider); } public string GetValueOrThrow(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs new file mode 100644 index 000000000..a1d98c8cb --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs @@ -0,0 +1,68 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Models; +using Repositories.Contracts; +using System; +using System.Threading.Tasks; +using NJsonSchema; + +namespace SqlMembershipObtainer +{ + public class SchemaValidatorFunction + { + private readonly ILoggingRepository _loggingRepository = null; + private readonly SchemaProvider _schemaProvider = null; + + public SchemaValidatorFunction(ILoggingRepository loggingRepository, SchemaProvider schemaProvider) + { + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + _schemaProvider = schemaProvider ?? throw new ArgumentNullException(nameof(schemaProvider)); + } + + [FunctionName(nameof(SchemaValidatorFunction))] + public async Task ValidateSchemasAsync([ActivityTrigger] SchemaValidatorRequest request) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); + + var isValidJson = true; + + if (_schemaProvider.Schemas.Count == 0) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = request.RunId, + Message = $"No json schemas have been loaded. Skipping schema validation." + }); + + return isValidJson; + } + + if (_schemaProvider.Schemas.TryGetValue(Schema.SqlMembershipSchema, out string value)) + { + var schema = await JsonSchema.FromJsonAsync(value); + var errors = schema.Validate(request.Query); + if (errors.Count > 0) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Query not valid: {errors}", RunId = request.RunId }, VerbosityLevel.DEBUG); + isValidJson = false; + } + } + + else + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = request.RunId, + Message = $"No SqlMembership schema has been loaded. Skipping schema validation." + }); + + return isValidJson; + } + + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); + return isValidJson; + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs new file mode 100644 index 000000000..79b24350d --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs @@ -0,0 +1,12 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. +using System; + +namespace SqlMembershipObtainer +{ + public class SchemaValidatorRequest + { + public string Query { get; set; } + public Guid? RunId { get; set; } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs index ec247c791..20b82f7aa 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs @@ -70,6 +70,31 @@ await context.CallActivityAsync( return; } + else + { + try + { + var hasValidJson = await context.CallActivityAsync(nameof(SchemaValidatorFunction), new SchemaValidatorRequest { Query = currentPart.ToString(), RunId = syncJob.RunId }); + if (!hasValidJson) + { + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { Status = SyncStatus.SchemaError, SyncJob = syncJob }); + return; + } + } + catch (JsonReaderException) + { + await context.CallActivityAsync(nameof(LoggerFunction), + new LoggerRequest + { + SyncJob = syncJob, + Message = $"Source query is not valid for job:{syncJob.Id}" + }); + + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { Status = SyncStatus.QueryNotValid, SyncJob = syncJob }); + return; + } + } + var query = JsonConvert.DeserializeObject(currentQueryAsString); var graphProfilesResponse = await context.CallSubOrchestratorAsync( nameof(OrganizationProcessorFunction), diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj index 67971eae7..dbc6cf2b6 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj @@ -16,6 +16,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln index 76dedabd1..a65a944d4 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln @@ -37,8 +37,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Entities", "..\..\..\Entiti EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework", "..\..\..\Repositories.EntityFramework\Repositories.EntityFramework.csproj", "{279D7C62-303B-4C72-A80E-8A3B4EE34E8D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{524EFA1D-B572-41E1-9D76-A4328C98B789}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Mail", "..\..\..\Repositories.Mail\Repositories.Mail.csproj", "{454EEE07-02C1-48A9-B751-88E94EC162C1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.RetryPolicyProvider", "..\..\..\Repositories.RetryPolicyProvider\Repositories.RetryPolicyProvider.csproj", "{98F62F05-1FD5-4F24-8B3B-A9FD5740381F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -121,6 +125,10 @@ Global {454EEE07-02C1-48A9-B751-88E94EC162C1}.Debug|Any CPU.Build.0 = Debug|Any CPU {454EEE07-02C1-48A9-B751-88E94EC162C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {454EEE07-02C1-48A9-B751-88E94EC162C1}.Release|Any CPU.Build.0 = Release|Any CPU + {98F62F05-1FD5-4F24-8B3B-A9FD5740381F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98F62F05-1FD5-4F24-8B3B-A9FD5740381F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98F62F05-1FD5-4F24-8B3B-A9FD5740381F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98F62F05-1FD5-4F24-8B3B-A9FD5740381F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Models/Models.csproj b/Service/GroupMembershipManagement/Models/Models.csproj index a2ad99dcf..1057c45b0 100644 --- a/Service/GroupMembershipManagement/Models/Models.csproj +++ b/Service/GroupMembershipManagement/Models/Models.csproj @@ -3,5 +3,23 @@ net6.0 + + + + Always + + + Always + + + Always + + + Always + + + Always + + diff --git a/Service/GroupMembershipManagement/Models/Schema.cs b/Service/GroupMembershipManagement/Models/Schema.cs new file mode 100644 index 000000000..f2ac7a005 --- /dev/null +++ b/Service/GroupMembershipManagement/Models/Schema.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Models +{ + public enum Schema + { + GroupMembershipSchema = 0, + GroupOwnershipSchema = 1, + PlaceMembershipSchema = 2, + SqlMembershipSchema = 3, + TeamsChannelMembershipSchema = 4 + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Models/SchemaProvider.cs b/Service/GroupMembershipManagement/Models/SchemaProvider.cs new file mode 100644 index 000000000..9053cbf09 --- /dev/null +++ b/Service/GroupMembershipManagement/Models/SchemaProvider.cs @@ -0,0 +1,12 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Collections.Generic; + +namespace Models +{ + public class SchemaProvider + { + public Dictionary Schemas { get; set; } = new Dictionary(); + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Models/Schemas/SqlMembershipSchema.json b/Service/GroupMembershipManagement/Models/Schemas/SqlMembershipSchema.json index e4c94c138..fa9af1d1e 100644 --- a/Service/GroupMembershipManagement/Models/Schemas/SqlMembershipSchema.json +++ b/Service/GroupMembershipManagement/Models/Schemas/SqlMembershipSchema.json @@ -1,50 +1,47 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["SqlMembership"] - }, - "source": { - "type": "object", - "anyOf": [ - { - "required": ["manager"] - }, - { - "required": ["filter"] - } - ], - "properties": { - "manager": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "minimum": 1 - }, - "depth": { - "type": "integer", - "minimum": 1 - } + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["SqlMembership"] + }, + "source": { + "type": "object", + "anyOf": [ + { + "required": ["manager"] + }, + { + "required": ["filter"] + } + ], + "properties": { + "manager": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1 }, - "required": ["id"], - "additionalProperties": false + "depth": { + "type": "integer", + "minimum": 1 + } }, - "filter": { - "type": "string" - } + "required": ["id"], + "additionalProperties": false }, - "additionalProperties": false + "filter": { + "type": "string" + } }, - "exclusionary": { - "type": "boolean" - } + "additionalProperties": false }, - "required": ["type", "source"], - "additionalProperties": false - } -} + "exclusionary": { + "type": "boolean" + } + }, + "required": ["type", "source"], + "additionalProperties": false +} \ No newline at end of file From 7e83854e84fdffd02a975d139ac3943ef7374019 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Fri, 12 Jul 2024 10:47:12 -0700 Subject: [PATCH 0072/1479] schema validation tests in SqlMembershipObtainer function --- .../Helpers/SchemaProviderFactory.cs | 34 +++++++++++++++ .../Helpers/SqlMembershipJobCreator.cs | 4 +- .../OrchestratorFunctionTests.cs | 41 +++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs new file mode 100644 index 000000000..a0e247179 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs @@ -0,0 +1,34 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; +using System; +using System.IO; + +namespace Services.Tests.Helpers +{ + internal static class SchemaProviderFactory + { + public static SchemaProvider CreateJsonSchemaProvider() + { + var jsonSchemaProvider = new SchemaProvider(); + var schemaFolderName = "Schemas"; + + var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; + var jsonSchemaDirectory = Path.Combine(currentDirectory, schemaFolderName); + + + if (jsonSchemaDirectory != null && Directory.Exists(jsonSchemaDirectory)) + { + var files = Directory.EnumerateFiles(jsonSchemaDirectory); + foreach (var file in files) + { + var fileName = Path.GetFileNameWithoutExtension(file); + jsonSchemaProvider.Schemas.Add((Schema)Enum.Parse(typeof(Schema), fileName), File.ReadAllText(file)); + } + } + + return jsonSchemaProvider; + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/SqlMembershipJobCreator.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/SqlMembershipJobCreator.cs index 8d366c2cf..699ed0d2b 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/SqlMembershipJobCreator.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/SqlMembershipJobCreator.cs @@ -37,8 +37,8 @@ public static List CreateSampleSyncJobs(int numberOfJobs, string syncTy public static string GetJobQuery(string syncType, string managerId) { var individualQueries = $"{{\"type\":\"{syncType}\"," + - $"\"source\": {{\"id\":[{managerId}]," + - $"\"filter\":\"(Attribute = 'Value')\"}} }}"; + $"\"source\":{{\"manager\":{{\"id\":{managerId}}}," + + $"\"filter\":\"(Attribute = 'Value')\"}}}}"; return $"[{string.Join(",", individualQueries)}]"; diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs index 304b8bee7..17a9f6b01 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs @@ -42,6 +42,8 @@ public class OrchestratorFunctionTests private TelemetryClient _telemetryClient; private Mock _sqlMembershipObtainerService; private Mock _serviceBusQueueRepository; + SchemaProvider _schemaProvider; + private bool _isValid = true; [TestInitialize] public void Setup() @@ -118,6 +120,14 @@ public void Setup() { await CallQueueMessageSenderFunctionAsync(request as MembershipAggregatorHttpRequest); }); + _schemaProvider = SchemaProviderFactory.CreateJsonSchemaProvider(); + + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => _isValid); } [TestMethod] @@ -190,6 +200,31 @@ public async Task TestInvalidSqlMembershipQueryAsync() It.Is(x => x.Status == SyncStatus.QueryNotValid)), Times.Once()); } + [TestMethod] + public async Task TestInvalidSchemaAsync() + { + _syncJob.Query = "[{\"type\":\"SqlMembership\",\"source\":{\"manager\":{\"id\":[1, 2]},\"filter\":\"Attribute = 'Value'\"}}]"; + + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => false); + + var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object, _executionContext.Object); + + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message.Contains($"Query not valid")), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once()); + + _context.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), + It.Is(x => x.Status == SyncStatus.SchemaError)), Times.Once()); + } + [TestMethod] public async Task TestInvalidFilePathFailureAsync() { @@ -300,6 +335,12 @@ private async Task CallLoggerFunctionAsync(LoggerRequest request) return await function.SendGroupMembershipAsync(request); } + private async Task CallSchemaValidatorFunctionAsync(SchemaValidatorRequest request) + { + var function = new SchemaValidatorFunction(_loggingRepository.Object, _schemaProvider); + return await function.ValidateSchemasAsync(request); + } + private async Task CallQueueMessageSenderFunctionAsync(MembershipAggregatorHttpRequest request) { var function = new QueueMessageSenderFunction(_loggingRepository.Object, _serviceBusQueueRepository.Object); From 7cbdabf35c50506d90d6c106cc169b4fe8cae346 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 15 Jul 2024 14:27:55 -0700 Subject: [PATCH 0073/1479] schema validation check in PlaceMembershipObtainer function --- .../SchemaValidatorFunction.cs | 68 +++++++++++++++++++ .../SchemaValidator/SchemaValidatorRequest.cs | 12 ++++ .../Orchestrator/OrchestratorFunction.cs | 20 ++++++ .../Function/PlaceMembershipObtainer.csproj | 1 + .../Function/PlaceMembershipObtainer.sln | 6 ++ 5 files changed, 107 insertions(+) create mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs create mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs new file mode 100644 index 000000000..67bd30ec8 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs @@ -0,0 +1,68 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Models; +using Repositories.Contracts; +using System; +using System.Threading.Tasks; +using NJsonSchema; + +namespace Hosts.PlaceMembershipObtainer +{ + public class SchemaValidatorFunction + { + private readonly ILoggingRepository _loggingRepository = null; + private readonly SchemaProvider _schemaProvider = null; + + public SchemaValidatorFunction(ILoggingRepository loggingRepository, SchemaProvider schemaProvider) + { + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + _schemaProvider = schemaProvider ?? throw new ArgumentNullException(nameof(schemaProvider)); + } + + [FunctionName(nameof(SchemaValidatorFunction))] + public async Task ValidateSchemasAsync([ActivityTrigger] SchemaValidatorRequest request) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); + + var isValidJson = true; + + if (_schemaProvider.Schemas.Count == 0) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = request.RunId, + Message = $"No json schemas have been loaded. Skipping schema validation." + }); + + return isValidJson; + } + + if (_schemaProvider.Schemas.TryGetValue(Schema.PlaceMembershipSchema, out string value)) + { + var schema = await JsonSchema.FromJsonAsync(value); + var errors = schema.Validate(request.Query); + if (errors.Count > 0) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Query not valid: {errors}", RunId = request.RunId }, VerbosityLevel.DEBUG); + isValidJson = false; + } + } + + else + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = request.RunId, + Message = $"No PlaceMembership schema has been loaded. Skipping schema validation." + }); + + return isValidJson; + } + + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); + return isValidJson; + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs new file mode 100644 index 000000000..74fbb42af --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs @@ -0,0 +1,12 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. +using System; + +namespace Hosts.PlaceMembershipObtainer +{ + public class SchemaValidatorRequest + { + public string Query { get; set; } + public Guid? RunId { get; set; } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs index ce32f700b..94a4d2de1 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs @@ -4,6 +4,7 @@ using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Configuration; using Models; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Repositories.Contracts; using Services; @@ -68,6 +69,25 @@ public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrat return; } + else + { + try + { + var hasValidJson = await context.CallActivityAsync(nameof(SchemaValidatorFunction), new SchemaValidatorRequest { Query = currentPart.ToString(), RunId = syncJob.RunId }); + if (!hasValidJson) + { + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { Status = SyncStatus.SchemaError, SyncJob = syncJob }); + return; + } + } + catch (JsonReaderException) + { + if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { RunId = runId, Message = $"Source query is not valid for job:{syncJob.Id}" }); + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { Status = SyncStatus.QueryNotValid, SyncJob = syncJob }); + return; + } + } + var response = await context.CallSubOrchestratorAsync<(List Users, SyncStatus Status)>(nameof(SubOrchestratorFunction), new SubOrchestratorRequest { SyncJob = syncJob, Url = currentQueryAsString, RunId = runId }); diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj index 59e7dec88..4d29da1f6 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj @@ -8,6 +8,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.sln b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.sln index c39e2010c..fa9fafa41 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.sln +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.sln @@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Services", "..\Servic EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\Models.csproj", "{57A8CCEB-A21C-472E-AFFA-D5B8777695DB}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{ECE51FFA-5861-4BAE-9C43-7C2476EAF9B5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {57A8CCEB-A21C-472E-AFFA-D5B8777695DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {57A8CCEB-A21C-472E-AFFA-D5B8777695DB}.Release|Any CPU.ActiveCfg = Release|Any CPU {57A8CCEB-A21C-472E-AFFA-D5B8777695DB}.Release|Any CPU.Build.0 = Release|Any CPU + {ECE51FFA-5861-4BAE-9C43-7C2476EAF9B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECE51FFA-5861-4BAE-9C43-7C2476EAF9B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECE51FFA-5861-4BAE-9C43-7C2476EAF9B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECE51FFA-5861-4BAE-9C43-7C2476EAF9B5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 49611f6aa9c24d81ae57ed53f35c4963a89d391b Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 16 Jul 2024 07:08:45 -0700 Subject: [PATCH 0074/1479] schema validation tests in PlaceMembershipObtainer function --- .../Helpers/PlaceMembershipJobCreator.cs | 54 +++++ .../Helpers/SchemaProviderFactory.cs | 34 +++ .../OrchestratorFunctionTests.cs | 199 ++++++++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Helpers/PlaceMembershipJobCreator.cs create mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs create mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Helpers/PlaceMembershipJobCreator.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Helpers/PlaceMembershipJobCreator.cs new file mode 100644 index 000000000..58db156da --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Helpers/PlaceMembershipJobCreator.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Tests.Services.Helpers +{ + public class QuerySample + { + public List QueryParts { get; set; } = new List(); + + public string GetQuery() + { + var parts = string.Join(",", QueryParts.OrderBy(x => x.Index).Select(x => GetPartQuery(x.Index))); + return $"[{parts}]"; + } + + private string GetPartQuery(int partIndex) + { + var queryPart = QueryParts.First(x => x.Index == partIndex); + return $"{{'type':'{queryPart.Type}','source': '{queryPart.SourceId}'}}"; + } + + public Guid GetSourceId(int partIndex) + { + return QueryParts.First(x => x.Index == partIndex).SourceId; + } + + public static QuerySample GenerateQuerySample(string syncType, int numberOfParts = 2) + { + var sampleQuery = new QuerySample(); + for (int i = 0; i < numberOfParts; i++) + { + sampleQuery.QueryParts.Add(new QueryPart + { + Index = i, + Type = syncType, + SourceId = Guid.NewGuid() + }); + } + + return sampleQuery; + } + } + + public class QueryPart + { + public int Index { get; set; } + public string Type { get; set; } + public Guid SourceId { get; set; } + public bool IsDestinationPart { get; set; } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs new file mode 100644 index 000000000..60507e380 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs @@ -0,0 +1,34 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; +using System; +using System.IO; + +namespace Tests.Services.Helpers +{ + internal static class SchemaProviderFactory + { + public static SchemaProvider CreateJsonSchemaProvider() + { + var jsonSchemaProvider = new SchemaProvider(); + var schemaFolderName = "Schemas"; + + var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; + var jsonSchemaDirectory = Path.Combine(currentDirectory, schemaFolderName); + + + if (jsonSchemaDirectory != null && Directory.Exists(jsonSchemaDirectory)) + { + var files = Directory.EnumerateFiles(jsonSchemaDirectory); + foreach (var file in files) + { + var fileName = Path.GetFileNameWithoutExtension(file); + jsonSchemaProvider.Schemas.Add((Schema)Enum.Parse(typeof(Schema), fileName), File.ReadAllText(file)); + } + } + + return jsonSchemaProvider; + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs new file mode 100644 index 000000000..7c8a0c878 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs @@ -0,0 +1,199 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Models; +using Tests.Services.Helpers; +using Moq; +using Hosts.PlaceMembershipObtainer; +using Repositories.Contracts; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Services; +using Repositories.Contracts.InjectConfig; + +namespace Tests.Services +{ + [TestClass] + public class OrchestratorFunctionTests + { + private int _profilesCount = 10; + private List _profiles; + private Mock _configuration; + private Mock _loggingRepository; + private Mock _context; + private Mock _executionContext; + private SyncJob _syncJob; + private QuerySample _querySample; + private OrchestratorRequest _orchestratorRequest; + private SyncStatus _subOrchestratorResponseStatus; + private SyncStatus _senderResponseStatus = SyncStatus.InProgress; + private string _senderResponseFilePath = "file-path"; + private DurableHttpResponse _membershipAggregatorResponse; + private TelemetryClient _telemetryClient; + private PlaceMembershipObtainerService _placeMembershipObtainerService; + private Mock _serviceBusQueueRepository; + SchemaProvider _schemaProvider; + private bool _isValid = true; + private int _usersToReturn; + + + [TestInitialize] + public void Setup() + { + _configuration = new Mock(); + _loggingRepository = new Mock(); + var mockGraphGroupRepository = new Mock(); + var mockBlobStorageRepository = new Mock(); + var mockSyncJob = new Mock(); + var mockDryRunValue = new Mock(); + _querySample = QuerySample.GenerateQuerySample("PlaceMembership"); + var syncJob = new SyncJob + { + Id = Guid.NewGuid(), + TargetOfficeGroupId = Guid.NewGuid(), + Query = _querySample.GetQuery(), + Status = "InProgress", + Period = 6 + }; + + _orchestratorRequest = new OrchestratorRequest + { + CurrentPart = 1, + TotalParts = _querySample.QueryParts.Count + 1, + SyncJob = syncJob, + IsDestinationPart = false + }; + _usersToReturn = 10; + mockDryRunValue.Setup(d => d.DryRunEnabled).Returns(true); + _placeMembershipObtainerService = new PlaceMembershipObtainerService( + mockGraphGroupRepository.Object, + mockBlobStorageRepository.Object, + mockSyncJob.Object, + mockDryRunValue.Object); + _context = new Mock(); + _executionContext = new Mock(); + _telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); + _serviceBusQueueRepository = new Mock(); + + _context.Setup(x => x.GetInput()) + .Returns(() => _orchestratorRequest); + + _profiles = new List(); + for (int i = 0; i < _profilesCount; i++) + { + _profiles.Add(new GraphProfileInformation + { + Id = Guid.NewGuid().ToString(), + PersonnelNumber = (i + 1).ToString(), + UserPrincipalName = $"user{i}@domain.com" + }); + } + + _membershipAggregatorResponse = new DurableHttpResponse(HttpStatusCode.NoContent); + + _subOrchestratorResponseStatus = SyncStatus.InProgress; + + _context.Setup(x => x.CallSubOrchestratorAsync<(List Users, SyncStatus Status)>(It.IsAny(), It.IsAny())) + .ReturnsAsync(() => + { + var users = new List(); + for (var i = 0; i < _usersToReturn; i++) + { + users.Add(new AzureADUser { ObjectId = Guid.NewGuid() }); + } + + var response = (Users: users, Status: _subOrchestratorResponseStatus); + return response; + }); + + string _filePath = null; + _context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + .Callback(async (name, request) => + { + _filePath = await CallUsersSenderFunctionAsync(request as UsersSenderRequest); + }) + .ReturnsAsync(() => _filePath); + + _context.Setup(x => x.CallHttpAsync(It.IsAny())).ReturnsAsync(() => _membershipAggregatorResponse); + + _context.Setup(x => x.CallActivityAsync(nameof(QueueMessageSenderFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallQueueMessageSenderFunctionAsync(request as MembershipAggregatorHttpRequest); + }); + + _schemaProvider = SchemaProviderFactory.CreateJsonSchemaProvider(); + + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => _isValid); + } + + [TestMethod] + public async Task TestValidPlaceMembershipQueryAsync() + { + var orchestratorFunction = new OrchestratorFunction(_loggingRepository.Object, _placeMembershipObtainerService, _configuration.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message == $"{nameof(OrchestratorFunction)} function completed"), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Once); + } + + + [TestMethod] + public async Task TestInvalidSchemaAsync() + { + var syncJob = new SyncJob + { + Id = Guid.NewGuid(), + TargetOfficeGroupId = Guid.NewGuid(), + Query = "[{\"type\":\"PlaceMembership\",\"sources\":\"https://graph.microsoft.com/v1.0/users?$count=true&$filter=mail+eq+'USER2@M365x720024.onmicrosoft.com'\"}]", + Status = "InProgress", + Period = 6 + }; + + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => false); + + var orchestratorFunction = new OrchestratorFunction(_loggingRepository.Object, _placeMembershipObtainerService, _configuration.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); + _context.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), + It.Is(x => x.Status == SyncStatus.SchemaError)), Times.Once()); + } + + private async Task CallUsersSenderFunctionAsync(UsersSenderRequest request) + { + var function = new UsersSenderFunction(_loggingRepository.Object, _placeMembershipObtainerService); + return await function.SendUsersAsync(request); + } + + private async Task CallSchemaValidatorFunctionAsync(SchemaValidatorRequest request) + { + var function = new SchemaValidatorFunction(_loggingRepository.Object, _schemaProvider); + return await function.ValidateSchemasAsync(request); + } + + private async Task CallQueueMessageSenderFunctionAsync(MembershipAggregatorHttpRequest request) + { + var function = new QueueMessageSenderFunction(_loggingRepository.Object, _serviceBusQueueRepository.Object); + await function.SendMessageAsync(request); + } + } +} From 72c79bf6d49eb2bc4d2221208bf5cb67cc920fc9 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 15 Jul 2024 15:51:44 -0700 Subject: [PATCH 0075/1479] Fix notifier issue --- .../Hosts.FunctionBase/CommonStartup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs index e841f2d6e..edfa1dc85 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs @@ -183,7 +183,7 @@ public override void Configure(IFunctionsHostBuilder builder) configuration.GetValue("Mail:SkipMailNotifications", false)); }); - builder.Services.AddSingleton(services => + builder.Services.AddScoped(services => { var mailConfig = services.GetService(); var graphCredentials = services.GetService>().Value; From e2c51f03cc21b9034406bd5268f8ddef92584b7f Mon Sep 17 00:00:00 2001 From: abgonz Date: Mon, 15 Jul 2024 07:53:50 -0700 Subject: [PATCH 0076/1479] Fix viewing details for Job Reader --- .../AdvancedQuery/AdvancedQuery.base.tsx | 3 +++ .../AdvancedViewSourcePart.base.tsx | 5 ++++- .../GroupQuerySource/GroupQuerySource.base.tsx | 3 +++ .../HRQuerySource/HRQuerySource.base.tsx | 14 +++++++++++--- .../MembershipConfiguration.base.tsx | 5 ++++- .../RunConfiguration/RunConfiguration.base.tsx | 11 ++++++++++- .../components/SourcePart/SourcePart.base.tsx | 15 +++++++++++++-- .../src/pages/JobDetails/JobDetails.base.tsx | 18 +++++++++--------- .../ManageMembership/ManageMembership.base.tsx | 8 ++++---- 9 files changed, 61 insertions(+), 21 deletions(-) diff --git a/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx b/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx index 197e768ff..35c06bb46 100644 --- a/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx +++ b/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx @@ -27,6 +27,7 @@ import { removeUnusedProperties } from '../../utils/sourcePartUtils'; import { SourcePartType } from '../../models/SourcePartType'; import { SourcePartQuery } from '../../models/SourcePartQuery'; import { validateGroup } from '../../store/groups.api'; +import { selectIsJobWriter } from '../../store/roles.slice'; const getClassNames = classNamesFunction< IAdvancedQueryStyleProps, @@ -74,6 +75,7 @@ export const AdvancedQueryBase: React.FunctionComponent = ( const schema = schemaDefinition; const ajv = new Ajv(); const advancedViewQueryFromStore = useSelector(manageMembershipAdvancedViewQuery); + const isJobWriter = useSelector(selectIsJobWriter); useEffect(() => { setLocalQuery(advancedViewQueryFromStore || defaultAdvancedViewQuery); @@ -172,6 +174,7 @@ export const AdvancedQueryBase: React.FunctionComponent = ( value={localQuery} onChange={handleQueryChange} onBlur={handleBlur} + disabled={!isJobWriter} /> {validationMessage && (
diff --git a/UI/web-app/src/components/AdvancedViewSourcePart/AdvancedViewSourcePart.base.tsx b/UI/web-app/src/components/AdvancedViewSourcePart/AdvancedViewSourcePart.base.tsx index 0e3960dda..8e8fbdd52 100644 --- a/UI/web-app/src/components/AdvancedViewSourcePart/AdvancedViewSourcePart.base.tsx +++ b/UI/web-app/src/components/AdvancedViewSourcePart/AdvancedViewSourcePart.base.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT license. import React, { useEffect, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { IProcessedStyleSet, TextField, @@ -27,6 +27,7 @@ import { import { ISourcePart } from '../../models/ISourcePart'; import { GroupOwnershipSourcePart } from '../../models/GroupOwnershipSourcePart'; import { PlaceMembershipSourcePart } from '../../models/PlaceMembershipSourcePart'; +import { selectIsJobWriter } from '../../store/roles.slice'; const getClassNames = classNamesFunction< IAdvancedViewSourcePartStyleProps, @@ -52,6 +53,7 @@ export const AdvancedViewSourcePartBase: React.FunctionComponent(JSON.stringify(part.query)); const schema = part.query.type === 'GroupOwnership' ? GroupOwnershipSchema : PlaceMembershipSchema; const ajv = new Ajv(); + const isJobWriter = useSelector(selectIsJobWriter); useEffect(() => { setLocalQuery(JSON.stringify(part.query)); @@ -124,6 +126,7 @@ export const AdvancedViewSourcePartBase: React.FunctionComponent {validationMessage && (
diff --git a/UI/web-app/src/components/GroupQuerySource/GroupQuerySource.base.tsx b/UI/web-app/src/components/GroupQuerySource/GroupQuerySource.base.tsx index 9720da796..b5fd680c0 100644 --- a/UI/web-app/src/components/GroupQuerySource/GroupQuerySource.base.tsx +++ b/UI/web-app/src/components/GroupQuerySource/GroupQuerySource.base.tsx @@ -21,6 +21,7 @@ import { AppDispatch } from '../../store'; import { searchDestinations } from '../../store/manageMembership.api'; import { IsGroupMembershipSourcePartQuery } from '../../models/GroupMembershipSourcePart'; import { useSelectedGroupById } from '../../store/groupPart.slice'; +import { selectIsJobWriter } from '../../store/roles.slice'; import { searchGroups } from '../../store/groups.api'; export const getClassNames = classNamesFunction(); @@ -33,6 +34,7 @@ export const GroupQuerySourceBase: React.FunctionComponent(); + const isJobWriter = useSelector(selectIsJobWriter); const groupId: string = IsGroupMembershipSourcePartQuery(part.query) ? part.query.source : ''; const [localSearchResults, setLocalSearchResults] = useState([]); @@ -113,6 +115,7 @@ export const GroupQuerySourceBase: React.FunctionComponent
); diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 131fc2037..6d5174a82 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -22,6 +22,7 @@ import { selectJobOwnerFilterSuggestions } from '../../store/jobs.slice'; import { fetchDefaultSqlMembershipSourceAttributes } from '../../store/sqlMembershipSources.api'; import { fetchAttributeValues } from '../../store/sqlMembershipSources.api'; import { selectAttributes, selectSource, selectAttributeValues, setAttributeValues } from '../../store/sqlMembershipSources.slice'; +import { selectIsJobWriter } from '../../store/roles.slice'; import { SqlMembershipAttribute, SqlMembershipAttributeValue } from '../../models'; import { IFilterPart } from '../../models/IFilterPart'; import { Group } from '../../models/Group'; @@ -53,6 +54,7 @@ export const HRQuerySourceBase: React.FunctionComponent = (p const orgLeaderDetails = useSelector(selectOrgLeaderDetails); const objectIdEmployeeIdMapping = useSelector(selectObjectIdEmployeeIdMapping); const ownerPickerSuggestions = useSelector(selectJobOwnerFilterSuggestions); + const isJobWriter = useSelector(selectIsJobWriter); const [isDragAndDropEnabled, setIsDragAndDropEnabled] = useState(false); const [isDisabled, setIsDisabled] = useState(true); const [includeOrg, setIncludeOrg] = useState(false); @@ -1523,6 +1525,7 @@ const checkType = (value: string, type: string | undefined): string => { root: classNames.horizontalChoiceGroup, flexContainer: classNames.horizontalChoiceGroupContainer }} + disabled={!isJobWriter} /> {(includeOrg || (source?.manager?.id && objectIdEmployeeIdMapping[source.manager.id] && objectIdEmployeeIdMapping[source.manager.id].text !== undefined)) && ( @@ -1551,6 +1554,7 @@ const checkType = (value: string, type: string | undefined): string => { onChange={handleOrgLeaderChange} styles={{ root: classNames.textField, text: classNames.textFieldGroup }} pickerCalloutProps={{directionalHint: DirectionalHint.bottomCenter}} + disabled={!isJobWriter} />
@@ -1565,7 +1569,7 @@ const checkType = (value: string, type: string | undefined): string => { { root: classNames.horizontalChoiceGroup, flexContainer: classNames.horizontalChoiceGroupContainer }} + disabled={!isJobWriter} /> @@ -1609,6 +1614,7 @@ const checkType = (value: string, type: string | undefined): string => { root: classNames.horizontalChoiceGroup, flexContainer: classNames.horizontalChoiceGroupContainer }} + disabled={!isJobWriter} /> {(includeFilter || source.filter) && @@ -1621,6 +1627,7 @@ const checkType = (value: string, type: string | undefined): string => { styles={{ root: classNames.expandButton }} onClick={toggleExpand} title={expanded ? strings.ManageMembership.labels.collapse : strings.ManageMembership.labels.expand} + disabled={!isJobWriter} /> } @@ -1643,6 +1650,7 @@ const checkType = (value: string, type: string | undefined): string => { styles={{ root: classNames.textField, fieldGroup: classNames.textFieldGroup }} validateOnLoad={false} validateOnFocusOut={false} + disabled={!isJobWriter} > ) : attributes && attributes.length > 0 && expanded && (includeFilter || source.filter) ? ( @@ -1650,13 +1658,13 @@ const checkType = (value: string, type: string | undefined): string => { 1)}> + disabled={(!(selectedIndices.length > 1)) || !isJobWriter}> {strings.HROnboarding.group} 0 && groups.length > 0 && groupingEnabled)}> + disabled={(!(selectedIndices.length > 0 && groups.length > 0 && groupingEnabled)) || !isJobWriter}> {strings.HROnboarding.ungroup}
diff --git a/UI/web-app/src/components/MembershipConfiguration/MembershipConfiguration.base.tsx b/UI/web-app/src/components/MembershipConfiguration/MembershipConfiguration.base.tsx index ebfbdc887..f9cfe9845 100644 --- a/UI/web-app/src/components/MembershipConfiguration/MembershipConfiguration.base.tsx +++ b/UI/web-app/src/components/MembershipConfiguration/MembershipConfiguration.base.tsx @@ -29,6 +29,7 @@ import { useStrings } from '../../store/hooks'; import { HRSourcePartSource } from '../../models/HRSourcePart'; import { ISourcePart } from '../../models/ISourcePart'; import { SourcePartType } from '../../models/SourcePartType'; +import { selectIsJobWriter } from '../../store/roles.slice'; const getClassNames = classNamesFunction(); @@ -48,6 +49,7 @@ export const MembershipConfigurationBase: React.FunctionComponent + onClick={newSourcePart} + disabled={!isJobWriter}> {strings.ManageMembership.labels.addSourcePart} diff --git a/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx b/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx index 15e2629f4..416ab4ca1 100644 --- a/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx +++ b/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx @@ -39,7 +39,7 @@ import { } from '../../store/manageMembership.slice'; import { AppDispatch } from '../../store'; import { useDispatch, useSelector } from 'react-redux'; -import { selectIsJobTenantWriter } from '../../store/roles.slice'; +import { selectIsJobTenantWriter, selectIsJobWriter } from '../../store/roles.slice'; const getClassNames = classNamesFunction< IRunConfigurationStyleProps, @@ -68,6 +68,7 @@ export const RunConfigurationBase: React.FunctionComponent {startDateOption === 'RequestedDate' && ( )}
@@ -158,6 +161,7 @@ export const RunConfigurationBase: React.FunctionComponent
@@ -189,6 +193,7 @@ export const RunConfigurationBase: React.FunctionComponent {useThresholdLimits === 'No' && (
@@ -241,6 +248,7 @@ export const RunConfigurationBase: React.FunctionComponent )} diff --git a/UI/web-app/src/components/SourcePart/SourcePart.base.tsx b/UI/web-app/src/components/SourcePart/SourcePart.base.tsx index 8dec08acc..0b73b038e 100644 --- a/UI/web-app/src/components/SourcePart/SourcePart.base.tsx +++ b/UI/web-app/src/components/SourcePart/SourcePart.base.tsx @@ -26,6 +26,7 @@ import { SourcePartQuery } from '../../models/SourcePartQuery'; import { AdvancedViewSourcePart } from '../AdvancedViewSourcePart'; import { selectSource } from '../../store/sqlMembershipSources.slice'; import { SqlMembershipSource } from '../../models'; +import { selectIsJobWriter } from '../../store/roles.slice'; const getClassNames = classNamesFunction(); @@ -53,6 +54,7 @@ export const SourcePartBase: React.FunctionComponent = (props: const [isExclusionary, setIsExclusionary] = useState(query.exclusionary); const [errorMessage, setErrorMessage] = useState(''); const isEditingExistingJob = useSelector(manageMembershipIsEditingExistingJob); + const isJobWriter = useSelector(selectIsJobWriter); const [expanded, setExpanded] = useState(part.isNew ||isEditingExistingJob); const hrSource = useSelector(selectSource); @@ -190,6 +192,7 @@ export const SourcePartBase: React.FunctionComponent = (props: required={true} selectedKey={part.query.type} onChange={handleSourceTypeChanged} + disabled={!isJobWriter} /> = (props: required={true} onChange={handleExclusionaryChange} selectedKey={isExclusionary ? 'Yes' : 'No'} + disabled={!isJobWriter} /> {isEditingExistingJob ? <> - : + : {strings.delete} } @@ -227,7 +236,9 @@ export const SourcePartBase: React.FunctionComponent = (props: {part.query.type === SourcePartType.HR && (part.query.source.filter !== "" || part.query.source.manager?.id !== undefined) && (totalSourceParts === part.id) && ( + onClick={handleCopy} + disabled={!isJobWriter} + > {strings.copy} )} diff --git a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx index 6f0cdf61e..2832a1755 100644 --- a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx +++ b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx @@ -210,15 +210,15 @@ export const JobDetailsBase: React.FunctionComponent = ( useLinkButton={true} actionOnClick={openRunConfiguration} /> - {jobDetails?.source}} - removeButton={isJobWriter} - editButton={canEditJob} - actionText={canEditJob ? strings.JobDetails.editButton : ''} - useLinkButton={true} - actionOnClick={openMembershipConfiguration} - /> + {jobDetails?.source}} + removeButton={isJobWriter} + editButton={canEditJob} + actionText={canEditJob ? strings.JobDetails.editButton : strings.JobDetails.viewDetails} + useLinkButton={true} + actionOnClick={openMembershipConfiguration} + />
{canDeleteJob && { - let editingExistingJob = !!locationState?.jobId && isJobWriter; + let editingExistingJob = !!locationState?.jobId; dispatch(setIsEditingExistingJob(editingExistingJob)); if (!editingExistingJob) { @@ -120,7 +120,7 @@ export const ManageMembershipBase: React.FunctionComponent { if (jobDetailsRef.current) { @@ -294,7 +294,7 @@ export const ManageMembershipBase: React.FunctionComponent Date: Tue, 16 Jul 2024 12:36:44 -0700 Subject: [PATCH 0077/1479] Add fix for delete button --- .../components/SourcePart/SourcePart.base.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/UI/web-app/src/components/SourcePart/SourcePart.base.tsx b/UI/web-app/src/components/SourcePart/SourcePart.base.tsx index 0b73b038e..ea35a5f4e 100644 --- a/UI/web-app/src/components/SourcePart/SourcePart.base.tsx +++ b/UI/web-app/src/components/SourcePart/SourcePart.base.tsx @@ -203,17 +203,14 @@ export const SourcePartBase: React.FunctionComponent = (props: selectedKey={isExclusionary ? 'Yes' : 'No'} disabled={!isJobWriter} /> - {isEditingExistingJob ? - <> - : - {strings.delete} - - } + + {strings.delete} +
{part.query.type === SourcePartType.HR && ( From a53cb107487439cdc37762c7b513da95268214aa Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 17 Jul 2024 11:48:05 -0700 Subject: [PATCH 0078/1479] Ensure timestamps are being treated as UTC --- UI/web-app/src/utils/dateUtils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/UI/web-app/src/utils/dateUtils.ts b/UI/web-app/src/utils/dateUtils.ts index d3199d186..eff5d1374 100644 --- a/UI/web-app/src/utils/dateUtils.ts +++ b/UI/web-app/src/utils/dateUtils.ts @@ -4,7 +4,8 @@ const SQLMinDate = new Date(Date.UTC(1753, 0, 1)); export function formatLastRunTime(lastSuccessfulRunTime: string): [string, number] { - const lastRunTimeDate = new Date(lastSuccessfulRunTime); + const utcLastRunTime = lastSuccessfulRunTime.endsWith('Z') ? lastSuccessfulRunTime : `${lastSuccessfulRunTime}Z`; + const lastRunTimeDate = new Date(utcLastRunTime); const today = new Date(); const hoursAgo = Math.round(Math.abs(today.getTime() - lastRunTimeDate.getTime()) / 36e5); @@ -22,7 +23,8 @@ export function formatLastRunTime(lastSuccessfulRunTime: string): [string, numbe } export function formatNextRunTime(estimatedNextRunTime: string, enabled: boolean): [string, number] { - const estimatedNextRunTimeDate = new Date(estimatedNextRunTime); + const utcEstimatedNextRunTime = estimatedNextRunTime.endsWith('Z') ? estimatedNextRunTime : `${estimatedNextRunTime}Z`; + const estimatedNextRunTimeDate = new Date(utcEstimatedNextRunTime); const today = new Date(); const hoursLeft = Math.round(Math.abs(estimatedNextRunTimeDate.getTime() - today.getTime()) / 36e5); From dbecdafa7c6aaa92f8cb5c60739a159eb8a3d5b5 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 16 Jul 2024 10:38:11 -0700 Subject: [PATCH 0079/1479] schema validation check in GroupMembershipObtainer function --- .../SchemaValidatorFunction.cs | 68 +++++++++++++++++++ .../SchemaValidator/SchemaValidatorRequest.cs | 12 ++++ .../Function/GroupMembershipObtainer.csproj | 1 + .../Function/GroupMembershipObtainer.sln | 6 ++ .../Orchestrator/OrchestratorFunction.cs | 22 ++++++ .../Models/Schemas/GroupMembershipSchema.json | 37 +++++----- 6 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs create mode 100644 Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs new file mode 100644 index 000000000..089a2bcb4 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs @@ -0,0 +1,68 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Models; +using Repositories.Contracts; +using System; +using System.Threading.Tasks; +using NJsonSchema; + +namespace Hosts.GroupMembershipObtainer +{ + public class SchemaValidatorFunction + { + private readonly ILoggingRepository _loggingRepository = null; + private readonly SchemaProvider _schemaProvider = null; + + public SchemaValidatorFunction(ILoggingRepository loggingRepository, SchemaProvider schemaProvider) + { + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + _schemaProvider = schemaProvider ?? throw new ArgumentNullException(nameof(schemaProvider)); + } + + [FunctionName(nameof(SchemaValidatorFunction))] + public async Task ValidateSchemasAsync([ActivityTrigger] SchemaValidatorRequest request) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); + + var isValidJson = true; + + if (_schemaProvider.Schemas.Count == 0) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = request.RunId, + Message = $"No json schemas have been loaded. Skipping schema validation." + }); + + return isValidJson; + } + + if (_schemaProvider.Schemas.TryGetValue(Schema.GroupMembershipSchema, out string value)) + { + var schema = await JsonSchema.FromJsonAsync(value); + var errors = schema.Validate(request.Query); + if (errors.Count > 0) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Query not valid: {errors}", RunId = request.RunId }, VerbosityLevel.DEBUG); + isValidJson = false; + } + } + + else + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = request.RunId, + Message = $"No GroupMembership schema has been loaded. Skipping schema validation." + }); + + return isValidJson; + } + + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); + return isValidJson; + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs new file mode 100644 index 000000000..134908180 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs @@ -0,0 +1,12 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. +using System; + +namespace Hosts.GroupMembershipObtainer +{ + public class SchemaValidatorRequest + { + public string Query { get; set; } + public Guid? RunId { get; set; } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj index 76860f8a9..45b5edd2f 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj @@ -9,6 +9,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.sln b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.sln index 6a7db7684..6dcd24a28 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.sln +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.sln @@ -40,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.FeatureFlag", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework", "..\..\..\Repositories.EntityFramework\Repositories.EntityFramework.csproj", "{1337D93F-C6FE-46BA-A090-272344CBFEA8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework.Contexts", "..\..\..\Repositories.EntityFramework.Contexts\Repositories.EntityFramework.Contexts.csproj", "{B8E8AFE6-8C99-4AD2-9334-D3BC69E4A55F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -122,6 +124,10 @@ Global {1337D93F-C6FE-46BA-A090-272344CBFEA8}.Debug|Any CPU.Build.0 = Debug|Any CPU {1337D93F-C6FE-46BA-A090-272344CBFEA8}.Release|Any CPU.ActiveCfg = Release|Any CPU {1337D93F-C6FE-46BA-A090-272344CBFEA8}.Release|Any CPU.Build.0 = Release|Any CPU + {B8E8AFE6-8C99-4AD2-9334-D3BC69E4A55F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8E8AFE6-8C99-4AD2-9334-D3BC69E4A55F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8E8AFE6-8C99-4AD2-9334-D3BC69E4A55F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8E8AFE6-8C99-4AD2-9334-D3BC69E4A55F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs index a26788b3b..fd287c085 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using Repositories.Contracts.InjectConfig; using Models.Notifications; +using Newtonsoft.Json.Linq; namespace Hosts.GroupMembershipObtainer { @@ -101,6 +102,27 @@ public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrat } else { + if (!mainRequest.IsDestinationPart) + { + try + { + var queryParts = JArray.Parse(syncJob.Query); + var currentPart = queryParts[mainRequest.CurrentPart - 1]; + var hasValidJson = await context.CallActivityAsync(nameof(SchemaValidatorFunction), new SchemaValidatorRequest { Query = currentPart.ToString(), RunId = syncJob.RunId }); + if (!hasValidJson) + { + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { Status = SyncStatus.SchemaError, SyncJob = syncJob }); + return; + } + } + catch (JsonReaderException) + { + if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { RunId = runId, Message = $"Source query is not valid for job:{syncJob.Id}" }); + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { Status = SyncStatus.QueryNotValid, SyncJob = syncJob }); + return; + } + } + var compressedResponse = await context.CallSubOrchestratorAsync(nameof(SubOrchestratorFunction), new GroupMembershipRequest { diff --git a/Service/GroupMembershipManagement/Models/Schemas/GroupMembershipSchema.json b/Service/GroupMembershipManagement/Models/Schemas/GroupMembershipSchema.json index 8751e7d22..a2e71bbd9 100644 --- a/Service/GroupMembershipManagement/Models/Schemas/GroupMembershipSchema.json +++ b/Service/GroupMembershipManagement/Models/Schemas/GroupMembershipSchema.json @@ -1,23 +1,20 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["GroupMembership"] - }, - "source": { - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", - "minLength": 36, - "maxLength": 36 - }, - "exclusionary": { - "type": "boolean" - } + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["GroupMembership"] }, - "required": ["type", "source"], - "additionalProperties": false - } -} + "source": { + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "minLength": 36, + "maxLength": 36 + }, + "exclusionary": { + "type": "boolean" + } + }, + "required": [ "type", "source" ], + "additionalProperties": false +} \ No newline at end of file From 8b8f626ef7227667eb5f1ec0e71d5a93e94bb039 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 16 Jul 2024 10:38:48 -0700 Subject: [PATCH 0080/1479] schema validation tests in GroupMembershipObtainer function --- .../Helpers/SchemaProviderFactory.cs | 56 ++++++++ .../Services.Tests/OrchestratorTests.cs | 124 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs new file mode 100644 index 000000000..beb3d7371 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs @@ -0,0 +1,56 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; +using System; +using System.IO; + +namespace Tests.Helpers +{ + internal static class SchemaProviderFactory + { + public static SchemaProvider CreateJsonSchemaProvider() + { + var jsonSchemaProvider = new SchemaProvider(); + var schemaFolderName = "Schemas"; + + var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; + var jsonSchemaDirectory = Path.Combine(currentDirectory, schemaFolderName); + + + if (jsonSchemaDirectory != null && Directory.Exists(jsonSchemaDirectory)) + { + var files = Directory.EnumerateFiles(jsonSchemaDirectory); + foreach (var file in files) + { + var fileName = Path.GetFileNameWithoutExtension(file); + jsonSchemaProvider.Schemas.Add((Schema)Enum.Parse(typeof(Schema), fileName), File.ReadAllText(file)); + } + } + + return jsonSchemaProvider; + } + + public static SchemaProvider CreateMissingGroupMembershipSchemaProvider() + { + var jsonSchemaProvider = new SchemaProvider(); + var schemaFolderName = "Schemas"; + + var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; + var jsonSchemaDirectory = Path.Combine(currentDirectory, schemaFolderName); + + if (jsonSchemaDirectory != null && Directory.Exists(jsonSchemaDirectory)) + { + var files = Directory.EnumerateFiles(jsonSchemaDirectory); + foreach (var file in files) + { + var fileName = Path.GetFileNameWithoutExtension(file); + if (fileName != "GroupMembershipSchema") jsonSchemaProvider.Schemas.Add((Schema)Enum.Parse(typeof(Schema), fileName), File.ReadAllText(file)); + } + } + + return jsonSchemaProvider; + } + + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs index 9ae06f11b..6c1bb2200 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs @@ -15,6 +15,7 @@ using Newtonsoft.Json; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; +using Tests.Helpers; using System; using System.Collections.Generic; using System.Linq; @@ -47,6 +48,8 @@ public class OrchestratorTests private SGMembershipCalculator _membershipCalculator; private DurableHttpResponse _membershipAgregatorResponse; private TelemetryClient _telemetryClient; + SchemaProvider _schemaProvider; + private bool _isValid = true; [TestInitialize] public void Setup() @@ -173,6 +176,14 @@ public void Setup() { await CallQueueMessageSenderFunctionAsync(request as MembershipAggregatorHttpRequest); }); + _schemaProvider = SchemaProviderFactory.CreateJsonSchemaProvider(); + + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => _isValid); } @@ -415,6 +426,114 @@ public async Task TestValidPartRequestAsync() } + [TestMethod] + public async Task TestMissingSchemasAsync() + { + _schemaProvider = new SchemaProvider(); + + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => _isValid); + + var orchestratorFunction = new OrchestratorFunction( + _loggingRepository.Object, + _membershipCalculator, + _configuration.Object, + _emailSenderRecipient.Object + ); + + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message == $"No json schemas have been loaded. Skipping schema validation."), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Once); + } + + [TestMethod] + public async Task TestMissingGroupMembershipSchemaAsync() + { + _schemaProvider = SchemaProviderFactory.CreateMissingGroupMembershipSchemaProvider(); + + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => _isValid); + + var orchestratorFunction = new OrchestratorFunction( + _loggingRepository.Object, + _membershipCalculator, + _configuration.Object, + _emailSenderRecipient.Object + ); + + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message == $"No GroupMembership schema has been loaded. Skipping schema validation."), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Once); + } + + [TestMethod] + public async Task TestInvalidSchemaAsync() + { + var orchestratorFunction = new OrchestratorFunction( + _loggingRepository.Object, + _membershipCalculator, + _configuration.Object, + _emailSenderRecipient.Object + ); + + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => false); + + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + _durableOrchestrationContext.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), + It.Is(x => x.Status == SyncStatus.SchemaError)), Times.Once()); + } + + [TestMethod] + public async Task TestJsonReaderExceptionAsync() + { + _schemaProvider = SchemaProviderFactory.CreateMissingGroupMembershipSchemaProvider(); + + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .ThrowsAsync(new JsonReaderException()); + + var orchestratorFunction = new OrchestratorFunction( + _loggingRepository.Object, + _membershipCalculator, + _configuration.Object, + _emailSenderRecipient.Object + ); + + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message.Contains("Source query is not valid for job")), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Once); + + _durableOrchestrationContext.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), + It.Is(x => x.Status == SyncStatus.QueryNotValid)), Times.Once()); + } + private async Task CallTelemetryTrackerFunctionAsync(TelemetryTrackerRequest request) { var telemetryTrackerFunction = new TelemetryTrackerFunction(_loggingRepository.Object, _telemetryClient); @@ -455,5 +574,10 @@ private async Task CallQueueMessageSenderFunctionAsync(MembershipAggregatorHttpR await function.SendMessageAsync(request); } + private async Task CallSchemaValidatorFunctionAsync(SchemaValidatorRequest request) + { + var function = new SchemaValidatorFunction(_loggingRepository.Object, _schemaProvider); + return await function.ValidateSchemasAsync(request); + } } } From 78bec4817d146250bda6ea836e2e887f46fd206f Mon Sep 17 00:00:00 2001 From: abgonz Date: Mon, 15 Jul 2024 12:00:33 -0700 Subject: [PATCH 0081/1479] Add PendingSyncJobChanges repository, migrations, models, and set up --- .../Hosts.FunctionBase/CommonStartup.cs | 1 + .../Hosts/WebApi/WebApi/Program.cs | 1 + .../Models/PendingSyncJobChange.cs | 43 ++ .../Models/ReviewStatus.cs | 13 + ...DatabasePendingSyncJobChangesRepository.cs | 18 + .../GMMContext.cs | 5 + ...pending_sync_job_changes_table.Designer.cs | 628 ++++++++++++++++++ ...5207_add_pending_sync_job_changes_table.cs | 37 ++ .../Migrations/GMMContextModelSnapshot.cs | 31 +- ...DatabasePendingSyncJobChangesRepository.cs | 48 ++ 10 files changed, 823 insertions(+), 2 deletions(-) create mode 100644 Service/GroupMembershipManagement/Models/PendingSyncJobChange.cs create mode 100644 Service/GroupMembershipManagement/Models/ReviewStatus.cs create mode 100644 Service/GroupMembershipManagement/Repositories.Contracts/IDatabasePendingSyncJobChangesRepository.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240715185207_add_pending_sync_job_changes_table.Designer.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240715185207_add_pending_sync_job_changes_table.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework/DatabasePendingSyncJobChangesRepository.cs diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs index edfa1dc85..f3ad7861b 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs @@ -124,6 +124,7 @@ public override void Configure(IFunctionsHostBuilder builder) builder.Services.AddSingleton(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs index 5aedab2d9..345f69afd 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs @@ -277,6 +277,7 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Service/GroupMembershipManagement/Models/PendingSyncJobChange.cs b/Service/GroupMembershipManagement/Models/PendingSyncJobChange.cs new file mode 100644 index 000000000..d95cc1bae --- /dev/null +++ b/Service/GroupMembershipManagement/Models/PendingSyncJobChange.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using System; + +namespace Models +{ + public class PendingSyncJobChange + { + /// + /// Gets or sets an id representing the PendingSyncJobChange. + /// + public Guid Id { get; set; } + /// + /// Gets or sets the id of the syncjob in the SyncJobs table. + /// + public Guid? SyncJobId { get; set; } + /// + /// Gets or sets the UTC date and time of when the change occurred. + /// The default time is the current UTC date and time. + /// + public DateTime ChangeTime { get; set; } + /// + /// Gets or sets the userId of the responsible for the change. + /// + public Guid RequestorUserId { get; set; } + /// + /// Gets or sets the alias of the responsible for the change. + /// + public string RequestorUPN { get; set; } + /// + /// Gets or sets the review status. + /// + public ReviewStatus Status { get; set; } + /// + /// Gets or sets the details of the change. + /// + /// + /// This is a JSON string that can be deserialized into + /// a object. + /// + public string ChangeDetails { get; set; } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Models/ReviewStatus.cs b/Service/GroupMembershipManagement/Models/ReviewStatus.cs new file mode 100644 index 000000000..536c06e53 --- /dev/null +++ b/Service/GroupMembershipManagement/Models/ReviewStatus.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Models +{ + public enum ReviewStatus + { + NotSet = 0, + PendingReview = 1, + SubmissionApproved = 2, + SubmissionRejected = 3 + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/IDatabasePendingSyncJobChangesRepository.cs b/Service/GroupMembershipManagement/Repositories.Contracts/IDatabasePendingSyncJobChangesRepository.cs new file mode 100644 index 000000000..88110f3d2 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.Contracts/IDatabasePendingSyncJobChangesRepository.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Repositories.Contracts +{ + public interface IDatabasePendingSyncJobChangesRepository + { + Task InsertPendingSyncJobChangeAsync(PendingSyncJobChange pendingSyncJobChange); + Task> GetAllPendingSyncJobChangesAsync(); + Task> GetActivePendingSyncJobChangesAsync(); + Task GetPendingSyncJobChangeByObjectIdAsync(Guid objectId); + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs index 9fffbb202..9be3f8489 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs @@ -14,6 +14,7 @@ public class GMMContext : DbContext { public DbSet SyncJobs { get; set; } = null!; public DbSet PurgedSyncJobs { get; set; } = null!; + public DbSet PendingSyncJobChanges { get; set; } = null!; public DbSet Statuses { get; set; } = null!; public DbSet Settings { get; set; } = null!; public DbSet SqlMembershipSources { get; set; } = null!; @@ -33,6 +34,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().Property(p => p.Id) .ValueGeneratedOnAdd() .HasDefaultValueSql("NEWID()"); + + modelBuilder.Entity().Property(p => p.Id) + .ValueGeneratedOnAdd() + .HasDefaultValueSql("NEWSEQUENTIALID()"); modelBuilder.Entity(entity => { diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240715185207_add_pending_sync_job_changes_table.Designer.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240715185207_add_pending_sync_job_changes_table.Designer.cs new file mode 100644 index 000000000..d230a1d61 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240715185207_add_pending_sync_job_changes_table.Designer.cs @@ -0,0 +1,628 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Repositories.EntityFramework.Contexts; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + [DbContext(typeof(GMMContext))] + [Migration("20240715185207_add_pending_sync_job_changes_table")] + partial class add_pending_sync_job_changes_table + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.22") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.Property("DestinationOwnersId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("DestinationOwnersId", "SyncJobsId"); + + b.HasIndex("SyncJobsId"); + + b.ToTable("DestinationOwnerSyncJob"); + }); + + modelBuilder.Entity("Entities.SqlMembershipSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Attributes") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomLabel") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SqlMembershipSources"); + + b.HasData( + new + { + Id = new Guid("3eeff080-d199-4876-bff8-8426fca6909c"), + Name = "SqlMembership" + }); + }); + + modelBuilder.Entity("Entities.SyncJobChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("ChangeDetails") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeSource") + .HasColumnType("int"); + + b.Property("ChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValue(new DateTime(2024, 7, 15, 18, 52, 7, 209, DateTimeKind.Utc).AddTicks(2460)); + + b.Property("ChangedByDisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedByObjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ChangeSource"); + + b.HasIndex("ChangeTime"); + + b.HasIndex("ChangedByObjectId"); + + b.HasIndex("SyncJobId"); + + b.ToTable("SyncJobChanges"); + }); + + modelBuilder.Entity("Entities.ThresholdNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("CardStateName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ChangePercentageForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("float") + .HasDefaultValue(0.0); + + b.Property("ChangePercentageForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("float") + .HasDefaultValue(0.0); + + b.Property("ChangeQuantityForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ChangeQuantityForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("CreatedTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("LastUpdatedTime") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("ResolutionName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ResolvedBy") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ResolvedTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("StatusName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(100); + + b.Property("ThresholdPercentageForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(20); + + b.HasKey("Id"); + + b.HasIndex("SyncJobId"); + + b.ToTable("ThresholdNotifications"); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("DestinationNames"); + }); + + modelBuilder.Entity("Models.DestinationOwner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("ObjectId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.ToTable("DestinationOwners"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("NotificationTypeID") + .HasColumnType("int"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("NotificationTypeID"); + + b.HasIndex("SyncJobId", "NotificationTypeID") + .IsUnique(); + + b.ToTable("JobNotifications"); + }); + + modelBuilder.Entity("Models.NotificationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .IsUnicode(false) + .HasColumnType("varchar(max)"); + + b.HasKey("Id"); + + b.ToTable("NotificationTypes"); + + b.HasData( + new + { + Id = 1, + Disabled = false, + Name = "ThresholdNotification" + }, + new + { + Id = 2, + Disabled = false, + Name = "SyncStartedNotification" + }, + new + { + Id = 3, + Disabled = false, + Name = "SyncCompletedNotification" + }, + new + { + Id = 4, + Disabled = false, + Name = "DestinationNotExistNotification" + }, + new + { + Id = 5, + Disabled = false, + Name = "SourceNotExistNotification" + }, + new + { + Id = 6, + Disabled = false, + Name = "NotOwnerNotification" + }, + new + { + Id = 7, + Disabled = false, + Name = "NotValidSourceNotification" + }, + new + { + Id = 8, + Disabled = false, + Name = "NoDataNotification" + }, + new + { + Id = 9, + Disabled = false, + Name = "NormalThresholdNotification" + }, + new + { + Id = 10, + Disabled = false, + Name = "InactiveSyncJobNotification" + }); + }); + + modelBuilder.Entity("Models.PendingSyncJobChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("ChangeDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeTime") + .HasColumnType("datetime2"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.ToTable("PendingSyncJobChanges"); + }); + + modelBuilder.Entity("Models.PurgedSyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("PurgedAt") + .HasColumnType("datetime2"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PurgedSyncJobs"); + }); + + modelBuilder.Entity("Models.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("SettingKey") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SettingValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SettingKey") + .IsUnique(); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("Models.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SortPriority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Statuses", (string)null); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(450)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Status") + .IsUnique() + .HasFilter("[Status] IS NOT NULL"); + + b.ToTable("SyncJobs"); + }); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.HasOne("Models.DestinationOwner", null) + .WithMany() + .HasForeignKey("DestinationOwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Entities.ThresholdNotification", b => + { + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.HasOne("Models.SyncJob", "SyncJob") + .WithOne("DestinationName") + .HasForeignKey("Models.DestinationName", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.HasOne("Models.NotificationType", "NotificationType") + .WithMany() + .HasForeignKey("NotificationTypeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", "SyncJob") + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationType"); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.HasOne("Models.Status", "StatusDetails") + .WithOne() + .HasForeignKey("Models.SyncJob", "Status") + .HasPrincipalKey("Models.Status", "Name") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("StatusDetails"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Navigation("DestinationName"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240715185207_add_pending_sync_job_changes_table.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240715185207_add_pending_sync_job_changes_table.cs new file mode 100644 index 000000000..566cc128a --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240715185207_add_pending_sync_job_changes_table.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + public partial class add_pending_sync_job_changes_table : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PendingSyncJobChanges", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"), + SyncJobId = table.Column(type: "uniqueidentifier", nullable: true), + ChangeTime = table.Column(type: "datetime2", nullable: false), + RequestorUserId = table.Column(type: "uniqueidentifier", nullable: false), + RequestorUPN = table.Column(type: "nvarchar(max)", nullable: false), + Status = table.Column(type: "int", nullable: false), + ChangeDetails = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PendingSyncJobChanges", x => x.Id); + }); + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PendingSyncJobChanges"); + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs index 059044d68..9b57c946f 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs @@ -64,7 +64,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasData( new { - Id = new Guid("dee1f40f-f1f1-4887-9d57-bd5d6cf7669c"), + Id = new Guid("3eeff080-d199-4876-bff8-8426fca6909c"), Name = "SqlMembership" }); }); @@ -90,7 +90,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ChangeTime") .ValueGeneratedOnAdd() .HasColumnType("datetime2") - .HasDefaultValue(new DateTime(2024, 7, 9, 18, 15, 47, 251, DateTimeKind.Utc).AddTicks(4032)); + .HasDefaultValue(new DateTime(2024, 7, 15, 18, 52, 7, 209, DateTimeKind.Utc).AddTicks(2460)); b.Property("ChangedByDisplayName") .IsRequired() @@ -343,6 +343,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("Models.PendingSyncJobChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("ChangeDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeTime") + .HasColumnType("datetime2"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.ToTable("PendingSyncJobChanges"); + }); + modelBuilder.Entity("Models.PurgedSyncJob", b => { b.Property("Id") diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework/DatabasePendingSyncJobChangesRepository.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework/DatabasePendingSyncJobChangesRepository.cs new file mode 100644 index 000000000..fc662daf8 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework/DatabasePendingSyncJobChangesRepository.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Models; +using Repositories.Contracts; +using Repositories.EntityFramework.Contexts; +using System; + +namespace Repositories.EntityFramework +{ + public class DatabasePendingSyncJobChangesRepository : IDatabasePendingSyncJobChangesRepository + { + private readonly GMMContext _writeContext; + private readonly GMMReadContext _readContext; + + public DatabasePendingSyncJobChangesRepository(GMMContext writeContext, GMMReadContext readContext) + { + _writeContext = writeContext ?? throw new ArgumentNullException(nameof(writeContext)); + _readContext = readContext ?? throw new ArgumentNullException(nameof(readContext)); + } + + public async Task InsertPendingSyncJobChangeAsync(PendingSyncJobChange pendingSyncJobChange) + { + var entry = await _writeContext.Set().AddAsync(pendingSyncJobChange); + await _writeContext.SaveChangesAsync(); + return entry.Entity.Id; + } + + public async Task> GetAllPendingSyncJobChangesAsync() + { + return await _readContext.PendingSyncJobChanges + .ToListAsync(); + } + + public async Task> GetActivePendingSyncJobChangesAsync() + { + return await _readContext.PendingSyncJobChanges + .Where(job => job.Status != ReviewStatus.SubmissionRejected) + .ToListAsync(); + } + + public async Task GetPendingSyncJobChangeByObjectIdAsync(Guid objectId) + { + return await _readContext.PendingSyncJobChanges.FromSqlRaw(@"SELECT * FROM [dbo].[PendingSyncJobChanges] WHERE JSON_VALUE(Destination, '$[0].value.objectId') = {0}", objectId.ToString()).FirstOrDefaultAsync(); + } + } +} \ No newline at end of file From 715af84068283443645edb880771156f86dc8939 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Wed, 17 Jul 2024 06:48:08 -0700 Subject: [PATCH 0082/1479] schema validation check in GroupOwnershipObtainer function --- .../SchemaValidatorFunction.cs | 70 +++++++++++++++++++ .../SchemaValidator/SchemaValidatorRequest.cs | 12 ++++ .../Function/GroupOwnershipObtainer.csproj | 1 + .../Orchestrator/OrchestratorFunction.cs | 25 +++++++ .../Models/Schemas/GroupOwnershipSchema.json | 21 +----- 5 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs create mode 100644 Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs new file mode 100644 index 000000000..3ac027b0c --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs @@ -0,0 +1,70 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Models; +using Repositories.Contracts; +using System; +using System.Threading.Tasks; +using NJsonSchema; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Hosts.GroupOwnershipObtainer +{ + public class SchemaValidatorFunction + { + private readonly ILoggingRepository _loggingRepository = null; + private readonly SchemaProvider _schemaProvider = null; + + public SchemaValidatorFunction(ILoggingRepository loggingRepository, SchemaProvider schemaProvider) + { + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + _schemaProvider = schemaProvider ?? throw new ArgumentNullException(nameof(schemaProvider)); + } + + [FunctionName(nameof(SchemaValidatorFunction))] + public async Task ValidateSchemasAsync([ActivityTrigger] SchemaValidatorRequest request) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); + + var isValidJson = true; + + if (_schemaProvider.Schemas.Count == 0) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = request.RunId, + Message = $"No json schemas have been loaded. Skipping schema validation." + }); + + return isValidJson; + } + + if (_schemaProvider.Schemas.TryGetValue(Schema.GroupOwnershipSchema, out string value)) + { + var schema = await JsonSchema.FromJsonAsync(value); + var errors = schema.Validate(request.Query); + if (errors.Count > 0) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Query not valid: {errors}", RunId = request.RunId }, VerbosityLevel.DEBUG); + isValidJson = false; + } + } + + else + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + RunId = request.RunId, + Message = $"No GroupOwnership schema has been loaded. Skipping schema validation." + }); + + return isValidJson; + } + + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); + return isValidJson; + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs new file mode 100644 index 000000000..27907ba66 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorRequest.cs @@ -0,0 +1,12 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. +using System; + +namespace Hosts.GroupOwnershipObtainer +{ + public class SchemaValidatorRequest + { + public string Query { get; set; } + public Guid? RunId { get; set; } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/GroupOwnershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/GroupOwnershipObtainer.csproj index 332be832a..45dff0620 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/GroupOwnershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/GroupOwnershipObtainer.csproj @@ -7,6 +7,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Orchestrator/OrchestratorFunction.cs index d6ee12d87..bc47e598a 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/Orchestrator/OrchestratorFunction.cs @@ -91,6 +91,31 @@ await context.CallActivityAsync( return; } + else + { + try + { + var hasValidJson = await context.CallActivityAsync(nameof(SchemaValidatorFunction), new SchemaValidatorRequest { Query = currentPart.ToString(), RunId = syncJob.RunId }); + if (!hasValidJson) + { + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { Status = SyncStatus.SchemaError, SyncJob = syncJob }); + return; + } + } + catch (JsonReaderException) + { + await context.CallActivityAsync(nameof(LoggerFunction), + new LoggerRequest + { + SyncJob = syncJob, + Message = $"Source query is not valid for job:{syncJob.Id}" + }); + + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { Status = SyncStatus.QueryNotValid, SyncJob = syncJob }); + return; + } + } + var syncJobs = new List(); var segmentResponse = await context.CallActivityAsync>(nameof(GetJobsSegmentedFunction), new GetJobsSegmentedRequest { RunId = syncJob.RunId }); syncJobs.AddRange(segmentResponse); diff --git a/Service/GroupMembershipManagement/Models/Schemas/GroupOwnershipSchema.json b/Service/GroupMembershipManagement/Models/Schemas/GroupOwnershipSchema.json index 029c95888..4d5539792 100644 --- a/Service/GroupMembershipManagement/Models/Schemas/GroupOwnershipSchema.json +++ b/Service/GroupMembershipManagement/Models/Schemas/GroupOwnershipSchema.json @@ -13,25 +13,7 @@ "items": { "type": "string", "enum": ["All", "Hybrid", "GroupMembership"] - }, - "oneOf": [ - { - "contains": { "const": "All" }, - "not": { "contains": { "const": "Hybrid" } } - }, - { - "contains": { "const": "Hybrid" }, - "not": { "contains": { "const": "All" } } - }, - { - "not": { - "anyOf": [ - { "contains": { "const": "All" } }, - { "contains": { "const": "Hybrid" } } - ] - } } - ] }, "exclusionary": { "type": "boolean" @@ -39,5 +21,4 @@ }, "required": ["type", "source"], "additionalProperties": false -} - \ No newline at end of file +} \ No newline at end of file From a02760d94367cb53fdda2e8a82b52673aafe90ba Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Wed, 17 Jul 2024 06:48:30 -0700 Subject: [PATCH 0083/1479] schema validation tests in GroupOwnershipObtainer function --- .../Helpers/SchemaProviderFactory.cs | 32 ++++++++++ .../Services.Tests/OrchestratorTests.cs | 62 ++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs new file mode 100644 index 000000000..6467b97da --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/Helpers/SchemaProviderFactory.cs @@ -0,0 +1,32 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; + +namespace Services.Tests.Helpers +{ + internal static class SchemaProviderFactory + { + public static SchemaProvider CreateJsonSchemaProvider() + { + var jsonSchemaProvider = new SchemaProvider(); + var schemaFolderName = "Schemas"; + + var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; + var jsonSchemaDirectory = Path.Combine(currentDirectory, schemaFolderName); + + + if (jsonSchemaDirectory != null && Directory.Exists(jsonSchemaDirectory)) + { + var files = Directory.EnumerateFiles(jsonSchemaDirectory); + foreach (var file in files) + { + var fileName = Path.GetFileNameWithoutExtension(file); + jsonSchemaProvider.Schemas.Add((Schema)Enum.Parse(typeof(Schema), fileName), File.ReadAllText(file)); + } + } + + return jsonSchemaProvider; + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/OrchestratorTests.cs index fe542a293..4eef6b9cb 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/OrchestratorTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using DIConcreteTypes; using Hosts.GroupOwnershipObtainer; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; @@ -15,6 +16,7 @@ using Repositories.Contracts.InjectConfig; using Services.Contracts; using Services.Entities; +using Services.Tests.Helpers; namespace Services.Tests { @@ -31,7 +33,8 @@ public class OrchestratorTests private Mock _serviceBusQueueRepository = null!; private Mock _durableOrchestrationContext = null!; private Mock _configurationRefresherProvider = null!; - + SchemaProvider _schemaProvider; + private bool _isValid = true; private List _sampleSyncJobs = null!; private SyncJob _groupOwnershipObtainerSyncJob = null!; private TelemetryClient _telemetryClient = null!; @@ -187,6 +190,14 @@ public void Setup() { await CallQueueMessageSenderFunctionAsync((MembershipAggregatorHttpRequest)request); }); + _schemaProvider = SchemaProviderFactory.CreateJsonSchemaProvider(); + + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => _isValid); } [TestMethod] @@ -373,6 +384,49 @@ public async Task TestGraphAPITimeoutExceptionAsync() It.Is(s => s == SyncStatus.Idle)), Times.Once); } + [TestMethod] + public async Task TestInvalidSchemaAsync() + { + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => false); + + var orchestratorFunction = new OrchestratorFunction(_configuration.Object); + + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); + + _syncJobRepository.Verify(x => x.UpdateSyncJobStatusAsync( + It.IsAny>(), + SyncStatus.SchemaError), Times.Once); + } + + [TestMethod] + public async Task TestMissingSchemasAsync() + { + _schemaProvider = new SchemaProvider(); + + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + .Callback(async (name, request) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => _isValid); + + var orchestratorFunction = new OrchestratorFunction(_configuration.Object); + + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); + + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message == $"No json schemas have been loaded. Skipping schema validation."), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Once); + } + private async Task CallLoggerFunctionAsync(LoggerRequest request) { var function = new LoggerFunction(_loggingRepository.Object); @@ -420,5 +474,11 @@ private async Task CallQueueMessageSenderFunctionAsync(MembershipAggregatorHttpR var function = new QueueMessageSenderFunction(_loggingRepository.Object, _serviceBusQueueRepository.Object); await function.SendMessageAsync(request); } + + private async Task CallSchemaValidatorFunctionAsync(SchemaValidatorRequest request) + { + var function = new SchemaValidatorFunction(_loggingRepository.Object, _schemaProvider); + return await function.ValidateSchemasAsync(request); + } } } From d19a876df2acb8601bf07ed1e9b9dd7f07eaab73 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Thu, 18 Jul 2024 12:53:53 -0700 Subject: [PATCH 0084/1479] display filter text field if query includes sql expressions --- .../src/components/HRQuerySource/HRQuerySource.base.tsx | 9 ++++++++- .../src/components/HRQuerySource/QuerySerializer.ts | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 6d5174a82..9c9a1e695 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -26,7 +26,7 @@ import { selectIsJobWriter } from '../../store/roles.slice'; import { SqlMembershipAttribute, SqlMembershipAttributeValue } from '../../models'; import { IFilterPart } from '../../models/IFilterPart'; import { Group } from '../../models/Group'; -import { parseGroup, stringifyGroups } from './QuerySerializer'; +import { containsSqlExpression, parseGroup, stringifyGroups } from './QuerySerializer'; export const getClassNames = classNamesFunction(); @@ -214,6 +214,13 @@ const checkType = (value: string, type: string | undefined): string => { }; useEffect(() => { + if (props.source.filter) { + if (containsSqlExpression(props.source.filter)) { + setFilterTextEnabled(true); + return; + } + } + if (props.source.filter && !groupingEnabled && (props.source.filter.includes("(") || props.source.filter.includes(")"))) { const groups = parseGroup(props.source.filter); if (groups.length <= 0) { diff --git a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts index 7566fab58..76f13f2ea 100644 --- a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts +++ b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts @@ -1,6 +1,12 @@ import { Group } from "../../models/Group"; import { IFilterPart } from "../../models/IFilterPart"; +export function containsSqlExpression(filter: string): boolean { + const sqlExpressions = ['IN', 'BETWEEN', 'NOT', 'LIKE', 'IS NULL', 'IS NOT NULL']; + const regex = new RegExp(`\\b(${sqlExpressions.join('|')})\\b`, 'i'); + return regex.test(filter); +}; + export function stringifyGroup(group: Group, isChild?: boolean, childIndex?: number, childrenLength?: number): string { let result = '('; From ddc285b1aeb4d1ff67c3dce92b6f4f2192baf2db Mon Sep 17 00:00:00 2001 From: abgonz Date: Thu, 18 Jul 2024 10:15:32 -0700 Subject: [PATCH 0085/1479] Preserve list filter state using local storage --- .../JobsListFilter/JobsListFilter.base.tsx | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx b/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx index d2763de39..5cc7d1a6f 100644 --- a/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx +++ b/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx @@ -36,6 +36,17 @@ import { const getClassNames = classNamesFunction(); +const FILTER_STATE_STORAGE_KEY = 'jobsListFilterState'; + +const saveFilterStateToLocalStorage = (state: any) => { + localStorage.setItem(FILTER_STATE_STORAGE_KEY, JSON.stringify(state)); +}; + +const loadFilterStateFromLocalStorage = () => { + const state = localStorage.getItem(FILTER_STATE_STORAGE_KEY); + return state ? JSON.parse(state) : null; +}; + export const JobsListFilterBase: React.FunctionComponent = (props: IJobsListFilterProps) => { const { className, @@ -128,8 +139,28 @@ export const JobsListFilterBase: React.FunctionComponent = const dispatch = useDispatch(); useEffect(() => { + const savedFilterState = loadFilterStateFromLocalStorage(); + if (savedFilterState) { + setDestinationId(savedFilterState.destinationId); + setStatusSelectedItem(savedFilterState.statusSelectedItem); + setActionRequiredSelectedItem(savedFilterState.actionRequiredSelectedItem); + setDestinationTypeSelectedItem(savedFilterState.destinationTypeSelectedItem); + setDestinationName(savedFilterState.destinationName); + setSelectedOwners(savedFilterState.selectedOwners); + } + }, []); - }, [dispatch, ownerPickerSuggestions]); + useEffect(() => { + const filterState = { + destinationId, + statusSelectedItem, + actionRequiredSelectedItem, + destinationTypeSelectedItem, + destinationName, + selectedOwners, + }; + saveFilterStateToLocalStorage(filterState); + }, [destinationId, statusSelectedItem, actionRequiredSelectedItem, destinationTypeSelectedItem, destinationName, selectedOwners]); const handleIdChanged = (event: React.FormEvent, newValue?: string): void => { const inputGuid = newValue || ''; @@ -210,6 +241,8 @@ export const JobsListFilterBase: React.FunctionComponent = setIdValidationErrorMessage(undefined); setActionRequiredSelectedItem(actionRequiredDropdownOptions[0]); setStatusSelectedItem(statusDropdownOptions[0]); + + localStorage.removeItem(FILTER_STATE_STORAGE_KEY); }; const getPickerSuggestions = async ( From 354746d4109546fd717be636f906c86a416d92a7 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 1 Jul 2024 14:53:58 -0700 Subject: [PATCH 0086/1479] Updated Notifier to support a new NotificationType.GuestUserFailureNotification --- .../Orchestrator/OrchestratorFunction.cs | 24 +++++++++++++++---- .../Orchestrator/OrchestratorFunction.cs | 6 +++++ .../Models/Notifications/NotificationType.cs | 1 + .../LocalizationRepository.en-US.resx | 7 ++++++ .../NotificationConstants.cs | 1 + 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Orchestrator/OrchestratorFunction.cs index c7ca10927..46f17306f 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Orchestrator/OrchestratorFunction.cs @@ -158,6 +158,25 @@ await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), RunId = syncJob.RunId }); + var groupName = await context.CallActivityAsync(nameof(GroupNameReaderFunction), + new GroupNameReaderRequest { RunId = groupMembership.RunId, GroupId = groupMembership.Destination.ObjectId }); + + var additionalContent = new[] + { + groupMembership.Destination.ObjectId.ToString(), + groupName, + membersAddedResponse.SuccessCount.ToString(), + membersRemovedResponse.SuccessCount.ToString(), + }; + + await context.CallActivityAsync(nameof(EmailSenderFunction), + new EmailSenderRequest + { + SyncJob = syncJob, + NotificationType = NotificationMessageType.GuestUserFailureNotification, + AdditionalContentParams = additionalContent + }); + TrackSyncCompleteEvent(context, syncJob, syncCompleteEvent, "Failure"); if (syncJob?.RunId.HasValue ?? false) @@ -173,11 +192,6 @@ await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), var groupName = await context.CallActivityAsync(nameof(GroupNameReaderFunction), new GroupNameReaderRequest { RunId = groupMembership.RunId, GroupId = groupMembership.Destination.ObjectId }); - var groupOwners = await context.CallActivityAsync>(nameof(GroupOwnersReaderFunction), - new GroupOwnersReaderRequest { RunId = groupMembership.RunId, GroupId = groupMembership.Destination.ObjectId }); - - var ownerEmails = string.Join(";", groupOwners.Where(x => !string.IsNullOrWhiteSpace(x.Mail)).Select(x => x.Mail)); - var additionalContent = new[] { groupMembership.Destination.ObjectId.ToString(), diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs index ab9d257ce..f9656ddee 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs @@ -100,6 +100,12 @@ await context.CallActivityAsync(nameof(LoggerFunction), await context.CallActivityAsync(nameof(SendNotification), message); break; + case nameof(NotificationMessageType.GuestUserFailureNotification): + message.SubjectTemplate = NotificationConstants.DisabledNotificationSubject; + message.ContentTemplate = NotificationConstants.GuestUserFailureEmailBody; + await context.CallActivityAsync(nameof(SendNotification), message); + break; + default: await context.CallActivityAsync(nameof(LoggerFunction), new LoggerRequest diff --git a/Service/GroupMembershipManagement/Models/Notifications/NotificationType.cs b/Service/GroupMembershipManagement/Models/Notifications/NotificationType.cs index 96d35746d..9e1f75651 100644 --- a/Service/GroupMembershipManagement/Models/Notifications/NotificationType.cs +++ b/Service/GroupMembershipManagement/Models/Notifications/NotificationType.cs @@ -12,5 +12,6 @@ public enum NotificationMessageType NoDataNotification = 7, NormalThresholdNotification = 8, InactiveSyncJobNotification = 9, + GuestUserFailureNotification = 10, } } diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index dfde50971..d83fea13a 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -1149,4 +1149,11 @@ The group sync will be PAUSED until your response is received. Sync Job Paused + + You received this notification because you are listed as an owner of **{1}**. + +The group **{1}** with Id: {0} **has been set to the status "GuestUsersCannotBeAddedToUnifiedGroup"** because GMM failed to add guest users to the destination, due to the destination not having the right settings to allow guest users to be added by GMM. + +Even though GMM couldn't add the guest users, it still **added the remaining {2} users** and **removed the expected {3} users** from the destination. + \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs b/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs index a0d245d16..31cb6f7ab 100644 --- a/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs +++ b/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs @@ -26,5 +26,6 @@ public static class NotificationConstants public const string SyncThresholdDisablingJobEmailSubject = "SyncThresholdDisablingJobEmailSubject"; public const string SyncDisabledInactivityEmailBody = "SyncDisabledInactivityEmailBody"; public const string SyncDisabledInactivityEmailSubject = "SyncDisabledInactivityEmailSubject"; + public const string GuestUserFailureEmailBody = "GuestUserFailureEmailBody"; } } From 7a7491414a64d056c465ad70b5f67066892a2ab4 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 1 Jul 2024 15:17:05 -0700 Subject: [PATCH 0087/1479] Removed unnecessary await operators from NotifierService --- .../Hosts/Notifier/Services.Notifier/NotifierService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index 4417af7e2..836a084c8 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -174,7 +174,7 @@ private void TrackSentNotificationEvent(Guid groupId) var notification = await CreateActionableNotification(threshold, job, sendDisableJobNotification); return notification; } - private async Task<(SyncJob job, string[] additionalContentParameters)> ParseMessageContentAsync(string messageBody) + private (SyncJob job, string[] additionalContentParameters) ParseMessageContentAsync(string messageBody) { var messageContent = JsonSerializer.Deserialize>(messageBody); @@ -194,7 +194,7 @@ private void TrackSentNotificationEvent(Guid groupId) } public async Task SendEmailAsync(string messageType, string messageBody, string subjectTemplate, string contentTemplate) { - var (job, additionalContentParameters) = await ParseMessageContentAsync(messageBody); + var (job, additionalContentParameters) = ParseMessageContentAsync(messageBody); if (!Enum.TryParse(messageType, true, out var messageTypeEnum)) { @@ -221,7 +221,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage if (!NotificationConstants.DestinationNotExistContent.Equals(contentTemplate, StringComparison.InvariantCultureIgnoreCase)) { - var destinationObjectId = (await ParseDestinationAsync(job)).ObjectId; + var destinationObjectId = ParseDestinationAsync(job).ObjectId; var owners = await _graphGroupRepository.GetGroupOwnersAsync(destinationObjectId); ownerEmails = string.Join(";", owners.Where(x => !string.IsNullOrWhiteSpace(x.Mail)).Select(x => x.Mail)); } @@ -291,7 +291,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage return await _jobNotificationRepository.IsNotificationDisabledForJobAsync(jobId, notificationType.Id); } - public async Task ParseDestinationAsync(SyncJob syncJob) + public AzureADGroup ParseDestinationAsync(SyncJob syncJob) { if (string.IsNullOrWhiteSpace(syncJob.Destination)) return null; From bd6aeaa3fe0594600bf80c404fff1dd01f9cc6bc Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 2 Jul 2024 10:20:28 -0700 Subject: [PATCH 0088/1479] Fixed tests for Orchestrator --- .../GraphUpdater/Services.Tests/OrchestratorTests.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs index 6c28ca848..21b7bf25f 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs @@ -536,12 +536,8 @@ public async Task RunOrchestratorExceptionTest() var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(syncJob); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(await DownloadFileAsync(fileDownloaderRequest, mockLoggingRepo, blobStorageRepository)); context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(true); JobStatusUpdaterRequest updateJobRequest = null; context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) @@ -550,15 +546,11 @@ public async Task RunOrchestratorExceptionTest() updateJobRequest = request as JobStatusUpdaterRequest; }); - context.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny())) - .Returns(() => Task.FromResult(new GroupUpdaterSubOrchestratorResponse() { SuccessCount = 1, UsersNotFound = new List(), UsersAlreadyExist = new List() })); - var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); await Assert.ThrowsExceptionAsync(async () => await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object)); Assert.IsFalse(mockLoggingRepo.MessagesLogged.Any(x => x.Message == nameof(OrchestratorFunction) + " function completed")); Assert.IsTrue(mockLoggingRepo.MessagesLogged.Any(x => x.Message.Contains("Caught unexpected exception, marking sync job as errored."))); - Assert.AreEqual(SyncStatus.Error, updateJobRequest.Status); var logProperties = mockLoggingRepo.SyncJobPropertiesHistory[syncJob.RunId.Value].Properties; From 7bd1156c66d4bcecf0117ca898ba6c31c4c77ef0 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Fri, 19 Jul 2024 11:56:21 -0700 Subject: [PATCH 0089/1479] Created migration for adding GuestUserFailureNotification to Sql NotificationTypes table --- ...27_notification_type_add_guest.Designer.cs | 637 ++++++++++++++++++ ...40719185127_notification_type_add_guest.cs | 26 + .../Migrations/GMMContextModelSnapshot.cs | 17 +- 3 files changed, 676 insertions(+), 4 deletions(-) create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240719185127_notification_type_add_guest.Designer.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240719185127_notification_type_add_guest.cs diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240719185127_notification_type_add_guest.Designer.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240719185127_notification_type_add_guest.Designer.cs new file mode 100644 index 000000000..764bce699 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240719185127_notification_type_add_guest.Designer.cs @@ -0,0 +1,637 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Repositories.EntityFramework.Contexts; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + [DbContext(typeof(GMMContext))] + [Migration("20240719185127_notification_type_add_guest")] + partial class notification_type_add_guest + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.22") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.Property("DestinationOwnersId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("DestinationOwnersId", "SyncJobsId"); + + b.HasIndex("SyncJobsId"); + + b.ToTable("DestinationOwnerSyncJob"); + }); + + modelBuilder.Entity("Entities.SqlMembershipSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Attributes") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomLabel") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SqlMembershipSources"); + + b.HasData( + new + { + Id = new Guid("f6476897-8d60-4ead-8dce-475c511a4eab"), + Name = "SqlMembership" + }); + }); + + modelBuilder.Entity("Entities.SyncJobChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("ChangeDetails") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeSource") + .HasColumnType("int"); + + b.Property("ChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValue(new DateTime(2024, 7, 19, 18, 51, 27, 252, DateTimeKind.Utc).AddTicks(6194)); + + b.Property("ChangedByDisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedByObjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ChangeSource"); + + b.HasIndex("ChangeTime"); + + b.HasIndex("ChangedByObjectId"); + + b.HasIndex("SyncJobId"); + + b.ToTable("SyncJobChanges"); + }); + + modelBuilder.Entity("Entities.ThresholdNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("CardStateName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ChangePercentageForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("float") + .HasDefaultValue(0.0); + + b.Property("ChangePercentageForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("float") + .HasDefaultValue(0.0); + + b.Property("ChangeQuantityForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ChangeQuantityForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("CreatedTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("LastUpdatedTime") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("ResolutionName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ResolvedBy") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ResolvedTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("StatusName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(100); + + b.Property("ThresholdPercentageForRemovals") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(20); + + b.HasKey("Id"); + + b.HasIndex("SyncJobId"); + + b.ToTable("ThresholdNotifications"); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("DestinationNames"); + }); + + modelBuilder.Entity("Models.DestinationOwner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("ObjectId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.ToTable("DestinationOwners"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("NotificationTypeID") + .HasColumnType("int"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("NotificationTypeID"); + + b.HasIndex("SyncJobId", "NotificationTypeID") + .IsUnique(); + + b.ToTable("JobNotifications"); + }); + + modelBuilder.Entity("Models.NotificationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .IsUnicode(false) + .HasColumnType("varchar(max)"); + + b.HasKey("Id"); + + b.ToTable("NotificationTypes"); + + b.HasData( + new + { + Id = 1, + Disabled = false, + Name = "ThresholdNotification" + }, + new + { + Id = 2, + Disabled = false, + Name = "SyncStartedNotification" + }, + new + { + Id = 3, + Disabled = false, + Name = "SyncCompletedNotification" + }, + new + { + Id = 4, + Disabled = false, + Name = "DestinationNotExistNotification" + }, + new + { + Id = 5, + Disabled = false, + Name = "SourceNotExistNotification" + }, + new + { + Id = 6, + Disabled = false, + Name = "NotOwnerNotification" + }, + new + { + Id = 7, + Disabled = false, + Name = "NotValidSourceNotification" + }, + new + { + Id = 8, + Disabled = false, + Name = "NoDataNotification" + }, + new + { + Id = 9, + Disabled = false, + Name = "NormalThresholdNotification" + }, + new + { + Id = 10, + Disabled = false, + Name = "InactiveSyncJobNotification" + }, + new + { + Id = 11, + Disabled = false, + Name = "GuestUserFailureNotification" + }); + }); + + modelBuilder.Entity("Models.PendingSyncJobChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("ChangeDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeTime") + .HasColumnType("datetime2"); + + b.Property("RequestorUPN") + .HasColumnType("nvarchar(max)"); + + b.Property("RequestorUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.ToTable("PendingSyncJobChanges"); + }); + + modelBuilder.Entity("Models.PurgedSyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("PurgedAt") + .HasColumnType("datetime2"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PurgedSyncJobs"); + }); + + modelBuilder.Entity("Models.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("SettingKey") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SettingValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SettingKey") + .IsUnique(); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("Models.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SortPriority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Statuses", (string)null); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(450)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Status") + .IsUnique() + .HasFilter("[Status] IS NOT NULL"); + + b.ToTable("SyncJobs"); + }); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.HasOne("Models.DestinationOwner", null) + .WithMany() + .HasForeignKey("DestinationOwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Entities.ThresholdNotification", b => + { + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.HasOne("Models.SyncJob", "SyncJob") + .WithOne("DestinationName") + .HasForeignKey("Models.DestinationName", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.HasOne("Models.NotificationType", "NotificationType") + .WithMany() + .HasForeignKey("NotificationTypeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", "SyncJob") + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationType"); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.HasOne("Models.Status", "StatusDetails") + .WithOne() + .HasForeignKey("Models.SyncJob", "Status") + .HasPrincipalKey("Models.Status", "Name") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("StatusDetails"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Navigation("DestinationName"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240719185127_notification_type_add_guest.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240719185127_notification_type_add_guest.cs new file mode 100644 index 000000000..e266c1543 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240719185127_notification_type_add_guest.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + public partial class notification_type_add_guest : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "NotificationTypes", + columns: new[] { "Id", "Disabled", "Name" }, + values: new object[] { 11, false, "GuestUserFailureNotification" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "NotificationTypes", + keyColumn: "Id", + keyValue: 11); + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs index 9b57c946f..e2b4bf0b1 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/GMMContextModelSnapshot.cs @@ -64,7 +64,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasData( new { - Id = new Guid("3eeff080-d199-4876-bff8-8426fca6909c"), + Id = new Guid("f6476897-8d60-4ead-8dce-475c511a4eab"), Name = "SqlMembership" }); }); @@ -90,7 +90,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ChangeTime") .ValueGeneratedOnAdd() .HasColumnType("datetime2") - .HasDefaultValue(new DateTime(2024, 7, 15, 18, 52, 7, 209, DateTimeKind.Utc).AddTicks(2460)); + .HasDefaultValue(new DateTime(2024, 7, 19, 18, 51, 27, 252, DateTimeKind.Utc).AddTicks(6194)); b.Property("ChangedByDisplayName") .IsRequired() @@ -340,6 +340,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) Id = 10, Disabled = false, Name = "InactiveSyncJobNotification" + }, + new + { + Id = 11, + Disabled = false, + Name = "GuestUserFailureNotification" }); }); @@ -348,7 +354,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier") - .HasDefaultValueSql("NEWID()"); + .HasDefaultValueSql("NEWSEQUENTIALID()"); b.Property("ChangeDetails") .HasColumnType("nvarchar(max)"); @@ -356,9 +362,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ChangeTime") .HasColumnType("datetime2"); - b.Property("Requestor") + b.Property("RequestorUPN") .HasColumnType("nvarchar(max)"); + b.Property("RequestorUserId") + .HasColumnType("uniqueidentifier"); + b.Property("Status") .HasColumnType("int"); From db95034af5281dc6c56d80b9b52625570cc6a4a3 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Fri, 19 Jul 2024 13:18:11 -0700 Subject: [PATCH 0090/1479] fix the build --- .../Function/PlaceMembershipObtainer.sln | 6 ------ .../Function/SqlMembershipObtainer.sln | 6 ------ 2 files changed, 12 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.sln b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.sln index fa9fafa41..c39e2010c 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.sln +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.sln @@ -23,8 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Services", "..\Servic EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\Models.csproj", "{57A8CCEB-A21C-472E-AFFA-D5B8777695DB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{ECE51FFA-5861-4BAE-9C43-7C2476EAF9B5}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -71,10 +69,6 @@ Global {57A8CCEB-A21C-472E-AFFA-D5B8777695DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {57A8CCEB-A21C-472E-AFFA-D5B8777695DB}.Release|Any CPU.ActiveCfg = Release|Any CPU {57A8CCEB-A21C-472E-AFFA-D5B8777695DB}.Release|Any CPU.Build.0 = Release|Any CPU - {ECE51FFA-5861-4BAE-9C43-7C2476EAF9B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ECE51FFA-5861-4BAE-9C43-7C2476EAF9B5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ECE51FFA-5861-4BAE-9C43-7C2476EAF9B5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ECE51FFA-5861-4BAE-9C43-7C2476EAF9B5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln index a65a944d4..4c15e0fc0 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.sln @@ -37,8 +37,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Entities", "..\..\..\Entiti EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework", "..\..\..\Repositories.EntityFramework\Repositories.EntityFramework.csproj", "{279D7C62-303B-4C72-A80E-8A3B4EE34E8D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.NotificationRepository", "..\..\..\Repositories.Notifications\Repositories.NotificationRepository.csproj", "{524EFA1D-B572-41E1-9D76-A4328C98B789}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Mail", "..\..\..\Repositories.Mail\Repositories.Mail.csproj", "{454EEE07-02C1-48A9-B751-88E94EC162C1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.RetryPolicyProvider", "..\..\..\Repositories.RetryPolicyProvider\Repositories.RetryPolicyProvider.csproj", "{98F62F05-1FD5-4F24-8B3B-A9FD5740381F}" @@ -117,10 +115,6 @@ Global {279D7C62-303B-4C72-A80E-8A3B4EE34E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU {279D7C62-303B-4C72-A80E-8A3B4EE34E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU {279D7C62-303B-4C72-A80E-8A3B4EE34E8D}.Release|Any CPU.Build.0 = Release|Any CPU - {524EFA1D-B572-41E1-9D76-A4328C98B789}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {524EFA1D-B572-41E1-9D76-A4328C98B789}.Debug|Any CPU.Build.0 = Debug|Any CPU - {524EFA1D-B572-41E1-9D76-A4328C98B789}.Release|Any CPU.ActiveCfg = Release|Any CPU - {524EFA1D-B572-41E1-9D76-A4328C98B789}.Release|Any CPU.Build.0 = Release|Any CPU {454EEE07-02C1-48A9-B751-88E94EC162C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {454EEE07-02C1-48A9-B751-88E94EC162C1}.Debug|Any CPU.Build.0 = Debug|Any CPU {454EEE07-02C1-48A9-B751-88E94EC162C1}.Release|Any CPU.ActiveCfg = Release|Any CPU From c7cb06652f592d58934bc29b2d9b6d063639a23e Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Fri, 19 Jul 2024 13:44:16 -0700 Subject: [PATCH 0091/1479] Updated TeamsChannel documentation with instructions on setting up Graph app to have Application permissions for tenants with MFA --- .../Set-TeamsChannelServiceAccountSecrets.ps1 | 26 +++++++++++++++++-- .../TeamsChannelServiceAccountSetup.md | 12 +++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Scripts/Set-TeamsChannelServiceAccountSecrets.ps1 b/Scripts/Set-TeamsChannelServiceAccountSecrets.ps1 index 7b06f0ce3..27cea882a 100644 --- a/Scripts/Set-TeamsChannelServiceAccountSecrets.ps1 +++ b/Scripts/Set-TeamsChannelServiceAccountSecrets.ps1 @@ -22,7 +22,8 @@ Set-TeamsChannelServiceAccountSecrets -SubscriptionName "" -EnvironmentAbbreviation "" ` -teamsChannelServiceAccountUsername $teamsChannelServiceAccountUsername ` -teamsChannelServiceAccountPassword $teamsChannelServiceAccountPassword ` - -teamsChannelServiceAccountObjectId $teamsChannelServiceAccountObjectId + -teamsChannelServiceAccountObjectId $teamsChannelServiceAccountObjectId ` + -GmmGraphAppHasTeamsChannelApplicationPermissions $false #> function Set-TeamsChannelServiceAccountSecrets { @@ -39,7 +40,9 @@ function Set-TeamsChannelServiceAccountSecrets { [Parameter(Mandatory=$True)] [SecureString] $teamsChannelServiceAccountPassword, [Parameter(Mandatory=$True)] - [SecureString] $teamsChannelServiceAccountObjectId + [SecureString] $teamsChannelServiceAccountObjectId, + [Parameter(Mandatory=$False)] + [SecureString] $GmmGraphAppHasTeamsChannelApplicationPermissions ) Write-Verbose "Set-TeamsChannelServiceAccountSecrets starting..." @@ -80,4 +83,23 @@ function Set-TeamsChannelServiceAccountSecrets { -SecretValue $teamsChannelServiceAccountObjectId Write-Verbose "teamsChannelServiceAccountObjectId added to vault..." + + #region If GMM Graph App has Application permission for Channel.ReadBasic.All and ChannelMember.ReadWrite.All, add app config value to indicate it + if($GmmGraphAppHasTeamsChannelApplicationPermissions) { + + $dataResourceGroupName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation" + $appConfigName = "$SolutionAbbreviation-appConfig-$EnvironmentAbbreviation" + $appConfigObject = Get-AzAppConfigurationStore -ResourceGroupName $dataResourceGroupName -Name $appConfigName; + + Set-AzAppConfigurationKeyValue -Endpoint $appConfigObject.Endpoint ` + -Key "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted" ` + -Value "true" ` + -ContentType "boolean" ` + -Etag { tag1="TeamsChannel" } + + Write-Host "Updated TeamsChannel:IsChannelReadWriteApplicationPermissionGranted key with the value True"; + + } + + #endregion } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Repositories.TeamsChannel/Documentation/TeamsChannelServiceAccountSetup.md b/Service/GroupMembershipManagement/Repositories.TeamsChannel/Documentation/TeamsChannelServiceAccountSetup.md index 2d64953a0..6c6e51122 100644 --- a/Service/GroupMembershipManagement/Repositories.TeamsChannel/Documentation/TeamsChannelServiceAccountSetup.md +++ b/Service/GroupMembershipManagement/Repositories.TeamsChannel/Documentation/TeamsChannelServiceAccountSetup.md @@ -2,12 +2,16 @@ 1) Open the Azure Portal from the tenant where the Graph App is created in. 2) Ensure that the `Channel.ReadBasic.All` and `ChannelMember.ReadWrite.All` Delegated Permissions have been granted for the `-Graph-` application. + + a. **If you are in an environment that requires MFA for all service accounts**, then add the Channel.ReadBasic.All and ChannelMember.ReadWrite.All **Application Permission** instead (Delegated permissions don't work when MFA is required on the service account). + + 3) Enable 'Allow public client flows' in `-Graph-` application -> Authentication. 4) Create a new service account: * This can be done by creating a new user from the tenant where the Graph App is created in * Make sure the user has a usage location set - * This account will be used by GMM to get information about channels and to add and remove users from channels. + * This account will be used by GMM to get information about channels and to add and remove users from channels. * Please note username & password of this user. 5) Run [Set-TeamsChannelServiceAccountSecrets.ps1](/Scripts/Set-TeamsChannelServiceAccountSecrets.ps1) to store the service account information in the prereqs keyvault @@ -25,8 +29,12 @@ -EnvironmentAbbreviation "" ` -teamsChannelServiceAccountUsername $teamsChannelServiceAccountUsername ` -teamsChannelServiceAccountPassword $teamsChannelServiceAccountPassword ` - -teamsChannelServiceAccountObjectId $teamsChannelServiceAccountObjectId + -teamsChannelServiceAccountObjectId $teamsChannelServiceAccountObjectId ` + -GmmGraphAppHasTeamsChannelApplicationPermissions $false ``` + +>Note: Make sure that **if you added Channel.ReadBasic.All and ChannelMember.ReadWrite.All Application permissions** to your GMM Graph App, that you set the GmmGraphAppHasTeamsChannelApplicationPermissions parameter in the Set-TeamsChannelServiceAccountSecrets script to $true! + 6) Assign the following two licenses to this user by going to [this](https://admin.microsoft.com/AdminPortal/Home#/licenses) link from your demo tenant page. You may have to unassign some licenses from other users to do this. - Enterprise Mobility + Security E5 From c6fcef4f57363e076a29c49f137d0d19fcb22197 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 15 Jul 2024 12:37:26 -0700 Subject: [PATCH 0092/1479] Removed the staging app references from PS scripts --- .../Set-ADFManagedIdentityRoles.ps1 | 30 +++++------ ...t-AppConfigurationManagedIdentityRoles.ps1 | 48 +++++++---------- .../Set-KeyVaultAccessRoles.ps1 | 52 +++++++++--------- .../Set-LogAnalyticsReaderRole.ps1 | 40 ++++++-------- ...geAccountContainerManagedIdentityRoles.ps1 | 53 +++++++++---------- 5 files changed, 98 insertions(+), 125 deletions(-) diff --git a/Scripts/PostDeployment/Set-ADFManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-ADFManagedIdentityRoles.ps1 index ac20c4aa1..78fdea3c1 100644 --- a/Scripts/PostDeployment/Set-ADFManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-ADFManagedIdentityRoles.ps1 @@ -57,29 +57,25 @@ function Set-ADFManagedIdentityRoles } foreach ($functionApp in $functionApps) - { - $ProductionFunctionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp" - $StagingFunctionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp/slots/staging" - $functionAppBasedOnSlots = @($ProductionFunctionAppName,$StagingFunctionAppName) + { + $functionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp" - foreach ($fa in $functionAppBasedOnSlots) - { - $servicePrincipal = Get-AzADServicePrincipal -DisplayName $fa; + $servicePrincipal = Get-AzADServicePrincipal -DisplayName $functionAppName; - if ($servicePrincipal) - { - $servicePrincipals.Add($servicePrincipal) - } - elseif ($null -eq $servicePrincipal) { - Write-Host "Function $fa was not found!" - } + if ($servicePrincipal) + { + $servicePrincipals.Add($servicePrincipal) } + elseif ($null -eq $servicePrincipal) { + Write-Host "Function $functionAppName was not found!" + } + } foreach ($appService in $appServices) - { + { $servicePrincipal = Get-AzADServicePrincipal -DisplayName "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$appService" - + if ($servicePrincipal) { $servicePrincipals.Add($servicePrincipal) @@ -108,6 +104,6 @@ function Set-ADFManagedIdentityRoles Write-Host "$servicePrincipalName already has access to the $azureDataFactoryName ADF resource."; } } - + Write-Host "Done attempting to add Data Factory Contributor role assignments."; } \ No newline at end of file diff --git a/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 index 3240f4f9f..d83e815ab 100644 --- a/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 @@ -35,8 +35,7 @@ function Set-AppConfigurationManagedIdentityRoles [string] $ErrorActionPreference = $Stop ) - $functionApps = @("GraphUpdater","MembershipAggregator","GroupMembershipObtainer","SqlMembershipObtainer","PlaceMembershipObtainer","AzureMaintenance","AzureUserReader","JobScheduler","JobTrigger","NonProdService","Notifier","TeamsChannelMembershipObtainer","GroupOwnershipObtainer", "TeamsChannelUpdater", "DestinationAttributesUpdater") - $webApi = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-webapi" + $apps = @("WebApi","GraphUpdater","MembershipAggregator","GroupMembershipObtainer","SqlMembershipObtainer","PlaceMembershipObtainer","AzureMaintenance","AzureUserReader","JobScheduler","JobTrigger","NonProdService","Notifier","TeamsChannelMembershipObtainer","GroupOwnershipObtainer", "TeamsChannelUpdater", "DestinationAttributesUpdater") $resourceGroupName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation"; if($DataResourceGroupName) @@ -47,43 +46,36 @@ function Set-AppConfigurationManagedIdentityRoles $appConfigName = "$SolutionAbbreviation-appConfig-$EnvironmentAbbreviation" $appConfigObject = Get-AzAppConfigurationStore -ResourceGroupName $resourceGroupName -Name $appConfigName; - foreach ($functionApp in $functionApps) + foreach ($app in $apps) { Write-Host "Granting app service access to app configuration"; - $ProductionFunctionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp" - $StagingFunctionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp/slots/staging" + $appName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$app" - $functionAppBasedOnSlots = @($ProductionFunctionAppName,$StagingFunctionAppName, $webApi) + Write-Host "FunctionAppName: $appName" - foreach ($fa in $functionAppBasedOnSlots) - { - - Write-Host "FunctionAppName: $fa" + $appServicePrincipal = Get-AzADServicePrincipal -DisplayName $appName; - $appServicePrincipal = Get-AzADServicePrincipal -DisplayName $fa; + # Grant the app service access to the app configuration + if ($appServicePrincipal) + { - # Grant the app service access to the app configuration - if ($appServicePrincipal) + if ($null -eq (Get-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $appConfigObject.Id)) { - - if ($null -eq (Get-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $appConfigObject.Id)) - { - $assignment = New-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $appConfigObject.Id -RoleDefinitionName "App Configuration Data Reader"; - if ($assignment) { - Write-Host "Added role assignment to allow $fa to read from the $appConfigName app configuration."; - } - else { - Write-Host "Failed to add role assignment to allow $fa to read from the $appConfigName app configuration. Please double check that you have permission to perform this operation"; - } + $assignment = New-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $appConfigObject.Id -RoleDefinitionName "App Configuration Data Reader"; + if ($assignment) { + Write-Host "Added role assignment to allow $appName to read from the $appConfigName app configuration."; } - else - { - Write-Host "$fa can already read keys from the $appConfigName app configuration."; + else { + Write-Host "Failed to add role assignment to allow $appName to read from the $appConfigName app configuration. Please double check that you have permission to perform this operation"; } - } elseif ($null -eq $appServicePrincipal) { - Write-Host "Function $fa was not found!" } + else + { + Write-Host "$appName can already read keys from the $appConfigName app configuration."; + } + } elseif ($null -eq $appServicePrincipal) { + Write-Host "App $appName was not found!" } } diff --git a/Scripts/PostDeployment/Set-KeyVaultAccessRoles.ps1 b/Scripts/PostDeployment/Set-KeyVaultAccessRoles.ps1 index ddc0ce4bd..50ce04011 100644 --- a/Scripts/PostDeployment/Set-KeyVaultAccessRoles.ps1 +++ b/Scripts/PostDeployment/Set-KeyVaultAccessRoles.ps1 @@ -81,34 +81,30 @@ function Set-KeyVaultAccessRoles { # Grant the Function Apps access to the keyvaults foreach ($functionApp in $functionApps) { - $ProductionFunctionAppName = $functionApp.Name - $StagingFunctionAppName = "$($functionApp.Name)/slots/staging" - $functionAppBasedOnSlots = @($ProductionFunctionAppName, $StagingFunctionAppName) - - foreach ($fa in $functionAppBasedOnSlots) { - $functionServicePrincipal = Get-AzADServicePrincipal -DisplayName $fa; - - # Grant the app service access to the keyvaults - if ($functionServicePrincipal) { - # prereqs keyvault - Set-KVRoleAssignment ` - -ObjectId $functionServicePrincipal.Id ` - -DisplayName $fa ` - -Scope $prereqsKeyVault.ResourceId ` - -RoleDefinitionName "Key Vault Secrets User" ` - -KeyVaultName $prereqsKeyVault.VaultName - - # data keyvault - Set-KVRoleAssignment ` - -ObjectId $functionServicePrincipal.Id ` - -DisplayName $fa ` - -Scope $dataKeyVault.ResourceId ` - -RoleDefinitionName "Key Vault Secrets User" ` - -KeyVaultName $dataKeyVault.VaultName - } - else { - Write-Host "Function $fa was not found!" - } + $functionAppName = $functionApp.Name + + $functionServicePrincipal = Get-AzADServicePrincipal -DisplayName $functionAppName; + + # Grant the app service access to the keyvaults + if ($functionServicePrincipal) { + # prereqs keyvault + Set-KVRoleAssignment ` + -ObjectId $functionServicePrincipal.Id ` + -DisplayName $functionAppName ` + -Scope $prereqsKeyVault.ResourceId ` + -RoleDefinitionName "Key Vault Secrets User" ` + -KeyVaultName $prereqsKeyVault.VaultName + + # data keyvault + Set-KVRoleAssignment ` + -ObjectId $functionServicePrincipal.Id ` + -DisplayName $functionAppName ` + -Scope $dataKeyVault.ResourceId ` + -RoleDefinitionName "Key Vault Secrets User" ` + -KeyVaultName $dataKeyVault.VaultName + } + else { + Write-Host "Function $functionAppName was not found!" } } diff --git a/Scripts/PostDeployment/Set-LogAnalyticsReaderRole.ps1 b/Scripts/PostDeployment/Set-LogAnalyticsReaderRole.ps1 index bc685840e..d32973aa4 100644 --- a/Scripts/PostDeployment/Set-LogAnalyticsReaderRole.ps1 +++ b/Scripts/PostDeployment/Set-LogAnalyticsReaderRole.ps1 @@ -50,39 +50,33 @@ function Set-LogAnalyticsReaderRole { Write-Host "Granting app service access to Log Analytics resource"; - $ProductionFunctionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp" - $StagingFunctionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp/slots/staging" + $functionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp" - $functionAppBasedOnSlots = @($ProductionFunctionAppName,$StagingFunctionAppName) + $appServicePrincipal = Get-AzADServicePrincipal -DisplayName $functionAppName; - foreach ($fa in $functionAppBasedOnSlots) + # Grant the app service access to the Log Analytics resource logs + if ($appServicePrincipal) { - $appServicePrincipal = Get-AzADServicePrincipal -DisplayName $fa; + $logAnalyticsObject = Get-AzOperationalInsightsWorkspace -ResourceGroupName $resourceGroupName -Name $logAnalyticsWorkspaceResourceName; - # Grant the app service access to the Log Analytics resource logs - if ($appServicePrincipal) + if ($null -eq (Get-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $logAnalyticsObject.ResourceId)) { - $logAnalyticsObject = Get-AzOperationalInsightsWorkspace -ResourceGroupName $resourceGroupName -Name $logAnalyticsWorkspaceResourceName; - - if ($null -eq (Get-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $logAnalyticsObject.ResourceId)) - { - $assignment = New-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $logAnalyticsObject.ResourceId -RoleDefinitionName "Log Analytics Reader"; - if ($assignment) { - Write-Host "Added role assignment to allow $fa to access on the $logAnalyticsWorkspaceResourceName logs."; - } - else { - Write-Host "Failed to add role assignment to allow $fa to access on the $logAnalyticsWorkspaceResourceName logs. Please double check that you have permission to perform this operation"; - } + $assignment = New-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $logAnalyticsObject.ResourceId -RoleDefinitionName "Log Analytics Reader"; + if ($assignment) { + Write-Host "Added role assignment to allow $functionAppName to access on the $logAnalyticsWorkspaceResourceName logs."; } - else - { - Write-Host "$fa already has access to $logAnalyticsWorkspaceResourceName logs."; + else { + Write-Host "Failed to add role assignment to allow $functionAppName to access on the $logAnalyticsWorkspaceResourceName logs. Please double check that you have permission to perform this operation"; } } - elseif ($null -eq $appServicePrincipal) { - Write-Host "Function $fa was not found!" + else + { + Write-Host "$functionAppName already has access to $logAnalyticsWorkspaceResourceName logs."; } } + elseif ($null -eq $appServicePrincipal) { + Write-Host "Function $functionAppName was not found!" + } } Write-Host "Done attempting to add Log Analytics Reader role assignment(s)."; diff --git a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 index d2d3f0ce5..9e6910907 100644 --- a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 @@ -50,46 +50,41 @@ function Set-StorageAccountContainerManagedIdentityRoles $resourceGroupName = $DataResourceGroupName } - $ProductionFunctionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp" - $StagingFunctionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp/slots/staging" + $functionAppName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-$functionApp" - $functionAppBasedOnSlots = @($ProductionFunctionAppName,$StagingFunctionAppName) + $appServicePrincipal = Get-AzADServicePrincipal -DisplayName $functionAppName; - foreach ($fa in $functionAppBasedOnSlots) + # Grant the app service access to the storage account blobs + if ($appServicePrincipal) { - $appServicePrincipal = Get-AzADServicePrincipal -DisplayName $fa; + $resources = Get-AzResource -ResourceGroupName $resourceGroupName - # Grant the app service access to the storage account blobs - if ($appServicePrincipal) - { - $resources = Get-AzResource -ResourceGroupName $resourceGroupName + $filteredStorageAccountsList = $resources | Where-Object { + $_.ResourceType -eq "Microsoft.Storage/storageAccounts" -and $_.Name -like "jobs$EnvironmentAbbreviation*" + } - $filteredStorageAccountsList = $resources | Where-Object { - $_.ResourceType -eq "Microsoft.Storage/storageAccounts" -and $_.Name -like "jobs$EnvironmentAbbreviation*" - } + $storageAccountObject = $filteredStorageAccountsList[0] + $storageAccountName = $storageAccountObject.Name - $storageAccountObject = $filteredStorageAccountsList[0] - $storageAccountName = $storageAccountObject.Name - - if ($null -eq (Get-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $storageAccountObject.Id)) - { - $assignment = New-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $storageAccountObject.Id -RoleDefinitionName "Storage Blob Data Contributor"; - if ($assignment) { - Write-Host "Added role assignment to allow $fa to access on the $storageAccountName blobs."; - } - else { - Write-Host "Failed to add role assignment to allow $fa to access on the $storageAccountName blobs. Please double check that you have permission to perform this operation"; - } + if ($null -eq (Get-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $storageAccountObject.Id)) + { + $assignment = New-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $storageAccountObject.Id -RoleDefinitionName "Storage Blob Data Contributor"; + if ($assignment) { + Write-Host "Added role assignment to allow $functionAppName to access on the $storageAccountName blobs."; } - else - { - Write-Host "$fa already has access to $storageAccountName blobs."; + else { + Write-Host "Failed to add role assignment to allow $functionAppName to access on the $storageAccountName blobs. Please double check that you have permission to perform this operation"; } } - elseif ($null -eq $appServicePrincipal) { - Write-Host "Function $fa was not found!" + else + { + Write-Host "$functionAppName already has access to $storageAccountName blobs."; } } + elseif ($null -eq $appServicePrincipal) { + Write-Host "Function $functionAppName was not found!" + + } } Write-Host "Done attempting to add Storage Blob Data Contributor role assignments."; From 0eb21f29a7f98a5d06b851399ad712fd4f99a2fb Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 15 Jul 2024 12:42:23 -0700 Subject: [PATCH 0093/1479] Removed the staging slot and associated bicep from all function apps in GMM --- .../Infrastructure/compute/functionApp.bicep | 25 ---- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 86 ------------ .../Infrastructure/compute/template.bicep | 58 -------- .../Infrastructure/data/template.bicep | 12 +- .../Infrastructure/compute/functionApp.bicep | 22 --- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 128 ----------------- .../Infrastructure/compute/template.bicep | 56 -------- .../Infrastructure/data/template.bicep | 12 -- .../Infrastructure/compute/functionApp.bicep | 17 --- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 86 ------------ .../Infrastructure/compute/template.bicep | 51 ------- .../Infrastructure/data/template.bicep | 12 -- .../Infrastructure/compute/functionApp.bicep | 33 ----- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 126 ----------------- .../Infrastructure/compute/template.bicep | 68 --------- .../Infrastructure/data/template.bicep | 12 -- .../Infrastructure/compute/functionApp.bicep | 29 ---- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 86 ------------ .../Infrastructure/compute/template.bicep | 63 --------- .../Infrastructure/data/template.bicep | 13 -- .../Infrastructure/compute/functionApp.bicep | 23 ---- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 86 ------------ .../Infrastructure/compute/template.bicep | 57 -------- .../Infrastructure/data/template.bicep | 12 +- .../Infrastructure/compute/functionApp.bicep | 24 ---- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 80 ----------- .../Infrastructure/compute/template.bicep | 61 +-------- .../data/keyVaultSecretsSecure.bicep | 10 ++ .../Infrastructure/data/storageAccount.bicep | 110 +++++++++++++++ .../Infrastructure/data/template.bicep | 16 +++ .../Infrastructure/compute/functionApp.bicep | 21 --- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 86 ------------ .../Infrastructure/compute/template.bicep | 55 -------- .../Infrastructure/data/template.bicep | 12 -- .../Infrastructure/compute/functionApp.bicep | 23 ---- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 129 ------------------ .../Infrastructure/compute/template.bicep | 60 -------- .../Infrastructure/data/template.bicep | 12 +- .../Infrastructure/compute/functionApp.bicep | 19 --- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 124 ----------------- .../Infrastructure/compute/template.bicep | 55 -------- .../Infrastructure/data/template.bicep | 12 -- .../Infrastructure/compute/functionApp.bicep | 18 --- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 86 ------------ .../Infrastructure/compute/template.bicep | 54 -------- .../Infrastructure/data/template.bicep | 12 -- .../Infrastructure/compute/functionApp.bicep | 21 --- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 85 ------------ .../Infrastructure/compute/template.bicep | 61 +-------- .../data/keyVaultSecretsSecure.bicep | 10 ++ .../Infrastructure/data/storageAccount.bicep | 108 +++++++++++++++ .../Infrastructure/data/template.bicep | 16 +++ .../Infrastructure/compute/functionApp.bicep | 25 ---- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 86 ------------ .../Infrastructure/compute/template.bicep | 72 ++-------- .../Infrastructure/data/template.bicep | 19 +-- .../Infrastructure/compute/functionApp.bicep | 29 ---- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 86 ------------ .../Infrastructure/compute/template.bicep | 47 ------- .../Infrastructure/data/template.bicep | 12 +- .../Infrastructure/compute/functionApp.bicep | 24 ---- .../compute/functionAppRBAC.bicep | 22 --- .../compute/functionAppSlot.bicep | 86 ------------ .../Infrastructure/compute/template.bicep | 58 -------- .../Infrastructure/data/template.bicep | 12 -- 79 files changed, 297 insertions(+), 3142 deletions(-) delete mode 100644 Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionAppSlot.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/keyVaultSecretsSecure.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/storageAccount.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/keyVaultSecretsSecure.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/storageAccount.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep delete mode 100644 Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionAppSlot.bicep diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionApp.bicep index 5e8129b8b..11625bfb4 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionApp.bicep @@ -82,29 +82,4 @@ resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@ } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.LoggerFunction.Disabled' - 'AzureWebJobs.RetrieveBackupsFunction.Disabled' - 'AzureWebJobs.ReviewAndDeleteFunction.Disabled' - 'AzureWebJobs.TableBackupFunction.Disabled' - 'AzureWebJobs.BackUpInactiveJobsFunction.Disabled' - 'AzureWebJobs.ReadGroupNameFunction.Disabled' - 'AzureWebJobs.ReadSyncJobsFunction.Disabled' - 'AzureWebJobs.RemoveBackUpsFunction.Disabled' - 'AzureWebJobs.RemoveInactiveJobsFunction.Disabled' - 'AzureWebJobs.SendEmailFunction.Disabled' - 'AzureWebJobs.ExpireNotificationsFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 424a2c56c..000000000 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,86 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep index 8087e0f70..36e89f0e5 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep @@ -48,18 +48,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the \'data\' key vault.') param dataKeyVaultName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' @@ -87,7 +78,6 @@ var jobsStorageAccountConnectionString = resourceId(subscription().subscriptionI var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var azureMaintenanceStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'azureMaintenanceStorageAccountProd') -var azureMaintenanceStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'azureMaintenanceStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') @@ -134,25 +124,6 @@ var appSettings = { serviceBusNotificationsQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusNotificationsQueue, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(azureMaintenanceStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}AzureMaintenanceStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.LoggerFunction.Disabled': 1 - 'AzureWebJobs.RetrieveBackupsFunction.Disabled': 1 - 'AzureWebJobs.ReviewAndDeleteFunction.Disabled': 1 - 'AzureWebJobs.TableBackupFunction.Disabled': 1 - 'AzureWebJobs.BackUpInactiveJobsFunction.Disabled': 1 - 'AzureWebJobs.ReadGroupNameFunction.Disabled': 1 - 'AzureWebJobs.ReadSyncJobsFunction.Disabled': 1 - 'AzureWebJobs.RemoveBackUpsFunction.Disabled': 1 - 'AzureWebJobs.RemoveInactiveJobsFunction.Disabled': 1 - 'AzureWebJobs.SendEmailFunction.Disabled': 1 - 'AzureWebJobs.ExpireNotificationsFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'AzureMaintenanceStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(azureMaintenanceStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}AzureMaintenance' @@ -222,23 +193,6 @@ module functionAppTemplate_AzureMaintenance 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_AzureMaintenance 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-AzureMaintenance' - params: { - name: '${functionAppName}-AzureMaintenance/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_AzureMaintenance - ] -} module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-AzureMaintenance' @@ -250,11 +204,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_AzureMaintenance.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_AzureMaintenance.outputs.msi } dependsOn: [ functionAppTemplate_AzureMaintenance - functionAppSlotTemplate_AzureMaintenance ] } @@ -266,13 +218,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-AzureMaintenance/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/data/template.bicep index b8e97d619..12c4c00dd 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/data/template.bicep @@ -15,12 +15,12 @@ param tenantId string param storageAccountName string param storageAccountSku string = 'Standard_LRS' + @description('Resource location.') param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('am${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('am${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module azureMaintenanceStorageAccountProd 'storageAccount.bicep' = { name: 'amProdstorageAccountTemplate' @@ -32,13 +32,3 @@ module azureMaintenanceStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'azureMaintenanceStorageAccountProd' } } -module azureMaintenanceStorageAccountStaging 'storageAccount.bicep' = { - name: 'amStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'azureMaintenanceStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionApp.bicep index 067f981e0..1a6c8611f 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionApp.bicep @@ -122,26 +122,4 @@ module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { } } - - -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.UserCreatorSubOrchestratorFunction.Disabled' - 'AzureWebJobs.UserReaderSubOrchestratorFunction.Disabled' - 'AzureWebJobs.AzureUserCreatorFunction.Disabled' - 'AzureWebJobs.AzureUserReaderFunction.Disabled' - 'AzureWebJobs.PersonnelNumberReaderFunction.Disabled' - 'AzureWebJobs.UploadUsersFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 66b699c92..000000000 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,128 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('Name of the \'data\' key vault.') -param dataKeyVaultName string - -@description('Name of the resource group where the \'data\' key vault is located.') -param dataKeyVaultResourceGroup string - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -module secretsTemplate 'keyVaultSecrets.bicep' = { - name: 'secretsTemplate-AzureUserReaderStaging' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - keyVaultParameters: [ - { - name: 'azureUserReaderStagingUrl' - value: 'https://${functionAppSlot.properties.defaultHostName}' - } - { - name: 'azureUserReaderStagingFunctionName' - value: '${name}-AzureUserReader/staging' - } - ] - } -} - -module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { - name: 'secureSecretsTemplate-AzureUserReaderStaging' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - keyVaultSecrets: { - secrets: [ - { - name: 'azureUserReaderStagingKey' - value: listkeys('${functionAppSlot.id}/host/default', '2018-11-01').functionKeys.default - } - ] - } - } -} - - - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep index 33937b36e..d2b029119 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep @@ -42,18 +42,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the resource group where the \'prereqs\' key vault is located.') param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' @@ -86,7 +77,6 @@ var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, da var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var azureUserReaderStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'azureUserReaderStorageAccountProd') -var azureUserReaderStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'azureUserReaderStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') module servicePlanTemplate 'servicePlan.bicep' = { @@ -127,20 +117,6 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(azureUserReaderStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}AzureUserReaderStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.UserCreatorSubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.UserReaderSubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.AzureUserCreatorFunction.Disabled': 1 - 'AzureWebJobs.AzureUserReaderFunction.Disabled': 1 - 'AzureWebJobs.PersonnelNumberReaderFunction.Disabled': 1 - 'AzureWebJobs.UploadUsersFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'AzureUserReaderStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(azureUserReaderStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}AzureUserReader' @@ -199,26 +175,6 @@ module functionAppTemplate_AzureUserReader 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_AzureUserReader 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-AzureUserReader' - params: { - name: '${functionAppName}-AzureUserReader/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - dataKeyVaultName: dataKeyVaultName - dataKeyVaultResourceGroup: dataKeyVaultResourceGroup - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_AzureUserReader - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-AzureUserReader' params: { @@ -229,11 +185,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_AzureUserReader.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_AzureUserReader.outputs.msi } dependsOn: [ functionAppTemplate_AzureUserReader - functionAppSlotTemplate_AzureUserReader ] } @@ -245,13 +199,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-AzureUserReader/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/data/template.bicep index cb6f79b2e..05ea74a73 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/data/template.bicep @@ -21,7 +21,6 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('aur${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('aur${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module azureUserReaderStorageAccountProd 'storageAccount.bicep' = { name: 'aurProdstorageAccountTemplate' @@ -33,14 +32,3 @@ module azureUserReaderStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'azureUserReaderStorageAccountProd' } } - -module azureUserReaderStorageAccountStaging 'storageAccount.bicep' = { - name: 'aurStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'azureUserReaderStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionApp.bicep index 3f773c3d5..11625bfb4 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionApp.bicep @@ -82,21 +82,4 @@ resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@ } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.AttributeCacheUpdaterFunction.Disabled' - 'AzureWebJobs.AttributeReaderFunction.Disabled' - 'AzureWebJobs.DestinationReaderFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 424a2c56c..000000000 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,86 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep index 42e0b993e..a9e29dd2f 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep @@ -68,18 +68,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the \'data\' key vault.') param dataKeyVaultName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' @@ -111,7 +102,6 @@ var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKe var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var destinationAttributesUpdaterStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'destinationAttributesUpdaterStorageAccountProd') -var destinationAttributesUpdaterStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'destinationAttributesUpdaterStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') module servicePlanTemplate 'servicePlan.bicep' = { @@ -160,17 +150,6 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(destinationAttributesUpdaterStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}DestinationAttributesUpdaterStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.AttributeCacheUpdaterFunction.Disabled': 1 - 'AzureWebJobs.AttributeReaderFunction.Disabled': 1 - 'AzureWebJobs.DestinationReaderFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'DestinationAttrUpdaterStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(destinationAttributesUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}DestinationAttributesUpdater' @@ -227,24 +206,6 @@ module functionAppTemplate_DestinationAttributesUpdater 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_DestinationAttributesUpdater 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-DestinationAttributesUpdater' - params: { - name: '${functionAppName}-DestinationAttributesUpdater/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_DestinationAttributesUpdater - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-DestinationAttributesUpdater' params: { @@ -255,11 +216,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_DestinationAttributesUpdater.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_DestinationAttributesUpdater.outputs.msi } dependsOn: [ functionAppTemplate_DestinationAttributesUpdater - functionAppSlotTemplate_DestinationAttributesUpdater ] } @@ -271,13 +230,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-DestinationAttributesUpdater/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/data/template.bicep index 4674005f1..881d44e60 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/data/template.bicep @@ -21,7 +21,6 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('dau${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('dau${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module destinationAttributesUpdaterStorageAccountProd 'storageAccount.bicep' = { name: 'dauProdstorageAccountTemplate' @@ -33,14 +32,3 @@ module destinationAttributesUpdaterStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'destinationAttributesUpdaterStorageAccountProd' } } - -module destinationAttributesUpdaterStorageAccountStaging 'storageAccount.bicep' = { - name: 'dauStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'destinationAttributesUpdaterStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionApp.bicep index c7c093791..d671604ba 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionApp.bicep @@ -122,37 +122,4 @@ module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { } } - -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.GroupUpdaterSubOrchestratorFunction.Disabled' - 'AzureWebJobs.EmailSenderFunction.Disabled' - 'AzureWebJobs.FileDownloaderFunction.Disabled' - 'AzureWebJobs.GroupNameReaderFunction.Disabled' - 'AzureWebJobs.GroupOwnersReaderFunction.Disabled' - 'AzureWebJobs.GroupUpdaterFunction.Disabled' - 'AzureWebJobs.GroupValidatorFunction.Disabled' - 'AzureWebJobs.JobReaderFunction.Disabled' - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' - 'AzureWebJobs.LoggerFunction.Disabled' - 'AzureWebJobs.CacheUpdaterFunction.Disabled' - 'AzureWebJobs.CacheUserUpdaterSubOrchestratorFunction.Disabled' - 'AzureWebJobs.MessageEntity.Disabled' - 'AzureWebJobs.MessageOrchestrator.Disabled' - 'AzureWebJobs.MessageProcessorOrchestrator.Disabled' - 'AzureWebJobs.MessageTrackerFunction.Disabled' - 'AzureWebJobs.StatusReaderFunction.Disabled' - 'AzureWebJobs.TelemetryTrackerFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index b3d1bc265..000000000 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,126 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('Name of the \'data\' key vault.') -param dataKeyVaultName string - -@description('Name of the resource group where the \'data\' key vault is located.') -param dataKeyVaultResourceGroup string - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -module secretsTemplate 'keyVaultSecrets.bicep' = { - name: 'secretsTemplate-GraphUpdaterStaging' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - keyVaultParameters: [ - { - name: 'graphUpdaterStagingUrl' - value: 'https://${functionAppSlot.properties.defaultHostName}/api/StarterFunction' - } - { - name: 'graphUpdaterStagingFunctionName' - value: '${name}-GraphUpdater/staging' - } - ] - } -} - -module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { - name: 'secureSecretsTemplate-GraphUpdaterStaging' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - keyVaultSecrets: { - secrets: [ - { - name: 'graphUpdaterStagingFunctionKey' - value: listkeys('${functionAppSlot.id}/host/default', '2018-11-01').functionKeys.default - } - ] - } - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep index 3be64f2ed..4b1893e45 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep @@ -42,18 +42,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the resource group where the \'prereqs\' key vault is located.') param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' @@ -86,7 +77,6 @@ var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKey var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') var graphUpdaterStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUpdaterStorageAccountProd') -var graphUpdaterStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUpdaterStorageAccountStaging') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierProviderId') var serviceBusFQN = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusFQN') @@ -144,32 +134,6 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GraphUpdaterStaging' - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(graphUpdaterStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.GroupUpdaterSubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.EmailSenderFunction.Disabled': 1 - 'AzureWebJobs.FileDownloaderFunction.Disabled': 1 - 'AzureWebJobs.GroupNameReaderFunction.Disabled': 1 - 'AzureWebJobs.GroupOwnersReaderFunction.Disabled': 1 - 'AzureWebJobs.GroupUpdaterFunction.Disabled': 1 - 'AzureWebJobs.GroupValidatorFunction.Disabled': 1 - 'AzureWebJobs.JobReaderFunction.Disabled': 1 - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 1 - 'AzureWebJobs.LoggerFunction.Disabled': 1 - 'AzureWebJobs.CacheUpdaterFunction.Disabled': 1 - 'AzureWebJobs.CacheUserUpdaterSubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.MessageEntity.Disabled': 1 - 'AzureWebJobs.MessageOrchestrator.Disabled': 1 - 'AzureWebJobs.MessageProcessorOrchestrator.Disabled': 1 - 'AzureWebJobs.MessageTrackerFunction.Disabled': 1 - 'AzureWebJobs.StatusReaderFunction.Disabled': 1 - 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'GraphUpdaterStaging' -} - var productionSettings = { AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GraphUpdater' AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(graphUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' @@ -228,26 +192,6 @@ module functionAppTemplate_GraphUpdater 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_GraphUpdater 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-GraphUpdater' - params: { - name: '${functionAppName}-GraphUpdater/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - dataKeyVaultName: dataKeyVaultName - dataKeyVaultResourceGroup: dataKeyVaultResourceGroup - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_GraphUpdater - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-GraphUpdater' params: { @@ -258,11 +202,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_GraphUpdater.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_GraphUpdater.outputs.msi } dependsOn: [ functionAppTemplate_GraphUpdater - functionAppSlotTemplate_GraphUpdater ] } @@ -275,13 +217,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-GraphUpdater/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/data/template.bicep index 0a14fe5b5..783acc2d3 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/data/template.bicep @@ -21,7 +21,6 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('gu${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('gu${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module graphUpdaterStorageAccountProd 'storageAccount.bicep' = { name: 'guProdstorageAccountTemplate' @@ -33,14 +32,3 @@ module graphUpdaterStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'graphUpdaterStorageAccountProd' } } - -module graphUpdaterStorageAccountStaging 'storageAccount.bicep' = { - name: 'guStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'graphUpdaterStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionApp.bicep index 9162c014a..11625bfb4 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionApp.bicep @@ -82,33 +82,4 @@ resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@ } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.SubOrchestratorFunction.Disabled' - 'AzureWebJobs.DeltaUsersReaderFunction.Disabled' - 'AzureWebJobs.DeltaUsersSenderFunction.Disabled' - 'AzureWebJobs.EmailSenderFunction.Disabled' - 'AzureWebJobs.FileDownloaderFunction.Disabled' - 'AzureWebJobs.GroupsReaderFunction.Disabled' - 'AzureWebJobs.GroupValidatorFunction.Disabled' - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' - 'AzureWebJobs.MembersReaderFunction.Disabled' - 'AzureWebJobs.SourceGroupsReaderFunction.Disabled' - 'AzureWebJobs.SubsequentDeltaUsersReaderFunction.Disabled' - 'AzureWebJobs.SubsequentMembersReaderFunction.Disabled' - 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled' - 'AzureWebJobs.UsersReaderFunction.Disabled' - 'AzureWebJobs.UsersSenderFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 424a2c56c..000000000 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,86 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep index e117c61fa..c7ebe0263 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep @@ -42,18 +42,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the resource group where the \'prereqs\' key vault is located.') param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' @@ -92,7 +83,6 @@ var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKe var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var groupMembershipObtainerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'groupMembershipObtainerStorageAccountProd') -var groupMembershipObtainerStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'groupMembershipObtainerStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') @@ -144,29 +134,6 @@ var appSettings = { serviceBusNotificationsQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusNotificationsQueue, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupMembershipObtainerStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.SubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.DeltaUsersReaderFunction.Disabled': 1 - 'AzureWebJobs.DeltaUsersSenderFunction.Disabled': 1 - 'AzureWebJobs.EmailSenderFunction.Disabled': 1 - 'AzureWebJobs.FileDownloaderFunction.Disabled': 1 - 'AzureWebJobs.GroupsReaderFunction.Disabled': 1 - 'AzureWebJobs.GroupValidatorFunction.Disabled': 1 - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 1 - 'AzureWebJobs.MembersReaderFunction.Disabled': 1 - 'AzureWebJobs.SourceGroupsReaderFunction.Disabled': 1 - 'AzureWebJobs.SubsequentDeltaUsersReaderFunction.Disabled': 1 - 'AzureWebJobs.SubsequentMembersReaderFunction.Disabled': 1 - 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled': 1 - 'AzureWebJobs.UsersReaderFunction.Disabled': 1 - 'AzureWebJobs.UsersSenderFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'GroupMembershipObtainerStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupMembershipObtainer' @@ -223,24 +190,6 @@ module functionAppTemplate_GroupMembershipObtainer 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_GroupMembershipObtainer 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-GroupMembershipObtainer' - params: { - name: '${functionAppName}-GroupMembershipObtainer/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_GroupMembershipObtainer - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-GroupMembershipObtainer' params: { @@ -251,11 +200,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_GroupMembershipObtainer.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_GroupMembershipObtainer.outputs.msi } dependsOn: [ functionAppTemplate_GroupMembershipObtainer - functionAppSlotTemplate_GroupMembershipObtainer ] } @@ -267,13 +214,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-GroupMembershipObtainer/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/data/template.bicep index 515e46c1c..4b057f1bb 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/data/template.bicep @@ -21,7 +21,6 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('gmo${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('gmo${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module gmoStorageAccountProd 'storageAccount.bicep' = { name: 'gmoProdstorageAccountTemplate' @@ -33,15 +32,3 @@ module gmoStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'groupMembershipObtainerStorageAccountProd' } } - -module gmoStorageAccountStaging 'storageAccount.bicep' = { - name: 'gmoStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'groupMembershipObtainerStorageAccountStaging' - } -} - diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionApp.bicep index b589b3934..6804c35a4 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionApp.bicep @@ -82,27 +82,4 @@ resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@ } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.GetGroupOwnersFunction.Disabled' - 'AzureWebJobs.GetJobsSegmentedFunction.Disabled' - 'AzureWebJobs.JobsFilterFunction.Disabled' - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' - 'AzureWebJobs.LoggerFunction.Disabled' - 'AzureWebJobs.TelemetryTrackerFunction.Disabled' - 'AzureWebJobs.UsersSenderFunction.Disabled' - 'AzureWebJobs.QueueMessageSenderFunction.Disabled' - 'AzureWebJobs.FeatureFlagFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 9cd555bde..000000000 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,86 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param appSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: appSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep index 9ae815383..d51ae6083 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep @@ -42,18 +42,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the resource group where the \'prereqs\' key vault is located.') param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' @@ -92,7 +83,6 @@ var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKe var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var groupOwnershipObtainerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'groupOwnershipObtainerStorageAccountProd') -var groupOwnershipObtainerStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'groupOwnershipObtainerStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') module servicePlanTemplate 'servicePlan.bicep' = { @@ -142,23 +132,6 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupOwnershipObtainerStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupOwnershipObtainerStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.GetGroupOwnersFunction.Disabled': 1 - 'AzureWebJobs.GetJobsSegmentedFunction.Disabled': 1 - 'AzureWebJobs.JobsFilterFunction.Disabled': 1 - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 1 - 'AzureWebJobs.LoggerFunction.Disabled': 1 - 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 1 - 'AzureWebJobs.UsersSenderFunction.Disabled': 1 - 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 1 - 'AzureWebJobs.FeatureFlagFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'GroupOwnershipObtainerStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupOwnershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupOwnershipObtainer' @@ -226,24 +199,6 @@ module functionAppTemplate_GroupOwnershipObtainer 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_GroupOwnershipObtainer 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-GroupOwnershipObtainer' - params: { - name: '${functionAppName}-GroupOwnershipObtainer/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - appSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_GroupOwnershipObtainer - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-GroupOwnershipObtainer' params: { @@ -254,11 +209,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_GroupOwnershipObtainer.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_GroupOwnershipObtainer.outputs.msi } dependsOn: [ functionAppTemplate_GroupOwnershipObtainer - functionAppSlotTemplate_GroupOwnershipObtainer ] } @@ -270,13 +223,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-GroupOwnershipObtainer/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/data/template.bicep index 8525ad2c6..c94d60c38 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/data/template.bicep @@ -21,7 +21,7 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('goo${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('goo${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) + module groupOwnershipObtainerStorageAccountProd 'storageAccount.bicep' = { name: 'gooProdstorageAccountTemplate' params: { @@ -32,13 +32,3 @@ module groupOwnershipObtainerStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'groupOwnershipObtainerStorageAccountProd' } } -module groupOwnershipObtainerStorageAccountStaging 'storageAccount.bicep' = { - name: 'gooStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'groupOwnershipObtainerStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionApp.bicep index b4d9b4c26..d1a35c0a9 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionApp.bicep @@ -112,28 +112,4 @@ module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.LoggerFunction.Disabled' - 'AzureWebJobs.GetJobsSubOrchestratorFunction.Disabled' - 'AzureWebJobs.GetJobsSegmentedFunction.Disabled' - 'AzureWebJobs.ResetJobsFunction.Disabled' - 'AzureWebJobs.DistributeJobsFunction.Disabled' - 'AzureWebJobs.UpdateJobsSubOrchestratorFunction.Disabled' - 'AzureWebJobs.BatchUpdateJobsFunction.Disabled' - 'AzureWebJobs.PipelineInvocationStarterFunction.Disabled' - 'AzureWebJobs.StatusCallbackOrchestratorFunction.Disabled' - 'AzureWebJobs.CheckJobSchedulerStatusFunction.Disabled' - 'AzureWebJobs.PostCallbackFunction.Disabled' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 056236fc3..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,80 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: 'SystemAssigned' - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep index ee077e64d..eaf95541a 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep @@ -68,18 +68,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the \'data\' key vault.') param dataKeyVaultName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' @@ -98,6 +89,7 @@ var storageAccountConnectionString = resourceId(subscription().subscriptionId, d var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') +var jobSchedulerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobSchedulerStorageAccountProd') module servicePlanTemplate 'servicePlan.bicep' = { name: 'servicePlanTemplate-JobScheduler' @@ -118,9 +110,10 @@ var commonSettings = { } var appSettings = { + WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(jobSchedulerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + WEBSITE_CONTENTSHARE: toLower('functionApp-JobScheduler') APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(storageAccountConnectionString, '2019-09-01').secretUriWithVersion})' - WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(storageAccountConnectionString, '2019-09-01').secretUriWithVersion})' jobSchedulerSchedule: '0 0 0 * * Sun' logAnalyticsCustomerId: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsCustomerId, '2019-09-01').secretUriWithVersion})' logAnalyticsPrimarySharedKey: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsPrimarySharedKey, '2019-09-01').secretUriWithVersion})' @@ -129,27 +122,8 @@ var appSettings = { 'ConnectionStrings:JobsContextReadOnly': '@Microsoft.KeyVault(SecretUri=${reference(replicaJobsMSIConnectionString, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - WEBSITE_CONTENTSHARE: toLower('functionApp-JobScheduler-staging') - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobSchedulerStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.LoggerFunction.Disabled': 1 - 'AzureWebJobs.GetJobsSubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.GetJobsSegmentedFunction.Disabled': 1 - 'AzureWebJobs.ResetJobsFunction.Disabled': 1 - 'AzureWebJobs.DistributeJobsFunction.Disabled': 1 - 'AzureWebJobs.UpdateJobsSubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.BatchUpdateJobsFunction.Disabled': 1 - 'AzureWebJobs.PipelineInvocationStarterFunction.Disabled': 1 - 'AzureWebJobs.StatusCallbackOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.CheckJobSchedulerStatusFunction.Disabled': 1 - 'AzureWebJobs.PostCallbackFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'JobSchedulerStaging' -} - var productionSettings = { - WEBSITE_CONTENTSHARE: toLower('functionApp-JobScheduler') + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobSchedulerStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobScheduler' 'AzureWebJobs.StarterFunction.Disabled': 0 'AzureWebJobs.OrchestratorFunction.Disabled': 0 @@ -195,21 +169,6 @@ module functionAppTemplate_JobScheduler 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_JobScheduler 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-JobScheduler' - params: { - name: '${functionAppName}-JobScheduler/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_JobScheduler - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-JobScheduler' params: { @@ -220,11 +179,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_JobScheduler.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_JobScheduler.outputs.msi } dependsOn: [ functionAppTemplate_JobScheduler - functionAppSlotTemplate_JobScheduler ] } @@ -236,13 +193,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-03-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-03-01' = { - name: '${functionAppName}-JobScheduler/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/keyVaultSecretsSecure.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/keyVaultSecretsSecure.bicep new file mode 100644 index 000000000..bbb8c8b38 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/keyVaultSecretsSecure.bicep @@ -0,0 +1,10 @@ +param keyVaultName string +@secure() +param keyVaultSecrets object + +resource secrets 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = [for secret in keyVaultSecrets.secrets: { + name: '${keyVaultName}/${secret.name}' + properties: { + value: secret.value + } +}] diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/storageAccount.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/storageAccount.bicep new file mode 100644 index 000000000..632d30074 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/storageAccount.bicep @@ -0,0 +1,110 @@ +@description('Storage account alphanumeric name.') +@minLength(1) +@maxLength(24) +param name string + +@description('Key vault name.') +param keyVaultName string + +@allowed([ + 'Standard_LRS' + 'Standard_GRS' + 'Standard_ZRS' + 'Premium_LRS' +]) +param sku string = 'Standard_LRS' + +@description('Key vault name.') +param addJobsStorageAccountPolicies bool = false + +@description('Specifies the Azure location where the storage account will be created.') +param location string + +@description('Key vault setting name to store the connection string.') +param storageAccountConnectionStringSettingName string + +resource storageAccount 'Microsoft.Storage/storageAccounts@2019-04-01' = { + name: name + location: location + kind: 'StorageV2' + sku: { + name: sku + } + properties: { + supportsHttpsTrafficOnly: true + allowBlobPublicAccess: false + minimumTlsVersion: 'TLS1_2' + } + identity: { + type: 'SystemAssigned' + } +} + +resource allBlobPolicy 'Microsoft.Storage/storageAccounts/managementPolicies@2022-05-01' = if (addJobsStorageAccountPolicies) { + name: 'default' + parent: storageAccount + properties: { + policy: { + rules: [ + { + definition: { + actions: { + baseBlob: { + delete: { + daysAfterModificationGreaterThan: 30 + } + } + } + filters: { + blobTypes: [ + 'blockBlob' + ] + } + } + enabled: true + name: '30-Day Blob Deletion Policy' + type: 'Lifecycle' + } + { + definition: { + actions: { + baseBlob: { + delete: { + daysAfterModificationGreaterThan: 7 + } + } + } + filters: { + blobTypes: [ + 'blockBlob' + ] + prefixMatch: [ + 'membership/cache/' + ] + } + } + enabled: true + name: '7-Day Cache Blob Deletion Policy' + type: 'Lifecycle' + } + ] + } + } +} + +module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { + name: 'secureSecretsTemplate${name}' + params: { + keyVaultName: keyVaultName + keyVaultSecrets: { + secrets: [ + { + name: storageAccountConnectionStringSettingName + value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value}' + } + ] + } + } +} + + diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/template.bicep index 8ee5ace63..ac93cda81 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/data/template.bicep @@ -14,5 +14,21 @@ param tenantId string @description('Enter storage account name.') param storageAccountName string +param storageAccountSku string = 'Standard_LRS' + @description('Resource location.') param location string + +var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' +var prodStorageAccountName = substring('js${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) + +module groupOwnershipObtainerStorageAccountProd 'storageAccount.bicep' = { + name: 'jsProdstorageAccountTemplate' + params: { + name: prodStorageAccountName + sku: storageAccountSku + keyVaultName: keyVaultName + location: location + storageAccountConnectionStringSettingName: 'jobSchedulerStorageAccountProd' + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionApp.bicep index bb9b03b1a..11625bfb4 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionApp.bicep @@ -82,25 +82,4 @@ resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@ } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.SubOrchestratorFunction.Disabled' - 'AzureWebJobs.EmailSenderFunction.Disabled' - 'AzureWebJobs.GroupNameReaderFunction.Disabled' - 'AzureWebJobs.GroupVerifierFunction.Disabled' - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' - 'AzureWebJobs.SyncJobsReaderFunction.Disabled' - 'AzureWebJobs.TopicMessageSenderFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 424a2c56c..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,86 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep index f943fad7b..e4fde88f1 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep @@ -68,18 +68,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the \'data\' key vault.') param dataKeyVaultName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' @@ -113,7 +104,6 @@ var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyV var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') var jobTriggerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobTriggerStorageAccountProd') -var jobTriggerStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobTriggerStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') module servicePlanTemplate 'servicePlan.bicep' = { @@ -164,21 +154,6 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobTriggerStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobTriggerStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.SubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.EmailSenderFunction.Disabled': 1 - 'AzureWebJobs.GroupNameReaderFunction.Disabled': 1 - 'AzureWebJobs.GroupVerifierFunction.Disabled': 1 - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 1 - 'AzureWebJobs.SyncJobsReaderFunction.Disabled': 1 - 'AzureWebJobs.TopicMessageSenderFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'JobTriggerStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobTriggerStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobTrigger' @@ -235,24 +210,6 @@ module functionAppTemplate_JobTrigger 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_JobTrigger 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-JobTrigger' - params: { - name: '${functionAppName}-JobTrigger/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_JobTrigger - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-JobTrigger' params: { @@ -263,11 +220,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_JobTrigger.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_JobTrigger.outputs.msi } dependsOn: [ functionAppTemplate_JobTrigger - functionAppSlotTemplate_JobTrigger ] } @@ -279,13 +234,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-JobTrigger/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/data/template.bicep index 392efa122..3d35e9d92 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/data/template.bicep @@ -21,7 +21,6 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('jt${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('jt${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module jobTriggerStorageAccountProd 'storageAccount.bicep' = { name: 'jtProdstorageAccountTemplate' @@ -33,14 +32,3 @@ module jobTriggerStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'jobTriggerStorageAccountProd' } } - -module jobTriggerStorageAccountStaging 'storageAccount.bicep' = { - name: 'jtStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'jobTriggerStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionApp.bicep index ce0ec5ad4..520ef4fc2 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionApp.bicep @@ -105,27 +105,4 @@ module secretsTemplate 'keyVaultSecrets.bicep' = { } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.ServiceBusStarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.MembershipSubOrchestratorFunction.Disabled' - 'AzureWebJobs.DeltaCalculatorFunction.Disabled' - 'AzureWebJobs.FileDownloaderFunction.Disabled' - 'AzureWebJobs.FileUploaderFunction.Disabled' - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' - 'AzureWebJobs.JobTrackerEntity.Disabled' - 'AzureWebJobs.LoggerFunction.Disabled' - 'AzureWebJobs.TelemetryTrackerFunction.Disabled' - 'AzureWebJobs.TopicMessageSenderFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 39924324c..000000000 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,129 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('Name of the \'data\' key vault.') -param dataKeyVaultName string - -@description('Name of the resource group where the \'data\' key vault is located.') -param dataKeyVaultResourceGroup string - -@description('Tenant id.') -param tenantId string - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -module secretsTemplate 'keyVaultSecrets.bicep' = { - name: 'secretsTemplate-MembershipAggregatorStaging' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - keyVaultParameters: [ - { - name: 'membershipAggregatorStagingUrl' - value: 'https://${functionAppSlot.properties.defaultHostName}/api/StarterFunction' - } - { - name: 'membershipAggregatorStagingFunctionName' - value: '${name}-MembershipAggregator/staging' - } - ] - } -} - -module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { - name: 'secureSecretsTemplate-MembershipAggregatorStaging' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - keyVaultSecrets: { - secrets: [ - { - name: 'membershipAggregatorStagingFunctionKey' - value: listkeys('${functionAppSlot.id}/host/default', '2018-11-01').functionKeys.default - } - ] - } - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep index c021eb5f6..c2a88d3fa 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep @@ -62,18 +62,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the \'data\' key vault.') param dataKeyVaultName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' @@ -116,7 +107,6 @@ var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dat var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var membershipAggregatorStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipAggregatorStorageAccountProd') -var membershipAggregatorStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipAggregatorStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') module servicePlanTemplate 'servicePlan.bicep' = { @@ -168,23 +158,6 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(membershipAggregatorStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}MembershipAggregatorStaging' - 'AzureWebJobs.ServiceBusStarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.MembershipSubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.DeltaCalculatorFunction.Disabled': 1 - 'AzureWebJobs.FileDownloaderFunction.Disabled': 1 - 'AzureWebJobs.FileUploaderFunction.Disabled': 1 - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 1 - 'AzureWebJobs.JobTrackerEntity.Disabled': 1 - 'AzureWebJobs.LoggerFunction.Disabled': 1 - 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 1 - 'AzureWebJobs.TopicMessageSenderFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: '${environmentAbbreviation}MembershipAggregatorStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(membershipAggregatorStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}MembershipAggregator' @@ -255,27 +228,6 @@ module functionAppTemplate_MembershipAggregator 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_MembershipAggregator 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-MembershipAggregator' - params: { - name: '${functionAppName}-MembershipAggregator/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - dataKeyVaultName: dataKeyVaultName - dataKeyVaultResourceGroup: dataKeyVaultResourceGroup - tenantId: tenantId - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_MembershipAggregator - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-MembershipAggregator' params: { @@ -286,11 +238,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_MembershipAggregator.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_MembershipAggregator.outputs.msi } dependsOn: [ functionAppTemplate_MembershipAggregator - functionAppSlotTemplate_MembershipAggregator ] } @@ -302,13 +252,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-MembershipAggregator/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/data/template.bicep index 5c6356fef..e8d2b024e 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/data/template.bicep @@ -20,7 +20,7 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('ma${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('ma${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) + module membershipAggregatorStorageAccountProd 'storageAccount.bicep' = { name: 'maProdstorageAccountTemplate' params: { @@ -31,13 +31,3 @@ module membershipAggregatorStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'membershipAggregatorStorageAccountProd' } } -module membershipAggregatorStorageAccountStaging 'storageAccount.bicep' = { - name: 'maStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'membershipAggregatorStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionApp.bicep index 14f65a3cf..a110ee826 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionApp.bicep @@ -118,23 +118,4 @@ module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.GroupUpdaterSubOrchestratorFunction.Disabled' - 'AzureWebJobs.GroupCreatorAndRetrieverFunction.Disabled' - 'AzureWebJobs.GroupUpdaterFunction.Disabled' - 'AzureWebJobs.LoggerFunction.Disabled' - 'AzureWebJobs.TenantUserReaderFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index cf69b3f83..000000000 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,124 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('Name of the \'data\' key vault.') -param dataKeyVaultName string - -@description('Name of the resource group where the \'data\' key vault is located.') -param dataKeyVaultResourceGroup string - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -module secretsTemplate 'keyVaultSecrets.bicep' = { - name: 'secretsTemplate-NonProdServiceStaging' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - keyVaultParameters: [ - { - name: 'nonProdServiceStagingUrl' - value: 'https://${functionAppSlot.properties.defaultHostName}/api/StarterFunction' - } - ] - } -} - -module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { - name: 'secureSecretsTemplate-NonProdServiceStaging' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - keyVaultSecrets: { - secrets: [ - { - name: 'nonProdServiceStagingKey' - value: listkeys('${functionAppSlot.id}/host/default', '2018-11-01').functionKeys.default - } - ] - } - } -} - - - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep index 998208e36..e5e0e405c 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep @@ -68,18 +68,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the \'data\' key vault.') param dataKeyVaultName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' @@ -119,7 +110,6 @@ var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, da var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var nonProdServiceStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'nonProdServiceStorageAccountProd') -var nonProdServiceStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'nonProdServiceStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') param appConfigurationKeyData array = [ @@ -204,19 +194,6 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(nonProdServiceStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}NonProdServiceStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.GroupUpdaterSubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.GroupCreatorAndRetrieverFunction.Disabled': 1 - 'AzureWebJobs.GroupUpdaterFunction.Disabled': 1 - 'AzureWebJobs.LoggerFunction.Disabled': 1 - 'AzureWebJobs.TenantUserReaderFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'NonProdServiceStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(nonProdServiceStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}NonProdService' @@ -282,26 +259,6 @@ module functionAppTemplate_NonProdService 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_NonProdService 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-NonProdService' - params: { - name: '${functionAppName}-NonProdService/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - dataKeyVaultName: dataKeyVaultName - dataKeyVaultResourceGroup: dataKeyVaultResourceGroup - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_NonProdService - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-NonProdService' params: { @@ -312,11 +269,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_NonProdService.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_NonProdService.outputs.msi } dependsOn: [ functionAppTemplate_NonProdService - functionAppSlotTemplate_NonProdService ] } @@ -328,13 +283,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-NonProdService/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/data/template.bicep index 76755ba6e..4541c2488 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/data/template.bicep @@ -21,7 +21,6 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('nps${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('nps${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module nonProdServiceStorageAccountProd 'storageAccount.bicep' = { name: 'npsProdstorageAccountTemplate' @@ -33,14 +32,3 @@ module nonProdServiceStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'nonProdServiceStorageAccountProd' } } - -module nonProdServiceStorageAccountStaging 'storageAccount.bicep' = { - name: 'npsStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'nonProdServiceStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionApp.bicep index dbda0b35a..f19185273 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionApp.bicep @@ -118,22 +118,4 @@ module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.RetrieveNotificationsFunction.Disabled' - 'AzureWebJobs.LoggerFunction.Disabled' - 'AzureWebJobs.UpdateNotificationStatusFunction.Disabled' - 'AzureWebJobs.SendNotificationFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 424a2c56c..000000000 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,86 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep index 5a3f5dd23..9fc6964e8 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep @@ -74,18 +74,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the \'data\' key vault.') param dataKeyVaultName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' @@ -100,7 +91,6 @@ param setRBACPermissions bool = false var logAnalyticsCustomerId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsCustomerId') var logAnalyticsPrimarySharedKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsPrimarySharedKey') -var storageAccountConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'storageAccountConnectionString') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var graphAppClientId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppClientId') var graphAppClientSecret = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppClientSecret') @@ -113,7 +103,6 @@ var jobsStorageAccountConnectionString = resourceId(subscription().subscriptionI var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var notifierStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierStorageAccountProd') -var notifierStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierStorageAccountStaging') var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') var serviceBusFailedNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusFailedNotificationsQueue') var serviceBusFQN = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusFQN') @@ -164,19 +153,6 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(notifierStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - WEBSITE_CONTENTSHARE: toLower('functionApp-Notifier-staging') - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}NotifierStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.RetrieveNotificationsFunction.Disabled': 1 - 'AzureWebJobs.LoggerFunction.Disabled': 1 - 'AzureWebJobs.UpdateNotificationStatusFunction.Disabled': 1 - 'AzureWebJobs.SendNotificationFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'NotifierStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(notifierStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}Notifier' @@ -235,24 +211,6 @@ module functionAppTemplate_Notifier 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_Notifier 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-Notifier' - params: { - name: '${functionAppName}-Notifier/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_Notifier - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-Notifier' params: { @@ -263,11 +221,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_Notifier.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_Notifier.outputs.msi } dependsOn: [ functionAppTemplate_Notifier - functionAppSlotTemplate_Notifier ] } @@ -279,13 +235,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-Notifier/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/data/template.bicep index ba629150b..104bad45d 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/data/template.bicep @@ -21,7 +21,6 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('ntf${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('ntf${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module notifierStorageAccountProd 'storageAccount.bicep' = { name: 'ntfProdstorageAccountTemplate' @@ -33,14 +32,3 @@ module notifierStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'notifierStorageAccountProd' } } - -module notifierStorageAccountStaging 'storageAccount.bicep' = { - name: 'ntfStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'notifierStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionApp.bicep index aac666b28..e455b4815 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionApp.bicep @@ -81,25 +81,4 @@ resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@ } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.UsersSenderFunction.Disabled' - 'AzureWebJobs.QueueMessageSenderFunction.Disabled' - 'AzureWebJobs.SubOrchestratorFunction.Disabled' - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' - 'AzureWebJobs.RoomsReaderFunction.Disabled' - 'AzureWebJobs.WorkspacesReaderFunction.Disabled' - 'AzureWebJobs.UsersReaderFunction.Disabled' - 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index f63831d4a..000000000 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,85 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep index 0c59e0cb8..9beee11e6 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep @@ -42,18 +42,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the resource group where the \'prereqs\' key vault is located.') param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' @@ -87,10 +78,10 @@ var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, pre var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') -var storageAccountConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'storageAccountConnectionString') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') +var placeMembershipObtainerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'placeMembershipObtainerStorageAccountProd') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') module servicePlanTemplate 'servicePlan.bicep' = { @@ -112,10 +103,9 @@ var commonSettings = { } var appSettings = { + WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(placeMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' 'AzureFunctionsJobHost:extensions:durableTask:extendedSessionsEnabled': toLower(environmentAbbreviation) == 'prodv2' ? 'True' : 'False' APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(storageAccountConnectionString, '2019-09-01').secretUriWithVersion})' - WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(storageAccountConnectionString, '2019-09-01').secretUriWithVersion})' serviceBusSyncJobTopic: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusSyncJobTopic, '2019-09-01').secretUriWithVersion})' gmmServiceBus__fullyQualifiedNamespace: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusFQN, '2019-09-01').secretUriWithVersion})' serviceBusMembershipAggregatorQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusMembershipAggregatorQueue, '2019-09-01').secretUriWithVersion})' @@ -139,23 +129,8 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - WEBSITE_CONTENTSHARE: toLower('functionApp-PlaceMembershipObtainer-staging') - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}PlaceMembershipObtainerStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.UsersSenderFunction.Disabled': 1 - 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 1 - 'AzureWebJobs.SubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 1 - 'AzureWebJobs.RoomsReaderFunction.Disabled': 1 - 'AzureWebJobs.WorkspacesReaderFunction.Disabled': 1 - 'AzureWebJobs.UsersReaderFunction.Disabled': 1 - 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'PlaceMembershipObtainerStaging' -} - var productionSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(placeMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-PlaceMembershipObtainer') AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}PlaceMembershipObtainer' 'AzureWebJobs.StarterFunction.Disabled': 0 @@ -221,24 +196,6 @@ module functionAppTemplate_PlaceMembershipObtainer 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_PlaceMembershipObtainer 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-PlaceMembershipObtainer' - params: { - name: '${functionAppName}-PlaceMembershipObtainer/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_PlaceMembershipObtainer - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-PlaceMembershipObtainer' params: { @@ -249,11 +206,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_PlaceMembershipObtainer.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_PlaceMembershipObtainer.outputs.msi } dependsOn: [ functionAppTemplate_PlaceMembershipObtainer - functionAppSlotTemplate_PlaceMembershipObtainer ] } @@ -265,13 +220,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-03-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-03-01' = { - name: '${functionAppName}-PlaceMembershipObtainer/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/keyVaultSecretsSecure.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/keyVaultSecretsSecure.bicep new file mode 100644 index 000000000..bbb8c8b38 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/keyVaultSecretsSecure.bicep @@ -0,0 +1,10 @@ +param keyVaultName string +@secure() +param keyVaultSecrets object + +resource secrets 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = [for secret in keyVaultSecrets.secrets: { + name: '${keyVaultName}/${secret.name}' + properties: { + value: secret.value + } +}] diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/storageAccount.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/storageAccount.bicep new file mode 100644 index 000000000..4cb0b158d --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/storageAccount.bicep @@ -0,0 +1,108 @@ +@description('Storage account alphanumeric name.') +@minLength(1) +@maxLength(24) +param name string + +@description('Key vault name.') +param keyVaultName string + +@allowed([ + 'Standard_LRS' + 'Standard_GRS' + 'Standard_ZRS' + 'Premium_LRS' +]) +param sku string = 'Standard_LRS' + +@description('Key vault name.') +param addJobsStorageAccountPolicies bool = false + +@description('Specifies the Azure location where the storage account will be created.') +param location string + +@description('Key vault setting name to store the connection string.') +param storageAccountConnectionStringSettingName string + +resource storageAccount 'Microsoft.Storage/storageAccounts@2019-04-01' = { + name: name + location: location + kind: 'StorageV2' + sku: { + name: sku + } + properties: { + supportsHttpsTrafficOnly: true + allowBlobPublicAccess: false + minimumTlsVersion: 'TLS1_2' + } + identity: { + type: 'SystemAssigned' + } +} + +resource allBlobPolicy 'Microsoft.Storage/storageAccounts/managementPolicies@2022-05-01' = if (addJobsStorageAccountPolicies) { + name: 'default' + parent: storageAccount + properties: { + policy: { + rules: [ + { + definition: { + actions: { + baseBlob: { + delete: { + daysAfterModificationGreaterThan: 30 + } + } + } + filters: { + blobTypes: [ + 'blockBlob' + ] + } + } + enabled: true + name: '30-Day Blob Deletion Policy' + type: 'Lifecycle' + } + { + definition: { + actions: { + baseBlob: { + delete: { + daysAfterModificationGreaterThan: 7 + } + } + } + filters: { + blobTypes: [ + 'blockBlob' + ] + prefixMatch: [ + 'membership/cache/' + ] + } + } + enabled: true + name: '7-Day Cache Blob Deletion Policy' + type: 'Lifecycle' + } + ] + } + } +} + +module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { + name: 'secureSecretsTemplate${name}' + params: { + keyVaultName: keyVaultName + keyVaultSecrets: { + secrets: [ + { + name: storageAccountConnectionStringSettingName + value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value}' + } + ] + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/template.bicep index 8ee5ace63..dfc091d87 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/data/template.bicep @@ -14,5 +14,21 @@ param tenantId string @description('Enter storage account name.') param storageAccountName string +param storageAccountSku string = 'Standard_LRS' + @description('Resource location.') param location string + +var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' +var prodStorageAccountName = substring('pmo${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) + +module notifierStorageAccountProd 'storageAccount.bicep' = { + name: 'pmoProdstorageAccountTemplate' + params: { + name: prodStorageAccountName + sku: storageAccountSku + keyVaultName: keyVaultName + location: location + storageAccountConnectionStringSettingName: 'placeMembershipObtainerStorageAccountProd' + } +} diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionApp.bicep index a5fd04756..11625bfb4 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionApp.bicep @@ -82,29 +82,4 @@ resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@ } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.ManagerOrgProcessorFunction.Disabled' - 'AzureWebJobs.OrganizationProcessorFunction.Disabled' - 'AzureWebJobs.ChildEntitiesFilterFunction.Disabled' - 'AzureWebJobs.GroupMembershipSenderFunction.Disabled' - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' - 'AzureWebJobs.LoggerFunction.Disabled' - 'AzureWebJobs.ManagerOrgReaderFunction.Disabled' - 'AzureWebJobs.TableNameReaderFunction.Disabled' - 'AzureWebJobs.TelemetryTrackerFunction.Disabled' - 'AzureWebJobs.FeatureFlagFunction.Disabled' - 'AzureWebJobs.QueueMessageSenderFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 424a2c56c..000000000 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,86 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep index 559cb2422..cf1307688 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep @@ -42,18 +42,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the \'data\' key vault.') param dataKeyVaultName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' @@ -116,7 +107,6 @@ var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, da var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierProviderId') var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var sqlMembershipObtainerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'sqlMembershipObtainerStorageAccountProd') -var sqlMembershipObtainerStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'sqlMembershipObtainerStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') module servicePlanTemplate 'servicePlan.bicep' = { @@ -175,28 +165,22 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(sqlMembershipObtainerStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}SqlMembershipObtainerStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.ManagerOrgProcessorFunction.Disabled': 1 - 'AzureWebJobs.OrganizationProcessorFunction.Disabled': 1 - 'AzureWebJobs.ChildEntitiesFilterFunction.Disabled': 1 - 'AzureWebJobs.GroupMembershipSenderFunction.Disabled': 1 - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 1 - 'AzureWebJobs.LoggerFunction.Disabled': 1 - 'AzureWebJobs.ManagerOrgReaderFunction.Disabled': 1 - 'AzureWebJobs.TableNameReaderFunction.Disabled': 1 - 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 1 - 'AzureWebJobs.FeatureFlagFunction.Disabled': 1 - 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'SqlMembershipObtainerStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(sqlMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}SqlMembershipObtainer' + 'AzureWebJobs.StarterFunction.Disabled': 0 + 'AzureWebJobs.OrchestratorFunction.Disabled': 0 + 'AzureWebJobs.ManagerOrgProcessorFunction.Disabled': 0 + 'AzureWebJobs.OrganizationProcessorFunction.Disabled': 0 + 'AzureWebJobs.ChildEntitiesFilterFunction.Disabled': 0 + 'AzureWebJobs.GroupMembershipSenderFunction.Disabled': 0 + 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 0 + 'AzureWebJobs.LoggerFunction.Disabled': 0 + 'AzureWebJobs.ManagerOrgReaderFunction.Disabled': 0 + 'AzureWebJobs.TableNameReaderFunction.Disabled': 0 + 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 0 + 'AzureWebJobs.FeatureFlagFunction.Disabled': 0 + 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 0 AzureFunctionsWebHost__hostid: 'SqlMembershipObtainer' } @@ -250,24 +234,6 @@ module functionAppTemplate_SqlMembershipObtainer 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_SqlMembershipObtainer 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-SqlMembershipObtainer' - params: { - name: '${functionAppName}-SqlMembershipObtainer/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_SqlMembershipObtainer - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-SqlMembershipObtainer' params: { @@ -278,11 +244,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_SqlMembershipObtainer.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_SqlMembershipObtainer.outputs.msi } dependsOn: [ functionAppTemplate_SqlMembershipObtainer - functionAppSlotTemplate_SqlMembershipObtainer ] } @@ -294,13 +258,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-SqlMembershipObtainer/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/data/template.bicep index 7382d3c8d..6fd4177e8 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/data/template.bicep @@ -8,12 +8,12 @@ param environmentAbbreviation string @maxLength(3) param solutionAbbreviation string = 'gmm' +@description('Enter tenant Id.') +param tenantId string + @description('Enter storage account name.') param storageAccountName string -@description('Tenant id.') -param tenantId string - @description('Resource location.') param location string @@ -24,7 +24,6 @@ param storageAccountSku string = 'Standard_LRS' var dataKeyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('sqlmo${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('sqlmo${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module smoStorageAccountProd 'storageAccount.bicep' = { name: 'smoProdstorageAccountTemplate' @@ -37,15 +36,3 @@ module smoStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'sqlMembershipObtainerStorageAccountProd' } } - -module smoStorageAccountStaging 'storageAccount.bicep' = { - name: 'smoStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: dataKeyVaultName - location: location - sqlMembershipObtainerStorageAccountName: 'sqlMembershipObtainerStorageAccountNameStaging' - storageAccountConnectionStringSettingName: 'sqlMembershipObtainerStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionApp.bicep index 9162c014a..11625bfb4 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionApp.bicep @@ -82,33 +82,4 @@ resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@ } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.SubOrchestratorFunction.Disabled' - 'AzureWebJobs.DeltaUsersReaderFunction.Disabled' - 'AzureWebJobs.DeltaUsersSenderFunction.Disabled' - 'AzureWebJobs.EmailSenderFunction.Disabled' - 'AzureWebJobs.FileDownloaderFunction.Disabled' - 'AzureWebJobs.GroupsReaderFunction.Disabled' - 'AzureWebJobs.GroupValidatorFunction.Disabled' - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' - 'AzureWebJobs.MembersReaderFunction.Disabled' - 'AzureWebJobs.SourceGroupsReaderFunction.Disabled' - 'AzureWebJobs.SubsequentDeltaUsersReaderFunction.Disabled' - 'AzureWebJobs.SubsequentMembersReaderFunction.Disabled' - 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled' - 'AzureWebJobs.UsersReaderFunction.Disabled' - 'AzureWebJobs.UsersSenderFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 424a2c56c..000000000 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,86 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep index 58974e732..824659129 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep @@ -42,18 +42,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the resource group where the \'prereqs\' key vault is located.') param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' @@ -93,7 +84,6 @@ var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, da var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var teamsChannelMembershipObtainerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'teamsChannelMembershipObtainerStorageAccountProd') -var teamsChannelMembershipObtainerStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'teamsChannelMembershipObtainerStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') module servicePlanTemplate 'servicePlan.bicep' = { @@ -144,13 +134,6 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelMembershipObtainerStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}TeamsChannelMOStaging' - 'TeamsChannelMembershipObtainer:IsDryRunEnabled': 1 - AzureFunctionsWebHost__hostid: 'TeamsChannelMOStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}TeamsChannelMO' @@ -208,24 +191,6 @@ module functionAppTemplate_TeamsChannelMembershipObtainer 'functionApp.bicep' = ] } -module functionAppSlotTemplate_TeamsChannelMembershipObtainer 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-TeamsChannelMembershipObtainer' - params: { - name: '${functionAppName}-TeamsChannelMembershipObtainer/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_TeamsChannelMembershipObtainer - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-TeamsChannelMembershipObtainer' params: { @@ -236,11 +201,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_TeamsChannelMembershipObtainer.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_TeamsChannelMembershipObtainer.outputs.msi } dependsOn: [ functionAppTemplate_TeamsChannelMembershipObtainer - functionAppSlotTemplate_TeamsChannelMembershipObtainer ] } @@ -252,13 +215,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-TeamsChannelMembershipObtainer/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/data/template.bicep index 737fc124f..a0fc70580 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/data/template.bicep @@ -21,7 +21,7 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('tcmo${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('tcmo${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) + module teamsChannelMembershipObtainerStorageAccountProd 'storageAccount.bicep' = { name: 'tcmoProdstorageAccountTemplate' params: { @@ -32,13 +32,3 @@ module teamsChannelMembershipObtainerStorageAccountProd 'storageAccount.bicep' = storageAccountConnectionStringSettingName: 'teamsChannelMembershipObtainerStorageAccountProd' } } -module teamsChannelMembershipObtainerStorageAccountStaging 'storageAccount.bicep' = { - name: 'tcmoStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'teamsChannelMembershipObtainerStorageAccountStaging' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionApp.bicep index 448b2f4eb..11625bfb4 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionApp.bicep @@ -82,28 +82,4 @@ resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@ } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.JobReaderFunction.Disabled' - 'AzureWebJobs.FileDownloaderFunction.Disabled' - 'AzureWebJobs.LoggerFunction.Disabled' - 'AzureWebJobs.EmailSenderFunction.Disabled' - 'AzureWebJobs.GroupOwnersReaderFunction.Disabled' - 'AzureWebJobs.GroupNameReaderFunction.Disabled' - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' - 'AzureWebJobs.TelemetryTrackerFunction.Disabled' - 'AzureWebJobs.TeamsChannelUpdaterSubOrchestratorFunction.Disabled' - 'AzureWebJobs.TeamsUpdaterFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..57760cd75 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionAppRBAC.bicep @@ -16,8 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string param functionName string @@ -40,23 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { roleName: 'Key Vault Secrets User' } } - -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 424a2c56c..000000000 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,86 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep index a01421db0..3f2ca2485 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep @@ -42,18 +42,9 @@ param functionAppKind string = 'functionapp' @description('Maximum elastic worker count.') param maximumElasticWorkerCount int = 1 -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Enter storage account name.') param storageAccountName string -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the resource group where the \'prereqs\' key vault is located.') param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' @@ -92,7 +83,6 @@ var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, da var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var teamsChannelUpdaterStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'teamsChannelUpdaterStorageAccountProd') -var teamsChannelUpdaterStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'teamsChannelUpdaterStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') module servicePlanTemplate 'servicePlan.bicep' = { @@ -143,24 +133,6 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelUpdaterStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}TeamsChannelUpdaterStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.JobReaderFunction.Disabled': 1 - 'AzureWebJobs.FileDownloaderFunction.Disabled': 1 - 'AzureWebJobs.LoggerFunction.Disabled': 1 - 'AzureWebJobs.EmailSenderFunction.Disabled': 1 - 'AzureWebJobs.GroupOwnersReaderFunction.Disabled': 1 - 'AzureWebJobs.GroupNameReaderFunction.Disabled': 1 - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 1 - 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 1 - 'AzureWebJobs.TeamsChannelUpdaterSubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.TeamsUpdaterFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'TeamsChannelUpdaterStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}TeamsChannelUpdater' @@ -229,24 +201,6 @@ module functionAppTemplate_TeamsChannelUpdater 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_TeamsChannelUpdater 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-TeamsChannelUpdater' - params: { - name: '${functionAppName}-TeamsChannelUpdater/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_TeamsChannelUpdater - ] -} - module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-TeamsChannelUpdater' params: { @@ -257,11 +211,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_TeamsChannelUpdater.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_TeamsChannelUpdater.outputs.msi } dependsOn: [ functionAppTemplate_TeamsChannelUpdater - functionAppSlotTemplate_TeamsChannelUpdater ] } @@ -273,13 +225,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { functionAppRBAC ] } - -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-TeamsChannelUpdater/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/data/template.bicep index b7ad7f220..56247a99e 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/data/template.bicep @@ -21,7 +21,6 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('tcu${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('tcu${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module teamsChannelUpdaterStorageAccountProd 'storageAccount.bicep' = { name: 'tcuProdstorageAccountTemplate' @@ -33,14 +32,3 @@ module teamsChannelUpdaterStorageAccountProd 'storageAccount.bicep' = { storageAccountConnectionStringSettingName: 'teamsChannelUpdaterStorageAccountProd' } } - -module teamsChannelUpdaterStorageAccountStaging 'storageAccount.bicep' = { - name: 'tcuStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'teamsChannelUpdaterStorageAccountStaging' - } -} From c5e297b8621bc3a9cef45d9b87139b8a271d5974 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 15 Jul 2024 13:32:15 -0700 Subject: [PATCH 0094/1479] Updated and rearranged activity function disabled settings for function app biceps --- .../Infrastructure/compute/template.bicep | 19 +++++------ .../Infrastructure/compute/template.bicep | 18 ++++++++--- .../Infrastructure/compute/template.bicep | 16 +++++++--- .../Infrastructure/compute/template.bicep | 27 +++++++++++++--- .../Infrastructure/compute/template.bicep | 32 ++++++++++++++++--- .../Infrastructure/compute/template.bicep | 14 ++++---- .../Infrastructure/compute/template.bicep | 24 +++++++------- .../Infrastructure/compute/template.bicep | 24 +++++++++++--- .../Infrastructure/compute/template.bicep | 16 ++++++---- .../Infrastructure/compute/template.bicep | 16 +++++++--- .../Infrastructure/compute/template.bicep | 19 ++++++++--- .../Infrastructure/compute/template.bicep | 20 ++++++------ .../Infrastructure/compute/template.bicep | 15 ++++----- .../Infrastructure/compute/template.bicep | 20 ++++++++---- .../Infrastructure/compute/template.bicep | 22 ++++++------- 15 files changed, 194 insertions(+), 108 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep index 36e89f0e5..b2f84162a 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep @@ -100,6 +100,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(azureMaintenanceStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}AzureMaintenance' + AzureFunctionsWebHost__hostid: 'AzureMaintenance' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(azureMaintenanceStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-AzureMaintenance') APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' @@ -124,23 +127,17 @@ var appSettings = { serviceBusNotificationsQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusNotificationsQueue, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(azureMaintenanceStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}AzureMaintenance' +var activityFunctionSettings = { 'AzureWebJobs.StarterFunction.Disabled': 0 'AzureWebJobs.OrchestratorFunction.Disabled': 0 - 'AzureWebJobs.LoggerFunction.Disabled': 0 - 'AzureWebJobs.RetrieveBackupsFunction.Disabled': 0 - 'AzureWebJobs.ReviewAndDeleteFunction.Disabled': 0 - 'AzureWebJobs.TableBackupFunction.Disabled': 0 'AzureWebJobs.BackUpInactiveJobsFunction.Disabled': 0 + 'AzureWebJobs.EmailSenderFunction.Disabled': 0 + 'AzureWebJobs.ExpireNotificationsFunction.Disabled': 0 + 'AzureWebJobs.LoggerFunction.Disabled': 0 'AzureWebJobs.ReadGroupNameFunction.Disabled': 0 'AzureWebJobs.ReadSyncJobsFunction.Disabled': 0 'AzureWebJobs.RemoveBackUpsFunction.Disabled': 0 'AzureWebJobs.RemoveInactiveJobsFunction.Disabled': 0 - 'AzureWebJobs.SendEmailFunction.Disabled': 0 - 'AzureWebJobs.ExpireNotificationsFunction.Disabled': 0 - AzureFunctionsWebHost__hostid: 'AzureMaintenance' } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -213,7 +210,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-AzureMaintenance/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep index d2b029119..706824a25 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep @@ -98,6 +98,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(azureUserReaderStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}AzureUserReader' + AzureFunctionsWebHost__hostid: 'AzureUserReader' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(azureUserReaderStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-AzureUserReader') APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' @@ -117,10 +120,15 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(azureUserReaderStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}AzureUserReader' - AzureFunctionsWebHost__hostid: 'AzureUserReader' +var activityFunctionSettings = { + 'AzureWebJobs.OrchestratorFunction.Disabled': 0 + 'AzureWebJobs.PersonnelNumberReaderFunction.Disabled': 0 + 'AzureWebJobs.StarterFunction.Disabled': 0 + 'AzureWebJobs.UploadUsersFunction.Disabled': 0 + 'AzureWebJobs.AzureUserCreatorFunction.Disabled': 0 + 'AzureWebJobs.UserCreatorSubOrchestratorFunction.Disabled': 0 + 'AzureWebJobs.AzureUserReaderFunction.Disabled': 0 + 'AzureWebJobs.UserReaderSubOrchestratorFunction.Disabled': 0 } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -194,7 +202,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-AzureUserReader/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep index a9e29dd2f..5f66c62ae 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep @@ -123,6 +123,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(destinationAttributesUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}DestinationAttributesUpdater' + AzureFunctionsWebHost__hostid: 'DestinationAttributesUpdater' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(destinationAttributesUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-DestinationAttributesUpdater') APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' @@ -150,10 +153,13 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(destinationAttributesUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}DestinationAttributesUpdater' - AzureFunctionsWebHost__hostid: 'DestinationAttributesUpdater' +var activityFunctionSettings = { + 'AzureWebJobs.StarterFunction.Disabled': 0 + 'AzureWebJobs.OrchestratorFunction.Disabled': 0 + 'AzureWebJobs.AttributeCacheUpdaterFunction.Disabled': 0 + 'AzureWebJobs.AttributeReaderFunction.Disabled': 0 + 'AzureWebJobs.DestinationReaderFunction.Disabled': 0 + 'AzureWebJobs.LoggerFunction.Disabled': 0 } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -225,7 +231,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-DestinationAttributesUpdater/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep index 4b1893e45..0a5b66d0d 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep @@ -105,6 +105,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(graphUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GraphUpdater' + AzureFunctionsWebHost__hostid: 'GraphUpdater' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(graphUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-GraphUpdater') APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' @@ -134,10 +137,24 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GraphUpdater' - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(graphUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsWebHost__hostid: 'GraphUpdater' +var activityFunctionSettings = { + 'AzureWebJobs.StarterFunction.Disabled': 0 + 'AzureWebJobs.OrchestratorFunction.Disabled': 0 + 'AzureWebJobs.QueueMessageOrchestratorFunction.Disabled': 0 + 'AzureWebJobs.CacheUserUpdaterSubOrchestratorFunction.Disabled': 0 + 'AzureWebJobs.GroupUpdaterSubOrchestratorFunction.Disabled': 0 + 'AzureWebJobs.CacheUpdaterFunction.Disabled': 0 + 'AzureWebJobs.EmailSenderFunction.Disabled': 0 + 'AzureWebJobs.FileDownloaderFunction.Disabled': 0 + 'AzureWebJobs.GroupNameReaderFunction.Disabled': 0 + 'AzureWebJobs.GroupOwnersReaderFunction.Disabled': 0 + 'AzureWebJobs.GroupUpdaterFunction.Disabled': 0 + 'AzureWebJobs.GroupValidatorFunction.Disabled': 0 + 'AzureWebJobs.JobReaderFunction.Disabled': 0 + 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 0 + 'AzureWebJobs.LoggerFunction.Disabled': 0 + 'AzureWebJobs.MessageReaderFunction.Disabled': 0 + 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 0 } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -212,7 +229,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-GraphUpdater/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep index c7ebe0263..3abcd38d9 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep @@ -105,6 +105,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupMembershipObtainer' + AzureFunctionsWebHost__hostid: 'GroupMembershipObtainer' 'AzureFunctionsJobHost:extensions:durableTask:extendedSessionsEnabled': toLower(environmentAbbreviation) == 'prodv2' ? 'True' : 'False' APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' @@ -134,10 +137,29 @@ var appSettings = { serviceBusNotificationsQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusNotificationsQueue, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupMembershipObtainer' - AzureFunctionsWebHost__hostid: 'GroupMembershipObtainer' +var activityFunctionSettings = { + 'AzureWebJobs.StarterFunction.Disabled': 0 + 'AzureWebJobs.OrchestratorFunction.Disabled': 0 + 'AzureWebJobs.SubOrchestratorFunction.Disabled': 0 + 'AzureWebJobs.DeltaUsersReaderFunction.Disabled': 0 + 'AzureWebJobs.DeltaUsersSenderFunction.Disabled': 0 + 'AzureWebJobs.DestinationNameReaderFunction.Disabled': 0 + 'AzureWebJobs.EmailSenderFunction.Disabled': 0 + 'AzureWebJobs.FeatureFlagReaderFunction.Disabled': 0 + 'AzureWebJobs.FileDeleterFunction.Disabled': 0 + 'AzureWebJobs.FileDownloaderFunction.Disabled': 0 + 'AzureWebJobs.GetTransitiveGroupCountFunction.Disabled': 0 + 'AzureWebJobs.GetUserCountFunction.Disabled': 0 + 'AzureWebJobs.GroupReaderFunction.Disabled': 0 + 'AzureWebJobs.GroupValidatorFunction.Disabled': 0 + 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 0 + 'AzureWebJobs.MembersReaderFunction.Disabled': 0 + 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 0 + 'AzureWebJobs.SubsequentDeltaUsersReaderFunction.Disabled': 0 + 'AzureWebJobs.SubsequentMembersReaderFunction.Disabled': 0 + 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled': 0 + 'AzureWebJobs.UsersReaderFunction.Disabled': 0 + 'AzureWebJobs.UsersSenderFunction.Disabled': 0 } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -209,7 +231,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-GroupMembershipObtainer/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep index d51ae6083..4422b92ce 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep @@ -104,6 +104,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupOwnershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupOwnershipObtainer' + AzureFunctionsWebHost__hostid: 'GroupOwnershipObtainer' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(groupOwnershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-GroupOwnershipObtainer') 'AzureFunctionsJobHost:extensions:durableTask:extendedSessionsEnabled': toLower(environmentAbbreviation) == 'prodv2' ? 'True' : 'False' @@ -132,21 +135,18 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupOwnershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupOwnershipObtainer' +var activityFunctionSettings = { 'AzureWebJobs.StarterFunction.Disabled': 0 'AzureWebJobs.OrchestratorFunction.Disabled': 0 + 'AzureWebJobs.FeatureFlagFunction.Disabled': 0 'AzureWebJobs.GetGroupOwnersFunction.Disabled': 0 'AzureWebJobs.GetJobsSegmentedFunction.Disabled': 0 'AzureWebJobs.JobsFilterFunction.Disabled': 0 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 0 'AzureWebJobs.LoggerFunction.Disabled': 0 + 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 0 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 0 'AzureWebJobs.UsersSenderFunction.Disabled': 0 - 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 0 - 'AzureWebJobs.FeatureFlagFunction.Disabled': 0 - AzureFunctionsWebHost__hostid: 'GroupOwnershipObtainer' } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -218,7 +218,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-GroupOwnershipObtainer/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep index eaf95541a..80877a3a6 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep @@ -85,7 +85,6 @@ param setRBACPermissions bool = false var logAnalyticsCustomerId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsCustomerId') var logAnalyticsPrimarySharedKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsPrimarySharedKey') -var storageAccountConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'storageAccountConnectionString') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') @@ -110,10 +109,12 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobSchedulerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobScheduler' + AzureFunctionsWebHost__hostid: 'JobScheduler' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(jobSchedulerStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-JobScheduler') APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(storageAccountConnectionString, '2019-09-01').secretUriWithVersion})' jobSchedulerSchedule: '0 0 0 * * Sun' logAnalyticsCustomerId: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsCustomerId, '2019-09-01').secretUriWithVersion})' logAnalyticsPrimarySharedKey: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsPrimarySharedKey, '2019-09-01').secretUriWithVersion})' @@ -122,23 +123,20 @@ var appSettings = { 'ConnectionStrings:JobsContextReadOnly': '@Microsoft.KeyVault(SecretUri=${reference(replicaJobsMSIConnectionString, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobSchedulerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobScheduler' +var activityFunctionSettings = { 'AzureWebJobs.StarterFunction.Disabled': 0 + 'AzureWebJobs.PipelineInvocationStarterFunction.Disabled': 0 'AzureWebJobs.OrchestratorFunction.Disabled': 0 - 'AzureWebJobs.LoggerFunction.Disabled': 0 'AzureWebJobs.GetJobsSubOrchestratorFunction.Disabled': 0 - 'AzureWebJobs.GetJobsSegmentedFunction.Disabled': 0 - 'AzureWebJobs.ResetJobsFunction.Disabled': 0 - 'AzureWebJobs.DistributeJobsFunction.Disabled': 0 + 'AzureWebJobs.StatusCallbackOrchestratorFunction.Disabled': 0 'AzureWebJobs.UpdateJobsSubOrchestratorFunction.Disabled': 0 'AzureWebJobs.BatchUpdateJobsFunction.Disabled': 0 - 'AzureWebJobs.PipelineInvocationStarterFunction.Disabled': 0 - 'AzureWebJobs.StatusCallbackOrchestratorFunction.Disabled': 0 'AzureWebJobs.CheckJobSchedulerStatusFunction.Disabled': 0 + 'AzureWebJobs.DistributeJobsFunction.Disabled': 0 + 'AzureWebJobs.GetJobsSegmentedFunction.Disabled': 0 + 'AzureWebJobs.LoggerFunction.Disabled': 0 'AzureWebJobs.PostCallbackFunction.Disabled': 0 - AzureFunctionsWebHost__hostid: 'JobScheduler' + 'AzureWebJobs.ResetJobsFunction.Disabled': 0 } module existingLogAnalyticsWorkspace 'logAnalyticsWorkspace.bicep' = { @@ -188,7 +186,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-03-01' = { name: '${functionAppName}-JobScheduler/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep index e4fde88f1..f03a60e35 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep @@ -125,6 +125,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobTriggerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobTrigger' + AzureFunctionsWebHost__hostid: 'JobTrigger' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(jobTriggerStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-JobTrigger') APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' @@ -154,10 +157,21 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobTriggerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobTrigger' - AzureFunctionsWebHost__hostid: 'JobTrigger' +var activityFunctionSettings = { + 'AzureWebJobs.StarterFunction.Disabled': 0 + 'AzureWebJobs.OrchestratorFunction.Disabled': 0 + 'AzureWebJobs.SubOrchestratorFunction.Disabled': 0 + 'AzureWebJobs.DestinationNameReaderFunction.Disabled': 0 + 'AzureWebJobs.DestinationVerifierFunction.Disabled': 0 + 'AzureWebJobs.EmailSenderFunction.Disabled': 0 + 'AzureWebJobs.GetJobsFunction.Disabled': 0 + 'AzureWebJobs.JobTrackerFunction.Disabled': 0 + 'AzureWebJobs.JobUpdaterFunction.Disabled': 0 + 'AzureWebJobs.LoggerFunction.Disabled': 0 + 'AzureWebJobs.ParseAndValidateDestinationFunction.Disabled': 0 + 'AzureWebJobs.SchemaValidatorFunction.Disabled': 0 + 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 0 + 'AzureWebJobs.TopicMessageSenderFunction.Disabled': 0 } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -229,7 +243,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-JobTrigger/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep index c2a88d3fa..b0dd66efa 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep @@ -128,6 +128,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(membershipAggregatorStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}MembershipAggregator' + AzureFunctionsWebHost__hostid: 'MembershipAggregator' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(membershipAggregatorStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-MembershipAggregator') APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' @@ -158,21 +161,20 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(membershipAggregatorStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}MembershipAggregator' - 'AzureWebJobs.ServiceBusStarterFunction.Disabled': 0 +var activityFunctionSettings = { + 'AzureWebJobs.StarterFunction.Disabled': 0 'AzureWebJobs.OrchestratorFunction.Disabled': 0 'AzureWebJobs.MembershipSubOrchestratorFunction.Disabled': 0 'AzureWebJobs.DeltaCalculatorFunction.Disabled': 0 + 'AzureWebJobs.EmailSenderFunction.Disabled': 0 'AzureWebJobs.FileDownloaderFunction.Disabled': 0 'AzureWebJobs.FileUploaderFunction.Disabled': 0 + 'AzureWebJobs.GroupNameReaderFunction.Disabled': 0 + 'AzureWebJobs.JobReaderFunction.Disabled': 0 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 0 - 'AzureWebJobs.JobTrackerEntity.Disabled': 0 'AzureWebJobs.LoggerFunction.Disabled': 0 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 0 'AzureWebJobs.TopicMessageSenderFunction.Disabled': 0 - AzureFunctionsWebHost__hostid: 'MembershipAggregator' } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -247,7 +249,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-MembershipAggregator/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep index e5e0e405c..bd5d33ca1 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep @@ -177,6 +177,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(nonProdServiceStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}NonProdService' + AzureFunctionsWebHost__hostid: 'NonProdService' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(nonProdServiceStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-NonProdService') APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' @@ -194,17 +197,20 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(nonProdServiceStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}NonProdService' +var activityFunctionSettings = { 'AzureWebJobs.StarterFunction.Disabled': 0 'AzureWebJobs.OrchestratorFunction.Disabled': 0 'AzureWebJobs.GroupUpdaterSubOrchestratorFunction.Disabled': 0 + 'AzureWebJobs.IntegrationTestingPrepSubOrchestratorFunction.Disabled': 0 + 'AzureWebJobs.LoadTestingPrepSubOrchestratorFunction.Disabled': 0 'AzureWebJobs.GroupCreatorAndRetrieverFunction.Disabled': 0 'AzureWebJobs.GroupUpdaterFunction.Disabled': 0 + 'AzureWebJobs.LoadTestingGroupCalculatorFunction.Disabled': 0 + 'AzureWebJobs.LoadTestingSyncJobCreatorFunction.Disabled': 0 + 'AzureWebJobs.LoadTestingSyncJobRetrieverFunction.Disabled': 0 'AzureWebJobs.LoggerFunction.Disabled': 0 + 'AzureWebJobs.TenantUserCountFunction.Disabled': 0 'AzureWebJobs.TenantUserReaderFunction.Disabled': 0 - AzureFunctionsWebHost__hostid: 'NonProdService' } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -278,7 +284,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-NonProdService/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep index 9fc6964e8..13b7ce063 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep @@ -127,6 +127,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(notifierStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}Notifier' + AzureFunctionsWebHost__hostid: 'Notifier' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(notifierStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-Notifier') APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' @@ -153,10 +156,16 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(notifierStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}Notifier' - AzureFunctionsWebHost__hostid: 'Notifier' +var activityFunctionSettings = { + 'AzureWebJobs.StarterFunction.Disabled': 0 + 'AzureWebJobs.OrchestratorFunction.Disabled': 0 + 'AzureWebJobs.CreateThresholdNotificationFunction.Disabled': 0 + 'AzureWebJobs.LoggerFunction.Disabled': 0 + 'AzureWebJobs.RetrieveNotificationsFunction.Disabled': 0 + 'AzureWebJobs.SendNormalThresholdNotification.Disabled': 0 + 'AzureWebJobs.SendNotification.Disabled': 0 + 'AzureWebJobs.SendThresholdNotification.Disabled': 0 + 'AzureWebJobs.UpdateNotificationStatusFunction.Disabled': 0 } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -230,7 +239,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-Notifier/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep index 9beee11e6..5f44400fa 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep @@ -103,7 +103,11 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(placeMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}PlaceMembershipObtainer' + AzureFunctionsWebHost__hostid: 'PlaceMembershipObtainer' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(placeMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + WEBSITE_CONTENTSHARE: toLower('functionApp-PlaceMembershipObtainer') 'AzureFunctionsJobHost:extensions:durableTask:extendedSessionsEnabled': toLower(environmentAbbreviation) == 'prodv2' ? 'True' : 'False' APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' serviceBusSyncJobTopic: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusSyncJobTopic, '2019-09-01').secretUriWithVersion})' @@ -129,21 +133,17 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(placeMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - WEBSITE_CONTENTSHARE: toLower('functionApp-PlaceMembershipObtainer') - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}PlaceMembershipObtainer' +var activityFunctionSettings = { 'AzureWebJobs.StarterFunction.Disabled': 0 'AzureWebJobs.OrchestratorFunction.Disabled': 0 - 'AzureWebJobs.UsersSenderFunction.Disabled': 0 - 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 0 'AzureWebJobs.SubOrchestratorFunction.Disabled': 0 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 0 + 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 0 'AzureWebJobs.RoomsReaderFunction.Disabled': 0 - 'AzureWebJobs.WorkspacesReaderFunction.Disabled': 0 - 'AzureWebJobs.UsersReaderFunction.Disabled': 0 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled': 0 - AzureFunctionsWebHost__hostid: 'PlaceMembershipObtainer' + 'AzureWebJobs.UsersReaderFunction.Disabled': 0 + 'AzureWebJobs.UsersSenderFunction.Disabled': 0 + 'AzureWebJobs.WorkspacesReaderFunction.Disabled': 0 } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -215,7 +215,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-03-01' = { name: '${functionAppName}-PlaceMembershipObtainer/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep index cf1307688..b9a979c1f 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep @@ -128,6 +128,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(sqlMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}SqlMembershipObtainer' + AzureFunctionsWebHost__hostid: 'SqlMembershipObtainer' APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(sqlMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-SqlMembershipObtainer') @@ -165,23 +168,19 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(sqlMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}SqlMembershipObtainer' +var activityFunctionSettings = { 'AzureWebJobs.StarterFunction.Disabled': 0 'AzureWebJobs.OrchestratorFunction.Disabled': 0 - 'AzureWebJobs.ManagerOrgProcessorFunction.Disabled': 0 'AzureWebJobs.OrganizationProcessorFunction.Disabled': 0 'AzureWebJobs.ChildEntitiesFilterFunction.Disabled': 0 + 'AzureWebJobs.FeatureFlagFunction.Disabled': 0 'AzureWebJobs.GroupMembershipSenderFunction.Disabled': 0 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 0 'AzureWebJobs.LoggerFunction.Disabled': 0 'AzureWebJobs.ManagerOrgReaderFunction.Disabled': 0 + 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 0 'AzureWebJobs.TableNameReaderFunction.Disabled': 0 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 0 - 'AzureWebJobs.FeatureFlagFunction.Disabled': 0 - 'AzureWebJobs.QueueMessageSenderFunction.Disabled': 0 - AzureFunctionsWebHost__hostid: 'SqlMembershipObtainer' } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -253,7 +252,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-SqlMembershipObtainer/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep index 824659129..0031ba7c0 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep @@ -105,6 +105,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}TeamsChannelMO' + AzureFunctionsWebHost__hostid: 'TeamsChannelMO' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-TeamsChannelMembershipObtainer') 'AzureFunctionsJobHost:extensions:durableTask:extendedSessionsEnabled': toLower(environmentAbbreviation) == 'prodv2' ? 'True' : 'False' @@ -134,11 +137,16 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}TeamsChannelMO' - 'TeamsChannelMembershipObtainer:IsDryRunEnabled': 0 - AzureFunctionsWebHost__hostid: 'TeamsChannelMO' +var activityFunctionSettings = { + 'AzureWebJobs.StarterFunction.Disabled': 0 + 'AzureWebJobs.OrchestratorFunction.Disabled': 0 + 'AzureWebJobs.ChannelValidatorFunction.Disabled': 0 + 'AzureWebJobs.FileUploaderFunction.Disabled': 0 + 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 0 + 'AzureWebJobs.LoggerFunction.Disabled': 0 + 'AzureWebJobs.QueueMessageSender.Disabled': 0 + 'AzureWebJobs.TelemetryTracker.Disabled': 0 + 'AzureWebJobs.UserReaderFunction.Disabled': 0 } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -210,7 +218,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-TeamsChannelMembershipObtainer/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep index 3f2ca2485..a19718f49 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep @@ -104,6 +104,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}TeamsChannelUpdater' + AzureFunctionsWebHost__hostid: 'TeamsChannelUpdater' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-TeamsChannelUpdater') 'AzureFunctionsJobHost:extensions:durableTask:extendedSessionsEnabled': toLower(environmentAbbreviation) == 'prodv2' ? 'True' : 'False' @@ -133,22 +136,19 @@ var appSettings = { 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}TeamsChannelUpdater' +var activityFunctionSettings = { 'AzureWebJobs.StarterFunction.Disabled': 0 'AzureWebJobs.OrchestratorFunction.Disabled': 0 - 'AzureWebJobs.JobReaderFunction.Disabled': 0 - 'AzureWebJobs.FileDownloaderFunction.Disabled': 0 - 'AzureWebJobs.LoggerFunction.Disabled': 0 + 'AzureWebJobs.QueueMessageOrchestratorFunction.Disabled': 0 'AzureWebJobs.EmailSenderFunction.Disabled': 0 - 'AzureWebJobs.GroupOwnersReaderFunction.Disabled': 0 + 'AzureWebJobs.FileDownloaderFunction.Disabled': 0 'AzureWebJobs.GroupNameReaderFunction.Disabled': 0 + 'AzureWebJobs.JobReaderFunction.Disabled': 0 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 0 - 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 0 - 'AzureWebJobs.TeamsChannelUpdaterSubOrchestratorFunction.Disabled': 0 + 'AzureWebJobs.LoggerFunction.Disabled': 0 + 'AzureWebJobs.MessageReaderFunction.Disabled': 0 'AzureWebJobs.TeamsUpdaterFunction.Disabled': 0 - AzureFunctionsWebHost__hostid: 'TeamsChannelUpdater' + 'AzureWebJobs.TelemetryTrackerFunction.Disabled': 0 } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -220,7 +220,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-TeamsChannelUpdater/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] From a97a6ad960a75abfb8be7e91d811ff228389f642 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 15 Jul 2024 16:11:48 -0700 Subject: [PATCH 0095/1479] Deploy function app and no staging / slot swap --- yaml/deploy-functionapps.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/yaml/deploy-functionapps.yml b/yaml/deploy-functionapps.yml index e58efad47..6f1679778 100644 --- a/yaml/deploy-functionapps.yml +++ b/yaml/deploy-functionapps.yml @@ -46,26 +46,17 @@ steps: displayName: 'Wait for ARM resources' - task: AzureFunctionApp@1 - displayName: 'deploy ${{ parameters.name }} function app (staging)' + displayName: 'deploy ${{ parameters.name }} function app' inputs: appType: 'functionapp' azureSubscription: ${{parameters.serviceConnection}} appName: '${{ parameters.solutionAbbreviation }}-compute-${{ parameters.environmentAbbreviation }}-${{ parameters.name }}' Package: '${{ parameters.root }}/function_packages/${{ parameters.name }}.zip' deploymentMethod: 'runFromPackage' - deployToSlotOrASE: true - slotName: 'staging' + # deployToSlotOrASE: true + # slotName: 'staging' resourceGroupName: '${{ parameters.solutionAbbreviation }}-compute-${{ parameters.environmentAbbreviation }}' -- task: AzureAppServiceManage@0 - displayName: 'Swap ${{ parameters.name }} function app deployment slots' - inputs: - azureSubscription: ${{parameters.serviceConnection}} - webAppName: '${{ parameters.solutionAbbreviation }}-compute-${{ parameters.environmentAbbreviation }}-${{ parameters.name }}' - resourceGroupName: ${{parameters.solutionAbbreviation}}-compute-${{parameters.environmentAbbreviation}} - sourceSlot: staging - swapWithProduction: true - # This sets the "Enforce Scale Out Limit" in the function app configuration. This is apparently the only way to do it automatically. ARM templates don't support it. # https://docs.microsoft.com/en-us/azure/azure-functions/event-driven-scaling#limit-scale-out - task: AzureCLI@2 From 5490958b625e5de5dad0b021ad5d0aef99ebf13a Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 15 Jul 2024 16:14:32 -0700 Subject: [PATCH 0096/1479] Updated naming of deploy-functionapp.yml to be more accurate --- yaml/{deploy-functionapps.yml => deploy-functionapp.yml} | 0 yaml/deploy-pipeline.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename yaml/{deploy-functionapps.yml => deploy-functionapp.yml} (100%) diff --git a/yaml/deploy-functionapps.yml b/yaml/deploy-functionapp.yml similarity index 100% rename from yaml/deploy-functionapps.yml rename to yaml/deploy-functionapp.yml diff --git a/yaml/deploy-pipeline.yml b/yaml/deploy-pipeline.yml index da1c5b4c9..bcdd044e8 100644 --- a/yaml/deploy-pipeline.yml +++ b/yaml/deploy-pipeline.yml @@ -75,7 +75,7 @@ stages: downloadType: 'single' artifactName: $(Build.BuildNumber)_$(buildType) - - template: deploy-functionapps.yml + - template: deploy-functionapp.yml parameters: solutionAbbreviation: ${{ parameters.solutionAbbreviation }} environmentAbbreviation: ${{ parameters.environmentAbbreviation }} From 8e6b986a7a0c06180b78edd3c03ec49bf517cbf2 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 22 Jul 2024 13:01:42 -0700 Subject: [PATCH 0097/1479] Updated blob repository to upload file blocks --- .../BlobStorageRepository.cs | 27 +++++++++++++++++++ .../IBlobStorageRepository.cs | 2 ++ 2 files changed, 29 insertions(+) diff --git a/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs b/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs index 6272b4448..1d5631191 100644 --- a/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs @@ -3,11 +3,14 @@ using Azure.Identity; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs.Specialized; using Models; using Repositories.Contracts; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; namespace Repositories.BlobStorage @@ -101,5 +104,29 @@ public async Task UploadFileAsync(string path, string content, Dictionary 0) blobClient.SetMetadata(metadata); } + + public async Task UploadFileBlockAsync(string path, string content, Dictionary metadata = null) + { + var blockBlobClient = _containerClient.GetBlockBlobClient(path); + var blockIdBytes = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()); + var blockId = Convert.ToBase64String(blockIdBytes); + var byteArray = Encoding.UTF8.GetBytes(content); + + using (MemoryStream stream = new MemoryStream(byteArray)) + { + await blockBlobClient.StageBlockAsync(blockId, stream); + } + + if (metadata != null && metadata.Count > 0) + await blockBlobClient.SetMetadataAsync(metadata); + + return blockId; + } + + public async Task CommitFileAsync(string path, List blockIds) + { + var blockBlobClient = _containerClient.GetBlockBlobClient(path); + var response = await blockBlobClient.CommitBlockListAsync(blockIds); + } } } diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/IBlobStorageRepository.cs b/Service/GroupMembershipManagement/Repositories.Contracts/IBlobStorageRepository.cs index 226063d0f..55f73a2a4 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts/IBlobStorageRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Contracts/IBlobStorageRepository.cs @@ -9,10 +9,12 @@ namespace Repositories.Contracts public interface IBlobStorageRepository { public Task UploadFileAsync(string path, string content, Dictionary metadata = null); + Task UploadFileBlockAsync(string path, string content, Dictionary metadata = null); public Task DeleteFileAsync(string path); public Task DownloadFileAsync(string path); public Task DownloadCacheFileAsync(string path); public Task DeleteFilesAsync(string path); public Task GetBlobMetadataAsync(string path); + public Task CommitFileAsync(string path, List blockIds); } } From f50d6042a2194686e4043b43ba6161141f045545 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 22 Jul 2024 13:23:16 -0700 Subject: [PATCH 0098/1479] Updated mocks --- .../Services.Tests/Mocks/MockBlobStorageRepository.cs | 10 ++++++++++ .../Services.Tests/Mocks/MockBlobStorageRepository.cs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Mocks/MockBlobStorageRepository.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Mocks/MockBlobStorageRepository.cs index 2ddbdaa02..fce57c70a 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Mocks/MockBlobStorageRepository.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Mocks/MockBlobStorageRepository.cs @@ -54,5 +54,15 @@ public Task UploadFileAsync(string path, string content, Dictionary UploadFileBlockAsync(string path, string content, Dictionary metadata = null) + { + throw new NotImplementedException(); + } + + public Task CommitFileAsync(string path, List blockIds) + { + throw new NotImplementedException(); + } } } diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs index 0303a41d4..f5b77cc6d 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs @@ -43,5 +43,15 @@ public Task UploadFileAsync(string path, string content, Dictionary UploadFileBlockAsync(string path, string content, Dictionary metadata = null) + { + throw new NotImplementedException(); + } + + public Task CommitFileAsync(string path, List blockIds) + { + throw new NotImplementedException(); + } } } From 0b8081995e43570f38681dd25fddf38e219b02ca Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 23 Jul 2024 14:01:29 -0700 Subject: [PATCH 0099/1479] bug fix: update org leader id correctly --- .../components/HRQuerySource/HRQuerySource.base.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 9c9a1e695..8eab3c76e 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -77,6 +77,7 @@ export const HRQuerySourceBase: React.FunctionComponent = (p const [groupingEnabled, setGroupingEnabled] = useState(false); const [filterTextEnabled, setFilterTextEnabled] = useState(false); const [expanded, setExpanded] = useState(true); + const [orgLeaderUpdated, setOrgLeaderUpdated] = useState(false); useEffect(() => { if (!groupingEnabled) { @@ -256,7 +257,7 @@ const checkType = (value: string, type: string | undefined): string => { }, [orgLeaderDetails.maxDepth]); useEffect(() => { - if (orgLeaderDetails.employeeId > 0 && partId === orgLeaderDetails.partId) { + if (orgLeaderUpdated && orgLeaderDetails.employeeId > 0 && partId === orgLeaderDetails.partId) { const id: number = orgLeaderDetails.employeeId; const newSource = { ...props.source, @@ -268,7 +269,7 @@ const checkType = (value: string, type: string | undefined): string => { setSource(newSource); onSourceChange(newSource, partId); } - }, [orgLeaderDetails.employeeId, orgLeaderDetails.objectId]); + }, [orgLeaderUpdated, orgLeaderDetails.employeeId, orgLeaderDetails.objectId]); useEffect(() => { if (source?.manager?.id) { @@ -279,7 +280,7 @@ const checkType = (value: string, type: string | undefined): string => { })) } } - }, [source]); + }, [props.source.manager?.id]); useEffect(() => { if (orgLeaderDetails.employeeId === 0 && orgLeaderDetails.maxDepth === 0 && includeOrg && partId === orgLeaderDetails.partId) { @@ -311,7 +312,8 @@ const checkType = (value: string, type: string | undefined): string => { key: items[0].key as number, text: items[0].text as string, partId: partId as number - })) + })); + setOrgLeaderUpdated(true); } }; From b673b0b642bdee075a0c076d930a26bb48200f50 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 23 Jul 2024 15:41:14 -0700 Subject: [PATCH 0100/1479] update filter query while editing source part --- .../src/components/HRQuerySource/HRQuerySource.base.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 8eab3c76e..405f2b809 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -728,10 +728,10 @@ const checkType = (value: string, type: string | undefined): string => { const regex = /(?<= [Aa][Nn][Dd] | [Oo][Rr] )/; let segments = props.source.filter?.split(regex); - if (item && (props.source.filter?.length === 0 || (segments?.length == children.length - 1))) { + if (item && (props.source.filter == undefined || props.source.filter?.length === 0 || (segments?.length == children.length - 1))) { const a = item.key.toString(); let filter: string; - if (source.filter !== "") { + if (source.filter && source.filter !== "") { filter = `${source.filter} ` + a; } else { filter = a; From 22716bfa21d6e7af3d6bc5a8b8929a53a1c2ec06 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Fri, 19 Jul 2024 13:54:04 -0700 Subject: [PATCH 0101/1479] Quick fix for notification subject --- .../Notifier/Services.Notifier/NotifierService.cs | 12 ++++++++++-- .../Resources/LocalizationRepository.en-US.resx | 12 ++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index 836a084c8..302a61023 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -104,9 +104,17 @@ await _loggingRepository.LogMessageAsync(new LogMessage "; + var cardState = notification.CardState; + + string subjectKey = cardState == ThresholdNotificationCardState.DisabledCard + ? "SyncThresholdDisablingJobEmailSubject" + : "SyncThresholdEmailSubject"; + + string subject = _localizationRepository.TranslateSetting(subjectKey, groupName); + var message = new EmailMessage { - Subject = _localizationRepository.TranslateSetting("SyncThresholdEmailSubject", groupName), + Subject = subject, Content = string.Format(htmlTemplate, adaptiveCard), SenderAddress = _emailSenderAndRecipients.SenderAddress, SenderPassword = _emailSenderAndRecipients.SenderPassword, @@ -409,7 +417,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage string contentTemplate; string[] additionalContent; - string[] additionalSubjectContent = new[] { job.TargetOfficeGroupId.ToString(), groupName }; + string[] additionalSubjectContent = new[] { groupName }; var thresholdEmail = GetNormalThresholdEmail(groupName, threshold, job); contentTemplate = thresholdEmail.ContentTemplate; diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index d83fea13a..e9fe29247 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -1013,7 +1013,7 @@ } - Onboarding + [Awareness] Onboarding notification You received this notification because you are listed as an owner of **{1}**. @@ -1036,7 +1036,7 @@ The group **{1}** with Id: {0} **has completed its initial sync**. - Management Alert + [Action Required] Sync disabled You received this notification because you are listed as an owner of **{1}**. @@ -1078,7 +1078,7 @@ If you believe this group should still be managed by Group Membership Management If not remediated by {2}, your group's membership will still remain, but its affiliation with GMM (including its membership definition) will be removed. You will need to re-onboard if you still want GMM to manage your group after that date. - Group Membership Management (GMM) update + [Final Notice] Sync disabled Decrease membership threshold {0}%. Detected threshold {1}%. @@ -1096,7 +1096,7 @@ Please review your membership definition by visiting the GMM UI. - Group Membership Management - No data found for group {1} + [Action Required] No data found for group {1} You received this notification because you are listed as an owner of **{0}**. @@ -1138,10 +1138,10 @@ The group sync will be PAUSED until your response is received. - [Action Required] GMM: Synchronization job for group {1} has been disabled. + [Action Required] Synchronization job for group {0} has been disabled. - [Action Required] GMM: Membership Change Alert review for {0}. + [Action Required] Membership Change Alert review for {0}. Ignore Thresholds Once From 9818c852332a3d3fa2d754f75283f31e0fb1f278 Mon Sep 17 00:00:00 2001 From: abgonz Date: Thu, 25 Jul 2024 11:54:38 -0700 Subject: [PATCH 0102/1479] Change GraphGroupRepository and GraphUserRepository to Scoped in NonProdService --- .../Hosts/NonProdService/Function/Startup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Startup.cs index 49c6e3d52..c78f5415d 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Startup.cs @@ -31,8 +31,8 @@ public override void Configure(IFunctionsHostBuilder builder) builder.Services .AddGraphAPIClient() - .AddSingleton() - .AddSingleton(); + .AddScoped() + .AddScoped(); builder.Services.AddOptions().Configure((settings, configuration) => { From 94ea7d49a222b8f92ea03de3d8b82749c4979ae1 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 16 Apr 2024 17:04:02 -0700 Subject: [PATCH 0103/1479] Updated the localization to only show changes that went over or equal threshold and normalize wording between normal and resolved threshold notifs --- .../Resources/LocalizationRepository.en-US.resx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index e9fe29247..98df134f8 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -199,19 +199,19 @@ }, { "type": "TextBlock", - "text": "${$root.changeQuantityForAdditions} members will be **added**, which will increase the group size by **${$root.changePercentageForAdditions}%**.", + "text": "GMM has identified **${$root.changeQuantityForAdditions}** members to be added, increasing the group size by **${$root.changePercentageForAdditions}%**, which is more than the current additions threshold of **${$root.thresholdPercentageForAdditions}%**.", "wrap": true, "spacing": "Small", "color": "Accent", - "$when": "${$root.changeQuantityForAdditions > 0}" + "$when": "${$root.changePercentageForAdditions > $root.thresholdPercentageForAdditions}" }, { "type": "TextBlock", - "text": "${$root.changeQuantityForRemovals} members will be **removed**, which will decrease the group size by **${$root.changePercentageForRemovals}%**.", + "text": "GMM has identified **${$root.changeQuantityForRemovals}** members to be removed, decreasing the group size by **${$root.changePercentageForRemovals}%**, which is more than the current removals threshold of **${$root.thresholdPercentageForRemovals}%**", "wrap": true, "spacing": "Small", "color": "Accent", - "$when": "${$root.changeQuantityForRemovals > 0}" + "$when": "${$root.changePercentageForRemovals > $root.thresholdPercentageForRemovals}" }, { "type": "TextBlock", @@ -606,7 +606,7 @@ "wrap": true, "spacing": "Small", "color": "Accent", - "$when": "${$root.changeQuantityForAdditions > 0}" + "$when": "${$root.changePercentageForAdditions > $root.thresholdPercentageForAdditions}" }, { "type": "TextBlock", @@ -614,7 +614,7 @@ "wrap": true, "spacing": "Small", "color": "Accent", - "$when": "${$root.changeQuantityForRemovals > 0}" + "$when": "${$root.changePercentageForRemovals > $root.thresholdPercentageForRemovals}" }, { "type": "Container", From 69547602a76be6eed1e8b4f8f1e2b865e54d1141 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Thu, 18 Apr 2024 12:32:03 -0700 Subject: [PATCH 0104/1479] Set the ResolvedBy to 'GMM Support' when an authorized gmm support member resolves the notification --- .../Services.WebApi/ResolveNotificationHandler.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs index 2e3e08f3e..6b1610a25 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs @@ -55,11 +55,14 @@ await _loggingRepository.LogMessageAsync(new LogMessage return response; } + var isInAuthorizedGroup = false; + var isGroupOwner = await _graphGroupRepository.IsEmailRecipientOwnerOfGroupAsync(request.UserIdentifier, thresholdNotification.TargetOfficeGroupId); if (!isGroupOwner) { - // Check if user is in the list of GMM Admins - var isInAuthorizedGroup = await _graphGroupRepository.IsEmailRecipientMemberOfGroupAsync(request.UserIdentifier, _gmmEmailReceivers.ActionableMessageViewerGroupId); + // Check if user is in the list of GMM Support / Actionable Message Viewer Group + isInAuthorizedGroup = await _graphGroupRepository.IsEmailRecipientMemberOfGroupAsync(request.UserIdentifier, _gmmEmailReceivers.ActionableMessageViewerGroupId); + if (!isInAuthorizedGroup) { // Unauthorized @@ -73,7 +76,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage var resolvedByMail = request.UserIdentifier; Guid userId; - if(Guid.TryParse(resolvedByMail, out userId)) + if (Guid.TryParse(resolvedByMail, out userId)) { var user = await _graphGroupRepository.GetUserByUpnOrIdAsync(userId.ToString(), true); resolvedByMail = user.Mail; @@ -83,7 +86,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage thresholdNotification.Status = ThresholdNotificationStatus.Resolved; thresholdNotification.CardState = ThresholdNotificationCardState.NoCard; thresholdNotification.Resolution = resolution; - thresholdNotification.ResolvedBy = resolvedByMail; + thresholdNotification.ResolvedBy = isInAuthorizedGroup ? "GMM Support" : resolvedByMail; thresholdNotification.ResolvedTime = DateTime.UtcNow; await handleSyncJobResolution(thresholdNotification); @@ -105,13 +108,13 @@ private async Task handleSyncJobResolution(ThresholdNotification notification) job.IgnoreThresholdOnce = true; job.Status = SyncStatus.Idle.ToString(); await _syncJobRepository.UpdateSyncJobFromNotificationAsync(job, SyncStatus.Idle); - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Resolved Notification. Setting the status of the sync back to Idle."}); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Resolved Notification. Setting the status of the sync back to Idle." }); } else if (notification.Resolution == ThresholdNotificationResolution.Paused) { job.Status = SyncStatus.CustomerPaused.ToString(); await _syncJobRepository.UpdateSyncJobFromNotificationAsync(job, SyncStatus.CustomerPaused); - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Resolved Notification. Setting the status of the sync to CustomerPaused."}); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Resolved Notification. Setting the status of the sync to CustomerPaused." }); } } private void TrackNotificationResponseEvent(Guid groupId, string timeElapsedForResponse) From 0e2220e00fdb0c03695e8d5dab84edf9f7eb943b Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 22 Jul 2024 16:29:14 -0700 Subject: [PATCH 0105/1479] Updated ThresholdNotificationService to provide ChangePercentage values that are more specific and will pass conditions --- .../NotificationsControllerTests.cs | 7 +- .../LocalizationRepository.en-US.resx | 284 +++++++++--------- .../ThresholdNotificationService.cs | 20 +- 3 files changed, 162 insertions(+), 149 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs index 8fac2eeed..915f2d6df 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs @@ -459,8 +459,8 @@ public async Task GetNotificationCard_HandleExpiredTestAsync() private void ValidateUnresolvedCard(string cardJson) { Assert.IsTrue(cardJson.Contains($"The most recent attempt to update the membership of your GMM managed group '**{_groupName}**")); - Assert.IsTrue(cardJson.Contains($"{_thresholdNotification.ChangeQuantityForAdditions} members will be **added**, which will increase the group size by **{_thresholdNotification.ChangePercentageForAdditions}%**.")); - Assert.IsTrue(cardJson.Contains($"{_thresholdNotification.ChangeQuantityForRemovals} members will be **removed**, which will decrease the group size by **{_thresholdNotification.ChangePercentageForRemovals}%**.")); + Assert.IsTrue(cardJson.Contains($"GMM has identified **{_thresholdNotification.ChangeQuantityForAdditions}** members to be added, increasing the group size by **{Math.Round(_thresholdNotification.ChangePercentageForAdditions, 1)}%**, which is more than the current additions threshold of **{_thresholdNotification.ThresholdPercentageForAdditions}%**.")); + Assert.IsTrue(cardJson.Contains($"GMM has identified **{_thresholdNotification.ChangeQuantityForRemovals}** members to be removed, decreasing the group size by **{Math.Round(_thresholdNotification.ChangePercentageForRemovals, 1)}%**, which is more than the current removals threshold of **{_thresholdNotification.ThresholdPercentageForRemovals}%**.")); Assert.IsTrue(cardJson.Contains($"https://{_hostname}/api/v1/notifications/{_thresholdNotification.Id}/resolve")); Assert.IsTrue(cardJson.Contains($"\\\"resolution\\\":\\\"{ThresholdNotificationResolution.Paused}\\\"")); Assert.IsTrue(cardJson.Contains($"\\\"resolution\\\":\\\"{ThresholdNotificationResolution.IgnoreOnce}\\\"")); @@ -470,9 +470,10 @@ private void ValidateUnresolvedCard(string cardJson) private void ValidateDisabledCard(string cardJson) { + Console.WriteLine(cardJson); Assert.IsTrue(cardJson.Contains($"Synchronization of your GMM group **{_groupName}** has been disabled. If no action is taken, the sync will be deleted on ")); Assert.IsTrue(cardJson.Contains("After this period, if you wish to reenable the sync, you will need to follow GMM's onboarding process again.")) ; - Assert.IsTrue(cardJson.Contains($"{_thresholdNotification.ChangeQuantityForAdditions} members will be **added**, which will increase the group size by **{_thresholdNotification.ChangePercentageForAdditions}%**.")); + Assert.IsTrue(cardJson.Contains($"GMM has identified **{_thresholdNotification.ChangeQuantityForAdditions}** members to be added, increasing the group size by **{Math.Round(_thresholdNotification.ChangePercentageForAdditions, 1)}%**, which is more than the current additions threshold of **{_thresholdNotification.ThresholdPercentageForAdditions}%**.")); Assert.IsTrue(cardJson.Contains($"{_thresholdNotification.Id}")); Assert.IsTrue(cardJson.Contains($"\"originator\":\"{_providerId}\"")); } diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 98df134f8..777791448 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -207,7 +207,7 @@ }, { "type": "TextBlock", - "text": "GMM has identified **${$root.changeQuantityForRemovals}** members to be removed, decreasing the group size by **${$root.changePercentageForRemovals}%**, which is more than the current removals threshold of **${$root.thresholdPercentageForRemovals}%**", + "text": "GMM has identified **${$root.changeQuantityForRemovals}** members to be removed, decreasing the group size by **${$root.changePercentageForRemovals}%**, which is more than the current removals threshold of **${$root.thresholdPercentageForRemovals}%**.", "wrap": true, "spacing": "Small", "color": "Accent", @@ -270,154 +270,154 @@ { - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.0", - "originator": "${$root.providerId}", - "createdUtc": "${$root.cardCreatedTime}", - "body": [ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "originator": "${$root.providerId}", + "createdUtc": "${$root.cardCreatedTime}", + "body": [ + { + "type": "Container", + "padding": "Small", + "style": "emphasis", + "items": [ + { + "type": "ColumnSet", + "padding": "None", + "columns": [ { - "type": "Container", - "padding": "Small", - "style": "emphasis", - "items": [ - { - "type": "ColumnSet", - "padding": "None", - "columns": [ - { - "type": "Column", - "padding": "None", - "width": "auto", - "items": [ - { - "type": "Image", - "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAGwklEQVR4nOWbMWjjSBSGJ0GFiilUuFChQgcqBKtChwNx4UKFl/NyXnDhgGGzELgUOnChwoWLBXNs4cJFChcuXLgwR7zkwIWyKOA9tGAOHwSjgBMUsEAprthyCxcuFuaKtXOKLNmSLHsC9+BvxtbMe9/Mk2ZGoz2EENi2dbvd1P39ffLu7o6zLEv8+vUr+PLlC5jNZtLiPzRNf+Y4DkAIweHh4R+pVOrPbDZ7v2nbnz594l6/fj2ZzWag1+v9mM/nb578ASEUu3RdZ2VZrrAsqwMA7LlQSNkAAJvnea1cLpd0XWej+EKSpLmokyRJ0/17rIE3Go0cx3H9iAGvBSIIgvr+/fvjMD65fLG3AqBarZ5ACMchArdXaO21FEUZ1Wr1BDuARqORYxhmsMJxe9FzsixXzs/PU7qus+PxGPqlTrPZzCqKohwcHFysgWJTFGXU6/XCzgGYpklmMpmWj3M2SZJmPp8/Oz8/T206ulRVFYrFYm2ey57tiaLYMwyD2gkAVVWFRCJx7eUITdPDsDkaNtUoijK8QJAkaXY6nfRWAZTL5ZJf40Fzcssg7NPT03dbAXB8fPzeq0FJktp+Ob1t+aShnc1mm7ECyOVyDXdDBEFMGo1GDkfgTrXbbWk+Gp5AyGQyrVgAePU8TdPD4XBI4w5+IcMwKJqmh+5gPXwPB8Aj522WZfXJZELgDtpLPM9ra+YSwQGoqiq4K2NZVscd5DqtmYkGA2CaJul+1EEIx6ZpkrgDDKJ5OkQHkM1mm+4LVVUVcAcWVIZhUPOpeXgAjUYj5877bU5utqVWq5WJBGA+t3+8KJ1Od3AG0u/3Occ0OKzCAahUKrLzQoIgJtfX1wmcAJzr+Ri0GoArb+xSqVTGPZQJgpjEBYCm6aEvgGq1euLsfa/dExzSNI3fIAUeRZKk2Ww2s74AXLn/LHp/FwIIfd+IeI69vwvtAwDAhw8figAAFsxNkqTP4H9i+wAAcHV19ZOj7OHNmze/Y/Jn57aHEAJ7e3s2+G8EPCCEfsDo005tv9vtppwFgiDc4nIGh+3f398ngSP/0+n0X/jc2b3t393dcc6Cw8PDv3E5g8MIy7JEZwHHcRYmX3yt2+2mPn78+PPt7a1gWRY3nU4hAABACKcMw/wjiuJNMpkc5XK5S57nZ6EqZ1n2yVz5uez26LrO5vP5sxXvAzzn+oIgqO12Wwo8ESJJcuViAUfgqVTqPETQviB6vZ64FoD7QpzBVyoVOc7FDwDALhQK9ZUACIJ4FgBEUext2OsrR4MvAOdq6+Li4gBH8I5zBHEH/wjBb0MX23DfQc8vQUilUufPCoB7BypoIC6Futa9zMcWfL/f5yLc8B5feSGEQD6fP4sCYTAYMNgBzPM+9DBeGsIR0seZCliCb7fbUsS8jwUAAMBeTJawAJjf+CLdyGICgERR7GEBYBgGFdXpOAEAAGzTNMl9sGO7urrKAsfyG6Oxl5eXv+wcwGg0Su66TT8bjUbczgFYlsWt/9du7ObmRlzKUVVVBcehx43ktRrzeWuL4x6AIISxPZ895Xk2d8Nlbsz1oaUUeHh4YN1lUW02m5Fx1bUtIwL+7yFK5RcXF0fuMgjhdDqdRqkOAADAq1evmpqm/QoAAEdHR/XIFX33ZXkeADyGVBxHXmOYBD2mgUuR6xJFUQ/0FHj79m3Hsqygo2WlxbDpyrq0iS83gQB8+/aNe/nyZX+TxhaWTCZHcdQTh7148cIKlAJz2Yv5M86pMEVRhizLFUVRFI/DkaHqGgwGTBgAsUGIeh+gKMpw1+U+0xRUi33CdQDs09PTd+4yhmEGmxyVnZ/gCt1zsixX3HXNd5VC936r1coEAoCQ/2FpryMnQRWl5xRFUeIAwDDM4DHeIAAQ8t1+sovFYi0KAE3T+LBbYjGlwJPd78AAEEJAluWKFwQI4bhWqxXDQvBIr7XOJxKJa0VRlEqlIq/5Xsnz+lwu13gSbxgACCHQ6XTSfsdQKYoywp4q3eG2OOJ5XluKNywAhBAYj8fQ8VXXEggI4fjk5OQ3TdP4IBB28GIE+R3zjwRgoXq9XvD7iMkJQ5KktqIoyioggiCoW4Jg8zyv+Z103wjAQqu+5nKKIIiJ3+dtCCFQLBZrMUOwvR6dsQNYqFarFdd9Out8BHlJVVUhhpSwGYYZBDniv1TgejSFArBQv9/nSqVS2QHjCdQgX560Wq1MhBukLQiCupjkBNFSwfxtMQIAoLjeFvd6PVFRlDNJknSO41AikbimaXrofEXlJ8MwqFqtdlYoFHRRFHUI4WPAEEIkiqJeKBT0arV6FqQ+t/4FtljDw9p0jt4AAAAASUVORK5CYII=", - "altText": "Synchronization Exception Icon", - "size": "Small" - } - ] - }, - { - "type": "Column", - "padding": "None", - "verticalContentAlignment": "Center", - "width": "auto", - "items": [ - { - "type": "TextBlock", - "text": "Membership Change Alert", - "wrap": true, - "weight": "Bolder" - }, - { - "type": "TextBlock", - "text": "Group Membership Management", - "wrap": true, - "spacing": "None", - "color": "Dark", - "size": "Small" - } - ] - } - ] - } - ] + "type": "Column", + "padding": "None", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAGwklEQVR4nOWbMWjjSBSGJ0GFiilUuFChQgcqBKtChwNx4UKFl/NyXnDhgGGzELgUOnChwoWLBXNs4cJFChcuXLgwR7zkwIWyKOA9tGAOHwSjgBMUsEAprthyCxcuFuaKtXOKLNmSLHsC9+BvxtbMe9/Mk2ZGoz2EENi2dbvd1P39ffLu7o6zLEv8+vUr+PLlC5jNZtLiPzRNf+Y4DkAIweHh4R+pVOrPbDZ7v2nbnz594l6/fj2ZzWag1+v9mM/nb578ASEUu3RdZ2VZrrAsqwMA7LlQSNkAAJvnea1cLpd0XWej+EKSpLmokyRJ0/17rIE3Go0cx3H9iAGvBSIIgvr+/fvjMD65fLG3AqBarZ5ACMchArdXaO21FEUZ1Wr1BDuARqORYxhmsMJxe9FzsixXzs/PU7qus+PxGPqlTrPZzCqKohwcHFysgWJTFGXU6/XCzgGYpklmMpmWj3M2SZJmPp8/Oz8/T206ulRVFYrFYm2ey57tiaLYMwyD2gkAVVWFRCJx7eUITdPDsDkaNtUoijK8QJAkaXY6nfRWAZTL5ZJf40Fzcssg7NPT03dbAXB8fPzeq0FJktp+Ob1t+aShnc1mm7ECyOVyDXdDBEFMGo1GDkfgTrXbbWk+Gp5AyGQyrVgAePU8TdPD4XBI4w5+IcMwKJqmh+5gPXwPB8Aj522WZfXJZELgDtpLPM9ra+YSwQGoqiq4K2NZVscd5DqtmYkGA2CaJul+1EEIx6ZpkrgDDKJ5OkQHkM1mm+4LVVUVcAcWVIZhUPOpeXgAjUYj5877bU5utqVWq5WJBGA+t3+8KJ1Od3AG0u/3Occ0OKzCAahUKrLzQoIgJtfX1wmcAJzr+Ri0GoArb+xSqVTGPZQJgpjEBYCm6aEvgGq1euLsfa/dExzSNI3fIAUeRZKk2Ww2s74AXLn/LHp/FwIIfd+IeI69vwvtAwDAhw8figAAFsxNkqTP4H9i+wAAcHV19ZOj7OHNmze/Y/Jn57aHEAJ7e3s2+G8EPCCEfsDo005tv9vtppwFgiDc4nIGh+3f398ngSP/0+n0X/jc2b3t393dcc6Cw8PDv3E5g8MIy7JEZwHHcRYmX3yt2+2mPn78+PPt7a1gWRY3nU4hAABACKcMw/wjiuJNMpkc5XK5S57nZ6EqZ1n2yVz5uez26LrO5vP5sxXvAzzn+oIgqO12Wwo8ESJJcuViAUfgqVTqPETQviB6vZ64FoD7QpzBVyoVOc7FDwDALhQK9ZUACIJ4FgBEUext2OsrR4MvAOdq6+Li4gBH8I5zBHEH/wjBb0MX23DfQc8vQUilUufPCoB7BypoIC6Futa9zMcWfL/f5yLc8B5feSGEQD6fP4sCYTAYMNgBzPM+9DBeGsIR0seZCliCb7fbUsS8jwUAAMBeTJawAJjf+CLdyGICgERR7GEBYBgGFdXpOAEAAGzTNMl9sGO7urrKAsfyG6Oxl5eXv+wcwGg0Su66TT8bjUbczgFYlsWt/9du7ObmRlzKUVVVBcehx43ktRrzeWuL4x6AIISxPZ895Xk2d8Nlbsz1oaUUeHh4YN1lUW02m5Fx1bUtIwL+7yFK5RcXF0fuMgjhdDqdRqkOAADAq1evmpqm/QoAAEdHR/XIFX33ZXkeADyGVBxHXmOYBD2mgUuR6xJFUQ/0FHj79m3Hsqygo2WlxbDpyrq0iS83gQB8+/aNe/nyZX+TxhaWTCZHcdQTh7148cIKlAJz2Yv5M86pMEVRhizLFUVRFI/DkaHqGgwGTBgAsUGIeh+gKMpw1+U+0xRUi33CdQDs09PTd+4yhmEGmxyVnZ/gCt1zsixX3HXNd5VC936r1coEAoCQ/2FpryMnQRWl5xRFUeIAwDDM4DHeIAAQ8t1+sovFYi0KAE3T+LBbYjGlwJPd78AAEEJAluWKFwQI4bhWqxXDQvBIr7XOJxKJa0VRlEqlIq/5Xsnz+lwu13gSbxgACCHQ6XTSfsdQKYoywp4q3eG2OOJ5XluKNywAhBAYj8fQ8VXXEggI4fjk5OQ3TdP4IBB28GIE+R3zjwRgoXq9XvD7iMkJQ5KktqIoyioggiCoW4Jg8zyv+Z103wjAQqu+5nKKIIiJ3+dtCCFQLBZrMUOwvR6dsQNYqFarFdd9Out8BHlJVVUhhpSwGYYZBDniv1TgejSFArBQv9/nSqVS2QHjCdQgX560Wq1MhBukLQiCupjkBNFSwfxtMQIAoLjeFvd6PVFRlDNJknSO41AikbimaXrofEXlJ8MwqFqtdlYoFHRRFHUI4WPAEEIkiqJeKBT0arV6FqQ+t/4FtljDw9p0jt4AAAAASUVORK5CYII=", + "altText": "Synchronization Exception Icon", + "size": "Small" + } + ] }, { - "type": "Container", - "padding": "Default", - "separator": true, - "items": [ - { - "type": "TextBlock", - "text": "${$root.groupName}", - "wrap": true, - "size": "Large", - "weight": "Bolder" - } - ] + "type": "Column", + "padding": "None", + "verticalContentAlignment": "Center", + "width": "auto", + "items": [ + { + "type": "TextBlock", + "text": "Membership Change Alert", + "wrap": true, + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "Group Membership Management", + "wrap": true, + "spacing": "None", + "color": "Dark", + "size": "Small" + } + ] + } + ] + } + ] + }, + { + "type": "Container", + "padding": "Default", + "separator": true, + "items": [ + { + "type": "TextBlock", + "text": "${$root.groupName}", + "wrap": true, + "size": "Large", + "weight": "Bolder" + } + ] + }, + { + "type": "Container", + "padding": "Default", + "items": [ + { + "type": "TextBlock", + "text": "Synchronization of your GMM group **${$root.groupName}** has been disabled. If no action is taken, the sync will be deleted on **${$root.jobExpirationDate}** UTC. After this period, if you wish to reenable the sync, you will need to follow GMM's onboarding process again.", + "wrap": true, + "color": "Attention" + }, + { + "type": "TextBlock", + "text": "GMM has identified **${$root.changeQuantityForAdditions}** members to be added, increasing the group size by **${$root.changePercentageForAdditions}%**, which is more than the current additions threshold of **${$root.thresholdPercentageForAdditions}%**.", + "wrap": true, + "spacing": "Small", + "color": "Accent", + "$when": "${$root.changePercentageForAdditions > $root.thresholdPercentageForAdditions}" + }, + { + "type": "TextBlock", + "text": "GMM has identified **${$root.changeQuantityForRemovals}** members to be removed, decreasing the group size by **${$root.changePercentageForRemovals}%**, which is more than the current removals threshold of **${$root.thresholdPercentageForRemovals}%**.", + "wrap": true, + "spacing": "Small", + "color": "Accent", + "$when": "${$root.changePercentageForRemovals > $root.thresholdPercentageForRemovals}" + }, + { + "type": "TextBlock", + "text": "Do you want to proceed?", + "wrap": true, + "color": "Accent", + "size": "Default", + "weight": "Bolder" + }, + { + "type": "ActionSet", + "actions": [ + { + "type": "Action.Http", + "title": "No, Pause Sync Job", + "method": "POST", + "url": "https://${$root.apiHostname}/api/v1/notifications/${$root.notificationId}/resolve", + "body": "{\"resolution\":\"Paused\"}", + "isPrimary": true, + "style": "positive" }, { - "type": "Container", - "padding": "Default", - "items": [ - { - "type": "TextBlock", - "text": "Synchronization of your GMM group **${$root.groupName}** has been disabled. If no action is taken, the sync will be deleted on **${$root.jobExpirationDate}** UTC. After this period, if you wish to reenable the sync, you will need to follow GMM's onboarding process again.", - "wrap": true, - "color": "Attention" - }, - { - "type": "TextBlock", - "text": "${$root.changeQuantityForAdditions} members will be **added**, which will increase the group size by **${$root.changePercentageForAdditions}%**.", - "wrap": true, - "spacing": "Small", - "color": "Accent", - "$when": "${$root.changeQuantityForAdditions > 0}" - }, - { - "type": "TextBlock", - "text": "${$root.changeQuantityForRemovals} members will be **removed**, which will decrease the group size by **${$root.changePercentageForRemovals}%**.", - "wrap": true, - "spacing": "Small", - "color": "Accent", - "$when": "${$root.changeQuantityForRemovals > 0}" - }, - { - "type": "TextBlock", - "text": "Do you want to proceed?", - "wrap": true, - "color": "Accent", - "size": "Default", - "weight": "Bolder" - }, - { - "type": "ActionSet", - "actions": [ - { - "type": "Action.Http", - "title": "No, Pause Sync Job", - "method": "POST", - "url": "https://${$root.apiHostname}/api/v1/notifications/${$root.notificationId}/resolve", - "body": "{\"resolution\":\"Paused\"}", - "isPrimary": true, - "style": "positive" - }, - { - "type": "Action.Http", - "title": "Yes, Apply Changes", - "method": "POST", - "url": "https://${$root.apiHostname}/api/v1/notifications/${$root.notificationId}/resolve", - "body": "{\"resolution\":\"IgnoreOnce\"}" - } - ], - "spacing": "Large" - }, - { - "type": "Container", - "padding": "Medium", - "separator": true, - "items": [ - { - "type": "TextBlock", - "text": "**${$root.thresholdPercentageForAdditions}% increase** and **${$root.thresholdPercentageForRemovals}% decrease** alerts are configured for this group. If you would like to change these alert percentages or disable them, please pause the sync and contact GMM support for further inquiry.", - "wrap": true - } - ], - "spacing": "Medium" - } - ], - "separator": true + "type": "Action.Http", + "title": "Yes, Apply Changes", + "method": "POST", + "url": "https://${$root.apiHostname}/api/v1/notifications/${$root.notificationId}/resolve", + "body": "{\"resolution\":\"IgnoreOnce\"}" } - ], - "autoInvokeAction": { - "method": "POST", - "url": "https://${$root.apiHostname}/api/v1/notifications/${$root.notificationId}/card", - "body": "", - "type": "Action.Http" + ], + "spacing": "Large" + }, + { + "type": "Container", + "padding": "Medium", + "separator": true, + "items": [ + { + "type": "TextBlock", + "text": "**${$root.thresholdPercentageForAdditions}% increase** and **${$root.thresholdPercentageForRemovals}% decrease** alerts are configured for this group. If you would like to change these alert percentages or disable them, please pause the sync and contact GMM support for further inquiry.", + "wrap": true + } + ], + "spacing": "Medium" } - } + ], + "separator": true + } + ], + "autoInvokeAction": { + "method": "POST", + "url": "https://${$root.apiHostname}/api/v1/notifications/${$root.notificationId}/card", + "body": "", + "type": "Action.Http" + } +} { diff --git a/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs b/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs index 756f8bc81..35c791ba5 100644 --- a/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs +++ b/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs @@ -60,8 +60,8 @@ public async Task CreateNotificationCardAsync(ThresholdNotification noti GroupName = groupName, ChangeQuantityForAdditions = notification.ChangeQuantityForAdditions, ChangeQuantityForRemovals = notification.ChangeQuantityForRemovals, - ChangePercentageForAdditions = notification.ChangePercentageForAdditions, - ChangePercentageForRemovals = notification.ChangePercentageForRemovals, + ChangePercentageForAdditions = GetTruncatedPercentage(notification.ChangePercentageForAdditions, notification.ThresholdPercentageForAdditions), + ChangePercentageForRemovals = GetTruncatedPercentage(notification.ChangePercentageForRemovals, notification.ThresholdPercentageForRemovals), ThresholdPercentageForAdditions = notification.ThresholdPercentageForAdditions, ThresholdPercentageForRemovals = notification.ThresholdPercentageForRemovals, ApiHostname = _apiHostname, @@ -107,8 +107,8 @@ public async Task CreateResolvedNotificationCardAsync(ThresholdNotificat GroupName = groupName, ChangeQuantityForAdditions = notification.ChangeQuantityForAdditions, ChangeQuantityForRemovals = notification.ChangeQuantityForRemovals, - ChangePercentageForAdditions = notification.ChangePercentageForAdditions, - ChangePercentageForRemovals = notification.ChangePercentageForRemovals, + ChangePercentageForAdditions = GetTruncatedPercentage(notification.ChangePercentageForAdditions, notification.ThresholdPercentageForAdditions), + ChangePercentageForRemovals = GetTruncatedPercentage(notification.ChangePercentageForRemovals, notification.ThresholdPercentageForRemovals), ThresholdPercentageForAdditions = notification.ThresholdPercentageForAdditions, ThresholdPercentageForRemovals = notification.ThresholdPercentageForRemovals, ResolvedBy = notification.ResolvedBy, @@ -125,6 +125,18 @@ public async Task CreateResolvedNotificationCardAsync(ThresholdNotificat return card; } + private double GetTruncatedPercentage(double changePercentage, int thresholdPercentage) + { + if (changePercentage - thresholdPercentage >= 1) + { + return Math.Round(changePercentage, 1); + } + else + { + return Math.Ceiling(changePercentage * 100) / 100.0; + } + } + /// public async Task CreateUnauthorizedNotificationCardAsync(ThresholdNotification notification) { From 306333ae77f3233b8a3f55630452c70ecd94c21e Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Fri, 26 Jul 2024 15:24:16 -0700 Subject: [PATCH 0106/1479] Removed PII from logging for WebAPI ResolveNotificationHandler --- .../Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs index 6b1610a25..39e8b277a 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs @@ -47,7 +47,7 @@ protected override async Task ExecuteCoreAsync(Reso await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"ResolveNotificationHandler request: " + - $"ThresholdNotificationId: {request.ThresholdNotificationId}, UserIdentifier: {request.UserIdentifier}, TargetOfficeGroupId: {thresholdNotification?.TargetOfficeGroupId}" + $"ThresholdNotificationId: {request.ThresholdNotificationId}, TargetOfficeGroupId: {thresholdNotification?.TargetOfficeGroupId}" }); if (thresholdNotification == null) { From 385623ba1c2b9aad65a6e479f3f990651c651921 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 29 Jul 2024 12:32:38 -0700 Subject: [PATCH 0107/1479] Fixed a bug where the PostBatch does not set the GraphUpdater status to GuestError when it should be --- .../Repositories.GraphGroups/GraphGroupMembershipUpdater.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupMembershipUpdater.cs b/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupMembershipUpdater.cs index a47e4c741..c2e50e80b 100644 --- a/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupMembershipUpdater.cs +++ b/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupMembershipUpdater.cs @@ -339,6 +339,8 @@ await _loggingRepository.LogMessageAsync(new LogMessage Message = $"{response.RequestId} was not added because it is a guest user and the destination does not allow guest users", RunId = RunId }); + + postResponse.ResponseCode = ResponseCode.GuestError; } } } From f4e37883d4febf7247c7d4dcece49679260c2f73 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Fri, 26 Jul 2024 11:12:42 -0700 Subject: [PATCH 0108/1479] Removed the breaking_changes.md file since we have it in the Tenant repo. --- breaking_changes.md | 319 -------------------------------------------- 1 file changed, 319 deletions(-) delete mode 100644 breaking_changes.md diff --git a/breaking_changes.md b/breaking_changes.md deleted file mode 100644 index 0ff187557..000000000 --- a/breaking_changes.md +++ /dev/null @@ -1,319 +0,0 @@ -# Breaking Changes - -## 7/9/2024 - -The storage of threshold notifications will be migrated from the current storage table to a SQL table. To accommodate this update, users will need to run the following script to migrate existing records. - -1. Export the existing storage table to a CSV file. This can be done using Azure Storage Explorer or any preferred tool for accessing Azure Storage. -2. Run the Set-MigrateStorageTable.ps1 script to migrate existing records from the storage table to the SQL table. - -``` - . .\Set-MigrateStorageTable.ps1 - Set-MigrateStorageTable -connectionString "" ` - -notificationsCsvPath "" ` -``` - -## 5/1/2024 - -Local auth has been disabled for App Configuration resource. -Going forward, the service connection used to deploy GMM resources must have "Azure App Configuration Data Owner" RBAC permission, before deploying this version. - -To grant the permission run Set-ServicePrincipalManagedIdentityRoles script. -``` -1. . .\Set-ServicePrincipalManagedIdentityRoles.ps1 -2. Set-ServicePrincipalManagedIdentityRoles -solutionAbbreviation -environmentAbbreviation -``` -Reference: -https://github.com/Azure/AppConfiguration/issues/692#issuecomment-1991914653 - -## 2/26/2024 -SyncJobs with SqlMembership source part need to be updated with a new JSON schema: - -Old format -``` -[ - { - "type": "SqlMembership", - "sources": - [ - { - "ids": [1,2,3], - "depth": 2, - "filter": "filter statement" - } - ] - } -] -``` - -New format -``` -[ - { - "type": "SqlMembership", - "source": { - "manager": { - "id": 1, - "depth": 2 - }, - "filter": "filter statement" - } - }, - { - "type": "SqlMembership", - "source": { - "manager": { - "id": 2, - "depth": 2 - }, - "filter": "filter statement" - } - }, - { - "type": "SqlMembership", - "source": { - "manager": { - "id": 3, - "depth": 2 - }, - "filter": "filter statement" - } - } -] -``` - -With this new schema, only one manager id is supported per source part. - -A powershell script has been added to help convert all existing SqlMembership jobs to the new format. -Script can be found here [Set-UpdateSqlMembershipQuery.ps1]( -https://github.com/microsoftgraph/group-membership-management/tree/main/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Scripts/Set-UpdateSqlMembershipQuery.ps1). - - 1. . ./Set-UpdateSqlMembershipQuery.ps1 - 2. Set-UpdateSqlMembershipQuery -ConnectionString "" - -## 8/1/2023 -SecurityGroup function has been renamed to GroupMembershipObtainer. -SecurityGroup service bus topic has been renamed to GroupMembership. - -SyncJob must set the type to GroupMembership in their query. -``` -[ - { - "type": "GroupMembership", - "source": "" - } -] -``` - -Existing jobs will need to be updated to use the new type. -To manually update the type, you can use the following T-SQL statement. -``` -UPDATE SyncJobs SET Query = REPLACE(Query, 'SecurityGroup', 'GroupMembership') WHERE Query LIKE '%SecurityGroup%' -``` -Once the renamed function is deployed, you can remove the old "SecurityGroup" function. - -## 7/26/2023 -### Create the jobs table in SQL database - -* Go to https://``-compute-``-webapi.azurewebsites.net/swagger/index.html -* Hit the endpoint `admin/databaseMigration`. This will create the jobs table in ``-data-``-jobs database -* A successful deployment to your environment will copy the jobs from storage account to sql database - -## 11/23/2022 - -### Updated keyVaultReaders_nonprod and keyVaultReaders_prod JSON schema - -See section [Create an Azure DevOps pipeline](README.md#create-an-azure-devops-pipeline) for more information. - -Previously only one set of permissions were supported, these were assigned to KeyVault 'secrets'. -With this change it now supports KeyVault 'secrets' and 'certificates' permissions. - -Old schema: -``` -[ - { - "objectId": "", - "permissions": [ "get", "set", "list" ] - } -] -``` -New schema: -``` -[ - { - "objectId": "", - "secrets": [ "get", "set", "list" ], - "certificates": [ "get", "set", "list" ] - } -] -``` - -If no certificate permissions are needed, you can omit the 'certificates' property. -``` -[ - { - "objectId": "", - "secrets": [ "get", "set", "list" ] - } -] -``` - -Existing GMM pipelines will need to update these variables: -- keyVaultReaders_nonprod -- keyVaultReaders_prod - -## 09/22/2022 - -### - SecurityGroup query format has changed to JSON -SecurityGroup query format has been updated to provide a single way to specify hybrid sync jobs. - -Previous query format sample. -``` -[ - { - "type": "SecurityGroup", - "sources": - [ - "a167b6c1-a2b3-4e16-aa8b-0ad0de5f44d9", - "04a8c19e-96a4-4570-b946-befd5bedca0e" - ] - } -] -``` -New query format sample: -``` -[ - { - "type": "SecurityGroup", - "source": "a167b6c1-a2b3-4e16-aa8b-0ad0de5f44d9" - }, - { - "type": "SecurityGroup", - "source": "04a8c19e-96a4-4570-b946-befd5bedca0e" - } -] -``` - -A powershell script has been added to help convert all existing SecurityGroup jobs to the new format. -Script can be found here [Set-UpdateSecurityGroupQuery.ps1]( -Service\GroupMembershipManagement\Hosts\SecurityGroup\Scripts\Set-UpdateSecurityGroupQuery.ps1). - - 1. . ./Set-UpdateSecurityGroupQuery.ps1 - 2. Set-UpdateSecurityGroupQuery -SubscriptionName "" ` - -SolutionAbbreviation "" ` - -EnvironmentAbbreviation "" ` - -Verbose - - -## 05/02/2022 - -### - SecurityGroup query format has changed to JSON -SecurityGroup query format has been updated in order to support hybrid sync jobs. List of semicolon separated group ids list has been replaced by a JSON query. - -Previous query format sample, list of group ids separated by semicolon. -``` -a167b6c1-a2b3-4e16-aa8b-0ad0de5f44d9;04a8c19e-96a4-4570-b946-befd5bedca0e -``` - -New query format sample: -``` -[ - { - "type": "SecurityGroup", - "sources": - [ - "a167b6c1-a2b3-4e16-aa8b-0ad0de5f44d9", - "04a8c19e-96a4-4570-b946-befd5bedca0e" - ] - } -] -``` - -A powershell script has been added to help convert all existing SecurityGroup jobs to the new format. -Script can be found here [Set-UpdateSecurityGroupQuery.ps1]( -Service\GroupMembershipManagement\Hosts\SecurityGroup\Scripts\Set-UpdateSecurityGroupQuery.ps1). - - 1. . ./Set-UpdateSecurityGroupQuery.ps1 - 2. Set-UpdateSecurityGroupQuery -SubscriptionName "" ` - -SolutionAbbreviation "" ` - -EnvironmentAbbreviation "" ` - -Verbose - -### - Type field has been removed. - -## 3/28/2022 -### Send group membership via blobs instead of queue - -GMM has been updated to send group membership through blobs instead of queues. So the 'membership' queue has been removed from the ARM templates and is not longer used by the code. - -See section [Grant SecurityGroup, GraphUpdater function access to storage account](README.md#grant-securitygroup-graphupdater-function-access-to-storage-account) for more information. - -Once these changes are deployed successfully to your enviroment it will be safe to delete the 'membership' queue from your Azure Resources. - -## 10/27/2021 -### GMM now uses an application secret instead of a certificate - -We have updated `Set-GraphCredentialsAzureADApplication.ps1` script to generate and store a client secret when creating ``-Graph-`` application. - -`graphAppCertificateName` which held the name of the certificate has been removed. -`graphAppClientSecret` has been added, the script will automatically create and populate it in the `prereqs` keyvault, and add it it to ``-Graph-`` application. - -In order to update GMM to use the application secret we need to: - -1. Create a new client secret for ``-Graph-`` application. -2. Add it to the prereqs keyvault. -3. Deploy GMM code. -4. Remove certificate. - -There are a couple of ways to accomplish these tasks: - -1. Running Set-GraphCredentialsAzureADApplication.ps1 script will take care of step 1 and 2 described above. -2. Or, manually creating the application secret and storing it in the prereqs keyvault. - -### Running the script -From your PowerShell command prompt navigate to the Scripts folder then type these commands: -``` -1. . ./Set-GraphCredentialsAzureADApplication.ps1 -2. Set-GraphCredentialsAzureADApplication -SubscriptionName "" ` - -SolutionAbbreviation "" ` - -EnvironmentAbbreviation "" ` - -TenantIdToCreateAppIn "" ` - -TenantIdWithKeyVault "" ` - -Verbose -``` -Follow the instructions on the screen. - -### Manual steps to create and store the application secret - -Creating the application secret - -1. In the Azure Portal navigate to your 'Azure Active Directory'. If you don't see it on your screen you can use the top search bar to locate it. -2. Navigate to 'App registrations' blade on the left menu. -3. Click on 'All applications" to locate and open your ``-Graph-`` application. -4. On your application screen click on 'Certificates & secrets' blade on the left menu. -5. Click on the 'Client secrets()' tabular menu. -6. Click on 'New client secret', provide a description, expiration finally click on 'Add. - -Copy the new secret since this is the only time it will be available and we need ti store it in the prereqs keyvault. - -Storing the application secret in the prereqs keyvault - -1. In the Azure Portal navigate to your 'Key vaults'. If you don't see it on your screen you can use the top search bar to locate it. -2. Locate and open ``-prereqs-`` keyvault. -3. Click on 'Secrets' blade on the left menu. -4. Click on 'Generate/Import' button. -5. Provide `graphAppClientSecret` as 'Name', and the new secret created in the previous section as 'Value'. -6. Click on 'Create' button. - -### Deploy latest GMM code -Once the new secret is generated and stored in the keyvault, you can proceed to deploy the latest GMM code to your environments. - -### Delete application certificate -1. In the Azure Portal navigate to your 'Azure Active Directory'. If you don't see it on your screen you can use the top search bar to locate it. -2. Navigate to 'App registrations' blade on the left menu. -3. Click on 'All applications" to locate and open your ``-Graph-`` application. -4. On your application screen click on 'Certificates and secrets' blade on the left menu. -5. Click on the 'Delete' button. (blue icon next to Certificate ID). -6. Locate and add your certificate. - -For more information about ``-Graph-`` application see section [Create ``-Graph-`` Azure Application](README.md#populate-prereqs-keyvault) From 83a2150e3ca2e3cf38db22a6ab284223a5060b3f Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Thu, 25 Jul 2024 15:51:29 -0700 Subject: [PATCH 0109/1479] Fixed bug with teams channels service account object Id. --- .../Hosts/JobTrigger/Function/Startup.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs index fbddcba45..ebbd25d5f 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs @@ -55,7 +55,8 @@ public override void Configure(IFunctionsHostBuilder builder) .AddSingleton>(services => { var configuration = services.GetService(); - return new KeyVaultSecret(Guid.Parse(configuration["teamsChannelServiceAccountObjectId"])); + var serviceAccountObjectId = string.IsNullOrWhiteSpace(configuration["teamsChannelServiceAccountObjectId"]) ? Guid.Empty : Guid.Parse(configuration["teamsChannelServiceAccountObjectId"]); + return new KeyVaultSecret(serviceAccountObjectId); }); builder.Services.AddGraphAPIClient(); From 1c38af7aaeb7b0ee1cdd194a3c435178912a0d09 Mon Sep 17 00:00:00 2001 From: abgonz Date: Tue, 30 Jul 2024 14:38:50 -0700 Subject: [PATCH 0110/1479] Upgrade packages in WebApi --- .../Hosts/WebApi/WebApi/WebApi.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj index 37e4abbf6..9a4ea7e94 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj @@ -16,11 +16,12 @@ - + + From 2af65b5b740e8a601e937c481dc0c862fb21d718 Mon Sep 17 00:00:00 2001 From: abgonz Date: Mon, 22 Jul 2024 10:35:42 -0700 Subject: [PATCH 0111/1479] Sort Jobs List by name --- .../Requests/GetJobsRequest.cs | 1 + .../WebApi/Services.WebApi/GetJobsHandler.cs | 8 +++++++- .../Controllers/v1/Jobs/JobsController.cs | 4 ++-- .../src/apis/entities/ODataQueryOptions.ts | 1 + UI/web-app/src/apis/jobs/JobsApi.ts | 9 ++++++--- .../src/components/JobsList/JobsList.base.tsx | 7 ++++++- UI/web-app/src/models/PagingOptions.ts | 1 + UI/web-app/src/store/pagingBar.slice.tsx | 18 +++++++++++++----- 8 files changed, 37 insertions(+), 12 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetJobsRequest.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetJobsRequest.cs index 28f5ce469..0d1286ec4 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetJobsRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetJobsRequest.cs @@ -10,5 +10,6 @@ namespace Services.Messages.Requests public class GetJobsRequest : RequestBase { public ODataQueryOptions? QueryOptions { get; set; } + public string? CustomSortBy { get; set; } } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetJobsHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetJobsHandler.cs index a93b2d2d5..b07ec5555 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetJobsHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetJobsHandler.cs @@ -73,6 +73,12 @@ protected override async Task ExecuteCoreAsync(GetJobsRequest r var targetGroups = (await _graphGroupRepository.GetGroupsAsync(jobs.Select(x => x.TargetOfficeGroupId).ToList())) .ToDictionary(x => x.ObjectId); + if (request.CustomSortBy == "targetGroupName") + { + var jobsWithNames = jobs.Select(job => new { Job = job, TargetGroupName = targetGroups.ContainsKey(job.TargetOfficeGroupId) ? targetGroups[job.TargetOfficeGroupId].Name : null }).ToList(); + jobs = jobsWithNames.OrderBy(job => job.TargetGroupName).Select(job => job.Job).ToList(); + } + foreach (var job in jobs) { var type = job.Destination.Contains("GroupMembership") ? "Group" : "Channel"; @@ -118,7 +124,7 @@ private IQueryable GetSyncJobsQuery() { var query = _databaseSyncJobsRepository.GetSyncJobs(true); - if (_httpContextAccessor.HttpContext.User.IsInRole(Roles.JOB_TENANT_READER) || + if (_httpContextAccessor.HttpContext.User.IsInRole(Roles.JOB_TENANT_READER) || _httpContextAccessor.HttpContext.User.IsInRole(Roles.JOB_TENANT_WRITER)) { return query; diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Jobs/JobsController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Jobs/JobsController.cs index 2814fde5e..46a500e49 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Jobs/JobsController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Jobs/JobsController.cs @@ -32,9 +32,9 @@ public JobsController( [Authorize(Roles = Models.Roles.JOB_OWNER_READER + "," + Models.Roles.JOB_OWNER_WRITER + "," + Models.Roles.JOB_TENANT_READER + "," + Models.Roles.JOB_TENANT_WRITER)] [HttpGet()] - public async Task>> GetJobsAsync(ODataQueryOptions queryOptions) + public async Task>> GetJobsAsync(ODataQueryOptions queryOptions, [FromQuery] string? customSortBy = null) { - var response = await _getJobsRequestHandler.ExecuteAsync(new GetJobsRequest { QueryOptions = queryOptions }); + var response = await _getJobsRequestHandler.ExecuteAsync(new GetJobsRequest { QueryOptions = queryOptions, CustomSortBy = customSortBy }); Response.Headers.Add("x-total-pages", response.TotalNumberOfPages.ToString()); Response.Headers.Add("x-current-page", response.CurrentPage.ToString()); return Ok(response.Model); diff --git a/UI/web-app/src/apis/entities/ODataQueryOptions.ts b/UI/web-app/src/apis/entities/ODataQueryOptions.ts index 749dfcfa5..f05d2076f 100644 --- a/UI/web-app/src/apis/entities/ODataQueryOptions.ts +++ b/UI/web-app/src/apis/entities/ODataQueryOptions.ts @@ -6,4 +6,5 @@ export type ODataQueryOptions = { $skip?: number; $filter?: string; $orderBy?: string; + customSortBy?: string; }; diff --git a/UI/web-app/src/apis/jobs/JobsApi.ts b/UI/web-app/src/apis/jobs/JobsApi.ts index 61f1ebe7b..06dd6a19c 100644 --- a/UI/web-app/src/apis/jobs/JobsApi.ts +++ b/UI/web-app/src/apis/jobs/JobsApi.ts @@ -34,17 +34,20 @@ export class JobsApi extends ApiBase implements IJobsApi { const response = await this.httpClient.post('/', jobWithSerializedQuery); this.ensureSuccessStatusCode(response); return response; -} + } private mapPagingOptionsToODataQueryOptions(pagingOptions?: PagingOptions): ODataQueryOptions | undefined { - return pagingOptions + const queryOptions: ODataQueryOptions = pagingOptions ? { $skip: pagingOptions.itemsToSkip, $top: pagingOptions.pageSize, $orderBy: pagingOptions.orderBy, $filter: pagingOptions.filter, + customSortBy: pagingOptions.customSortBy } - : undefined; + : {}; + + return queryOptions; } private mapJobEntityToJob(entity: JobEntity): Job { diff --git a/UI/web-app/src/components/JobsList/JobsList.base.tsx b/UI/web-app/src/components/JobsList/JobsList.base.tsx index 885d32299..8fa8af654 100644 --- a/UI/web-app/src/components/JobsList/JobsList.base.tsx +++ b/UI/web-app/src/components/JobsList/JobsList.base.tsx @@ -60,6 +60,7 @@ import { selectPagingBarfilterDestinationType, selectPagingBarfilterDestinationOwner, setPagingBarVisible, + setCustomSortBy, } from '../../store/pagingBar.slice'; import { resetManageMembership } from '../../store/manageMembership.slice'; @@ -148,7 +149,7 @@ export const JobsListBase: React.FunctionComponent = ( isResizable: true, isSorted: sortKey === 'targetGroupName', isSortedDescending, - columnActionsMode: 0, + showSortIconWhenUnsorted: true, }, { key: 'lastSuccessfulRunTime', @@ -236,6 +237,10 @@ export const JobsListBase: React.FunctionComponent = ( const isSortedDescending: boolean = !!column.isSorted && !column.isSortedDescending; dispatch(setSortKey(column.key)); dispatch(setIsSortedDescending(isSortedDescending)); + + if (column.key === 'targetGroupName') { + dispatch(setCustomSortBy(column.key)); + } } } diff --git a/UI/web-app/src/models/PagingOptions.ts b/UI/web-app/src/models/PagingOptions.ts index 3047973f0..748083a1a 100644 --- a/UI/web-app/src/models/PagingOptions.ts +++ b/UI/web-app/src/models/PagingOptions.ts @@ -6,4 +6,5 @@ export type PagingOptions = { itemsToSkip?: number; filter?: string; orderBy?: string; + customSortBy?: string; } \ No newline at end of file diff --git a/UI/web-app/src/store/pagingBar.slice.tsx b/UI/web-app/src/store/pagingBar.slice.tsx index 158446d8c..7acb9c568 100644 --- a/UI/web-app/src/store/pagingBar.slice.tsx +++ b/UI/web-app/src/store/pagingBar.slice.tsx @@ -21,6 +21,7 @@ export type PagingBarState = { filterDestinationType?: string; filterDestinationName?: string; filterDestinationOwner?: string; + customSortBy?: string; } // Define the initial state using that type @@ -37,7 +38,8 @@ const initialState: PagingBarState = { filterDestinationId: undefined, filterDestinationType: undefined, filterDestinationName: undefined, - filterDestinationOwner: undefined + filterDestinationOwner: undefined, + customSortBy: undefined }; export const pagingBarSlice = createSlice({ @@ -82,6 +84,9 @@ export const pagingBarSlice = createSlice({ }, setFilterDestinationOwner: (state, action) => { state.filterDestinationOwner = action.payload; + }, + setCustomSortBy: (state, action) => { + state.customSortBy = action.payload; } }, extraReducers: (builder) => { @@ -104,7 +109,8 @@ export const { setFilterDestinationName, setFilterDestinationOwner, setFilterActionRequired, - setFilterStatus + setFilterStatus, + setCustomSortBy } = pagingBarSlice.actions; export const selectPagingBar = (state: RootState) => state.pagingBar; export const selectPagingBarVisible = (state: RootState) => state.pagingBar.visible; @@ -129,12 +135,13 @@ export const selectPagingOptions = (state: RootState) => { filterDestinationId, filterDestinationName, filterDestinationType, - filterDestinationOwner + filterDestinationOwner, + customSortBy } = state.pagingBar; let orderByString: string | undefined = undefined; let filters: string[] = []; - if (sortKey !== undefined) { + if (sortKey !== undefined && sortKey !== 'targetGroupName') { orderByString = sortKey + (isSortedDescending ? ' desc' : ''); } if (filterDestinationId) { @@ -171,7 +178,8 @@ export const selectPagingOptions = (state: RootState) => { orderBy: orderByString, filter: filterString, sortKey, - isSortedDescending + isSortedDescending, + customSortBy: customSortBy !== 'targetGroupName' ? customSortBy : undefined }; }; From 34771ae51cfa7a5c23fc8d16410f11c242a304ec Mon Sep 17 00:00:00 2001 From: abgonz Date: Tue, 30 Jul 2024 15:40:33 -0700 Subject: [PATCH 0112/1479] Process groups in batches in LoadTestingPrepSubOrchestratorFunction, and add RequestId to AzureUserCreator request --- .../Requests/AzureUserCreatorRequest.cs | 1 + .../UserCreatorSubOrchestratorFunction.cs | 3 +- .../LoadTestingPrepSubOrchestratorFunction.cs | 35 ++++++++++++------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Requests/AzureUserCreatorRequest.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Requests/AzureUserCreatorRequest.cs index caffe26f8..bfb8982bf 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Requests/AzureUserCreatorRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Requests/AzureUserCreatorRequest.cs @@ -9,5 +9,6 @@ public class AzureUserCreatorRequest { public List PersonnelNumbers { get; set; } public TenantInformation TenantInformation { get; set; } + public string RequestId { get; set; } } } diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs index 794a9a3d3..da568c392 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs @@ -53,7 +53,8 @@ public async Task> CreateUsersAsync( var userCreatorRequest = new AzureUserCreatorRequest { PersonnelNumbers = batch, - TenantInformation = request.TenantInformation + TenantInformation = request.TenantInformation, + RequestId = context.InstanceId }; var newProfiles = await context.CallActivityAsync>(nameof(AzureUserCreatorFunction), userCreatorRequest); diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/LoadTestingPrepSubOrchestrator/LoadTestingPrepSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/LoadTestingPrepSubOrchestrator/LoadTestingPrepSubOrchestratorFunction.cs index 85f3478ff..e0041b5ea 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/LoadTestingPrepSubOrchestrator/LoadTestingPrepSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/LoadTestingPrepSubOrchestrator/LoadTestingPrepSubOrchestratorFunction.cs @@ -8,6 +8,7 @@ using Repositories.Contracts; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Hosts.NonProdService @@ -47,22 +48,30 @@ public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrat { var groupCount = groupSizesAndCounts[groupSize]; var groupIds = new List(); - // For each group size, create however many groups are needed and put the ids into a list associated with the group size. - for (var i = 0; i < groupCount; i++) + + // Process groups in batches + const int batchSize = 10; + for (var i = 0; i < groupCount; i += batchSize) { - var groupCreateResponse = await context.CallActivityAsync( - nameof(GroupCreatorAndRetrieverFunction), - new GroupCreatorAndRetrieverRequest - { - GroupName = $"LoadTesting_DestinationGroup_{groupSize}_{i+1}", - TestGroupType = TestGroupType.LoadTesting, - GroupOwnersIds = new List() { options.DestinationGroupOwnerId }, - RetrieveMembers = false, - RunId = runId - }); + var batchTasks = new List>(); + for (var j = 0; j < batchSize && (i + j) < groupCount; j++) + { + batchTasks.Add(context.CallActivityAsync( + nameof(GroupCreatorAndRetrieverFunction), + new GroupCreatorAndRetrieverRequest + { + GroupName = $"LoadTesting_DestinationGroup_{groupSize}_{i + j + 1}", + TestGroupType = TestGroupType.LoadTesting, + GroupOwnersIds = new List() { options.DestinationGroupOwnerId }, + RetrieveMembers = false, + RunId = runId + })); + } - groupIds.Add(groupCreateResponse.TargetGroup.ObjectId); + var batchResults = await Task.WhenAll(batchTasks); + groupIds.AddRange(batchResults.Select(result => result.TargetGroup.ObjectId)); } + groupSizesAndIds.Add(groupSize, groupIds); } From 65c9d5f874c270a70d543f80b2db914026fb54a4 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 31 Jul 2024 12:55:01 -0700 Subject: [PATCH 0113/1479] Fixed a bug where when a sync job has no changes and is set to Idle in MA, it did not reset TV to 0 --- .../MembershipSubOrchestratorFunction.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs index dca148011..1a71c0d7e 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs @@ -252,6 +252,7 @@ await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), { SyncJob = request.SyncJob, Status = SyncStatus.Idle, + ThresholdViolations = 0, DeltaStatus = MembershipDeltaStatus.NoChanges }); } From 4dc568fc54b6f20554a8957db52fc61ec3a09a9f Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 23 Jul 2024 10:40:39 -0700 Subject: [PATCH 0114/1479] Update roles and create new tables --- .../WebApi/Scripts/Set-AppRolesIfNeeded.ps1 | 12 +- .../Hosts/WebApi/WebApi.Models/Roles.cs | 5 +- .../Models/Operations.cs | 11 + .../Models/ServiceStatus.cs | 11 + .../GMMContext.cs | 15 + ...3_create_servicestatuses_table.Designer.cs | 511 ++++++++++++++++++ ...0711210843_create_servicestatuses_table.cs | 60 ++ ...e_service_status_history_table.Designer.cs | 511 ++++++++++++++++++ ...946_create_service_status_history_table.cs | 53 ++ 9 files changed, 1185 insertions(+), 4 deletions(-) create mode 100644 Service/GroupMembershipManagement/Models/Operations.cs create mode 100644 Service/GroupMembershipManagement/Models/ServiceStatus.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.Designer.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711212946_create_service_status_history_table.Designer.cs create mode 100644 Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711212946_create_service_status_history_table.cs diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 b/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 index 87921b331..0b6bef7e2 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 @@ -115,7 +115,7 @@ function Set-AppRolesIfNeeded { @{ DisplayName = "Hyperlink Administrator" Description = "Can add, update, or remove custom URLs." - Value = "Settings.Hyperlink.ReadWrite.All" + Value = "Hyperlink.ReadWrite.All" Id = [Guid]::NewGuid().ToString() IsEnabled = $True AllowedMemberTypes = @($memberTypes) @@ -123,7 +123,15 @@ function Set-AppRolesIfNeeded { @{ DisplayName = "Custom Membership Provider Administrator" Description = "Can add, update, or remove custom field names." - Value = "Settings.CustomSource.ReadWrite.All" + Value = "CustomSource.ReadWrite.All" + Id = [Guid]::NewGuid().ToString() + IsEnabled = $True + AllowedMemberTypes = @($memberTypes) + }, + @{ + DisplayName = "Reset Administrator" + Description = "Can reset or stop GMM." + Value = "Operations.Reset" Id = [Guid]::NewGuid().ToString() IsEnabled = $True AllowedMemberTypes = @($memberTypes) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs index c3fef15f9..836db5b4d 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs @@ -13,7 +13,8 @@ public class Roles public const string JOB_TENANT_READER = "Job.Read.All"; public const string JOB_TENANT_WRITER = "Job.ReadWrite.All"; public const string SUBMISSION_REVIEWER = "Submission.ReadWrite.All"; - public const string HYPERLINK_ADMINISTRATOR = "Settings.Hyperlink.ReadWrite.All"; - public const string CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR = "Settings.CustomSource.ReadWrite.All"; + public const string HYPERLINK_ADMINISTRATOR = "Hyperlink.ReadWrite.All"; + public const string CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR = "CustomSource.ReadWrite.All"; + public const string RESET_ADMINISTRATOR = "Operations.Reset"; } } diff --git a/Service/GroupMembershipManagement/Models/Operations.cs b/Service/GroupMembershipManagement/Models/Operations.cs new file mode 100644 index 000000000..9dccff50f --- /dev/null +++ b/Service/GroupMembershipManagement/Models/Operations.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Models +{ + public enum Operations + { + Reset, + Stop + } +} diff --git a/Service/GroupMembershipManagement/Models/ServiceStatus.cs b/Service/GroupMembershipManagement/Models/ServiceStatus.cs new file mode 100644 index 000000000..631b8dcb8 --- /dev/null +++ b/Service/GroupMembershipManagement/Models/ServiceStatus.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Models +{ + public enum ServiceStatus + { + Running, + Stopped + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs index 9be3f8489..fc9fa222b 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs @@ -24,6 +24,8 @@ public class GMMContext : DbContext public DbSet DestinationOwners { get; set; } public DbSet SyncJobChanges { get; set; } = null!; public DbSet ThresholdNotifications { get; set; } = null!; + public DbSet ServiceStatus { get; set; } + public DbSet ServiceStatusHistory { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -192,6 +194,19 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .IsUnicode(false); }); SeedNotificationTypes(modelBuilder); + + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Status) + .HasConversion( + v => v.ToString(), + v => (ServiceStatuses)Enum.Parse(typeof(ServiceStatuses), v)) + .IsUnicode(false); + entity.Ignore(x => x.Status); + entity.ToTable("ServiceStatuses"); + }); } private void SeedNotificationTypes(ModelBuilder modelBuilder) { diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.Designer.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.Designer.cs new file mode 100644 index 000000000..6f2d06ea3 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.Designer.cs @@ -0,0 +1,511 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Repositories.EntityFramework.Contexts; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + [DbContext(typeof(GMMContext))] + [Migration("20240711210843_create_servicestatuses_table")] + partial class create_servicestatuses_table + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.22") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.Property("DestinationOwnersId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("DestinationOwnersId", "SyncJobsId"); + + b.HasIndex("SyncJobsId"); + + b.ToTable("DestinationOwnerSyncJob"); + }); + + modelBuilder.Entity("Entities.SqlMembershipSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Attributes") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomLabel") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SqlMembershipSources"); + + b.HasData( + new + { + Id = new Guid("2736fc08-37c0-43a6-b47c-dc9ab9dccc5f"), + Name = "SqlMembership" + }); + }); + + modelBuilder.Entity("Entities.SyncJobChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("ChangeDetails") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeSource") + .HasColumnType("int"); + + b.Property("ChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValue(new DateTime(2024, 7, 11, 21, 8, 43, 699, DateTimeKind.Utc).AddTicks(5041)); + + b.Property("ChangedByDisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedByObjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ChangeSource"); + + b.HasIndex("ChangeTime"); + + b.HasIndex("ChangedByObjectId"); + + b.HasIndex("SyncJobId"); + + b.ToTable("SyncJobChanges"); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("DestinationNames"); + }); + + modelBuilder.Entity("Models.DestinationOwner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("ObjectId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.ToTable("DestinationOwners"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("NotificationTypeID") + .HasColumnType("int"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("NotificationTypeID"); + + b.HasIndex("SyncJobId", "NotificationTypeID") + .IsUnique(); + + b.ToTable("JobNotifications"); + }); + + modelBuilder.Entity("Models.NotificationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .IsUnicode(false) + .HasColumnType("varchar(max)"); + + b.HasKey("Id"); + + b.ToTable("NotificationTypes"); + + b.HasData( + new + { + Id = 1, + Disabled = false, + Name = "ThresholdNotification" + }, + new + { + Id = 2, + Disabled = false, + Name = "SyncStartedNotification" + }, + new + { + Id = 3, + Disabled = false, + Name = "SyncCompletedNotification" + }, + new + { + Id = 4, + Disabled = false, + Name = "DestinationNotExistNotification" + }, + new + { + Id = 5, + Disabled = false, + Name = "SourceNotExistNotification" + }, + new + { + Id = 6, + Disabled = false, + Name = "NotOwnerNotification" + }, + new + { + Id = 7, + Disabled = false, + Name = "NotValidSourceNotification" + }, + new + { + Id = 8, + Disabled = false, + Name = "NoDataNotification" + }, + new + { + Id = 9, + Disabled = false, + Name = "NormalThresholdNotification" + }, + new + { + Id = 10, + Disabled = false, + Name = "InactiveSyncJobNotification" + }); + }); + + modelBuilder.Entity("Models.PurgedSyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("PurgedAt") + .HasColumnType("datetime2"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PurgedSyncJobs"); + }); + + modelBuilder.Entity("Models.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("SettingKey") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SettingValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SettingKey") + .IsUnique(); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("Models.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SortPriority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Statuses", (string)null); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(450)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Status") + .IsUnique() + .HasFilter("[Status] IS NOT NULL"); + + b.ToTable("SyncJobs"); + }); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.HasOne("Models.DestinationOwner", null) + .WithMany() + .HasForeignKey("DestinationOwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.HasOne("Models.SyncJob", "SyncJob") + .WithOne("DestinationName") + .HasForeignKey("Models.DestinationName", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.HasOne("Models.NotificationType", "NotificationType") + .WithMany() + .HasForeignKey("NotificationTypeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", "SyncJob") + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationType"); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.HasOne("Models.Status", "StatusDetails") + .WithOne() + .HasForeignKey("Models.SyncJob", "Status") + .HasPrincipalKey("Models.Status", "Name") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("StatusDetails"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Navigation("DestinationName"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs new file mode 100644 index 000000000..27056d853 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.EntityFrameworkCore.Migrations; +using Models; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + public partial class create_servicestatuses_table : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + var tableName = "ServiceStatuses"; + migrationBuilder.CreateTable( + name: tableName, + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"), + Name = table.Column(type: "nvarchar(255)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServiceStatuses", x => x.Id); + }); + + var statuses = new Dictionary + { + { Guid.Parse("6B8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Running }, + { Guid.Parse("6C8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Stopped }, + { Guid.Parse("6D8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Starting }, + { Guid.Parse("6E8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Stopping } + }; + + foreach (var status in statuses) + { + migrationBuilder.InsertData( + table: tableName, + columns: new[] { "Id", "Name" }, + columnTypes: new[] { "uniqueidentifier", "nvarchar(255)" }, + values: new object[,] + { + { status.Key, status.Value.ToString() } + }); + } + + migrationBuilder.AddUniqueConstraint( + name: "AK_ServiceStatuses_Name", + table: tableName, + column: "Name"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint(name: "AK_ServiceStatuses_Name", table: "ServiceStatuses"); + migrationBuilder.DropTable(name: "ServiceStatuses"); + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711212946_create_service_status_history_table.Designer.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711212946_create_service_status_history_table.Designer.cs new file mode 100644 index 000000000..f1d750bcb --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711212946_create_service_status_history_table.Designer.cs @@ -0,0 +1,511 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Repositories.EntityFramework.Contexts; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + [DbContext(typeof(GMMContext))] + [Migration("20240711212946_create_service_status_history_table")] + partial class create_service_status_history_table + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.22") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.Property("DestinationOwnersId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("DestinationOwnersId", "SyncJobsId"); + + b.HasIndex("SyncJobsId"); + + b.ToTable("DestinationOwnerSyncJob"); + }); + + modelBuilder.Entity("Entities.SqlMembershipSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Attributes") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomLabel") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SqlMembershipSources"); + + b.HasData( + new + { + Id = new Guid("3c244cb8-9d30-4aef-a0b8-9d3634804e54"), + Name = "SqlMembership" + }); + }); + + modelBuilder.Entity("Entities.SyncJobChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("ChangeDetails") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangeSource") + .HasColumnType("int"); + + b.Property("ChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValue(new DateTime(2024, 7, 11, 21, 29, 46, 720, DateTimeKind.Utc).AddTicks(4242)); + + b.Property("ChangedByDisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedByObjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ChangeSource"); + + b.HasIndex("ChangeTime"); + + b.HasIndex("ChangedByObjectId"); + + b.HasIndex("SyncJobId"); + + b.ToTable("SyncJobChanges"); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("DestinationNames"); + }); + + modelBuilder.Entity("Models.DestinationOwner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("ObjectId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.ToTable("DestinationOwners"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("NotificationTypeID") + .HasColumnType("int"); + + b.Property("SyncJobId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("NotificationTypeID"); + + b.HasIndex("SyncJobId", "NotificationTypeID") + .IsUnique(); + + b.ToTable("JobNotifications"); + }); + + modelBuilder.Entity("Models.NotificationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .IsUnicode(false) + .HasColumnType("varchar(max)"); + + b.HasKey("Id"); + + b.ToTable("NotificationTypes"); + + b.HasData( + new + { + Id = 1, + Disabled = false, + Name = "ThresholdNotification" + }, + new + { + Id = 2, + Disabled = false, + Name = "SyncStartedNotification" + }, + new + { + Id = 3, + Disabled = false, + Name = "SyncCompletedNotification" + }, + new + { + Id = 4, + Disabled = false, + Name = "DestinationNotExistNotification" + }, + new + { + Id = 5, + Disabled = false, + Name = "SourceNotExistNotification" + }, + new + { + Id = 6, + Disabled = false, + Name = "NotOwnerNotification" + }, + new + { + Id = 7, + Disabled = false, + Name = "NotValidSourceNotification" + }, + new + { + Id = 8, + Disabled = false, + Name = "NoDataNotification" + }, + new + { + Id = 9, + Disabled = false, + Name = "NormalThresholdNotification" + }, + new + { + Id = 10, + Disabled = false, + Name = "InactiveSyncJobNotification" + }); + }); + + modelBuilder.Entity("Models.PurgedSyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("PurgedAt") + .HasColumnType("datetime2"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(max)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PurgedSyncJobs"); + }); + + modelBuilder.Entity("Models.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("SettingKey") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SettingValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SettingKey") + .IsUnique(); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("Models.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SortPriority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Statuses", (string)null); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("AllowEmptyDestination") + .HasColumnType("bit"); + + b.Property("Destination") + .HasColumnType("nvarchar(max)"); + + b.Property("DryRunTimeStamp") + .HasColumnType("datetime2"); + + b.Property("IgnoreThresholdOnce") + .HasColumnType("bit"); + + b.Property("IsDryRunEnabled") + .HasColumnType("bit"); + + b.Property("LastRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulRunTime") + .HasColumnType("datetime2"); + + b.Property("LastSuccessfulStartTime") + .HasColumnType("datetime2"); + + b.Property("Period") + .HasColumnType("int"); + + b.Property("Query") + .HasColumnType("nvarchar(max)"); + + b.Property("Requestor") + .HasColumnType("nvarchar(max)"); + + b.Property("RunId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("nvarchar(450)"); + + b.Property("TargetOfficeGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThresholdPercentageForAdditions") + .HasColumnType("int"); + + b.Property("ThresholdPercentageForRemovals") + .HasColumnType("int"); + + b.Property("ThresholdViolations") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Status") + .IsUnique() + .HasFilter("[Status] IS NOT NULL"); + + b.ToTable("SyncJobs"); + }); + + modelBuilder.Entity("DestinationOwnerSyncJob", b => + { + b.HasOne("Models.DestinationOwner", null) + .WithMany() + .HasForeignKey("DestinationOwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", null) + .WithMany() + .HasForeignKey("SyncJobsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.DestinationName", b => + { + b.HasOne("Models.SyncJob", "SyncJob") + .WithOne("DestinationName") + .HasForeignKey("Models.DestinationName", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.JobNotification", b => + { + b.HasOne("Models.NotificationType", "NotificationType") + .WithMany() + .HasForeignKey("NotificationTypeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.SyncJob", "SyncJob") + .WithMany() + .HasForeignKey("SyncJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationType"); + + b.Navigation("SyncJob"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.HasOne("Models.Status", "StatusDetails") + .WithOne() + .HasForeignKey("Models.SyncJob", "Status") + .HasPrincipalKey("Models.Status", "Name") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("StatusDetails"); + }); + + modelBuilder.Entity("Models.SyncJob", b => + { + b.Navigation("DestinationName"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711212946_create_service_status_history_table.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711212946_create_service_status_history_table.cs new file mode 100644 index 000000000..94328841a --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711212946_create_service_status_history_table.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repositories.EntityFramework.Contexts.Migrations +{ + public partial class create_service_status_history_table : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + var tableName = "ServiceStatusHistory"; + migrationBuilder.CreateTable( + name: tableName, + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"), + ServiceStatusId = table.Column(type: "uniqueidentifier", nullable: false), + RequestorObjectId = table.Column(type: "uniqueidentifier", nullable: false), + Timestamp = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + }, + constraints: table => + { + table.PrimaryKey("PK_ServiceStatusHistory", x => x.Id); + }); + + migrationBuilder.AddForeignKey( + name: "FK_ServiceStatusHistory_ServiceStatuses", + table: tableName, + column: "ServiceStatusId", + principalTable: "ServiceStatuses", + principalColumn: "Id", + onDelete: ReferentialAction.NoAction); + + migrationBuilder.InsertData( + table: tableName, + columns: new[] { "ServiceStatusId", "RequestorObjectId" }, + columnTypes: new[] { "uniqueidentifier", "uniqueidentifier" }, + values: new object[,] + { + { Guid.Parse("6B8AB321-D03F-EF11-86C3-6045BDC8336C"), Guid.Empty } // Adds a row with the Running status + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey(name: "FK_ServiceStatusHistory_ServiceStatuses", table: "ServiceStatusHistory"); + migrationBuilder.DropTable(name: "ServiceStatusHistory"); + } + } +} From 80722b8f63b9ae27c1ca26b23f11d632e98419ca Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 24 Jul 2024 15:50:39 -0700 Subject: [PATCH 0115/1479] Added Service Status repository --- .../Entities/ServiceStatus.cs | 15 +++++++ .../Entities/ServiceStatusHistory.cs | 17 ++++++++ .../GroupMembershipManagement.sln | 8 +++- .../{ServiceStatus.cs => ServiceStatuses.cs} | 6 ++- .../IServiceStatusRepository.cs | 15 +++++++ ...0711210843_create_servicestatuses_table.cs | 2 +- .../Repositories.ServiceStatus.csproj | 14 ++++++ .../ServiceStatusRepository.cs | 43 +++++++++++++++++++ 8 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 Service/GroupMembershipManagement/Entities/ServiceStatus.cs create mode 100644 Service/GroupMembershipManagement/Entities/ServiceStatusHistory.cs rename Service/GroupMembershipManagement/Models/{ServiceStatus.cs => ServiceStatuses.cs} (59%) create mode 100644 Service/GroupMembershipManagement/Repositories.Contracts/IServiceStatusRepository.cs create mode 100644 Service/GroupMembershipManagement/Repositories.ServiceStatus/Repositories.ServiceStatus.csproj create mode 100644 Service/GroupMembershipManagement/Repositories.ServiceStatus/ServiceStatusRepository.cs diff --git a/Service/GroupMembershipManagement/Entities/ServiceStatus.cs b/Service/GroupMembershipManagement/Entities/ServiceStatus.cs new file mode 100644 index 000000000..2a95ab781 --- /dev/null +++ b/Service/GroupMembershipManagement/Entities/ServiceStatus.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; +using System; + +namespace Entities +{ + public class ServiceStatus + { + public Guid Id { get; set; } + public string Name { get; set; } + public ServiceStatuses Status { get; set; } + } +} diff --git a/Service/GroupMembershipManagement/Entities/ServiceStatusHistory.cs b/Service/GroupMembershipManagement/Entities/ServiceStatusHistory.cs new file mode 100644 index 000000000..08a88c710 --- /dev/null +++ b/Service/GroupMembershipManagement/Entities/ServiceStatusHistory.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; + +namespace Entities +{ + public class ServiceStatusHistory + { + public Guid Id { get; set; } + public Guid ServiceStatusId { get; set; } + public Guid RequestorObjectId { get; set; } + public DateTime Timestamp { get; set; } + public ServiceStatus StatusDetails { get; set; } + + } +} diff --git a/Service/GroupMembershipManagement/GroupMembershipManagement.sln b/Service/GroupMembershipManagement/GroupMembershipManagement.sln index bfe7abf07..250575954 100644 --- a/Service/GroupMembershipManagement/GroupMembershipManagement.sln +++ b/Service/GroupMembershipManagement/GroupMembershipManagement.sln @@ -59,7 +59,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.FeatureFlag", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Contracts.Tests", "Repositories.Contracts.Tests\Repositories.Contracts.Tests.csproj", "{36E351DC-46F2-40FE-9F07-D15334B99AC1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repositories.RetryPolicyProvider", "Repositories.RetryPolicyProvider\Repositories.RetryPolicyProvider.csproj", "{1536370F-54D6-4325-B212-8AC535357DDF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.RetryPolicyProvider", "Repositories.RetryPolicyProvider\Repositories.RetryPolicyProvider.csproj", "{1536370F-54D6-4325-B212-8AC535357DDF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repositories.ServiceStatus", "Repositories.ServiceStatus\Repositories.ServiceStatus.csproj", "{CF416C44-4B7F-4722-A866-353A9FBD710A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -183,6 +185,10 @@ Global {1536370F-54D6-4325-B212-8AC535357DDF}.Debug|Any CPU.Build.0 = Debug|Any CPU {1536370F-54D6-4325-B212-8AC535357DDF}.Release|Any CPU.ActiveCfg = Release|Any CPU {1536370F-54D6-4325-B212-8AC535357DDF}.Release|Any CPU.Build.0 = Release|Any CPU + {CF416C44-4B7F-4722-A866-353A9FBD710A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF416C44-4B7F-4722-A866-353A9FBD710A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF416C44-4B7F-4722-A866-353A9FBD710A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF416C44-4B7F-4722-A866-353A9FBD710A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Models/ServiceStatus.cs b/Service/GroupMembershipManagement/Models/ServiceStatuses.cs similarity index 59% rename from Service/GroupMembershipManagement/Models/ServiceStatus.cs rename to Service/GroupMembershipManagement/Models/ServiceStatuses.cs index 631b8dcb8..7074bd813 100644 --- a/Service/GroupMembershipManagement/Models/ServiceStatus.cs +++ b/Service/GroupMembershipManagement/Models/ServiceStatuses.cs @@ -3,9 +3,11 @@ namespace Models { - public enum ServiceStatus + public enum ServiceStatuses { Running, - Stopped + Stopped, + Resetting, + Stopping } } diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/IServiceStatusRepository.cs b/Service/GroupMembershipManagement/Repositories.Contracts/IServiceStatusRepository.cs new file mode 100644 index 000000000..c0591463c --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.Contracts/IServiceStatusRepository.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; +using System; +using System.Threading.Tasks; + +namespace Repositories.Contracts +{ + public interface IServiceStatusRepository + { + Task GetCurrentServiceStatusAsync(); + Task SetServiceStatusAsync(ServiceStatuses status, Guid requestorId); + } +} diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs index 27056d853..cf1f12c83 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs @@ -29,7 +29,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { { Guid.Parse("6B8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Running }, { Guid.Parse("6C8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Stopped }, - { Guid.Parse("6D8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Starting }, + { Guid.Parse("6D8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Resetting }, { Guid.Parse("6E8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Stopping } }; diff --git a/Service/GroupMembershipManagement/Repositories.ServiceStatus/Repositories.ServiceStatus.csproj b/Service/GroupMembershipManagement/Repositories.ServiceStatus/Repositories.ServiceStatus.csproj new file mode 100644 index 000000000..ad758765b --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.ServiceStatus/Repositories.ServiceStatus.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/Service/GroupMembershipManagement/Repositories.ServiceStatus/ServiceStatusRepository.cs b/Service/GroupMembershipManagement/Repositories.ServiceStatus/ServiceStatusRepository.cs new file mode 100644 index 000000000..edd0413ec --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.ServiceStatus/ServiceStatusRepository.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Entities; +using Microsoft.EntityFrameworkCore; +using Repositories.Contracts; +using Repositories.EntityFramework.Contexts; +using ServiceStatusEntity = Entities.ServiceStatus; + +namespace Repositories.ServiceStatus +{ + public class ServiceStatusRepository : IServiceStatusRepository + { + private readonly GMMContext _writeContext; + private readonly GMMReadContext _readContext; + + public ServiceStatusRepository(GMMContext writeContext, GMMReadContext readContext) + { + _writeContext = writeContext ?? throw new ArgumentNullException(nameof(writeContext)); + _readContext = readContext ?? throw new ArgumentNullException(nameof(readContext)); + } + + public async Task GetCurrentServiceStatusAsync() + { + var status = await _readContext.ServiceStatusHistory.Include(x => x.StatusDetails).OrderByDescending(s => s.Timestamp).FirstAsync(); + return status.StatusDetails.Status; + } + + public Task SetServiceStatusAsync(Models.ServiceStatuses status, Guid requestorId) + { + _writeContext.ServiceStatusHistory.Add(new ServiceStatusHistory + { + RequestorObjectId = requestorId, + StatusDetails = new ServiceStatusEntity + { + Status = status + } + }); + + return _writeContext.SaveChangesAsync(); + } + } +} From 3d62769c788368c9e8c4b0304e5cc1b38980e034 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Thu, 25 Jul 2024 16:15:55 -0700 Subject: [PATCH 0116/1479] Added transient statuses --- .../Migrations/20240711210843_create_servicestatuses_table.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs index cf1f12c83..9b978464e 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs @@ -30,7 +30,8 @@ protected override void Up(MigrationBuilder migrationBuilder) { Guid.Parse("6B8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Running }, { Guid.Parse("6C8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Stopped }, { Guid.Parse("6D8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Resetting }, - { Guid.Parse("6E8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Stopping } + { Guid.Parse("6E8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Stopping }, + { Guid.Parse("6F8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Starting } }; foreach (var status in statuses) From 907f25bf1ec43fa66a0fdfac469ef03a35bbecf7 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Thu, 25 Jul 2024 16:17:00 -0700 Subject: [PATCH 0117/1479] Updated Status Entities --- .../Entities/ServiceStatus.cs | 3 +-- .../Models/ServiceStatuses.cs | 3 ++- .../GMMContext.cs | 3 +-- .../ServiceStatusRepository.cs | 15 +++++++-------- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Service/GroupMembershipManagement/Entities/ServiceStatus.cs b/Service/GroupMembershipManagement/Entities/ServiceStatus.cs index 2a95ab781..d66b497ee 100644 --- a/Service/GroupMembershipManagement/Entities/ServiceStatus.cs +++ b/Service/GroupMembershipManagement/Entities/ServiceStatus.cs @@ -9,7 +9,6 @@ namespace Entities public class ServiceStatus { public Guid Id { get; set; } - public string Name { get; set; } - public ServiceStatuses Status { get; set; } + public ServiceStatuses Name { get; set; } } } diff --git a/Service/GroupMembershipManagement/Models/ServiceStatuses.cs b/Service/GroupMembershipManagement/Models/ServiceStatuses.cs index 7074bd813..20a73bc60 100644 --- a/Service/GroupMembershipManagement/Models/ServiceStatuses.cs +++ b/Service/GroupMembershipManagement/Models/ServiceStatuses.cs @@ -8,6 +8,7 @@ public enum ServiceStatuses Running, Stopped, Resetting, - Stopping + Stopping, + Starting } } diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs index fc9fa222b..56f6fb7c3 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/GMMContext.cs @@ -199,12 +199,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); - entity.Property(e => e.Status) + entity.Property(e => e.Name) .HasConversion( v => v.ToString(), v => (ServiceStatuses)Enum.Parse(typeof(ServiceStatuses), v)) .IsUnicode(false); - entity.Ignore(x => x.Status); entity.ToTable("ServiceStatuses"); }); } diff --git a/Service/GroupMembershipManagement/Repositories.ServiceStatus/ServiceStatusRepository.cs b/Service/GroupMembershipManagement/Repositories.ServiceStatus/ServiceStatusRepository.cs index edd0413ec..e5295bf52 100644 --- a/Service/GroupMembershipManagement/Repositories.ServiceStatus/ServiceStatusRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.ServiceStatus/ServiceStatusRepository.cs @@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore; using Repositories.Contracts; using Repositories.EntityFramework.Contexts; -using ServiceStatusEntity = Entities.ServiceStatus; namespace Repositories.ServiceStatus { @@ -23,20 +22,20 @@ public ServiceStatusRepository(GMMContext writeContext, GMMReadContext readConte public async Task GetCurrentServiceStatusAsync() { var status = await _readContext.ServiceStatusHistory.Include(x => x.StatusDetails).OrderByDescending(s => s.Timestamp).FirstAsync(); - return status.StatusDetails.Status; + return status.StatusDetails.Name; } public Task SetServiceStatusAsync(Models.ServiceStatuses status, Guid requestorId) { - _writeContext.ServiceStatusHistory.Add(new ServiceStatusHistory + var statusDetails = _readContext.ServiceStatus.Single(s => s.Name == status); + var statusHistory = new ServiceStatusHistory { RequestorObjectId = requestorId, - StatusDetails = new ServiceStatusEntity - { - Status = status - } - }); + ServiceStatusId = statusDetails.Id, + Timestamp = DateTime.UtcNow + }; + _writeContext.ServiceStatusHistory.Add(statusHistory); return _writeContext.SaveChangesAsync(); } } From c22bfb9af24c615b7f8ee20833de69ecbb9e771f Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Thu, 25 Jul 2024 16:18:01 -0700 Subject: [PATCH 0118/1479] Created OperationsQueue --- .../IOperationsTaskQueue.cs | 13 +++++ .../Services.WebApi/OperationsTaskQueue.cs | 49 +++++++++++++++++++ .../WebApi/WebApi.Models/OperationDetails.cs | 13 +++++ .../Models/Operations.cs | 3 +- 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IOperationsTaskQueue.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/OperationsTaskQueue.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/OperationDetails.cs diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IOperationsTaskQueue.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IOperationsTaskQueue.cs new file mode 100644 index 000000000..8d0f03c88 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IOperationsTaskQueue.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using WebApi.Models; + +namespace Services.WebApi.Contracts +{ + public interface IOperationsTaskQueue + { + Task QueueAsync(OperationDetails operation); + Task DequeueAsync(); + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/OperationsTaskQueue.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/OperationsTaskQueue.cs new file mode 100644 index 000000000..a8532bfc4 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/OperationsTaskQueue.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Repositories.Contracts; +using Services.WebApi.Contracts; +using WebApi.Models; + +namespace Services.WebApi +{ + public class OperationsTaskQueue : IOperationsTaskQueue + { + private readonly Queue _operations; + private readonly SemaphoreSlim _signal = new SemaphoreSlim(1); + private readonly ILoggingRepository _loggingRepository; + + public OperationsTaskQueue(ILoggingRepository loggingRepository) + { + _operations = new Queue(); + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + } + + public async Task DequeueAsync() + { + if (_operations.Count == 0) + { + _signal.Release(); + return null; + } + + var operation = _operations.Dequeue(); + await _loggingRepository.LogMessageAsync(new Models.LogMessage { Message = $"Dequeued operation {operation.Operation}" }); + + _signal.Release(); + + return await Task.FromResult(operation); + } + + public async Task QueueAsync(OperationDetails operation) + { + await _signal.WaitAsync(); + + await _loggingRepository.LogMessageAsync(new Models.LogMessage { Message = $"Queuing operation {operation.Operation}" }); + if (!_operations.Any(o => o.Operation == operation.Operation)) + { + _operations.Enqueue(operation); + } + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/OperationDetails.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/OperationDetails.cs new file mode 100644 index 000000000..7afd98b74 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/OperationDetails.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; + +namespace WebApi.Models +{ + public class OperationDetails + { + public Operations Operation { get; set; } + public Guid RequestorId { get; set; } + } +} diff --git a/Service/GroupMembershipManagement/Models/Operations.cs b/Service/GroupMembershipManagement/Models/Operations.cs index 9dccff50f..b331c01ae 100644 --- a/Service/GroupMembershipManagement/Models/Operations.cs +++ b/Service/GroupMembershipManagement/Models/Operations.cs @@ -6,6 +6,7 @@ namespace Models public enum Operations { Reset, - Stop + Stop, + Start } } From 2fd6fc75c9b581ffbb719a051656313dd815c48e Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Thu, 25 Jul 2024 16:18:41 -0700 Subject: [PATCH 0119/1479] Created ResourceManagerService --- .../ResourceManagerServiceConfiguration.cs | 12 ++ .../IResourceManagerService.cs | 12 ++ .../Services.WebApi/ResourceManagerService.cs | 140 ++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/ResourceManagerServiceConfiguration.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IResourceManagerService.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResourceManagerService.cs diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/ResourceManagerServiceConfiguration.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/ResourceManagerServiceConfiguration.cs new file mode 100644 index 000000000..94a24070a --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/ResourceManagerServiceConfiguration.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Services.Entities +{ + public class ResourceManagerServiceConfiguration + { + public string SubscriptionId { get; set; } + public string DataResourceGroup { get; set; } + public string ComputeResourceGroup { get; set; } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IResourceManagerService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IResourceManagerService.cs new file mode 100644 index 000000000..b7622287b --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IResourceManagerService.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Services.Contracts +{ + public interface IResourceManagerService + { + Task StartWebSitesAsync(Guid requestorId, CancellationToken cancellationToken); + Task StopWebSitesAsync(Guid requestorId, CancellationToken cancellationToken); + Task ResetWebSitesAsync(Guid requestorId, CancellationToken cancellationToken); + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResourceManagerService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResourceManagerService.cs new file mode 100644 index 000000000..610ab86ee --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResourceManagerService.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Azure.Identity; +using Azure.ResourceManager; +using Azure.ResourceManager.AppService; +using Azure.ResourceManager.Resources; +using Models; +using Repositories.Contracts; +using Services.Contracts; +using Services.Entities; +using System.Text.RegularExpressions; + +namespace Services.WebApi +{ + public class ResourceManagerService : IResourceManagerService + { + private readonly ArmClient _client; + private readonly SubscriptionResource _subscription; + private readonly string _dataResourceGroupName; + private readonly string _computeResourceGroupName; + private readonly ILoggingRepository _loggingRepository; + + private ResourceGroupResource _computeResourceGroup; + private ResourceGroupResource _dataResourceGroup; + + public ResourceManagerService(ResourceManagerServiceConfiguration serviceConfiguration, + ILoggingRepository loggingRepository) + { + var subscritpionResourceId = SubscriptionResource.CreateResourceIdentifier(serviceConfiguration.SubscriptionId); + _client = new ArmClient(new DefaultAzureCredential(), serviceConfiguration.SubscriptionId); + _subscription = _client.GetSubscriptionResource(subscritpionResourceId); + _dataResourceGroupName = serviceConfiguration.DataResourceGroup; + _computeResourceGroupName = serviceConfiguration.ComputeResourceGroup; + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + } + + public async Task ResetWebSitesAsync(Guid requestorId, CancellationToken cancellationToken) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = "Resetting GMM..." + }); + + var webSites = await GetWebSitesAsync(cancellationToken); + foreach (var webSite in webSites) + { + Console.WriteLine($"Stopping {webSite.Data.Name}"); + await webSite.StopAsync(); + } + + var keyvaultNamePattern = @"SecretUri=https://(?.*)?\.vault.azure.net"; + var secretNamePattern = @"(.*\/)(?.*)\/(?.*)"; + + foreach (var webSite in webSites) + { + var appSettings = await webSite.GetApplicationSettingsAsync(cancellationToken); + foreach (var appSetting in appSettings.Value.Properties) + { + if (appSetting.Key.Equals("WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", StringComparison.InvariantCultureIgnoreCase)) + { + var keyvaultNameMatch = Regex.Match(appSetting.Value, keyvaultNamePattern); + var secretNameMatch = Regex.Match(appSetting.Value, secretNamePattern); + Console.WriteLine(secretNameMatch.Groups["secretName"]); + } + } + } + + foreach (var webSite in webSites) + { + await webSite.StartAsync(cancellationToken); + } + } + + public async Task StopWebSitesAsync(Guid requestorId, CancellationToken cancellationToken) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = "Stopping GMM..." + }); + + var webSites = await GetWebSitesAsync(cancellationToken); + foreach (var webSite in webSites) + { + await webSite.StopAsync(cancellationToken); + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Stopped {webSite.Data.Name}" + }); + } + } + + public async Task StartWebSitesAsync(Guid requestorId, CancellationToken cancellationToken) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = "Starting GMM..." + }); + + var webSites = await GetWebSitesAsync(cancellationToken); + foreach (var webSite in webSites) + { + await webSite.StartAsync(cancellationToken); + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Started {webSite.Data.Name}" + }); + } + } + + private async Task> GetWebSitesAsync(CancellationToken cancellationToken) + { + await GetResourceGroupsAsync(cancellationToken); + var allWebsites = _computeResourceGroup.GetWebSites(); + var filteredWebsites = allWebsites.Where(x => !x.Data.Name.EndsWith("-webapi", StringComparison.InvariantCultureIgnoreCase)).ToList(); + return filteredWebsites; + } + + private async Task GetResourceGroupsAsync(CancellationToken cancellationToken) + { + if (_computeResourceGroup == null && _dataResourceGroup == null) + { + var resourceGroupTasks = new List>> + { + _subscription.GetResourceGroupAsync(_computeResourceGroupName, cancellationToken), + _subscription.GetResourceGroupAsync(_dataResourceGroupName, cancellationToken) + }; + + var resourceGroups = await Task.WhenAll(resourceGroupTasks); + _computeResourceGroup = resourceGroups + .Where(r => r.Value.Data.Name.Equals(_computeResourceGroupName, StringComparison.InvariantCultureIgnoreCase)) + .First().Value; + + _dataResourceGroup = resourceGroups + .Where(r => r.Value.Data.Name.Equals(_dataResourceGroupName, StringComparison.InvariantCultureIgnoreCase)) + .First().Value; + } + } + } +} From 23edcc359855d864a640b5bae6a0703b60b80f7e Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Thu, 25 Jul 2024 16:19:04 -0700 Subject: [PATCH 0120/1479] Created OperationsBackgroundService --- .../OperationsBackgroundService.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs new file mode 100644 index 000000000..4a9516bbe --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Models; +using Repositories.Contracts; +using Services.Contracts; +using Services.WebApi.Contracts; + +namespace WebApi.BackgroundServices +{ + public class OperationsBackgroundService : BackgroundService + { + private readonly IResourceManagerService _resourceManagerService; + private readonly IOperationsTaskQueue _backgroundTaskQueue; + private readonly ILoggingRepository _loggingRepository; + private readonly IServiceProvider _services; + + public OperationsBackgroundService(IResourceManagerService resourceManagerService, + IOperationsTaskQueue backgroundTaskQueue, + ILoggingRepository loggingRepository, + IServiceProvider services) + { + _resourceManagerService = resourceManagerService ?? throw new ArgumentNullException(nameof(resourceManagerService)); + _backgroundTaskQueue = backgroundTaskQueue ?? throw new ArgumentNullException(nameof(backgroundTaskQueue)); + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + _services = services ?? throw new ArgumentNullException(nameof(services)); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + var operationDetails = await _backgroundTaskQueue.DequeueAsync(); + + if (operationDetails != null) + { + if (operationDetails.Operation == Operations.Reset) + { + await _resourceManagerService.ResetWebSitesAsync(operationDetails.RequestorId, cancellationToken); + await SetStatusAsync(ServiceStatuses.Running, operationDetails.RequestorId); + await _loggingRepository + .LogMessageAsync(new LogMessage + { + Message = "Reset operation completed." + }); + } + else if (operationDetails.Operation == Operations.Stop) + { + await _resourceManagerService.StopWebSitesAsync(operationDetails.RequestorId, cancellationToken); + await SetStatusAsync(ServiceStatuses.Stopped, operationDetails.RequestorId); + await _loggingRepository + .LogMessageAsync(new LogMessage + { + Message = "Stop operation completed." + }); + } + else if (operationDetails.Operation == Operations.Start) + { + await _resourceManagerService.StartWebSitesAsync(operationDetails.RequestorId, cancellationToken); + await SetStatusAsync(ServiceStatuses.Running, operationDetails.RequestorId); + await _loggingRepository + .LogMessageAsync(new LogMessage + { + Message = "Start operation completed." + }); + } + } + + await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); + } + catch (OperationCanceledException) + { + // No op, handle cancellation + } + catch (Exception ex) + { + await _loggingRepository + .LogMessageAsync(new LogMessage + { + Message = $"Unexpected error in {nameof(OperationsBackgroundService)}\n{ex.Message}" + }); + + await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken); + } + } + } + + private async Task SetStatusAsync(ServiceStatuses status, Guid requestorId) + { + using (var scope = _services.CreateScope()) + { + var scopedStatusRepository = scope.ServiceProvider.GetRequiredService(); + await scopedStatusRepository.SetServiceStatusAsync(status, requestorId); + } + } + } +} From 70cbadf8e3a38c4485c2082ea57e985951452604 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Thu, 25 Jul 2024 16:20:05 -0700 Subject: [PATCH 0121/1479] Created PostOperationHandler --- .../Requests/PostOperationRequest.cs | 20 +++ .../Responses/PostOperationResponse.cs | 15 +++ .../Services.WebApi/PostOperationHandler.cs | 122 ++++++++++++++++++ .../Configuration/MessageHandlerInjector.cs | 4 +- .../Hosts/WebApi/WebApi/Program.cs | 30 +++++ 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/PostOperationRequest.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/PostOperationResponse.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/PostOperationHandler.cs diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/PostOperationRequest.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/PostOperationRequest.cs new file mode 100644 index 000000000..1c03dc36c --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/PostOperationRequest.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Models; +using Services.Messages.Contracts.Requests; + +namespace Services.Messages.Requests +{ + public class PostOperationRequest : RequestBase + { + public PostOperationRequest(Operations operation, Guid requestorId) + { + Operation = operation; + RequestorId = requestorId; + } + + public Operations Operation { get; } + public Guid RequestorId { get; } + + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/PostOperationResponse.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/PostOperationResponse.cs new file mode 100644 index 000000000..e4f78b1b9 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/PostOperationResponse.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Services.Messages.Contracts.Responses; +using System.Net; + +namespace Services.Messages.Responses +{ + public class PostOperationResponse : ResponseBase + { + public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK; + public string? ErrorCode { get; set; } + public List? ResponseData { get; set; } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/PostOperationHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/PostOperationHandler.cs new file mode 100644 index 000000000..95cfd2691 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/PostOperationHandler.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; +using Repositories.Contracts; +using Services.Contracts; +using Services.Messages.Requests; +using Services.Messages.Responses; +using Services.WebApi.Contracts; +using System.Net; +using WebApi.Models; + +namespace Services.WebApi +{ + public class PostOperationHandler : RequestHandlerBase + { + private readonly ILoggingRepository _loggingRepository; + private readonly IServiceStatusRepository _serviceStatusRepository; + private readonly IOperationsTaskQueue _backgroundTaskService; + + public PostOperationHandler(ILoggingRepository loggingRepository, + IServiceStatusRepository serviceStatusRepository, + IOperationsTaskQueue backgroundTaskService) : base(loggingRepository) + { + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + _serviceStatusRepository = serviceStatusRepository ?? throw new ArgumentNullException(nameof(serviceStatusRepository)); + _backgroundTaskService = backgroundTaskService ?? throw new ArgumentNullException(nameof(backgroundTaskService)); + } + + protected override async Task ExecuteCoreAsync(PostOperationRequest request) + { + try + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Processing operation {request.Operation}." + }); + + var currentStatus = await _serviceStatusRepository.GetCurrentServiceStatusAsync(); + + if (request.Operation == Operations.Stop && currentStatus != ServiceStatuses.Running) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Operation {request.Operation}. Service is already {currentStatus}" + }); + + return new PostOperationResponse + { + StatusCode = HttpStatusCode.OK, + ResponseData = new List { currentStatus.ToString() } + }; + } + + if (request.Operation == Operations.Reset && currentStatus == ServiceStatuses.Resetting) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Operation {request.Operation}. Service is already {currentStatus}" + }); + + return new PostOperationResponse + { + StatusCode = HttpStatusCode.OK, + ResponseData = new List { currentStatus.ToString() } + }; + } + + if (request.Operation == Operations.Start && currentStatus == ServiceStatuses.Running) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Operation {request.Operation}. Service is already {currentStatus}" + }); + + return new PostOperationResponse + { + StatusCode = HttpStatusCode.OK, + ResponseData = new List { currentStatus.ToString() } + }; + } + + var tempStatus = request.Operation switch + { + Operations.Start => ServiceStatuses.Starting, + Operations.Stop => ServiceStatuses.Stopping, + Operations.Reset => ServiceStatuses.Resetting, + _ => throw new InvalidOperationException($"Invalid operation {request.Operation}") + }; + + var operationDetails = new OperationDetails + { + Operation = request.Operation, + RequestorId = request.RequestorId + }; + + await _backgroundTaskService.QueueAsync(operationDetails); + await _serviceStatusRepository.SetServiceStatusAsync(tempStatus, request.RequestorId); + currentStatus = await _serviceStatusRepository.GetCurrentServiceStatusAsync(); + + return new PostOperationResponse + { + StatusCode = HttpStatusCode.OK, + ResponseData = new List { currentStatus.ToString() } + }; + } + catch (Exception ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Error in {nameof(PostOperationHandler)} with operation {request.Operation}\n{ex.Message}" + }); + + return new PostOperationResponse + { + StatusCode = HttpStatusCode.InternalServerError, + ResponseData = new List { $"Unable to process {nameof(PostOperationRequest)} to {request.Operation}" } + }; + } + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs index c6d2aa21e..de66e53c4 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs @@ -16,7 +16,7 @@ public static IServiceCollection InjectMessageHandlers(this IServiceCollection s services.AddTransient, SearchDestinationsHandler>(); services.AddTransient, GetGroupEndpointsHandler>(); services.AddTransient, GetGroupOnboardingStatusHandler>(); - + services.AddTransient, GetSettingHandler>(); services.AddTransient, GetAllSettingsHandler>(); services.AddTransient, PatchSettingHandler>(); @@ -41,6 +41,8 @@ public static IServiceCollection InjectMessageHandlers(this IServiceCollection s services.AddTransient, PostJobHandler>(); services.AddTransient, RemoveGMMHandler>(); + services.AddTransient, PostOperationHandler>(); + return services; } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs index 345f69afd..4a3229f98 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs @@ -30,10 +30,16 @@ using Repositories.Localization; using Repositories.Logging; using Repositories.NotificationsRepository; +using Repositories.ServiceStatus; using Repositories.SqlMembershipRepository; +using Services.Contracts; using Services.Contracts.Notifications; +using Services.Entities; using Services.Notifications; +using Services.WebApi; +using Services.WebApi.Contracts; using System.Security.Claims; +using WebApi.BackgroundServices; using WebApi.Configuration; using WebApi.Models; @@ -45,6 +51,13 @@ public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); + builder.WebHost.ConfigureServices(services => + { + services.AddHostedService(); + }); + + builder.Services.AddSingleton(); + builder.Services.AddHttpContextAccessor(); builder.Services.AddControllers(options => @@ -283,7 +296,24 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddOptions() + .Configure>((settings, configuration, dataFactorySecrets) => + { + var computeResourceGroup = dataFactorySecrets.ResourceGroup.Replace("data", "compute", StringComparison.InvariantCultureIgnoreCase); + settings.SubscriptionId = dataFactorySecrets.SubscriptionId; + settings.DataResourceGroup = dataFactorySecrets.ResourceGroup; + settings.ComputeResourceGroup = computeResourceGroup; + + }); + builder.Services.AddSingleton(services => + { + var settings = services.GetRequiredService>(); + var loggingRepository = services.GetRequiredService(); + return new ResourceManagerService(settings.Value, loggingRepository); + }); var app = builder.Build(); From 49fc389011819dfde99e04e7e7dbff2b706d4f8e Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Thu, 25 Jul 2024 16:20:50 -0700 Subject: [PATCH 0122/1479] Added project and packages references --- .../Services.WebApi.Contracts.csproj | 1 + .../Hosts/WebApi/Services.WebApi/Services.WebApi.csproj | 3 +++ .../Hosts/WebApi/WebApi/WebApi.csproj | 2 ++ .../Hosts/WebApi/WebApi/WebApi.sln | 6 ++++++ 4 files changed, 12 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/Services.WebApi.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/Services.WebApi.Contracts.csproj index ccb4e8d27..ea3135b1b 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/Services.WebApi.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/Services.WebApi.Contracts.csproj @@ -9,6 +9,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj index 6094e1773..b496cb893 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj @@ -7,6 +7,8 @@ + + @@ -21,6 +23,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj index 9a4ea7e94..f7e9716ec 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj @@ -35,6 +35,8 @@ + + diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.sln b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.sln index 78eae6206..dfa895cd0 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.sln +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.sln @@ -49,6 +49,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqlMembershipObtainer.Entit EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Entities", "..\..\GraphUpdater\Services.Entities\Services.Entities.csproj", "{3AE58292-C456-4C09-AD12-EC374BA1E4DF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.ServiceStatus", "..\..\..\Repositories.ServiceStatus\Repositories.ServiceStatus.csproj", "{88B1C083-EE8D-4337-B411-BAF8A3AB098F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -151,6 +153,10 @@ Global {3AE58292-C456-4C09-AD12-EC374BA1E4DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AE58292-C456-4C09-AD12-EC374BA1E4DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AE58292-C456-4C09-AD12-EC374BA1E4DF}.Release|Any CPU.Build.0 = Release|Any CPU + {88B1C083-EE8D-4337-B411-BAF8A3AB098F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88B1C083-EE8D-4337-B411-BAF8A3AB098F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88B1C083-EE8D-4337-B411-BAF8A3AB098F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88B1C083-EE8D-4337-B411-BAF8A3AB098F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 3d53c9bee52999d86cfd2ae00936b2b51aaca829 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 29 Jul 2024 10:08:19 -0700 Subject: [PATCH 0123/1479] Updated scripts to grant permissions to the webapi --- .../Set-ServiceBusManagedIdentityRoles.ps1 | 11 +++++++++++ ...geAccountContainerManagedIdentityRoles.ps1 | 19 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Scripts/PostDeployment/Set-ServiceBusManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-ServiceBusManagedIdentityRoles.ps1 index 3ce4734ed..af65ffb41 100644 --- a/Scripts/PostDeployment/Set-ServiceBusManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-ServiceBusManagedIdentityRoles.ps1 @@ -77,5 +77,16 @@ function Set-ServiceBusManagedIdentityRoles { } } + $webApi = Get-AzWebApp -ResourceGroupName $ComputeResourceGroupName -Name "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-webapi" + $webApiSP = $webApi.Identity.PrincipalId + + if ($null -eq (Get-AzRoleAssignment -ObjectId $webApiSP -Scope $serviceBusNamespace.Id -RoleDefinitionName "Azure Service Bus Data Receiver")) { + New-AzRoleAssignment -ObjectId $webApiSP -Scope $serviceBusNamespace.Id -RoleDefinitionName "Azure Service Bus Data Receiver"; + Write-Host "Added role assignment to allow $($webApi.Name) to receive on the $($serviceBusNamespace.Name) namespace."; + } + else { + Write-Host "$($webApi.Name) can already receive messages from the $($serviceBusNamespace.Name) queue."; + } + Write-Host "Done."; } \ No newline at end of file diff --git a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 index 9e6910907..360916d01 100644 --- a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 @@ -87,5 +87,22 @@ function Set-StorageAccountContainerManagedIdentityRoles } } - Write-Host "Done attempting to add Storage Blob Data Contributor role assignments."; + $webApiPermissions = @("Storage Queue Data Contributor","Storage Table Data Contributor") + $webApi = Get-AzWebApp -ResourceGroupName "$SolutionAbbreviation-compute-$EnvironmentAbbreviation" -Name "$SolutionAbbreviation-compute-$EnvironmentAbbreviation-webapi" + $webApiSP = $webApi.Identity.PrincipalId + $dataRG = Get-AzResourceGroup -Name "$SolutionAbbreviation-data-$EnvironmentAbbreviation" + $dataRGResourceId = $dataRG.ResourceId + + foreach($permission in $webApiPermissions) + { + if ($null -eq (Get-AzRoleAssignment -ObjectId $webApiSP -Scope $dataRGResourceId -RoleDefinitionName $permission)) { + New-AzRoleAssignment -ObjectId $webApiSP -Scope $dataRGResourceId -RoleDefinitionName $permission; + Write-Host "Added role assignment $permission to $($webApi.Name) with scope $dataRGResourceId."; + } + else { + Write-Host "$($webApi.Name) can already $permission with scope $dataRGResourceId."; + } + } + + Write-Host "Done attempting to add Storage role assignments."; } \ No newline at end of file From 930d6be09774c172c8cf74eef4a9b4ab55bdb489 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 29 Jul 2024 10:33:09 -0700 Subject: [PATCH 0124/1479] Updated webapi permissions --- ...geAccountContainerManagedIdentityRoles.ps1 | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 index 360916d01..f9aa1ee56 100644 --- a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 @@ -104,5 +104,30 @@ function Set-StorageAccountContainerManagedIdentityRoles } } + $currentSubscription = (Get-AzContext).Subscription + $resourceGroups = @("$SolutionAbbreviation-data-$EnvironmentAbbreviation","$SolutionAbbreviation-compute-$EnvironmentAbbreviation") + + foreach ($resourceGroup in $resourceGroups) { + $scope = "/subscriptions/$($currentSubscription.Id)/resourceGroups/$resourceGroup" + if ($null -eq (Get-AzRoleAssignment -ObjectId $webApiSP -Scope $scope -RoleDefinitionName "Reader")) { + New-AzRoleAssignment -ObjectId $webApiSP -Scope $scope -RoleDefinitionName "Reader"; + Write-Host "Added role assignment Reader to $($webApi.Name) with scope $scope."; + } + else { + Write-Host "$($webApi.Name) can already Reader with scope $scope."; + } + } + + $scope = "/subscriptions/$($currentSubscription.Id)/resourceGroups/$SolutionAbbreviation-compute-$EnvironmentAbbreviation" + if ($null -eq (Get-AzRoleAssignment -ObjectId $webApiSP -Scope $scope -RoleDefinitionName "Website Contributor")) { + New-AzRoleAssignment -ObjectId $webApiSP -Scope $scope -RoleDefinitionName "Website Contributor"; + Write-Host "Added role assignment Website Contributor to $($webApi.Name) with scope $scope."; + } + else { + Write-Host "$($webApi.Name) can already Website Contributor with scope $scope."; + } + Write-Host "Done attempting to add Storage role assignments."; -} \ No newline at end of file +} + +Set-StorageAccountContainerManagedIdentityRoles -SolutionAbbreviation gmm -EnvironmentAbbreviation ar \ No newline at end of file From 360a0650eb84b493edfb224c099092b96224b3a0 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 29 Jul 2024 16:00:12 -0700 Subject: [PATCH 0125/1479] Updated manager and background services --- .../Services.Entities/OperationsSettings.cs | 18 ++ .../IResourceManagerService.cs | 3 +- .../Services.WebApi/ResourceManagerService.cs | 186 +++++++++++-- .../OperationsBackgroundService.cs | 261 +++++++++++++++++- .../Hosts/WebApi/WebApi/Program.cs | 33 ++- 5 files changed, 459 insertions(+), 42 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/OperationsSettings.cs diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/OperationsSettings.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/OperationsSettings.cs new file mode 100644 index 000000000..e09e199d2 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/OperationsSettings.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Services.Entities +{ + public class OperationsSettings + { + public string ServiceBusFQN { get; set; } + public string SyncJobTopic { get; set; } + public string MembershipUpdatersTopic { get; set; } + public string MembershipAggregatorQueue { get; set; } + public string JobSchedulerFunctionKey { get; set; } + public string JobSchedulerFunctionBaseUrl { get; set; } + public string DataResourceGroupName { get; set; } + public string ComputeResourceGroupName { get; set; } + + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IResourceManagerService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IResourceManagerService.cs index b7622287b..2b08c5123 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IResourceManagerService.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/IResourceManagerService.cs @@ -6,7 +6,8 @@ namespace Services.Contracts public interface IResourceManagerService { Task StartWebSitesAsync(Guid requestorId, CancellationToken cancellationToken); + Task StartWebSiteAsync(Guid requestorId, string websiteName, CancellationToken cancellationToken); Task StopWebSitesAsync(Guid requestorId, CancellationToken cancellationToken); - Task ResetWebSitesAsync(Guid requestorId, CancellationToken cancellationToken); + Task> GetWebSitesStorageAccountsAsync(CancellationToken cancellationToken); } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResourceManagerService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResourceManagerService.cs index 610ab86ee..b8a846657 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResourceManagerService.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResourceManagerService.cs @@ -5,14 +5,17 @@ using Azure.ResourceManager; using Azure.ResourceManager.AppService; using Azure.ResourceManager.Resources; +using Azure.Security.KeyVault.Secrets; using Models; using Repositories.Contracts; using Services.Contracts; using Services.Entities; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; namespace Services.WebApi { + [ExcludeFromCodeCoverage] public class ResourceManagerService : IResourceManagerService { private readonly ArmClient _client; @@ -35,81 +38,200 @@ public ResourceManagerService(ResourceManagerServiceConfiguration serviceConfigu _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - public async Task ResetWebSitesAsync(Guid requestorId, CancellationToken cancellationToken) + public async Task StopWebSitesAsync(Guid requestorId, CancellationToken cancellationToken) { await _loggingRepository.LogMessageAsync(new LogMessage { - Message = "Resetting GMM..." + Message = "Stopping GMM..." }); var webSites = await GetWebSitesAsync(cancellationToken); foreach (var webSite in webSites) { - Console.WriteLine($"Stopping {webSite.Data.Name}"); - await webSite.StopAsync(); + await webSite.StopAsync(cancellationToken); + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Stopped {webSite.Data.Name}" + }); } + } - var keyvaultNamePattern = @"SecretUri=https://(?.*)?\.vault.azure.net"; - var secretNamePattern = @"(.*\/)(?.*)\/(?.*)"; + public async Task StartWebSitesAsync(Guid requestorId, CancellationToken cancellationToken) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = "Starting GMM..." + }); + + var webSites = await GetWebSitesAsync(cancellationToken); + WebSiteResource? jobTrigger = null; foreach (var webSite in webSites) { - var appSettings = await webSite.GetApplicationSettingsAsync(cancellationToken); - foreach (var appSetting in appSettings.Value.Properties) + // JobTrigger should be started last + if (webSite.Data.Name.Contains("-JobTrigger", StringComparison.InvariantCultureIgnoreCase)) { - if (appSetting.Key.Equals("WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", StringComparison.InvariantCultureIgnoreCase)) - { - var keyvaultNameMatch = Regex.Match(appSetting.Value, keyvaultNamePattern); - var secretNameMatch = Regex.Match(appSetting.Value, secretNamePattern); - Console.WriteLine(secretNameMatch.Groups["secretName"]); - } + jobTrigger = webSite; + continue; } + + await webSite.StartAsync(cancellationToken); + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Started {webSite.Data.Name}" + }); } - foreach (var webSite in webSites) + if (jobTrigger != null) { - await webSite.StartAsync(cancellationToken); + await jobTrigger.StartAsync(cancellationToken); + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Started {jobTrigger.Data.Name}" + }); } } - public async Task StopWebSitesAsync(Guid requestorId, CancellationToken cancellationToken) + public async Task StartWebSiteAsync(Guid requestorId, string websiteName, CancellationToken cancellationToken) { await _loggingRepository.LogMessageAsync(new LogMessage { - Message = "Stopping GMM..." + Message = $"Starting {websiteName}..." }); - var webSites = await GetWebSitesAsync(cancellationToken); - foreach (var webSite in webSites) + try { - await webSite.StopAsync(cancellationToken); + await GetResourceGroupsAsync(cancellationToken); + var websiteResponse = _computeResourceGroup.GetWebSite(websiteName, cancellationToken); + var website = websiteResponse.Value; + + var response = await website.StartAsync(cancellationToken); await _loggingRepository.LogMessageAsync(new LogMessage { - Message = $"Stopped {webSite.Data.Name}" + Message = $"{websiteName} responded with code: {response.Status}." + }); + + var retryCount = 3; + var currentAttempts = 1; + + do + { + await Task.Delay(currentAttempts++ * 5000, cancellationToken); + website = _computeResourceGroup.GetWebSite(websiteName, cancellationToken); + if (website.Data.State == "Running") + { + break; + } + + } while (currentAttempts <= retryCount); + + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"{websiteName} is now {website.Data.State}." + }); + + // Give the website time to fully start + await Task.Delay(10000, cancellationToken); + } + catch (Azure.RequestFailedException rex) when (rex.Status == 404) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"{websiteName} was not found." }); } + catch (Exception ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Failed to start {websiteName}. {ex.Message}" + }); + } + } - public async Task StartWebSitesAsync(Guid requestorId, CancellationToken cancellationToken) + public async Task> GetWebSitesStorageAccountsAsync(CancellationToken cancellationToken) { await _loggingRepository.LogMessageAsync(new LogMessage { - Message = "Starting GMM..." + Message = "Getting storage account names..." }); + var secretNamePattern = @"(.*\/)(?.*)\/(?.*)"; + var keyvaultNamePattern = @"SecretUri=https://(?.*)?\.vault.azure.net"; var webSites = await GetWebSitesAsync(cancellationToken); + + // keyvault name, secret information + var kvSecrets = new Dictionary>(); + foreach (var webSite in webSites) { - await webSite.StartAsync(cancellationToken); - await _loggingRepository.LogMessageAsync(new LogMessage + var appSettings = await webSite.GetApplicationSettingsAsync(cancellationToken); + foreach (var appSetting in appSettings.Value.Properties) { - Message = $"Started {webSite.Data.Name}" - }); + if (appSetting.Key.Equals("WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", StringComparison.InvariantCultureIgnoreCase)) + { + var secretNameMatch = Regex.Match(appSetting.Value, secretNamePattern); + var keyvaultNameMatch = Regex.Match(appSetting.Value, keyvaultNamePattern); + + if (secretNameMatch.Groups["secretName"].Success && keyvaultNameMatch.Groups["kvName"].Success) + { + var keyvaultName = keyvaultNameMatch.Groups["kvName"].Value; + var secretName = secretNameMatch.Groups["secretName"].Value; + + if (kvSecrets.ContainsKey(keyvaultName)) + { + kvSecrets[keyvaultName].Add(new SecretInformation + { + SecretName = secretName, + FunctionName = webSite.Data.Name + }); + } + else + { + kvSecrets.Add(keyvaultName, new List + { + new SecretInformation + { + SecretName = secretName, + FunctionName = webSite.Data.Name + } + }); + } + } + } + } } + + var accountNamePattern = @"AccountName=(?.*?);"; + + foreach (var kvSecret in kvSecrets) + { + var kvUri = "https://" + kvSecret.Key + ".vault.azure.net"; + var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential()); + + foreach (var secretName in kvSecret.Value) + { + var secret = await client.GetSecretAsync(secretName.SecretName, cancellationToken: cancellationToken); + var accountNameMatch = Regex.Match(secret.Value.Value, accountNamePattern); + if (accountNameMatch.Groups["accountName"].Success) + { + secretName.SecretValue = accountNameMatch.Groups["accountName"].Value; + } + } + } + + // FunctionName, StorageAccountName + return kvSecrets.Values.SelectMany(x => x).ToList().ToDictionary(x => x.FunctionName, x => x.SecretValue); } private async Task> GetWebSitesAsync(CancellationToken cancellationToken) { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = "Getting websites..." + }); + await GetResourceGroupsAsync(cancellationToken); var allWebsites = _computeResourceGroup.GetWebSites(); var filteredWebsites = allWebsites.Where(x => !x.Data.Name.EndsWith("-webapi", StringComparison.InvariantCultureIgnoreCase)).ToList(); @@ -136,5 +258,13 @@ private async Task GetResourceGroupsAsync(CancellationToken cancellationToken) .First().Value; } } + + private class SecretInformation + { + public string SecretName { get; set; } + public string SecretValue { get; set; } + public string FunctionName { get; set; } + } + } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs index 4a9516bbe..7928206d6 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs @@ -1,28 +1,50 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Azure.Data.Tables; +using Azure.Identity; +using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; +using Azure.Storage.Queues; using Models; +using Newtonsoft.Json; +using Polly; using Repositories.Contracts; using Services.Contracts; +using Services.Entities; using Services.WebApi.Contracts; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using WebApi.Models; namespace WebApi.BackgroundServices { + [ExcludeFromCodeCoverage] public class OperationsBackgroundService : BackgroundService { + private readonly OperationsSettings _operationsSettings; private readonly IResourceManagerService _resourceManagerService; private readonly IOperationsTaskQueue _backgroundTaskQueue; private readonly ILoggingRepository _loggingRepository; private readonly IServiceProvider _services; + private readonly ServiceBusClient _serviceBusClient; + private readonly ServiceBusAdministrationClient _sbAdministrationClient; + private readonly HttpClient _httpClient; - public OperationsBackgroundService(IResourceManagerService resourceManagerService, + public OperationsBackgroundService(OperationsSettings operationsSettings, + IResourceManagerService resourceManagerService, IOperationsTaskQueue backgroundTaskQueue, ILoggingRepository loggingRepository, IServiceProvider services) { + _operationsSettings = operationsSettings ?? throw new ArgumentNullException(nameof(operationsSettings)); _resourceManagerService = resourceManagerService ?? throw new ArgumentNullException(nameof(resourceManagerService)); _backgroundTaskQueue = backgroundTaskQueue ?? throw new ArgumentNullException(nameof(backgroundTaskQueue)); _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); _services = services ?? throw new ArgumentNullException(nameof(services)); + + _serviceBusClient = new ServiceBusClient(operationsSettings.ServiceBusFQN, new DefaultAzureCredential()); + _sbAdministrationClient = new ServiceBusAdministrationClient(_operationsSettings.ServiceBusFQN, new DefaultAzureCredential()); + _httpClient = new HttpClient(); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) @@ -37,8 +59,15 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) { if (operationDetails.Operation == Operations.Reset) { - await _resourceManagerService.ResetWebSitesAsync(operationDetails.RequestorId, cancellationToken); - await SetStatusAsync(ServiceStatuses.Running, operationDetails.RequestorId); + await _resourceManagerService.StopWebSitesAsync(operationDetails.RequestorId, cancellationToken); + await ClearInternalTablesAndQueuesAsync(cancellationToken); + await ClearQueueAsync(_operationsSettings.MembershipAggregatorQueue, cancellationToken); + await ClearAllTopicsAsync(cancellationToken); + await ResetJobsInProgressAsync(); + await SetStatusAsync(ServiceStatuses.Stopped, operationDetails.RequestorId); + await CallJobSchedulerAsync(operationDetails.RequestorId, cancellationToken); + await StartGMMAsync(operationDetails, cancellationToken); + await _loggingRepository .LogMessageAsync(new LogMessage { @@ -48,6 +77,9 @@ await _loggingRepository else if (operationDetails.Operation == Operations.Stop) { await _resourceManagerService.StopWebSitesAsync(operationDetails.RequestorId, cancellationToken); + await ClearInternalTablesAndQueuesAsync(cancellationToken); + await ClearQueueAsync(_operationsSettings.MembershipAggregatorQueue, cancellationToken); + await ClearAllTopicsAsync(cancellationToken); await SetStatusAsync(ServiceStatuses.Stopped, operationDetails.RequestorId); await _loggingRepository .LogMessageAsync(new LogMessage @@ -57,8 +89,7 @@ await _loggingRepository } else if (operationDetails.Operation == Operations.Start) { - await _resourceManagerService.StartWebSitesAsync(operationDetails.RequestorId, cancellationToken); - await SetStatusAsync(ServiceStatuses.Running, operationDetails.RequestorId); + await StartGMMAsync(operationDetails, cancellationToken); await _loggingRepository .LogMessageAsync(new LogMessage { @@ -86,6 +117,12 @@ await _loggingRepository } } + private async Task StartGMMAsync(OperationDetails operationDetails, CancellationToken cancellationToken) + { + await _resourceManagerService.StartWebSitesAsync(operationDetails.RequestorId, cancellationToken); + await SetStatusAsync(ServiceStatuses.Running, operationDetails.RequestorId); + } + private async Task SetStatusAsync(ServiceStatuses status, Guid requestorId) { using (var scope = _services.CreateScope()) @@ -94,5 +131,219 @@ private async Task SetStatusAsync(ServiceStatuses status, Guid requestorId) await scopedStatusRepository.SetServiceStatusAsync(status, requestorId); } } + + private async Task ClearQueueAsync(string queueName, CancellationToken cancellationToken) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Clearing queue {queueName}" + }); + + var receiver = _serviceBusClient.CreateReceiver(queueName, new ServiceBusReceiverOptions { ReceiveMode = ServiceBusReceiveMode.ReceiveAndDelete }); + + while (true) + { + var messages = await receiver.ReceiveMessagesAsync(100, TimeSpan.FromSeconds(10), cancellationToken); + if (messages == null || messages.Count == 0) + { + break; + } + } + + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Clearing queue {queueName} completed" + }); + } + + private async Task ClearTopicAsync(string topicName, string subscriptionName, CancellationToken cancellationToken) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Clearing topic {topicName} subscription {subscriptionName}" + }); + + var receiver = _serviceBusClient.CreateReceiver(topicName, subscriptionName, new ServiceBusReceiverOptions { ReceiveMode = ServiceBusReceiveMode.ReceiveAndDelete }); + + while (true) + { + var messages = await receiver.ReceiveMessagesAsync(100, TimeSpan.FromSeconds(10), cancellationToken); + if (messages == null || messages.Count == 0) + { + break; + } + } + + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Clearing topic {topicName} subscription {subscriptionName} completed" + }); + } + + private async Task ClearAllTopicsAsync(CancellationToken cancellationToken) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = "Clearing topics and their subscriptions..." + }); + + var subscriptions = await Task.WhenAll(GetSubscriptionsAsync(_operationsSettings.MembershipUpdatersTopic), + GetSubscriptionsAsync(_operationsSettings.SyncJobTopic)); + + var membershipUpdaterSubscriptions = subscriptions.FirstOrDefault(s => s.topicName == _operationsSettings.MembershipUpdatersTopic).subscriptions; + var syncJobSubscriptions = subscriptions.FirstOrDefault(s => s.topicName == _operationsSettings.SyncJobTopic).subscriptions; + var clearTopicTasks = new List(); + + foreach (var subscription in membershipUpdaterSubscriptions) + { + clearTopicTasks.Add(ClearTopicAsync(_operationsSettings.MembershipUpdatersTopic, subscription, cancellationToken)); + } + + foreach (var subscription in syncJobSubscriptions) + { + clearTopicTasks.Add(ClearTopicAsync(_operationsSettings.SyncJobTopic, subscription, cancellationToken)); + } + + await Task.WhenAll(clearTopicTasks); + } + + private async Task<(string topicName, List subscriptions)> GetSubscriptionsAsync(string topicName) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Getting subscriptions for topic {topicName}" + }); + + var subscriptionNames = new List(); + await foreach (var subscription in _sbAdministrationClient.GetSubscriptionsAsync(topicName)) + { + subscriptionNames.Add(subscription.SubscriptionName); + } + + return (topicName, subscriptionNames); + } + + private async Task ClearInternalTablesAndQueuesAsync(CancellationToken cancellationToken) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = "Clearing function's internal tables and queues..." + }); + + // FunctionName, StorageAccountName + var storageAccounts = await _resourceManagerService.GetWebSitesStorageAccountsAsync(cancellationToken); + foreach (var storageAccount in storageAccounts) + { + await ClearInternalQueuesAsync(storageAccount.Value, storageAccount.Key); + await DeleteInternalTablesAsync(storageAccount.Value, storageAccount.Key); + } + + // Deleting a table takes at least 40 seconds, so we need to wait a bit before restarting the functions + // reference: https://learn.microsoft.com/en-us/rest/api/storageservices/delete-table#remarks + await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken); + } + + private async Task DeleteInternalTablesAsync(string storageAccountName, string functionName) + { + var tableServiceClient = new TableServiceClient(new Uri($"https://{storageAccountName}.table.core.windows.net"), new DefaultAzureCredential()); + var tables = tableServiceClient.QueryAsync(); + + await foreach (var table in tables) + { + try + { + if (!table.Name.EndsWith("History", StringComparison.InvariantCultureIgnoreCase) && + !table.Name.EndsWith("Instances", StringComparison.InvariantCultureIgnoreCase)) + continue; + + await tableServiceClient.DeleteTableAsync(table.Name); + + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Deleted table {table.Name} from account {storageAccountName} used by {functionName}" + }); + } + catch (Exception ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Failed to delete table {table.Name} from account {storageAccountName} used by {functionName}.\n{ex}" + }); + } + } + } + + private async Task ClearInternalQueuesAsync(string storageAccountName, string functionName) + { + var queueClient = new QueueServiceClient(new Uri($"https://{storageAccountName}.queue.core.windows.net"), new DefaultAzureCredential()); + var queues = queueClient.GetQueuesAsync(); + await foreach (var queue in queues) + { + try + { + if (!queue.Name.Contains("-control-", StringComparison.InvariantCultureIgnoreCase) && + !queue.Name.EndsWith("-workitems", StringComparison.InvariantCultureIgnoreCase)) + continue; + + var individualClient = queueClient.GetQueueClient(queue.Name); + await individualClient.ClearMessagesAsync(); + + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Cleared queue {queue.Name} from account {storageAccountName} used by {functionName}" + }); + + } + catch (Exception ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Failed to clear queue {queue.Name} from account {storageAccountName} used by {functionName}.\n{ex}" + }); + } + } + } + + private async Task ResetJobsInProgressAsync() + { + using (var scope = _services.CreateScope()) + { + var databaseSyncJobsRepository = scope.ServiceProvider.GetRequiredService(); + var jobsInProgress = await databaseSyncJobsRepository.GetSyncJobsAsync(true, SyncStatus.InProgress); + await databaseSyncJobsRepository.UpdateSyncJobsAsync(jobsInProgress, SyncStatus.Idle); + } + } + + private async Task CallJobSchedulerAsync(Guid requestorId, CancellationToken cancellationToken) + { + try + { + var retryPolicy = Policy.HandleResult(r => !r.IsSuccessStatusCode) + .WaitAndRetryAsync(5, + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + onRetry: async (response, timespan, retry, context) => + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Failed to call JobScheduler (${response.Result.StatusCode}). Retrying... {retry}" }); + }); + + await _resourceManagerService.StartWebSiteAsync(requestorId, $"{_operationsSettings.ComputeResourceGroupName}-JobScheduler", cancellationToken); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = "Calling JobScheduler..." }); + var jobSchedulerUrl = $"{_operationsSettings.JobSchedulerFunctionBaseUrl}/api/PipelineInvocationStarterFunction?code={_operationsSettings.JobSchedulerFunctionKey}"; + + await retryPolicy.ExecuteAsync(async () => + { + var request = new HttpRequestMessage(HttpMethod.Post, jobSchedulerUrl); + request.Content = new StringContent(JsonConvert.SerializeObject(new { DelayForDeploymentInMinutes = 5 }), Encoding.UTF8, "application/json"); + var response = await _httpClient.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"JobScheduler response: {response.StatusCode}.\n{responseContent}" }); + return response; + }); + } + catch (Exception ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Failed to call JobScheduler.\n{ex}" }); + } + } } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs index 4a3229f98..77f68f2af 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs @@ -51,13 +51,6 @@ public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); - builder.WebHost.ConfigureServices(services => - { - services.AddHostedService(); - }); - - builder.Services.AddSingleton(); - builder.Services.AddHttpContextAccessor(); builder.Services.AddControllers(options => @@ -305,7 +298,6 @@ public static void Main(string[] args) settings.SubscriptionId = dataFactorySecrets.SubscriptionId; settings.DataResourceGroup = dataFactorySecrets.ResourceGroup; settings.ComputeResourceGroup = computeResourceGroup; - }); builder.Services.AddSingleton(services => @@ -315,6 +307,31 @@ public static void Main(string[] args) return new ResourceManagerService(settings.Value, loggingRepository); }); + builder.Services.AddOptions().Configure((settings, configuration, services) => + { + var rmsc = services.GetRequiredService>(); + configuration.GetSection("Settings:ServiceBus").Bind(settings); + var functionBaseUrl = configuration.GetValue("Settings:JobSchedulerFunctionBaseUrl"); + var functionKey = configuration.GetValue("Settings:JobSchedulerFunctionKey"); + settings.JobSchedulerFunctionBaseUrl = functionBaseUrl; + settings.JobSchedulerFunctionKey = functionKey; + settings.DataResourceGroupName = rmsc.Value.DataResourceGroup; + settings.ComputeResourceGroupName = rmsc.Value.ComputeResourceGroup; + }); + + builder.Services.AddSingleton(services => + { + var settings = services.GetRequiredService>(); + return settings.Value; + }); + + builder.WebHost.ConfigureServices(services => + { + services.AddHostedService(); + }); + + builder.Services.AddSingleton(); + var app = builder.Build(); if (app.Environment.IsDevelopment()) From 8e6fafa51dd6985751aec488794681a3efc13f70 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 29 Jul 2024 16:00:29 -0700 Subject: [PATCH 0126/1479] added nuget packages --- .../Hosts/WebApi/Services.WebApi/Services.WebApi.csproj | 1 + .../Hosts/WebApi/WebApi/WebApi.csproj | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj index b496cb893..5c59369dd 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj @@ -9,6 +9,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj index f7e9716ec..09314379a 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj @@ -7,6 +7,9 @@ + + + From 2428f6b947e13605d9213269d17348325b318526 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 29 Jul 2024 16:00:52 -0700 Subject: [PATCH 0127/1479] added new settings to webapi bicep temaplte --- .../Infrastructure/compute/template.bicep | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep index f066e3293..c6310f456 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep @@ -81,6 +81,13 @@ var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataReso var sqlServerMSIConnectionString = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'sqlServerMSIConnectionString') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') +var serviceBusFQN = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusFQN') +var serviceBusMembershipAggregatorQueue = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusMembershipAggregatorQueue') +var serviceBusMembershipUpdatersTopic = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusMembershipUpdatersTopic') +var serviceBusSyncJobTopic = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusSyncJobTopic') +var jobSchedulerFunctionBaseUrl = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobSchedulerFunctionBaseUrl') +var jobSchedulerFunctionKey = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobSchedulerFunctionKey') + resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = { scope: resourceGroup(dataResourceGroup) name: appInsightsName @@ -191,6 +198,30 @@ var appSettings = [ name: 'Settings:GraphCredentials:UserAssignedManagedIdentityClientId' value: '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' } + { + name: 'Settings:ServiceBus:ServiceBusFQN' + value: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusFQN, '2019-09-01').secretUriWithVersion})' + } + { + name: 'Settings:ServiceBus:MembershipAggregatorQueue' + value: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusMembershipAggregatorQueue, '2019-09-01').secretUriWithVersion})' + } + { + name: 'Settings:ServiceBus:MembershipUpdatersTopic' + value: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusMembershipUpdatersTopic, '2019-09-01').secretUriWithVersion})' + } + { + name: 'Settings:ServiceBus:SyncJobTopic' + value: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusSyncJobTopic, '2019-09-01').secretUriWithVersion})' + } + { + name: 'Settings:JobSchedulerFunctionBaseUrl' + value: '@Microsoft.KeyVault(SecretUri=${reference(jobSchedulerFunctionBaseUrl, '2019-09-01').secretUriWithVersion})' + } + { + name: 'Settings:JobSchedulerFunctionKey' + value: '@Microsoft.KeyVault(SecretUri=${reference(jobSchedulerFunctionKey, '2019-09-01').secretUriWithVersion})' + } ] resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { From 69ca6f3a990cd195259137da4390c2fb99e9f99e Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 30 Jul 2024 16:00:06 -0700 Subject: [PATCH 0128/1479] Added GetServiceStatus models and service --- .../Requests/GetServiceStatusRequest.cs | 11 +++++ .../Responses/GetServiceStatusResponse.cs | 15 +++++++ .../GetServiceStatusHandler.cs | 45 +++++++++++++++++++ .../Configuration/MessageHandlerInjector.cs | 1 + 4 files changed, 72 insertions(+) create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetServiceStatusRequest.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetServiceStatusResponse.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetServiceStatusRequest.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetServiceStatusRequest.cs new file mode 100644 index 000000000..e4e1ae59b --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetServiceStatusRequest.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Services.Messages.Contracts.Requests; + +namespace Services.Messages.Requests +{ + public class GetServiceStatusRequest : RequestBase + { + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetServiceStatusResponse.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetServiceStatusResponse.cs new file mode 100644 index 000000000..8c5301289 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetServiceStatusResponse.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Models; +using Services.Messages.Contracts.Responses; +using System.Net; + +namespace Services.Messages.Responses +{ + public class GetServiceStatusResponse : ResponseBase + { + public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK; + public string? ErrorCode { get; set; } + public ServiceStatuses Status { get; set; } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs new file mode 100644 index 000000000..4c5b4f3e4 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; +using Repositories.Contracts; +using Services.Contracts; +using Services.Messages.Requests; +using Services.Messages.Responses; + +namespace Services.WebApi +{ + public class GetServiceStatusHandler : RequestHandlerBase + { + private readonly ILoggingRepository _loggingRepository; + private readonly IServiceStatusRepository _serviceStatusRepository; + + public GetServiceStatusHandler(ILoggingRepository loggingRepository, + IServiceStatusRepository serviceStatusRepository) : base(loggingRepository) + { + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + _serviceStatusRepository = serviceStatusRepository ?? throw new ArgumentNullException(nameof(serviceStatusRepository)); + } + + protected override async Task ExecuteCoreAsync(GetServiceStatusRequest request) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Retrieving service status." + }); + + var currentStatus = await _serviceStatusRepository.GetCurrentServiceStatusAsync(); + + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Current service status is {currentStatus}." + }); + + return new GetServiceStatusResponse + { + StatusCode = System.Net.HttpStatusCode.OK, + Status = currentStatus + }; + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs index de66e53c4..dcce59eb8 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs @@ -42,6 +42,7 @@ public static IServiceCollection InjectMessageHandlers(this IServiceCollection s services.AddTransient, RemoveGMMHandler>(); services.AddTransient, PostOperationHandler>(); + services.AddTransient, GetServiceStatusHandler>(); return services; } From e49dcbe967311d9d335e6ef414323916be6fd1dd Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 30 Jul 2024 16:00:40 -0700 Subject: [PATCH 0129/1479] Added new service status --- .../Responses/PostOperationResponse.cs | 2 + .../Services.WebApi/PostOperationHandler.cs | 9 +- .../WebApi.Tests/OperationsControllerTests.cs | 216 ++++++++++++++++++ .../OperationsBackgroundService.cs | 7 +- .../v1/Operations/OperationsController.cs | 69 ++++++ .../Models/ServiceStatuses.cs | 3 +- ...0711210843_create_servicestatuses_table.cs | 3 +- 7 files changed, 301 insertions(+), 8 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/OperationsControllerTests.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Operations/OperationsController.cs diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/PostOperationResponse.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/PostOperationResponse.cs index e4f78b1b9..b8fa4b5b3 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/PostOperationResponse.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/PostOperationResponse.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Models; using Services.Messages.Contracts.Responses; using System.Net; @@ -11,5 +12,6 @@ public class PostOperationResponse : ResponseBase public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK; public string? ErrorCode { get; set; } public List? ResponseData { get; set; } + public ServiceStatuses Status { get; set; } } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/PostOperationHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/PostOperationHandler.cs index 95cfd2691..9b7154059 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/PostOperationHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/PostOperationHandler.cs @@ -48,7 +48,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage return new PostOperationResponse { StatusCode = HttpStatusCode.OK, - ResponseData = new List { currentStatus.ToString() } + Status = currentStatus }; } @@ -62,7 +62,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage return new PostOperationResponse { StatusCode = HttpStatusCode.OK, - ResponseData = new List { currentStatus.ToString() } + Status = currentStatus }; } @@ -76,7 +76,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage return new PostOperationResponse { StatusCode = HttpStatusCode.OK, - ResponseData = new List { currentStatus.ToString() } + Status = currentStatus }; } @@ -101,7 +101,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage return new PostOperationResponse { StatusCode = HttpStatusCode.OK, - ResponseData = new List { currentStatus.ToString() } + Status = currentStatus }; } catch (Exception ex) @@ -114,6 +114,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage return new PostOperationResponse { StatusCode = HttpStatusCode.InternalServerError, + ErrorCode = "Error", ResponseData = new List { $"Unable to process {nameof(PostOperationRequest)} to {request.Operation}" } }; } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/OperationsControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/OperationsControllerTests.cs new file mode 100644 index 000000000..10b66606c --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/OperationsControllerTests.cs @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Models; +using Moq; +using Repositories.Contracts; +using Services.Messages.Responses; +using Services.WebApi; +using Services.WebApi.Contracts; +using System.Net; +using System.Security.Claims; +using WebApi.Controllers.v1.Operations; +using WebApi.Models; + +namespace WebApi.Tests +{ + [TestClass] + public class OperationsControllerTests + { + private HttpContext _context = null!; + private OperationsController _operationsController = null!; + private PostOperationHandler _postResetRequestHandler = null!; + private GetServiceStatusHandler _getServiceStatusRequestHandler = null!; + private Mock _loggingRepository = null!; + private Mock _serviceStatusRepository = null!; + private Mock _backgroundTaskService = null!; + + [TestInitialize] + public void Initialize() + { + _context = new DefaultHttpContext(); + + _loggingRepository = new Mock(); + _serviceStatusRepository = new Mock(); + _backgroundTaskService = new Mock(); + + _postResetRequestHandler = new PostOperationHandler(_loggingRepository.Object, + _serviceStatusRepository.Object, + _backgroundTaskService.Object); + + _getServiceStatusRequestHandler = new GetServiceStatusHandler(_loggingRepository.Object, + _serviceStatusRepository.Object); + + _operationsController = new OperationsController(_postResetRequestHandler, + _getServiceStatusRequestHandler) + { + ControllerContext = CreateControllerContext(new List + { + new Claim(ClaimTypes.Role, Roles.RESET_ADMINISTRATOR), + new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", Guid.NewGuid().ToString()) + }) + }; + } + + [TestMethod] + public async Task GetServiceStatusAsync() + { + _serviceStatusRepository.Setup(x => x.GetCurrentServiceStatusAsync()) + .ReturnsAsync(ServiceStatuses.Running); + + var response = await _operationsController.GetCurrentStatusAsync(); + var result = response as OkObjectResult; + var serviceStatusResponse = result.Value as GetServiceStatusResponse; + Assert.IsNotNull(serviceStatusResponse); + Assert.AreEqual(HttpStatusCode.OK, serviceStatusResponse.StatusCode); + Assert.AreEqual(ServiceStatuses.Running, serviceStatusResponse.Status); + } + + [TestMethod] + public async Task GetServiceStatusAsync_InternalServerError() + { + _serviceStatusRepository.Setup(x => x.GetCurrentServiceStatusAsync()) + .ThrowsAsync(new Exception("An error occurred")); + + var response = await _operationsController.GetCurrentStatusAsync(); + var result = response as ObjectResult; + Assert.IsNotNull(result); + Assert.AreEqual((int)HttpStatusCode.InternalServerError, result.StatusCode); + } + + [TestMethod] + public async Task StopWhenRunningAsync() + { + var statusChecks = 0; + _serviceStatusRepository.Setup(x => x.GetCurrentServiceStatusAsync()) + .ReturnsAsync(() => + { + if (statusChecks == 0) + { + statusChecks++; + return ServiceStatuses.Running; + } + + return ServiceStatuses.Stopping; + }); + + + var response = await _operationsController.ProcessOperationAsync(Operations.Stop); + var result = response as OkObjectResult; + var postOperationResponse = result.Value as PostOperationResponse; + Assert.IsNotNull(postOperationResponse); + Assert.AreEqual(HttpStatusCode.OK, postOperationResponse.StatusCode); + Assert.AreEqual(ServiceStatuses.Stopping, postOperationResponse.Status); + } + + [TestMethod] + public async Task StopWhenStoppedAsync() + { + _serviceStatusRepository.Setup(x => x.GetCurrentServiceStatusAsync()) + .ReturnsAsync(ServiceStatuses.Stopped); + + var response = await _operationsController.ProcessOperationAsync(Operations.Stop); + var result = response as OkObjectResult; + var postOperationResponse = result.Value as PostOperationResponse; + Assert.IsNotNull(postOperationResponse); + Assert.AreEqual(HttpStatusCode.OK, postOperationResponse.StatusCode); + Assert.AreEqual(ServiceStatuses.Stopped, postOperationResponse.Status); + } + + [TestMethod] + public async Task ResetWhenRunningAsync() + { + var statusChecks = 0; + _serviceStatusRepository.Setup(x => x.GetCurrentServiceStatusAsync()) + .ReturnsAsync(() => + { + if (statusChecks == 0) + { + statusChecks++; + return ServiceStatuses.Running; + } + + return ServiceStatuses.Resetting; + }); + + + var response = await _operationsController.ProcessOperationAsync(Operations.Reset); + var result = response as OkObjectResult; + var postOperationResponse = result.Value as PostOperationResponse; + Assert.IsNotNull(postOperationResponse); + Assert.AreEqual(HttpStatusCode.OK, postOperationResponse.StatusCode); + Assert.AreEqual(ServiceStatuses.Resetting, postOperationResponse.Status); + } + + [TestMethod] + public async Task ResetWhenResettingAsync() + { + _serviceStatusRepository.Setup(x => x.GetCurrentServiceStatusAsync()) + .ReturnsAsync(ServiceStatuses.Resetting); + + var response = await _operationsController.ProcessOperationAsync(Operations.Reset); + var result = response as OkObjectResult; + var postOperationResponse = result.Value as PostOperationResponse; + Assert.IsNotNull(postOperationResponse); + Assert.AreEqual(HttpStatusCode.OK, postOperationResponse.StatusCode); + Assert.AreEqual(ServiceStatuses.Resetting, postOperationResponse.Status); + } + + [TestMethod] + public async Task StartWhenNotRunningAsync() + { + var statusChecks = 0; + _serviceStatusRepository.Setup(x => x.GetCurrentServiceStatusAsync()) + .ReturnsAsync(() => + { + if (statusChecks == 0) + { + statusChecks++; + return ServiceStatuses.Stopped; + } + + return ServiceStatuses.Running; + }); + + + var response = await _operationsController.ProcessOperationAsync(Operations.Start); + var result = response as OkObjectResult; + var postOperationResponse = result.Value as PostOperationResponse; + Assert.IsNotNull(postOperationResponse); + Assert.AreEqual(HttpStatusCode.OK, postOperationResponse.StatusCode); + Assert.AreEqual(ServiceStatuses.Running, postOperationResponse.Status); + } + + [TestMethod] + public async Task StartWhenRunningAsync() + { + _serviceStatusRepository.Setup(x => x.GetCurrentServiceStatusAsync()) + .ReturnsAsync(ServiceStatuses.Running); + + var response = await _operationsController.ProcessOperationAsync(Operations.Start); + var result = response as OkObjectResult; + var postOperationResponse = result.Value as PostOperationResponse; + Assert.IsNotNull(postOperationResponse); + Assert.AreEqual(HttpStatusCode.OK, postOperationResponse.StatusCode); + Assert.AreEqual(ServiceStatuses.Running, postOperationResponse.Status); + } + + + private ControllerContext CreateControllerContext(List claims) + { + return new ControllerContext { HttpContext = CreateHttpContext(claims) }; + } + + private HttpContext CreateHttpContext(List claims) + { + var identity = new ClaimsIdentity(claims, "TestAuthType"); + var principal = new ClaimsPrincipal(identity); + var httpContext = new DefaultHttpContext(); + httpContext.User = principal; + + return httpContext; + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs index 7928206d6..6604fc1c2 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs @@ -49,11 +49,13 @@ public OperationsBackgroundService(OperationsSettings operationsSettings, protected override async Task ExecuteAsync(CancellationToken cancellationToken) { + OperationDetails? operationDetails = null; + while (!cancellationToken.IsCancellationRequested) { try { - var operationDetails = await _backgroundTaskQueue.DequeueAsync(); + operationDetails = await _backgroundTaskQueue.DequeueAsync(); if (operationDetails != null) { @@ -112,6 +114,7 @@ await _loggingRepository Message = $"Unexpected error in {nameof(OperationsBackgroundService)}\n{ex.Message}" }); + await SetStatusAsync(ServiceStatuses.Error, operationDetails?.RequestorId ?? Guid.Empty); await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken); } } @@ -144,7 +147,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage while (true) { var messages = await receiver.ReceiveMessagesAsync(100, TimeSpan.FromSeconds(10), cancellationToken); - if (messages == null || messages.Count == 0) + if (messages.Count == 0) { break; } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Operations/OperationsController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Operations/OperationsController.cs new file mode 100644 index 000000000..4b4b6efd3 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Operations/OperationsController.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Services.Contracts; +using Services.Messages.Requests; +using Services.Messages.Responses; +using System.Security.Claims; +using ServiceOperations = Models.Operations; + +namespace WebApi.Controllers.v1.Operations +{ + [ApiController] + [ApiVersion("1.0")] + [Route("api/v{version:apiVersion}/operations")] + public class OperationsController : ControllerBase + { + private readonly IRequestHandler _postResetRequestHandler; + private readonly IRequestHandler _getServiceStatusRequestHandler; + + + public OperationsController(IRequestHandler postResetRequestHandler, + IRequestHandler getServiceStatusRequestHandler) + { + _postResetRequestHandler = postResetRequestHandler ?? throw new ArgumentNullException(nameof(postResetRequestHandler)); + _getServiceStatusRequestHandler = getServiceStatusRequestHandler ?? throw new ArgumentNullException(nameof(getServiceStatusRequestHandler)); + } + + [Authorize(Roles = Models.Roles.RESET_ADMINISTRATOR)] + [HttpPost("{operation}")] + public async Task ProcessOperationAsync(ServiceOperations operation) + { + try + { + var claimsIdentity = User.Identity as ClaimsIdentity; + var userId = claimsIdentity?.Claims.FirstOrDefault(c => c.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value ?? Guid.Empty.ToString(); + var response = await _postResetRequestHandler.ExecuteAsync(new PostOperationRequest(operation, Guid.Parse(userId))); + return response.StatusCode switch + { + System.Net.HttpStatusCode.OK => Ok(response), + _ => Problem(statusCode: (int)System.Net.HttpStatusCode.InternalServerError) + }; + } + catch (Exception ex) + { + return Problem(statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: $"An error occurred: ${ex}"); + } + } + + [Authorize] + [HttpGet("servicestatus")] + public async Task GetCurrentStatusAsync() + { + try + { + var response = await _getServiceStatusRequestHandler.ExecuteAsync(new GetServiceStatusRequest()); + return response.StatusCode switch + { + System.Net.HttpStatusCode.OK => Ok(response), + _ => Problem(statusCode: (int)System.Net.HttpStatusCode.InternalServerError) + }; + } + catch (Exception ex) + { + return Problem(statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: $"An error occurred: ${ex}"); + } + } + } +} diff --git a/Service/GroupMembershipManagement/Models/ServiceStatuses.cs b/Service/GroupMembershipManagement/Models/ServiceStatuses.cs index 20a73bc60..a523e89fb 100644 --- a/Service/GroupMembershipManagement/Models/ServiceStatuses.cs +++ b/Service/GroupMembershipManagement/Models/ServiceStatuses.cs @@ -9,6 +9,7 @@ public enum ServiceStatuses Stopped, Resetting, Stopping, - Starting + Starting, + Error } } diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs index 9b978464e..f6318767f 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Migrations/20240711210843_create_servicestatuses_table.cs @@ -31,7 +31,8 @@ protected override void Up(MigrationBuilder migrationBuilder) { Guid.Parse("6C8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Stopped }, { Guid.Parse("6D8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Resetting }, { Guid.Parse("6E8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Stopping }, - { Guid.Parse("6F8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Starting } + { Guid.Parse("6F8AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Starting }, + { Guid.Parse("708AB321-D03F-EF11-86C3-6045BDC8336C"), ServiceStatuses.Error } }; foreach (var status in statuses) From 29ef88c942cea17c6d9b04458103617e5a9493fe Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 30 Jul 2024 16:10:07 -0700 Subject: [PATCH 0130/1479] Updated WebAPI coverage --- yaml/build-webapi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yaml/build-webapi.yml b/yaml/build-webapi.yml index 204ee470a..e6f2bbf63 100644 --- a/yaml/build-webapi.yml +++ b/yaml/build-webapi.yml @@ -60,7 +60,7 @@ stages: arguments: '--configuration $(BuildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura - /p:threshold=86 + /p:threshold=85 /p:thresholdType=line /p:thresholdStat=total /p:CoverletOutput=$(Build.SourcesDirectory)\TestResults\Coverage\webapi\ From 86737cff6d3faa4cb5d6ef89c361fed544ee6c02 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 30 Jul 2024 16:39:50 -0700 Subject: [PATCH 0131/1479] Updated script --- .../Set-StorageAccountContainerManagedIdentityRoles.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 index f9aa1ee56..7ae8d1430 100644 --- a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 @@ -128,6 +128,4 @@ function Set-StorageAccountContainerManagedIdentityRoles } Write-Host "Done attempting to add Storage role assignments."; -} - -Set-StorageAccountContainerManagedIdentityRoles -SolutionAbbreviation gmm -EnvironmentAbbreviation ar \ No newline at end of file +} \ No newline at end of file From 3e0fb96cb4ec4149437813aa5b997e4583d7f234 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Thu, 1 Aug 2024 05:55:06 -0700 Subject: [PATCH 0132/1479] hr ui fixes --- .../src/components/HRQuerySource/HRQuerySource.base.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 405f2b809..41449f459 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -71,7 +71,6 @@ export const HRQuerySourceBase: React.FunctionComponent = (p const [filteredValueOptions, setFilteredValueOptions] = useState({}); const [items, setItems] = useState([]); let options: IComboBoxOption[] = []; - let valueOptions: IComboBoxOption[] = []; const [groups, setGroups] = useState([]); const [selectedIndices, setSelectedIndices] = useState([]); const [groupingEnabled, setGroupingEnabled] = useState(false); @@ -1205,7 +1204,7 @@ const checkType = (value: string, type: string | undefined): string => { onChange={(event, option) => handleAttributeValueChange(item.attribute, event, option, index)} allowFreeInput autoComplete="off" - useComboBoxAsMenuWidth={true} + useComboBoxAsMenuWidth={false} /> } else { return { setSelectedIndices([]); selection.setAllSelected(false); setGroupingEnabled(true); + setFilteredOptions({}); + setFilteredValueOptions({}); } const renderItems = (items: IFilterPart[], isUpDownEnabled: boolean, groupIndex: number, childIndex?: number) => { From 073b9883750d10a2bee92a8b5e0b98ac88a9ba9f Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 31 Jul 2024 14:39:40 -0700 Subject: [PATCH 0133/1479] Simplified the migration by removing database rename step, this will just be managed by external teams themselves to save on deployment time and complexity --- Scripts/Set-UpdateSqlDatabaseNames.ps1 | 145 ------------------------- Scripts/main.ps1 | 8 -- 2 files changed, 153 deletions(-) delete mode 100644 Scripts/Set-UpdateSqlDatabaseNames.ps1 diff --git a/Scripts/Set-UpdateSqlDatabaseNames.ps1 b/Scripts/Set-UpdateSqlDatabaseNames.ps1 deleted file mode 100644 index 39a470ea2..000000000 --- a/Scripts/Set-UpdateSqlDatabaseNames.ps1 +++ /dev/null @@ -1,145 +0,0 @@ -$ErrorActionPreference = "Stop" -<# -.SYNOPSIS -Creates or updates Destination column - -.DESCRIPTION -Long description - -.PARAMETER SubscriptionName -Subscription name - -.PARAMETER SolutionAbbreviation -Abbreviation used to denote the overall solution - -.PARAMETER EnvironmentAbbreviation -Abbreviation for the environment - -.EXAMPLE -Set-UpdateSqlDatabaseNames -SubscriptionName "" ` - -SolutionAbbreviation "" ` - -EnvironmentAbbreviation "" ` - -Verbose -#> -function Set-UpdateSqlDatabaseNames { - [CmdletBinding()] - param( - [Parameter(Mandatory = $True)] - [string] $SubscriptionName, - [Parameter(Mandatory = $True)] - [string] $SolutionAbbreviation, - [Parameter(Mandatory = $True)] - [string] $EnvironmentAbbreviation - ) - - Write-Host "Start Set-UpdateSqlDatabaseNames" - - Set-AzContext -SubscriptionName $SubscriptionName - $resourceGroupName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation" - $sqlServerName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation" - $sqlReplicaServerName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation-r" - - $adfOldDatabaseName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation" - $jobsOldDatabaseName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation-jobs" - $jobsOldReplicaDatabaseName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation-jobs-R" - try { - - $adfOldDatabase = Get-AzSqlDatabase -ResourceGroupName $resourceGroupName ` - -ServerName $sqlServerName ` - -DatabaseName $adfOldDatabaseName - - Write-Host $adfOldDatabase.DatabaseName - - $jobsOldDatabase = Get-AzSqlDatabase -ResourceGroupName $resourceGroupName ` - -ServerName $sqlServerName ` - -DatabaseName $jobsOldDatabaseName - - Write-Host $jobsOldDatabase.DatabaseName - - } - catch { - Write-Host "An old database could not be found. This environment must already be migrated. Terminating script." - return - } - - # Remove the authorization locks on the databases so they can be moved - Write-Host "Getting authorization locks for this environment." - $authorizationLocks = (Get-AzResourceLock -ResourceGroupName $resourceGroupName) | Where-Object { $_.ResourceType -eq "Microsoft.Sql/servers/databases" } - - if($authorizationLocks) { - Write-Host "Removing old Sql authorization locks removed from this environment..." - - $authorizationLocks | ForEach-Object { - Remove-AzResourceLock -LockId $_.LockId -Force - } - - Write-Host "Sql authorization locks removed from this environment." - } - else { - Write-Host "There are no Sql authorization locks on this environment, proceeding with migration." - } - - try { - # Get the old replica database if it exists. - Write-Host "Checking if old replica database still exists..." - $jobsOldReplicaDatabase = Get-AzSqlDatabase -ResourceGroupName $resourceGroupName ` - -ServerName $sqlReplicaServerName ` - -DatabaseName $jobsOldReplicaDatabaseName - - Write-Host "Found the old replica database!" - } - catch { - Write-Host "Replica database doesn't exist. Proceeding with migration." - } - - # Delete old replica database and its link if it exists - if($jobsOldReplicaDatabase) { - Write-Host "Getting replication link for old database from Sql server." - $oldReplicationLink = $jobsOldReplicaDatabase | Get-AzSqlDatabaseReplicationLink -PartnerResourceGroupName $resourceGroupName -PartnerServerName $sqlServerName - - if ($oldReplicationLink) { - Write-Host "Removing old replication link..." - - $oldReplicationLink | Remove-AzSqlDatabaseSecondary - - Write-Host "Old replication link removed." - } - else { - Write-Host "No replication link exists, proceeding with migration" - } - - Write-Host "Deleting the old replica database now..." - - Remove-AzSqlDatabase -ResourceGroupName $resourceGroupName -ServerName $sqlReplicaServerName -DatabaseName $jobsOldReplicaDatabaseName - - Write-Host "Deleted the old replica database." - } - - try { - # Update the database names - Write-Host "Updating the database names" - - $adfNewDatabaseName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation-adf" - Set-AzSqlDatabase -ResourceGroupName $resourceGroupName ` - -ServerName $sqlServerName ` - -DatabaseName $adfOldDatabaseName ` - -NewName $adfNewDatabaseName - - Write-Host $adfOldDatabase.DatabaseName - - $jobsNewDatabaseName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation" - Set-AzSqlDatabase -ResourceGroupName $resourceGroupName ` - -ServerName $sqlServerName ` - -DatabaseName $jobsOldDatabaseName ` - -NewName $jobsNewDatabaseName - - Write-Host "Updated the database names" - } - catch { - Write-Host "Error updating the database names. Please check that the authorization locks and replication links have been removed accordingly." - Write-Host "Error: $_" - throw - } - - Write-Host "Finish Set-UpdateSqlDatabaseNames" -} \ No newline at end of file diff --git a/Scripts/main.ps1 b/Scripts/main.ps1 index c6768be3f..8d242cdff 100644 --- a/Scripts/main.ps1 +++ b/Scripts/main.ps1 @@ -18,12 +18,4 @@ function Update-GmmMigrationIfNeeded { Set-AzContext -SubscriptionName $SubscriptionName - Write-Verbose "Set-UpdateSqlDatabaseNames starting..." - . ($scriptsDirectory + '\Scripts\Set-UpdateSqlDatabaseNames.ps1') - Set-UpdateSqlDatabaseNames -SubscriptionName $SubscriptionName ` - -SolutionAbbreviation $SolutionAbbreviation ` - -EnvironmentAbbreviation $EnvironmentAbbreviation ` - -Verbose - - Write-Verbose "Set-UpdateSqlDatabaseNames completed." } \ No newline at end of file From 3d332a7ac653bfa8e689db02e8a0f1b27fadab76 Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 31 Jul 2024 15:11:01 -0700 Subject: [PATCH 0134/1479] Update GroupCreatorAndRetriever Function, add logging, increase timeout --- .../Function/UserCreator/AzureUserCreatorFunction.cs | 6 ++++++ .../UserCreatorSubOrchestratorFunction.cs | 7 +++++++ .../GroupCreatorAndRetrieverFunction.cs | 3 ++- .../GraphGroupInformationReader.cs | 10 ++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs index 09a9ef234..0b0ed4ae0 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs @@ -28,6 +28,12 @@ public async Task> AddUsersAsync([ActivityTrigger] { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(AzureUserCreatorFunction)} function started" }, VerbosityLevel.DEBUG); + if (request == null) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(AzureUserCreatorFunction)} exception, request is null" }, VerbosityLevel.DEBUG); + throw new ArgumentNullException(nameof(request)); + } + var newUsers = request.PersonnelNumbers.Select(x => new GraphUser { DisplayName = $"{request.TenantInformation.EmailPrefix} {x}", diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs index da568c392..57329f71d 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs @@ -57,6 +57,13 @@ public async Task> CreateUsersAsync( RequestId = context.InstanceId }; + if (!context.IsReplaying) + _ = _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"UserCreatorRequest: {Newtonsoft.Json.JsonConvert.SerializeObject(userCreatorRequest)}", + RunId = null + }); + var newProfiles = await context.CallActivityAsync>(nameof(AzureUserCreatorFunction), userCreatorRequest); profiles.AddRange(newProfiles); diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs index 7e98892f0..72e05b007 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs @@ -33,8 +33,9 @@ public async Task GenerateGroup([ActivityTrigg var attempts = 0; while(group == null && attempts < 5) { - group = await _graphGroupRepository.GetGroup(request.GroupName); await Task.Delay(5000); + group = await _graphGroupRepository.GetGroup(request.GroupName); + attempts++; } if(group == null) diff --git a/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupInformationReader.cs b/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupInformationReader.cs index 9aaad85d2..8209d5dc2 100644 --- a/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupInformationReader.cs +++ b/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupInformationReader.cs @@ -465,6 +465,16 @@ public async Task> GetGroupsAsync(List groupIds, Guid? } } + catch (ODataError ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"ODataError: {ex.GetBaseException().ToString()}", + RunId = runId + }); + + throw; + } catch (Exception ex) { await _loggingRepository.LogMessageAsync(new LogMessage From 717ecb1b4d09fea8fa896d409726fc69b9bb3d64 Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 31 Jul 2024 21:01:09 -0700 Subject: [PATCH 0135/1479] Prevent creation of user with invalid OnPremisesImmutableId --- .../UserCreator/AzureUserCreatorFunction.cs | 24 ++++++++++--------- .../UserCreatorSubOrchestratorFunction.cs | 11 +++++++++ .../GroupCreatorAndRetrieverFunction.cs | 2 +- .../LoadTestingGroupCalculatorFunction.cs | 2 +- .../GraphGroupInformationReader.cs | 12 +++++++++- .../GraphUserRepository.cs | 14 ++++++++++- 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs index 0b0ed4ae0..ced2d5b5d 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs @@ -34,17 +34,19 @@ public async Task> AddUsersAsync([ActivityTrigger] throw new ArgumentNullException(nameof(request)); } - var newUsers = request.PersonnelNumbers.Select(x => new GraphUser - { - DisplayName = $"{request.TenantInformation.EmailPrefix} {x}", - AccountEnabled = true, - Password = PasswordGenerator.GeneratePassword(), - MailNickname = $"{request.TenantInformation.EmailPrefix}{x}", - UsageLocation = request.TenantInformation.CountryCode, - UserPrincipalName = $"{request.TenantInformation.EmailPrefix}{x}@{request.TenantInformation.TenantDomain}", - OnPremisesImmutableId = x - }) - .ToList(); + var newUsers = request.PersonnelNumbers + .Where(x => long.TryParse(x, out _)) + .Select(x => new GraphUser + { + DisplayName = $"{request.TenantInformation.EmailPrefix} {x}", + AccountEnabled = true, + Password = PasswordGenerator.GeneratePassword(), + MailNickname = $"{request.TenantInformation.EmailPrefix}{x}", + UsageLocation = request.TenantInformation.CountryCode, + UserPrincipalName = $"{request.TenantInformation.EmailPrefix}{x}@{request.TenantInformation.TenantDomain}", + OnPremisesImmutableId = x + }) + .ToList(); var newProfiles = await _graphUserRepository.AddUsersAsync(newUsers, null); diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs index 57329f71d..df9043387 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs @@ -41,6 +41,17 @@ public async Task> CreateUsersAsync( RunId = null }); + if (request.PersonnelNumbers == null || !request.PersonnelNumbers.Any()) + { + if (!context.IsReplaying) + _ = _loggingRepository.LogMessageAsync(new LogMessage + { + Message = "No personnel numbers provided. Skipping user creation.", + RunId = null + }); + return new List(); + } + while ((batch = request.PersonnelNumbers.Skip(skip).Take(take).ToList()).Count > 0) { if (!context.IsReplaying) diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs index 72e05b007..75f15b9e9 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs @@ -33,9 +33,9 @@ public async Task GenerateGroup([ActivityTrigg var attempts = 0; while(group == null && attempts < 5) { - await Task.Delay(5000); group = await _graphGroupRepository.GetGroup(request.GroupName); attempts++; + await Task.Delay(5000); } if(group == null) diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingGroupCalculator/LoadTestingGroupCalculatorFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingGroupCalculator/LoadTestingGroupCalculatorFunction.cs index f4dd3bef3..98c88c0e5 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingGroupCalculator/LoadTestingGroupCalculatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingGroupCalculator/LoadTestingGroupCalculatorFunction.cs @@ -16,7 +16,7 @@ namespace Hosts.NonProdService public class LoadTestingGroupCalculatorFunction { private readonly ILoggingRepository _loggingRepository = null; - private readonly List _groupSizes = new List { 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 200000 }; + private readonly List _groupSizes = new List { 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 200000, 250000 }; public LoadTestingGroupCalculatorFunction(ILoggingRepository loggingRepository) { diff --git a/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupInformationReader.cs b/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupInformationReader.cs index 8209d5dc2..a49eb3470 100644 --- a/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupInformationReader.cs +++ b/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupInformationReader.cs @@ -398,6 +398,16 @@ public async Task CreateGroupAsync(string newGroupName, TestGroupType testGroupT var group = await _graphServiceClient.Groups.PostAsync(groupDefinition); } + catch (ODataError ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = ex.GetBaseException().ToString(), + RunId = runId + }); + + throw; + } catch (Exception e) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Error creating group: {e}" }); @@ -469,7 +479,7 @@ public async Task> GetGroupsAsync(List groupIds, Guid? { await _loggingRepository.LogMessageAsync(new LogMessage { - Message = $"ODataError: {ex.GetBaseException().ToString()}", + Message = ex.GetBaseException().ToString(), RunId = runId }); diff --git a/Service/GroupMembershipManagement/Repositories.GraphUsers/GraphUserRepository.cs b/Service/GroupMembershipManagement/Repositories.GraphUsers/GraphUserRepository.cs index 9a68c93b0..b5a9f62c7 100644 --- a/Service/GroupMembershipManagement/Repositories.GraphUsers/GraphUserRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.GraphUsers/GraphUserRepository.cs @@ -104,7 +104,19 @@ public async Task> GetAzureADObjectIdsAsync(IList limit = pnQueue.Count >= FILTER_CONDITION_LIMIT ? 10 : pnQueue.Count; for (var i = 0; i < limit; i++) { - requestPersonnelNumbers.Add(pnQueue.Dequeue()); + var personnelNumber = pnQueue.Dequeue(); + if (!string.IsNullOrEmpty(personnelNumber)) + { + requestPersonnelNumbers.Add(personnelNumber); + } + else + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Skipped empty onPremisesImmutableId.", + RunId = runId + }); + } } // build filter expression From a243011d3fd5f509a3a7c74c3c8a4dc680916f67 Mon Sep 17 00:00:00 2001 From: abgonz Date: Sun, 4 Aug 2024 07:51:04 -0700 Subject: [PATCH 0136/1479] Add null check for other request object properties --- .../Function/UserCreator/AzureUserCreatorFunction.cs | 6 +++--- .../GroupCreatorAndRetrieverFunction.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs index ced2d5b5d..9a1c639f1 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs @@ -28,10 +28,10 @@ public async Task> AddUsersAsync([ActivityTrigger] { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(AzureUserCreatorFunction)} function started" }, VerbosityLevel.DEBUG); - if (request == null) + if (request == null || request.TenantInformation == null) { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(AzureUserCreatorFunction)} exception, request is null" }, VerbosityLevel.DEBUG); - throw new ArgumentNullException(nameof(request)); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(AzureUserCreatorFunction)} exception, request or TenantInformation is null" }, VerbosityLevel.DEBUG); + throw new ArgumentNullException(request == null ? nameof(request) : nameof(request.TenantInformation)); } var newUsers = request.PersonnelNumbers diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs index 75f15b9e9..5fc89dadb 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs @@ -33,8 +33,8 @@ public async Task GenerateGroup([ActivityTrigg var attempts = 0; while(group == null && attempts < 5) { - group = await _graphGroupRepository.GetGroup(request.GroupName); attempts++; + group = await _graphGroupRepository.GetGroup(request.GroupName); await Task.Delay(5000); } From 54011cf4359cb67cbfa8d4f8ff5a3a9d2128eee3 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 31 Jul 2024 13:28:06 -0700 Subject: [PATCH 0137/1479] Updated nuget packages --- .../Hosts/WebApi/Services.WebApi/Services.WebApi.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj index 5c59369dd..7437294d2 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj @@ -7,13 +7,13 @@ - + - + From f9d05e8226b6fc80038109baa36e48fc857ca53b Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 31 Jul 2024 13:35:24 -0700 Subject: [PATCH 0138/1479] Updated nuget packages --- .../Common.DependencyInjection.csproj | 2 +- .../Hosts.FunctionBase/Hosts.FunctionBase.csproj | 4 ++-- .../Hosts/Console/DemoUserSetup/DemoUserSetup.csproj | 2 +- .../Repositories.Integration.Tests.csproj | 2 +- .../Hosts/JobScheduler/Services/Services.csproj | 2 +- .../Hosts/JobTrigger/Function/JobTrigger.csproj | 2 +- .../Hosts/NonProdService/Function/NonProdService.csproj | 2 +- .../Hosts/NonProdService/Services.Tests/Services.Tests.csproj | 2 +- .../Hosts/Notifier/Services.Notifier/Services.Notifier.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 2 +- .../SqlMembershipObtainer/Services/Services/Services.csproj | 4 ++-- .../Hosts/WebApi/WebApi/WebApi.csproj | 2 +- .../Repositories.BlobStorage/Repositories.BlobStorage.csproj | 2 +- .../Repositories.DataFactory/Repositories.DataFactory.csproj | 4 ++-- .../Repositories.EntityFramework.Contexts.csproj | 4 ++-- .../Repositories.EntityFramework.csproj | 2 +- .../Repositories.SqlMembershipRepository.csproj | 4 ++-- 17 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Service/GroupMembershipManagement/Common.DependencyInjection/Common.DependencyInjection.csproj b/Service/GroupMembershipManagement/Common.DependencyInjection/Common.DependencyInjection.csproj index 3d57695d3..7e5175f99 100644 --- a/Service/GroupMembershipManagement/Common.DependencyInjection/Common.DependencyInjection.csproj +++ b/Service/GroupMembershipManagement/Common.DependencyInjection/Common.DependencyInjection.csproj @@ -5,7 +5,7 @@ - + diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj b/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj index 913d3b06d..9eaac355b 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj @@ -5,11 +5,11 @@ - + - + diff --git a/Service/GroupMembershipManagement/Hosts/Console/DemoUserSetup/DemoUserSetup.csproj b/Service/GroupMembershipManagement/Hosts/Console/DemoUserSetup/DemoUserSetup.csproj index aaead6566..4255a7b62 100644 --- a/Service/GroupMembershipManagement/Hosts/Console/DemoUserSetup/DemoUserSetup.csproj +++ b/Service/GroupMembershipManagement/Hosts/Console/DemoUserSetup/DemoUserSetup.csproj @@ -6,7 +6,7 @@ - + diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories.Integration.Tests/Repositories.Integration.Tests.csproj b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories.Integration.Tests/Repositories.Integration.Tests.csproj index 395d6544d..c960a27bf 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories.Integration.Tests/Repositories.Integration.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories.Integration.Tests/Repositories.Integration.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/Services.csproj index df924e2d9..f7d44f382 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/Services.csproj @@ -4,7 +4,7 @@ net6.0 - + diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj index d1a61a5be..70bc684c6 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj @@ -5,7 +5,7 @@ 41fcfc96-9608-4426-9cd8-181202f026a4 - + diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj index d5eff5871..f062002f4 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj @@ -5,7 +5,7 @@ 41fcfc96-9608-4426-9cd8-181202f026a4 - + diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Tests/Services.Tests.csproj index cc67c0af9..697600956 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Tests/Services.Tests.csproj @@ -12,7 +12,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/Services.Notifier.csproj b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/Services.Notifier.csproj index f14b42a77..88a4fc349 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/Services.Notifier.csproj +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/Services.Notifier.csproj @@ -5,7 +5,7 @@ - + diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Services.Tests.csproj index 76a79dd3d..3561d0f87 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Services.Tests.csproj @@ -19,7 +19,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services/Services/Services.csproj index 8e18a4b71..c87da4b70 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services/Services/Services.csproj @@ -7,9 +7,9 @@ - + - + diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj index 09314379a..49b78dd23 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj @@ -23,7 +23,7 @@ - + diff --git a/Service/GroupMembershipManagement/Repositories.BlobStorage/Repositories.BlobStorage.csproj b/Service/GroupMembershipManagement/Repositories.BlobStorage/Repositories.BlobStorage.csproj index 0985f5a70..57b3741a1 100644 --- a/Service/GroupMembershipManagement/Repositories.BlobStorage/Repositories.BlobStorage.csproj +++ b/Service/GroupMembershipManagement/Repositories.BlobStorage/Repositories.BlobStorage.csproj @@ -5,7 +5,7 @@ - + diff --git a/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj b/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj index 4c17e75f2..83cd9d1b3 100644 --- a/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj +++ b/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj @@ -5,13 +5,13 @@ - + - + diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Repositories.EntityFramework.Contexts.csproj b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Repositories.EntityFramework.Contexts.csproj index d0cb2d87e..92db6dbf9 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Repositories.EntityFramework.Contexts.csproj +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Repositories.EntityFramework.Contexts.csproj @@ -7,8 +7,8 @@ - - + + all diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework/Repositories.EntityFramework.csproj b/Service/GroupMembershipManagement/Repositories.EntityFramework/Repositories.EntityFramework.csproj index 73ef5ba91..db896ba05 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework/Repositories.EntityFramework.csproj +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework/Repositories.EntityFramework.csproj @@ -7,7 +7,7 @@ - + diff --git a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/Repositories.SqlMembershipRepository.csproj b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/Repositories.SqlMembershipRepository.csproj index 34eed91d6..1a4e3e473 100644 --- a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/Repositories.SqlMembershipRepository.csproj +++ b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/Repositories.SqlMembershipRepository.csproj @@ -7,8 +7,8 @@ - - + + From 31b5a8c6bb1d928e0b25cde83eb77ab90a7661ee Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 23 Jul 2024 14:39:10 -0700 Subject: [PATCH 0139/1479] Add operation page --- .../components/AppHeader/AppHeader.base.tsx | 2 +- .../components/Operation/Operation.base.tsx | 29 +++++++++++++++ .../components/Operation/Operation.styles.ts | 37 +++++++++++++++++++ .../src/components/Operation/Operation.ts | 21 +++++++++++ .../components/Operation/Operation.types.ts | 22 +++++++++++ UI/web-app/src/components/Operation/index.ts | 2 + UI/web-app/src/index.tsx | 2 +- .../pages/AdminConfig/AdminConfig.types.ts | 5 +++ .../pages/AdminConfig/AdminConfig.view.tsx | 30 ++++++++++++++- .../src/services/localization/IStrings.ts | 7 ++++ .../i18n/locales/en/translations.ts | 9 ++++- .../i18n/locales/es/translations.ts | 9 ++++- 12 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 UI/web-app/src/components/Operation/Operation.base.tsx create mode 100644 UI/web-app/src/components/Operation/Operation.styles.ts create mode 100644 UI/web-app/src/components/Operation/Operation.ts create mode 100644 UI/web-app/src/components/Operation/Operation.types.ts create mode 100644 UI/web-app/src/components/Operation/index.ts diff --git a/UI/web-app/src/components/AppHeader/AppHeader.base.tsx b/UI/web-app/src/components/AppHeader/AppHeader.base.tsx index 7b749a30b..9ab88c861 100644 --- a/UI/web-app/src/components/AppHeader/AppHeader.base.tsx +++ b/UI/web-app/src/components/AppHeader/AppHeader.base.tsx @@ -51,7 +51,7 @@ export const AppHeaderBase: React.FunctionComponent = ( const navigate = useNavigate(); const onSettingsButtonClicked = (): void => { - navigate('/AdminConfig', { replace: false, state: { item: 1 } }); + navigate('/Admin', { replace: false, state: { item: 1 } }); }; const onLogoClicked = () => { diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx new file mode 100644 index 000000000..b0fe65569 --- /dev/null +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React from 'react'; +import { classNamesFunction, type IProcessedStyleSet } from '@fluentui/react'; +import { useTheme } from '@fluentui/react/lib/Theme'; +import type { + OperationProps, + OperationStyleProps, + OperationStyles, +} from './Operation.types'; +import { useStrings } from '../../store/hooks'; + +export const getClassNames = classNamesFunction(); + +export const OperationBase: React.FunctionComponent = (props: OperationProps) => { + const { title, className, styles } = props; + const classNames: IProcessedStyleSet = getClassNames(styles, { + className, + theme: useTheme(), + }); + const strings = useStrings(); + + return ( +
+
{title}
+
+ ); +}; \ No newline at end of file diff --git a/UI/web-app/src/components/Operation/Operation.styles.ts b/UI/web-app/src/components/Operation/Operation.styles.ts new file mode 100644 index 000000000..ad5441fdb --- /dev/null +++ b/UI/web-app/src/components/Operation/Operation.styles.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import type { OperationStyleProps, OperationStyles } from './Operation.types'; + +export const getStyles = (props: OperationStyleProps): OperationStyles => { + const { className, theme } = props; + + return { + root: [ + { + padding: '0px 14px', + display: 'flex', + alignItems: 'flex-start', + gap: '15px', + }, + className, + ], + card: { + borderRadius: 10, + marginBottom: 10, + backgroundColor: theme.palette.white, + margin: 10, + outline: `1px solid ${theme.palette.neutralQuaternary}`, + width: '649px', + padding: 24, + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '4px', + }, + title: { + fontWeight: 600, + fontSize: 16, + }, + }; +}; \ No newline at end of file diff --git a/UI/web-app/src/components/Operation/Operation.ts b/UI/web-app/src/components/Operation/Operation.ts new file mode 100644 index 000000000..898343d34 --- /dev/null +++ b/UI/web-app/src/components/Operation/Operation.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { styled } from '@fluentui/react'; +import type * as React from 'react'; + +import { OperationBase } from './Operation.base'; +import { getStyles } from './Operation.styles'; +import { + type OperationProps, + type OperationStyleProps, + type OperationStyles, +} from './Operation.types'; + +export const Operation: React.FunctionComponent = styled< + OperationProps, + OperationStyleProps, + OperationStyles +>(OperationBase, getStyles, undefined, { + scope: 'Operation', +}); \ No newline at end of file diff --git a/UI/web-app/src/components/Operation/Operation.types.ts b/UI/web-app/src/components/Operation/Operation.types.ts new file mode 100644 index 000000000..53497f3d3 --- /dev/null +++ b/UI/web-app/src/components/Operation/Operation.types.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { type IStyle, type IStyleFunctionOrObject, type ITheme } from '@fluentui/react'; +import type React from 'react'; + +export type OperationStyles = { + root: IStyle; + card: IStyle; + title: IStyle; +}; + +export type OperationStyleProps = { + className?: string; + theme: ITheme; +}; + +export type OperationProps = React.AllHTMLAttributes & { + className?: string; + styles?: IStyleFunctionOrObject; + title: string; +}; \ No newline at end of file diff --git a/UI/web-app/src/components/Operation/index.ts b/UI/web-app/src/components/Operation/index.ts new file mode 100644 index 000000000..f65350172 --- /dev/null +++ b/UI/web-app/src/components/Operation/index.ts @@ -0,0 +1,2 @@ +export * from './Operation'; +export * from './Operation.types'; \ No newline at end of file diff --git a/UI/web-app/src/index.tsx b/UI/web-app/src/index.tsx index ed2e2547e..37ac18953 100644 --- a/UI/web-app/src/index.tsx +++ b/UI/web-app/src/index.tsx @@ -53,7 +53,7 @@ ReactDOM.render( } /> } /> } /> - } /> + } /> } /> } /> diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts b/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts index 89e18124a..6c03804d7 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts @@ -59,6 +59,11 @@ export type HyperlinkSettingsProps = { setHasValidationErrors: React.Dispatch>; }; +export type OperationsProps = { + classNames: IProcessedStyleSet; + strings: IStrings['AdminConfig']; +}; + export type CustomSourceSettingsProps = { classNames: IProcessedStyleSet; sqlMembershipSource: SqlMembershipSource | undefined; diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx index 23339ae78..1892053f3 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx @@ -4,9 +4,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { classNamesFunction, IProcessedStyleSet, Pivot, PivotItem, PrimaryButton, TextField, Text, IColumn, SelectionMode, ShimmeredDetailsList } from '@fluentui/react'; import { useTheme } from '@fluentui/react/lib/Theme'; -import { AdminConfigStyleProps, AdminConfigStyles, AdminConfigViewProps, CustomLabelCellProps, CustomSourceSettingsProps, HyperlinkSettingsProps } from './AdminConfig.types'; +import { AdminConfigStyleProps, AdminConfigStyles, AdminConfigViewProps, CustomLabelCellProps, CustomSourceSettingsProps, HyperlinkSettingsProps, OperationsProps } from './AdminConfig.types'; import { PageSection } from '../../components/PageSection'; import { HyperlinkSetting } from '../../components/HyperlinkSetting'; +import { Operation } from '../../components/Operation'; import { Page } from '../../components/Page'; import { PageHeader } from '../../components/PageHeader'; import { SettingKey, SqlMembershipAttribute, SqlMembershipSource } from '../../models'; @@ -100,6 +101,19 @@ export const AdminConfigView: React.FunctionComponent = (p strings={strings} /> } + {true && + + + + } @@ -114,7 +128,19 @@ export const AdminConfigView: React.FunctionComponent = (p ); }; - +const Operations: React.FunctionComponent = (props: OperationsProps) => { + const { classNames, strings} = props; + return ( +
+
{strings.Operations.labels.description}
+
+ +
+
+ ); +} const HyperlinkSettings: React.FunctionComponent = (props: HyperlinkSettingsProps) => { const { classNames, strings, settings, setSettings, setHasValidationErrors } = props; diff --git a/UI/web-app/src/services/localization/IStrings.ts b/UI/web-app/src/services/localization/IStrings.ts index 8009499af..12857a917 100644 --- a/UI/web-app/src/services/localization/IStrings.ts +++ b/UI/web-app/src/services/localization/IStrings.ts @@ -99,6 +99,13 @@ export type IStrings = { description: string; }, }, + Operations: { + labels: { + operations: string; + description: string; + title: string; + }; + }; CustomSourceSettings: { labels: { customSource: string; diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index 832a6b219..b94d78cdd 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -80,7 +80,7 @@ export const strings: IStrings = { }, AdminConfig: { labels: { - pageTitle: "Admin Configuration", + pageTitle: "Admin Center", saveButton: "Save", saveSuccess: "Saved successfully.", }, @@ -102,6 +102,13 @@ export const strings: IStrings = { description: "This is the link that shows on the bottom left corner of the dashboard. It takes you to an internal site that has all the details on how XMM handles and stores user data.", }, }, + Operations: { + labels: { + operations: "Operations", + title: "Stop & Restart Buttons", + description: "Use the Stop button to halt GMM operations and the Restart button to reboot GMM at any time.", + } + }, CustomSourceSettings: { labels: { customSource: "Custom Source", diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index cfeeff48c..10f8c5350 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -82,7 +82,7 @@ export const strings: IStrings = { }, AdminConfig: { labels: { - pageTitle: "Configuración de Administrador", + pageTitle: "Centro de Administrador", saveButton: "Guardar", saveSuccess: "Guardado exitosamente." }, @@ -104,6 +104,13 @@ export const strings: IStrings = { description: "Esta es la liga que se muestra en la esquina inferior izquierda del dashboard. Te lleva a un sitio interno que tiene todos los detalles sobre cómo XMM maneja y almacena los datos de los usuarios.", }, }, + Operations: { + labels: { + operations: "Operaciones", + title: "Botones de Detener y Reiniciar", + description: "Utilice el botón Detener para detener las operaciones de GMM y el botón Reiniciar para reiniciar GMM en cualquier momento.", + } + }, CustomSourceSettings: { labels: { customSource: "Origen Personalizado", From eb4807051a5af4d5a512b208c0fe4da25aa30653 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 24 Jul 2024 14:09:28 -0700 Subject: [PATCH 0140/1479] Add operation button --- .../components/Operation/Operation.base.tsx | 41 +++++++++++++++---- .../components/Operation/Operation.styles.ts | 9 ++++ .../components/Operation/Operation.types.ts | 2 + 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index b0fe65569..1becdac85 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import React from 'react'; -import { classNamesFunction, type IProcessedStyleSet } from '@fluentui/react'; +import React, { useState } from 'react'; +import { classNamesFunction, type IProcessedStyleSet, DefaultButton, PrimaryButton } from '@fluentui/react'; import { useTheme } from '@fluentui/react/lib/Theme'; import type { OperationProps, @@ -21,9 +21,36 @@ export const OperationBase: React.FunctionComponent = (props: Op }); const strings = useStrings(); - return ( -
-
{title}
+ const [isStopped, setIsStopped] = useState(false); + const [isRestarting, setIsRestarting] = useState(false); + + const handleStop = () => { + setIsStopped(true); + setIsRestarting(false); + }; + + const handleRestart = () => { + setIsRestarting(true); + setIsStopped(false); + }; + +return ( +
+
{title}
+
+ +
- ); -}; \ No newline at end of file +
+); +}; diff --git a/UI/web-app/src/components/Operation/Operation.styles.ts b/UI/web-app/src/components/Operation/Operation.styles.ts index ad5441fdb..7173454fa 100644 --- a/UI/web-app/src/components/Operation/Operation.styles.ts +++ b/UI/web-app/src/components/Operation/Operation.styles.ts @@ -33,5 +33,14 @@ export const getStyles = (props: OperationStyleProps): OperationStyles => { fontWeight: 600, fontSize: 16, }, + buttonContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + button: { + margin: '10px 0', + width: '200px', + }, }; }; \ No newline at end of file diff --git a/UI/web-app/src/components/Operation/Operation.types.ts b/UI/web-app/src/components/Operation/Operation.types.ts index 53497f3d3..8043dfb05 100644 --- a/UI/web-app/src/components/Operation/Operation.types.ts +++ b/UI/web-app/src/components/Operation/Operation.types.ts @@ -8,6 +8,8 @@ export type OperationStyles = { root: IStyle; card: IStyle; title: IStyle; + buttonContainer: IStyle; + button: IStyle; }; export type OperationStyleProps = { From cc94f0e920a297a004004d6a1041231cbc9bbb75 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 29 Jul 2024 14:00:36 -0700 Subject: [PATCH 0141/1479] Change the description places --- UI/web-app/src/components/Operation/Operation.base.tsx | 4 +++- UI/web-app/src/components/Operation/Operation.styles.ts | 4 ++++ UI/web-app/src/components/Operation/Operation.types.ts | 2 ++ UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx | 4 +--- .../src/services/localization/i18n/locales/en/translations.ts | 4 ++-- .../src/services/localization/i18n/locales/es/translations.ts | 2 +- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index 1becdac85..7869a7bb9 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -14,7 +14,7 @@ import { useStrings } from '../../store/hooks'; export const getClassNames = classNamesFunction(); export const OperationBase: React.FunctionComponent = (props: OperationProps) => { - const { title, className, styles } = props; + const { title, description, className, styles } = props; const classNames: IProcessedStyleSet = getClassNames(styles, { className, theme: useTheme(), @@ -37,7 +37,9 @@ export const OperationBase: React.FunctionComponent = (props: Op return (
{title}
+
{description}
+ { margin: '10px 0', width: '200px', }, + description: { + fontSize: 14, + fontWeight: 400, + }, }; }; \ No newline at end of file diff --git a/UI/web-app/src/components/Operation/Operation.types.ts b/UI/web-app/src/components/Operation/Operation.types.ts index 8043dfb05..347bca60a 100644 --- a/UI/web-app/src/components/Operation/Operation.types.ts +++ b/UI/web-app/src/components/Operation/Operation.types.ts @@ -8,6 +8,7 @@ export type OperationStyles = { root: IStyle; card: IStyle; title: IStyle; + description: IStyle; buttonContainer: IStyle; button: IStyle; }; @@ -21,4 +22,5 @@ export type OperationProps = React.AllHTMLAttributes & { className?: string; styles?: IStyleFunctionOrObject; title: string; + description: string; }; \ No newline at end of file diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx index 1892053f3..a161e7a2b 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx @@ -132,13 +132,11 @@ const Operations: React.FunctionComponent = (props: OperationsP const { classNames, strings} = props; return (
-
{strings.Operations.labels.description}
-
-
); } const HyperlinkSettings: React.FunctionComponent = (props: HyperlinkSettingsProps) => { diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index b94d78cdd..8a53b1fae 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -105,8 +105,8 @@ export const strings: IStrings = { Operations: { labels: { operations: "Operations", - title: "Stop & Restart Buttons", - description: "Use the Stop button to halt GMM operations and the Restart button to reboot GMM at any time.", + title: "GMM Control Panel", + description: "Use the stop button to halt GMM operations and the restart button to reboot GMM at any time.", } }, CustomSourceSettings: { diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index 10f8c5350..8ae3d0c77 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -107,7 +107,7 @@ export const strings: IStrings = { Operations: { labels: { operations: "Operaciones", - title: "Botones de Detener y Reiniciar", + title: "Panel de Control GMM", description: "Utilice el botón Detener para detener las operaciones de GMM y el botón Reiniciar para reiniciar GMM en cualquier momento.", } }, From ebcc11db49830a29053c4e0c9531aa684a306b20 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 31 Jul 2024 10:35:18 -0700 Subject: [PATCH 0142/1479] Disable operation tab --- UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx index a161e7a2b..2148d6194 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx @@ -101,7 +101,7 @@ export const AdminConfigView: React.FunctionComponent = (p strings={strings} /> } - {true && + {false && Date: Wed, 31 Jul 2024 11:21:34 -0700 Subject: [PATCH 0143/1479] Change restart to reset --- .../src/components/Operation/Operation.base.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index 7869a7bb9..be63fd904 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -22,15 +22,15 @@ export const OperationBase: React.FunctionComponent = (props: Op const strings = useStrings(); const [isStopped, setIsStopped] = useState(false); - const [isRestarting, setIsRestarting] = useState(false); + const [isResetting, setIsResetting] = useState(false); const handleStop = () => { setIsStopped(true); - setIsRestarting(false); + setIsResetting(false); }; - const handleRestart = () => { - setIsRestarting(true); + const handleReset = () => { + setIsResetting(true); setIsStopped(false); }; @@ -41,15 +41,15 @@ return (
From 2d1e6de7e53d449623a6c718dfc9a8a217e6233d Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 1 Aug 2024 15:47:11 -0700 Subject: [PATCH 0144/1479] Change description --- .../src/services/localization/i18n/locales/en/translations.ts | 2 +- .../src/services/localization/i18n/locales/es/translations.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index 8a53b1fae..be0c89054 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -106,7 +106,7 @@ export const strings: IStrings = { labels: { operations: "Operations", title: "GMM Control Panel", - description: "Use the stop button to halt GMM operations and the restart button to reboot GMM at any time.", + description: "Use the stop button to halt GMM operations and the reset button to reboot GMM at any time.", } }, CustomSourceSettings: { diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index 8ae3d0c77..2f81a41cd 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -108,7 +108,7 @@ export const strings: IStrings = { labels: { operations: "Operaciones", title: "Panel de Control GMM", - description: "Utilice el botón Detener para detener las operaciones de GMM y el botón Reiniciar para reiniciar GMM en cualquier momento.", + description: "Utilice el botón Detener para detener las operaciones de GMM y el botón Restablecer para restablecer GMM en cualquier momento." } }, CustomSourceSettings: { From d735682957568513731332c7bcf0883a96d86dbe Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 24 Jul 2024 14:33:43 -0700 Subject: [PATCH 0145/1479] Add Role for operation tab --- .../Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs | 1 + .../Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs | 2 ++ .../WebApi/WebApi/Controllers/v1/Roles/RolesController.cs | 2 ++ UI/web-app/src/apis/roles/IRolesApi.ts | 1 + UI/web-app/src/components/AppHeader/AppHeader.base.tsx | 5 +++-- UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx | 4 +++- UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts | 1 + UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx | 4 ++-- UI/web-app/src/store/roles.slice.tsx | 3 +++ 9 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs index 76ca210fa..2f26d7916 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs @@ -19,5 +19,6 @@ public RolesObject() public bool IsSubmissionReviewer { get; set; } public bool IsHyperlinkAdministrator { get; set; } public bool IsCustomMembershipProviderAdministrator { get; set; } + public bool IsOperationsResetAdministrator { get; set; } } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs index f7d02483a..8382c1db2 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs @@ -36,6 +36,7 @@ public void GetAllRolesStatus_ForVariousUsers() new Claim(ClaimTypes.Role, Roles.SUBMISSION_REVIEWER), new Claim(ClaimTypes.Role, Roles.HYPERLINK_ADMINISTRATOR), new Claim(ClaimTypes.Role, Roles.CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR), + new Claim(ClaimTypes.Role, Roles.RESET_ADMINISTRATOR), }; _rolesController.ControllerContext = CreateControllerContext(claims); @@ -56,6 +57,7 @@ public void GetAllRolesStatus_ForVariousUsers() Assert.IsTrue(rolesStatuses.IsSubmissionReviewer); Assert.IsTrue(rolesStatuses.IsHyperlinkAdministrator); Assert.IsTrue(rolesStatuses.IsCustomMembershipProviderAdministrator); + Assert.IsTrue(rolesStatuses.IsOperationsResetAdministrator); } private ControllerContext CreateControllerContext(List claims) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs index 47280f8f4..407d94d0c 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs @@ -38,6 +38,7 @@ public RolesController() var isSubmissionReviewer = User.IsInRole(Models.Roles.SUBMISSION_REVIEWER); var isHyperlinkAdministrator = User.IsInRole(Models.Roles.HYPERLINK_ADMINISTRATOR); var isCustomMembershipProviderAdministrator = User.IsInRole(Models.Roles.CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR); + var isOperationsResetAdministrator = User.IsInRole(Models.Roles.RESET_ADMINISTRATOR); var roleStatus = new Models.DTOs.RolesObject { @@ -50,6 +51,7 @@ public RolesController() IsSubmissionReviewer = isSubmissionReviewer, IsHyperlinkAdministrator = isHyperlinkAdministrator, IsCustomMembershipProviderAdministrator = isCustomMembershipProviderAdministrator + IsOperationsResetAdministrator = isOperationsResetAdministrator }; return Ok(roleStatus); diff --git a/UI/web-app/src/apis/roles/IRolesApi.ts b/UI/web-app/src/apis/roles/IRolesApi.ts index 6f9a9863c..38f52ce78 100644 --- a/UI/web-app/src/apis/roles/IRolesApi.ts +++ b/UI/web-app/src/apis/roles/IRolesApi.ts @@ -11,6 +11,7 @@ export interface Roles { isJobTenantWriter(): boolean; isHyperlinkAdministrator(): boolean; isCustomMembershipProviderAdministrator(): boolean; + isOperationsResetAdministrator(): boolean; } diff --git a/UI/web-app/src/components/AppHeader/AppHeader.base.tsx b/UI/web-app/src/components/AppHeader/AppHeader.base.tsx index 9ab88c861..8aed9763c 100644 --- a/UI/web-app/src/components/AppHeader/AppHeader.base.tsx +++ b/UI/web-app/src/components/AppHeader/AppHeader.base.tsx @@ -15,7 +15,7 @@ import { selectProfilePhoto } from '../../store/profile.slice'; import { getProfilePhoto } from '../../store/profile.api'; import logo from '../../logo.svg'; import { useStrings } from '../../store/hooks'; -import { selectIsCustomMembershipProviderAdministrator, selectIsHyperlinkAdministrator } from '../../store/roles.slice'; +import { selectIsCustomMembershipProviderAdministrator, selectIsHyperlinkAdministrator, selectIsOperationsResetAdministrator} from '../../store/roles.slice'; const getClassNames = classNamesFunction< IAppHeaderStyleProps, @@ -40,7 +40,8 @@ export const AppHeaderBase: React.FunctionComponent = ( const profilePhoto = useSelector(selectProfilePhoto); const isHyperlinkAdmin = useSelector(selectIsHyperlinkAdministrator); const isCustomMembershipProviderAdmin = useSelector(selectIsCustomMembershipProviderAdministrator); - const canViewSettings = isHyperlinkAdmin || isCustomMembershipProviderAdmin; + const isOperationsResetAdministrator = useSelector(selectIsOperationsResetAdministrator); + const canViewSettings = isHyperlinkAdmin || isCustomMembershipProviderAdmin || isOperationsResetAdministrator; useEffect(() => { if (!profilePhoto) { diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx index 5c52f08de..648e1d132 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx @@ -19,7 +19,7 @@ import { setPagingBarVisible } from '../../store/pagingBar.slice'; import { selectSource, selectAttributes, selectIsSourceSaving, selectAreAttributesSaving, setSource, setAttributes } from '../../store/sqlMembershipSources.slice'; import { SqlMembershipAttribute, SqlMembershipSource } from '../../models'; import { patchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceCustomLabel } from '../../store/sqlMembershipSources.api'; -import { selectIsCustomMembershipProviderAdministrator, selectIsHyperlinkAdministrator } from '../../store/roles.slice'; +import { selectIsCustomMembershipProviderAdministrator, selectIsHyperlinkAdministrator, selectIsOperationsResetAdministrator} from '../../store/roles.slice'; export const AdminConfigBase: React.FunctionComponent = (props: AdminConfigProps) => { @@ -40,6 +40,7 @@ export const AdminConfigBase: React.FunctionComponent = (props const areSettingsSaving = useSelector(selectIsSaving); const isHyperlinkAdmin = useSelector(selectIsHyperlinkAdministrator); const isCustomMembershipProviderAdmin = useSelector(selectIsCustomMembershipProviderAdministrator); + const isOperationsResetAdministrator = useSelector(selectIsOperationsResetAdministrator); const strings = useStrings().AdminConfig; const generateSettings = () => ({ @@ -114,6 +115,7 @@ export const AdminConfigBase: React.FunctionComponent = (props sqlMembershipSourceAttributes={sqlMembershipSourceAttributes} isHyperlinkAdmin={isHyperlinkAdmin} isCustomMembershipProviderAdmin={isCustomMembershipProviderAdmin} + isOperationsResetAdministrator={isOperationsResetAdministrator} /> ); }; diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts b/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts index 6c03804d7..26012031f 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts @@ -49,6 +49,7 @@ export type AdminConfigViewProps = AdminConfigProps & { strings: IStrings['AdminConfig']; isHyperlinkAdmin: boolean; isCustomMembershipProviderAdmin: boolean; + isOperationsResetAdministrator: boolean; }; export type HyperlinkSettingsProps = { diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx index 2148d6194..faecf93b3 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx @@ -16,7 +16,7 @@ const getClassNames = classNamesFunction = (props: AdminConfigViewProps) => { // extract props - const { className, isSaving, onSave, settings, sqlMembershipSource, sqlMembershipSourceAttributes, strings, styles, isHyperlinkAdmin, isCustomMembershipProviderAdmin } = props; + const { className, isSaving, onSave, settings, sqlMembershipSource, sqlMembershipSourceAttributes, strings, styles, isHyperlinkAdmin, isCustomMembershipProviderAdmin, isOperationsResetAdministrator } = props; // generate class names const classNames: IProcessedStyleSet = getClassNames(styles, { @@ -101,7 +101,7 @@ export const AdminConfigView: React.FunctionComponent = (p strings={strings} />
} - {false && + {!isOperationsResetAdministrator && state.roles.isJobTe export const selectIsSubmissionReviewer = (state: RootState) => state.roles.isSubmissionReviewer; export const selectIsHyperlinkAdministrator = (state: RootState) => state.roles.isHyperlinkAdministrator; export const selectIsCustomMembershipProviderAdministrator = (state: RootState) => state.roles.isCustomMembershipProviderAdministrator; +export const selectIsOperationsResetAdministrator = (state: RootState) => state.roles.isOperationsResetAdministrator; export const selectHasAccess = (state: RootState) => { return state.roles.isJobOwnerReader || state.roles.isJobOwnerWriter || state.roles.isJobTenantReader || state.roles.isJobTenantWriter; From c2ac4526d4047b910855188a32107f3e0dd7cfa6 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 31 Jul 2024 13:42:05 -0700 Subject: [PATCH 0146/1479] Adjust Role name --- .../Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs index 407d94d0c..63f0691c1 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs @@ -50,7 +50,7 @@ public RolesController() IsJobTenantWriter = isJobTenantWriter, IsSubmissionReviewer = isSubmissionReviewer, IsHyperlinkAdministrator = isHyperlinkAdministrator, - IsCustomMembershipProviderAdministrator = isCustomMembershipProviderAdministrator + IsCustomMembershipProviderAdministrator = isCustomMembershipProviderAdministrator, IsOperationsResetAdministrator = isOperationsResetAdministrator }; From 71ecfd24ae715bd4dd365a66ebb64b568347354b Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 29 Jul 2024 14:59:44 -0700 Subject: [PATCH 0147/1479] Add webapi for the buttons --- UI/web-app/src/apis/GMMApi.ts | 9 ++- UI/web-app/src/apis/IGMMApi.ts | 2 + .../src/apis/operations/IOperationsApi.ts | 10 +++ .../src/apis/operations/OperationsApi.ts | 26 +++++++ UI/web-app/src/apis/operations/index.ts | 5 ++ .../components/Operation/Operation.base.tsx | 76 +++++++++++-------- .../components/Operation/Operation.types.ts | 1 + UI/web-app/src/models/OperationStatus.ts | 8 ++ UI/web-app/src/models/index.ts | 3 +- UI/web-app/src/store/operations.api.tsx | 43 +++++++++++ UI/web-app/src/store/operations.slice.tsx | 75 ++++++++++++++++++ UI/web-app/src/store/store.ts | 2 + 12 files changed, 226 insertions(+), 34 deletions(-) create mode 100644 UI/web-app/src/apis/operations/IOperationsApi.ts create mode 100644 UI/web-app/src/apis/operations/OperationsApi.ts create mode 100644 UI/web-app/src/apis/operations/index.ts create mode 100644 UI/web-app/src/models/OperationStatus.ts create mode 100644 UI/web-app/src/store/operations.api.tsx create mode 100644 UI/web-app/src/store/operations.slice.tsx diff --git a/UI/web-app/src/apis/GMMApi.ts b/UI/web-app/src/apis/GMMApi.ts index ad522b567..9a5d5c39e 100644 --- a/UI/web-app/src/apis/GMMApi.ts +++ b/UI/web-app/src/apis/GMMApi.ts @@ -8,20 +8,22 @@ import { IRolesApi, RolesApi } from './roles'; import { ISettingsApi } from './settings/ISettingsApi'; import { SettingsApi } from './settings/SettingsApi'; import { ISqlMembershipSourcesApi, SqlMembershipSourcesApi } from './sqlMembershipSources'; +import { IOperationsApi, OperationsApi} from './operations'; export class GMMApi implements IGMMApi { private _jobsApi: IJobsApi; private _settingsApi: ISettingsApi; private _rolesApi: IRolesApi; private _sqlMembershipSourcesApi: ISqlMembershipSourcesApi; - + private _operationsApi: IOperationsApi; + constructor(options: ApiOptions) { const { baseUrl } = options; this._jobsApi = new JobsApi({ ...options, baseUrl: `${baseUrl}/jobs` }); this._settingsApi = new SettingsApi({ ...options, baseUrl: `${baseUrl}/settings` }); this._rolesApi = new RolesApi({ ...options, baseUrl: `${baseUrl}/roles` }); this._sqlMembershipSourcesApi = new SqlMembershipSourcesApi({ ...options, baseUrl: `${baseUrl}/sqlMembershipSources` }); - + this._operationsApi = new OperationsApi({ ...options, baseUrl: `${baseUrl}/operations` }); } public get jobs(): IJobsApi { @@ -36,4 +38,7 @@ export class GMMApi implements IGMMApi { public get sqlMembershipSources(): ISqlMembershipSourcesApi { return this._sqlMembershipSourcesApi; } + public get operationsApi(): IOperationsApi { + return this._operationsApi; + } } diff --git a/UI/web-app/src/apis/IGMMApi.ts b/UI/web-app/src/apis/IGMMApi.ts index 5404aafb9..81ff8880d 100644 --- a/UI/web-app/src/apis/IGMMApi.ts +++ b/UI/web-app/src/apis/IGMMApi.ts @@ -5,10 +5,12 @@ import { IJobsApi } from './jobs'; import { IRolesApi } from './roles'; import { ISettingsApi } from './settings'; import { ISqlMembershipSourcesApi } from './sqlMembershipSources'; +import { IOperationsApi } from './operations'; export interface IGMMApi { settings: ISettingsApi; jobs: IJobsApi; roles: IRolesApi; sqlMembershipSources: ISqlMembershipSourcesApi; + operationsApi: IOperationsApi; } diff --git a/UI/web-app/src/apis/operations/IOperationsApi.ts b/UI/web-app/src/apis/operations/IOperationsApi.ts new file mode 100644 index 000000000..ccac1dfd4 --- /dev/null +++ b/UI/web-app/src/apis/operations/IOperationsApi.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { OperationStatus } from '../../models'; + +export interface IOperationsApi { + fetchOperationStatus(): Promise; + stopOperation(): Promise; + resetOperation(): Promise; +} diff --git a/UI/web-app/src/apis/operations/OperationsApi.ts b/UI/web-app/src/apis/operations/OperationsApi.ts new file mode 100644 index 000000000..345c310fc --- /dev/null +++ b/UI/web-app/src/apis/operations/OperationsApi.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { OperationStatus } from '../../models/OperationStatus'; +import { ApiBase } from '../ApiBase'; +import { IOperationsApi } from './IOperationsApi'; + + +export class OperationsApi extends ApiBase implements IOperationsApi { + public async fetchOperationStatus(): Promise { + const response = await this.httpClient.get('/'); + this.ensureSuccessStatusCode(response); + return response.data; + } + public async stopOperation(): Promise { + const response = await this.httpClient.post('/stop', {}); + this.ensureSuccessStatusCode(response); + return response.data; + } + public async resetOperation(): Promise { + const response = await this.httpClient.post('/reset', {}); + this.ensureSuccessStatusCode(response); + return response.data; + } + +} diff --git a/UI/web-app/src/apis/operations/index.ts b/UI/web-app/src/apis/operations/index.ts new file mode 100644 index 000000000..d9fa61d63 --- /dev/null +++ b/UI/web-app/src/apis/operations/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export * from './IOperationsApi'; +export * from './OperationsApi'; \ No newline at end of file diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index be63fd904..22e659af7 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import React, { useState } from 'react'; -import { classNamesFunction, type IProcessedStyleSet, DefaultButton, PrimaryButton } from '@fluentui/react'; +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { classNamesFunction, type IProcessedStyleSet, DefaultButton } from '@fluentui/react'; import { useTheme } from '@fluentui/react/lib/Theme'; import type { OperationProps, @@ -10,6 +11,10 @@ import type { OperationStyles, } from './Operation.types'; import { useStrings } from '../../store/hooks'; +import { OperationStatus } from '../../models/OperationStatus'; +import { fetchOperationStatus,stopOperation, resetOperation } from '../../store/operations.api'; +import { resetError, selectOperationStatus, selectOperationIsLoading, selectOperationError } from '../../store/operations.slice'; +import { AppDispatch } from '../../store'; export const getClassNames = classNamesFunction(); @@ -19,40 +24,49 @@ export const OperationBase: React.FunctionComponent = (props: Op className, theme: useTheme(), }); + + const dispatch: AppDispatch = useDispatch(); + const status = useSelector(selectOperationStatus); + const isLoading = useSelector(selectOperationIsLoading); + const error = useSelector(selectOperationError); const strings = useStrings(); - const [isStopped, setIsStopped] = useState(false); - const [isResetting, setIsResetting] = useState(false); + useEffect(() => { + dispatch(fetchOperationStatus()); + }, [dispatch]); - const handleStop = () => { - setIsStopped(true); - setIsResetting(false); + const handleStop = async () => { + dispatch(stopOperation()); }; - const handleReset = () => { - setIsResetting(true); - setIsStopped(false); + const handleReset = async () => { + dispatch(resetOperation()); }; -return ( -
-
{title}
-
{description}
-
- - - + useEffect(() => { + if (error) { + console.error('Operation error:', error); + dispatch(resetError()); + } + }, [error, dispatch]); + + return ( +
+
{title}
+
+ + +
-
-); -}; + ); +}; \ No newline at end of file diff --git a/UI/web-app/src/components/Operation/Operation.types.ts b/UI/web-app/src/components/Operation/Operation.types.ts index 347bca60a..e1a7d45b0 100644 --- a/UI/web-app/src/components/Operation/Operation.types.ts +++ b/UI/web-app/src/components/Operation/Operation.types.ts @@ -3,6 +3,7 @@ import { type IStyle, type IStyleFunctionOrObject, type ITheme } from '@fluentui/react'; import type React from 'react'; +import { IOperationsApi } from '../../apis/operations/IOperationsApi'; export type OperationStyles = { root: IStyle; diff --git a/UI/web-app/src/models/OperationStatus.ts b/UI/web-app/src/models/OperationStatus.ts new file mode 100644 index 000000000..f6689374c --- /dev/null +++ b/UI/web-app/src/models/OperationStatus.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export const enum OperationStatus { + Running = 0, + Stopped = 1, + Stopping = 2, +} diff --git a/UI/web-app/src/models/index.ts b/UI/web-app/src/models/index.ts index cb1393e2e..104175cb5 100644 --- a/UI/web-app/src/models/index.ts +++ b/UI/web-app/src/models/index.ts @@ -26,4 +26,5 @@ export * from './ISourcePart'; export * from './HRSourcePart'; export * from './GroupMembershipSourcePart'; export * from './GroupOwnershipSourcePart'; -export * from './PlaceMembershipSourcePart'; \ No newline at end of file +export * from './PlaceMembershipSourcePart'; +export * from './OperationStatus'; \ No newline at end of file diff --git a/UI/web-app/src/store/operations.api.tsx b/UI/web-app/src/store/operations.api.tsx new file mode 100644 index 000000000..b70672b1c --- /dev/null +++ b/UI/web-app/src/store/operations.api.tsx @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { OperationStatus } from '../models/OperationStatus'; +import { ThunkConfig } from './store'; + +export const fetchOperationStatus = createAsyncThunk( + 'operations/fetchOperationStatus', + async (_, { extra }) => { + const { gmmApi } = extra.apis; + + try { + return await gmmApi.operationsApi.fetchOperationStatus(); + } catch (error) { + throw new Error('Failed to fetch settings data!'); + } + } +); + +export const stopOperation = createAsyncThunk( + 'operations/stopOperation', + async (_, { extra }) => { + const { gmmApi } = extra.apis; + try { + await gmmApi.operationsApi.stopOperation(); + } catch (error) { + throw new Error('Failed to stop the operation!'); + } + } + ); + + export const resetOperation = createAsyncThunk( + 'operations/resetOperation', + async (_, { extra }) => { + const { gmmApi } = extra.apis; + try { + await gmmApi.operationsApi.resetOperation(); + } catch (error) { + throw new Error('Failed to reset the operation!'); + } + } + ); \ No newline at end of file diff --git a/UI/web-app/src/store/operations.slice.tsx b/UI/web-app/src/store/operations.slice.tsx new file mode 100644 index 000000000..303de684a --- /dev/null +++ b/UI/web-app/src/store/operations.slice.tsx @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { OperationStatus } from '../models/OperationStatus'; +import { fetchOperationStatus, stopOperation, restartOperation } from './operations.api'; +import type { RootState } from './store'; + +interface OperationsState { + status: OperationStatus | null; + isLoading: boolean; + error: string | null; +} + +const initialState: OperationsState = { + status: null, + isLoading: false, + error: null, +}; + +const operationsSlice = createSlice({ + name: 'operations', + initialState, + reducers: { + resetError(state) { + state.error = null; + } + }, + extraReducers: (builder) => { + builder + .addCase(fetchOperationStatus.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchOperationStatus.fulfilled, (state, action: PayloadAction) => { + state.status = action.payload; + state.isLoading = false; + }) + .addCase(fetchOperationStatus.rejected, (state, action) => { + state.isLoading = false; + state.error = 'Failed to fetch operation status.'; + }) + .addCase(stopOperation.pending, (state) => { + state.isLoading = true; + state.status = OperationStatus.Stopping; + }) + .addCase(stopOperation.fulfilled, (state) => { + state.isLoading = false; + state.status = OperationStatus.Stopped; + }) + .addCase(stopOperation.rejected, (state, action) => { + state.isLoading = false; + state.error = 'Failed to stop the operation.'; + }) + .addCase(restartOperation.pending, (state) => { + state.isLoading = true; + }) + .addCase(restartOperation.fulfilled, (state) => { + state.isLoading = false; + state.status = OperationStatus.Running; + }) + .addCase(restartOperation.rejected, (state, action) => { + state.isLoading = false; + state.error = 'Failed to restart the operation.'; + }); + } +}); + +export const { resetError } = operationsSlice.actions; + +export const selectOperationStatus = (state: RootState) => state.operations.status; +export const selectOperationIsLoading = (state: RootState) => state.operations.isLoading; +export const selectOperationError = (state: RootState) => state.operations.error; + +export default operationsSlice.reducer; \ No newline at end of file diff --git a/UI/web-app/src/store/store.ts b/UI/web-app/src/store/store.ts index 63393aa9f..3551e3ffe 100644 --- a/UI/web-app/src/store/store.ts +++ b/UI/web-app/src/store/store.ts @@ -15,6 +15,7 @@ import orgLeaderDetailsReducer from './orgLeaderDetails.slice'; import settingsReducer from './settings.slice'; import rolesReducer from './roles.slice'; import sqlMembershipSourcesReducer from './sqlMembershipSources.slice'; +import operationsReducer from './operations.slice'; import { Services } from '../services'; import { MsalAuthenticationService, TokenType } from '../services/auth'; @@ -56,6 +57,7 @@ const rootReducer = combineReducers({ settings: settingsReducer, roles: rolesReducer, sqlMembershipSources: sqlMembershipSourcesReducer, + operations: operationsReducer, }); export const store = configureStore({ From e06fd3e629a125086071eff5a7236339c911322f Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 29 Jul 2024 15:04:09 -0700 Subject: [PATCH 0148/1479] Fix rebase --- UI/web-app/src/components/Operation/Operation.base.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index 22e659af7..cfb7c7b8d 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -53,6 +53,7 @@ export const OperationBase: React.FunctionComponent = (props: Op return (
{title}
+
{description}
Date: Wed, 31 Jul 2024 15:05:17 -0700 Subject: [PATCH 0149/1479] Add webapi status --- .../src/apis/operations/IOperationsApi.ts | 10 ++- .../src/apis/operations/OperationsApi.ts | 19 +++--- .../components/Operation/Operation.base.tsx | 46 +++++++++----- UI/web-app/src/models/OperationStatus.ts | 8 --- UI/web-app/src/models/Operations.ts | 8 +++ UI/web-app/src/models/ServiceStatuses.ts | 11 ++++ UI/web-app/src/models/index.ts | 3 +- UI/web-app/src/store/operations.api.tsx | 48 +++++++-------- UI/web-app/src/store/operations.slice.tsx | 61 +++++++++++-------- 9 files changed, 121 insertions(+), 93 deletions(-) delete mode 100644 UI/web-app/src/models/OperationStatus.ts create mode 100644 UI/web-app/src/models/Operations.ts create mode 100644 UI/web-app/src/models/ServiceStatuses.ts diff --git a/UI/web-app/src/apis/operations/IOperationsApi.ts b/UI/web-app/src/apis/operations/IOperationsApi.ts index ccac1dfd4..b0689aa24 100644 --- a/UI/web-app/src/apis/operations/IOperationsApi.ts +++ b/UI/web-app/src/apis/operations/IOperationsApi.ts @@ -1,10 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { OperationStatus } from '../../models'; - +import { ServiceStatuses, Operations } from '../../models'; export interface IOperationsApi { - fetchOperationStatus(): Promise; - stopOperation(): Promise; - resetOperation(): Promise; -} + fetchServiceStatus(): Promise; + processOperation(operation: Operations): Promise; +} \ No newline at end of file diff --git a/UI/web-app/src/apis/operations/OperationsApi.ts b/UI/web-app/src/apis/operations/OperationsApi.ts index 345c310fc..7c8f4c182 100644 --- a/UI/web-app/src/apis/operations/OperationsApi.ts +++ b/UI/web-app/src/apis/operations/OperationsApi.ts @@ -1,26 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { OperationStatus } from '../../models/OperationStatus'; +import { ServiceStatuses, Operations } from '../../models'; import { ApiBase } from '../ApiBase'; import { IOperationsApi } from './IOperationsApi'; export class OperationsApi extends ApiBase implements IOperationsApi { - public async fetchOperationStatus(): Promise { - const response = await this.httpClient.get('/'); + + public async processOperation(operation: Operations): Promise { + const response = await this.httpClient.post(`/operations/${operation}`, {}); this.ensureSuccessStatusCode(response); return response.data; } - public async stopOperation(): Promise { - const response = await this.httpClient.post('/stop', {}); - this.ensureSuccessStatusCode(response); - return response.data; - } - public async resetOperation(): Promise { - const response = await this.httpClient.post('/reset', {}); + + public async fetchServiceStatus(): Promise { + const response = await this.httpClient.get('/operations/servicestatus'); this.ensureSuccessStatusCode(response); - return response.data; + return response.data; } } diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index cfb7c7b8d..f6eed20ee 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import React, { useState, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { classNamesFunction, type IProcessedStyleSet, DefaultButton } from '@fluentui/react'; import { useTheme } from '@fluentui/react/lib/Theme'; @@ -11,10 +11,11 @@ import type { OperationStyles, } from './Operation.types'; import { useStrings } from '../../store/hooks'; -import { OperationStatus } from '../../models/OperationStatus'; -import { fetchOperationStatus,stopOperation, resetOperation } from '../../store/operations.api'; +import { ServiceStatuses } from '../../models/ServiceStatuses'; +import { fetchServiceStatus, processOperation } from '../../store/operations.api'; import { resetError, selectOperationStatus, selectOperationIsLoading, selectOperationError } from '../../store/operations.slice'; import { AppDispatch } from '../../store'; +import { Operations } from '../../models/Operations'; export const getClassNames = classNamesFunction(); @@ -32,15 +33,19 @@ export const OperationBase: React.FunctionComponent = (props: Op const strings = useStrings(); useEffect(() => { - dispatch(fetchOperationStatus()); + dispatch(fetchServiceStatus()); }, [dispatch]); const handleStop = async () => { - dispatch(stopOperation()); + dispatch(processOperation(Operations.Stop)); }; const handleReset = async () => { - dispatch(resetOperation()); + dispatch(processOperation(Operations.Reset)); + }; + + const handleStart = async () => { + dispatch(processOperation(Operations.Start)); }; useEffect(() => { @@ -50,24 +55,35 @@ export const OperationBase: React.FunctionComponent = (props: Op } }, [error, dispatch]); + const isButtonDisabled = isLoading || status === ServiceStatuses.Stopping || status === ServiceStatuses.Resetting || status === ServiceStatuses.Starting || !!error; + return (
{title}
{description}
+ {status === ServiceStatuses.Stopped ? ( + + ) : ( + + )} -
); -}; \ No newline at end of file +}; diff --git a/UI/web-app/src/models/OperationStatus.ts b/UI/web-app/src/models/OperationStatus.ts deleted file mode 100644 index f6689374c..000000000 --- a/UI/web-app/src/models/OperationStatus.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -export const enum OperationStatus { - Running = 0, - Stopped = 1, - Stopping = 2, -} diff --git a/UI/web-app/src/models/Operations.ts b/UI/web-app/src/models/Operations.ts new file mode 100644 index 000000000..d2f082e97 --- /dev/null +++ b/UI/web-app/src/models/Operations.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export enum Operations { + Reset, + Stop, + Start +} diff --git a/UI/web-app/src/models/ServiceStatuses.ts b/UI/web-app/src/models/ServiceStatuses.ts new file mode 100644 index 000000000..d3ea9cdc0 --- /dev/null +++ b/UI/web-app/src/models/ServiceStatuses.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export const enum ServiceStatuses { + Running, + Stopped, + Resetting, + Stopping, + Starting, + Error +} diff --git a/UI/web-app/src/models/index.ts b/UI/web-app/src/models/index.ts index 104175cb5..e438476aa 100644 --- a/UI/web-app/src/models/index.ts +++ b/UI/web-app/src/models/index.ts @@ -27,4 +27,5 @@ export * from './HRSourcePart'; export * from './GroupMembershipSourcePart'; export * from './GroupOwnershipSourcePart'; export * from './PlaceMembershipSourcePart'; -export * from './OperationStatus'; \ No newline at end of file +export * from './ServiceStatuses'; +export * from './Operations'; \ No newline at end of file diff --git a/UI/web-app/src/store/operations.api.tsx b/UI/web-app/src/store/operations.api.tsx index b70672b1c..b38f34ea1 100644 --- a/UI/web-app/src/store/operations.api.tsx +++ b/UI/web-app/src/store/operations.api.tsx @@ -2,42 +2,36 @@ // Licensed under the MIT license. import { createAsyncThunk } from '@reduxjs/toolkit'; -import { OperationStatus } from '../models/OperationStatus'; +import { ServiceStatuses } from '../models/ServiceStatuses'; import { ThunkConfig } from './store'; +import { Operations } from '../models/Operations'; -export const fetchOperationStatus = createAsyncThunk( - 'operations/fetchOperationStatus', +export const fetchServiceStatus = createAsyncThunk( + 'operations/fetchServiceStatus', async (_, { extra }) => { const { gmmApi } = extra.apis; try { - return await gmmApi.operationsApi.fetchOperationStatus(); + return await gmmApi.operationsApi.fetchServiceStatus(); } catch (error) { - throw new Error('Failed to fetch settings data!'); + throw new Error('Failed to fetch service status data!'); } } ); -export const stopOperation = createAsyncThunk( - 'operations/stopOperation', - async (_, { extra }) => { - const { gmmApi } = extra.apis; - try { - await gmmApi.operationsApi.stopOperation(); - } catch (error) { - throw new Error('Failed to stop the operation!'); - } - } - ); - - export const resetOperation = createAsyncThunk( - 'operations/resetOperation', - async (_, { extra }) => { - const { gmmApi } = extra.apis; - try { - await gmmApi.operationsApi.resetOperation(); - } catch (error) { - throw new Error('Failed to reset the operation!'); - } +export const processOperation = createAsyncThunk( + 'operations/processOperation', + async (operation, { extra }) => { + const { gmmApi } = extra.apis; + try { + await gmmApi.operationsApi.processOperation(operation); + } catch (error) { + throw new Error(`Failed to process the ${operation} operation!`); } - ); \ No newline at end of file + } +); + +// Usage examples: +export const resetOperation = () => processOperation(Operations.Reset); +export const stopOperation = () => processOperation(Operations.Stop); +export const startOperation = () => processOperation(Operations.Start); \ No newline at end of file diff --git a/UI/web-app/src/store/operations.slice.tsx b/UI/web-app/src/store/operations.slice.tsx index 303de684a..d9e7bf036 100644 --- a/UI/web-app/src/store/operations.slice.tsx +++ b/UI/web-app/src/store/operations.slice.tsx @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { OperationStatus } from '../models/OperationStatus'; -import { fetchOperationStatus, stopOperation, restartOperation } from './operations.api'; +import { ServiceStatuses } from '../models/ServiceStatuses'; +import { fetchServiceStatus, processOperation } from './operations.api'; +import { Operations } from '../models/Operations'; import type { RootState } from './store'; interface OperationsState { - status: OperationStatus | null; + status: ServiceStatuses | null; isLoading: boolean; error: string | null; } @@ -28,40 +29,50 @@ const operationsSlice = createSlice({ }, extraReducers: (builder) => { builder - .addCase(fetchOperationStatus.pending, (state) => { + .addCase(fetchServiceStatus.pending, (state) => { state.isLoading = true; state.error = null; }) - .addCase(fetchOperationStatus.fulfilled, (state, action: PayloadAction) => { + .addCase(fetchServiceStatus.fulfilled, (state, action: PayloadAction) => { state.status = action.payload; state.isLoading = false; }) - .addCase(fetchOperationStatus.rejected, (state, action) => { + .addCase(fetchServiceStatus.rejected, (state, action) => { state.isLoading = false; - state.error = 'Failed to fetch operation status.'; + state.error = 'Failed to fetch service status.'; }) - .addCase(stopOperation.pending, (state) => { - state.isLoading = true; - state.status = OperationStatus.Stopping; - }) - .addCase(stopOperation.fulfilled, (state) => { - state.isLoading = false; - state.status = OperationStatus.Stopped; - }) - .addCase(stopOperation.rejected, (state, action) => { - state.isLoading = false; - state.error = 'Failed to stop the operation.'; - }) - .addCase(restartOperation.pending, (state) => { + .addCase(processOperation.pending, (state, action) => { state.isLoading = true; + state.error = null; + switch (action.meta.arg) { + case Operations.Stop: + state.status = ServiceStatuses.Stopping; + break; + case Operations.Reset: + state.status = ServiceStatuses.Resetting; + break; + case Operations.Start: + state.status = ServiceStatuses.Starting; + break; + } }) - .addCase(restartOperation.fulfilled, (state) => { + .addCase(processOperation.fulfilled, (state, action) => { state.isLoading = false; - state.status = OperationStatus.Running; + switch (action.meta.arg) { + case Operations.Stop: + state.status = ServiceStatuses.Stopped; + break; + case Operations.Reset: + state.status = ServiceStatuses.Running; + break; + case Operations.Start: + state.status = ServiceStatuses.Running; + break; + } }) - .addCase(restartOperation.rejected, (state, action) => { + .addCase(processOperation.rejected, (state, action) => { state.isLoading = false; - state.error = 'Failed to restart the operation.'; + state.error = `Failed to process the ${action.meta.arg} operation.`; }); } }); @@ -72,4 +83,4 @@ export const selectOperationStatus = (state: RootState) => state.operations.stat export const selectOperationIsLoading = (state: RootState) => state.operations.isLoading; export const selectOperationError = (state: RootState) => state.operations.error; -export default operationsSlice.reducer; \ No newline at end of file +export default operationsSlice.reducer; From 619d6cbc598ec26c7102feb49e0866e5f5f2cae3 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 31 Jul 2024 15:34:17 -0700 Subject: [PATCH 0150/1479] Change pakcage --- .../GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj index 49b78dd23..4d5910f9c 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj @@ -38,7 +38,6 @@ - From 8e66a0267ad5810d1b93e263c300ae72a19b2544 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 1 Aug 2024 14:18:33 -0700 Subject: [PATCH 0151/1479] Connected with webapi --- UI/web-app/src/apis/operations/OperationsApi.ts | 10 +++++----- .../src/components/Operation/Operation.base.tsx | 8 ++++---- UI/web-app/src/models/GetServiceStatusResponse.ts | 11 +++++++++++ UI/web-app/src/models/index.ts | 3 ++- .../src/pages/AdminConfig/AdminConfig.view.tsx | 2 +- UI/web-app/src/store/operations.slice.tsx | 13 +------------ 6 files changed, 24 insertions(+), 23 deletions(-) create mode 100644 UI/web-app/src/models/GetServiceStatusResponse.ts diff --git a/UI/web-app/src/apis/operations/OperationsApi.ts b/UI/web-app/src/apis/operations/OperationsApi.ts index 7c8f4c182..f171edb36 100644 --- a/UI/web-app/src/apis/operations/OperationsApi.ts +++ b/UI/web-app/src/apis/operations/OperationsApi.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { ServiceStatuses, Operations } from '../../models'; +import { ServiceStatuses, Operations, GetServiceStatusResponse } from '../../models'; import { ApiBase } from '../ApiBase'; import { IOperationsApi } from './IOperationsApi'; @@ -9,15 +9,15 @@ import { IOperationsApi } from './IOperationsApi'; export class OperationsApi extends ApiBase implements IOperationsApi { public async processOperation(operation: Operations): Promise { - const response = await this.httpClient.post(`/operations/${operation}`, {}); + const response = await this.httpClient.post(`/${Operations[operation]}`, {}); this.ensureSuccessStatusCode(response); return response.data; } public async fetchServiceStatus(): Promise { - const response = await this.httpClient.get('/operations/servicestatus'); + const response = await this.httpClient.get('/servicestatus'); this.ensureSuccessStatusCode(response); - return response.data; + const serviceStatus = response.data.status; + return serviceStatus; } - } diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index f6eed20ee..26b35c32b 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -62,16 +62,16 @@ export const OperationBase: React.FunctionComponent = (props: Op
{title}
{description}
- {status === ServiceStatuses.Stopped ? ( + {status === ServiceStatuses.Stopped || status === ServiceStatuses.Starting ? ( ) : ( = (props: Op
diff --git a/UI/web-app/src/models/GetServiceStatusResponse.ts b/UI/web-app/src/models/GetServiceStatusResponse.ts new file mode 100644 index 000000000..e85ee2cb2 --- /dev/null +++ b/UI/web-app/src/models/GetServiceStatusResponse.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { ServiceStatuses } from "./ServiceStatuses"; + +export interface GetServiceStatusResponse { + statusCode: number; + errorCode: string | null; + status: ServiceStatuses; + } + \ No newline at end of file diff --git a/UI/web-app/src/models/index.ts b/UI/web-app/src/models/index.ts index e438476aa..2d7506efd 100644 --- a/UI/web-app/src/models/index.ts +++ b/UI/web-app/src/models/index.ts @@ -28,4 +28,5 @@ export * from './GroupMembershipSourcePart'; export * from './GroupOwnershipSourcePart'; export * from './PlaceMembershipSourcePart'; export * from './ServiceStatuses'; -export * from './Operations'; \ No newline at end of file +export * from './Operations'; +export * from './GetServiceStatusResponse'; \ No newline at end of file diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx index faecf93b3..0c4c1b4a8 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx @@ -101,7 +101,7 @@ export const AdminConfigView: React.FunctionComponent = (p strings={strings} /> } - {!isOperationsResetAdministrator && + {isOperationsResetAdministrator && { + .addCase(fetchServiceStatus.rejected, (state) => { state.isLoading = false; state.error = 'Failed to fetch service status.'; }) @@ -58,17 +58,6 @@ const operationsSlice = createSlice({ }) .addCase(processOperation.fulfilled, (state, action) => { state.isLoading = false; - switch (action.meta.arg) { - case Operations.Stop: - state.status = ServiceStatuses.Stopped; - break; - case Operations.Reset: - state.status = ServiceStatuses.Running; - break; - case Operations.Start: - state.status = ServiceStatuses.Running; - break; - } }) .addCase(processOperation.rejected, (state, action) => { state.isLoading = false; From af1135bde28c50f9d0da8b141d1ebb1a7ae3ea2d Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 1 Aug 2024 15:31:38 -0700 Subject: [PATCH 0152/1479] Control behavior --- .../components/Operation/Operation.base.tsx | 55 +++++++------------ UI/web-app/src/store/operations.slice.tsx | 24 ++++++-- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index 26b35c32b..73c59562d 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -1,51 +1,36 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { classNamesFunction, type IProcessedStyleSet, DefaultButton } from '@fluentui/react'; +import { classNamesFunction, DefaultButton } from '@fluentui/react'; import { useTheme } from '@fluentui/react/lib/Theme'; -import type { - OperationProps, - OperationStyleProps, - OperationStyles, -} from './Operation.types'; -import { useStrings } from '../../store/hooks'; -import { ServiceStatuses } from '../../models/ServiceStatuses'; import { fetchServiceStatus, processOperation } from '../../store/operations.api'; -import { resetError, selectOperationStatus, selectOperationIsLoading, selectOperationError } from '../../store/operations.slice'; +import { resetError, selectOperationDisplayStatus, selectOperationIsLoading, selectOperationError, selectOperationInProgress } from '../../store/operations.slice'; import { AppDispatch } from '../../store'; +import { ServiceStatuses } from '../../models/ServiceStatuses'; import { Operations } from '../../models/Operations'; +import type { OperationProps, OperationStyles, OperationStyleProps } from './Operation.types'; export const getClassNames = classNamesFunction(); export const OperationBase: React.FunctionComponent = (props: OperationProps) => { const { title, description, className, styles } = props; - const classNames: IProcessedStyleSet = getClassNames(styles, { + const classNames = getClassNames(styles, { className, theme: useTheme(), }); const dispatch: AppDispatch = useDispatch(); - const status = useSelector(selectOperationStatus); + const displayStatus = useSelector(selectOperationDisplayStatus); const isLoading = useSelector(selectOperationIsLoading); const error = useSelector(selectOperationError); - const strings = useStrings(); + const isOperationInProgress = useSelector(selectOperationInProgress); useEffect(() => { dispatch(fetchServiceStatus()); }, [dispatch]); - const handleStop = async () => { - dispatch(processOperation(Operations.Stop)); - }; - - const handleReset = async () => { - dispatch(processOperation(Operations.Reset)); - }; - - const handleStart = async () => { - dispatch(processOperation(Operations.Start)); + const handleOperation = (operation: Operations) => { + dispatch(processOperation(operation)) + .then(() => dispatch(fetchServiceStatus())); }; useEffect(() => { @@ -55,32 +40,32 @@ export const OperationBase: React.FunctionComponent = (props: Op } }, [error, dispatch]); - const isButtonDisabled = isLoading || status === ServiceStatuses.Stopping || status === ServiceStatuses.Resetting || status === ServiceStatuses.Starting || !!error; + const isButtonDisabled = isLoading || isOperationInProgress || !!error; return (
{title}
{description}
- {status === ServiceStatuses.Stopped || status === ServiceStatuses.Starting ? ( + {displayStatus === ServiceStatuses.Stopped || displayStatus === ServiceStatuses.Starting ? ( handleOperation(Operations.Start)} disabled={isButtonDisabled} className={classNames.button} /> ) : ( handleOperation(Operations.Stop)} + disabled={isButtonDisabled || displayStatus !== ServiceStatuses.Running} className={classNames.button} /> )} handleOperation(Operations.Reset)} + disabled={isButtonDisabled || displayStatus !== ServiceStatuses.Running} className={classNames.button} />
diff --git a/UI/web-app/src/store/operations.slice.tsx b/UI/web-app/src/store/operations.slice.tsx index 78763e295..5fb946cda 100644 --- a/UI/web-app/src/store/operations.slice.tsx +++ b/UI/web-app/src/store/operations.slice.tsx @@ -1,6 +1,3 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { ServiceStatuses } from '../models/ServiceStatuses'; import { fetchServiceStatus, processOperation } from './operations.api'; @@ -9,14 +6,18 @@ import type { RootState } from './store'; interface OperationsState { status: ServiceStatuses | null; + displayStatus: ServiceStatuses | null; isLoading: boolean; error: string | null; + isOperationInProgress: boolean; } const initialState: OperationsState = { status: null, + displayStatus: null, isLoading: false, error: null, + isOperationInProgress: false, }; const operationsSlice = createSlice({ @@ -25,7 +26,7 @@ const operationsSlice = createSlice({ reducers: { resetError(state) { state.error = null; - } + }, }, extraReducers: (builder) => { builder @@ -36,32 +37,43 @@ const operationsSlice = createSlice({ .addCase(fetchServiceStatus.fulfilled, (state, action: PayloadAction) => { state.status = action.payload; state.isLoading = false; + if (state.status === ServiceStatuses.Stopped && state.displayStatus === ServiceStatuses.Resetting) { + return; + } + state.displayStatus = action.payload; + state.isOperationInProgress = false; }) .addCase(fetchServiceStatus.rejected, (state) => { state.isLoading = false; state.error = 'Failed to fetch service status.'; + state.isOperationInProgress = false; }) .addCase(processOperation.pending, (state, action) => { state.isLoading = true; state.error = null; + state.isOperationInProgress = true; switch (action.meta.arg) { case Operations.Stop: state.status = ServiceStatuses.Stopping; + state.displayStatus = ServiceStatuses.Stopping; break; case Operations.Reset: state.status = ServiceStatuses.Resetting; + state.displayStatus = ServiceStatuses.Resetting; break; case Operations.Start: state.status = ServiceStatuses.Starting; + state.displayStatus = ServiceStatuses.Starting; break; } }) - .addCase(processOperation.fulfilled, (state, action) => { + .addCase(processOperation.fulfilled, (state) => { state.isLoading = false; }) .addCase(processOperation.rejected, (state, action) => { state.isLoading = false; state.error = `Failed to process the ${action.meta.arg} operation.`; + state.isOperationInProgress = false; }); } }); @@ -69,7 +81,9 @@ const operationsSlice = createSlice({ export const { resetError } = operationsSlice.actions; export const selectOperationStatus = (state: RootState) => state.operations.status; +export const selectOperationDisplayStatus = (state: RootState) => state.operations.displayStatus; export const selectOperationIsLoading = (state: RootState) => state.operations.isLoading; export const selectOperationError = (state: RootState) => state.operations.error; +export const selectOperationInProgress = (state: RootState) => state.operations.isOperationInProgress; export default operationsSlice.reducer; From a0e1a1fa8a80afbe4d3c8c271c391be0036b2a90 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 1 Aug 2024 15:43:04 -0700 Subject: [PATCH 0153/1479] Disable starting --- UI/web-app/src/components/Operation/Operation.base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index 73c59562d..f2eab35bc 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -51,7 +51,7 @@ export const OperationBase: React.FunctionComponent = (props: Op handleOperation(Operations.Start)} - disabled={isButtonDisabled} + disabled={isButtonDisabled || displayStatus == ServiceStatuses.Starting} className={classNames.button} /> ) : ( From 7f489881691818b9a7fc8fd06061f8afa296c504 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Fri, 2 Aug 2024 10:31:13 -0700 Subject: [PATCH 0154/1479] localizering button text --- UI/web-app/src/components/Operation/Operation.base.tsx | 8 ++++---- UI/web-app/src/components/Operation/Operation.types.ts | 10 ++++++++++ UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx | 1 + UI/web-app/src/services/localization/IStrings.ts | 8 ++++++++ .../localization/i18n/locales/en/translations.ts | 8 ++++++++ .../localization/i18n/locales/es/translations.ts | 8 ++++++++ 6 files changed, 39 insertions(+), 4 deletions(-) diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index f2eab35bc..3f2246c04 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -12,7 +12,7 @@ import type { OperationProps, OperationStyles, OperationStyleProps } from './Ope export const getClassNames = classNamesFunction(); export const OperationBase: React.FunctionComponent = (props: OperationProps) => { - const { title, description, className, styles } = props; + const { title, description, className, styles, buttonText} = props; const classNames = getClassNames(styles, { className, theme: useTheme(), @@ -49,21 +49,21 @@ export const OperationBase: React.FunctionComponent = (props: Op
{displayStatus === ServiceStatuses.Stopped || displayStatus === ServiceStatuses.Starting ? ( handleOperation(Operations.Start)} disabled={isButtonDisabled || displayStatus == ServiceStatuses.Starting} className={classNames.button} /> ) : ( handleOperation(Operations.Stop)} disabled={isButtonDisabled || displayStatus !== ServiceStatuses.Running} className={classNames.button} /> )} handleOperation(Operations.Reset)} disabled={isButtonDisabled || displayStatus !== ServiceStatuses.Running} className={classNames.button} diff --git a/UI/web-app/src/components/Operation/Operation.types.ts b/UI/web-app/src/components/Operation/Operation.types.ts index e1a7d45b0..61927fdd7 100644 --- a/UI/web-app/src/components/Operation/Operation.types.ts +++ b/UI/web-app/src/components/Operation/Operation.types.ts @@ -24,4 +24,14 @@ export type OperationProps = React.AllHTMLAttributes & { styles?: IStyleFunctionOrObject; title: string; description: string; + buttonText: ButtonTexts; +}; + +export type ButtonTexts = { + stop: string; + stopping: string; + reset: string; + resetting: string; + start: string; + starting: string; }; \ No newline at end of file diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx index 0c4c1b4a8..1f276042f 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx @@ -135,6 +135,7 @@ const Operations: React.FunctionComponent = (props: OperationsP
); diff --git a/UI/web-app/src/services/localization/IStrings.ts b/UI/web-app/src/services/localization/IStrings.ts index 12857a917..0853de65c 100644 --- a/UI/web-app/src/services/localization/IStrings.ts +++ b/UI/web-app/src/services/localization/IStrings.ts @@ -105,6 +105,14 @@ export type IStrings = { description: string; title: string; }; + buttons: { + stop: string; + stopping: string; + reset: string; + resetting: string; + start: string; + starting: string; + }; }; CustomSourceSettings: { labels: { diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index be0c89054..c01a115d2 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -107,6 +107,14 @@ export const strings: IStrings = { operations: "Operations", title: "GMM Control Panel", description: "Use the stop button to halt GMM operations and the reset button to reboot GMM at any time.", + }, + buttons: { + stop: "Stop GMM", + stopping: "Stopping", + reset: "Reset GMM", + resetting: "Resetting", + start: "Start GMM", + starting: "Starting", } }, CustomSourceSettings: { diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index 2f81a41cd..59cad7da0 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -109,6 +109,14 @@ export const strings: IStrings = { operations: "Operaciones", title: "Panel de Control GMM", description: "Utilice el botón Detener para detener las operaciones de GMM y el botón Restablecer para restablecer GMM en cualquier momento." + }, + buttons: { + stop: "Detener GMM", + stopping: "Deteniendo", + reset: "Restablecer GMM", + resetting: "Restableciendo", + start: "Iniciar GMM", + starting: "Iniciando", } }, CustomSourceSettings: { From a8e55e5d9cbc7165db238f69f44446d5cfcc6cdc Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 2 Aug 2024 11:13:10 -0700 Subject: [PATCH 0155/1479] Updated to .NET 8.0, updated nuget packages --- .../Common.DependencyInjection.csproj | 14 ++++++------- .../Common.FunctionSetup.csproj | 2 +- .../DIConcreteTypes/DIConcreteTypes.csproj | 2 +- .../Entities/Entities.csproj | 6 +++--- .../GroupMembershipManagement.sln | 16 +++++++-------- .../Hosts.FunctionBase.csproj | 14 ++++++------- .../Function/AzureMaintenance.csproj | 6 +++--- .../Services.Entities.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 8 ++++---- .../AzureMaintenance/Services/Services.csproj | 2 +- .../Function/AzureUserReader.csproj | 6 +++--- .../Services.Contracts.csproj | 2 +- .../Services.Entities.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 8 ++++---- .../AzureUserReader/Services/Services.csproj | 2 +- .../DemoUserSetup/DemoUserSetup.csproj | 8 ++++---- .../DestinationAttributesUpdater.csproj | 6 +++--- .../Services.Contracts.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 8 ++++---- .../Services/Services.csproj | 2 +- .../GraphUpdater/Function/GraphUpdater.csproj | 10 +++++----- .../Repositories.Integration.Tests.csproj | 8 ++++---- .../Repositories.MembershipDifference.csproj | 2 +- .../Services.Contracts.csproj | 4 ++-- .../Services.Entities.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 10 +++++----- .../GraphUpdater/Services/Services.csproj | 6 +++--- .../Function/GroupMembershipObtainer.csproj | 12 +++++------ .../Services.Tests/Tests.Services.csproj | 8 ++++---- .../GroupMembershipObtainerService.csproj | 4 ++-- .../Function/GroupOwnershipObtainer.csproj | 10 +++++----- .../Services.Contracts.csproj | 2 +- .../Services.Entities.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 8 ++++---- .../Services/Services.csproj | 2 +- .../ConsoleApp/JobSchedulerConsoleApp.csproj | 2 +- .../JobScheduler/Function/JobScheduler.csproj | 6 +++--- .../Services.Contracts.csproj | 2 +- .../Services.Entities.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 8 ++++---- .../JobScheduler/Services/Services.csproj | 6 +++--- .../JobTrigger/Function/JobTrigger.csproj | 8 ++++---- .../Services.Contracts.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 8 ++++---- .../Hosts/JobTrigger/Services/Services.csproj | 4 ++-- .../Function/MembershipAggregator.csproj | 8 ++++---- .../Services.Contracts.csproj | 4 ++-- .../Services.Entities.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 10 +++++----- .../Services/Services.csproj | 6 +++--- .../Function/NonProdService.csproj | 8 ++++---- .../Services.Contracts.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 8 ++++---- .../NonProdService/Services/Services.csproj | 2 +- .../Hosts/Notifier/Function/Notifier.csproj | 10 +++++----- .../Services.Notifier.Contracts.csproj | 2 +- .../Services.Notifier.Tests.csproj | 14 ++++++------- .../Services.Notifier.csproj | 8 ++++---- .../Function/PlaceMembershipObtainer.csproj | 12 +++++------ .../Services.Tests/Tests.Services.csproj | 10 +++++----- .../Services/Services.csproj | 10 +++++----- .../SqlMembershipObtainer.Entities.csproj | 4 ++-- .../Function/SqlMembershipObtainer.csproj | 10 +++++----- .../Services.Contracts.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 8 ++++---- .../Services/Services/Services.csproj | 6 +++--- .../TeamsChannelMembershipObtainer.csproj | 10 +++++----- ...embershipObtainer.Service.Contracts.csproj | 2 +- .../Services.Tests/Services.Tests.csproj | 8 ++++---- ...msChannelMembershipObtainer.Service.csproj | 10 +++++----- .../Function/TeamsChannelUpdater.csproj | 8 ++++---- ...vices.TeamsChannelUpdater.Contracts.csproj | 2 +- .../Services.TeamsChannelUpdater.csproj | 8 ++++---- .../Services.Tests/Services.Tests.csproj | 8 ++++---- .../Services.Messages.Contracts.csproj | 2 +- .../Services.Messages.csproj | 6 +++--- .../Services.WebApi.Contracts.csproj | 2 +- .../Services.WebApi/Services.WebApi.csproj | 12 +++++------ .../WebApi/WebApi.Models/WebApi.Models.csproj | 2 +- .../WebApi/WebApi.Tests/WebApi.Tests.csproj | 2 +- .../Hosts/WebApi/WebApi/WebApi.csproj | 20 +++++++++---------- .../Models.Tests/Models.Tests.csproj | 8 ++++---- .../Models/Models.csproj | 2 +- ...ositories.AzureBlobBackupRepository.csproj | 2 +- .../Repositories.BlobStorage.csproj | 4 ++-- .../Repositories.Contracts.Tests.csproj | 13 +++++++----- .../Repositories.Contracts.csproj | 4 ++-- .../Repositories.DataFactory.csproj | 12 +++++------ ...positories.EntityFramework.Contexts.csproj | 8 ++++---- .../Repositories.EntityFramework.csproj | 4 ++-- .../Repositories.FeatureFlag.csproj | 6 +++--- .../Repositories.GraphAzureADGroups.csproj | 8 ++++---- .../Repositories.GraphAzureADUsers.csproj | 6 +++--- .../Repositories.Localization.csproj | 4 ++-- .../Repositories.Logging.csproj | 4 ++-- .../Repositories.LoggingRepos.Tests.csproj | 4 ++-- .../Repositories.Mail.csproj | 6 +++--- .../Repositories.Mocks.csproj | 4 ++-- .../Repositories.RetryPolicyProvider.csproj | 4 ++-- .../Repositories.ServiceBusQueue.csproj | 4 ++-- ...ories.ServiceBusSubscriptions.Tests.csproj | 2 +- ...epositories.ServiceBusSubscriptions.csproj | 4 ++-- ...Repositories.ServiceBusTopics.Tests.csproj | 2 +- .../Repositories.ServiceStatus.csproj | 2 +- ...epositories.SqlMembershipRepository.csproj | 4 ++-- .../Repositories.SyncJobs.Tests.csproj | 2 +- .../Repositories.TeamsChannel.csproj | 8 ++++---- .../Repositories.ServiceBusTopics.csproj | 8 ++++---- .../Services.Contracts.csproj | 2 +- .../Services.Notifications.csproj | 6 +++--- Service/GroupMembershipManagement/global.json | 2 +- 111 files changed, 327 insertions(+), 326 deletions(-) diff --git a/Service/GroupMembershipManagement/Common.DependencyInjection/Common.DependencyInjection.csproj b/Service/GroupMembershipManagement/Common.DependencyInjection/Common.DependencyInjection.csproj index 7e5175f99..2a42f4cca 100644 --- a/Service/GroupMembershipManagement/Common.DependencyInjection/Common.DependencyInjection.csproj +++ b/Service/GroupMembershipManagement/Common.DependencyInjection/Common.DependencyInjection.csproj @@ -1,18 +1,18 @@ - net6.0 + net8.0 - - - - - + + + + + - + diff --git a/Service/GroupMembershipManagement/Common.FunctionSetup/Common.FunctionSetup.csproj b/Service/GroupMembershipManagement/Common.FunctionSetup/Common.FunctionSetup.csproj index dbc151713..58990cd56 100644 --- a/Service/GroupMembershipManagement/Common.FunctionSetup/Common.FunctionSetup.csproj +++ b/Service/GroupMembershipManagement/Common.FunctionSetup/Common.FunctionSetup.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/DIConcreteTypes/DIConcreteTypes.csproj b/Service/GroupMembershipManagement/DIConcreteTypes/DIConcreteTypes.csproj index 83b62e15f..d098efea6 100644 --- a/Service/GroupMembershipManagement/DIConcreteTypes/DIConcreteTypes.csproj +++ b/Service/GroupMembershipManagement/DIConcreteTypes/DIConcreteTypes.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Entities/Entities.csproj b/Service/GroupMembershipManagement/Entities/Entities.csproj index de1444407..80c410295 100644 --- a/Service/GroupMembershipManagement/Entities/Entities.csproj +++ b/Service/GroupMembershipManagement/Entities/Entities.csproj @@ -1,12 +1,12 @@  - net6.0 + net8.0 - - + + diff --git a/Service/GroupMembershipManagement/GroupMembershipManagement.sln b/Service/GroupMembershipManagement/GroupMembershipManagement.sln index 250575954..18a95a617 100644 --- a/Service/GroupMembershipManagement/GroupMembershipManagement.sln +++ b/Service/GroupMembershipManagement/GroupMembershipManagement.sln @@ -61,7 +61,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Contracts.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.RetryPolicyProvider", "Repositories.RetryPolicyProvider\Repositories.RetryPolicyProvider.csproj", "{1536370F-54D6-4325-B212-8AC535357DDF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repositories.ServiceStatus", "Repositories.ServiceStatus\Repositories.ServiceStatus.csproj", "{CF416C44-4B7F-4722-A866-353A9FBD710A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.ServiceStatus", "Repositories.ServiceStatus\Repositories.ServiceStatus.csproj", "{CF416C44-4B7F-4722-A866-353A9FBD710A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Contracts", "Hosts\GraphUpdater\Services.Contracts\Services.Contracts.csproj", "{8963EBC9-4182-465E-9C38-0B69847FD9CE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -157,14 +159,6 @@ Global {A96E6F5F-0022-47A0-A4DE-B915DF4746D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {A96E6F5F-0022-47A0-A4DE-B915DF4746D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {A96E6F5F-0022-47A0-A4DE-B915DF4746D6}.Release|Any CPU.Build.0 = Release|Any CPU - {684F33BF-821D-41F8-A9D2-EBA0AD1A64E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {684F33BF-821D-41F8-A9D2-EBA0AD1A64E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {684F33BF-821D-41F8-A9D2-EBA0AD1A64E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {684F33BF-821D-41F8-A9D2-EBA0AD1A64E7}.Release|Any CPU.Build.0 = Release|Any CPU - {31948D74-32A0-45D4-8732-C63D2E1A1622}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {31948D74-32A0-45D4-8732-C63D2E1A1622}.Debug|Any CPU.Build.0 = Debug|Any CPU - {31948D74-32A0-45D4-8732-C63D2E1A1622}.Release|Any CPU.ActiveCfg = Release|Any CPU - {31948D74-32A0-45D4-8732-C63D2E1A1622}.Release|Any CPU.Build.0 = Release|Any CPU {9A389F97-A10B-433F-91CF-DC89A7B6F5C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9A389F97-A10B-433F-91CF-DC89A7B6F5C6}.Debug|Any CPU.Build.0 = Debug|Any CPU {9A389F97-A10B-433F-91CF-DC89A7B6F5C6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -189,6 +183,10 @@ Global {CF416C44-4B7F-4722-A866-353A9FBD710A}.Debug|Any CPU.Build.0 = Debug|Any CPU {CF416C44-4B7F-4722-A866-353A9FBD710A}.Release|Any CPU.ActiveCfg = Release|Any CPU {CF416C44-4B7F-4722-A866-353A9FBD710A}.Release|Any CPU.Build.0 = Release|Any CPU + {8963EBC9-4182-465E-9C38-0B69847FD9CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8963EBC9-4182-465E-9C38-0B69847FD9CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8963EBC9-4182-465E-9C38-0B69847FD9CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8963EBC9-4182-465E-9C38-0B69847FD9CE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj b/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj index 9eaac355b..b74677025 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj @@ -1,20 +1,20 @@  - net6.0 + net8.0 - + - - - - - + + + + + diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj index b443c503d..61a978707 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj @@ -1,14 +1,14 @@  - net6.0 + net8.0 v4 Hosts.AzureMaintenance Hosts.AzureMaintenance - - + + diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Services.Entities.csproj b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Services.Entities.csproj index 822f52d6f..fc65222cf 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Services.Entities.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Entities/Services.Entities.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/Services.Tests.csproj index 4f0d33eed..fd7be6df0 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false @@ -11,10 +11,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/Services.csproj index 5b72c98c5..8296dbe2e 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Services/Services.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.csproj b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.csproj index eb9f64b71..69b569bf2 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.csproj @@ -1,14 +1,14 @@ - net6.0 + net8.0 v4 Hosts.AzureUserReader Hosts.AzureUserReader - - + + diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Contracts/Services.Contracts.csproj index f68c3057e..efb444454 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Contracts/Services.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Entities/Services.Entities.csproj b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Entities/Services.Entities.csproj index 532fb90a6..9737093a2 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Entities/Services.Entities.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Entities/Services.Entities.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/Services.Tests.csproj index 5dd10ec71..1a342dbe3 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false @@ -11,10 +11,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services/Services.csproj index 59e0a15e3..07d7b4d98 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services/Services.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/Console/DemoUserSetup/DemoUserSetup.csproj b/Service/GroupMembershipManagement/Hosts/Console/DemoUserSetup/DemoUserSetup.csproj index 4255a7b62..4a314b0bd 100644 --- a/Service/GroupMembershipManagement/Hosts/Console/DemoUserSetup/DemoUserSetup.csproj +++ b/Service/GroupMembershipManagement/Hosts/Console/DemoUserSetup/DemoUserSetup.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 - - - + + + diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/DestinationAttributesUpdater.csproj b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/DestinationAttributesUpdater.csproj index 7f14991e9..cb2a7c277 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/DestinationAttributesUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/DestinationAttributesUpdater.csproj @@ -1,12 +1,12 @@  - net6.0 + net8.0 v4 - - + + diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Contracts/Services.Contracts.csproj index afc99f5e2..b6c38e2c5 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Contracts/Services.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/Services.Tests.csproj index 957068541..0c550238d 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false @@ -11,10 +11,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/Services.csproj index 9cbff6c68..42bee1bd2 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/Services.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj index ad2fc3e76..ffb56a312 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj @@ -1,14 +1,14 @@ - net6.0 + net8.0 v4 - + - - - + + + diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories.Integration.Tests/Repositories.Integration.Tests.csproj b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories.Integration.Tests/Repositories.Integration.Tests.csproj index c960a27bf..f9d7e227d 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories.Integration.Tests/Repositories.Integration.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories.Integration.Tests/Repositories.Integration.Tests.csproj @@ -1,16 +1,16 @@ - net6.0 + net8.0 false - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories/Repositories.MembershipDifference/Repositories.MembershipDifference.csproj b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories/Repositories.MembershipDifference/Repositories.MembershipDifference.csproj index fcdf08138..a84b1f3b9 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories/Repositories.MembershipDifference/Repositories.MembershipDifference.csproj +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Repositories/Repositories.MembershipDifference/Repositories.MembershipDifference.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Contracts/Services.Contracts.csproj index 38eabf148..c5355a596 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Contracts/Services.Contracts.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/Services.Entities.csproj b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/Services.Entities.csproj index ff6083487..eb4ff41e9 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/Services.Entities.csproj +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Entities/Services.Entities.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Services.Tests.csproj index eaade321c..dce8e5fa1 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false @@ -11,15 +11,15 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services/Services.csproj index 47f2a3722..75c34da65 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services/Services.csproj @@ -1,12 +1,12 @@ - net6.0 + net8.0 - - + + diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj index 45b5edd2f..f53efe6b4 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj @@ -1,16 +1,16 @@ - net6.0 + net8.0 v4 - + - - - - + + + + diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Tests.Services.csproj b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Tests.Services.csproj index 4c11d4314..b4dc80bef 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Tests.Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Tests.Services.csproj @@ -1,16 +1,16 @@ - net6.0 + net8.0 false - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services/GroupMembershipObtainerService/GroupMembershipObtainerService.csproj b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services/GroupMembershipObtainerService/GroupMembershipObtainerService.csproj index 5eedb1b09..37d604dae 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services/GroupMembershipObtainerService/GroupMembershipObtainerService.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services/GroupMembershipObtainerService/GroupMembershipObtainerService.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/GroupOwnershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/GroupOwnershipObtainer.csproj index 45dff0620..d7476a9c3 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/GroupOwnershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/GroupOwnershipObtainer.csproj @@ -1,13 +1,13 @@ - net6.0 + net8.0 v4 - - - - + + + + diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Contracts/Services.Contracts.csproj index 96da281df..e69925a49 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Contracts/Services.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Entities/Services.Entities.csproj b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Entities/Services.Entities.csproj index 132c02c59..30402ac0e 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Entities/Services.Entities.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Entities/Services.Entities.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/Services.Tests.csproj index 02c278247..7cb78fb99 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable @@ -15,10 +15,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services/Services.csproj index 08a8a9c4d..656c85c08 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services/Services.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/ConsoleApp/JobSchedulerConsoleApp.csproj b/Service/GroupMembershipManagement/Hosts/JobScheduler/ConsoleApp/JobSchedulerConsoleApp.csproj index 015a43843..713dc9338 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/ConsoleApp/JobSchedulerConsoleApp.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/ConsoleApp/JobSchedulerConsoleApp.csproj @@ -1,7 +1,7 @@  Exe - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj index b3f971644..e90d3711f 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 v4 Hosts.AzureMaintenance Hosts.AzureMaintenance @@ -9,8 +9,8 @@ - - + + diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Contracts/Services.Contracts.csproj index 1d87c5a4a..0bcda5cfb 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Contracts/Services.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Entities/Services.Entities.csproj b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Entities/Services.Entities.csproj index 6eb841d89..78a1df07a 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Entities/Services.Entities.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Entities/Services.Entities.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Tests/Services.Tests.csproj index 5bc4a35f8..e6e59e8ee 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false @@ -11,10 +11,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/Services.csproj index f7d44f382..ed589674d 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Services/Services.csproj @@ -1,12 +1,12 @@  - net6.0 + net8.0 - - + + diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj index 70bc684c6..4fe1a8b70 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj @@ -1,15 +1,15 @@  - net6.0 + net8.0 v4 41fcfc96-9608-4426-9cd8-181202f026a4 - - - + + + diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/Services.Contracts.csproj index 532fb90a6..9737093a2 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/Services.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/Services.Tests.csproj index 0208518d2..43af511c3 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false @@ -11,10 +11,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/Services.csproj index 23470219d..713ba9296 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/Services.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj index f9890df54..707326d89 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj @@ -1,13 +1,13 @@ - net6.0 + net8.0 v4 - - - + + + diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Contracts/Services.Contracts.csproj index e4b68754a..e5d5e1fbe 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Contracts/Services.Contracts.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Services.Entities.csproj b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Services.Entities.csproj index 4cc6925ee..902a7e1ea 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Services.Entities.csproj +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Services.Entities.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/Services.Tests.csproj index 48b7bc092..fb7a1c462 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false @@ -11,11 +11,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/Services.csproj index 795633452..2efdf9212 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/Services.csproj @@ -1,12 +1,12 @@ - net6.0 + net8.0 - - + + diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj index f062002f4..b711a7d66 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj @@ -1,15 +1,15 @@  - net6.0 + net8.0 v4 41fcfc96-9608-4426-9cd8-181202f026a4 - - - + + + diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Contracts/Services.Contracts.csproj index 532fb90a6..9737093a2 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Contracts/Services.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Tests/Services.Tests.csproj index 697600956..ca14a026e 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false @@ -11,11 +11,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/NonProdService/Services/Services.csproj index 1a0cc813d..377be0f55 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Services/Services.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj index b5dc9830b..985ba514d 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj @@ -1,15 +1,15 @@  - net6.0 + net8.0 v4 Hosts.AzureMaintenance Hosts.AzureMaintenance - - - - + + + + diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/Services.Notifier.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/Services.Notifier.Contracts.csproj index 1b3fbe831..1b9c7c652 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/Services.Notifier.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/Services.Notifier.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/Services.Notifier.Tests.csproj b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/Services.Notifier.Tests.csproj index 304a9f1f8..42b450c75 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/Services.Notifier.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/Services.Notifier.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false @@ -11,13 +11,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/Services.Notifier.csproj b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/Services.Notifier.csproj index 88a4fc349..f94a5c16e 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/Services.Notifier.csproj +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/Services.Notifier.csproj @@ -1,13 +1,13 @@  - net6.0 + net8.0 - + - - + + diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj index 4d29da1f6..58e304dc3 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj @@ -1,14 +1,14 @@ - net6.0 + net8.0 v4 - - - - - + + + + + diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj index bbae1d855..b3b993958 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj @@ -1,17 +1,17 @@ - net6.0 + net8.0 false - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services/Services.csproj index 901a2ee57..9867e10b3 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services/Services.csproj @@ -1,16 +1,16 @@ - net6.0 + net8.0 enable enable - - - - + + + + diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/SqlMembershipObtainer.Entities.csproj b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/SqlMembershipObtainer.Entities.csproj index 3dacf09c3..14ca0790f 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/SqlMembershipObtainer.Entities.csproj +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/SqlMembershipObtainer.Entities.csproj @@ -1,12 +1,12 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj index dbc6cf2b6..862e51602 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 v4 SqlMembershipObtainer SqlMembershipObtainer @@ -12,11 +12,11 @@ - + - - - + + + diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Contracts/Services.Contracts.csproj index cc660c7bd..6b2552d35 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Contracts/Services.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Services.Tests.csproj index 3561d0f87..80b878e4d 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false @@ -11,10 +11,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services/Services/Services.csproj index c87da4b70..336d46043 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services/Services/Services.csproj @@ -1,16 +1,16 @@ - net6.0 + net8.0 enable enable - + - + diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/TeamsChannelMembershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/TeamsChannelMembershipObtainer.csproj index 205990fc1..ac9b7414b 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/TeamsChannelMembershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/TeamsChannelMembershipObtainer.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 v4 Hosts.$(MSBuildProjectName) Hosts.$(MSBuildProjectName.Replace(" ", "_")) @@ -10,10 +10,10 @@ <_FunctionsSkipCleanOutput>true - - - - + + + + diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services.Contracts/TeamsChannelMembershipObtainer.Service.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services.Contracts/TeamsChannelMembershipObtainer.Service.Contracts.csproj index 5d6a498d4..99c506bae 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services.Contracts/TeamsChannelMembershipObtainer.Service.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services.Contracts/TeamsChannelMembershipObtainer.Service.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services.Tests/Services.Tests.csproj index 817ea2886..7df6ed70d 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable @@ -13,10 +13,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services/TeamsChannelMembershipObtainer.Service/TeamsChannelMembershipObtainer.Service.csproj b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services/TeamsChannelMembershipObtainer.Service/TeamsChannelMembershipObtainer.Service.csproj index 6b881d755..19b1072f8 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services/TeamsChannelMembershipObtainer.Service/TeamsChannelMembershipObtainer.Service.csproj +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Services/TeamsChannelMembershipObtainer.Service/TeamsChannelMembershipObtainer.Service.csproj @@ -1,16 +1,16 @@ - net6.0 + net8.0 enable enable - - - - + + + + diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.csproj b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.csproj index 88215ec03..56d0216ea 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 v4 Hosts.$(MSBuildProjectName) Hosts.$(MSBuildProjectName.Replace(" ", "_")) @@ -10,10 +10,10 @@ <_FunctionsSkipCleanOutput>true - + - - + + diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater.Contracts/Services.TeamsChannelUpdater.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater.Contracts/Services.TeamsChannelUpdater.Contracts.csproj index 5d6a498d4..99c506bae 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater.Contracts/Services.TeamsChannelUpdater.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater.Contracts/Services.TeamsChannelUpdater.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater/Services.TeamsChannelUpdater.csproj b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater/Services.TeamsChannelUpdater.csproj index 6d107d2b4..3e1107ba5 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater/Services.TeamsChannelUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.TeamsChannelUpdater/Services.TeamsChannelUpdater.csproj @@ -1,15 +1,15 @@  - net6.0 + net8.0 enable enable - - - + + + diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/Services.Tests.csproj index 16b7cc71a..1fdf68b08 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable @@ -13,10 +13,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages.Contracts/Services.Messages.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages.Contracts/Services.Messages.Contracts.csproj index 132c02c59..30402ac0e 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages.Contracts/Services.Messages.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages.Contracts/Services.Messages.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Services.Messages.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Services.Messages.csproj index 01638a591..26cd2c7e3 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Services.Messages.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Services.Messages.csproj @@ -1,14 +1,14 @@  - net6.0 + net8.0 enable enable - - + + diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/Services.WebApi.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/Services.WebApi.Contracts.csproj index ea3135b1b..0a10c40d8 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/Services.WebApi.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi.Contracts/Services.WebApi.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj index 7437294d2..a6af131c9 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable @@ -10,12 +10,12 @@ - - - + + + - - + + diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/WebApi.Models.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/WebApi.Models.csproj index afc99f5e2..b6c38e2c5 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/WebApi.Models.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/WebApi.Models.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/WebApi.Tests.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/WebApi.Tests.csproj index 733a381ad..0ab94dcf8 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/WebApi.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/WebApi.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj index 4d5910f9c..8193afbfd 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj @@ -1,28 +1,28 @@  - net6.0 + net8.0 enable enable - + - - - - - + + + + + - - + + - + diff --git a/Service/GroupMembershipManagement/Models.Tests/Models.Tests.csproj b/Service/GroupMembershipManagement/Models.Tests/Models.Tests.csproj index 061132a98..5a78801c2 100644 --- a/Service/GroupMembershipManagement/Models.Tests/Models.Tests.csproj +++ b/Service/GroupMembershipManagement/Models.Tests/Models.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable @@ -9,9 +9,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Service/GroupMembershipManagement/Models/Models.csproj b/Service/GroupMembershipManagement/Models/Models.csproj index 1057c45b0..926b21146 100644 --- a/Service/GroupMembershipManagement/Models/Models.csproj +++ b/Service/GroupMembershipManagement/Models/Models.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Repositories.AzureBlobBackupRepository/Repositories.AzureBlobBackupRepository.csproj b/Service/GroupMembershipManagement/Repositories.AzureBlobBackupRepository/Repositories.AzureBlobBackupRepository.csproj index 9a1ebe7d1..828685abd 100644 --- a/Service/GroupMembershipManagement/Repositories.AzureBlobBackupRepository/Repositories.AzureBlobBackupRepository.csproj +++ b/Service/GroupMembershipManagement/Repositories.AzureBlobBackupRepository/Repositories.AzureBlobBackupRepository.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Repositories.BlobStorage/Repositories.BlobStorage.csproj b/Service/GroupMembershipManagement/Repositories.BlobStorage/Repositories.BlobStorage.csproj index 57b3741a1..10cc3ef15 100644 --- a/Service/GroupMembershipManagement/Repositories.BlobStorage/Repositories.BlobStorage.csproj +++ b/Service/GroupMembershipManagement/Repositories.BlobStorage/Repositories.BlobStorage.csproj @@ -1,12 +1,12 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Repositories.Contracts.Tests/Repositories.Contracts.Tests.csproj b/Service/GroupMembershipManagement/Repositories.Contracts.Tests/Repositories.Contracts.Tests.csproj index daeefe977..5a78801c2 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts.Tests/Repositories.Contracts.Tests.csproj +++ b/Service/GroupMembershipManagement/Repositories.Contracts.Tests/Repositories.Contracts.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable @@ -9,10 +9,13 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/Repositories.Contracts.csproj b/Service/GroupMembershipManagement/Repositories.Contracts/Repositories.Contracts.csproj index 9bc083f03..c0e1eb7fa 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts/Repositories.Contracts.csproj +++ b/Service/GroupMembershipManagement/Repositories.Contracts/Repositories.Contracts.csproj @@ -1,12 +1,12 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj b/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj index 83cd9d1b3..29acc6098 100644 --- a/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj +++ b/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj @@ -1,16 +1,16 @@ - net6.0 + net8.0 - - - - - + + + + + diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Repositories.EntityFramework.Contexts.csproj b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Repositories.EntityFramework.Contexts.csproj index 92db6dbf9..45ae4e3f1 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Repositories.EntityFramework.Contexts.csproj +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework.Contexts/Repositories.EntityFramework.Contexts.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable @@ -9,12 +9,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework/Repositories.EntityFramework.csproj b/Service/GroupMembershipManagement/Repositories.EntityFramework/Repositories.EntityFramework.csproj index db896ba05..c85303cb7 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework/Repositories.EntityFramework.csproj +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework/Repositories.EntityFramework.csproj @@ -1,14 +1,14 @@ - net6.0 + net8.0 enable enable - + diff --git a/Service/GroupMembershipManagement/Repositories.FeatureFlag/Repositories.FeatureFlag.csproj b/Service/GroupMembershipManagement/Repositories.FeatureFlag/Repositories.FeatureFlag.csproj index fb8ad25ca..2a8982976 100644 --- a/Service/GroupMembershipManagement/Repositories.FeatureFlag/Repositories.FeatureFlag.csproj +++ b/Service/GroupMembershipManagement/Repositories.FeatureFlag/Repositories.FeatureFlag.csproj @@ -1,14 +1,14 @@ - net6.0 + net8.0 enable enable - - + + diff --git a/Service/GroupMembershipManagement/Repositories.GraphGroups/Repositories.GraphAzureADGroups.csproj b/Service/GroupMembershipManagement/Repositories.GraphGroups/Repositories.GraphAzureADGroups.csproj index b7c88d25c..054a56c00 100644 --- a/Service/GroupMembershipManagement/Repositories.GraphGroups/Repositories.GraphAzureADGroups.csproj +++ b/Service/GroupMembershipManagement/Repositories.GraphGroups/Repositories.GraphAzureADGroups.csproj @@ -1,13 +1,13 @@  - net6.0 + net8.0 - - - + + + diff --git a/Service/GroupMembershipManagement/Repositories.GraphUsers/Repositories.GraphAzureADUsers.csproj b/Service/GroupMembershipManagement/Repositories.GraphUsers/Repositories.GraphAzureADUsers.csproj index 3aa560248..52199d932 100644 --- a/Service/GroupMembershipManagement/Repositories.GraphUsers/Repositories.GraphAzureADUsers.csproj +++ b/Service/GroupMembershipManagement/Repositories.GraphUsers/Repositories.GraphAzureADUsers.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 @@ -13,8 +13,8 @@ - - + + diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Repositories.Localization.csproj b/Service/GroupMembershipManagement/Repositories.Localization/Repositories.Localization.csproj index ca4071a79..ed86cfa98 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Repositories.Localization.csproj +++ b/Service/GroupMembershipManagement/Repositories.Localization/Repositories.Localization.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Repositories.Logging/Repositories.Logging.csproj b/Service/GroupMembershipManagement/Repositories.Logging/Repositories.Logging.csproj index f2b890035..771776b42 100644 --- a/Service/GroupMembershipManagement/Repositories.Logging/Repositories.Logging.csproj +++ b/Service/GroupMembershipManagement/Repositories.Logging/Repositories.Logging.csproj @@ -1,12 +1,12 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Repositories.LoggingRepos.Tests/Repositories.LoggingRepos.Tests.csproj b/Service/GroupMembershipManagement/Repositories.LoggingRepos.Tests/Repositories.LoggingRepos.Tests.csproj index 74ae89666..406d2e769 100644 --- a/Service/GroupMembershipManagement/Repositories.LoggingRepos.Tests/Repositories.LoggingRepos.Tests.csproj +++ b/Service/GroupMembershipManagement/Repositories.LoggingRepos.Tests/Repositories.LoggingRepos.Tests.csproj @@ -1,13 +1,13 @@  - net6.0 + net8.0 false - + diff --git a/Service/GroupMembershipManagement/Repositories.Mail/Repositories.Mail.csproj b/Service/GroupMembershipManagement/Repositories.Mail/Repositories.Mail.csproj index 6d1184a37..923424019 100644 --- a/Service/GroupMembershipManagement/Repositories.Mail/Repositories.Mail.csproj +++ b/Service/GroupMembershipManagement/Repositories.Mail/Repositories.Mail.csproj @@ -1,12 +1,12 @@  - net6.0 + net8.0 - - + + diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj b/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj index fae589593..19a85e2cf 100644 --- a/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj +++ b/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj @@ -1,13 +1,13 @@ - net6.0 + net8.0 false - + diff --git a/Service/GroupMembershipManagement/Repositories.RetryPolicyProvider/Repositories.RetryPolicyProvider.csproj b/Service/GroupMembershipManagement/Repositories.RetryPolicyProvider/Repositories.RetryPolicyProvider.csproj index f57a4387c..d960ebeed 100644 --- a/Service/GroupMembershipManagement/Repositories.RetryPolicyProvider/Repositories.RetryPolicyProvider.csproj +++ b/Service/GroupMembershipManagement/Repositories.RetryPolicyProvider/Repositories.RetryPolicyProvider.csproj @@ -1,13 +1,13 @@ - net6.0 + net8.0 Repositories.RetryPolicyProvider Repositories.RetryPolicyProvider - + diff --git a/Service/GroupMembershipManagement/Repositories.ServiceBusQueue/Repositories.ServiceBusQueue.csproj b/Service/GroupMembershipManagement/Repositories.ServiceBusQueue/Repositories.ServiceBusQueue.csproj index 898f08a0b..1e839ca03 100644 --- a/Service/GroupMembershipManagement/Repositories.ServiceBusQueue/Repositories.ServiceBusQueue.csproj +++ b/Service/GroupMembershipManagement/Repositories.ServiceBusQueue/Repositories.ServiceBusQueue.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Repositories.ServiceBusSubscriptions.Tests/Repositories.ServiceBusSubscriptions.Tests.csproj b/Service/GroupMembershipManagement/Repositories.ServiceBusSubscriptions.Tests/Repositories.ServiceBusSubscriptions.Tests.csproj index 9336fe616..8d2730047 100644 --- a/Service/GroupMembershipManagement/Repositories.ServiceBusSubscriptions.Tests/Repositories.ServiceBusSubscriptions.Tests.csproj +++ b/Service/GroupMembershipManagement/Repositories.ServiceBusSubscriptions.Tests/Repositories.ServiceBusSubscriptions.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false diff --git a/Service/GroupMembershipManagement/Repositories.ServiceBusSubscriptions/Repositories.ServiceBusSubscriptions.csproj b/Service/GroupMembershipManagement/Repositories.ServiceBusSubscriptions/Repositories.ServiceBusSubscriptions.csproj index de02b2879..d8c2317cb 100644 --- a/Service/GroupMembershipManagement/Repositories.ServiceBusSubscriptions/Repositories.ServiceBusSubscriptions.csproj +++ b/Service/GroupMembershipManagement/Repositories.ServiceBusSubscriptions/Repositories.ServiceBusSubscriptions.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Repositories.ServiceBusTopics.Tests/Repositories.ServiceBusTopics.Tests.csproj b/Service/GroupMembershipManagement/Repositories.ServiceBusTopics.Tests/Repositories.ServiceBusTopics.Tests.csproj index a0a601c40..19832d493 100644 --- a/Service/GroupMembershipManagement/Repositories.ServiceBusTopics.Tests/Repositories.ServiceBusTopics.Tests.csproj +++ b/Service/GroupMembershipManagement/Repositories.ServiceBusTopics.Tests/Repositories.ServiceBusTopics.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false diff --git a/Service/GroupMembershipManagement/Repositories.ServiceStatus/Repositories.ServiceStatus.csproj b/Service/GroupMembershipManagement/Repositories.ServiceStatus/Repositories.ServiceStatus.csproj index ad758765b..8a33a48c9 100644 --- a/Service/GroupMembershipManagement/Repositories.ServiceStatus/Repositories.ServiceStatus.csproj +++ b/Service/GroupMembershipManagement/Repositories.ServiceStatus/Repositories.ServiceStatus.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/Repositories.SqlMembershipRepository.csproj b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/Repositories.SqlMembershipRepository.csproj index 1a4e3e473..2d95e9ccd 100644 --- a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/Repositories.SqlMembershipRepository.csproj +++ b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/Repositories.SqlMembershipRepository.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable @@ -9,7 +9,7 @@ - + diff --git a/Service/GroupMembershipManagement/Repositories.SyncJobs.Tests/Repositories.SyncJobs.Tests.csproj b/Service/GroupMembershipManagement/Repositories.SyncJobs.Tests/Repositories.SyncJobs.Tests.csproj index a0a601c40..19832d493 100644 --- a/Service/GroupMembershipManagement/Repositories.SyncJobs.Tests/Repositories.SyncJobs.Tests.csproj +++ b/Service/GroupMembershipManagement/Repositories.SyncJobs.Tests/Repositories.SyncJobs.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false diff --git a/Service/GroupMembershipManagement/Repositories.TeamsChannel/Repositories.TeamsChannel.csproj b/Service/GroupMembershipManagement/Repositories.TeamsChannel/Repositories.TeamsChannel.csproj index 2484dbf5e..2dfbbbc1d 100644 --- a/Service/GroupMembershipManagement/Repositories.TeamsChannel/Repositories.TeamsChannel.csproj +++ b/Service/GroupMembershipManagement/Repositories.TeamsChannel/Repositories.TeamsChannel.csproj @@ -1,15 +1,15 @@ - net6.0 + net8.0 enable enable - - - + + + diff --git a/Service/GroupMembershipManagement/Repositories.Topics/Repositories.ServiceBusTopics.csproj b/Service/GroupMembershipManagement/Repositories.Topics/Repositories.ServiceBusTopics.csproj index 09cd87ad1..a6da85f2a 100644 --- a/Service/GroupMembershipManagement/Repositories.Topics/Repositories.ServiceBusTopics.csproj +++ b/Service/GroupMembershipManagement/Repositories.Topics/Repositories.ServiceBusTopics.csproj @@ -1,14 +1,14 @@  - net6.0 + net8.0 Repositories.ServiceBusTopics - - - + + + diff --git a/Service/GroupMembershipManagement/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Services.Contracts/Services.Contracts.csproj index 160c2db3f..8dcc5080b 100644 --- a/Service/GroupMembershipManagement/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Services.Contracts/Services.Contracts.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable diff --git a/Service/GroupMembershipManagement/Services.Notifications/Services.Notifications.csproj b/Service/GroupMembershipManagement/Services.Notifications/Services.Notifications.csproj index 9acd2158e..9fe461e96 100644 --- a/Service/GroupMembershipManagement/Services.Notifications/Services.Notifications.csproj +++ b/Service/GroupMembershipManagement/Services.Notifications/Services.Notifications.csproj @@ -1,14 +1,14 @@ - net6.0 + net8.0 enable enable - - + + diff --git a/Service/GroupMembershipManagement/global.json b/Service/GroupMembershipManagement/global.json index 66ff958f2..fd07d882a 100644 --- a/Service/GroupMembershipManagement/global.json +++ b/Service/GroupMembershipManagement/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "6.0.412" + "version": "8.0.303" } } From 52d8e0dac2053f5354a5ef96264f7e6beb90e1dc Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 2 Aug 2024 11:18:30 -0700 Subject: [PATCH 0156/1479] Added new azure function setting for .NET 8.0 --- .../Hosts/AzureMaintenance/Infrastructure/compute/template.bicep | 1 + .../Hosts/AzureUserReader/Infrastructure/compute/template.bicep | 1 + .../Infrastructure/compute/template.bicep | 1 + .../Hosts/GraphUpdater/Infrastructure/compute/template.bicep | 1 + .../Infrastructure/compute/template.bicep | 1 + .../GroupOwnershipObtainer/Infrastructure/compute/template.bicep | 1 + .../Hosts/JobScheduler/Infrastructure/compute/template.bicep | 1 + .../Hosts/JobTrigger/Infrastructure/compute/template.bicep | 1 + .../MembershipAggregator/Infrastructure/compute/template.bicep | 1 + .../Hosts/NonProdService/Infrastructure/compute/template.bicep | 1 + .../Hosts/Notifier/Infrastructure/compute/template.bicep | 1 + .../Infrastructure/compute/template.bicep | 1 + .../SqlMembershipObtainer/Infrastructure/compute/template.bicep | 1 + .../Infrastructure/compute/template.bicep | 1 + .../TeamsChannelUpdater/Infrastructure/compute/template.bicep | 1 + 15 files changed, 15 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep index b2f84162a..2b3602706 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep @@ -97,6 +97,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep index 706824a25..f71c44421 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep @@ -95,6 +95,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep index 5f66c62ae..3b136ec96 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep @@ -120,6 +120,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep index 0a5b66d0d..77da07e10 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep @@ -102,6 +102,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep index 3abcd38d9..4660dad13 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep @@ -102,6 +102,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep index 4422b92ce..9b4909316 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep @@ -101,6 +101,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep index 80877a3a6..4ddbb788f 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep @@ -106,6 +106,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep index f03a60e35..eb9c23d2a 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep @@ -122,6 +122,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep index b0dd66efa..15aec00f7 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep @@ -125,6 +125,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep index bd5d33ca1..d6e4dae0d 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep @@ -174,6 +174,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep index 13b7ce063..2484e9e91 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep @@ -124,6 +124,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep index 5f44400fa..086c2f6dc 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep @@ -100,6 +100,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep index b9a979c1f..2a7132e7f 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep @@ -125,6 +125,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep index 0031ba7c0..14c778323 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep @@ -102,6 +102,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep index a19718f49..69cca29c2 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep @@ -101,6 +101,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { From 2c5306d4fb21001fa4613b0f885844dd45ed6129 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 2 Aug 2024 13:20:49 -0700 Subject: [PATCH 0157/1479] Updated nuget package --- .../Hosts/JobScheduler/ConsoleApp/JobSchedulerConsoleApp.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/ConsoleApp/JobSchedulerConsoleApp.csproj b/Service/GroupMembershipManagement/Hosts/JobScheduler/ConsoleApp/JobSchedulerConsoleApp.csproj index 713dc9338..c5b1fa1a1 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/ConsoleApp/JobSchedulerConsoleApp.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/ConsoleApp/JobSchedulerConsoleApp.csproj @@ -4,7 +4,7 @@ net8.0 - + From a18b7bf3e8b4e2d3680ab280dd98a69780448199 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 2 Aug 2024 14:43:34 -0700 Subject: [PATCH 0158/1479] Updated JobTrigger test --- .../JobTrigger/Services.Tests/GraphGroupRepositoryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/GraphGroupRepositoryTests.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/GraphGroupRepositoryTests.cs index 10c4e3891..bee62fc50 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/GraphGroupRepositoryTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/GraphGroupRepositoryTests.cs @@ -213,7 +213,7 @@ public async Task TestGroupExistsSucceedsFailsRetriesAsync() }; var graphServiceClient = CreateCustomGraphServiceClient(chaosHandlerOption); _graphGroupRepository = new GraphGroupRepository(graphServiceClient, _telemetryClient, _loggingRepository.Object); - var exception = await Assert.ThrowsExceptionAsync(() => _graphGroupRepository.GroupExists(Guid.NewGuid())); + var exception = await Assert.ThrowsExceptionAsync(() => _graphGroupRepository.GroupExists(Guid.NewGuid())); Assert.IsTrue(exception.Message.Contains("Too many retries performed")); } From f785712023a623add1b6a21c6b3fb7f8cbfecb35 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 2 Aug 2024 14:44:12 -0700 Subject: [PATCH 0159/1479] Updated eftools version --- .github/workflows/ci.yml | 2 +- yaml/build-release-package.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05a4d5591..b1b4e7910 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Use .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.412 + dotnet-version: 8.0.303 - name: dotnet build run: | diff --git a/yaml/build-release-package.yml b/yaml/build-release-package.yml index 4e635aa1e..46c73ea96 100644 --- a/yaml/build-release-package.yml +++ b/yaml/build-release-package.yml @@ -78,7 +78,7 @@ stages: inputs: command: custom custom: 'tool' - arguments: 'update --global dotnet-ef --version 6.0.22' + arguments: 'update --global dotnet-ef --version 8.0.7' - task: PowerShell@2 displayName: 'Create migrations bundle' From 6bbec193ee2f731b31e57847c98e0ee32aa26123 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 2 Aug 2024 15:34:04 -0700 Subject: [PATCH 0160/1479] Updated GOO test --- .../Services.Tests/OrchestratorTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/OrchestratorTests.cs index 4eef6b9cb..e0586b623 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Services.Tests/OrchestratorTests.cs @@ -415,6 +415,14 @@ public async Task TestMissingSchemasAsync() }) .ReturnsAsync(() => _isValid); + List filteredGroupIds = new List(); + _groupOwnershipObtainerService.Setup(x => x.FilterSyncJobsBySourceTypes(It.IsAny>(), It.IsAny>())) + .Callback, List>((requestedSourceTypes, syncJobs) => + { + filteredGroupIds = _realGroupOwnershipObtainerService.FilterSyncJobsBySourceTypes(requestedSourceTypes, syncJobs); + }). + Returns(() => filteredGroupIds); + var orchestratorFunction = new OrchestratorFunction(_configuration.Object); await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); From 6896d0db5efc87ec577f7c802d4078020b0fb8ea Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 5 Aug 2024 09:24:21 -0700 Subject: [PATCH 0161/1479] Removed deprecated package --- .../GraphUpdaterServiceTests.cs | 19 +++---------------- .../GroupUpdaterServiceTests.cs | 1 - .../Services.Tests/OrchestratorTests.cs | 3 --- .../NotifierServiceTests.cs | 14 ++------------ .../PersonEntity.cs | 11 +++++------ .../SqlMembershipObtainer.Entities.csproj | 1 - .../ChildEntitiesFilterFunction.cs | 2 +- .../ManagerOrgReaderFunction.cs | 2 +- .../Helpers/OrganizationCreator.cs | 6 +++--- .../OrganizationProcessorFunctionTests.cs | 2 +- .../Repositories.Mail/MailRepository.cs | 12 +++++------- .../SqlMembershipRepository.cs | 4 ++-- 12 files changed, 23 insertions(+), 54 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GraphUpdaterServiceTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GraphUpdaterServiceTests.cs index cff7d2d4a..7a9b6c367 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GraphUpdaterServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GraphUpdaterServiceTests.cs @@ -3,30 +3,17 @@ using DIConcreteTypes; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Graph; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; using Models; +using Models.Notifications; +using Models.ServiceBus; +using Moq; using Repositories.Contracts; using Repositories.Mocks; using Services.Tests.Mocks; using System; using System.Collections.Generic; -using System.Linq; -using System.Net.Sockets; using System.Threading.Tasks; -using Models.ServiceBus; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphGroups; -using Repositories.Logging; -using Repositories.EntityFramework.Contexts.Migrations; -using Services.Contracts; -using Microsoft.Graph.Models.Security; -using Microsoft.IdentityModel.Abstractions; -using System.Security.Cryptography; -using Models.Notifications; -using Repositories.ServiceBusQueue; -using Microsoft.Azure.Documents.SystemFunctions; namespace Services.Tests { diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GroupUpdaterServiceTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GroupUpdaterServiceTests.cs index 811664f51..7c00c9372 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GroupUpdaterServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GroupUpdaterServiceTests.cs @@ -3,7 +3,6 @@ using DIConcreteTypes; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.Documents.SystemFunctions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Moq; diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs index 21b7bf25f..9f6df7827 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs @@ -4,7 +4,6 @@ using Hosts.GraphUpdater; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.Documents.SystemFunctions; using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph.Models; using Microsoft.Identity.Client; @@ -16,7 +15,6 @@ using Newtonsoft.Json; using Repositories.Contracts; using Repositories.Mocks; -using Repositories.ServiceBusQueue; using Services.Contracts; using Services.Tests.Mocks; using System; @@ -25,7 +23,6 @@ using System.IO; using System.Linq; using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using ExecutionContext = Microsoft.Azure.WebJobs.ExecutionContext; diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs index 4c2f704f0..653bb1426 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Azure.Messaging.ServiceBus; using DIConcreteTypes; using Hosts.Notifier; using Microsoft.ApplicationInsights; @@ -9,6 +8,7 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Microsoft.Graph; using Microsoft.Kiota.Abstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; @@ -18,8 +18,8 @@ using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using Repositories.Localization; +using Repositories.Mail; using Repositories.RetryPolicyProvider; -using Repositories.ServiceBusQueue; using Services.Contracts.Notifications; using Services.Tests; using System; @@ -29,16 +29,6 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; -using DIConcreteTypes; -using Repositories.Logging; -using Models.Entities; -using Repositories.Mail; -using Microsoft.Graph; -using Microsoft.Azure.Documents.SystemFunctions; -using Microsoft.Graph.Me.SendMail; -using static Microsoft.Graph.Me.SendMail.SendMailRequestBuilder; -using System.Threading; -using Microsoft.Kiota.Abstractions; using System.Threading; using System.Threading.Tasks; diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/PersonEntity.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/PersonEntity.cs index 18dccc8f7..0b1e00ad6 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/PersonEntity.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/PersonEntity.cs @@ -1,13 +1,12 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.Cosmos.Table; using System; using System.Diagnostics.CodeAnalysis; namespace SqlMembershipObtainer.Entities { [ExcludeFromCodeCoverage] - public class PersonEntity : TableEntity + public class PersonEntity { public PersonEntity() { @@ -15,12 +14,12 @@ public PersonEntity() public PersonEntity(string reportsToPersonnelNbr, string personnelNumber) { - PartitionKey = reportsToPersonnelNbr; - RowKey = personnelNumber; + ReportsToPersonnelNbr = reportsToPersonnelNbr; + PersonnelNumber = personnelNumber; } - public string ReportsToPersonnelNbr { get { return PartitionKey; } } - public string PersonnelNumber { get { return RowKey; } } + public string ReportsToPersonnelNbr { get; set; } + public string PersonnelNumber { get; set; } public string Business { get; set; } public int Childcount { get; set; } public string Email { get; set; } diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/SqlMembershipObtainer.Entities.csproj b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/SqlMembershipObtainer.Entities.csproj index 14ca0790f..37af4b28d 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/SqlMembershipObtainer.Entities.csproj +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer.Common/SqlMembershipObtainer.Entities/SqlMembershipObtainer.Entities.csproj @@ -5,7 +5,6 @@ - diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ChildEntitiesFilter/ChildEntitiesFilterFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ChildEntitiesFilter/ChildEntitiesFilterFunction.cs index 6d66fe53e..499f25e48 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ChildEntitiesFilter/ChildEntitiesFilterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ChildEntitiesFilter/ChildEntitiesFilterFunction.cs @@ -34,7 +34,7 @@ public async Task FilterChildEntities([Activity await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(ChildEntitiesFilterFunction)} function completed", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); - var profiles = filteredEntities.Select(x => new GraphProfileInformation { PersonnelNumber = x.RowKey, Id = x.AzureObjectId }).Distinct().ToList(); + var profiles = filteredEntities.Select(x => new GraphProfileInformation { PersonnelNumber = x.PersonnelNumber, Id = x.AzureObjectId }).Distinct().ToList(); return new GraphProfileInformationResponse { diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ManagerOrgReader/ManagerOrgReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ManagerOrgReader/ManagerOrgReaderFunction.cs index d1f0a1f8e..84d704a0c 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ManagerOrgReader/ManagerOrgReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ManagerOrgReader/ManagerOrgReaderFunction.cs @@ -34,7 +34,7 @@ public async Task ReadUsersAsync([ActivityTrigg await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(ManagerOrgReaderFunction)} function completed", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); - var profiles = response.Select(x => new GraphProfileInformation { PersonnelNumber = x.RowKey, Id = x.AzureObjectId }).ToList(); + var profiles = response.Select(x => new GraphProfileInformation { PersonnelNumber = x.PersonnelNumber, Id = x.AzureObjectId }).ToList(); return new GraphProfileInformationResponse { diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/OrganizationCreator.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/OrganizationCreator.cs index 87f47cfc3..0c6449adf 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/OrganizationCreator.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/Helpers/OrganizationCreator.cs @@ -1,4 +1,4 @@ -// Copyright(c) Microsoft Corporation. +// Copyright(c) Microsoft Corporation. // Licensed under the MIT license. using SqlMembershipObtainer.Entities; using System; @@ -22,7 +22,7 @@ internal List GenerateOrganizationHierarchy(int maxLevel = 10 { foreach (var entity in level.Entities) { - var childCount = levels.Where(l => l.LevelId == (level.LevelId + 1)).SelectMany(e => e.Entities).Count(e => e.ReportsToPersonnelNbr == entity.RowKey); + var childCount = levels.Where(l => l.LevelId == (level.LevelId + 1)).SelectMany(e => e.Entities).Count(e => e.ReportsToPersonnelNbr == entity.PersonnelNumber); entity.Childcount = childCount; entity.HaschildrenInd = childCount > 0 ? "1" : "0"; } @@ -90,7 +90,7 @@ private List GenerateOrganizationHierarchyLevel(int currentLe foreach (var entity in level.Entities) { - var childLevels = GenerateOrganizationHierarchyLevel(currentLevel + 1, maxLevel, entity.RowKey); + var childLevels = GenerateOrganizationHierarchyLevel(currentLevel + 1, maxLevel, entity.PersonnelNumber); if (childLevels != null) levels.AddRange(childLevels.Where(x => x != null)); } diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrganizationProcessorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrganizationProcessorFunctionTests.cs index b7842ecf5..a4eb99725 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrganizationProcessorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrganizationProcessorFunctionTests.cs @@ -180,7 +180,7 @@ private void CustomizeOrganization(List organization) var isEven = index++ % 2 == 0; entity.CompanyCode = isEven ? "2" : "1"; entity.StandardTitle = isEven ? "PM" : "Engineer"; - entity.RowKey = Guid.NewGuid().ToString(); + entity.PersonnelNumber = Guid.NewGuid().ToString(); } } } diff --git a/Service/GroupMembershipManagement/Repositories.Mail/MailRepository.cs b/Service/GroupMembershipManagement/Repositories.Mail/MailRepository.cs index 605b57b88..74b2d6ac2 100644 --- a/Service/GroupMembershipManagement/Repositories.Mail/MailRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Mail/MailRepository.cs @@ -1,24 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using AdaptiveCards.Templating; using Microsoft.Graph; using Microsoft.Graph.Models; +using Microsoft.Kiota.Abstractions; using Models; using Models.AdaptiveCards; +using Polly.Wrap; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Security; -using System.Threading.Tasks; -using AdaptiveCards.Templating; using System.Text.RegularExpressions; -using System.Net.Http; -using Polly.Wrap; -using Microsoft.Kiota.Abstractions; -using Microsoft.Azure.Documents; -using System.Net; +using System.Threading.Tasks; namespace Repositories.Mail { diff --git a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs index a1a420315..aa080afd4 100644 --- a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs @@ -61,7 +61,7 @@ await retryPolicy.Execute(async () => { var response = new PersonEntity { - RowKey = reader.IsDBNull(id) ? null : reader.GetInt32(id).ToString(), + PersonnelNumber = reader.IsDBNull(id) ? null : reader.GetInt32(id).ToString(), AzureObjectId = reader.IsDBNull(azureObjectId) ? null : reader.GetString(azureObjectId) }; children.Add(response); @@ -175,7 +175,7 @@ await retryPolicy.Execute(async () => { var response = new PersonEntity { - RowKey = reader.IsDBNull(personnelNumber) ? null : reader.GetInt32(personnelNumber).ToString(), + PersonnelNumber = reader.IsDBNull(personnelNumber) ? null : reader.GetInt32(personnelNumber).ToString(), AzureObjectId = reader.IsDBNull(azureObjectId) ? null : reader.GetString(azureObjectId) }; filteredChildren.Add(response); From e34ee0cab3192ba2f50082b8b10023b763e1528c Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 5 Aug 2024 14:18:57 -0700 Subject: [PATCH 0162/1479] Updated delta logic --- .../GraphGroupDeltaReader.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupDeltaReader.cs b/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupDeltaReader.cs index 05d726944..7d1f91838 100644 --- a/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupDeltaReader.cs +++ b/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupDeltaReader.cs @@ -4,6 +4,7 @@ using Microsoft.Graph; using Microsoft.Graph.Models; using Microsoft.Kiota.Abstractions; +using Microsoft.Kiota.Abstractions.Serialization; using Models; using Repositories.Contracts; using System; @@ -162,15 +163,24 @@ private List ExtractDeltaMembers(Group group, bool includeMembersTo if (group != null && group.AdditionalData.TryGetValue("members@delta", out object membersJson)) { - var memberArray = JsonArray.Parse(membersJson.ToString()).AsArray(); - foreach (var member in memberArray) + var members = (membersJson as UntypedArray)?.GetValue(); + if (members == null) return users; + + foreach (UntypedObject memberObject in members) { - if (member["@odata.type"].ToString().Equals("#microsoft.graph.user", StringComparison.InvariantCultureIgnoreCase)) + var member = memberObject.GetValue(); + if (member == null) continue; + + var memberType = (member["@odata.type"] as UntypedString)?.GetValue(); + var memberId = (member["id"] as UntypedString)?.GetValue(); + if (memberType == null || memberId == null) continue; + + if (memberType.Equals("#microsoft.graph.user", StringComparison.InvariantCultureIgnoreCase)) { - if (member["@removed"] == null) - users.Add(new AzureADUser { ObjectId = Guid.Parse((string)member["id"]), MembershipAction = MembershipAction.Add }); + if (!member.ContainsKey("@removed")) + users.Add(new AzureADUser { ObjectId = Guid.Parse(memberId), MembershipAction = MembershipAction.Add }); else if (includeMembersToRemove) - users.Add(new AzureADUser { ObjectId = Guid.Parse((string)member["id"]), MembershipAction = MembershipAction.Remove }); + users.Add(new AzureADUser { ObjectId = Guid.Parse(memberId), MembershipAction = MembershipAction.Remove }); } } } From bf52048f0257c4f1f44585462944140c815754cc Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 5 Aug 2024 15:18:23 -0700 Subject: [PATCH 0163/1479] Removed DataFactory deprecated package --- .../DataFactoryRepository.cs | 53 +++++++++---------- .../Repositories.DataFactory.csproj | 2 +- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/Service/GroupMembershipManagement/Repositories.DataFactory/DataFactoryRepository.cs b/Service/GroupMembershipManagement/Repositories.DataFactory/DataFactoryRepository.cs index 874de152c..67415fcf9 100644 --- a/Service/GroupMembershipManagement/Repositories.DataFactory/DataFactoryRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.DataFactory/DataFactoryRepository.cs @@ -1,14 +1,15 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. +using Azure.Core; using Azure.Identity; -using Microsoft.Azure.Management.DataFactory; -using Microsoft.Azure.Management.DataFactory.Models; -using Microsoft.Identity.Client; -using Microsoft.Rest; +using Azure.ResourceManager; +using Azure.ResourceManager.DataFactory; +using Azure.ResourceManager.DataFactory.Models; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Repositories.DataFactory @@ -19,6 +20,8 @@ public class DataFactoryRepository : IDataFactoryRepository private readonly string _dataFactory = null; private readonly string _subscriptionId = null; private readonly string _resourceGroup = null; + private readonly ArmClient _client = null; + private readonly ResourceIdentifier _dataFactoryResourceId = null; public DataFactoryRepository(IDataFactorySecret dataFactorySecrets) { @@ -26,47 +29,41 @@ public DataFactoryRepository(IDataFactorySecret dataFact _dataFactory = dataFactorySecrets.DataFactoryName; _subscriptionId = dataFactorySecrets.SubscriptionId; _resourceGroup = dataFactorySecrets.ResourceGroup; + _client = new ArmClient(new DefaultAzureCredential()); + _dataFactoryResourceId = DataFactoryResource.CreateResourceIdentifier(_subscriptionId, _resourceGroup, _dataFactory); } public async Task GetMostRecentSucceededRunIdAsync() { var pipelineResponse = await GetDataFactoryPipelineRunsAsync(); - return pipelineResponse?.Value?.Count > 0 ? pipelineResponse?.Value?[0]?.RunId : null; + return pipelineResponse.Count > 0 ? pipelineResponse[0].RunId?.ToString() : null; } public async Task<(string latest, string previous)> GetTwoRecentSucceededRunIdsAsync() { var pipelineResponse = await GetDataFactoryPipelineRunsAsync(); - return (pipelineResponse?.Value?.Count >= 2) ? (pipelineResponse?.Value?[0]?.RunId, pipelineResponse?.Value?[1]?.RunId) : (null, null); + return (pipelineResponse?.Count >= 2) ? (pipelineResponse[0]?.RunId?.ToString(), pipelineResponse[1]?.RunId?.ToString()) : (null, null); } - private async Task GetDataFactoryPipelineRunsAsync() + private async Task> GetDataFactoryPipelineRunsAsync() { - var credentials = await GetCredentialsAsync(); - var client = new DataFactoryManagementClient(credentials) + var dataFactory = _client.GetDataFactoryResource(_dataFactoryResourceId); + RunFilterContent content = new RunFilterContent(DateTime.UtcNow.AddMonths(-1), DateTime.UtcNow) { - SubscriptionId = _subscriptionId + Filters = + { + new RunQueryFilter(RunQueryFilterOperand.PipelineName, RunQueryFilterOperator.EqualsValue, new string[] { _pipeline }), + new RunQueryFilter(RunQueryFilterOperand.Status, RunQueryFilterOperator.EqualsValue, new string[] { "Succeeded" }) + } }; - var pipeline = new RunQueryFilter("PipelineName", "Equals", new List { _pipeline }); - var status = new RunQueryFilter("Status", "Equals", new List { "Succeeded" }); - var pipelineRuns = new RunQueryOrderBy("RunEnd", "DESC"); - var before = DateTime.UtcNow; - var after = before.AddMonths(-1); - var param = new RunFilterParameters(after, before, null, new List { pipeline, status }, new List { pipelineRuns }); - var pipelineResponse = await client.PipelineRuns.QueryByFactoryAsync( - _resourceGroup, - _dataFactory, param); - return pipelineResponse; - } + var pipelineRuns = new List(); + await foreach (DataFactoryPipelineRunInfo item in dataFactory.GetPipelineRunsAsync(content)) + { + pipelineRuns.Add(item); + } - private async Task GetCredentialsAsync() - { - var defaultAzureCredential = new DefaultAzureCredential(); - var tokenRequestContext = new Azure.Core.TokenRequestContext(new[] { "https://management.azure.com/.default" }); - var accessToken = await defaultAzureCredential.GetTokenAsync(tokenRequestContext); - var tokenCredentials = new TokenCredentials(accessToken.Token); - return tokenCredentials; + return pipelineRuns.OrderByDescending(x => x.RunEndOn).ToList(); } } } diff --git a/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj b/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj index 29acc6098..c22a0eeeb 100644 --- a/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj +++ b/Service/GroupMembershipManagement/Repositories.DataFactory/Repositories.DataFactory.csproj @@ -6,7 +6,7 @@ - + From 87653432db324d6eb3fb8e68469f66eae0f99692 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Tue, 6 Aug 2024 10:20:39 -0700 Subject: [PATCH 0164/1479] Added missing Service Bus local settings to WebAPI. --- .../Hosts/WebApi/WebApi/appsettings.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json index 169d38875..9f299d75c 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json @@ -19,6 +19,12 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform "keyVaultName": "gmm-prereqs-", "keyVaultTenantId": "", "tenantId": "" + }, + "ServiceBus": { + "MembershipAggregatorQueue": "", + "MembershipUpdatersTopic": "", + "ServiceBusFQN": "", + "SyncJobTopic": "" } }, "ADF": { From 6ceffd3ca815991ae05dca19cdfccada3cc50f2f Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 7 Aug 2024 14:37:05 -0700 Subject: [PATCH 0165/1479] Renamed the Notifier storage account for consistency --- .../Hosts/Notifier/Infrastructure/data/template.bicep | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/data/template.bicep index 104bad45d..0ed650bde 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/data/template.bicep @@ -20,10 +20,10 @@ param storageAccountSku string = 'Standard_LRS' param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' -var prodStorageAccountName = substring('ntf${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) +var prodStorageAccountName = substring('n${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) module notifierStorageAccountProd 'storageAccount.bicep' = { - name: 'ntfProdstorageAccountTemplate' + name: 'nProdstorageAccountTemplate' params: { name: prodStorageAccountName sku: storageAccountSku From d02dd9a37d437dc1cc8b70ab53e81d67129b70a6 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 7 Aug 2024 16:37:25 -0700 Subject: [PATCH 0166/1479] Renamed the Sql Membership Obtainer storage account for consistency --- .../SqlMembershipObtainer/Infrastructure/data/template.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/data/template.bicep index 6fd4177e8..d6c31ba4c 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/data/template.bicep @@ -23,7 +23,7 @@ param storageAccountSku string = 'Standard_LRS' /* This creates the internal storage accounts used by SqlMemberhipObtainer function */ var dataKeyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' -var prodStorageAccountName = substring('sqlmo${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) +var prodStorageAccountName = substring('smo${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) module smoStorageAccountProd 'storageAccount.bicep' = { name: 'smoProdstorageAccountTemplate' From 4c2ec3f504189aba495987b1cc585de01e7ef65f Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 7 Aug 2024 14:23:11 -0700 Subject: [PATCH 0167/1479] Update nitpicker.yaml to target features/int branch --- .config/merlinbot/nitpicker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/merlinbot/nitpicker.yaml b/.config/merlinbot/nitpicker.yaml index 266e63448..16018fe28 100644 --- a/.config/merlinbot/nitpicker.yaml +++ b/.config/merlinbot/nitpicker.yaml @@ -1,5 +1,5 @@ branches: - - name: develop + - name: features/int comments: - markdown: | # Is your code accessible? From b326b1baa627f834acb8e378ec6fff51b826ada6 Mon Sep 17 00:00:00 2001 From: abgonz Date: Tue, 6 Aug 2024 11:11:53 -0700 Subject: [PATCH 0168/1479] Add tooltip to requestor field, swap for people picker component, rename getJobOwnerFilterSuggestions to more generic getPeoplePickerSuggestions --- .../Services.WebApi/GetJobDetailsHandler.cs | 5 +- .../WebApi.Tests/JobDetailsControllerTests.cs | 2 +- UI/web-app/src/apis/GraphApi.ts | 2 +- UI/web-app/src/apis/IGraphApi.ts | 2 +- .../HRQuerySource/HRQuerySource.base.tsx | 8 +- .../JobsListFilter/JobsListFilter.base.tsx | 8 +- .../RunConfiguration.base.tsx | 77 +++++++++++++++---- .../RunConfiguration.styles.ts | 18 +++++ .../RunConfiguration.types.ts | 3 + .../src/services/localization/IStrings.ts | 1 + .../i18n/locales/en/translations.ts | 1 + .../i18n/locales/es/translations.ts | 1 + UI/web-app/src/store/jobs.api.tsx | 8 +- UI/web-app/src/store/jobs.slice.tsx | 6 +- .../src/store/manageMembership.slice.tsx | 2 +- 15 files changed, 108 insertions(+), 36 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetJobDetailsHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetJobDetailsHandler.cs index 1eb71ca5b..58ac9f6cb 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetJobDetailsHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetJobDetailsHandler.cs @@ -59,15 +59,12 @@ await _loggingRepository.LogMessageAsync(new LogMessage }); } - bool isRequestorOwner = await _graphGroupRepository.IsEmailRecipientOwnerOfGroupAsync(job.Requestor, job.TargetOfficeGroupId); - string requestor = isRequestorOwner ? job.Requestor : job.Requestor + " (Not an Owner)"; - var dto = new SyncJobDetailsDTO ( startDate: job.StartDate, lastSuccessfulStartTime: job.LastSuccessfulStartTime, source: job.Query, - requestor: requestor, + requestor: job.Requestor, thresholdViolations: job.ThresholdViolations, thresholdPercentageForAdditions: job.ThresholdPercentageForAdditions, thresholdPercentageForRemovals: job.ThresholdPercentageForRemovals, diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs index a14389b56..e3d8c8619 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs @@ -173,7 +173,7 @@ public async Task GetJobDetailsTestRequestorNotAnOwnerAsync(string role) var job = result.Value as SyncJobDetails; Assert.IsNotNull(job); - Assert.AreEqual("example@microsoft.com (Not an Owner)", job.Requestor); + Assert.AreEqual("example@microsoft.com", job.Requestor); } [TestMethod] diff --git a/UI/web-app/src/apis/GraphApi.ts b/UI/web-app/src/apis/GraphApi.ts index 6df5315e8..cd250b614 100644 --- a/UI/web-app/src/apis/GraphApi.ts +++ b/UI/web-app/src/apis/GraphApi.ts @@ -23,7 +23,7 @@ export class GraphApi extends ApiBase implements IGraphApi { } - public async getJobOwnerFilterSuggestions(displayName: string, mail: string): Promise { + public async getPeoplePickerSuggestions(displayName: string, mail: string): Promise { const response = await this.httpClient.get>(`/users`, { params: { $select: 'displayName,mail,id', diff --git a/UI/web-app/src/apis/IGraphApi.ts b/UI/web-app/src/apis/IGraphApi.ts index 004430831..61420df31 100644 --- a/UI/web-app/src/apis/IGraphApi.ts +++ b/UI/web-app/src/apis/IGraphApi.ts @@ -8,5 +8,5 @@ export interface IGraphApi { getUser(input: string): Promise; getPreferredLanguage(user: User): Promise; getProfilePhotoUrl(user: User): Promise; - getJobOwnerFilterSuggestions(displayName: string, mail: string): Promise; + getPeoplePickerSuggestions(displayName: string, mail: string): Promise; } \ No newline at end of file diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 41449f459..1cc8d1548 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -16,9 +16,9 @@ import { HRSourcePartSource } from '../../models/HRSourcePart'; import { AppDispatch } from '../../store'; import { useDispatch, useSelector } from 'react-redux'; import { fetchOrgLeaderDetails, fetchOrgLeaderDetailsUsingId } from '../../store/orgLeaderDetails.api'; -import { getJobOwnerFilterSuggestions } from '../../store/jobs.api'; +import { getPeoplePickerSuggestions } from '../../store/jobs.api'; import { updateOrgLeaderDetails, selectOrgLeaderDetails, selectObjectIdEmployeeIdMapping } from '../../store/orgLeaderDetails.slice'; -import { selectJobOwnerFilterSuggestions } from '../../store/jobs.slice'; +import { selectPeoplePickerSuggestions } from '../../store/jobs.slice'; import { fetchDefaultSqlMembershipSourceAttributes } from '../../store/sqlMembershipSources.api'; import { fetchAttributeValues } from '../../store/sqlMembershipSources.api'; import { selectAttributes, selectSource, selectAttributeValues, setAttributeValues } from '../../store/sqlMembershipSources.slice'; @@ -53,7 +53,7 @@ export const HRQuerySourceBase: React.FunctionComponent = (p const dispatch = useDispatch(); const orgLeaderDetails = useSelector(selectOrgLeaderDetails); const objectIdEmployeeIdMapping = useSelector(selectObjectIdEmployeeIdMapping); - const ownerPickerSuggestions = useSelector(selectJobOwnerFilterSuggestions); + const ownerPickerSuggestions = useSelector(selectPeoplePickerSuggestions); const isJobWriter = useSelector(selectIsJobWriter); const [isDragAndDropEnabled, setIsDragAndDropEnabled] = useState(false); const [isDisabled, setIsDisabled] = useState(true); @@ -298,7 +298,7 @@ const checkType = (value: string, type: string | undefined): string => { const handleOrgLeaderInputChange = (input: string): string => { setIncludeOrg(true); setOrgErrorMessage(''); - dispatch(getJobOwnerFilterSuggestions({displayName: input, alias: input})) + dispatch(getPeoplePickerSuggestions({displayName: input, alias: input})) return input; } diff --git a/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx b/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx index 5cc7d1a6f..167c03a50 100644 --- a/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx +++ b/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx @@ -23,8 +23,8 @@ import { IPersonaProps } from '@fluentui/react/lib/Persona'; import { useDispatch, useSelector } from 'react-redux'; import { selectIsJobTenantWriter, selectIsSubmissionReviewer } from '../../store/roles.slice'; import { AppDispatch } from '../../store'; -import { selectJobOwnerFilterSuggestions } from '../../store/jobs.slice'; -import { getJobOwnerFilterSuggestions } from '../../store/jobs.api'; +import { selectPeoplePickerSuggestions } from '../../store/jobs.slice'; +import { getPeoplePickerSuggestions } from '../../store/jobs.api'; import { setFilterActionRequired, setFilterStatus, @@ -135,7 +135,7 @@ export const JobsListFilterBase: React.FunctionComponent = const [selectedOwners, setSelectedOwners] = useState([]); const isTenantJobWriter = useSelector(selectIsJobTenantWriter); const isSubmissionReviewer = useSelector(selectIsSubmissionReviewer); - const ownerPickerSuggestions = useSelector(selectJobOwnerFilterSuggestions); + const ownerPickerSuggestions = useSelector(selectPeoplePickerSuggestions); const dispatch = useDispatch(); useEffect(() => { @@ -209,7 +209,7 @@ export const JobsListFilterBase: React.FunctionComponent = const handleOwnersInputChanged = (input: string): string => { if (input.trim()) { - dispatch(getJobOwnerFilterSuggestions({displayName: input, alias: input})) + dispatch(getPeoplePickerSuggestions({displayName: input, alias: input})) } return input; } diff --git a/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx b/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx index 416ab4ca1..794a32fcc 100644 --- a/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx +++ b/UI/web-app/src/components/RunConfiguration/RunConfiguration.base.tsx @@ -7,8 +7,13 @@ import { classNamesFunction, useTheme, ChoiceGroup, IChoiceGroupOption, DatePicker, Dropdown, Checkbox, - TextField, MessageBar, MessageBarType, + Label, + TooltipHost, + IconButton, + NormalPeoplePicker, + IPersonaProps, + DirectionalHint, } from '@fluentui/react'; import { IRunConfigurationProps, @@ -35,11 +40,13 @@ import { setShowIncreaseDropdown, setStartDateOption, setUseThresholdLimits, - manageMembershipRequestor + manageMembershipRequestor, } from '../../store/manageMembership.slice'; import { AppDispatch } from '../../store'; import { useDispatch, useSelector } from 'react-redux'; import { selectIsJobTenantWriter, selectIsJobWriter } from '../../store/roles.slice'; +import { getPeoplePickerSuggestions } from '../../store/jobs.api'; +import { selectPeoplePickerSuggestions } from '../../store/jobs.slice'; const getClassNames = classNamesFunction< IRunConfigurationStyleProps, @@ -56,10 +63,24 @@ export const RunConfigurationBase: React.FunctionComponent { + if (!requestor) return []; + return [{ + key: requestor, + text: requestor, + secondaryText: requestor, + }]; + }; + const dispatch = useDispatch(); const period: number = useSelector(manageMembershipPeriod); const startDate: string = useSelector(manageMembershipStartDate); const requestor: string = useSelector(manageMembershipRequestor); + const requestorPickerSuggestions = useSelector(selectPeoplePickerSuggestions); + const requestorPersona = mapRequestorToPersonaProps(requestor); + const [requestorPersonaState, setRequestorPersonaState] = React.useState(mapRequestorToPersonaProps(requestor)); + const thresholdPercentageForAdditions: number = useSelector(manageMembershipThresholdPercentageForAdditions); const thresholdPercentageForRemovals: number = useSelector(manageMembershipThresholdPercentageForRemovals); @@ -97,22 +118,52 @@ export const RunConfigurationBase: React.FunctionComponent ({ key: `${(i + 1) * 10}`, text: `${(i + 1) * 10}%` })); const decreaseOptions = Array.from({ length: 11 }, (_, i) => ({ key: `${i * 5}`, text: `${i * 5}%` })); - const handleRequestorChange = (ev: React.FormEvent, newValue?: string) => { - dispatch(setNewJobRequestor(newValue || '')); + const getPickerSuggestions = async ( + text: string + ): Promise => { + return text && requestorPickerSuggestions ? requestorPickerSuggestions : []; + }; + + const handleRequestorInputChange = (input: string): string => { + if (input.trim() !== "") { + dispatch(getPeoplePickerSuggestions({ displayName: input, alias: input })); + } + return input; + }; + + const handleRequestorChange = (items?: IPersonaProps[] | undefined) => { + if (items && items.length > 0) { + dispatch(setNewJobRequestor(items[0].secondaryText || items[0].text || '' )); + } else { + dispatch(setNewJobRequestor('')); + } + setRequestorPersonaState(items || []); }; return (
{isJobTenantWriter && - +
+
+ + + + +
+ +
} ( } ); -export const getJobOwnerFilterSuggestions = createAsyncThunk( - 'filter/getJobOwnerFilterSuggestions', +export const getPeoplePickerSuggestions = createAsyncThunk( + 'filter/getPeoplePickerSuggestions', async (input, { extra }) => { const { graphApi } = extra.apis; try { - return await graphApi.getJobOwnerFilterSuggestions(input.displayName, input.alias); + return await graphApi.getPeoplePickerSuggestions(input.displayName, input.alias); } catch (error) { - throw new Error('Failed to call getJobOwnerFilterSuggestions endpoint'); + throw new Error('Failed to call getPeoplePickerSuggestions endpoint'); } } ); diff --git a/UI/web-app/src/store/jobs.slice.tsx b/UI/web-app/src/store/jobs.slice.tsx index d0745e226..130ef1d5c 100644 --- a/UI/web-app/src/store/jobs.slice.tsx +++ b/UI/web-app/src/store/jobs.slice.tsx @@ -4,7 +4,7 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { fetchJobDetails, patchJobDetails, removeGMM } from './jobDetails.api'; -import { fetchJobs, postJob, getJobOwnerFilterSuggestions } from './jobs.api'; +import { fetchJobs, postJob, getPeoplePickerSuggestions } from './jobs.api'; import type { RootState } from './store'; import { type Job } from '../models/Job'; import { type JobDetails } from '../models/JobDetails'; @@ -115,7 +115,7 @@ export const jobsSlice = createSlice({ }); // jobOwnerFilterSuggestions - builder.addCase(getJobOwnerFilterSuggestions.fulfilled, (state, {payload}: PayloadAction) => { + builder.addCase(getPeoplePickerSuggestions.fulfilled, (state, {payload}: PayloadAction) => { state.jobOwnerFilterSuggestions = payload; }); @@ -161,7 +161,7 @@ export const selectPatchJobDetailsError = (state: RootState) => state.jobs.patch export const selectPostJobLoading = (state: RootState) => state.jobs.postJobLoading; export const selectPostJobError = (state: RootState) => state.jobs.postJobError; -export const selectJobOwnerFilterSuggestions = (state: RootState) => state.jobs.jobOwnerFilterSuggestions; +export const selectPeoplePickerSuggestions = (state: RootState) => state.jobs.jobOwnerFilterSuggestions; export const selectRemoveGMMLoading = (state: RootState) => state.jobs.removeGMMLoading; export const selectRemoveGMMResponse = (state: RootState) => state.jobs.removeGMMResponse; diff --git a/UI/web-app/src/store/manageMembership.slice.tsx b/UI/web-app/src/store/manageMembership.slice.tsx index 8460a7724..da0a5452d 100644 --- a/UI/web-app/src/store/manageMembership.slice.tsx +++ b/UI/web-app/src/store/manageMembership.slice.tsx @@ -65,7 +65,7 @@ const initialState: ManageMembershipState = { compositeQuery: {} as SyncJobQuery, advancedViewQuery: '', sourceParts: [], - isEditingExistingJob: false + isEditingExistingJob: false, }; const manageMembershipSlice = createSlice({ From 29b35dac0b933ea8a9eb3e606f9205621a829f04 Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 7 Aug 2024 14:11:55 -0700 Subject: [PATCH 0169/1479] Set up new General Settings Admin role, added General tab in Admin Center --- .../Hosts/WebApi/Documentation/WebApiSetup.md | 6 ++- .../WebApi/Scripts/Set-AppRolesIfNeeded.ps1 | 8 ++++ .../WebApi/WebApi.Models/DTOs/RolesObject.cs | 1 + .../Hosts/WebApi/WebApi.Models/Roles.cs | 1 + .../WebApi.Tests/RolesControllerTests.cs | 2 + .../Controllers/v1/Roles/RolesController.cs | 4 +- .../components/AppHeader/AppHeader.base.tsx | 7 +-- .../GeneralSetting/GeneralSetting.base.tsx | 36 +++++++++++++++ .../GeneralSetting/GeneralSetting.styles.ts | 41 +++++++++++++++++ .../GeneralSetting/GeneralSetting.ts | 21 +++++++++ .../GeneralSetting/GeneralSetting.types.ts | 24 ++++++++++ .../src/components/GeneralSetting/index.ts | 2 + .../pages/AdminConfig/AdminConfig.base.tsx | 10 ++++- .../pages/AdminConfig/AdminConfig.types.ts | 6 +++ .../pages/AdminConfig/AdminConfig.view.tsx | 44 ++++++++++++++++++- .../src/services/localization/IStrings.ts | 9 +++- .../i18n/locales/en/translations.ts | 7 +++ .../i18n/locales/es/translations.ts | 7 +++ UI/web-app/src/store/roles.slice.tsx | 14 +++++- 19 files changed, 236 insertions(+), 14 deletions(-) create mode 100644 UI/web-app/src/components/GeneralSetting/GeneralSetting.base.tsx create mode 100644 UI/web-app/src/components/GeneralSetting/GeneralSetting.styles.ts create mode 100644 UI/web-app/src/components/GeneralSetting/GeneralSetting.ts create mode 100644 UI/web-app/src/components/GeneralSetting/GeneralSetting.types.ts create mode 100644 UI/web-app/src/components/GeneralSetting/index.ts diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md b/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md index 9a7f5fcc4..ef35fa233 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md @@ -69,11 +69,13 @@ The roles are: - _Note: for Submission Reviewers to be able to see all pending requests, they need to also have the Job Tenant Reader role_ - Hyperlink Administrator - - Users with this role can **add, update, and remove** custom urls from the Admin Settings page. + - Users with this role can **add, update, and remove** custom urls from the Admin Center page. - Custom Membership Provider Administrator - - Users with this role can **add, update, and remove** custom field names from the Admin Settings page. + - Users with this role can **add, update, and remove** custom field names from the Admin Center page. +- General Settings Administrator + - Users with this role can **update** general settings from the Admin Center page. ## Add a role to a group diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 b/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 index 0b6bef7e2..5d4c874f9 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Scripts/Set-AppRolesIfNeeded.ps1 @@ -128,6 +128,14 @@ function Set-AppRolesIfNeeded { IsEnabled = $True AllowedMemberTypes = @($memberTypes) }, + @{ + DisplayName = "General Settings Administrator" + Description = "Can update general settings." + Value = "GeneralSettings.ReadWrite.All" + Id = [Guid]::NewGuid().ToString() + IsEnabled = $True + AllowedMemberTypes = @($memberTypes) + }, @{ DisplayName = "Reset Administrator" Description = "Can reset or stop GMM." diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs index 2f26d7916..aca9c3dd4 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/RolesObject.cs @@ -20,5 +20,6 @@ public RolesObject() public bool IsHyperlinkAdministrator { get; set; } public bool IsCustomMembershipProviderAdministrator { get; set; } public bool IsOperationsResetAdministrator { get; set; } + public bool IsGeneralSettingsAdministrator { get; set; } } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs index 836db5b4d..262e2cda9 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Roles.cs @@ -16,5 +16,6 @@ public class Roles public const string HYPERLINK_ADMINISTRATOR = "Hyperlink.ReadWrite.All"; public const string CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR = "CustomSource.ReadWrite.All"; public const string RESET_ADMINISTRATOR = "Operations.Reset"; + public const string GENERAL_SETTINGS_ADMINISTRATOR = "GeneralSettings.ReadWrite.All"; } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs index 8382c1db2..636876d6e 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/RolesControllerTests.cs @@ -37,6 +37,7 @@ public void GetAllRolesStatus_ForVariousUsers() new Claim(ClaimTypes.Role, Roles.HYPERLINK_ADMINISTRATOR), new Claim(ClaimTypes.Role, Roles.CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR), new Claim(ClaimTypes.Role, Roles.RESET_ADMINISTRATOR), + new Claim(ClaimTypes.Role, Roles.GENERAL_SETTINGS_ADMINISTRATOR) }; _rolesController.ControllerContext = CreateControllerContext(claims); @@ -58,6 +59,7 @@ public void GetAllRolesStatus_ForVariousUsers() Assert.IsTrue(rolesStatuses.IsHyperlinkAdministrator); Assert.IsTrue(rolesStatuses.IsCustomMembershipProviderAdministrator); Assert.IsTrue(rolesStatuses.IsOperationsResetAdministrator); + Assert.IsTrue(rolesStatuses.IsGeneralSettingsAdministrator); } private ControllerContext CreateControllerContext(List claims) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs index 63f0691c1..636612c35 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Roles/RolesController.cs @@ -39,6 +39,7 @@ public RolesController() var isHyperlinkAdministrator = User.IsInRole(Models.Roles.HYPERLINK_ADMINISTRATOR); var isCustomMembershipProviderAdministrator = User.IsInRole(Models.Roles.CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR); var isOperationsResetAdministrator = User.IsInRole(Models.Roles.RESET_ADMINISTRATOR); + var isGeneralSettingsAdministrator = User.IsInRole(Models.Roles.GENERAL_SETTINGS_ADMINISTRATOR); var roleStatus = new Models.DTOs.RolesObject { @@ -51,7 +52,8 @@ public RolesController() IsSubmissionReviewer = isSubmissionReviewer, IsHyperlinkAdministrator = isHyperlinkAdministrator, IsCustomMembershipProviderAdministrator = isCustomMembershipProviderAdministrator, - IsOperationsResetAdministrator = isOperationsResetAdministrator + IsOperationsResetAdministrator = isOperationsResetAdministrator, + IsGeneralSettingsAdministrator = isGeneralSettingsAdministrator }; return Ok(roleStatus); diff --git a/UI/web-app/src/components/AppHeader/AppHeader.base.tsx b/UI/web-app/src/components/AppHeader/AppHeader.base.tsx index 8aed9763c..0f7d39b5a 100644 --- a/UI/web-app/src/components/AppHeader/AppHeader.base.tsx +++ b/UI/web-app/src/components/AppHeader/AppHeader.base.tsx @@ -15,7 +15,7 @@ import { selectProfilePhoto } from '../../store/profile.slice'; import { getProfilePhoto } from '../../store/profile.api'; import logo from '../../logo.svg'; import { useStrings } from '../../store/hooks'; -import { selectIsCustomMembershipProviderAdministrator, selectIsHyperlinkAdministrator, selectIsOperationsResetAdministrator} from '../../store/roles.slice'; +import { selectHasAdminCenterPermissions } from '../../store/roles.slice'; const getClassNames = classNamesFunction< IAppHeaderStyleProps, @@ -38,10 +38,7 @@ export const AppHeaderBase: React.FunctionComponent = ( const dispatch = useDispatch(); const profilePhoto = useSelector(selectProfilePhoto); - const isHyperlinkAdmin = useSelector(selectIsHyperlinkAdministrator); - const isCustomMembershipProviderAdmin = useSelector(selectIsCustomMembershipProviderAdministrator); - const isOperationsResetAdministrator = useSelector(selectIsOperationsResetAdministrator); - const canViewSettings = isHyperlinkAdmin || isCustomMembershipProviderAdmin || isOperationsResetAdministrator; + const canViewSettings = useSelector(selectHasAdminCenterPermissions); useEffect(() => { if (!profilePhoto) { diff --git a/UI/web-app/src/components/GeneralSetting/GeneralSetting.base.tsx b/UI/web-app/src/components/GeneralSetting/GeneralSetting.base.tsx new file mode 100644 index 000000000..011f4d1ed --- /dev/null +++ b/UI/web-app/src/components/GeneralSetting/GeneralSetting.base.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import { classNamesFunction, Toggle } from '@fluentui/react'; +import { useTheme } from '@fluentui/react/lib/Theme'; +import type { GeneralSettingProps, GeneralSettingStyles, GeneralSettingStyleProps } from './GeneralSetting.types'; +import { useStrings } from '../../store/hooks'; + +export const getClassNames = classNamesFunction(); + +export const GeneralSettingBase: React.FunctionComponent = (props: GeneralSettingProps) => { + const { title, description, className, styles} = props; + const classNames = getClassNames(styles, { + className, + theme: useTheme(), + }); + const strings = useStrings(); + + const [isReviewingOwnSubmissionsEnabled, setIsReviewingOwnSubmissionsEnabled] = useState(true); + + const handleSubmissionReviewerSettingChange = (ev: React.MouseEvent, checked?: boolean) => { + const newStatus = isReviewingOwnSubmissionsEnabled ? false : true; + setIsReviewingOwnSubmissionsEnabled(newStatus); + }; + + return ( +
+
{title}
+
{description}
+ +
+ ); +}; diff --git a/UI/web-app/src/components/GeneralSetting/GeneralSetting.styles.ts b/UI/web-app/src/components/GeneralSetting/GeneralSetting.styles.ts new file mode 100644 index 000000000..94c5c6fef --- /dev/null +++ b/UI/web-app/src/components/GeneralSetting/GeneralSetting.styles.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import type { GeneralSettingStyleProps, GeneralSettingStyles } from './GeneralSetting.types'; + +export const getStyles = (props: GeneralSettingStyleProps): GeneralSettingStyles => { + const { className, theme } = props; + + return { + root: [ + { + padding: '0px 14px', + display: 'flex', + alignItems: 'flex-start', + gap: '15px', + }, + className, + ], + card: { + borderRadius: 10, + marginBottom: 10, + backgroundColor: theme.palette.white, + margin: 10, + outline: `1px solid ${theme.palette.neutralQuaternary}`, + width: '649px', + padding: 24, + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '4px', + }, + title: { + fontWeight: 600, + fontSize: 16, + }, + description: { + fontSize: 14, + fontWeight: 400, + }, + }; +}; \ No newline at end of file diff --git a/UI/web-app/src/components/GeneralSetting/GeneralSetting.ts b/UI/web-app/src/components/GeneralSetting/GeneralSetting.ts new file mode 100644 index 000000000..b42d0c9ce --- /dev/null +++ b/UI/web-app/src/components/GeneralSetting/GeneralSetting.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { styled } from '@fluentui/react'; +import type * as React from 'react'; + +import { GeneralSettingBase } from './GeneralSetting.base'; +import { getStyles } from './GeneralSetting.styles'; +import { + type GeneralSettingProps, + type GeneralSettingStyleProps, + type GeneralSettingStyles, +} from './GeneralSetting.types'; + +export const GeneralSetting: React.FunctionComponent = styled< + GeneralSettingProps, + GeneralSettingStyleProps, + GeneralSettingStyles +>(GeneralSettingBase, getStyles, undefined, { + scope: 'GeneralSetting', +}); \ No newline at end of file diff --git a/UI/web-app/src/components/GeneralSetting/GeneralSetting.types.ts b/UI/web-app/src/components/GeneralSetting/GeneralSetting.types.ts new file mode 100644 index 000000000..053621d28 --- /dev/null +++ b/UI/web-app/src/components/GeneralSetting/GeneralSetting.types.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { type IStyle, type IStyleFunctionOrObject, type ITheme } from '@fluentui/react'; +import type React from 'react'; + +export type GeneralSettingStyles = { + root: IStyle; + card: IStyle; + title: IStyle; + description: IStyle; +}; + +export type GeneralSettingStyleProps = { + className?: string; + theme: ITheme; +}; + +export type GeneralSettingProps = React.AllHTMLAttributes & { + className?: string; + styles?: IStyleFunctionOrObject; + title: string; + description: string; +}; diff --git a/UI/web-app/src/components/GeneralSetting/index.ts b/UI/web-app/src/components/GeneralSetting/index.ts new file mode 100644 index 000000000..4770a781e --- /dev/null +++ b/UI/web-app/src/components/GeneralSetting/index.ts @@ -0,0 +1,2 @@ +export * from './GeneralSetting'; +export * from './GeneralSetting.types'; \ No newline at end of file diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx index 648e1d132..b3555bd58 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx @@ -19,7 +19,12 @@ import { setPagingBarVisible } from '../../store/pagingBar.slice'; import { selectSource, selectAttributes, selectIsSourceSaving, selectAreAttributesSaving, setSource, setAttributes } from '../../store/sqlMembershipSources.slice'; import { SqlMembershipAttribute, SqlMembershipSource } from '../../models'; import { patchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceCustomLabel } from '../../store/sqlMembershipSources.api'; -import { selectIsCustomMembershipProviderAdministrator, selectIsHyperlinkAdministrator, selectIsOperationsResetAdministrator} from '../../store/roles.slice'; +import { + selectIsCustomMembershipProviderAdministrator, + selectIsHyperlinkAdministrator, + selectIsOperationsResetAdministrator, + selectIsGeneralSettingsAdministrator, +} from '../../store/roles.slice'; export const AdminConfigBase: React.FunctionComponent = (props: AdminConfigProps) => { @@ -41,6 +46,8 @@ export const AdminConfigBase: React.FunctionComponent = (props const isHyperlinkAdmin = useSelector(selectIsHyperlinkAdministrator); const isCustomMembershipProviderAdmin = useSelector(selectIsCustomMembershipProviderAdministrator); const isOperationsResetAdministrator = useSelector(selectIsOperationsResetAdministrator); + const isGeneralSettingsAdministrator = useSelector(selectIsGeneralSettingsAdministrator); + const strings = useStrings().AdminConfig; const generateSettings = () => ({ @@ -116,6 +123,7 @@ export const AdminConfigBase: React.FunctionComponent = (props isHyperlinkAdmin={isHyperlinkAdmin} isCustomMembershipProviderAdmin={isCustomMembershipProviderAdmin} isOperationsResetAdministrator={isOperationsResetAdministrator} + isGeneralSettingsAdministrator={isGeneralSettingsAdministrator} /> ); }; diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts b/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts index 26012031f..0df169a11 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts @@ -50,6 +50,7 @@ export type AdminConfigViewProps = AdminConfigProps & { isHyperlinkAdmin: boolean; isCustomMembershipProviderAdmin: boolean; isOperationsResetAdministrator: boolean; + isGeneralSettingsAdministrator: boolean; }; export type HyperlinkSettingsProps = { @@ -65,6 +66,11 @@ export type OperationsProps = { strings: IStrings['AdminConfig']; }; +export type GeneralSettingsProps = { + classNames: IProcessedStyleSet; + strings: IStrings['AdminConfig']; +}; + export type CustomSourceSettingsProps = { classNames: IProcessedStyleSet; sqlMembershipSource: SqlMembershipSource | undefined; diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx index 1f276042f..a08977c21 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx @@ -4,19 +4,32 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { classNamesFunction, IProcessedStyleSet, Pivot, PivotItem, PrimaryButton, TextField, Text, IColumn, SelectionMode, ShimmeredDetailsList } from '@fluentui/react'; import { useTheme } from '@fluentui/react/lib/Theme'; -import { AdminConfigStyleProps, AdminConfigStyles, AdminConfigViewProps, CustomLabelCellProps, CustomSourceSettingsProps, HyperlinkSettingsProps, OperationsProps } from './AdminConfig.types'; +import { + AdminConfigStyleProps, + AdminConfigStyles, + AdminConfigViewProps, + CustomLabelCellProps, + CustomSourceSettingsProps, + HyperlinkSettingsProps, + OperationsProps, + GeneralSettingsProps } from './AdminConfig.types'; import { PageSection } from '../../components/PageSection'; import { HyperlinkSetting } from '../../components/HyperlinkSetting'; import { Operation } from '../../components/Operation'; import { Page } from '../../components/Page'; import { PageHeader } from '../../components/PageHeader'; import { SettingKey, SqlMembershipAttribute, SqlMembershipSource } from '../../models'; +import { GeneralSetting } from '../../components/GeneralSetting'; const getClassNames = classNamesFunction(); export const AdminConfigView: React.FunctionComponent = (props: AdminConfigViewProps) => { // extract props - const { className, isSaving, onSave, settings, sqlMembershipSource, sqlMembershipSourceAttributes, strings, styles, isHyperlinkAdmin, isCustomMembershipProviderAdmin, isOperationsResetAdministrator } = props; + const { className, isSaving, onSave, settings, sqlMembershipSource, sqlMembershipSourceAttributes, strings, styles, + isHyperlinkAdmin, + isCustomMembershipProviderAdmin, + isOperationsResetAdministrator, + isGeneralSettingsAdministrator } = props; // generate class names const classNames: IProcessedStyleSet = getClassNames(styles, { @@ -114,6 +127,19 @@ export const AdminConfigView: React.FunctionComponent = (p strings={strings} /> } + {isGeneralSettingsAdministrator && + + + + }
@@ -128,6 +154,7 @@ export const AdminConfigView: React.FunctionComponent = (p ); }; + const Operations: React.FunctionComponent = (props: OperationsProps) => { const { classNames, strings} = props; return ( @@ -140,6 +167,19 @@ const Operations: React.FunctionComponent = (props: OperationsP
); } + +const GeneralSettings: React.FunctionComponent = (props: GeneralSettingsProps) => { + const { strings} = props; + return ( +
+ +
+ ); +} + const HyperlinkSettings: React.FunctionComponent = (props: HyperlinkSettingsProps) => { const { classNames, strings, settings, setSettings, setHasValidationErrors } = props; diff --git a/UI/web-app/src/services/localization/IStrings.ts b/UI/web-app/src/services/localization/IStrings.ts index 81134770e..d55f5ab66 100644 --- a/UI/web-app/src/services/localization/IStrings.ts +++ b/UI/web-app/src/services/localization/IStrings.ts @@ -113,7 +113,7 @@ export type IStrings = { start: string; starting: string; }; - }; + }, CustomSourceSettings: { labels: { customSource: string; @@ -126,6 +126,13 @@ export type IStrings = { customLabelInputPlaceHolder: string; }, }, + GeneralSettings: { + labels: { + general: string; + reviewOwnSubmissionTitle: string; + reviewOwnSubmissionDescription: string; + } + } }, Authentication: { loginFailed: string; diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index 8ccd6da15..19924e73d 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -129,6 +129,13 @@ export const strings: IStrings = { customLabelInputPlaceHolder: "Enter a custom label", }, }, + GeneralSettings: { + labels: { + general: "General", + reviewOwnSubmissionTitle: "Submission Reviewers permissions", + reviewOwnSubmissionDescription: "Can submission reviewers review their own submissions?" + } + } }, Authentication: { loginFailed: 'An unexpected error occurred during login.' diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index 2e3649527..7ef6734b2 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -131,6 +131,13 @@ export const strings: IStrings = { customLabelInputPlaceHolder: "Entre un nombre personalizado", }, }, + GeneralSettings: { + labels: { + general: "General", + reviewOwnSubmissionTitle: "Permisos de los Verificadores de Solicitud", + reviewOwnSubmissionDescription: "¿Pueden los Verificadores de Solicitud aceptar o rechazar su propia solicitud?" + } + } }, Authentication: { loginFailed: 'Ocurrió un error inesperado durante el inicio de sesión.' diff --git a/UI/web-app/src/store/roles.slice.tsx b/UI/web-app/src/store/roles.slice.tsx index ad340202d..b8a6f2b41 100644 --- a/UI/web-app/src/store/roles.slice.tsx +++ b/UI/web-app/src/store/roles.slice.tsx @@ -5,7 +5,7 @@ import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from './store'; import { getAllRoles } from './roles.api'; -// Define a type for the slice stat +// Define a type for the slice state export type Roles = { isJobOwnerReader: boolean; isJobOwnerEnabler: boolean; @@ -17,10 +17,11 @@ export type Roles = { isHyperlinkAdministrator: boolean; isCustomMembershipProviderAdministrator: boolean; isOperationsResetAdministrator: boolean; + isGeneralSettingsAdministrator: boolean; isFetchingRoles: boolean; } -// Define the initial state using that ty +// Define the initial state using that type const initialState: Roles = { isJobOwnerReader: false, isJobOwnerEnabler: false, @@ -32,6 +33,7 @@ const initialState: Roles = { isHyperlinkAdministrator: false, isCustomMembershipProviderAdministrator: false, isOperationsResetAdministrator: false, + isGeneralSettingsAdministrator: false, isFetchingRoles: false, }; @@ -63,6 +65,7 @@ export const selectIsSubmissionReviewer = (state: RootState) => state.roles.isSu export const selectIsHyperlinkAdministrator = (state: RootState) => state.roles.isHyperlinkAdministrator; export const selectIsCustomMembershipProviderAdministrator = (state: RootState) => state.roles.isCustomMembershipProviderAdministrator; export const selectIsOperationsResetAdministrator = (state: RootState) => state.roles.isOperationsResetAdministrator; +export const selectIsGeneralSettingsAdministrator = (state: RootState) => state.roles.isGeneralSettingsAdministrator; export const selectHasAccess = (state: RootState) => { return state.roles.isJobOwnerReader || state.roles.isJobOwnerWriter || state.roles.isJobTenantReader || state.roles.isJobTenantWriter; @@ -76,4 +79,11 @@ export const selectIsJobWriter = (state: RootState) => { return state.roles.isJobOwnerWriter || state.roles.isJobTenantWriter; }; +export const selectHasAdminCenterPermissions = (state: RootState) => { + return state.roles.isHyperlinkAdministrator || + state.roles.isCustomMembershipProviderAdministrator || + state.roles.isOperationsResetAdministrator || + state.roles.isGeneralSettingsAdministrator; +}; + export default rolesSlice.reducer; From 2e3b84203930d89238efbd3c2bd39aefd31372f4 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 7 Aug 2024 15:18:35 -0700 Subject: [PATCH 0170/1479] Add rest adminstrator role --- .../Hosts/WebApi/Documentation/WebApiSetup.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md b/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md index ef35fa233..0f0656efd 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md @@ -77,6 +77,8 @@ The roles are: - General Settings Administrator - Users with this role can **update** general settings from the Admin Center page. +- Reset Administrator + - Users with this role can **reset, stop** GMM from the Admin Operations page. ## Add a role to a group From dc9d0e3425addbc2f05c869816c0686c6c66d9ab Mon Sep 17 00:00:00 2001 From: Yuqing Yang Date: Fri, 9 Aug 2024 17:32:01 +0000 Subject: [PATCH 0171/1479] Apply suggestions from code review --- .../Hosts/WebApi/Documentation/WebApiSetup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md b/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md index 0f0656efd..279fc8beb 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md @@ -78,7 +78,7 @@ The roles are: - Users with this role can **update** general settings from the Admin Center page. - Reset Administrator - - Users with this role can **reset, stop** GMM from the Admin Operations page. + - Users with this role can **reset, stop** GMM from the Admin Center page. ## Add a role to a group From bc8a162bea36bd2df05e4f1cfe0a830111b2d2ca Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Mon, 5 Aug 2024 16:45:17 -0700 Subject: [PATCH 0172/1479] Renamed the GetAttributeValues endpoint to GetAttributeMappings. (WebAPI changes) --- ...mbershipSourceAttributeMappingsRequest.cs} | 4 +-- ...bershipSourceAttributeMappingsResponse.cs} | 4 +-- ...mbershipSourceAttributeMappingsHandler.cs} | 30 +++++++++---------- ...ue.cs => SqlMembershipAttributeMapping.cs} | 4 +-- ...sModel.cs => GetAttributeMappingsModel.cs} | 4 +-- .../SqlMembershipSourcesControllerTests.cs | 30 +++++++++---------- .../Configuration/MessageHandlerInjector.cs | 2 +- .../SqlMembershipSourcesController.cs | 12 ++++---- .../ISqlMembershipRepository.cs | 2 +- .../SqlMembershipRepository.cs | 8 ++--- 10 files changed, 50 insertions(+), 50 deletions(-) rename Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/{GetDefaultSqlMembershipSourceAttributeValuesRequest.cs => GetDefaultSqlMembershipSourceAttributeMappingsRequest.cs} (61%) rename Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/{GetDefaultSqlMembershipSourceAttributeValuesResponse.cs => GetDefaultSqlMembershipSourceAttributeMappingsResponse.cs} (52%) rename Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/{GetDefaultSqlMembershipSourceAttributeValuesHandler.cs => GetDefaultSqlMembershipSourceAttributeMappingsHandler.cs} (75%) rename Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/{SqlMembershipAttributeValue.cs => SqlMembershipAttributeMapping.cs} (70%) rename Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Responses/{GetAttributeValuesModel.cs => GetAttributeMappingsModel.cs} (53%) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetDefaultSqlMembershipSourceAttributeValuesRequest.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetDefaultSqlMembershipSourceAttributeMappingsRequest.cs similarity index 61% rename from Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetDefaultSqlMembershipSourceAttributeValuesRequest.cs rename to Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetDefaultSqlMembershipSourceAttributeMappingsRequest.cs index 0f77f55c3..35a080dcb 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetDefaultSqlMembershipSourceAttributeValuesRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetDefaultSqlMembershipSourceAttributeMappingsRequest.cs @@ -5,9 +5,9 @@ namespace Services.Messages.Requests { - public class GetDefaultSqlMembershipSourceAttributeValuesRequest : RequestBase + public class GetDefaultSqlMembershipSourceAttributeMappingsRequest : RequestBase { - public GetDefaultSqlMembershipSourceAttributeValuesRequest(string attribute) + public GetDefaultSqlMembershipSourceAttributeMappingsRequest(string attribute) { Attribute = attribute; } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetDefaultSqlMembershipSourceAttributeValuesResponse.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetDefaultSqlMembershipSourceAttributeMappingsResponse.cs similarity index 52% rename from Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetDefaultSqlMembershipSourceAttributeValuesResponse.cs rename to Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetDefaultSqlMembershipSourceAttributeMappingsResponse.cs index bef608643..9f1750d35 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetDefaultSqlMembershipSourceAttributeValuesResponse.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetDefaultSqlMembershipSourceAttributeMappingsResponse.cs @@ -6,8 +6,8 @@ namespace Services.Messages.Responses { - public class GetDefaultSqlMembershipSourceAttributeValuesResponse : ResponseBase + public class GetDefaultSqlMembershipSourceAttributeMappingsResponse : ResponseBase { - public GetAttributeValuesModel Model { get; set; } = new GetAttributeValuesModel(); + public GetAttributeMappingsModel Model { get; set; } = new GetAttributeMappingsModel(); } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributeValuesHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributeMappingsHandler.cs similarity index 75% rename from Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributeValuesHandler.cs rename to Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributeMappingsHandler.cs index c750187ba..e80ac3be6 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributeValuesHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributeMappingsHandler.cs @@ -7,17 +7,17 @@ using Services.Contracts; using Services.Messages.Requests; using Services.Messages.Responses; -using SqlMembershipAttributeValueDTO = WebApi.Models.DTOs.SqlMembershipAttributeValue; +using SqlMembershipAttributeValueDTO = WebApi.Models.DTOs.SqlMembershipAttributeMapping; namespace Services { - public class GetDefaultSqlMembershipSourceAttributeValuesHandler : RequestHandlerBase + public class GetDefaultSqlMembershipSourceAttributeMappingsHandler : RequestHandlerBase { private readonly ILoggingRepository _loggingRepository; private readonly IDataFactoryRepository _dataFactoryRepository; private readonly ISqlMembershipRepository _sqlMembershipRepository; - public GetDefaultSqlMembershipSourceAttributeValuesHandler(ILoggingRepository loggingRepository, + public GetDefaultSqlMembershipSourceAttributeMappingsHandler(ILoggingRepository loggingRepository, IDataFactoryRepository dataFactoryRepository, ISqlMembershipRepository sqlMembershipRepository) : base(loggingRepository) { @@ -26,13 +26,13 @@ public GetDefaultSqlMembershipSourceAttributeValuesHandler(ILoggingRepository lo _sqlMembershipRepository = sqlMembershipRepository ?? throw new ArgumentNullException(nameof(sqlMembershipRepository)); } - protected override async Task ExecuteCoreAsync(GetDefaultSqlMembershipSourceAttributeValuesRequest request) + protected override async Task ExecuteCoreAsync(GetDefaultSqlMembershipSourceAttributeMappingsRequest request) { try { - var response = new GetDefaultSqlMembershipSourceAttributeValuesResponse(); - var attributeValues = await GetSqlAttributeValuesAsync(request.Attribute); - foreach (var attributeValue in attributeValues) + var response = new GetDefaultSqlMembershipSourceAttributeMappingsResponse(); + var attributeMappings = await GetSqlAttributeMappingsAsync(request.Attribute); + foreach (var attributeValue in attributeMappings) { var dto = new SqlMembershipAttributeValueDTO(attributeValue.Code, attributeValue.Description); @@ -42,33 +42,33 @@ protected override async Task> GetSqlAttributeValuesAsync(string attribute) + private async Task> GetSqlAttributeMappingsAsync(string attribute) { var tableName = await GetTableNameAsync(); - var attributes = await GetAttributeValuesAsync(attribute, tableName); + var attributes = await GetAttributeMappingsAsync(attribute, tableName); return attributes; } - private async Task> GetAttributeValuesAsync(string attribute, string tableName) + private async Task> GetAttributeMappingsAsync(string attribute, string tableName) { - var attributeValues = new List<(string Code, string Description)>(); + var attributeMappings = new List<(string Code, string Description)>(); try { - attributeValues = await _sqlMembershipRepository.GetAttributeValuesAsync(attribute, tableName); + attributeMappings = await _sqlMembershipRepository.GetAttributeMappingsAsync(attribute, tableName); } catch (SqlException ex) { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"An exception was thrown while attempting to get Sql Filter Attribute Values from mappings table '{tableName}': {ex.Message}" }); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"An exception was thrown while attempting to get Sql Filter Attribute Mappings from mappings table '{tableName}': {ex.Message}" }); throw ex; } - return attributeValues; + return attributeMappings; } private async Task GetTableNameAsync() diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/SqlMembershipAttributeValue.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/SqlMembershipAttributeMapping.cs similarity index 70% rename from Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/SqlMembershipAttributeValue.cs rename to Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/SqlMembershipAttributeMapping.cs index bc52a1717..049a42409 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/SqlMembershipAttributeValue.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/DTOs/SqlMembershipAttributeMapping.cs @@ -3,9 +3,9 @@ namespace WebApi.Models.DTOs { - public class SqlMembershipAttributeValue + public class SqlMembershipAttributeMapping { - public SqlMembershipAttributeValue(string code, string description) + public SqlMembershipAttributeMapping(string code, string description) { Code = code; Description = description; diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Responses/GetAttributeValuesModel.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Responses/GetAttributeMappingsModel.cs similarity index 53% rename from Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Responses/GetAttributeValuesModel.cs rename to Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Responses/GetAttributeMappingsModel.cs index 844edacf3..9f13aac86 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Responses/GetAttributeValuesModel.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Models/Responses/GetAttributeMappingsModel.cs @@ -3,9 +3,9 @@ namespace WebApi.Models.Responses { - public class GetAttributeValuesModel : List + public class GetAttributeMappingsModel : List { - public GetAttributeValuesModel() + public GetAttributeMappingsModel() { } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs index 5c7ef8855..4874c9755 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs @@ -26,7 +26,7 @@ public class SqlMembershipSourcesControllerTests private GetDefaultSqlMembershipSourceHandler _getDefaultSqlMembershipSourceHandler = null!; private GetDefaultSqlMembershipSourceAttributesHandler _getDefaultSqlMembershipSourceAttributesHandler = null!; - private GetDefaultSqlMembershipSourceAttributeValuesHandler _getDefaultSqlMembershipSourceAttributeValuesHandler = null!; + private GetDefaultSqlMembershipSourceAttributeMappingsHandler _getDefaultSqlMembershipSourceAttributeMappingsHandler = null!; private PatchDefaultSqlMembershipSourceCustomLabelHandler _patchDefaultSqlMembershipSourceCustomLabelHandler = null!; private PatchDefaultSqlMembershipSourceAttributesHandler _patchDefaultSqlMembershipSourceAttributesHandler = null!; private SqlMembershipSourcesController _sqlMembershipSourcesController = null!; @@ -41,11 +41,11 @@ public void Initialize() _getDefaultSqlMembershipSourceHandler = new GetDefaultSqlMembershipSourceHandler(_loggingRepository.Object, _databaseSqlMembershipSourcesRepository.Object); _getDefaultSqlMembershipSourceAttributesHandler = new GetDefaultSqlMembershipSourceAttributesHandler(_loggingRepository.Object, _databaseSqlMembershipSourcesRepository.Object, _dataFactoryRepository.Object, _sqlMembershipRepository.Object); - _getDefaultSqlMembershipSourceAttributeValuesHandler = new GetDefaultSqlMembershipSourceAttributeValuesHandler(_loggingRepository.Object, _dataFactoryRepository.Object, _sqlMembershipRepository.Object); + _getDefaultSqlMembershipSourceAttributeMappingsHandler = new GetDefaultSqlMembershipSourceAttributeMappingsHandler(_loggingRepository.Object, _dataFactoryRepository.Object, _sqlMembershipRepository.Object); _patchDefaultSqlMembershipSourceCustomLabelHandler = new PatchDefaultSqlMembershipSourceCustomLabelHandler(_loggingRepository.Object, _databaseSqlMembershipSourcesRepository.Object); _patchDefaultSqlMembershipSourceAttributesHandler = new PatchDefaultSqlMembershipSourceAttributesHandler(_loggingRepository.Object, _databaseSqlMembershipSourcesRepository.Object); - _sqlMembershipSourcesController = new SqlMembershipSourcesController(_getDefaultSqlMembershipSourceHandler, _getDefaultSqlMembershipSourceAttributesHandler, _getDefaultSqlMembershipSourceAttributeValuesHandler, _patchDefaultSqlMembershipSourceCustomLabelHandler, _patchDefaultSqlMembershipSourceAttributesHandler) + _sqlMembershipSourcesController = new SqlMembershipSourcesController(_getDefaultSqlMembershipSourceHandler, _getDefaultSqlMembershipSourceAttributesHandler, _getDefaultSqlMembershipSourceAttributeMappingsHandler, _patchDefaultSqlMembershipSourceCustomLabelHandler, _patchDefaultSqlMembershipSourceAttributesHandler) { ControllerContext = CreateControllerContext(new List { @@ -73,7 +73,7 @@ public void Initialize() _databaseSqlMembershipSourcesRepository.Setup(x => x.GetDefaultSourceAsync()).ReturnsAsync(() => _defaultSource); _databaseSqlMembershipSourcesRepository.Setup(x => x.GetDefaultSourceAttributesAsync()).ReturnsAsync(() => _storedAttributeSettings); _sqlMembershipRepository.Setup(x => x.GetColumnDetailsAsync(It.IsAny())).ReturnsAsync(new List<(string Name, string Type)> { ("Name1", "nvarchar"), ("Name2", "int"), ("Name3_Code", "nvarchar") }); - _sqlMembershipRepository.Setup(x => x.GetAttributeValuesAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List<(string Code, string Description)> { ("Code1", "Description1"), ("Code2", "Description2"), ("Code3", "Description3") }); + _sqlMembershipRepository.Setup(x => x.GetAttributeMappingsAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List<(string Code, string Description)> { ("Code1", "Description1"), ("Code2", "Description2"), ("Code3", "Description3") }); _sqlMembershipRepository.Setup(x => x.CheckIfTableExistsAsync(It.IsAny())).ReturnsAsync(true); _sqlMembershipRepository.Setup(x => x.CheckIfMappingsTableExistsAsync(It.IsAny())).ReturnsAsync(true); _dataFactoryRepository.Setup(x => x.GetMostRecentSucceededRunIdAsync()).ReturnsAsync("RUN ID"); @@ -263,29 +263,29 @@ public async Task PatchDefaultSourceAttributesWhenCustomMembershipProviderAdminT } [TestMethod] - public async Task SuccessfulGetHRFilterattributeValuesTestAsync() + public async Task SuccessfulGetHRFilterattributeMappingsTestAsync() { - var response = await _sqlMembershipSourcesController.GetDefaultSourceAttributeValuesAsync("attribute"); + var response = await _sqlMembershipSourcesController.GetDefaultSourceAttributeMappingsAsync("attribute"); Assert.IsNotNull(response); var okResult = response as OkObjectResult; Assert.IsNotNull(okResult); Assert.IsNotNull(okResult.Value); - var attributeValues = okResult.Value as GetAttributeValuesModel; - Assert.IsNotNull(attributeValues); - Assert.AreEqual(attributeValues.Count, 3); - Assert.AreEqual(attributeValues[0].Code, "Code1"); - Assert.AreEqual(attributeValues[0].Description, "Description1"); + var attributeMappings = okResult.Value as GetAttributeMappingsModel; + Assert.IsNotNull(attributeMappings); + Assert.AreEqual(attributeMappings.Count, 3); + Assert.AreEqual(attributeMappings[0].Code, "Code1"); + Assert.AreEqual(attributeMappings[0].Description, "Description1"); } [TestMethod] - public async Task ExceptionGetHRFilterattributeValuesTestAsync() + public async Task ExceptionGetHRFilterattributeMappingsTestAsync() { _sqlMembershipRepository.Setup(x => x.CheckIfMappingsTableExistsAsync(It.IsAny())).ReturnsAsync(false); - _sqlMembershipRepository.Setup(x => x.GetAttributeValuesAsync(It.IsAny(), It.IsAny())).Throws(new Exception("Unexpected exception triggered for testing")); + _sqlMembershipRepository.Setup(x => x.GetAttributeMappingsAsync(It.IsAny(), It.IsAny())).Throws(new Exception("Unexpected exception triggered for testing")); - var response = await _sqlMembershipSourcesController.GetDefaultSourceAttributeValuesAsync("attribute"); + var response = await _sqlMembershipSourcesController.GetDefaultSourceAttributeMappingsAsync("attribute"); Assert.IsNotNull(response); @@ -295,7 +295,7 @@ public async Task ExceptionGetHRFilterattributeValuesTestAsync() Assert.AreEqual(internalServerErrorResponse.StatusCode, (int)HttpStatusCode.InternalServerError); _loggingRepository.Verify(x => x.LogMessageAsync( - It.Is(m => m.Message.StartsWith("Unable to retrieve Sql Filter Attribute Values")), + It.Is(m => m.Message.StartsWith("Unable to retrieve Sql Filter Attribute Mappings")), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs index dcce59eb8..c7be83196 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs @@ -23,7 +23,7 @@ public static IServiceCollection InjectMessageHandlers(this IServiceCollection s services.AddTransient, GetDefaultSqlMembershipSourceHandler>(); services.AddTransient, GetDefaultSqlMembershipSourceAttributesHandler>(); - services.AddTransient, GetDefaultSqlMembershipSourceAttributeValuesHandler>(); + services.AddTransient, GetDefaultSqlMembershipSourceAttributeMappingsHandler>(); services.AddTransient, PatchDefaultSqlMembershipSourceCustomLabelHandler>(); services.AddTransient, PatchDefaultSqlMembershipSourceAttributesHandler>(); diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs index 1776f5659..6fa2d938a 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs @@ -17,20 +17,20 @@ public class SqlMembershipSourcesController : ControllerBase { private readonly IRequestHandler _getDefaultSqlMembershipSourceHandler; private readonly IRequestHandler _getDefaultSqlMembershipSourceAttributesHandler; - private readonly IRequestHandler _getDefaultSqlMembershipSourceAttributeValuesHandler; + private readonly IRequestHandler _getDefaultSqlMembershipSourceAttributeMappingsHandler; private readonly IRequestHandler _patchDefaultSqlMembershipSourceCustomLabelHandler; private readonly IRequestHandler _patchDefaultSqlMembershipSourceAttributesHandler; public SqlMembershipSourcesController( IRequestHandler getDefaultSqlMembershipSourceHandler, IRequestHandler getDefaultSqlMembershipSourceAttributesHandler, - IRequestHandler getDefaultSqlMembershipSourceAttributeValuesHandler, + IRequestHandler getDefaultSqlMembershipSourceAttributeMappingsHandler, IRequestHandler patchDefaultSqlMembershipSourceCustomLabelHandler, IRequestHandler patchDefaultSqlMembershipSourceAttributesHandler) { _getDefaultSqlMembershipSourceHandler = getDefaultSqlMembershipSourceHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceHandler)); _getDefaultSqlMembershipSourceAttributesHandler = getDefaultSqlMembershipSourceAttributesHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceAttributesHandler)); - _getDefaultSqlMembershipSourceAttributeValuesHandler = getDefaultSqlMembershipSourceAttributeValuesHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceAttributeValuesHandler)); + _getDefaultSqlMembershipSourceAttributeMappingsHandler = getDefaultSqlMembershipSourceAttributeMappingsHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceAttributeMappingsHandler)); _patchDefaultSqlMembershipSourceCustomLabelHandler = patchDefaultSqlMembershipSourceCustomLabelHandler ?? throw new ArgumentNullException(nameof(patchDefaultSqlMembershipSourceCustomLabelHandler)); _patchDefaultSqlMembershipSourceAttributesHandler = patchDefaultSqlMembershipSourceAttributesHandler ?? throw new ArgumentNullException(nameof(patchDefaultSqlMembershipSourceAttributesHandler)); } @@ -66,12 +66,12 @@ public async Task GetDefaultSourceAttributesAsync() } [Authorize()] - [HttpGet("attributeValues/{attribute}")] - public async Task GetDefaultSourceAttributeValuesAsync(string attribute) + [HttpGet("attributeMappings/{attribute}")] + public async Task GetDefaultSourceAttributeMappingsAsync(string attribute) { try { - var response = await _getDefaultSqlMembershipSourceAttributeValuesHandler.ExecuteAsync(new GetDefaultSqlMembershipSourceAttributeValuesRequest(attribute)); + var response = await _getDefaultSqlMembershipSourceAttributeMappingsHandler.ExecuteAsync(new GetDefaultSqlMembershipSourceAttributeMappingsRequest(attribute)); return Ok(response.Model); } catch (Exception ex) diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs index 29a9b84fe..bb9eae352 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs @@ -17,6 +17,6 @@ public interface ISqlMembershipRepository Task<(int maxDepth, string azureObjectId)> GetOrgLeaderAsync(int employeeId, string tableName); Task> GetColumnDetailsAsync(string tableName); Task CheckIfMappingsTableExistsAsync(string tableName); - Task> GetAttributeValuesAsync(string attribute, string tableName); + Task> GetAttributeMappingsAsync(string attribute, string tableName); } } diff --git a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs index aa080afd4..baef0abae 100644 --- a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs @@ -409,9 +409,9 @@ await retryPolicy.Execute(async () => return tableExists; } - public async Task> GetAttributeValuesAsync(string attribute, string tableName) + public async Task> GetAttributeMappingsAsync(string attribute, string tableName) { - var attributeValues = new List<(string Code, string Description)>(); + var attributeMappings = new List<(string Code, string Description)>(); var retryPolicy = GetRetryPolicy(); try @@ -435,7 +435,7 @@ await retryPolicy.Execute(async () => var code = reader.IsDBNull(codeOrdinal) ? null : reader.GetString(codeOrdinal).Trim(); var description = reader.IsDBNull(descriptionOrdinal) ? null : reader.GetString(descriptionOrdinal).Trim(); - attributeValues.Add((code, description)); + attributeMappings.Add((code, description)); } await reader.CloseAsync(); } @@ -449,7 +449,7 @@ await retryPolicy.Execute(async () => throw ex; } - return attributeValues; + return attributeMappings; } private RetryPolicy GetRetryPolicy() From e0c96f566ad9096c9e249b48139d15f84478ba0e Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Mon, 5 Aug 2024 17:02:57 -0700 Subject: [PATCH 0173/1479] Renamed the GetAttributeValues endpoint to GetAttributeMappings. (UI changes) --- .../ISqlMembershipSourcesApi.ts | 4 +- .../SqlMembershipSourcesApi.ts | 6 +-- .../HRQuerySource/HRQuerySource.base.tsx | 38 +++++++-------- ...uest.ts => GetAttributeMappingsRequest.ts} | 2 +- .../models/GetAttributeMappingsResponse.ts | 10 ++++ .../src/models/GetAttributeValuesResponse.ts | 10 ---- ...ue.ts => SqlMembershipAttributeMapping.ts} | 2 +- UI/web-app/src/models/index.ts | 2 +- .../src/store/sqlMembershipSources.api.tsx | 20 ++++---- .../src/store/sqlMembershipSources.slice.tsx | 46 +++++++++---------- 10 files changed, 70 insertions(+), 70 deletions(-) rename UI/web-app/src/models/{GetAttributeValuesRequest.ts => GetAttributeMappingsRequest.ts} (77%) create mode 100644 UI/web-app/src/models/GetAttributeMappingsResponse.ts delete mode 100644 UI/web-app/src/models/GetAttributeValuesResponse.ts rename UI/web-app/src/models/{SqlMembershipAttributeValue.ts => SqlMembershipAttributeMapping.ts} (73%) diff --git a/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts b/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts index 1a4095ffd..581bd09a4 100644 --- a/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts +++ b/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { SqlMembershipAttribute, SqlMembershipSource, SqlMembershipAttributeValue } from '../../models'; +import { SqlMembershipAttribute, SqlMembershipSource, SqlMembershipAttributeMapping } from '../../models'; export interface ISqlMembershipSourcesApi { fetchDefaultSqlMembershipSource(): Promise; fetchDefaultSqlMembershipSourceAttributes(): Promise; - fetchDefaultSqlMembershipSourceAttributeValues(attribute: string): Promise; + fetchDefaultSqlMembershipSourceAttributeMappings(attribute: string): Promise; patchDefaultSqlMembershipSourceCustomLabel(customLabel: string): Promise; patchDefaultSqlMembershipSourceAttributes(attributes: SqlMembershipAttribute[]): Promise; } diff --git a/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts b/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts index 9d0032049..90b08f180 100644 --- a/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts +++ b/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { SqlMembershipAttribute, SqlMembershipAttributeValue, SqlMembershipSource } from '../../models'; +import { SqlMembershipAttribute, SqlMembershipAttributeMapping, SqlMembershipSource } from '../../models'; import { ApiBase } from '../ApiBase'; import { ISqlMembershipSourcesApi } from './ISqlMembershipSourcesApi'; @@ -20,8 +20,8 @@ export class SqlMembershipSourcesApi extends ApiBase implements ISqlMembershipSo return response.data; } - public async fetchDefaultSqlMembershipSourceAttributeValues(attribute: string): Promise { - const response = await this.httpClient.get('/attributeValues/' + attribute); + public async fetchDefaultSqlMembershipSourceAttributeMappings(attribute: string): Promise { + const response = await this.httpClient.get('/attributeMappings/' + attribute); this.ensureSuccessStatusCode(response); return response.data; } diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 1cc8d1548..2946d6c1a 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -20,10 +20,10 @@ import { getPeoplePickerSuggestions } from '../../store/jobs.api'; import { updateOrgLeaderDetails, selectOrgLeaderDetails, selectObjectIdEmployeeIdMapping } from '../../store/orgLeaderDetails.slice'; import { selectPeoplePickerSuggestions } from '../../store/jobs.slice'; import { fetchDefaultSqlMembershipSourceAttributes } from '../../store/sqlMembershipSources.api'; -import { fetchAttributeValues } from '../../store/sqlMembershipSources.api'; -import { selectAttributes, selectSource, selectAttributeValues, setAttributeValues } from '../../store/sqlMembershipSources.slice'; +import { fetchAttributeMappings } from '../../store/sqlMembershipSources.api'; +import { selectAttributes, selectSource, selectAttributeMappings, setAttributeMappings } from '../../store/sqlMembershipSources.slice'; import { selectIsJobWriter } from '../../store/roles.slice'; -import { SqlMembershipAttribute, SqlMembershipAttributeValue } from '../../models'; +import { SqlMembershipAttribute, SqlMembershipAttributeMapping } from '../../models'; import { IFilterPart } from '../../models/IFilterPart'; import { Group } from '../../models/Group'; import { containsSqlExpression, parseGroup, stringifyGroups } from './QuerySerializer'; @@ -65,7 +65,7 @@ export const HRQuerySourceBase: React.FunctionComponent = (p const [children, setChildren] = useState([]); const excludeLeaderQuery = `EmployeeId <> ${source.manager?.id}` const attributes = useSelector(selectAttributes); - const attributeValues = useSelector(selectAttributeValues); + const attributeMappings = useSelector(selectAttributeMappings); const hrSource = useSelector(selectSource); const [filteredOptions, setFilteredOptions] = useState({}); const [filteredValueOptions, setFilteredValueOptions] = useState({}); @@ -106,9 +106,9 @@ export const HRQuerySourceBase: React.FunctionComponent = (p let att = mappedItems.map(item => item.attribute); let distinctAttributes = [...new Set(att)]; for (let i = 0; i < distinctAttributes.length; i++) { - if (attributeValues && attributeValues[distinctAttributes[i]] === undefined) { + if (attributeMappings && attributeMappings[distinctAttributes[i]] === undefined) { const selectedAttribute = attributes?.find(({ hasMapping, name }) => ((hasMapping && `${name}_Code` === distinctAttributes[i]) || (!hasMapping && name === distinctAttributes[i]))); - dispatch(fetchAttributeValues({attribute: distinctAttributes[i] as string, type: selectedAttribute?.type, hasMapping: selectedAttribute?.hasMapping })); + dispatch(fetchAttributeMappings({attribute: distinctAttributes[i] as string, type: selectedAttribute?.type, hasMapping: selectedAttribute?.hasMapping })); } } if (!groupingEnabled) { @@ -204,10 +204,10 @@ const checkType = (value: string, type: string | undefined): string => { return options; }; - const getValueOptions = (attributeValues?: SqlMembershipAttributeValue[]): IComboBoxOption[] => { - let valueOptions = attributeValues?.map((attributeValue, index) => ({ - key: attributeValue.code, - text: attributeValue.description ? attributeValue.description : attributeValue.code + const getValueOptions = (attributeMappings?: SqlMembershipAttributeMapping[]): IComboBoxOption[] => { + let valueOptions = attributeMappings?.map((attributeMapping, index) => ({ + key: attributeMapping.code, + text: attributeMapping.description ? attributeMapping.description : attributeMapping.code })) || []; valueOptions.sort((a, b) => a.text.localeCompare(b.text)); return valueOptions; @@ -704,8 +704,8 @@ const checkType = (value: string, type: string | undefined): string => { const handleAttributeChange = (event: React.FormEvent, item?: IComboBoxOption, index?: number, groupIndex?: number): void => { if (item) { const selectedAttribute = attributes?.find(({ hasMapping, name }) => ((hasMapping && `${name}_Code` === item.key) || (!hasMapping && name === item.key))); - if (attributeValues && attributeValues[item.key] === undefined) { - dispatch(fetchAttributeValues({attribute: item.key as string, type: selectedAttribute?.type, hasMapping: selectedAttribute?.hasMapping })); + if (attributeMappings && attributeMappings[item.key] === undefined) { + dispatch(fetchAttributeMappings({attribute: item.key as string, type: selectedAttribute?.type, hasMapping: selectedAttribute?.hasMapping })); } const updatedItems = items.map((it, idx) => { if (idx === index) { @@ -808,7 +808,7 @@ const checkType = (value: string, type: string | undefined): string => { const handleAttributeValueChange = (attribute: string, event: React.FormEvent, item?: IComboBoxOption, index?: number): void => { if (item) { const selectedValue = item.key.toString(); - const selectedValueAfterConversion = attributeValues[attribute] ? checkType(selectedValue, attributeValues[attribute.toString()].type) : selectedValue; + const selectedValueAfterConversion = attributeMappings[attribute] ? checkType(selectedValue, attributeMappings[attribute.toString()].type) : selectedValue; const updatedItems = items.map((it, idx) => { if (idx === index) { @@ -1012,12 +1012,12 @@ const checkType = (value: string, type: string | undefined): string => { const onAttributeValueChange = (text: string, index: number) => { let newFilteredValueOptions = { ...filteredValueOptions }; if (groupingEnabled && groups.length > 1) return; - const currentAttributeValues = attributeValues[items[index].attribute].values || []; - if (currentAttributeValues.length > 0) { + const currentAttributeMappings = attributeMappings[items[index].attribute].mappings || []; + if (currentAttributeMappings.length > 0) { if (!text) { - newFilteredValueOptions[index] = getValueOptions(currentAttributeValues); + newFilteredValueOptions[index] = getValueOptions(currentAttributeMappings); } else { - let valueOptions = getValueOptions(currentAttributeValues); + let valueOptions = getValueOptions(currentAttributeMappings); newFilteredValueOptions[index] = valueOptions.filter(opt => opt.text.toLowerCase().startsWith(text.toLowerCase())); } setFilteredValueOptions(newFilteredValueOptions); @@ -1196,10 +1196,10 @@ const checkType = (value: string, type: string | undefined): string => { styles={{root: classNames.root, title: classNames.dropdownTitle}} />; case 'value': - if (attributeValues && attributeValues[items[index].attribute] && attributeValues[items[index].attribute].values.length > 0) { + if (attributeMappings && attributeMappings[items[index].attribute] && attributeMappings[items[index].attribute].mappings.length > 0) { return onAttributeValueChange(text, index)} onChange={(event, option) => handleAttributeValueChange(item.attribute, event, option, index)} allowFreeInput diff --git a/UI/web-app/src/models/GetAttributeValuesRequest.ts b/UI/web-app/src/models/GetAttributeMappingsRequest.ts similarity index 77% rename from UI/web-app/src/models/GetAttributeValuesRequest.ts rename to UI/web-app/src/models/GetAttributeMappingsRequest.ts index de1e0add7..933dab053 100644 --- a/UI/web-app/src/models/GetAttributeValuesRequest.ts +++ b/UI/web-app/src/models/GetAttributeMappingsRequest.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export interface GetAttributeValuesRequest { +export interface GetAttributeMappingsRequest { attribute: string; type: string | undefined; hasMapping: boolean | undefined; diff --git a/UI/web-app/src/models/GetAttributeMappingsResponse.ts b/UI/web-app/src/models/GetAttributeMappingsResponse.ts new file mode 100644 index 000000000..00a7ca552 --- /dev/null +++ b/UI/web-app/src/models/GetAttributeMappingsResponse.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { SqlMembershipAttributeMapping } from "./SqlMembershipAttributeMapping"; + +export interface GetAttributeMappingsResponse { + mappings: SqlMembershipAttributeMapping[]; + attribute: string; + type: string | undefined; +} \ No newline at end of file diff --git a/UI/web-app/src/models/GetAttributeValuesResponse.ts b/UI/web-app/src/models/GetAttributeValuesResponse.ts deleted file mode 100644 index 7dda6bf88..000000000 --- a/UI/web-app/src/models/GetAttributeValuesResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { SqlMembershipAttributeValue } from "./SqlMembershipAttributeValue"; - -export interface GetAttributeValuesResponse { - values: SqlMembershipAttributeValue[]; - attribute: string; - type: string | undefined; -} \ No newline at end of file diff --git a/UI/web-app/src/models/SqlMembershipAttributeValue.ts b/UI/web-app/src/models/SqlMembershipAttributeMapping.ts similarity index 73% rename from UI/web-app/src/models/SqlMembershipAttributeValue.ts rename to UI/web-app/src/models/SqlMembershipAttributeMapping.ts index 0781f0f97..4646c0363 100644 --- a/UI/web-app/src/models/SqlMembershipAttributeValue.ts +++ b/UI/web-app/src/models/SqlMembershipAttributeMapping.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export type SqlMembershipAttributeValue = { +export type SqlMembershipAttributeMapping = { description: string; code: string; }; diff --git a/UI/web-app/src/models/index.ts b/UI/web-app/src/models/index.ts index 2d7506efd..8815d3638 100644 --- a/UI/web-app/src/models/index.ts +++ b/UI/web-app/src/models/index.ts @@ -18,7 +18,7 @@ export * from './SettingKey'; export * from './Status'; export * from './SqlMembershipSource'; export * from './SqlMembershipAttribute'; -export * from './SqlMembershipAttributeValue'; +export * from './SqlMembershipAttributeMapping'; export * from './User'; export * from './PeoplePickerPersona'; export * from './DestinationPickerPersona'; diff --git a/UI/web-app/src/store/sqlMembershipSources.api.tsx b/UI/web-app/src/store/sqlMembershipSources.api.tsx index f6be8eb3e..f14ee3217 100644 --- a/UI/web-app/src/store/sqlMembershipSources.api.tsx +++ b/UI/web-app/src/store/sqlMembershipSources.api.tsx @@ -4,8 +4,8 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { ThunkConfig } from './store'; import { SqlMembershipAttribute, SqlMembershipSource } from '../models'; -import { GetAttributeValuesResponse } from '../models/GetAttributeValuesResponse'; -import { GetAttributeValuesRequest } from '../models/GetAttributeValuesRequest'; +import { GetAttributeMappingsResponse } from '../models/GetAttributeMappingsResponse'; +import { GetAttributeMappingsRequest } from '../models/GetAttributeMappingsRequest'; export const fetchDefaultSqlMembershipSource = createAsyncThunk( 'sqlMembershipSources/fetchDefaultSqlMembershipSource', @@ -33,25 +33,25 @@ export const fetchDefaultSqlMembershipSourceAttributes = createAsyncThunk( - 'fetchSqlFilterAttributeValues', +export const fetchAttributeMappings = createAsyncThunk( + 'fetchSqlFilterAttributeMappings', async (request, { extra }) => { const { gmmApi } = extra.apis; - let payload: GetAttributeValuesResponse; + let payload: GetAttributeMappingsResponse; try { if (request.hasMapping && request.attribute.endsWith("_Code")) { - const response = await gmmApi.sqlMembershipSources.fetchDefaultSqlMembershipSourceAttributeValues(request.attribute.slice(0, -5)); - payload = { values: response, attribute: request.attribute, type: request.type }; + const response = await gmmApi.sqlMembershipSources.fetchDefaultSqlMembershipSourceAttributeMappings(request.attribute.slice(0, -5)); + payload = { mappings: response, attribute: request.attribute, type: request.type }; } else if (request.type === "bit") { - payload = { values: [{ description: "Yes", code: "1" }, { description: "No", code: "0" }], attribute: request.attribute, type: request.type }; + payload = { mappings: [{ description: "Yes", code: "1" }, { description: "No", code: "0" }], attribute: request.attribute, type: request.type }; } else { - payload = { values: [], attribute: request.attribute, type: request.type }; + payload = { mappings: [], attribute: request.attribute, type: request.type }; } return payload; } catch (error) { - payload = { values: [], attribute: request.attribute, type: request.type }; + payload = { mappings: [], attribute: request.attribute, type: request.type }; return payload; } } diff --git a/UI/web-app/src/store/sqlMembershipSources.slice.tsx b/UI/web-app/src/store/sqlMembershipSources.slice.tsx index 0dea54741..a2af3b20d 100644 --- a/UI/web-app/src/store/sqlMembershipSources.slice.tsx +++ b/UI/web-app/src/store/sqlMembershipSources.slice.tsx @@ -2,9 +2,9 @@ // Licensed under the MIT license. import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import { fetchAttributeValues, fetchDefaultSqlMembershipSource, fetchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceCustomLabel } from './sqlMembershipSources.api'; +import { fetchAttributeMappings, fetchDefaultSqlMembershipSource, fetchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceCustomLabel } from './sqlMembershipSources.api'; import type { RootState } from './store'; -import { SqlMembershipAttribute, SqlMembershipAttributeValue, SqlMembershipSource } from '../models'; +import { SqlMembershipAttribute, SqlMembershipAttributeMapping, SqlMembershipSource } from '../models'; export interface SettingsState { @@ -12,10 +12,10 @@ export interface SettingsState { attributes: SqlMembershipAttribute[] | undefined; isSourceLoading: boolean; areAttributesLoading: boolean; - areAttributeValuesLoading: boolean; - attributeValues: { + areAttributeMappingsLoading: boolean; + attributeMappings: { [attribute: string]: { - values: SqlMembershipAttributeValue[]; + mappings: SqlMembershipAttributeMapping[]; type: string | undefined; } }; @@ -29,10 +29,10 @@ export interface SettingsState { const initialState: SettingsState = { source: undefined, attributes: undefined, - attributeValues: {}, + attributeMappings: {}, isSourceLoading: false, areAttributesLoading: false, - areAttributeValuesLoading: false, + areAttributeMappingsLoading: false, isSourceSaving: false, areAttributesSaving: false, error: undefined, @@ -50,10 +50,10 @@ const sqlMembershipSourcesSlice = createSlice({ setAttributes: (state, action: PayloadAction) => { state.attributes = action.payload; }, - setAttributeValues: (state, action) => { - const { attribute, type, values} = action.payload; - state.attributeValues[attribute] = { - values: values, + setAttributeMappings: (state, action) => { + const { attribute, type, mappings} = action.payload; + state.attributeMappings[attribute] = { + mappings: mappings, type: type }; } @@ -84,19 +84,19 @@ const sqlMembershipSourcesSlice = createSlice({ state.error = action.error.message; }); - builder.addCase(fetchAttributeValues.pending, (state) => { - state.areAttributeValuesLoading = true; + builder.addCase(fetchAttributeMappings.pending, (state) => { + state.areAttributeMappingsLoading = true; }); - builder.addCase(fetchAttributeValues.fulfilled, (state, action) => { - state.areAttributeValuesLoading = false; - const { attribute, type, values} = action.payload; - state.attributeValues[attribute] = { - values: values, + builder.addCase(fetchAttributeMappings.fulfilled, (state, action) => { + state.areAttributeMappingsLoading = false; + const { attribute, type, mappings} = action.payload; + state.attributeMappings[attribute] = { + mappings: mappings, type: type }; }); - builder.addCase(fetchAttributeValues.rejected, (state, action) => { - state.areAttributeValuesLoading = false; + builder.addCase(fetchAttributeMappings.rejected, (state, action) => { + state.areAttributeMappingsLoading = false; state.error = action.error.message; }); @@ -133,13 +133,13 @@ const sqlMembershipSourcesSlice = createSlice({ }, }); -export const { setSource, setAttributes, setAttributeValues } = sqlMembershipSourcesSlice.actions; +export const { setSource, setAttributes, setAttributeMappings } = sqlMembershipSourcesSlice.actions; export const selectSource = (state: RootState) => state.sqlMembershipSources.source; export const selectAttributes = (state: RootState) => state.sqlMembershipSources.attributes; -export const selectAttributeValues = (state: RootState) => state.sqlMembershipSources.attributeValues; +export const selectAttributeMappings = (state: RootState) => state.sqlMembershipSources.attributeMappings; export const selectIsSourceLoading = (state: RootState) => state.sqlMembershipSources.isSourceLoading; export const selectAreAttributesLoading = (state: RootState) => state.sqlMembershipSources.areAttributesLoading; -export const selectAreAttributeValuesLoading = (state: RootState) => state.sqlMembershipSources.areAttributeValuesLoading; +export const selectAreAttributeMappingsLoading = (state: RootState) => state.sqlMembershipSources.areAttributeMappingsLoading; export const selectIsSourceSaving = (state: RootState) => state.sqlMembershipSources.isSourceSaving; export const selectAreAttributesSaving = (state: RootState) => state.sqlMembershipSources.areAttributesSaving; From 4faacad4dd0231a26036f9d4fe717dd7cad67f32 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Fri, 9 Aug 2024 13:54:18 -0700 Subject: [PATCH 0174/1479] Added a catch for Exceptions in GMO SubOrchestratorFunction to associate exceptions with runId Trying to catch OutOfMemoryException and NullReferenceException --- .../Function/GroupMembershipObtainer.sln | 6 +++++ .../SubOrchestratorFunction.cs | 6 +++++ .../SubOrchestratorFunctionTests.cs | 25 +++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.sln b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.sln index 6dcd24a28..108ae811b 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.sln +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.sln @@ -42,6 +42,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramewor EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.EntityFramework.Contexts", "..\..\..\Repositories.EntityFramework.Contexts\Repositories.EntityFramework.Contexts.csproj", "{B8E8AFE6-8C99-4AD2-9334-D3BC69E4A55F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.RetryPolicyProvider", "..\..\..\Repositories.RetryPolicyProvider\Repositories.RetryPolicyProvider.csproj", "{4A8D801F-F16D-4E3C-BBE1-921109E9F893}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -128,6 +130,10 @@ Global {B8E8AFE6-8C99-4AD2-9334-D3BC69E4A55F}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8E8AFE6-8C99-4AD2-9334-D3BC69E4A55F}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8E8AFE6-8C99-4AD2-9334-D3BC69E4A55F}.Release|Any CPU.Build.0 = Release|Any CPU + {4A8D801F-F16D-4E3C-BBE1-921109E9F893}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A8D801F-F16D-4E3C-BBE1-921109E9F893}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A8D801F-F16D-4E3C-BBE1-921109E9F893}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A8D801F-F16D-4E3C-BBE1-921109E9F893}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs index e7b89566c..544b300b0 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs @@ -233,6 +233,12 @@ public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurabl await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { SyncJob = request.SyncJob, Status = SyncStatus.TransientError }); throw; } + catch (Exception ex) + { + if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { Message = $"Caught Exception, marking sync job status as error. Exception:\n{ex}", RunId = request.RunId }); + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { SyncJob = request.SyncJob, Status = SyncStatus.Error }); + throw; + } } private void TrackCachedUsersEvent(Guid runId, int cachedUsersCount, Guid groupId) diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs index a8fe84af0..860440263 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs @@ -1059,6 +1059,31 @@ public async Task TestTransientExceptionAsync() ), Times.Once); } + [TestMethod] + public async Task TestOtherExceptionAsync() + { + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + .Throws(); + + var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); + var subOrchestratorFunction = new SubOrchestratorFunction(_deltaCachingConfig, _loggingRepository.Object, telemetryClient); + + await Assert.ThrowsExceptionAsync(async () => await subOrchestratorFunction.RunSubOrchestratorAsync(_durableOrchestrationContext.Object)); + + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message.StartsWith("Caught Exception")), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Once); + + + _syncJobRepository.Verify(x => x.UpdateSyncJobStatusAsync( + It.IsAny>(), + It.Is(s => s == SyncStatus.Error) + ), Times.Once); + } + private async Task CallGroupValidatorFunctionAsync(GroupValidatorRequest request) { var function = new GroupValidatorFunction(_loggingRepository.Object, _membershipCalculator, _emailSenderRecipient.Object); From b924c7525bc2d2ea3e086a1a529b8ec90e22039a Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 12 Aug 2024 16:00:25 -0700 Subject: [PATCH 0175/1479] Updated wording of email button from 'View in GMM UI' to 'Go to GMM UI' since deeplinks not enabled yet --- .../Resources/LocalizationRepository.en-US.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 777791448..89a243a60 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -991,7 +991,7 @@ "actions": [ { "type": "Action.OpenUrl", - "title": "View in GMM UI", + "title": "Go to GMM UI", "url": "${$root.UIUrl}" } ], From 4604d82bbdba636c50fc0f25254252b75b18b88d Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 12 Aug 2024 16:13:28 -0700 Subject: [PATCH 0176/1479] Updated the localization for purging emails with clairty on how to proceed when receiving the email --- .../Orchestrator/OrchestratorFunction.cs | 4 +- .../LocalizationRepository.en-US.resx | 79 +++++++++---------- .../NotificationConstants.cs | 4 +- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs index f9656ddee..3aee74b6b 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs @@ -95,8 +95,8 @@ await context.CallActivityAsync(nameof(LoggerFunction), break; case nameof(NotificationMessageType.InactiveSyncJobNotification): - message.SubjectTemplate = NotificationConstants.SyncDisabledInactivityEmailSubject; - message.ContentTemplate = NotificationConstants.SyncDisabledInactivityEmailBody; + message.SubjectTemplate = NotificationConstants.SyncPurgedForInactivityEmailSubject; + message.ContentTemplate = NotificationConstants.SyncPurgedForInactivityEmailBody; await context.CallActivityAsync(nameof(SendNotification), message); break; diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 89a243a60..977cef947 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -1,17 +1,17 @@  - @@ -1028,9 +1028,9 @@ The group **{1}** with Object Id: {0} **has started its initial sync**. {4} requested that GMM systematically manage this group's membership based on predefined rules. You can find more specifics on the predefined rules in the GMM UI. -The group **{1}** with Id: {0} **has completed its initial sync**. +The group **{1}** with Id: {0} **has completed its initial sync**. -**{2} users were added.** +**{2} users were added.** **{3} users were removed.** @@ -1064,21 +1064,20 @@ Please reach out to support to update the source definition and re-enable the me Contact support if you would like to re-enable GMM. - + You received this notification because you are listed as the owner of **{1}**. -Your group's membership synchronizations are currently disabled due to one of the following reasons: - -• Source and/or destination group(s) no longer exist. -• Group owner did not take action on a drastic membership change alert. -• Membership syncs were paused by owner for more than 30 days. +Your group's affiliation with GMM has been removed for inactivity due to one of the following reasons: -If you believe this group should still be managed by Group Membership Management (GMM), please take the necessary actions via the GMM UI or reach out to support to request assistance before {2}. +* Source and/or destination group(s) no longer exist. +* Group owner did not take action on a drastic membership change alert. +* Membership syncs were paused by owner for more than 30 days. -If not remediated by {2}, your group's membership will still remain, but its affiliation with GMM (including its membership definition) will be removed. You will need to re-onboard if you still want GMM to manage your group after that date. +Please reach out to the **CC'ed alias** for inquiries about re-onboarding or any questions you may have. +You can click on the "Go to GMM UI" option below to check the status of other groups you own and currently manage through GMM. - - [Final Notice] Sync disabled + + [Final Notice] Sync purged Decrease membership threshold {0}%. Detected threshold {1}%. @@ -1152,7 +1151,7 @@ The group sync will be PAUSED until your response is received. You received this notification because you are listed as an owner of **{1}**. -The group **{1}** with Id: {0} **has been set to the status "GuestUsersCannotBeAddedToUnifiedGroup"** because GMM failed to add guest users to the destination, due to the destination not having the right settings to allow guest users to be added by GMM. +The group **{1}** with Id: {0} **has been set to the status "GuestUsersCannotBeAddedToUnifiedGroup"** because GMM failed to add guest users to the destination, due to the destination not having the right settings to allow guest users to be added by GMM. Even though GMM couldn't add the guest users, it still **added the remaining {2} users** and **removed the expected {3} users** from the destination. diff --git a/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs b/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs index 31cb6f7ab..722f7cfd4 100644 --- a/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs +++ b/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs @@ -24,8 +24,8 @@ public static class NotificationConstants public const string SyncThresholdEmailSubject = "SyncThresholdEmailSubject"; public const string SyncThresholdBothEmailBody = "SyncThresholdBothEmailBody"; public const string SyncThresholdDisablingJobEmailSubject = "SyncThresholdDisablingJobEmailSubject"; - public const string SyncDisabledInactivityEmailBody = "SyncDisabledInactivityEmailBody"; - public const string SyncDisabledInactivityEmailSubject = "SyncDisabledInactivityEmailSubject"; + public const string SyncPurgedForInactivityEmailBody = "SyncPurgedForInactivityEmailBody"; + public const string SyncPurgedForInactivityEmailSubject = "SyncPurgedForInactivityEmailSubject"; public const string GuestUserFailureEmailBody = "GuestUserFailureEmailBody"; } } From 4709ed06f0c3622e04477c0c96d79e0078ebc9bb Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 12 Aug 2024 16:38:22 -0700 Subject: [PATCH 0177/1479] Updated MembershipAggregator NoChanges status to still hit the TelemetryFunction with Idle Success status --- .../MembershipSubOrchestratorFunction.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs index 1a71c0d7e..1d49247fd 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs @@ -244,9 +244,16 @@ await context.CallActivityAsync(nameof(LoggerFunction), RunId = request.SyncJob.RunId.GetValueOrDefault() }); + await context.CallActivityAsync(nameof(TelemetryTrackerFunction), new TelemetryTrackerRequest + { + JobStatus = SyncStatus.Idle, + ResultStatus = ResultStatus.Success, + RunId = runId + }); + if (!context.IsReplaying) TrackSyncCompleteEvent(context, dbSyncJob, syncCompleteEvent, "Success"); - + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { From ec30b922dda09cc6eab08800295387f7a9a6d1b5 Mon Sep 17 00:00:00 2001 From: abgonz Date: Tue, 13 Aug 2024 16:39:56 -0700 Subject: [PATCH 0178/1479] Capitalize the first letter of orderBy properties to match expected casing --- UI/web-app/src/apis/jobs/JobsApi.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/UI/web-app/src/apis/jobs/JobsApi.ts b/UI/web-app/src/apis/jobs/JobsApi.ts index 06dd6a19c..04e8dcef3 100644 --- a/UI/web-app/src/apis/jobs/JobsApi.ts +++ b/UI/web-app/src/apis/jobs/JobsApi.ts @@ -37,11 +37,15 @@ export class JobsApi extends ApiBase implements IJobsApi { } private mapPagingOptionsToODataQueryOptions(pagingOptions?: PagingOptions): ODataQueryOptions | undefined { + const capitalizeFirstLetter = (input: string): string => { + return input.charAt(0).toUpperCase() + input.slice(1); + }; + const queryOptions: ODataQueryOptions = pagingOptions ? { $skip: pagingOptions.itemsToSkip, $top: pagingOptions.pageSize, - $orderBy: pagingOptions.orderBy, + $orderBy: pagingOptions.orderBy ? capitalizeFirstLetter(pagingOptions.orderBy) : undefined, $filter: pagingOptions.filter, customSortBy: pagingOptions.customSortBy } From 3d841ab93c149ba39d6fc01995448f9e9e612e36 Mon Sep 17 00:00:00 2001 From: abgonz Date: Tue, 13 Aug 2024 16:46:09 -0700 Subject: [PATCH 0179/1479] Remove local sorting --- .../src/components/JobsList/JobsList.base.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/UI/web-app/src/components/JobsList/JobsList.base.tsx b/UI/web-app/src/components/JobsList/JobsList.base.tsx index 8fa8af654..ca28216bc 100644 --- a/UI/web-app/src/components/JobsList/JobsList.base.tsx +++ b/UI/web-app/src/components/JobsList/JobsList.base.tsx @@ -206,22 +206,6 @@ export const JobsListBase: React.FunctionComponent = ( }, ]; - const sortedItems = [...(items ? items : [])].sort((a, b) => { - if ( - sortKey === 'enabledOrNot' || - sortKey === 'lastSuccessfulRunTime' || - sortKey === 'estimatedNextRunTime' || - sortKey === 'targetGroupName' || - sortKey === 'targetGroupType' || - sortKey === 'actionRequired' - ) { - return isSortedDescending - ? (b[sortKey] || '').toString().localeCompare((a[sortKey] || '').toString()) - : (a[sortKey] || '').toString().localeCompare((b[sortKey] || '').toString()); - } - return 0; - }); - const onContextualItemClicked = ( ev?: React.MouseEvent | React.KeyboardEvent, item?: IContextualMenuItem @@ -397,7 +381,7 @@ export const JobsListBase: React.FunctionComponent = ( Date: Tue, 30 Jul 2024 15:09:12 -0700 Subject: [PATCH 0180/1479] Fixed actionable message unresolved / disabled card tests for consistency to avoid transient failures --- .../WebApi.Tests/NotificationsControllerTests.cs | 12 +++++++++--- .../ThresholdNotificationService.cs | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs index 915f2d6df..77a300182 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/NotificationsControllerTests.cs @@ -459,8 +459,12 @@ public async Task GetNotificationCard_HandleExpiredTestAsync() private void ValidateUnresolvedCard(string cardJson) { Assert.IsTrue(cardJson.Contains($"The most recent attempt to update the membership of your GMM managed group '**{_groupName}**")); - Assert.IsTrue(cardJson.Contains($"GMM has identified **{_thresholdNotification.ChangeQuantityForAdditions}** members to be added, increasing the group size by **{Math.Round(_thresholdNotification.ChangePercentageForAdditions, 1)}%**, which is more than the current additions threshold of **{_thresholdNotification.ThresholdPercentageForAdditions}%**.")); - Assert.IsTrue(cardJson.Contains($"GMM has identified **{_thresholdNotification.ChangeQuantityForRemovals}** members to be removed, decreasing the group size by **{Math.Round(_thresholdNotification.ChangePercentageForRemovals, 1)}%**, which is more than the current removals threshold of **{_thresholdNotification.ThresholdPercentageForRemovals}%**.")); + Assert.IsTrue(cardJson.Contains($"GMM has identified **{_thresholdNotification.ChangeQuantityForAdditions}** members to be added, increasing the group size by **")); + Assert.IsTrue(cardJson.Contains(Math.Round(_thresholdNotification.ChangePercentageForAdditions, 1).ToString())); + Assert.IsTrue(cardJson.Contains($"%**, which is more than the current additions threshold of **{_thresholdNotification.ThresholdPercentageForAdditions}%**.")); + Assert.IsTrue(cardJson.Contains($"GMM has identified **{_thresholdNotification.ChangeQuantityForRemovals}** members to be removed, decreasing the group size by **")); + Assert.IsTrue(cardJson.Contains(Math.Round(_thresholdNotification.ChangePercentageForRemovals, 1).ToString())); + Assert.IsTrue(cardJson.Contains($"%**, which is more than the current removals threshold of **{_thresholdNotification.ThresholdPercentageForRemovals}%**.")); Assert.IsTrue(cardJson.Contains($"https://{_hostname}/api/v1/notifications/{_thresholdNotification.Id}/resolve")); Assert.IsTrue(cardJson.Contains($"\\\"resolution\\\":\\\"{ThresholdNotificationResolution.Paused}\\\"")); Assert.IsTrue(cardJson.Contains($"\\\"resolution\\\":\\\"{ThresholdNotificationResolution.IgnoreOnce}\\\"")); @@ -473,7 +477,9 @@ private void ValidateDisabledCard(string cardJson) Console.WriteLine(cardJson); Assert.IsTrue(cardJson.Contains($"Synchronization of your GMM group **{_groupName}** has been disabled. If no action is taken, the sync will be deleted on ")); Assert.IsTrue(cardJson.Contains("After this period, if you wish to reenable the sync, you will need to follow GMM's onboarding process again.")) ; - Assert.IsTrue(cardJson.Contains($"GMM has identified **{_thresholdNotification.ChangeQuantityForAdditions}** members to be added, increasing the group size by **{Math.Round(_thresholdNotification.ChangePercentageForAdditions, 1)}%**, which is more than the current additions threshold of **{_thresholdNotification.ThresholdPercentageForAdditions}%**.")); + Assert.IsTrue(cardJson.Contains($"GMM has identified **{_thresholdNotification.ChangeQuantityForAdditions}** members to be added, increasing the group size by **")); + Assert.IsTrue(cardJson.Contains(Math.Round(_thresholdNotification.ChangePercentageForAdditions, 1).ToString())); + Assert.IsTrue(cardJson.Contains($"%**, which is more than the current additions threshold of **{_thresholdNotification.ThresholdPercentageForAdditions}%**.")); Assert.IsTrue(cardJson.Contains($"{_thresholdNotification.Id}")); Assert.IsTrue(cardJson.Contains($"\"originator\":\"{_providerId}\"")); } diff --git a/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs b/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs index 35c791ba5..3a8a90d2e 100644 --- a/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs +++ b/Service/GroupMembershipManagement/Services.Notifications/ThresholdNotificationService.cs @@ -51,7 +51,7 @@ public async Task CreateNotificationCardAsync(ThresholdNotification noti } else { - throw new NotSupportedException("Currently the Notifier trigger only supports NextCardState of DefaultCard and DisabledCard. Please check on this card"); + throw new NotSupportedException("Currently the Notifier trigger only supports NextCardState of DefaultCard, DisabledCard, and ExpiredCard. Please check on this card"); } var groupName = await _graphGroupRepository.GetGroupNameAsync(notification.TargetOfficeGroupId); From e9d3223a8f13dbbd440b3073e261480b5fdeea82 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 5 Aug 2024 11:12:00 -0700 Subject: [PATCH 0181/1479] Updated the resolvedByMail to be the actionablemessageviewergroup name if found, otherwise "GMM Support" --- .../ResolveNotificationHandler.cs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs index 39e8b277a..eafc51c98 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/ResolveNotificationHandler.cs @@ -73,20 +73,36 @@ await _loggingRepository.LogMessageAsync(new LogMessage if (thresholdNotification.Status != ThresholdNotificationStatus.Resolved) { - var resolvedByMail = request.UserIdentifier; - + var resolvedByValue = request.UserIdentifier; Guid userId; - if (Guid.TryParse(resolvedByMail, out userId)) + + if (isInAuthorizedGroup) + { + try + { + var groupName = await _graphGroupRepository.GetGroupNameAsync(_gmmEmailReceivers.ActionableMessageViewerGroupId); + resolvedByValue = groupName; + } + catch(Exception e) + { + await _loggingRepository.LogMessageAsync(new LogMessage + { + Message = $"Error getting group name: {e.Message}" + }); + resolvedByValue = "GMM Support"; + } + } + else if (Guid.TryParse(resolvedByValue, out userId)) { var user = await _graphGroupRepository.GetUserByUpnOrIdAsync(userId.ToString(), true); - resolvedByMail = user.Mail; + resolvedByValue = user != null ? user.Mail : request.UserIdentifier; } var resolution = Enum.Parse(request.Resolution); thresholdNotification.Status = ThresholdNotificationStatus.Resolved; thresholdNotification.CardState = ThresholdNotificationCardState.NoCard; thresholdNotification.Resolution = resolution; - thresholdNotification.ResolvedBy = isInAuthorizedGroup ? "GMM Support" : resolvedByMail; + thresholdNotification.ResolvedBy = resolvedByValue; thresholdNotification.ResolvedTime = DateTime.UtcNow; await handleSyncJobResolution(thresholdNotification); From 041f037d30e53168e7ca57e4af399b7a3a784676 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 12 Aug 2024 11:53:53 -0700 Subject: [PATCH 0182/1479] Updated WebApi code coverage --- yaml/build-webapi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yaml/build-webapi.yml b/yaml/build-webapi.yml index e6f2bbf63..17133fbf0 100644 --- a/yaml/build-webapi.yml +++ b/yaml/build-webapi.yml @@ -60,7 +60,7 @@ stages: arguments: '--configuration $(BuildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura - /p:threshold=85 + /p:threshold=84 /p:thresholdType=line /p:thresholdStat=total /p:CoverletOutput=$(Build.SourcesDirectory)\TestResults\Coverage\webapi\ From 97e12e7a9b5d8eff523de466c12849f59ac2c863 Mon Sep 17 00:00:00 2001 From: abgonz Date: Mon, 12 Aug 2024 23:45:55 -0700 Subject: [PATCH 0183/1479] General setting changes --- .../v1/Settings/SettingsController.cs | 2 +- .../Models/SettingConstant.cs | 4 +++- .../Models/SettingKey.cs | 1 + .../GeneralSetting/GeneralSetting.base.tsx | 12 +++++++----- .../GeneralSetting/GeneralSetting.types.ts | 2 ++ UI/web-app/src/models/SettingKey.ts | 2 ++ .../src/pages/AdminConfig/AdminConfig.base.tsx | 16 ++++++++++++++-- .../src/pages/AdminConfig/AdminConfig.types.ts | 2 ++ .../src/pages/AdminConfig/AdminConfig.view.tsx | 17 ++++++++++++++--- UI/web-app/src/store/settings.slice.tsx | 18 ++++++++++++++++++ 10 files changed, 64 insertions(+), 12 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Settings/SettingsController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Settings/SettingsController.cs index 4bb2ece8d..850ff4009 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Settings/SettingsController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/Settings/SettingsController.cs @@ -64,7 +64,7 @@ public async Task GetAllSettingsAsync() } } - [Authorize(Roles = Models.Roles.HYPERLINK_ADMINISTRATOR)] + [Authorize(Roles = $"{Models.Roles.HYPERLINK_ADMINISTRATOR}, {Models.Roles.GENERAL_SETTINGS_ADMINISTRATOR}")] [HttpPatch("{settingKey}")] public async Task PatchSettingAsync(SettingKey settingKey, [FromBody] string settingValue) { diff --git a/Service/GroupMembershipManagement/Models/SettingConstant.cs b/Service/GroupMembershipManagement/Models/SettingConstant.cs index 6a67acf4f..a1fcdb975 100644 --- a/Service/GroupMembershipManagement/Models/SettingConstant.cs +++ b/Service/GroupMembershipManagement/Models/SettingConstant.cs @@ -12,7 +12,9 @@ public class SettingConstants { { SettingKey.DashboardUrl, Guid.Parse("63BA3339-639A-4104-AC63-E1376F0445C9") }, { SettingKey.OutlookWarningUrl, Guid.Parse("DFF1D616-E1E7-4642-B37F-FDE617158A90")}, - { SettingKey.PrivacyPolicyUrl, Guid.Parse("6328107C-7332-47D1-A29C-CF9A49109AB0")} + { SettingKey.PrivacyPolicyUrl, Guid.Parse("6328107C-7332-47D1-A29C-CF9A49109AB0")}, + { SettingKey.UIUrl, Guid.Parse("446FDA16-C27B-4E0C-BF4D-5E563F47FC61")}, + { SettingKey.CanReviewOwnSubmissions, Guid.Parse("F901FC06-E92E-4361-B4CF-7F7283FB312B")} }; } } diff --git a/Service/GroupMembershipManagement/Models/SettingKey.cs b/Service/GroupMembershipManagement/Models/SettingKey.cs index b695b2d0c..cc5f4bd5b 100644 --- a/Service/GroupMembershipManagement/Models/SettingKey.cs +++ b/Service/GroupMembershipManagement/Models/SettingKey.cs @@ -9,5 +9,6 @@ public enum SettingKey OutlookWarningUrl = 1, PrivacyPolicyUrl = 2, UIUrl = 3, + CanReviewOwnSubmissions = 4, } } \ No newline at end of file diff --git a/UI/web-app/src/components/GeneralSetting/GeneralSetting.base.tsx b/UI/web-app/src/components/GeneralSetting/GeneralSetting.base.tsx index 011f4d1ed..97e9fc3be 100644 --- a/UI/web-app/src/components/GeneralSetting/GeneralSetting.base.tsx +++ b/UI/web-app/src/components/GeneralSetting/GeneralSetting.base.tsx @@ -7,18 +7,20 @@ import { useStrings } from '../../store/hooks'; export const getClassNames = classNamesFunction(); export const GeneralSettingBase: React.FunctionComponent = (props: GeneralSettingProps) => { - const { title, description, className, styles} = props; + const { title, description, className, generalSettingValue, onGeneralSettingChange, styles} = props; const classNames = getClassNames(styles, { className, theme: useTheme(), }); const strings = useStrings(); - const [isReviewingOwnSubmissionsEnabled, setIsReviewingOwnSubmissionsEnabled] = useState(true); + const isGeneralSettingEnabled = generalSettingValue === 'true'; + const [isToggleEnabled, setIsToggleEnabled] = useState(isGeneralSettingEnabled); const handleSubmissionReviewerSettingChange = (ev: React.MouseEvent, checked?: boolean) => { - const newStatus = isReviewingOwnSubmissionsEnabled ? false : true; - setIsReviewingOwnSubmissionsEnabled(newStatus); + const wrappedValue = checked ? '"true"' : '"false"'; + onGeneralSettingChange(wrappedValue); + setIsToggleEnabled(checked ?? false); }; return ( @@ -28,7 +30,7 @@ export const GeneralSettingBase: React.FunctionComponent =
diff --git a/UI/web-app/src/components/GeneralSetting/GeneralSetting.types.ts b/UI/web-app/src/components/GeneralSetting/GeneralSetting.types.ts index 053621d28..d658f3bab 100644 --- a/UI/web-app/src/components/GeneralSetting/GeneralSetting.types.ts +++ b/UI/web-app/src/components/GeneralSetting/GeneralSetting.types.ts @@ -21,4 +21,6 @@ export type GeneralSettingProps = React.AllHTMLAttributes & { styles?: IStyleFunctionOrObject; title: string; description: string; + onGeneralSettingChange: (value: string) => void; + generalSettingValue: string; }; diff --git a/UI/web-app/src/models/SettingKey.ts b/UI/web-app/src/models/SettingKey.ts index b185e8f2c..875cca66a 100644 --- a/UI/web-app/src/models/SettingKey.ts +++ b/UI/web-app/src/models/SettingKey.ts @@ -5,4 +5,6 @@ export const enum SettingKey { DashboardUrl = 0, OutlookWarningUrl = 1, PrivacyPolicyUrl = 2, + UIUrl = 3, + CanReviewOwnSubmissions = 4, } diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx index b3555bd58..45573e94b 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx @@ -9,6 +9,8 @@ import { selectIsSaving, selectOutlookWarningUrl, selectPrivacyPolicyUrl, + selectUIUrl, + selectCanReviewOwnSubmissions, } from '../../store/settings.slice'; import { patchSetting } from '../../store/settings.api'; import { AppDispatch } from '../../store'; @@ -38,6 +40,8 @@ export const AdminConfigBase: React.FunctionComponent = (props const dashboardUrl = useSelector(selectDashboardUrl); const outlookWarningUrl = useSelector(selectOutlookWarningUrl); const privacyPolicyUrl = useSelector(selectPrivacyPolicyUrl); + const UIUrl = useSelector(selectUIUrl); + const canReviewOwnSubmissions = useSelector(selectCanReviewOwnSubmissions); const sqlMembershipSource = useSelector(selectSource); const sqlMembershipSourceAttributes = useSelector(selectAttributes); const isSourceSaving = useSelector(selectIsSourceSaving); @@ -53,14 +57,16 @@ export const AdminConfigBase: React.FunctionComponent = (props const generateSettings = () => ({ [SettingKey.DashboardUrl]: dashboardUrl ?? '', [SettingKey.OutlookWarningUrl]: outlookWarningUrl ?? '', - [SettingKey.PrivacyPolicyUrl]: privacyPolicyUrl ?? '' + [SettingKey.PrivacyPolicyUrl]: privacyPolicyUrl ?? '', + [SettingKey.UIUrl]: UIUrl ?? '', + [SettingKey.CanReviewOwnSubmissions]: canReviewOwnSubmissions ? 'true' : 'false', }); const [settings, setSettings] = useState<{ readonly [key in SettingKey]: string }>(generateSettings()); useEffect(() => { setSettings(generateSettings()) - }, [dashboardUrl, outlookWarningUrl, privacyPolicyUrl]); + }, [dashboardUrl, outlookWarningUrl, privacyPolicyUrl, canReviewOwnSubmissions]); // Create an event handler that should be called when the user clicks the save button. const handleSave = (newSettings: { readonly [key in SettingKey]: string }, newSqlMembershipSource: SqlMembershipSource | undefined, newSqlMembershipAttributes: SqlMembershipAttribute[] | undefined) => { @@ -86,6 +92,12 @@ export const AdminConfigBase: React.FunctionComponent = (props settingValue: newSettings[SettingKey.PrivacyPolicyUrl], }) ); + dispatch( + patchSetting({ + settingKey: SettingKey.CanReviewOwnSubmissions, + settingValue: newSettings[SettingKey.CanReviewOwnSubmissions], + }) + ); } if (JSON.stringify(newSqlMembershipSource) !== JSON.stringify(sqlMembershipSource)) { diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts b/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts index 0df169a11..b25c4bc94 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts @@ -69,6 +69,8 @@ export type OperationsProps = { export type GeneralSettingsProps = { classNames: IProcessedStyleSet; strings: IStrings['AdminConfig']; + settings: { readonly [key in SettingKey]: string }; + setSettings: React.Dispatch>; }; export type CustomSourceSettingsProps = { diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx index a08977c21..6431a8fef 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx @@ -137,7 +137,9 @@ export const AdminConfigView: React.FunctionComponent = (p > + strings={strings} + settings={newSettings} + setSettings={setNewSettings} /> } @@ -169,12 +171,19 @@ const Operations: React.FunctionComponent = (props: OperationsP } const GeneralSettings: React.FunctionComponent = (props: GeneralSettingsProps) => { - const { strings} = props; + const { strings, settings, setSettings } = props; + + const handleSettingChange = (settingKey: SettingKey) => (newValue: string) => { + setSettings((settings) => ({ ...settings, [settingKey]: newValue })); + }; + return (
); @@ -187,7 +196,9 @@ const HyperlinkSettings: React.FunctionComponent = (prop const [urlValidations, setUrlValidations] = useState<{ readonly [key in SettingKey]: boolean }>({ [SettingKey.DashboardUrl]: true, [SettingKey.OutlookWarningUrl]: true, - [SettingKey.PrivacyPolicyUrl]: true + [SettingKey.PrivacyPolicyUrl]: true, + [SettingKey.UIUrl]: true, + [SettingKey.CanReviewOwnSubmissions]: true, }); useEffect(() => { diff --git a/UI/web-app/src/store/settings.slice.tsx b/UI/web-app/src/store/settings.slice.tsx index d024eda8d..7f635ed07 100644 --- a/UI/web-app/src/store/settings.slice.tsx +++ b/UI/web-app/src/store/settings.slice.tsx @@ -115,6 +115,24 @@ export const selectPrivacyPolicyUrl = (state: RootState) => { return privacyPolicySetting ? privacyPolicySetting.settingValue : undefined; }; +export const selectUIUrl = (state: RootState) => { + const settingsArray = state.settings.settings; + if (!settingsArray) { + return undefined; + } + const uiSetting = settingsArray.find((setting) => setting.settingKey === SettingKey.UIUrl); + return uiSetting ? uiSetting.settingValue : undefined; +} + +export const selectCanReviewOwnSubmissions = (state: RootState) => { + const settingsArray = state.settings.settings; + if (!settingsArray) { + return undefined; + } + const canReviewOwnSubmissionSetting = settingsArray.find((setting) => setting.settingKey === SettingKey.CanReviewOwnSubmissions); + return canReviewOwnSubmissionSetting ? canReviewOwnSubmissionSetting.settingValue === 'true' : undefined; +} + export const selectPatchSettingResponse = (state: RootState) => state.settings.patchSettingResponse; export const selectPatchSettingError = (state: RootState) => state.settings.patchSettingError; From e1227dfbba8497b5405ff7eb9834b6a65e648792 Mon Sep 17 00:00:00 2001 From: abgonz Date: Thu, 8 Aug 2024 15:36:55 -0700 Subject: [PATCH 0184/1479] Increase test coverage in webapi --- .../DestinationsControllerTests.cs | 97 +++++++++++++++++++ .../WebApi.Tests/JobDetailsControllerTests.cs | 78 +++++++++++++++ .../WebApi.Tests/JobsControllerTests.cs | 34 +++++++ .../WebApi.Tests/SettingsControllerTests.cs | 57 +++++++++++ 4 files changed, 266 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/DestinationsControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/DestinationsControllerTests.cs index eb0cb32f6..aae1b2468 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/DestinationsControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/DestinationsControllerTests.cs @@ -167,6 +167,103 @@ public async Task GetGroupReadyForOnboardingStatusAsync() Assert.AreEqual(OnboardingStatus.ReadyForOnboarding, onboardingStatus); } + [TestMethod] + public async Task GetGroupAppIdNotOwnerStatusAsync() + { + Guid groupNotOnboarded = Guid.NewGuid(); + _syncJobRepository.Setup(x => x.GetSyncJobByObjectIdAsync(It.IsAny())).ReturnsAsync((SyncJob)null); + _graphGroupRepository.Setup(x => x.IsAppIDOwnerOfGroup(It.IsAny(), It.Is(g => g == groupNotOnboarded))).ReturnsAsync(false); + _graphGroupRepository.Setup(x => x.IsEmailRecipientOwnerOfGroupAsync(It.IsAny(), It.Is(g => g == groupNotOnboarded))).ReturnsAsync(true); + + var response = await _destinationController.GetGroupOnboardingStatusAsync(groupNotOnboarded); + var result = response.Result as OkObjectResult; + + Assert.IsNotNull(response); + Assert.IsNotNull(result); + Assert.IsNotNull(result?.Value); + + var onboardingStatus = result.Value; + Assert.IsNotNull(onboardingStatus); + Assert.AreEqual(OnboardingStatus.AppIdNotOwner, onboardingStatus); + } + + [TestMethod] + public async Task GetGroupUserNotOwnerStatusAsync() + { + _destinationController = new DestinationController(_searchDestinationsHandler, _getGroupEndpointsHandler, _getGroupOnboardingStatusHandler) + { + ControllerContext = CreateControllerContext(new List + { + new Claim(ClaimTypes.Name, "user@domain.com"), + new Claim(ClaimTypes.Role, Roles.JOB_OWNER_WRITER), + new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", Guid.NewGuid().ToString()) + }) + }; + + Guid groupNotOnboarded = Guid.NewGuid(); + _syncJobRepository.Setup(x => x.GetSyncJobByObjectIdAsync(It.IsAny())).ReturnsAsync((SyncJob)null); + _graphGroupRepository.Setup(x => x.IsAppIDOwnerOfGroup(It.IsAny(), It.Is(g => g == groupNotOnboarded))).ReturnsAsync(true); + _graphGroupRepository.Setup(x => x.IsEmailRecipientOwnerOfGroupAsync(It.IsAny(), It.Is(g => g == groupNotOnboarded))).ReturnsAsync(false); + + var response = await _destinationController.GetGroupOnboardingStatusAsync(groupNotOnboarded); + var result = response.Result as OkObjectResult; + + Assert.IsNotNull(response); + Assert.IsNotNull(result); + Assert.IsNotNull(result?.Value); + + var onboardingStatus = result.Value; + Assert.IsNotNull(onboardingStatus); + Assert.AreEqual(OnboardingStatus.UserNotOwner, onboardingStatus); + } + + [TestMethod] + public async Task GetGroupOnboardingStatusWhenClaimIsNotFoundAsync() + { + _destinationController = new DestinationController(_searchDestinationsHandler, _getGroupEndpointsHandler, _getGroupOnboardingStatusHandler) + { + ControllerContext = CreateControllerContext(new List + { + new Claim(ClaimTypes.Name, "user@domain.com"), + new Claim(ClaimTypes.Role, Roles.JOB_OWNER_WRITER), + }) + }; + + Guid groupId = Guid.NewGuid(); + _syncJobRepository.Setup(x => x.GetSyncJobByObjectIdAsync(It.IsAny())).ReturnsAsync((SyncJob)null); + _graphGroupRepository.Setup(x => x.IsAppIDOwnerOfGroup(It.IsAny(), It.Is(g => g == groupId))).ReturnsAsync(true); + _graphGroupRepository.Setup(x => x.IsEmailRecipientOwnerOfGroupAsync(It.IsAny(), It.Is(g => g == groupId))).ReturnsAsync(false); + + var response = await _destinationController.GetGroupOnboardingStatusAsync(groupId); + var result = response.Result as ForbidResult; + + Assert.IsNotNull(response); + Assert.IsNotNull(result); + } + + [TestMethod] + public async Task GetGroupOnboardingStatusThrowsExceptionAsync() + { + _destinationController = new DestinationController(_searchDestinationsHandler, _getGroupEndpointsHandler, _getGroupOnboardingStatusHandler) + { + ControllerContext = CreateControllerContext(new List + { + new Claim(ClaimTypes.Name, "user@domain.com"), + new Claim(ClaimTypes.Role, Roles.JOB_OWNER_WRITER), + new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", Guid.NewGuid().ToString()) + }) + }; + + Guid groupNotOnboarded = Guid.NewGuid(); + _syncJobRepository.Setup(x => x.GetSyncJobByObjectIdAsync(It.IsAny())).ThrowsAsync(new Exception("Database error")); + + var response = await _destinationController.GetGroupOnboardingStatusAsync(groupNotOnboarded); + var result = response.Result as ObjectResult; + + Assert.IsNotNull(result); + Assert.AreEqual(500, result?.StatusCode); + } + private ControllerContext CreateControllerContext(HttpContext httpContext) { return new ControllerContext { HttpContext = httpContext }; diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs index e3d8c8619..bfb348994 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobDetailsControllerTests.cs @@ -354,6 +354,33 @@ public async Task PatchJobWhenIsAnOwner(string role) Assert.IsNotNull(result); } + [TestMethod] + public async Task PatchJobThrowsExceptionReturnsInternalServerError() + { + var userId = Guid.NewGuid().ToString(); + var context = CreateHttpContext(new List + { + new Claim(ClaimTypes.Name, "user@domain.com"), + new Claim(ClaimTypes.Role, Roles.JOB_TENANT_WRITER), + new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", userId) + }); + + _httpContextAccessor.Setup(x => x.HttpContext).Returns(context); + _syncJobRepository.Setup(x => x.GetSyncJobAsync(It.IsAny())).ThrowsAsync(new Exception()); + + _patchJobHandler = new PatchJobHandler(_loggingRepository.Object, _graphGroupRepository.Object, _syncJobRepository.Object); + _jobDetailsController = new JobDetailsController(_getJobDetailsHandler, _removeGMMHandler, _patchJobHandler); + + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace(x => x.Status, "Idle"); + + var response = await _jobDetailsController.UpdateSyncJobAsync(Guid.NewGuid(), patchDocument); + var result = response as ObjectResult; + + Assert.IsNotNull(result); + Assert.AreEqual(500, result.StatusCode); + } + [TestMethod] [DataRow(Roles.JOB_OWNER_DELETER)] public async Task RemoveGMMAsyncWhenIsAnAuthorizedUser(string role) @@ -430,6 +457,57 @@ public async Task RemoveGMMAsyncWhenInvalidGroup(string role) Assert.AreEqual(404, result.StatusCode); } + [TestMethod] + [DataRow(Roles.JOB_TENANT_WRITER)] + public async Task RemoveGMMAsyncWhenIsClaimIsNotFound(string role) + { + var syncJobId = Guid.NewGuid(); + + var context = CreateHttpContext(new List { + new Claim(ClaimTypes.Name, "user@domain.com"), + new Claim(ClaimTypes.Role, role) + }); + + _jobDetailsController = new JobDetailsController(_getJobDetailsHandler, _removeGMMHandler, _patchJobHandler) + { + ControllerContext = CreateControllerContext(context) + }; + + _graphGroupRepository.Setup(x => x.IsEmailRecipientOwnerOfGroupAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(() => false); + + var response = await _jobDetailsController.RemoveGMMAsync(syncJobId); + var result = response as ForbidResult; + + Assert.IsInstanceOfType(result, typeof(ForbidResult)); + + _syncJobRepository.Verify(x => x.DeleteSyncJobAsync(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task RemoveGMMThrowsExceptionReturnsInternalServerError() + { + var userId = Guid.NewGuid().ToString(); + var context = CreateHttpContext(new List + { + new Claim(ClaimTypes.Name, "user@domain.com"), + new Claim(ClaimTypes.Role, Roles.JOB_OWNER_DELETER), + new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", userId) + }); + + _httpContextAccessor.Setup(x => x.HttpContext).Returns(context); + + _removeGMMHandler = new RemoveGMMHandler(_loggingRepository.Object, _graphGroupRepository.Object, _syncJobRepository.Object); + _jobDetailsController = new JobDetailsController(_getJobDetailsHandler, _removeGMMHandler, _patchJobHandler); + + _syncJobRepository.Setup(x => x.DeleteSyncJobAsync(It.IsAny())).ThrowsAsync(new Exception()); + var response = await _jobDetailsController.RemoveGMMAsync(Guid.NewGuid()); + var result = response as ObjectResult; + + Assert.IsNotNull(result); + Assert.AreEqual(500, result.StatusCode); + } + private ControllerContext CreateControllerContext(HttpContext httpContext) { return new ControllerContext { HttpContext = httpContext }; diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobsControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobsControllerTests.cs index b281be566..ffee4d7ca 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobsControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/JobsControllerTests.cs @@ -379,6 +379,40 @@ public async Task PostJobCreationFailedTestAsync() var result = response as ObjectResult; Assert.IsNotNull(result); } + + [TestMethod] + public async Task PostJobCreationWhenClaimIsNotFoundTestAsync() + { + _context = CreateHttpContext(new List + { + new Claim(ClaimTypes.Name, "user@domain.com"), + new Claim(ClaimTypes.Role, Roles.JOB_TENANT_WRITER) + }); + + _httpContextAccessor.Setup(x => x.HttpContext).Returns(_context); + + _postJobHandler = new PostJobHandler(_databaseSyncJobsRepository.Object, + _destinationAttributesRepository.Object, + _graphGroupRepository.Object, + _loggingRepository.Object); + + _jobsController = new JobsController(_getJobsHandler, _postJobHandler); + _jobsController.ControllerContext = new ControllerContext + { + HttpContext = _context + }; + + _databaseSyncJobsRepository.Setup(x => x.CreateSyncJobAsync(It.IsAny())) + .ReturnsAsync(Guid.Empty); + + var response = await _jobsController.PostJobAsync(_newSyncJob); + + Assert.IsInstanceOfType(response, typeof(ForbidResult)); + var result = response as ForbidResult; + Assert.IsInstanceOfType(result, typeof(ForbidResult)); + + _databaseSyncJobsRepository.Verify(x => x.CreateSyncJobAsync(It.IsAny()), Times.Never); + } private async IAsyncEnumerable ToAsyncEnumerable(IEnumerable input) { foreach (var value in await Task.FromResult(input)) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SettingsControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SettingsControllerTests.cs index 206d4c824..5db15c032 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SettingsControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SettingsControllerTests.cs @@ -12,6 +12,7 @@ using WebApi.Models; using Services.WebApi; using WebApi.Controllers.v1.Jobs; +using System.Net; namespace Services.Tests { @@ -75,6 +76,17 @@ public async Task GetSettingByKeyTestAsync() Assert.IsNotNull(setting.SettingValue); } + [TestMethod] + public async Task GetSettingByKeyNotFoundTestAsync() + { + _settingsRepository.Setup(x => x.GetSettingByKeyAsync(_settingKey)).ReturnsAsync(() => null); + var response = await _settingsController.GetSettingByKeyAsync(_settingKey); + Assert.IsNotNull(response); + + var result = response as ObjectResult; + Assert.IsNull(result); + } + [TestMethod] public async Task GetAllSettingsTestAsync() { @@ -91,6 +103,38 @@ public async Task GetAllSettingsTestAsync() Assert.AreEqual(settingsResult.Count, _settings.Count); } + [TestMethod] + public async Task GetSettingByKeyExceptionTestAsync() + { + var nonExistentSettingKey = new SettingKey(); + + _settingsRepository.Setup(x => x.GetSettingByKeyAsync(nonExistentSettingKey)) + .ThrowsAsync(new Exception()); + + var response = await _settingsController.GetSettingByKeyAsync(nonExistentSettingKey); + Assert.IsNotNull(response); + + var internalServerErrorResponse = response as StatusCodeResult; + + Assert.IsNotNull(internalServerErrorResponse); + Assert.AreEqual(internalServerErrorResponse.StatusCode, (int)HttpStatusCode.InternalServerError); + } + + [TestMethod] + public async Task GetAllSettingsExceptionTestAsync() + { + _settingsRepository.Setup(x => x.GetAllSettingsAsync()) + .ThrowsAsync(new Exception()); + + var response = await _settingsController.GetAllSettingsAsync(); + Assert.IsNotNull(response); + + var internalServerErrorResponse = response as StatusCodeResult; + + Assert.IsNotNull(internalServerErrorResponse); + Assert.AreEqual(internalServerErrorResponse.StatusCode, (int)HttpStatusCode.InternalServerError); + } + [TestMethod] [DataRow(Roles.HYPERLINK_ADMINISTRATOR)] public async Task PatchSettingWhenHyperlinkAdminTestAsync(string role) @@ -113,6 +157,19 @@ public async Task PatchSettingWhenHyperlinkAdminTestAsync(string role) _settingsRepository.Verify(x => x.PatchSettingAsync(_settingKey, "updatedValue"), Times.Once()); } + + [TestMethod] + public async Task PatchSettingNotFoundTestAsync() + { + var nonExistentSettingKey = SettingKey.UIUrl; + _settingsRepository.Setup(x => x.PatchSettingAsync(nonExistentSettingKey, It.IsAny())) + .ThrowsAsync(new KeyNotFoundException()); + + var response = await _settingsController.PatchSettingAsync(nonExistentSettingKey, "updatedValue"); + + Assert.IsInstanceOfType(response, typeof(NotFoundResult)); + } + private ControllerContext CreateControllerContext(HttpContext httpContext) { return new ControllerContext { HttpContext = httpContext }; From 1ef8b8da48d0cf2682ac9e89c598e625a12fb823 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 19 Aug 2024 14:46:12 -0700 Subject: [PATCH 0185/1479] Adjust job trigger threshold metrics --- .../Activity/GetJobs/GetJobsFunction.cs | 11 +-- .../Services.Contracts/IJobTriggerService.cs | 2 +- .../Services.Tests/JobTriggerServiceTests.cs | 72 ++----------------- .../OrchestratorFunctionTests.cs | 34 ++------- .../JobTrigger/Services/JobTriggerService.cs | 31 +++++--- 5 files changed, 35 insertions(+), 115 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/GetJobs/GetJobsFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/GetJobs/GetJobsFunction.cs index 8b91e4575..621f132be 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/GetJobs/GetJobsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/GetJobs/GetJobsFunction.cs @@ -27,17 +27,8 @@ public GetJobsFunction(IJobTriggerService jobTriggerService, ILoggingRepository public async Task> GetJobsToUpdateAsync([ActivityTrigger] object obj) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GetJobsFunction)} function started at: {DateTime.UtcNow}" }, VerbosityLevel.DEBUG); - var (tableQuery, jobTriggerThresholdExceeded, maxJobsAllowed) = await _jobTriggerService.GetSyncJobsAsync(); + var tableQuery = await _jobTriggerService.GetSyncJobsAsync(); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GetJobsFunction)} function completed at: {DateTime.UtcNow}" }, VerbosityLevel.DEBUG); - - if (jobTriggerThresholdExceeded) - { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GetJobsFunction)} function is proceeding with {maxJobsAllowed} jobs due to JobTrigger threshold limit exceeded." }, VerbosityLevel.DEBUG); - return tableQuery.OrderBy(job => job.StartDate).Take(maxJobsAllowed).ToList(); - } - - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GetJobsFunction)} number of jobs about to be returned: {tableQuery.Count}" }, VerbosityLevel.DEBUG); - return tableQuery; } } diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/IJobTriggerService.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/IJobTriggerService.cs index bbf399c86..f7c849e18 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/IJobTriggerService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/IJobTriggerService.cs @@ -12,7 +12,7 @@ namespace Services.Contracts public interface IJobTriggerService { public Guid RunId { get; set; } - Task<(List jobs, bool jobTriggerThresholdExceeded, int maxJobsAllowed)> GetSyncJobsAsync(); + Task> GetSyncJobsAsync(); Task<(bool IsValid, string DestinationObject)> ParseAndValidateDestinationAsync(SyncJob syncJob); Task GetDestinationNameAsync(SyncJob job); Task SendEmailAsync(SyncJob job, NotificationMessageType notificationType, string[] additionalContentParameters); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/JobTriggerServiceTests.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/JobTriggerServiceTests.cs index 0963dd654..9e77ea33c 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/JobTriggerServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/JobTriggerServiceTests.cs @@ -325,8 +325,7 @@ public async Task VerifyJobsWithValidStartDateAreProcessed() _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(getDestinationObjectId(x))); _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(getDestinationObjectId(x))); - var response = await _jobTriggerService.GetSyncJobsAsync(); - var jobs = response.jobs; + var jobs = await _jobTriggerService.GetSyncJobsAsync(); var jobsToProcessCount = _serviceBusTopicsRepository.Subscriptions.Sum(x => x.Value.Count); @@ -345,8 +344,7 @@ public async Task VerifyJobsWithValidScheduledDateAreProcessed() _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(getDestinationObjectId(x))); _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(getDestinationObjectId(x))); - var response = await _jobTriggerService.GetSyncJobsAsync(); - var jobs = response.jobs; + var jobs = await _jobTriggerService.GetSyncJobsAsync(); var jobsToProcessCount = _serviceBusTopicsRepository.Subscriptions.Sum(x => x.Value.Count); @@ -365,8 +363,7 @@ public async Task VerifyJobsWithValidPeriodsAreProcessed() _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(getDestinationObjectId(x))); _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(getDestinationObjectId(x))); - var response = await _jobTriggerService.GetSyncJobsAsync(); - var jobs = response.jobs; + var jobs = await _jobTriggerService.GetSyncJobsAsync(); var jobsToProcessCount = _serviceBusTopicsRepository.Subscriptions.Sum(x => x.Value.Count); @@ -510,8 +507,7 @@ public async Task VerifyInitialSyncEmailNotificationIsSent() _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(getDestinationObjectId(x))); _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(getDestinationObjectId(x))); - var response = await _jobTriggerService.GetSyncJobsAsync(); - var jobs = response.jobs; + var jobs = await _jobTriggerService.GetSyncJobsAsync(); foreach (var job in jobs) { @@ -560,8 +556,7 @@ public async Task VerifyJobsAreProcessedWithMissingMailSendPermission() _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(getDestinationObjectId(x))); _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(getDestinationObjectId(x))); - var response = await _jobTriggerService.GetSyncJobsAsync(); - var jobs = response.jobs; + var jobs = await _jobTriggerService.GetSyncJobsAsync(); foreach (var job in jobs) { @@ -602,8 +597,7 @@ public async Task VerifyJobsAreProcessedWithMissingMailLicenses() _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(getDestinationObjectId(x))); _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(getDestinationObjectId(x))); - var response = await _jobTriggerService.GetSyncJobsAsync(); - var jobs = response.jobs; + var jobs = await _jobTriggerService.GetSyncJobsAsync(); foreach (var job in jobs) { @@ -643,8 +637,7 @@ public async Task VerifyJobsAreProcessedMailingExceptions() _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(getDestinationObjectId(x))); _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(getDestinationObjectId(x))); - var response = await _jobTriggerService.GetSyncJobsAsync(); - var jobs = response.jobs; + var jobs = await _jobTriggerService.GetSyncJobsAsync(); foreach (var job in jobs) { @@ -654,57 +647,6 @@ public async Task VerifyJobsAreProcessedMailingExceptions() Assert.AreEqual(validStartDateJobs, jobs.Count); } - [TestMethod] - public async Task VerifyJobsCountExceedMinimalNumberHigherThanThreshold() - { - var jobsProceedNow = 5; - var jobsNotProceedNow = 3; - - _syncJobRepository.Jobs.AddRange(SampleDataHelper.CreateSampleSyncJobs(jobsProceedNow, Organization)); - _syncJobRepository.Jobs.AddRange(SampleDataHelper.CreateSampleSyncJobs(jobsNotProceedNow, Organization, startDateBase: DateTime.UtcNow.AddDays(5))); - - _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(x.TargetOfficeGroupId)); - _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(x.TargetOfficeGroupId)); - - var response = await _jobTriggerService.GetSyncJobsAsync(); - var jobTriggerThresholdExceeded = response.jobTriggerThresholdExceeded; - Assert.AreEqual(true, jobTriggerThresholdExceeded); - } - - [TestMethod] - public async Task VerifyJobsCountExceedMinimalNumberLowerThanThreshold() - { - var jobsProceedNow = 5; - var jobsNotProceedNow = 20; - - _syncJobRepository.Jobs.AddRange(SampleDataHelper.CreateSampleSyncJobs(jobsProceedNow, Organization)); - _syncJobRepository.Jobs.AddRange(SampleDataHelper.CreateSampleSyncJobs(jobsNotProceedNow, Organization, startDateBase: DateTime.UtcNow.AddDays(5))); - - _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(x.TargetOfficeGroupId)); - _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(x.TargetOfficeGroupId)); - - var response = await _jobTriggerService.GetSyncJobsAsync(); - var jobTriggerThresholdExceeded = response.jobTriggerThresholdExceeded; - Assert.AreEqual(false, jobTriggerThresholdExceeded); - } - - [TestMethod] - public async Task VerifyJobsCountLowerThanMinimalHigherThanThreshold() - { - var jobsProceedNow = 3; - var jobsNotProceedNow = 3; - - _syncJobRepository.Jobs.AddRange(SampleDataHelper.CreateSampleSyncJobs(jobsProceedNow, Organization)); - _syncJobRepository.Jobs.AddRange(SampleDataHelper.CreateSampleSyncJobs(jobsNotProceedNow, Organization, startDateBase: DateTime.UtcNow.AddDays(5))); - - _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(x.TargetOfficeGroupId)); - _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(x.TargetOfficeGroupId)); - - var response = await _jobTriggerService.GetSyncJobsAsync(); - var jobTriggerThresholdExceeded = response.jobTriggerThresholdExceeded; - Assert.AreEqual(false, jobTriggerThresholdExceeded); - } - private class MockEmail : IEmailSenderRecipient { public string SenderAddress => ""; diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs index 8e2dbbe68..9575dc75d 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs @@ -35,7 +35,7 @@ public async Task ValidOrchestratorRunAsync() bool jobTriggerThresholdExceeded = false; int maxJobsAllowed = syncJobs.Count; jobTriggerService.Setup(x => x.GetSyncJobsAsync()) - .ReturnsAsync((syncJobs, jobTriggerThresholdExceeded, maxJobsAllowed)); + .ReturnsAsync(syncJobs); context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(GetJobsFunction)), It.IsAny())) .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); @@ -65,7 +65,7 @@ public async Task ZeroJobsRetrieved() bool jobTriggerThresholdExceeded = false; int maxJobsAllowed = syncJobs.Count; jobTriggerService.Setup(x => x.GetSyncJobsAsync()) - .ReturnsAsync((syncJobs, jobTriggerThresholdExceeded, maxJobsAllowed)); + .ReturnsAsync((syncJobs)); context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(GetJobsFunction)), It.IsAny())) .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); @@ -77,32 +77,6 @@ public async Task ZeroJobsRetrieved() Times.Exactly(syncJobs.Count)); } - [TestMethod] - public async Task jobTriggerThresholdExceededTrue() - { - var loggingRepository = new Mock(); - var graphRepository = new Mock(); - var jobTriggerService = new Mock(); - var context = new Mock(); - var syncJobs = SampleDataHelper.CreateSampleSyncJobs(10, "GroupMembership"); - var emptySyncJobsList = new List(); - var loggerJobProperties = new Dictionary(); - loggingRepository.SetupGet(x => x.SyncJobProperties).Returns(loggerJobProperties); - bool jobTriggerThresholdExceeded = true; - int maxJobsAllowed = 1; - jobTriggerService.Setup(x => x.GetSyncJobsAsync()) - .ReturnsAsync((syncJobs, jobTriggerThresholdExceeded, maxJobsAllowed)); - context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(GetJobsFunction)), It.IsAny())) - .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); - - context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x == nameof(SubOrchestratorFunction)), It.IsAny())); - var orchestrator = new OrchestratorFunction(loggingRepository.Object); - await orchestrator.RunOrchestratorAsync(context.Object); - - context.Verify(x => x.CallSubOrchestratorAsync(nameof(SubOrchestratorFunction), It.IsAny()), - Times.Exactly(1)); - } - [TestMethod] public async Task NoContinuationTokenRetrieved() { @@ -119,7 +93,7 @@ public async Task NoContinuationTokenRetrieved() bool jobTriggerThresholdExceeded = false; int maxJobsAllowed = syncJobs.Count; jobTriggerService.Setup(x => x.GetSyncJobsAsync()) - .ReturnsAsync((syncJobs, jobTriggerThresholdExceeded, maxJobsAllowed)); + .ReturnsAsync((syncJobs)); context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(GetJobsFunction)), It.IsAny())) .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); @@ -149,7 +123,7 @@ public async Task MultipleBatchesRetrieved() bool jobTriggerThresholdExceeded = false; int maxJobsAllowed = 2; jobTriggerService.Setup(x => x.GetSyncJobsAsync()) - .ReturnsAsync(() => (syncJobs1.Concat(syncJobs2).ToList(), jobTriggerThresholdExceeded, maxJobsAllowed)); + .ReturnsAsync(() => (syncJobs1.Concat(syncJobs2).ToList() )); context.SetupSequence(x => x.CallActivityAsync>(nameof(GetJobsFunction), It.IsAny())) .ReturnsAsync(() => diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs index 88d4a44f5..27d7417f2 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs @@ -25,8 +25,10 @@ public class JobTriggerService : IJobTriggerService enum Metric { - SyncJobsCount, - TotalSyncJobsCount + PotentialSyncJobCount, + TotalSyncJobsCount, + ActiveInProgressJobCount, + JobsDueToRunCount } private readonly ILoggingRepository _loggingRepository; @@ -91,18 +93,29 @@ TelemetryClient telemetryClient _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - public async Task<(List jobs, bool jobTriggerThresholdExceeded, int maxJobsAllowed)> GetSyncJobsAsync() + public async Task> GetSyncJobsAsync() { var jobs = await _databaseSyncJobsRepository.GetSyncJobsAsync(false, SyncStatus.Idle, SyncStatus.InProgress, SyncStatus.StuckInProgress, SyncStatus.TransientError); - var filteredJobs = ApplyJobTriggerFilters(jobs).ToList(); - var jobsExcludingFiltered = jobs.Except(filteredJobs).ToList(); + var jobsDueToRun = ApplyJobTriggerFilters(jobs).ToList(); + var jobsExcludingFiltered = jobs.Except(jobsDueToRun).ToList(); var activeInProgressJobs = jobsExcludingFiltered.Where(job => job.Status == SyncStatus.InProgress.ToString()).ToList(); - var syncJobsCount = filteredJobs.Count + activeInProgressJobs.Count; + var potentialSyncJobCount = jobsDueToRun.Count + activeInProgressJobs.Count; var totalSyncJobsCount = await _databaseSyncJobsRepository.GetSyncJobCountAsync(SyncStatus.All); - _telemetryClient.TrackMetric(nameof(Metric.SyncJobsCount), syncJobsCount); + _telemetryClient.TrackMetric(nameof(Metric.PotentialSyncJobCount), potentialSyncJobCount); _telemetryClient.TrackMetric(nameof(Metric.TotalSyncJobsCount), totalSyncJobsCount); - var jobTriggerThresholdExceeded = HasJobTriggerThresholdExceeded(syncJobsCount, totalSyncJobsCount); - return (filteredJobs, jobTriggerThresholdExceeded, _jobTriggerConfig.JobCountThreshold); + _telemetryClient.TrackMetric(nameof(Metric.ActiveInProgressJobCount), activeInProgressJobs.Count); + _telemetryClient.TrackMetric(nameof(Metric.JobsDueToRunCount), jobsDueToRun.Count); + _telemetryClient.TrackMetric(nameof(Metric.JobsDueToRunCount), jobsDueToRun.Count); + + var jobTriggerThresholdExceeded = HasJobTriggerThresholdExceeded(potentialSyncJobCount, totalSyncJobsCount); + + var jobsToBeStarted = jobsDueToRun; + if (jobTriggerThresholdExceeded) + { + jobsToBeStarted = jobsToBeStarted.OrderBy(job => job.StartDate).Take(_jobTriggerConfig.JobCountThreshold).ToList(); + } + + return jobsDueToRun; } public async Task GetDestinationNameAsync(SyncJob job) { From 6d3eefb2332bf4110a768a501d80f124a004f4a5 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 19 Aug 2024 14:56:21 -0700 Subject: [PATCH 0186/1479] job metric adjusted --- .../Hosts/JobTrigger/Services/JobTriggerService.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs index 27d7417f2..fe287a345 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs @@ -28,7 +28,8 @@ enum Metric PotentialSyncJobCount, TotalSyncJobsCount, ActiveInProgressJobCount, - JobsDueToRunCount + JobsDueToRunCount, + JobsToBeStarted } private readonly ILoggingRepository _loggingRepository; @@ -101,11 +102,11 @@ public async Task> GetSyncJobsAsync() var activeInProgressJobs = jobsExcludingFiltered.Where(job => job.Status == SyncStatus.InProgress.ToString()).ToList(); var potentialSyncJobCount = jobsDueToRun.Count + activeInProgressJobs.Count; var totalSyncJobsCount = await _databaseSyncJobsRepository.GetSyncJobCountAsync(SyncStatus.All); - _telemetryClient.TrackMetric(nameof(Metric.PotentialSyncJobCount), potentialSyncJobCount); - _telemetryClient.TrackMetric(nameof(Metric.TotalSyncJobsCount), totalSyncJobsCount); + _telemetryClient.TrackMetric(nameof(Metric.ActiveInProgressJobCount), activeInProgressJobs.Count); _telemetryClient.TrackMetric(nameof(Metric.JobsDueToRunCount), jobsDueToRun.Count); - _telemetryClient.TrackMetric(nameof(Metric.JobsDueToRunCount), jobsDueToRun.Count); + _telemetryClient.TrackMetric(nameof(Metric.PotentialSyncJobCount), potentialSyncJobCount); + _telemetryClient.TrackMetric(nameof(Metric.TotalSyncJobsCount), totalSyncJobsCount); var jobTriggerThresholdExceeded = HasJobTriggerThresholdExceeded(potentialSyncJobCount, totalSyncJobsCount); @@ -114,8 +115,9 @@ public async Task> GetSyncJobsAsync() { jobsToBeStarted = jobsToBeStarted.OrderBy(job => job.StartDate).Take(_jobTriggerConfig.JobCountThreshold).ToList(); } + _telemetryClient.TrackMetric(nameof(Metric.JobsToBeStarted), jobsToBeStarted.Count); - return jobsDueToRun; + return jobsToBeStarted; } public async Task GetDestinationNameAsync(SyncJob job) { From 9ae4b7a0a2e71ef6d58bf63bc98ebbf3e3b6d5c2 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 19 Aug 2024 15:26:43 -0700 Subject: [PATCH 0187/1479] Fix test --- .../Repositories.Mocks/MockJobTriggerConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/MockJobTriggerConfig.cs b/Service/GroupMembershipManagement/Repositories.Mocks/MockJobTriggerConfig.cs index 3b4b9defc..fd167c658 100644 --- a/Service/GroupMembershipManagement/Repositories.Mocks/MockJobTriggerConfig.cs +++ b/Service/GroupMembershipManagement/Repositories.Mocks/MockJobTriggerConfig.cs @@ -15,7 +15,7 @@ public MockJobTriggerConfig() { GMMHasGroupReadWriteAllPermissions = false; GMMHasChannelReadWriteAllPermissions = false; - JobCountThreshold = 4; + JobCountThreshold = 5; JobPerMilleThreshold = 250; } From f5b160a250ab97dbf92c1e68a797e1a53dd194bc Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Mon, 19 Aug 2024 16:06:25 -0700 Subject: [PATCH 0188/1479] Updated JT's service GetSyncJobsAsync to sort by LastRunTime instead of StartDate for threshold count. --- .../Hosts/JobTrigger/Services/JobTriggerService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs index fe287a345..f4b46d86a 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs @@ -113,7 +113,7 @@ public async Task> GetSyncJobsAsync() var jobsToBeStarted = jobsDueToRun; if (jobTriggerThresholdExceeded) { - jobsToBeStarted = jobsToBeStarted.OrderBy(job => job.StartDate).Take(_jobTriggerConfig.JobCountThreshold).ToList(); + jobsToBeStarted = jobsToBeStarted.OrderBy(job => job.LastRunTime).Take(_jobTriggerConfig.JobCountThreshold).ToList(); } _telemetryClient.TrackMetric(nameof(Metric.JobsToBeStarted), jobsToBeStarted.Count); From 694e6f8c3feacfbf6713ee55f1dc0e01141499fa Mon Sep 17 00:00:00 2001 From: abgonz Date: Mon, 26 Aug 2024 10:17:49 -0700 Subject: [PATCH 0189/1479] Patch: hide viewDetails button from non job writers --- UI/web-app/src/pages/JobDetails/JobDetails.base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx index 2832a1755..fea8c638d 100644 --- a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx +++ b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx @@ -215,7 +215,7 @@ export const JobDetailsBase: React.FunctionComponent = ( children={} removeButton={isJobWriter} editButton={canEditJob} - actionText={canEditJob ? strings.JobDetails.editButton : strings.JobDetails.viewDetails} + actionText={canEditJob ? strings.JobDetails.editButton : isJobWriter ? strings.JobDetails.viewDetails : ''} useLinkButton={true} actionOnClick={openMembershipConfiguration} /> From 7f31886dd6a6fc71a1632f6f87084c51bffa84ee Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 20 Aug 2024 14:34:29 -0700 Subject: [PATCH 0190/1479] Added SubmissionRejected to the list of ActionRequired statuses --- .../src/components/JobsListFilter/JobsListFilter.base.tsx | 4 ++++ UI/web-app/src/services/localization/IStrings.ts | 1 + .../src/services/localization/i18n/locales/en/translations.ts | 3 ++- .../src/services/localization/i18n/locales/es/translations.ts | 4 ++-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx b/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx index 167c03a50..52a045553 100644 --- a/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx +++ b/UI/web-app/src/components/JobsListFilter/JobsListFilter.base.tsx @@ -123,6 +123,10 @@ export const JobsListFilterBase: React.FunctionComponent = { key: SyncStatus.PendingReview, text: strings.JobsList.JobsListFilter.filters.actionRequired.options.pendingReview, + }, + { + key: SyncStatus.SubmissionRejected, + text: strings.JobsList.JobsListFilter.filters.actionRequired.options.submissionRejected, } ]; diff --git a/UI/web-app/src/services/localization/IStrings.ts b/UI/web-app/src/services/localization/IStrings.ts index d55f5ab66..9435fec67 100644 --- a/UI/web-app/src/services/localization/IStrings.ts +++ b/UI/web-app/src/services/localization/IStrings.ts @@ -258,6 +258,7 @@ export type IStrings = { notOwnerOfDestinationGroup: string; securityGroupNotFound: string; pendingReview: string; + submissionRejected: string; }; }; destinationType: { diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index 19924e73d..da0788e76 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -107,7 +107,7 @@ export const strings: IStrings = { operations: "Operations", title: "GMM Control Panel", description: "Use the stop button to halt GMM operations and the reset button to reboot GMM at any time.", - }, + }, buttons: { stop: "Stop GMM", stopping: "Stopping", @@ -263,6 +263,7 @@ export const strings: IStrings = { notOwnerOfDestinationGroup: 'Not Owner Of Destination Group', securityGroupNotFound: 'Security Group Not Found', pendingReview: 'Pending Review', + submissionRejected: 'Submission Rejected', }, }, destinationType: { diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index 7ef6734b2..4cdff8e83 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -263,10 +263,10 @@ export const strings: IStrings = { customerPaused: 'Pausado por el cliente', membershipDataNotFound: 'Datos de membresía no encontrados', destinationGroupNotFound: 'Grupo de destino no encontrado', - notOwnerOfDestinationGroup: - 'No es propietario del grupo de destino', + notOwnerOfDestinationGroup: 'No es propietario del grupo de destino', securityGroupNotFound: 'Grupo de seguridad no encontrado', pendingReview: 'Revisión pendiente', + submissionRejected: 'Solicitud rechazada', }, }, destinationType: { From 035844ccdaf1035827b0d19d4c721936109b8e3b Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 20 Aug 2024 15:05:10 -0700 Subject: [PATCH 0191/1479] Updated canEditJob to allow edit if job status is SubmissionRejected --- UI/web-app/src/pages/JobDetails/JobDetails.base.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx index fea8c638d..ab180ab20 100644 --- a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx +++ b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx @@ -100,7 +100,7 @@ export const JobDetailsBase: React.FunctionComponent = ( const isJobWriter = useSelector(selectIsJobWriter); const isJobOwnerDeleter: boolean = useSelector(selectIsJobOwnerDeleter); const canDeleteJob: boolean = isJobWriter || isJobOwnerDeleter; - const canEditJob: boolean = isJobWriter && (job.status !== SyncStatus.PendingReview && job.status !== SyncStatus.SubmissionRejected); + const canEditJob: boolean = isJobWriter && job.status !== SyncStatus.PendingReview; const showLoader: boolean = jobsLoading || removeGMMPending; const OpenInNewWindowIcon: IIconProps = { iconName: 'OpenInNewWindow' }; @@ -529,7 +529,7 @@ const MembershipConfiguration: React.FunctionComponent = ( fontWeight: 100 } const SQL_MIN_DATE = new Date('1753-01-01T00:00:00'); - + function splitDateString(value: string) { const isEmpty = value === ''; if (isEmpty) { From 73824cd9a5690339d7ca3151ccf25941c6cea9f2 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 21 Aug 2024 11:16:26 -0700 Subject: [PATCH 0192/1479] Updated user experience so that when submission is rejected or approved, the user can edit the job. --- .../src/pages/JobDetails/JobDetails.base.tsx | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx index ab180ab20..2d378a92b 100644 --- a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx +++ b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx @@ -68,6 +68,12 @@ export interface IContentProps extends React.AllHTMLAttributes { classNames: IProcessedStyleSet } +export interface IStatusContentProps extends React.AllHTMLAttributes { + job: Job, + resolveReview: () => void, + classNames: IProcessedStyleSet +} + const getClassNames = classNamesFunction< IJobDetailsStyleProps, IJobDetailsStyles @@ -100,11 +106,15 @@ export const JobDetailsBase: React.FunctionComponent = ( const isJobWriter = useSelector(selectIsJobWriter); const isJobOwnerDeleter: boolean = useSelector(selectIsJobOwnerDeleter); const canDeleteJob: boolean = isJobWriter || isJobOwnerDeleter; - const canEditJob: boolean = isJobWriter && job.status !== SyncStatus.PendingReview; + const [canEditJob, setCanEditJob] = useState(isJobWriter && job.status !== SyncStatus.PendingReview); const showLoader: boolean = jobsLoading || removeGMMPending; const OpenInNewWindowIcon: IIconProps = { iconName: 'OpenInNewWindow' }; + const resolveReview = () => { + setCanEditJob(isJobWriter); + }; + const onMessageBarDismiss = (): void => { dispatch(setGetJobDetailsError()); }; @@ -191,7 +201,7 @@ export const JobDetailsBase: React.FunctionComponent = ( } + children={} removeButton={true} /> = ( ) } -const MembershipStatusContent: React.FunctionComponent = ( - props: IContentProps +const MembershipStatusContent: React.FunctionComponent = ( + props: IStatusContentProps ) => { const dispatch = useDispatch(); const strings = useStrings(); - const { job, classNames } = props; + const { job, resolveReview, classNames } = props; const isSubmissionReviewer = useSelector(selectIsSubmissionReviewer); const patchError = useSelector(selectPatchJobDetailsError); const patchResponse = useSelector(selectPatchJobDetailsResponse); @@ -331,6 +341,7 @@ const MembershipStatusContent: React.FunctionComponent = ( const handleApproveSubmission = (approved: boolean) => { const statusBasedOnReview = approved ? SyncStatus.Idle : SyncStatus.SubmissionRejected; updateJobStatus(statusBasedOnReview); + resolveReview(); }; const displayMessage = (errorCode: string | undefined): string | undefined => { From 1d378f35f13d3edd362a974a07e5ca675328d761 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 21 Aug 2024 11:34:39 -0700 Subject: [PATCH 0193/1479] Moved export of types to the JobDetails.types.ts file --- .../src/pages/JobDetails/JobDetails.base.tsx | 16 ++-------------- .../src/pages/JobDetails/JobDetails.types.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx index 2d378a92b..cbc383990 100644 --- a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx +++ b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx @@ -50,8 +50,9 @@ import { type IJobDetailsProps, type IJobDetailsStyleProps, type IJobDetailsStyles, + type IContentProps, + type IStatusContentProps, } from './JobDetails.types'; -import { JobDetails } from '../../models/JobDetails'; import { useStrings } from '../../store/hooks'; import { setPagingBarVisible } from '../../store/pagingBar.slice'; import { selectIsJobOwnerDeleter, selectIsJobOwnerEnabler, selectIsJobWriter, selectIsSubmissionReviewer } from '../../store/roles.slice'; @@ -61,19 +62,6 @@ import { fetchJobs } from '../../store/jobs.api'; import { Loader } from '../../components/Loader'; import { setIsEditingExistingJob } from '../../store/manageMembership.slice'; - -export interface IContentProps extends React.AllHTMLAttributes { - job: Job, - jobDetails?: JobDetails, - classNames: IProcessedStyleSet -} - -export interface IStatusContentProps extends React.AllHTMLAttributes { - job: Job, - resolveReview: () => void, - classNames: IProcessedStyleSet -} - const getClassNames = classNamesFunction< IJobDetailsStyleProps, IJobDetailsStyles diff --git a/UI/web-app/src/pages/JobDetails/JobDetails.types.ts b/UI/web-app/src/pages/JobDetails/JobDetails.types.ts index 2536d9231..62eccde1e 100644 --- a/UI/web-app/src/pages/JobDetails/JobDetails.types.ts +++ b/UI/web-app/src/pages/JobDetails/JobDetails.types.ts @@ -5,9 +5,13 @@ import { type IStyle, type IStyleFunctionOrObject, type ITheme, + type IProcessedStyleSet } from '@fluentui/react'; import type React from 'react'; +import { type Job } from '../../models/Job'; +import { JobDetails } from '../../models/JobDetails'; + export interface IJobDetailsStyles { root: IStyle; itemTitle: IStyle; @@ -44,3 +48,15 @@ export interface IJobDetailsProps */ styles?: IStyleFunctionOrObject; } + +export interface IContentProps extends React.AllHTMLAttributes { + job: Job, + jobDetails?: JobDetails, + classNames: IProcessedStyleSet +} + +export interface IStatusContentProps extends React.AllHTMLAttributes { + job: Job, + resolveReview: () => void, + classNames: IProcessedStyleSet +} \ No newline at end of file From 7ceb0a85ec699e38c03632b8869d43dee973948c Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 13 Aug 2024 10:24:05 -0700 Subject: [PATCH 0194/1479] Migrate to isolated model --- .../Hosts.FunctionBase/CommonProgramBase.cs | 271 ++++++++++++++++++ .../CreateThresholdNotificationFunction.cs | 5 +- .../Activity/Logger/LoggerFunction.cs | 5 +- .../RetrieveNotificationsFunction.cs | 5 +- .../SendNormalThresholdNotification.cs | 5 +- .../SendNotification/SendNotification.cs | 5 +- .../SendThresholdNotification.cs | 5 +- .../UpdateNotificationStatusFunction.cs | 5 +- .../Hosts/Notifier/Function/Notifier.csproj | 15 +- .../Orchestrator/OrchestratorFunction.cs | 8 +- .../Hosts/Notifier/Function/Program.cs | 103 +++++++ .../Function/Starter/StarterFunction.cs | 11 +- .../Hosts/Notifier/Function/Startup.cs | 110 ------- .../Hosts/Notifier/Function/host.json | 17 +- .../OrchestratorTests.cs | 24 +- 15 files changed, 433 insertions(+), 161 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs create mode 100644 Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/Notifier/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs new file mode 100644 index 000000000..2ad24a3bf --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Azure.Identity; +using Common.DependencyInjection; +using DIConcreteTypes; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Localization; +using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Graph; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.Localization; +using Repositories.Logging; +using Repositories.Mail; +using Repositories.NotificationsRepository; +using Repositories.RetryPolicyProvider; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.CompilerServices; +using Microsoft.FeatureManagement; +using Azure.Messaging.ServiceBus; +using Repositories.EntityFramework.Contexts; +using Microsoft.EntityFrameworkCore; +using Repositories.EntityFramework; +using Repositories.FeatureFlag; +using Azure.Core; +using System.IO; +using Models; +using System.Data; + +namespace Hosts.FunctionBase +{ + public static class CommonServices + { + private const string SCHEMA_DIRECTORY = "Schemas"; + + public static void ConfigureCommonServices(IServiceCollection services, IConfiguration configuration, string functionName, string dryRunSettingName, string rootPath) + { + services.AddAzureAppConfiguration(); + services.AddFeatureManagement(); + services.AddScoped(); + + services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; }); + services.Configure(opts => + { + var supportedCultures = new List + { + new CultureInfo("en-US"), + new CultureInfo("es-ES"), + new CultureInfo("hi-IN") + }; + opts.DefaultRequestCulture = new RequestCulture("en-US"); + opts.SupportedCultures = supportedCultures; + opts.SupportedUICultures = supportedCultures; + }); + + services.AddOptions().Configure((settings, configuration) => + { + if (!string.IsNullOrEmpty(dryRunSettingName)) + { + var checkParse = bool.TryParse(configuration[dryRunSettingName], out bool value); + if (checkParse) + settings.DryRunEnabled = value; + } + + }); + + services.AddSingleton(services => + { + return new DryRunValue(services.GetService>().Value.DryRunEnabled); + }); + + services.AddSingleton(); + + services.AddSingleton>(new LogAnalyticsSecret(GetValueOrThrowBase("logAnalyticsCustomerId"), GetValueOrThrowBase("logAnalyticsPrimarySharedKey"), functionName)); + services.AddOptions().Configure((settings, configuration) => + { + settings.Verbosity = configuration.GetValue("GMM:LoggingVerbosity"); + }); + + services.AddDbContext(options => + options.UseSqlServer(GetValueOrThrowBase("ConnectionStrings:JobsContext")), + ServiceLifetime.Scoped + ); + + services.AddDbContext(options => + options.UseSqlServer(GetValueOrThrowBase("ConnectionStrings:JobsContextReadOnly")), + ServiceLifetime.Scoped + ); + + services.AddOptions().Configure((settings, configuration) => + { + settings.MaxRetryAfterAttempts = GetIntSettingBase(configuration, "MaxRetryAfterAttempts", 4); + settings.MaxExceptionHandlingAttempts = GetIntSettingBase(configuration, "MaxExceptionHandlingAttempts", 2); + }); + + services.AddSingleton(services => + { + var options = services.GetRequiredService>(); + return new GraphServiceAttemptsValue + { + MaxRetryAfterAttempts = options.Value.MaxRetryAfterAttempts, + MaxExceptionHandlingAttempts = options.Value.MaxExceptionHandlingAttempts + }; + }); + + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(services => + { + var creds = services.GetService>(); + return new AppConfigVerbosity(creds.Value.Verbosity); + }); + + services.AddOptions().Configure((settings, configuration) => + { + settings.LearnMoreAboutGMMUrl = configuration.GetValue("GMM:LearnMoreUrl"); + }); + + services.AddSingleton(services => + { + var creds = services.GetService>(); + return new GMMResources(creds.Value.LearnMoreAboutGMMUrl); + }); + + services.AddOptions() + .Configure((settings, configuration) => + { + configuration.GetSection("graphCredentials").Bind(settings); + var authenticationType = Common.DependencyInjection.ServiceCollectionExtensions.MapStringToAuthenticationType(configuration["GraphAPI:AuthenticationType"]); + settings.AuthenticationType = authenticationType; + }); + + services.AddOptions().Configure((settings, configuration) => + { + settings.SenderAddress = configuration.GetValue("senderAddress"); + settings.SenderPassword = configuration.GetValue("senderPassword"); + settings.SyncDisabledCCAddresses = configuration.GetValue("syncDisabledCCEmailAddresses"); + settings.SyncCompletedCCAddresses = configuration.GetValue("syncCompletedCCEmailAddresses"); + settings.SupportEmailAddresses = configuration.GetValue("supportEmailAddresses"); + }); + + services.AddSingleton(services => + { + var creds = services.GetService>(); + return new EmailSenderRecipient( + creds.Value.SenderAddress, + creds.Value.SenderPassword, + creds.Value.SyncCompletedCCAddresses, + creds.Value.SyncDisabledCCAddresses, + creds.Value.SupportEmailAddresses); + }); + + services.AddSingleton(services => + { + var configuration = services.GetService(); + return new MailConfig(configuration.GetValue("Mail:IsAdaptiveCardEnabled"), + configuration.GetValue("Mail:IsMailApplicationPermissionGranted", false), + configuration.GetValue("senderAddress"), + configuration.GetValue("Mail:SkipMailNotifications", false)); + }); + + services.AddScoped(services => + { + var mailConfig = services.GetService(); + var graphCredentials = services.GetService>().Value; + + TokenCredential graphTokenCredential; + + if (mailConfig.GMMHasSendMailApplicationPermissions) + { + graphTokenCredential = FunctionAppDI.CreateAuthProviderFromSecret(graphCredentials); + } + else + { + var mailCredentials = services.GetService>(); + graphCredentials.ServiceAccountUserName = mailCredentials.Value.SenderAddress; + graphCredentials.ServiceAccountPassword = mailCredentials.Value.SenderPassword; + + graphTokenCredential = FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials); + } + + return new MailRepository( + new GraphServiceClient(graphTokenCredential), + services.GetService(), + services.GetService(), + services.GetService(), + GetValueOrDefaultBase("actionableEmailProviderId"), + services.GetService(), + services.GetService(), + services.GetService() + ); + }); + + services.AddScoped(); + + services.AddOptions().Configure((settings, configuration) => + { + settings.IsThresholdNotificationEnabled = configuration.GetValue("ThresholdNotification:IsThresholdNotificationEnabled"); + }); + services.AddSingleton(services => + { + var creds = services.GetService>(); + return new ThresholdNotificationConfig(creds.Value.IsThresholdNotificationEnabled); + }); + + services.AddSingleton(sp => + { + var telemetryConfiguration = new TelemetryConfiguration(); + telemetryConfiguration.InstrumentationKey = Environment.GetEnvironmentVariable("APPINSIGHTS_INSTRUMENTATIONKEY"); + telemetryConfiguration.TelemetryInitializers.Add(new OperationCorrelationTelemetryInitializer()); + var tc = new TelemetryClient(telemetryConfiguration); + tc.Context.Operation.Name = functionName; + return tc; + }); + + services.AddSingleton(services => + { + var serviceBusFQN = GetValueOrDefaultBase("gmmServiceBus__fullyQualifiedNamespace"); + + if (string.IsNullOrWhiteSpace(serviceBusFQN)) + throw new ArgumentNullException($"Could not start because of missing configuration option: servicebus fully qualified namespace."); + + return new ServiceBusClient(serviceBusFQN, new DefaultAzureCredential()); + }); + + var jsonSchemasPath = Path.Combine(rootPath, SCHEMA_DIRECTORY); + var schemaProvider = new SchemaProvider(); + if (Directory.Exists(jsonSchemasPath)) + { + var files = Directory.EnumerateFiles(jsonSchemasPath); + foreach (var file in files) + { + var fileName = Path.GetFileNameWithoutExtension(file); + schemaProvider.Schemas.Add((Schema)Enum.Parse(typeof(Schema), fileName), File.ReadAllText(file)); + } + } + services.AddSingleton(schemaProvider); + } + + public static string GetValueOrThrowBase(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) + { + var value = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException($"Could not start because of missing configuration option: {key}. Requested by file {callerFile}:{callerLine}."); + return value; + } + + public static string GetValueOrDefaultBase(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) + { + return Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process) ?? string.Empty; + } + private static int GetIntSettingBase(IConfiguration configuration, string settingName, int defaultValue) + { + var checkParse = int.TryParse(configuration[settingName], out int value); + return checkParse ? value : defaultValue; + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/CreateThresholdNotification/CreateThresholdNotificationFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/CreateThresholdNotification/CreateThresholdNotificationFunction.cs index 0d8f70b5c..9e333aa19 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/CreateThresholdNotification/CreateThresholdNotificationFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/CreateThresholdNotification/CreateThresholdNotificationFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs; using Repositories.Contracts; using System; using System.Threading.Tasks; using Services.Notifier.Contracts; using Models; +using Microsoft.Azure.Functions.Worker; namespace Hosts.Notifier { @@ -22,7 +21,7 @@ public CreateThresholdNotificationFunction(ILoggingRepository loggingRepository, _notifierService = notifierService ?? throw new ArgumentNullException(nameof(notifierService)); } - [FunctionName(nameof(CreateThresholdNotificationFunction))] + [Function(nameof(CreateThresholdNotificationFunction))] public async Task CreateActionableNotificationFromContentAsync([ActivityTrigger] OrchestratorRequest message) { await _loggingRepository.LogMessageAsync(new LogMessage { RunId = message.RunId, Message = $"{nameof(CreateThresholdNotificationFunction)} function started at: {DateTime.UtcNow}" }); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/Logger/LoggerFunction.cs index 79070b3e9..95668b0ac 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/Logger/LoggerFunction.cs @@ -2,11 +2,10 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.Notifier { @@ -19,7 +18,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.RunId },request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/RetrieveNotifications/RetrieveNotificationsFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/RetrieveNotifications/RetrieveNotificationsFunction.cs index 849352be1..2fec4b118 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/RetrieveNotifications/RetrieveNotificationsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/RetrieveNotifications/RetrieveNotificationsFunction.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs; using Repositories.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; using Services.Notifier.Contracts; using Models; +using Microsoft.Azure.Functions.Worker; namespace Hosts.Notifier { @@ -23,7 +22,7 @@ public RetrieveNotificationsFunction(ILoggingRepository loggingRepository, INoti _notifierService = notifierService ?? throw new ArgumentNullException(nameof(notifierService)); } - [FunctionName(nameof(RetrieveNotificationsFunction))] + [Function(nameof(RetrieveNotificationsFunction))] public async Task> RetrieveNotificationsAsync([ActivityTrigger] object obj) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(RetrieveNotificationsFunction)} function started at: {DateTime.UtcNow}" }); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNormalThresholdNotification/SendNormalThresholdNotification.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNormalThresholdNotification/SendNormalThresholdNotification.cs index 15778f648..89dae2eb1 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNormalThresholdNotification/SendNormalThresholdNotification.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNormalThresholdNotification/SendNormalThresholdNotification.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. using Hosts.Notifier; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs; using Models; using Repositories.Contracts; using Services.Notifier.Contracts; @@ -12,6 +10,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureMaintenance.Activity.SendNormalThresholdNotification { @@ -26,7 +25,7 @@ public SendNormalThresholdNotification(ILoggingRepository loggingRepository, INo _notifierService = notifierService ?? throw new ArgumentNullException(nameof(notifierService)); } - [FunctionName(nameof(SendNormalThresholdNotification))] + [Function(nameof(SendNormalThresholdNotification))] public async Task SendNormalThresholdNotificationAsync([ActivityTrigger] OrchestratorRequest message) { await _loggingRepository.LogMessageAsync(new LogMessage { RunId = message.RunId, Message = $"{nameof(SendNotification)} function started at: {DateTime.UtcNow}" }); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNotification/SendNotification.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNotification/SendNotification.cs index 8e5aa07ab..7ebdb84ba 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNotification/SendNotification.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNotification/SendNotification.cs @@ -2,12 +2,11 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs; using Repositories.Contracts; using System; using System.Threading.Tasks; using Services.Notifier.Contracts; +using Microsoft.Azure.Functions.Worker; namespace Hosts.Notifier { @@ -22,7 +21,7 @@ public SendNotification(ILoggingRepository loggingRepository, INotifierService n _notifierService = notifierService ?? throw new ArgumentNullException(nameof(notifierService)); } - [FunctionName(nameof(SendNotification))] + [Function(nameof(SendNotification))] public async Task SendNotificationAsync([ActivityTrigger] OrchestratorRequest message) { await _loggingRepository.LogMessageAsync(new LogMessage { RunId = message.RunId, Message = $"{nameof(SendNotification)} function started at: {DateTime.UtcNow}" }); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotification.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotification.cs index c8fe03d63..7350b705d 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotification.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendThresholdNotification/SendThresholdNotification.cs @@ -2,13 +2,12 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs; using Repositories.Contracts; using System; using System.Threading.Tasks; using Services.Notifier.Contracts; using Models.ThresholdNotifications; +using Microsoft.Azure.Functions.Worker; namespace Hosts.Notifier { @@ -23,7 +22,7 @@ public SendThresholdNotification(ILoggingRepository loggingRepository, INotifier _notifierService = notifierService ?? throw new ArgumentNullException(nameof(notifierService)); } - [FunctionName(nameof(SendThresholdNotification))] + [Function(nameof(SendThresholdNotification))] public async Task SendThresholdNotificationAsync([ActivityTrigger] ThresholdNotification notification) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SendThresholdNotification)} function started at: {DateTime.UtcNow}" }); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/UpdateNotificationStatus/UpdateNotificationStatusFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/UpdateNotificationStatus/UpdateNotificationStatusFunction.cs index 2cbd8d590..fd89d6daf 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/UpdateNotificationStatus/UpdateNotificationStatusFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/UpdateNotificationStatus/UpdateNotificationStatusFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs; using Repositories.Contracts; using System; using System.Threading.Tasks; using Services.Notifier.Contracts; using Models; +using Microsoft.Azure.Functions.Worker; namespace Hosts.Notifier { @@ -22,7 +21,7 @@ public UpdateNotificationStatusFunction(ILoggingRepository loggingRepository, IN _notifierService = notifierService ?? throw new ArgumentNullException(nameof(notifierService)); } - [FunctionName(nameof(UpdateNotificationStatusFunction))] + [Function(nameof(UpdateNotificationStatusFunction))] public async Task UpdateNotificationStatusAsync([ActivityTrigger] UpdateNotificationStatusRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(UpdateNotificationStatusFunction)} function started at: {DateTime.UtcNow}" }); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj index 985ba514d..dc27dadcb 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj @@ -4,12 +4,16 @@ v4 Hosts.AzureMaintenance Hosts.AzureMaintenance + Exe + enabled - - - + + + + + @@ -26,4 +30,7 @@ Never - + + + + \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs index 3aee74b6b..0601d3370 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System.Threading.Tasks; using Models.ThresholdNotifications; @@ -13,6 +11,8 @@ using System.Text.Json; using System.Collections.Generic; using System; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.Notifier { @@ -22,9 +22,9 @@ public OrchestratorFunction() { } - [FunctionName(nameof(OrchestratorFunction))] + [Function(nameof(OrchestratorFunction))] public async Task RunOrchestratorAsync( - [OrchestrationTrigger] IDurableOrchestrationContext context) + [OrchestrationTrigger] TaskOrchestrationContext context) { var message = context.GetInput(); var messageContent = JsonSerializer.Deserialize>(message.MessageBody); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs new file mode 100644 index 000000000..d268c7140 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs @@ -0,0 +1,103 @@ +using Microsoft.Extensions.Hosting; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using DIConcreteTypes; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.GraphGroups; +using Repositories.ServiceBusQueue; +using Services.Contracts.Notifications; +using Services.Notifications; +using Services.Notifier; +using Services.Notifier.Contracts; +using System; +using Hosts.FunctionBase; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = "Notifier"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, "Notifier", dryRunSettingName, rootPath); + services.AddOptions().Configure((settings, configuration) => + { + settings.HandleInactiveJobsEnabled = GetBoolSetting(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); + settings.NumberOfDaysBeforeDeletion = GetIntSetting(configuration, "AzureMaintenance:NumberOfDaysBeforeDeletion", 0); + }); + services.AddSingleton(services => + { + return new HandleInactiveJobsConfig( + services.GetService>().Value.HandleInactiveJobsEnabled, + services.GetService>().Value.NumberOfDaysBeforeDeletion); + }); + + services.AddGraphAPIClient() + .AddLocalization(options => + { + options.ResourcesPath = "Resources"; + }) + .AddScoped() + .AddScoped(); + + services.AddOptions().Configure((settings, configuration) => + { + settings.ActionableEmailProviderId = configuration.GetValue("actionableEmailProviderId"); + settings.ApiHostname = configuration.GetValue("apiHostname"); + }); + services.AddOptions().Configure((settings, configuration) => + { + settings.MaximumNumberOfThresholdRecipients = GetIntSetting(configuration, "MaximumNumberOfThresholdRecipients", 10); + settings.NumberOfThresholdViolationsToNotify = GetIntSetting(configuration, "NumberOfThresholdViolationsToNotify", 3); + settings.NumberOfThresholdViolationsFollowUps = GetIntSetting(configuration, "NumberOfThresholdViolationsFollowUps", 3); + settings.NumberOfThresholdViolationsToDisableJob = GetIntSetting(configuration, "NumberOfThresholdViolationsToDisableJob", 10); + }); + services.AddSingleton(services => + { + return new ThresholdConfig + ( + services.GetService>().Value.MaximumNumberOfThresholdRecipients, + services.GetService>().Value.NumberOfThresholdViolationsToNotify, + services.GetService>().Value.NumberOfThresholdViolationsFollowUps, + services.GetService>().Value.NumberOfThresholdViolationsToDisableJob + ); + }); + services.AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var failedNotificationsQueue = configuration["serviceBusFailedNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(failedNotificationsQueue); + return new ServiceBusQueueRepository(sender); + }); + services.AddScoped((sp) => + { + return new ThresholdNotificationConfig(true); + }); + services.AddScoped(); + services.AddHttpClient(); + + }) + .Build(); + +host.Run(); +static bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) +{ + var checkParse = bool.TryParse(configuration[settingName], out bool value); + if (checkParse) + return value; + return defaultValue; +} + +static int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) +{ + var checkParse = int.TryParse(configuration[settingName], out int value); + if (checkParse) + return value; + return defaultValue; +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Starter/StarterFunction.cs index 026b6a1f8..b58025b18 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Starter/StarterFunction.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. using Azure.Messaging.ServiceBus; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; @@ -12,6 +10,9 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace Hosts.Notifier { public class StarterFunction @@ -27,10 +28,10 @@ public StarterFunction(ILoggingRepository loggingRepository, IThresholdNotificat _mailConfig = mailConfig ?? throw new ArgumentNullException(nameof(mailConfig)); ; } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task ProcessServiceBusMessageAsync( [ServiceBusTrigger("%serviceBusNotificationsQueue%", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient client) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started" }, VerbosityLevel.DEBUG); string messageBody = Encoding.UTF8.GetString(message.Body.ToArray()); @@ -59,7 +60,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage } else { - var instanceId = await starter.StartNewAsync(nameof(OrchestratorFunction), (orchestratorRequest)); + var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), (orchestratorRequest)); } diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Startup.cs deleted file mode 100644 index f06246c54..000000000 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Startup.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Azure.Messaging.ServiceBus; -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.FunctionBase; -using Hosts.Notifier; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphGroups; -using Repositories.ServiceBusQueue; -using Services.Contracts.Notifications; -using Services.Notifications; -using Services.Notifier; -using Services.Notifier.Contracts; -using System; - -[assembly: FunctionsStartup(typeof(Startup))] - -namespace Hosts.Notifier -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(Notifier); - protected override string DryRunSettingName => string.Empty; - protected string NotifierConfigSettingName => "Notifier:NotifierConfiguration"; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - settings.HandleInactiveJobsEnabled = GetBoolSetting(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); - settings.NumberOfDaysBeforeDeletion = GetIntSetting(configuration, "AzureMaintenance:NumberOfDaysBeforeDeletion", 0); - }); - builder.Services.AddSingleton(services => - { - return new HandleInactiveJobsConfig( - services.GetService>().Value.HandleInactiveJobsEnabled, - services.GetService>().Value.NumberOfDaysBeforeDeletion); - }); - - builder.Services.AddGraphAPIClient() - .AddLocalization(options => - { - options.ResourcesPath = "Resources"; - }) - .AddScoped() - .AddScoped(); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - settings.ActionableEmailProviderId = configuration.GetValue("actionableEmailProviderId"); - settings.ApiHostname = configuration.GetValue("apiHostname"); - }); - builder.Services.AddOptions().Configure((settings, configuration) => - { - settings.MaximumNumberOfThresholdRecipients = GetIntSetting(configuration, "MaximumNumberOfThresholdRecipients", 10); - settings.NumberOfThresholdViolationsToNotify = GetIntSetting(configuration, "NumberOfThresholdViolationsToNotify", 3); - settings.NumberOfThresholdViolationsFollowUps = GetIntSetting(configuration, "NumberOfThresholdViolationsFollowUps", 3); - settings.NumberOfThresholdViolationsToDisableJob = GetIntSetting(configuration, "NumberOfThresholdViolationsToDisableJob", 10); - }); - builder.Services.AddSingleton(services => - { - return new ThresholdConfig - ( - services.GetService>().Value.MaximumNumberOfThresholdRecipients, - services.GetService>().Value.NumberOfThresholdViolationsToNotify, - services.GetService>().Value.NumberOfThresholdViolationsFollowUps, - services.GetService>().Value.NumberOfThresholdViolationsToDisableJob - ); - }); - builder.Services.AddSingleton(services => - { - var configuration = services.GetRequiredService(); - var failedNotificationsQueue = configuration["serviceBusFailedNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(failedNotificationsQueue); - return new ServiceBusQueueRepository(sender); - }); - builder.Services.AddScoped((sp) => - { - return new ThresholdNotificationConfig(true); - }); - builder.Services.AddScoped(); - builder.Services.AddHttpClient(); - } - - private bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) - { - var checkParse = bool.TryParse(configuration[settingName], out bool value); - if (checkParse) - return value; - return defaultValue; - } - - private int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) - { - var checkParse = int.TryParse(configuration[settingName], out int value); - if (checkParse) - return value; - return defaultValue; - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/host.json b/Service/GroupMembershipManagement/Hosts/Notifier/Function/host.json index bb3b8dadd..f2b0e122e 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/host.json @@ -1,11 +1,12 @@ { - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingExcludedTypes": "Request", - "samplingSettings": { - "isEnabled": true - } - } + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingExcludedTypes": "Request", + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } } + } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs index ec3adb1af..bad8abd68 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs @@ -16,13 +16,14 @@ using Models.ServiceBus; using Services.Tests; using System.Linq; +using Microsoft.DurableTask; namespace Services.Notifier.Tests { [TestClass] public class OrchestratorFunctionTests { - private Mock _durableContext; + private Mock _durableContext; private Mock _loggerFunction; private OrchestratorFunction _orchestratorFunction; private const string GroupMembership = "GroupMembership"; @@ -30,7 +31,7 @@ public class OrchestratorFunctionTests [TestInitialize] public void SetupTest() { - _durableContext = new Mock(); + _durableContext = new Mock(); _loggerFunction = new Mock(); _orchestratorFunction = new OrchestratorFunction(); } @@ -60,19 +61,24 @@ public async Task RunOrchestratorAsync_ThresholdNotification_CallsAppropriateFun _durableContext.Setup(x => x.GetInput()).Returns(orchestratorRequest); var thresholdNotification = new ThresholdNotification(); - _durableContext.Setup(x => x.CallActivityAsync(nameof(CreateThresholdNotificationFunction), It.IsAny())) - .ReturnsAsync(thresholdNotification); + _durableContext.Setup(x => x.CallActivityAsync( + nameof(CreateThresholdNotificationFunction), It.IsAny(), null)) + .ReturnsAsync(thresholdNotification); await _orchestratorFunction.RunOrchestratorAsync(_durableContext.Object); _durableContext.Verify(x => x.CallActivityAsync( - nameof(CreateThresholdNotificationFunction), It.IsAny()), Times.Once); - _durableContext.Setup(x => x.CallActivityAsync(nameof(SendThresholdNotification), It.IsAny())) - .ReturnsAsync(false); + nameof(CreateThresholdNotificationFunction), It.IsAny(), null), Times.Once); + + _durableContext.Setup(x => x.CallActivityAsync( + nameof(SendThresholdNotification), It.IsAny(), null)) + .ReturnsAsync(false); + _durableContext.Verify(x => x.CallActivityAsync( - nameof(UpdateNotificationStatusFunction), It.IsAny()), Times.Once); + nameof(UpdateNotificationStatusFunction), It.IsAny(), null), Times.Once); + _durableContext.Verify(x => x.CallActivityAsync( - nameof(LoggerFunction), It.IsAny()), Times.AtLeastOnce); + nameof(LoggerFunction), It.IsAny(), null), Times.AtLeastOnce); } } } \ No newline at end of file From 02ea014f4bffecf918cbf96e0e3a56f46651ee14 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 13 Aug 2024 10:26:24 -0700 Subject: [PATCH 0195/1479] Change function worker runtime --- .../Hosts/Notifier/Infrastructure/compute/template.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep index 2484e9e91..fb3a1c2a9 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep @@ -122,7 +122,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } From 77c80520f75f9863d8572b640c067379d544d175 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 13 Aug 2024 14:41:37 -0700 Subject: [PATCH 0196/1479] Inital app config in Notifier --- .../Hosts/Notifier/Function/Program.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs index d268c7140..116795aed 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs @@ -15,9 +15,22 @@ using Services.Notifier.Contracts; using System; using Hosts.FunctionBase; +using Azure.Identity; +using System.Runtime.CompilerServices; var host = new HostBuilder() .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = GetValueOrThrow("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) .ConfigureServices((context, services) => { var configuration = context.Configuration; @@ -100,4 +113,12 @@ static int GetIntSetting(IConfiguration configuration, string settingName, int d if (checkParse) return value; return defaultValue; +} + +static string GetValueOrThrow(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) +{ + var value = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException($"Could not start because of missing configuration option: {key}. Requested by file {callerFile}:{callerLine}."); + return value; } \ No newline at end of file From 7fd9d21b611142b5d9080744f6f9e9113121bcdb Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 10:54:40 -0700 Subject: [PATCH 0197/1479] Add main class --- .../Hosts/Notifier/Function/Program.cs | 198 +++++++++--------- 1 file changed, 104 insertions(+), 94 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs index 116795aed..41c04b7f1 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs @@ -18,107 +18,117 @@ using Azure.Identity; using System.Runtime.CompilerServices; -var host = new HostBuilder() - .ConfigureFunctionsWorkerDefaults() - .ConfigureAppConfiguration((context, config) => - { - var settings = config.Build(); - var appConfigEndpoint = GetValueOrThrow("appConfigurationEndpoint"); +namespace Hosts.JobTrigger +{ - config.AddAzureAppConfiguration(options => - { - options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) - .UseFeatureFlags(); - }); - }) - .ConfigureServices((context, services) => + public class Program { - var configuration = context.Configuration; - var functionName = "Notifier"; - var dryRunSettingName = string.Empty; - var rootPath = context.HostingEnvironment.ContentRootPath; - CommonServices.ConfigureCommonServices(services, configuration, "Notifier", dryRunSettingName, rootPath); - services.AddOptions().Configure((settings, configuration) => + public static void Main (string[] args) { - settings.HandleInactiveJobsEnabled = GetBoolSetting(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); - settings.NumberOfDaysBeforeDeletion = GetIntSetting(configuration, "AzureMaintenance:NumberOfDaysBeforeDeletion", 0); - }); - services.AddSingleton(services => - { - return new HandleInactiveJobsConfig( - services.GetService>().Value.HandleInactiveJobsEnabled, - services.GetService>().Value.NumberOfDaysBeforeDeletion); - }); + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = GetValueOrThrow("appConfigurationEndpoint"); - services.AddGraphAPIClient() - .AddLocalization(options => - { - options.ResourcesPath = "Resources"; - }) - .AddScoped() - .AddScoped(); + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = "Notifier"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, "Notifier", dryRunSettingName, rootPath); + services.AddOptions().Configure((settings, configuration) => + { + settings.HandleInactiveJobsEnabled = GetBoolSetting(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); + settings.NumberOfDaysBeforeDeletion = GetIntSetting(configuration, "AzureMaintenance:NumberOfDaysBeforeDeletion", 0); + }); + services.AddSingleton(services => + { + return new HandleInactiveJobsConfig( + services.GetService>().Value.HandleInactiveJobsEnabled, + services.GetService>().Value.NumberOfDaysBeforeDeletion); + }); - services.AddOptions().Configure((settings, configuration) => - { - settings.ActionableEmailProviderId = configuration.GetValue("actionableEmailProviderId"); - settings.ApiHostname = configuration.GetValue("apiHostname"); - }); - services.AddOptions().Configure((settings, configuration) => - { - settings.MaximumNumberOfThresholdRecipients = GetIntSetting(configuration, "MaximumNumberOfThresholdRecipients", 10); - settings.NumberOfThresholdViolationsToNotify = GetIntSetting(configuration, "NumberOfThresholdViolationsToNotify", 3); - settings.NumberOfThresholdViolationsFollowUps = GetIntSetting(configuration, "NumberOfThresholdViolationsFollowUps", 3); - settings.NumberOfThresholdViolationsToDisableJob = GetIntSetting(configuration, "NumberOfThresholdViolationsToDisableJob", 10); - }); - services.AddSingleton(services => - { - return new ThresholdConfig - ( - services.GetService>().Value.MaximumNumberOfThresholdRecipients, - services.GetService>().Value.NumberOfThresholdViolationsToNotify, - services.GetService>().Value.NumberOfThresholdViolationsFollowUps, - services.GetService>().Value.NumberOfThresholdViolationsToDisableJob - ); - }); - services.AddSingleton(services => - { - var configuration = services.GetRequiredService(); - var failedNotificationsQueue = configuration["serviceBusFailedNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(failedNotificationsQueue); - return new ServiceBusQueueRepository(sender); - }); - services.AddScoped((sp) => - { - return new ThresholdNotificationConfig(true); - }); - services.AddScoped(); - services.AddHttpClient(); + services.AddGraphAPIClient() + .AddLocalization(options => + { + options.ResourcesPath = "Resources"; + }) + .AddScoped() + .AddScoped(); - }) - .Build(); + services.AddOptions().Configure((settings, configuration) => + { + settings.ActionableEmailProviderId = configuration.GetValue("actionableEmailProviderId"); + settings.ApiHostname = configuration.GetValue("apiHostname"); + }); + services.AddOptions().Configure((settings, configuration) => + { + settings.MaximumNumberOfThresholdRecipients = GetIntSetting(configuration, "MaximumNumberOfThresholdRecipients", 10); + settings.NumberOfThresholdViolationsToNotify = GetIntSetting(configuration, "NumberOfThresholdViolationsToNotify", 3); + settings.NumberOfThresholdViolationsFollowUps = GetIntSetting(configuration, "NumberOfThresholdViolationsFollowUps", 3); + settings.NumberOfThresholdViolationsToDisableJob = GetIntSetting(configuration, "NumberOfThresholdViolationsToDisableJob", 10); + }); + services.AddSingleton(services => + { + return new ThresholdConfig + ( + services.GetService>().Value.MaximumNumberOfThresholdRecipients, + services.GetService>().Value.NumberOfThresholdViolationsToNotify, + services.GetService>().Value.NumberOfThresholdViolationsFollowUps, + services.GetService>().Value.NumberOfThresholdViolationsToDisableJob + ); + }); + services.AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var failedNotificationsQueue = configuration["serviceBusFailedNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(failedNotificationsQueue); + return new ServiceBusQueueRepository(sender); + }); + services.AddScoped((sp) => + { + return new ThresholdNotificationConfig(true); + }); + services.AddScoped(); + services.AddHttpClient(); -host.Run(); -static bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) -{ - var checkParse = bool.TryParse(configuration[settingName], out bool value); - if (checkParse) - return value; - return defaultValue; -} + }) + .Build(); + + host.Run(); + } + static bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) + { + var checkParse = bool.TryParse(configuration[settingName], out bool value); + if (checkParse) + return value; + return defaultValue; + } -static int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) -{ - var checkParse = int.TryParse(configuration[settingName], out int value); - if (checkParse) - return value; - return defaultValue; -} + static int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) + { + var checkParse = int.TryParse(configuration[settingName], out int value); + if (checkParse) + return value; + return defaultValue; + } -static string GetValueOrThrow(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) -{ - var value = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentNullException($"Could not start because of missing configuration option: {key}. Requested by file {callerFile}:{callerLine}."); - return value; + static string GetValueOrThrow(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) + { + var value = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException($"Could not start because of missing configuration option: {key}. Requested by file {callerFile}:{callerLine}."); + return value; + } + } } \ No newline at end of file From af8959298baa1a801cb407140c5e52eaef91e6e6 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 11:07:13 -0700 Subject: [PATCH 0198/1479] Add share method in base class --- .../Hosts.FunctionBase/CommonProgramBase.cs | 10 ++++- .../Hosts/Notifier/Function/Program.cs | 40 +++++-------------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs index 2ad24a3bf..9d49828ce 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs @@ -262,10 +262,18 @@ public static string GetValueOrDefaultBase(string key, [CallerFilePath] string c { return Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process) ?? string.Empty; } - private static int GetIntSettingBase(IConfiguration configuration, string settingName, int defaultValue) + public static int GetIntSettingBase(IConfiguration configuration, string settingName, int defaultValue) { var checkParse = int.TryParse(configuration[settingName], out int value); return checkParse ? value : defaultValue; } + + public static bool GetBoolSettingBase(IConfiguration configuration, string settingName, bool defaultValue) + { + var checkParse = bool.TryParse(configuration[settingName], out bool value); + if (checkParse) + return value; + return defaultValue; + } } } diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs index 41c04b7f1..4c907450b 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + using Microsoft.Extensions.Hosting; using Azure.Messaging.ServiceBus; using Common.DependencyInjection; @@ -30,7 +33,7 @@ public static void Main (string[] args) .ConfigureAppConfiguration((context, config) => { var settings = config.Build(); - var appConfigEndpoint = GetValueOrThrow("appConfigurationEndpoint"); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); config.AddAzureAppConfiguration(options => { @@ -47,8 +50,8 @@ public static void Main (string[] args) CommonServices.ConfigureCommonServices(services, configuration, "Notifier", dryRunSettingName, rootPath); services.AddOptions().Configure((settings, configuration) => { - settings.HandleInactiveJobsEnabled = GetBoolSetting(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); - settings.NumberOfDaysBeforeDeletion = GetIntSetting(configuration, "AzureMaintenance:NumberOfDaysBeforeDeletion", 0); + settings.HandleInactiveJobsEnabled = CommonServices.GetBoolSettingBase(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); + settings.NumberOfDaysBeforeDeletion = CommonServices.GetIntSettingBase(configuration, "AzureMaintenance:NumberOfDaysBeforeDeletion", 0); }); services.AddSingleton(services => { @@ -72,10 +75,10 @@ public static void Main (string[] args) }); services.AddOptions().Configure((settings, configuration) => { - settings.MaximumNumberOfThresholdRecipients = GetIntSetting(configuration, "MaximumNumberOfThresholdRecipients", 10); - settings.NumberOfThresholdViolationsToNotify = GetIntSetting(configuration, "NumberOfThresholdViolationsToNotify", 3); - settings.NumberOfThresholdViolationsFollowUps = GetIntSetting(configuration, "NumberOfThresholdViolationsFollowUps", 3); - settings.NumberOfThresholdViolationsToDisableJob = GetIntSetting(configuration, "NumberOfThresholdViolationsToDisableJob", 10); + settings.MaximumNumberOfThresholdRecipients = CommonServices.GetIntSettingBase(configuration, "MaximumNumberOfThresholdRecipients", 10); + settings.NumberOfThresholdViolationsToNotify = CommonServices.GetIntSettingBase(configuration, "NumberOfThresholdViolationsToNotify", 3); + settings.NumberOfThresholdViolationsFollowUps = CommonServices.GetIntSettingBase(configuration, "NumberOfThresholdViolationsFollowUps", 3); + settings.NumberOfThresholdViolationsToDisableJob = CommonServices.GetIntSettingBase(configuration, "NumberOfThresholdViolationsToDisableJob", 10); }); services.AddSingleton(services => { @@ -107,28 +110,5 @@ public static void Main (string[] args) host.Run(); } - static bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) - { - var checkParse = bool.TryParse(configuration[settingName], out bool value); - if (checkParse) - return value; - return defaultValue; - } - - static int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) - { - var checkParse = int.TryParse(configuration[settingName], out int value); - if (checkParse) - return value; - return defaultValue; - } - - static string GetValueOrThrow(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) - { - var value = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentNullException($"Could not start because of missing configuration option: {key}. Requested by file {callerFile}:{callerLine}."); - return value; - } } } \ No newline at end of file From 1da495247bd298d9a27a135084ff92294ef7ed8e Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 22 Aug 2024 11:34:38 -0700 Subject: [PATCH 0199/1479] Fix comments --- .../Hosts/Notifier/Function/Program.cs | 4 ++-- .../Hosts/Notifier/Function/host.json | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs index 4c907450b..cddd0e457 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Program.cs @@ -21,7 +21,7 @@ using Azure.Identity; using System.Runtime.CompilerServices; -namespace Hosts.JobTrigger +namespace Hosts.Notifier { public class Program @@ -47,7 +47,7 @@ public static void Main (string[] args) var functionName = "Notifier"; var dryRunSettingName = string.Empty; var rootPath = context.HostingEnvironment.ContentRootPath; - CommonServices.ConfigureCommonServices(services, configuration, "Notifier", dryRunSettingName, rootPath); + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); services.AddOptions().Configure((settings, configuration) => { settings.HandleInactiveJobsEnabled = CommonServices.GetBoolSettingBase(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/host.json b/Service/GroupMembershipManagement/Hosts/Notifier/Function/host.json index f2b0e122e..278b52cde 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/host.json @@ -2,7 +2,6 @@ "version": "2.0", "logging": { "applicationInsights": { - "samplingExcludedTypes": "Request", "samplingSettings": { "isEnabled": true, "excludedTypes": "Request" From ca14c02aea44c63920bc3e1fc9ac1333338047ab Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 15 Jul 2024 12:22:34 -0700 Subject: [PATCH 0200/1479] Add job finalizer function --- .../Hosts/JobFinalizer/Function/.gitignore | 264 +++++++++++++++++ .../Hosts/JobFinalizer/Function/Function1.cs | 50 ++++ .../JobFinalizer/Function/JobFinalizer.csproj | 19 ++ .../JobFinalizer/Function/JobFinalizer.sln | 25 ++ .../Function/Properties/launchSettings.json | 9 + .../Properties/serviceDependencies.json | 11 + .../Properties/serviceDependencies.local.json | 11 + .../Hosts/JobFinalizer/Function/host.json | 12 + .../Infrastructure/compute/functionApp.bicep | 114 +++++++ .../compute/functionAppRBAC.bicep | 62 ++++ .../compute/functionAppSlot.bicep | 86 ++++++ .../Infrastructure/compute/keyVaultRBAC.bicep | 53 ++++ .../compute/keyVaultReader.bicep | 7 + .../compute/logAnalyticsWorkspace.bicep | 22 ++ .../compute/parameters/parameters.int.json | 5 + .../compute/parameters/parameters.prodv2.json | 5 + .../compute/parameters/parameters.ua.json | 5 + .../Infrastructure/compute/servicePlan.bicep | 25 ++ .../Infrastructure/compute/template.bicep | 279 ++++++++++++++++++ .../data/keyVaultSecretsSecure.bicep | 12 + .../data/parameters/parameters.int.json | 5 + .../data/parameters/parameters.prodv2.json | 5 + .../data/parameters/parameters.ua.json | 5 + .../Infrastructure/data/storageAccount.bicep | 110 +++++++ .../Infrastructure/data/template.bicep | 47 +++ .../Services/JobFinalizerService/Class1.cs | 7 + .../JobFinalizerService.csproj | 9 + .../JobFinalizerService.sln | 25 ++ 28 files changed, 1289 insertions(+) create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/.gitignore create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Function1.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/launchSettings.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.local.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/host.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionApp.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppRBAC.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppSlot.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultRBAC.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultReader.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/logAnalyticsWorkspace.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.int.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.prodv2.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.ua.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/servicePlan.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/keyVaultSecretsSecure.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.int.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.prodv2.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.ua.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/storageAccount.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/Class1.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.csproj create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.sln diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/.gitignore b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/.gitignore new file mode 100644 index 000000000..ff5b00c50 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Function1.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Function1.cs new file mode 100644 index 000000000..a84ccb8a2 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Function1.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Host; +using Microsoft.Extensions.Logging; + +namespace JobFinalizer +{ + public static class Function1 + { + [FunctionName("Function1")] + public static async Task> RunOrchestrator( + [OrchestrationTrigger] IDurableOrchestrationContext context) + { + var outputs = new List(); + + // Replace "hello" with the name of your Durable Activity Function. + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "Tokyo")); + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "Seattle")); + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "London")); + + // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"] + return outputs; + } + + [FunctionName(nameof(SayHello))] + public static string SayHello([ActivityTrigger] string name, ILogger log) + { + log.LogInformation("Saying hello to {name}.", name); + return $"Hello {name}!"; + } + + [FunctionName("Function1_HttpStart")] + public static async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req, + [DurableClient] IDurableOrchestrationClient starter, + ILogger log) + { + // Function input comes from the request content. + string instanceId = await starter.StartNewAsync("Function1", null); + + log.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId); + + return starter.CreateCheckStatusResponse(req, instanceId); + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj new file mode 100644 index 000000000..b741ed2d8 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj @@ -0,0 +1,19 @@ + + + net6.0 + v4 + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln new file mode 100644 index 000000000..1dd222d48 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35027.167 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobFinalizer", "JobFinalizer.csproj", "{D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {33D020E5-59C2-43D3-A4A8-0CE0EBF46921} + EndGlobalSection +EndGlobal diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/launchSettings.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/launchSettings.json new file mode 100644 index 000000000..0f9d3df1a --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "JobFinalizer": { + "commandName": "Project", + "commandLineArgs": "--port 7245", + "launchBrowser": false + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.json new file mode 100644 index 000000000..df4dcc9d8 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "appInsights1": { + "type": "appInsights" + }, + "storage1": { + "type": "storage", + "connectionId": "AzureWebJobsStorage" + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.local.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.local.json new file mode 100644 index 000000000..b804a2893 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.local.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "appInsights1": { + "type": "appInsights.sdk" + }, + "storage1": { + "type": "storage.emulator", + "connectionId": "AzureWebJobsStorage" + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/host.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/host.json new file mode 100644 index 000000000..ee5cf5f83 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionApp.bicep new file mode 100644 index 000000000..9162c014a --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionApp.bicep @@ -0,0 +1,114 @@ +@description('Function app name.') +@minLength(1) +param name string + +@description('Function app kind.') +@allowed([ + 'functionapp' + 'linux' + 'container' +]) +param kind string = 'functionapp' + +@description('Function app location.') +param location string + +@description('Service plan name.') +@minLength(1) +param servicePlanName string + +@description('app settings') +param secretSettings object + +@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') +param userManagedIdentities object = {} + +var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} + +@description('Log Analytics Workspace Id.') +param logAnalyticsWorkspaceId string + +resource functionApp 'Microsoft.Web/sites@2018-02-01' = { + name: name + location: location + kind: kind + properties: { + serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) + clientAffinityEnabled: false + httpsOnly: true + siteConfig: { + use32BitWorkerProcess : false + appSettings: secretSettings + ftpsState: 'Disabled' + } + } + identity: { + type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' + userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null + } +} + +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: 'functionApp-diagnostics' + scope: functionApp + properties: { + workspaceId: logAnalyticsWorkspaceId + logs: [ + { + category: 'FunctionAppLogs' + enabled: true + retentionPolicy: { + days: 0 + enabled: false + } + } + ] + } +} + +resource snScmBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2022-09-01' = { + parent: functionApp + name: 'scm' + properties: { + allow: false + } +} + +resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2022-09-01' = { + parent: functionApp + name: 'ftp' + properties: { + allow: false + } +} + +resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { + name: 'slotConfigNames' + parent: functionApp + properties: { + appSettingNames: [ + 'AzureFunctionsJobHost__extensions__durableTask__hubName' + 'AzureWebJobs.StarterFunction.Disabled' + 'AzureWebJobs.OrchestratorFunction.Disabled' + 'AzureWebJobs.SubOrchestratorFunction.Disabled' + 'AzureWebJobs.DeltaUsersReaderFunction.Disabled' + 'AzureWebJobs.DeltaUsersSenderFunction.Disabled' + 'AzureWebJobs.EmailSenderFunction.Disabled' + 'AzureWebJobs.FileDownloaderFunction.Disabled' + 'AzureWebJobs.GroupsReaderFunction.Disabled' + 'AzureWebJobs.GroupValidatorFunction.Disabled' + 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' + 'AzureWebJobs.MembersReaderFunction.Disabled' + 'AzureWebJobs.SourceGroupsReaderFunction.Disabled' + 'AzureWebJobs.SubsequentDeltaUsersReaderFunction.Disabled' + 'AzureWebJobs.SubsequentMembersReaderFunction.Disabled' + 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled' + 'AzureWebJobs.UsersReaderFunction.Disabled' + 'AzureWebJobs.UsersSenderFunction.Disabled' + 'AzureWebJobsStorage' + 'AzureFunctionsWebHost__hostid' + ] + } +} + +output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppRBAC.bicep new file mode 100644 index 000000000..f7819597b --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppRBAC.bicep @@ -0,0 +1,62 @@ +@description('Name of the resource group where the \'prereqs\' key vault is located.') +param prereqsKeyVaultName string + +@description('Name of the resource group where the \'prereqs\' key vault is located.') +param prereqsKeyVaultResourceGroup string + +@description('Name of the \'data\' key vault.') +param dataKeyVaultName string + +@description('Name of the resource group where the \'data\' key vault is located.') +param dataKeyVaultResourceGroup string + +@description('Flag to indicate if the deployment should set RBAC permissions.') +param setRBACPermissions bool + +@description('The principalId of the function app for the production slot.') +param productionSlotPrincipalId string + +@description('The principalId of the function app for the staging slot.') +param stagingSlotPrincipalId string + +param functionName string + +module functionAppPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { + name: 'prereqsKV-rbac-${functionName}' + scope: resourceGroup(prereqsKeyVaultResourceGroup) + params: { + keyVaultName: prereqsKeyVaultName + principalId: productionSlotPrincipalId + roleName: 'Key Vault Secrets User' + } +} + +module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { + name: 'dataKV-rbac-${functionName}' + scope: resourceGroup(dataKeyVaultResourceGroup) + params: { + keyVaultName: dataKeyVaultName + principalId: productionSlotPrincipalId + roleName: 'Key Vault Secrets User' + } +} + +module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { + name: 'prereqsKV-rbac-${functionName}Slot' + scope: resourceGroup(prereqsKeyVaultResourceGroup) + params: { + keyVaultName: prereqsKeyVaultName + principalId: stagingSlotPrincipalId + roleName: 'Key Vault Secrets User' + } +} + +module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { + name: 'dataKV-rbac-${functionName}Slot' + scope: resourceGroup(dataKeyVaultResourceGroup) + params: { + keyVaultName: dataKeyVaultName + principalId: stagingSlotPrincipalId + roleName: 'Key Vault Secrets User' + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppSlot.bicep new file mode 100644 index 000000000..424a2c56c --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppSlot.bicep @@ -0,0 +1,86 @@ +@description('Function app name.') +@minLength(1) +param name string + +@description('Function app kind.') +@allowed([ + 'functionapp' + 'linux' + 'container' +]) +param kind string = 'functionapp' + +@description('Function app location.') +param location string + +@description('Service plan name.') +@minLength(1) +param servicePlanName string + +@description('app settings') +param secretSettings object + +@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') +param userManagedIdentities object = {} + +var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} + +@description('Log Analytics Workspace Id.') +param logAnalyticsWorkspaceId string + +resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { + name: name + kind: kind + location: location + properties: { + clientAffinityEnabled: true + enabled: true + httpsOnly: true + serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) + siteConfig: { + use32BitWorkerProcess : false + appSettings: secretSettings + ftpsState: 'Disabled' + } + } + identity: { + type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' + userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null + } +} + +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: 'functionAppSlot-diagnostics' + scope: functionAppSlot + properties: { + workspaceId: logAnalyticsWorkspaceId + logs: [ + { + category: 'FunctionAppLogs' + enabled: true + retentionPolicy: { + days: 0 + enabled: false + } + } + ] + } +} + +resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { + parent: functionAppSlot + name: 'ftp' + properties: { + allow: false + } +} + +resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { + parent: functionAppSlot + name: 'scm' + properties: { + allow: false + } +} + +output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultRBAC.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultRBAC.bicep new file mode 100644 index 000000000..63ccc7c28 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultRBAC.bicep @@ -0,0 +1,53 @@ +@description('Specifies the name of the key vault.') +param keyVaultName string + +@description('Specifies the object ID of a user, service principal or security group in the Azure Active Directory tenant for the vault. The object ID must be unique for the list of access policies. Get it by using Get-AzADUser or Get-AzADServicePrincipal cmdlets.') +param principalId string + +@description('Principal Type') +@allowed([ + 'Device' + 'ForeignGroup' + 'Group' + 'ServicePrincipal' + 'User' +]) +param principaltype string = 'ServicePrincipal' + +@description('Specifies the role the user will get with the secret in the vault. Valid values are: Key Vault Administrator, Key Vault Certificates Officer, Key Vault Crypto Officer, Key Vault Crypto Service Encryption User, Key Vault Crypto User, Key Vault Reader, Key Vault Secrets Officer, Key Vault Secrets User.') +@allowed([ + 'Key Vault Administrator' + 'Key Vault Certificates Officer' + 'Key Vault Crypto Officer' + 'Key Vault Crypto Service Encryption User' + 'Key Vault Crypto User' + 'Key Vault Reader' + 'Key Vault Secrets Officer' + 'Key Vault Secrets User' +]) +param roleName string + +var roleIdMapping = { + 'Key Vault Administrator': '00482a5a-887f-4fb3-b363-3b7fe8e74483' + 'Key Vault Certificates Officer': 'a4417e6f-fecd-4de8-b567-7b0420556985' + 'Key Vault Crypto Officer': '14b46e9e-c2b7-41b4-b07b-48a6ebf60603' + 'Key Vault Crypto Service Encryption User': 'e147488a-f6f5-4113-8e2d-b22465e65bf6' + 'Key Vault Crypto User': '12338af0-0e69-4776-bea7-57ae8d297424' + 'Key Vault Reader': '21090545-7ca7-4776-b22c-e363652d74d2' + 'Key Vault Secrets Officer': 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7' + 'Key Vault Secrets User': '4633458b-17de-408a-b874-0445c86b69e6' +} + +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: keyVaultName +} + +resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(roleIdMapping[roleName], principalId, keyVault.id) + scope: keyVault + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdMapping[roleName]) + principalId: principalId + principalType: principaltype + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultReader.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultReader.bicep new file mode 100644 index 000000000..3d963b18c --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultReader.bicep @@ -0,0 +1,7 @@ +// Do not use this to return secrets like passwords or API keys +// This is only for returning plain values that are not sensitive +@description('Plain value') +@secure() +param value string +var plainValue = string(value) +output value string = plainValue diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/logAnalyticsWorkspace.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/logAnalyticsWorkspace.bicep new file mode 100644 index 000000000..90541fb06 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/logAnalyticsWorkspace.bicep @@ -0,0 +1,22 @@ +@minLength(2) +@maxLength(3) +@description('Enter an abbreviation for the solution.') +param solutionAbbreviation string + +@minLength(4) +@maxLength(7) +@description('Enter an abbreviation for the solution.') +param resourceGroupClassification string + +@minLength(2) +@maxLength(6) +@description('Enter an abbreviation for the environment.') +param environmentAbbreviation string + +param logAnalyticsName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}' + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-06-01' existing = { + name: logAnalyticsName +} + +output workspaceId string = logAnalyticsWorkspace.id diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.int.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.int.json new file mode 100644 index 000000000..0250aa964 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.int.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.prodv2.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.prodv2.json new file mode 100644 index 000000000..0250aa964 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.prodv2.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.ua.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.ua.json new file mode 100644 index 000000000..0250aa964 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.ua.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/servicePlan.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/servicePlan.bicep new file mode 100644 index 000000000..48d575c91 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/servicePlan.bicep @@ -0,0 +1,25 @@ +@description('Service plan name.') +@minLength(1) +param name string + +@description('Service plan sku.') +param sku string = 'Y1' + +@description('Service plan location.') +param location string + +@description('Maximum elastic worker count.') +param maximumElasticWorkerCount int = 1 + +resource servicePlan 'Microsoft.Web/serverfarms@2018-02-01' = { + name: name + location: location + properties: { + maximumElasticWorkerCount: maximumElasticWorkerCount + targetWorkerCount: maximumElasticWorkerCount + } + sku: { + name: sku + tier: 'Dynamic' + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep new file mode 100644 index 000000000..e117c61fa --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep @@ -0,0 +1,279 @@ +@description('Enter an abbreviation for the solution.') +@minLength(2) +@maxLength(3) +param solutionAbbreviation string = 'gmm' + +@description('Classify the types of resources in this resource group.') +@allowed([ + 'prereqs' + 'data' + 'compute' +]) +param resourceGroupClassification string = 'compute' + +@description('Enter an abbreviation for the environment.') +@minLength(2) +@maxLength(6) +param environmentAbbreviation string + +@description('Tenant id.') +param tenantId string + +@description('Service plan name.') +param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'GroupMembershipObtainer'),0,8)}' + +@description('Service plan sku') +param servicePlanSku string = 'Y1' + +@description('Resource location.') +param location string + +@description('Enter function app name.') +param functionAppName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}' + +@description('Function app kind.') +@allowed([ + 'functionapp' + 'linux' + 'container' +]) +param functionAppKind string = 'functionapp' + +@description('Maximum elastic worker count.') +param maximumElasticWorkerCount int = 1 + +@description('Enter application insights name.') +param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' + +@description('Resource group where Application Insights is located.') +param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' + +@description('Enter storage account name.') +param storageAccountName string + +@description('Resource group where storage account is located.') +param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' + +@description('Name of the resource group where the \'prereqs\' key vault is located.') +param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' + +@description('Name of the resource group where the \'prereqs\' key vault is located.') +param prereqsKeyVaultResourceGroup string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' + +@description('Name of the \'data\' key vault.') +param dataKeyVaultName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' + +@description('Name of the resource group where the \'data\' key vault is located.') +param dataKeyVaultResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' + +@description('Provides the endpoint for the app configuration resource.') +param appConfigurationEndpoint string = 'https://${solutionAbbreviation}-appconfig-${environmentAbbreviation}.azconfig.io' + +@description('Flag to indicate if the deployment should set RBAC permissions.') +param setRBACPermissions bool = false + +var logAnalyticsCustomerId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsCustomerId') +var logAnalyticsPrimarySharedKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsPrimarySharedKey') +var serviceBusFQN = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusFQN') +var serviceBusSyncJobTopic = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusSyncJobTopic') +var serviceBusMembershipAggregatorQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusMembershipAggregatorQueue') +var graphAppClientId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppClientId') +var graphAppClientSecret = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppClientSecret') +var graphAppCertificateName = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppCertificateName') +var graphAppTenantId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppTenantId') +var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderUsername') +var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') +var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') +var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') +var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') +var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') +var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') +var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierProviderId') +var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') +var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') +var groupMembershipObtainerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'groupMembershipObtainerStorageAccountProd') +var groupMembershipObtainerStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'groupMembershipObtainerStorageAccountStaging') +var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') +var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') + +module servicePlanTemplate 'servicePlan.bicep' = { + name: 'servicePlanTemplate-GroupMembershipObtainer' + params: { + name: servicePlanName + sku: servicePlanSku + location: location + maximumElasticWorkerCount: maximumElasticWorkerCount + } +} + +var commonSettings = { + WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 + WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 + SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 + FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_EXTENSION_VERSION: '~4' +} + +var appSettings = { + 'AzureFunctionsJobHost:extensions:durableTask:extendedSessionsEnabled': toLower(environmentAbbreviation) == 'prodv2' ? 'True' : 'False' + APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' + WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + WEBSITE_CONTENTSHARE: toLower('functionApp-GroupMembershipObtainer') + serviceBusMembershipAggregatorQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusMembershipAggregatorQueue, '2019-09-01').secretUriWithVersion})' + serviceBusSyncJobTopic: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusSyncJobTopic, '2019-09-01').secretUriWithVersion})' + gmmServiceBus__fullyQualifiedNamespace: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusFQN, '2019-09-01').secretUriWithVersion})' + 'graphCredentials:ClientCertificateName': '@Microsoft.KeyVault(SecretUri=${reference(graphAppCertificateName, '2019-09-01').secretUriWithVersion})' + 'graphCredentials:ClientSecret': '@Microsoft.KeyVault(SecretUri=${reference(graphAppClientSecret, '2019-09-01').secretUriWithVersion})' + 'graphCredentials:ClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphAppClientId, '2019-09-01').secretUriWithVersion})' + 'graphCredentials:TenantId': '@Microsoft.KeyVault(SecretUri=${reference(graphAppTenantId, '2019-09-01').secretUriWithVersion})' + 'graphCredentials:KeyVaultName': prereqsKeyVaultName + 'graphCredentials:KeyVaultTenantId': tenantId + 'ConnectionStrings:JobsContext': '@Microsoft.KeyVault(SecretUri=${reference(jobsMSIConnectionString, '2019-09-01').secretUriWithVersion})' + 'ConnectionStrings:JobsContextReadOnly': '@Microsoft.KeyVault(SecretUri=${reference(replicaJobsMSIConnectionString, '2019-09-01').secretUriWithVersion})' + logAnalyticsCustomerId: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsCustomerId, '2019-09-01').secretUriWithVersion})' + logAnalyticsPrimarySharedKey: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsPrimarySharedKey, '2019-09-01').secretUriWithVersion})' + senderAddress: '@Microsoft.KeyVault(SecretUri=${reference(senderUsername, '2019-09-01').secretUriWithVersion})' + senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' + syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' + supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' + membershipStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(membershipStorageAccountName, '2019-09-01').secretUriWithVersion})' + membershipContainerName: '@Microsoft.KeyVault(SecretUri=${reference(membershipContainerName, '2019-09-01').secretUriWithVersion})' + appConfigurationEndpoint: appConfigurationEndpoint + actionableEmailProviderId: '@Microsoft.KeyVault(SecretUri=${reference(actionableEmailProviderId, '2019-09-01').secretUriWithVersion})' + 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' + serviceBusNotificationsQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusNotificationsQueue, '2019-09-01').secretUriWithVersion})' +} + +var stagingSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountStaging, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupMembershipObtainerStaging' + 'AzureWebJobs.StarterFunction.Disabled': 1 + 'AzureWebJobs.OrchestratorFunction.Disabled': 1 + 'AzureWebJobs.SubOrchestratorFunction.Disabled': 1 + 'AzureWebJobs.DeltaUsersReaderFunction.Disabled': 1 + 'AzureWebJobs.DeltaUsersSenderFunction.Disabled': 1 + 'AzureWebJobs.EmailSenderFunction.Disabled': 1 + 'AzureWebJobs.FileDownloaderFunction.Disabled': 1 + 'AzureWebJobs.GroupsReaderFunction.Disabled': 1 + 'AzureWebJobs.GroupValidatorFunction.Disabled': 1 + 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 1 + 'AzureWebJobs.MembersReaderFunction.Disabled': 1 + 'AzureWebJobs.SourceGroupsReaderFunction.Disabled': 1 + 'AzureWebJobs.SubsequentDeltaUsersReaderFunction.Disabled': 1 + 'AzureWebJobs.SubsequentMembersReaderFunction.Disabled': 1 + 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled': 1 + 'AzureWebJobs.UsersReaderFunction.Disabled': 1 + 'AzureWebJobs.UsersSenderFunction.Disabled': 1 + AzureFunctionsWebHost__hostid: 'GroupMembershipObtainerStaging' +} + +var productionSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupMembershipObtainer' + AzureFunctionsWebHost__hostid: 'GroupMembershipObtainer' +} + +resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: dataKeyVaultName + scope: resourceGroup(dataKeyVaultResourceGroup) +} + +module userAssignedManagedIdentityNameReader 'keyVaultReader.bicep' = { + name: 'uamiNameReader-GroupMembershipObtainer' + params: { + value: dataKeyVault.getSecret('graphUserAssignedManagedIdentityName') + } + dependsOn: [ + dataKeyVault + ] +} + +resource graphUAMI 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = { + name: userAssignedManagedIdentityNameReader.outputs.value + scope: resourceGroup(dataKeyVaultResourceGroup) +} + +module existingLogAnalyticsWorkspace 'logAnalyticsWorkspace.bicep' = { + name: 'existingLogAnalyticsWorkspace' + scope: resourceGroup('${solutionAbbreviation}-data-${environmentAbbreviation}') + params: { + environmentAbbreviation: environmentAbbreviation + resourceGroupClassification: 'data' + solutionAbbreviation: solutionAbbreviation + } +} + +module functionAppTemplate_GroupMembershipObtainer 'functionApp.bicep' = { + name: 'functionAppTemplate-GroupMembershipObtainer' + params: { + name: '${functionAppName}-GroupMembershipObtainer' + kind: functionAppKind + location: location + servicePlanName: servicePlanName + secretSettings: commonSettings + userManagedIdentities:{ + '${graphUAMI.id}' : {} + } + logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId + } + dependsOn: [ + servicePlanTemplate + graphUAMI + existingLogAnalyticsWorkspace + ] +} + +module functionAppSlotTemplate_GroupMembershipObtainer 'functionAppSlot.bicep' = { + name: 'functionAppSlotTemplate-GroupMembershipObtainer' + params: { + name: '${functionAppName}-GroupMembershipObtainer/staging' + kind: functionAppKind + location: location + servicePlanName: servicePlanName + secretSettings: commonSettings + userManagedIdentities:{ + '${graphUAMI.id}' : {} + } + logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId + } + dependsOn: [ + functionAppTemplate_GroupMembershipObtainer + ] +} + +module functionAppRBAC 'functionAppRBAC.bicep' = { + name: 'functionAppsRBAC-GroupMembershipObtainer' + params: { + functionName: 'GroupMembershipObtainer' + prereqsKeyVaultName: prereqsKeyVaultName + prereqsKeyVaultResourceGroup: prereqsKeyVaultResourceGroup + dataKeyVaultName: dataKeyVaultName + dataKeyVaultResourceGroup: dataKeyVaultResourceGroup + setRBACPermissions: setRBACPermissions + productionSlotPrincipalId: functionAppTemplate_GroupMembershipObtainer.outputs.msi + stagingSlotPrincipalId: functionAppSlotTemplate_GroupMembershipObtainer.outputs.msi + } + dependsOn: [ + functionAppTemplate_GroupMembershipObtainer + functionAppSlotTemplate_GroupMembershipObtainer + ] +} + +resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { + name: '${functionAppName}-GroupMembershipObtainer/appsettings' + kind: 'string' + properties: union(commonSettings, appSettings, productionSettings) + dependsOn: [ + functionAppRBAC + ] +} + +resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { + name: '${functionAppName}-GroupMembershipObtainer/staging/appsettings' + kind: 'string' + properties: union(commonSettings, appSettings, stagingSettings) + dependsOn: [ + functionAppRBAC + functionAppSettings + ] +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/keyVaultSecretsSecure.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/keyVaultSecretsSecure.bicep new file mode 100644 index 000000000..942104ea1 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/keyVaultSecretsSecure.bicep @@ -0,0 +1,12 @@ +param keyVaultName string +@secure() +param keyVaultSecrets object + +resource secrets 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = [for secret in keyVaultSecrets.secrets: { + name: '${keyVaultName}/${secret.name}' + properties: { + value: secret.value + } +}] + + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.int.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.int.json new file mode 100644 index 000000000..0250aa964 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.int.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.prodv2.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.prodv2.json new file mode 100644 index 000000000..0250aa964 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.prodv2.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.ua.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.ua.json new file mode 100644 index 000000000..0250aa964 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.ua.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/storageAccount.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/storageAccount.bicep new file mode 100644 index 000000000..632d30074 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/storageAccount.bicep @@ -0,0 +1,110 @@ +@description('Storage account alphanumeric name.') +@minLength(1) +@maxLength(24) +param name string + +@description('Key vault name.') +param keyVaultName string + +@allowed([ + 'Standard_LRS' + 'Standard_GRS' + 'Standard_ZRS' + 'Premium_LRS' +]) +param sku string = 'Standard_LRS' + +@description('Key vault name.') +param addJobsStorageAccountPolicies bool = false + +@description('Specifies the Azure location where the storage account will be created.') +param location string + +@description('Key vault setting name to store the connection string.') +param storageAccountConnectionStringSettingName string + +resource storageAccount 'Microsoft.Storage/storageAccounts@2019-04-01' = { + name: name + location: location + kind: 'StorageV2' + sku: { + name: sku + } + properties: { + supportsHttpsTrafficOnly: true + allowBlobPublicAccess: false + minimumTlsVersion: 'TLS1_2' + } + identity: { + type: 'SystemAssigned' + } +} + +resource allBlobPolicy 'Microsoft.Storage/storageAccounts/managementPolicies@2022-05-01' = if (addJobsStorageAccountPolicies) { + name: 'default' + parent: storageAccount + properties: { + policy: { + rules: [ + { + definition: { + actions: { + baseBlob: { + delete: { + daysAfterModificationGreaterThan: 30 + } + } + } + filters: { + blobTypes: [ + 'blockBlob' + ] + } + } + enabled: true + name: '30-Day Blob Deletion Policy' + type: 'Lifecycle' + } + { + definition: { + actions: { + baseBlob: { + delete: { + daysAfterModificationGreaterThan: 7 + } + } + } + filters: { + blobTypes: [ + 'blockBlob' + ] + prefixMatch: [ + 'membership/cache/' + ] + } + } + enabled: true + name: '7-Day Cache Blob Deletion Policy' + type: 'Lifecycle' + } + ] + } + } +} + +module secureSecretsTemplate 'keyVaultSecretsSecure.bicep' = { + name: 'secureSecretsTemplate${name}' + params: { + keyVaultName: keyVaultName + keyVaultSecrets: { + secrets: [ + { + name: storageAccountConnectionStringSettingName + value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value}' + } + ] + } + } +} + + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep new file mode 100644 index 000000000..515e46c1c --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep @@ -0,0 +1,47 @@ +@description('Enter an abbreviation for the environment.') +@minLength(2) +@maxLength(6) +param environmentAbbreviation string + +@description('Enter an abbreviation for the solution.') +@minLength(2) +@maxLength(3) +param solutionAbbreviation string = 'gmm' + +@description('Enter tenant Id.') +param tenantId string + +@description('Enter storage account name.') +param storageAccountName string + +param storageAccountSku string = 'Standard_LRS' + +@description('Resource location.') +param location string + +var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' +var prodStorageAccountName = substring('gmo${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) +var stagingStorageAccountName = substring('gmo${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) + +module gmoStorageAccountProd 'storageAccount.bicep' = { + name: 'gmoProdstorageAccountTemplate' + params: { + name: prodStorageAccountName + sku: storageAccountSku + keyVaultName: keyVaultName + location: location + storageAccountConnectionStringSettingName: 'groupMembershipObtainerStorageAccountProd' + } +} + +module gmoStorageAccountStaging 'storageAccount.bicep' = { + name: 'gmoStagingstorageAccountTemplate' + params: { + name: stagingStorageAccountName + sku: storageAccountSku + keyVaultName: keyVaultName + location: location + storageAccountConnectionStringSettingName: 'groupMembershipObtainerStorageAccountStaging' + } +} + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/Class1.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/Class1.cs new file mode 100644 index 000000000..db9c4438f --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/Class1.cs @@ -0,0 +1,7 @@ +namespace JobFinalizerService +{ + public class Class1 + { + + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.csproj new file mode 100644 index 000000000..132c02c59 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.sln b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.sln new file mode 100644 index 000000000..d262312dd --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35027.167 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobFinalizerService", "JobFinalizerService.csproj", "{AC7A4643-5D81-494C-AC70-893FAC822B1F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AC7A4643-5D81-494C-AC70-893FAC822B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC7A4643-5D81-494C-AC70-893FAC822B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC7A4643-5D81-494C-AC70-893FAC822B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC7A4643-5D81-494C-AC70-893FAC822B1F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D9018227-C4CC-4159-99DE-055034687C1E} + EndGlobalSection +EndGlobal From 2bb24f91436a57a86290180f0d74c431fa3ce982 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 15 Jul 2024 14:55:10 -0700 Subject: [PATCH 0201/1479] Add basic structure of job finalizer --- Deployment/computeResources.bicep | 2 +- .../JobStatusUpdaterFunction.cs | 34 ++++++++++ .../JobStatusUpdaterRequest.cs | 13 ++++ .../Hosts/JobFinalizer/Function/Function1.cs | 50 -------------- .../Orchestrator/OrchestratorFunction.cs | 66 +++++++++++++++++++ .../Orchestrator/OrchrestratorRequest.cs | 13 ++++ .../Function/Properties/launchSettings.json | 9 --- .../Properties/serviceDependencies.json | 11 ---- .../Properties/serviceDependencies.local.json | 11 ---- .../Function/Starter/StarterFunction.cs | 51 ++++++++++++++ .../Hosts/JobFinalizer/Function/Startup.cs | 41 ++++++++++++ .../Hosts/JobFinalizer/Function/host.json | 31 ++++++--- .../Infrastructure/compute/template.bicep | 38 +++++------ .../Infrastructure/data/template.bicep | 4 +- .../Services/JobFinalizerService/Class1.cs | 7 -- .../JobFinalizerService.cs | 42 ++++++++++++ vsts-cicd.yml | 2 +- 17 files changed, 305 insertions(+), 120 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Function1.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/launchSettings.json delete mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.json delete mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.local.json create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/Class1.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.cs diff --git a/Deployment/computeResources.bicep b/Deployment/computeResources.bicep index 88b5223f0..f8194e9a1 100644 --- a/Deployment/computeResources.bicep +++ b/Deployment/computeResources.bicep @@ -24,7 +24,7 @@ param pipeline string var prereqsResourceGroupName = isManagedApplication ? managedResourceGroupName : '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' var dataResourceGroupName = isManagedApplication ? managedResourceGroupName : '${solutionAbbreviation}-data-${environmentAbbreviation}' var computeResourceGroupName = isManagedApplication ? managedResourceGroupName : '${solutionAbbreviation}-compute-${environmentAbbreviation}' - +// TODO: Add the new function here // function resources // ----------------- JobTrigger module jobTriggerDataResources '../Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/data/template.bicep' = { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs new file mode 100644 index 000000000..0cadc7f87 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Models; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using System.Threading.Tasks; + +namespace Hosts.GroupMembershipObtainer +{ + public class JobStatusUpdaterFunction + { + private readonly ILoggingRepository _loggingRepository; + private readonly SGMembershipCalculator _membershipCalculator; + + public JobStatusUpdaterFunction(ILoggingRepository loggingRepository, SGMembershipCalculator membershipCalculator) + { + _loggingRepository = loggingRepository; + _membershipCalculator = membershipCalculator; + } + + [FunctionName(nameof(JobStatusUpdaterFunction))] + public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest request) + { + if (request.SyncJob != null) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobStatusUpdaterFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); + await _membershipCalculator.UpdateSyncJobStatusAsync(request.SyncJob, request.Status); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobStatusUpdaterFunction)} function completed", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); + } + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs new file mode 100644 index 000000000..867eb42a3 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Models; +using System; + +namespace Hosts.GroupMembershipObtainer +{ + public class JobStatusUpdaterRequest + { + public SyncJob SyncJob { get; set; } + public SyncStatus Status { get; set; } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Function1.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Function1.cs deleted file mode 100644 index a84ccb8a2..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Function1.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Azure.WebJobs.Host; -using Microsoft.Extensions.Logging; - -namespace JobFinalizer -{ - public static class Function1 - { - [FunctionName("Function1")] - public static async Task> RunOrchestrator( - [OrchestrationTrigger] IDurableOrchestrationContext context) - { - var outputs = new List(); - - // Replace "hello" with the name of your Durable Activity Function. - outputs.Add(await context.CallActivityAsync(nameof(SayHello), "Tokyo")); - outputs.Add(await context.CallActivityAsync(nameof(SayHello), "Seattle")); - outputs.Add(await context.CallActivityAsync(nameof(SayHello), "London")); - - // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"] - return outputs; - } - - [FunctionName(nameof(SayHello))] - public static string SayHello([ActivityTrigger] string name, ILogger log) - { - log.LogInformation("Saying hello to {name}.", name); - return $"Hello {name}!"; - } - - [FunctionName("Function1_HttpStart")] - public static async Task HttpStart( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req, - [DurableClient] IDurableOrchestrationClient starter, - ILogger log) - { - // Function input comes from the request content. - string instanceId = await starter.StartNewAsync("Function1", null); - - log.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId); - - return starter.CreateCheckStatusResponse(req, instanceId); - } - } -} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs new file mode 100644 index 000000000..3ebea9a08 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs @@ -0,0 +1,66 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; +using Microsoft.Graph; +using Models; +using Models.Helpers; +using Newtonsoft.Json; +using Repositories.Contracts; +using Hosts.JobFinalizer; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Repositories.Contracts.InjectConfig; +using Models.Notifications; + +namespace Hosts.JobFinalizer +{ + public class OrchestratorFunction + { + private readonly ILoggingRepository _log; + + public OrchestratorFunction( + ILoggingRepository loggingRepository, + ) + { + _log = loggingRepository; + + } + + [FunctionName(nameof(OrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context, ExecutionContext executionContext) + { + var mainRequest = context.GetInput(); + if (mainRequest != null && mainRequest.SyncJob != null) + { + var syncJob = mainRequest.SyncJob; + var runId = syncJob.RunId.GetValueOrDefault(Guid.Empty); + var syncjobStatus = mainRequest.Status; + + if (!string.Equals(syncjobStatus, "unknown", StringComparison.OrdinalIgnoreCase)) + { + SyncStatus status = (SyncStatus)Enum.Parse(typeof(SyncStatus), syncjobStatus, true); + + } + else + { + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { SyncJob = syncJob, Status = SyncStatus.Error }); + if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { RunId = runId, Message = $"{syncJob.TargetOfficeGroupId} pass an unknown status. Marking job as {SyncStatus.Error}."}, VerbosityLevel.DEBUG); + return + } + + if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { Message = $"{nameof(OrchestratorFunction)} function started", RunId = runId }, VerbosityLevel.DEBUG); + + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { SyncJob = syncJob, Status = status }); + + if (!context.IsReplaying) + _ = _log.LogMessageAsync(new LogMessage { Message = $"{nameof(OrchestratorFunction)} function completed", RunId = runId, DynamicProperties = syncJob.ToDictionary() }, VerbosityLevel.DEBUG); + } + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs new file mode 100644 index 000000000..fd3ee3722 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; + +namespace Hosts.GroupMembershipObtainer +{ + public class OrchestratorRequest + { + public SyncJob SyncJob { get; set; } + public string Status { get; set; } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/launchSettings.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/launchSettings.json deleted file mode 100644 index 0f9d3df1a..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/launchSettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "profiles": { - "JobFinalizer": { - "commandName": "Project", - "commandLineArgs": "--port 7245", - "launchBrowser": false - } - } -} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.json deleted file mode 100644 index df4dcc9d8..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "dependencies": { - "appInsights1": { - "type": "appInsights" - }, - "storage1": { - "type": "storage", - "connectionId": "AzureWebJobsStorage" - } - } -} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.local.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.local.json deleted file mode 100644 index b804a2893..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Properties/serviceDependencies.local.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "dependencies": { - "appInsights1": { - "type": "appInsights.sdk" - }, - "storage1": { - "type": "storage.emulator", - "connectionId": "AzureWebJobsStorage" - } - } -} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs new file mode 100644 index 000000000..5556c2abc --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Azure.Messaging.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Models; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using System; +using System.Text; +using System.Threading.Tasks; + +namespace Hosts.JobFinalizer +{ + public class StarterFunction + { + private readonly ILoggingRepository _loggingRepository; + + public StarterFunction(ILoggingRepository loggingRepository) + { + _loggingRepository = loggingRepository; + } + + [FunctionName(nameof(StarterFunction))] + public async Task RunAsync( + [ServiceBusTrigger("%serviceBusSyncJobTopic%", "SyncJobStatus", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, + [DurableClient] IDurableOrchestrationClient starter) + { + var syncJob = JsonSerializer.DeserializeObject(Encoding.UTF8.GetString(message.Body)); + var runId = syncJob.RunId.GetValueOrDefault(Guid.Empty); + _loggingRepository.SetSyncJobProperties(runId, syncJob.ToDictionary()); + + string syncjobStatus = message.ApplicationProperties.ContainsKey("SyncjobStatus") + ? message.ApplicationProperties["SyncjobStatus"].ToString() + : "Unknown"; + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started", RunId = runId }, VerbosityLevel.DEBUG); + + var request = new OrchestratorRequest + { + SyncJob = syncJob, + Status = syncjobStatus + }; + + var instanceId = await starter.StartNewAsync(nameof(OrchestratorFunction), request); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"InstanceId: {instanceId} for job Id: {syncJob.Id} ", RunId = runId }); + + + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed", RunId = runId }, VerbosityLevel.DEBUG); + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs new file mode 100644 index 000000000..ea3b306ac --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using DIConcreteTypes; +using Hosts.FunctionBase; +using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Repositories.BlobStorage; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.GraphGroups; +using Repositories.ServiceBusQueue; + +// see https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection +[assembly: FunctionsStartup(typeof(Hosts.JobFinalizer.Startup))] + +namespace Hosts.JobFinalizer +{ + public class Startup : CommonStartup + { + protected override string FunctionName => nameof(JobFinalizer); + protected override string DryRunSettingName => "JobFinalizer:IsDryRunEnabled"; + + public override void Configure(IFunctionsHostBuilder builder) + { + base.Configure(builder); + + .AddScoped(services => + { + return new JobFinalizerService( + services.GetRequiredService(), + services.GetRequiredService(), + + ); + }) + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/host.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/host.json index ee5cf5f83..b715b753b 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/host.json @@ -1,12 +1,25 @@ { - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - }, - "enableLiveMetricsFilters": true - } + "version": "2.0", + "functionTimeout": "00:10:00", + "concurrency": { + "dynamicConcurrencyEnabled": true, + "snapshotPersistenceEnabled": true + }, + "extensions": { + "durableTask": { + "extendedSessionsEnabled": true, + "extendedSessionIdleTimeoutInSeconds": 30 } + }, + "logging": { + "logLevel": { + "Host.Concurrency": "Trace" + }, + "applicationInsights": { + "samplingExcludedTypes": "Request", + "samplingSettings": { + "isEnabled": true + } + } + } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep index e117c61fa..fafa70177 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep @@ -20,7 +20,7 @@ param environmentAbbreviation string param tenantId string @description('Service plan name.') -param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'GroupMembershipObtainer'),0,8)}' +param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'JobFinalizer'),0,8)}' @description('Service plan sku') param servicePlanSku string = 'Y1' @@ -91,13 +91,13 @@ var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, da var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierProviderId') var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') -var groupMembershipObtainerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'groupMembershipObtainerStorageAccountProd') -var groupMembershipObtainerStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'groupMembershipObtainerStorageAccountStaging') +var jobFinalizerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobFinalizerStorageAccountProd') +var jobFinalizerStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobFinalizerStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') module servicePlanTemplate 'servicePlan.bicep' = { - name: 'servicePlanTemplate-GroupMembershipObtainer' + name: 'servicePlanTemplate-JobFinalizer' params: { name: servicePlanName sku: servicePlanSku @@ -117,8 +117,8 @@ var commonSettings = { var appSettings = { 'AzureFunctionsJobHost:extensions:durableTask:extendedSessionsEnabled': toLower(environmentAbbreviation) == 'prodv2' ? 'True' : 'False' APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' - WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - WEBSITE_CONTENTSHARE: toLower('functionApp-GroupMembershipObtainer') + WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + WEBSITE_CONTENTSHARE: toLower('functionApp-JobFinalizer') serviceBusMembershipAggregatorQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusMembershipAggregatorQueue, '2019-09-01').secretUriWithVersion})' serviceBusSyncJobTopic: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusSyncJobTopic, '2019-09-01').secretUriWithVersion})' gmmServiceBus__fullyQualifiedNamespace: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusFQN, '2019-09-01').secretUriWithVersion})' @@ -145,7 +145,7 @@ var appSettings = { } var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountStaging, '2019-09-01').secretUriWithVersion})' + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountStaging, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupMembershipObtainerStaging' 'AzureWebJobs.StarterFunction.Disabled': 1 'AzureWebJobs.OrchestratorFunction.Disabled': 1 @@ -168,9 +168,9 @@ var stagingSettings = { } var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(groupMembershipObtainerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupMembershipObtainer' - AzureFunctionsWebHost__hostid: 'GroupMembershipObtainer' + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobFinalizer' + AzureFunctionsWebHost__hostid: 'JobFinalizer' } resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -179,7 +179,7 @@ resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { } module userAssignedManagedIdentityNameReader 'keyVaultReader.bicep' = { - name: 'uamiNameReader-GroupMembershipObtainer' + name: 'uamiNameReader-JobFinalizer' params: { value: dataKeyVault.getSecret('graphUserAssignedManagedIdentityName') } @@ -204,9 +204,9 @@ module existingLogAnalyticsWorkspace 'logAnalyticsWorkspace.bicep' = { } module functionAppTemplate_GroupMembershipObtainer 'functionApp.bicep' = { - name: 'functionAppTemplate-GroupMembershipObtainer' + name: 'functionAppTemplate-JobFinalizer' params: { - name: '${functionAppName}-GroupMembershipObtainer' + name: '${functionAppName}-JobFinalizer' kind: functionAppKind location: location servicePlanName: servicePlanName @@ -224,9 +224,9 @@ module functionAppTemplate_GroupMembershipObtainer 'functionApp.bicep' = { } module functionAppSlotTemplate_GroupMembershipObtainer 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-GroupMembershipObtainer' + name: 'functionAppSlotTemplate-JobFinalizer' params: { - name: '${functionAppName}-GroupMembershipObtainer/staging' + name: '${functionAppName}-JobFinalizer/staging' kind: functionAppKind location: location servicePlanName: servicePlanName @@ -242,9 +242,9 @@ module functionAppSlotTemplate_GroupMembershipObtainer 'functionAppSlot.bicep' = } module functionAppRBAC 'functionAppRBAC.bicep' = { - name: 'functionAppsRBAC-GroupMembershipObtainer' + name: 'functionAppsRBAC-JobFinalizer' params: { - functionName: 'GroupMembershipObtainer' + functionName: 'JobFinalizer' prereqsKeyVaultName: prereqsKeyVaultName prereqsKeyVaultResourceGroup: prereqsKeyVaultResourceGroup dataKeyVaultName: dataKeyVaultName @@ -260,7 +260,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { } resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { - name: '${functionAppName}-GroupMembershipObtainer/appsettings' + name: '${functionAppName}-JobFinalizer/appsettings' kind: 'string' properties: union(commonSettings, appSettings, productionSettings) dependsOn: [ @@ -269,7 +269,7 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { } resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-GroupMembershipObtainer/staging/appsettings' + name: '${functionAppName}-JobFinalizer/staging/appsettings' kind: 'string' properties: union(commonSettings, appSettings, stagingSettings) dependsOn: [ diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep index 515e46c1c..010696f83 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep @@ -30,7 +30,7 @@ module gmoStorageAccountProd 'storageAccount.bicep' = { sku: storageAccountSku keyVaultName: keyVaultName location: location - storageAccountConnectionStringSettingName: 'groupMembershipObtainerStorageAccountProd' + storageAccountConnectionStringSettingName: 'jobFinalizerStorageAccountProd' } } @@ -41,7 +41,7 @@ module gmoStorageAccountStaging 'storageAccount.bicep' = { sku: storageAccountSku keyVaultName: keyVaultName location: location - storageAccountConnectionStringSettingName: 'groupMembershipObtainerStorageAccountStaging' + storageAccountConnectionStringSettingName: 'jobFinalizerStorageAccountStaging' } } diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/Class1.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/Class1.cs deleted file mode 100644 index db9c4438f..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace JobFinalizerService -{ - public class Class1 - { - - } -} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.cs new file mode 100644 index 000000000..07ca65675 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Models; +using Models.Entities; +using Models.Notifications; +using Models.ServiceBus; +using Newtonsoft.Json; +using Polly; +using Polly.Retry; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Threading.Tasks; +using Models.Helpers; + +namespace Hosts.JobFinalizer +{ + public class JobFinalizerService + { + + private readonly ILoggingRepository _log; + private readonly IDatabaseSyncJobsRepository _databaseSyncJobsRepository; + + + public SGMembershipCalculator( + IDatabaseSyncJobsRepository databaseSyncJobsRepository, + ILoggingRepository logging, + ) + { + _log = logging; + _databaseSyncJobsRepository = databaseSyncJobsRepository; + } + + public async Task UpdateSyncJobStatusAsync(SyncJob job, SyncStatus status) + { + await _databaseSyncJobsRepository.UpdateSyncJobStatusAsync(new[] { job }, status); + } + + } +} \ No newline at end of file diff --git a/vsts-cicd.yml b/vsts-cicd.yml index 4b23271c4..6941c5bdc 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -45,7 +45,7 @@ stages: repoToCheckout: self checkoutPath: '$(Build.BuildNumber)' buildRelease: ${{variables.buildRelease}} - +#TODO: Modify the function here - template: yaml/build-functionapps.yml parameters: dependsOn: Build_Common From 5fd1f3f1f0b8ba5a2ae7ed0e9b1d54b492b4f5c4 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 16 Jul 2024 10:57:24 -0700 Subject: [PATCH 0202/1479] Add dependency of the basic stracture --- .../Hosts.FunctionBase.csproj | 1 - .../JobStatusUpdaterFunction.cs | 12 ++++----- .../JobStatusUpdaterRequest.cs | 2 +- .../JobFinalizer/Function/JobFinalizer.csproj | 20 +++++++++++--- .../JobFinalizer/Function/JobFinalizer.sln | 26 ++++++++++++++++++- .../Orchestrator/OrchestratorFunction.cs | 8 +++--- .../Orchestrator/OrchrestratorRequest.cs | 2 +- .../Function/Starter/StarterFunction.cs | 4 ++- .../Hosts/JobFinalizer/Function/Startup.cs | 12 +++------ .../IJobFInalizerService.cs | 14 ++++++++++ .../Services.Contracts.csproj} | 6 +++-- .../JobFinalizerService.cs | 10 +++---- .../Services/JobFinalizerService.csproj | 22 ++++++++++++++++ .../JobFinalizerService.sln | 0 .../JobFinalizer/Services/Services.csproj | 22 ++++++++++++++++ 15 files changed, 127 insertions(+), 34 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/IJobFInalizerService.cs rename Service/GroupMembershipManagement/Hosts/JobFinalizer/{Services/JobFinalizerService/JobFinalizerService.csproj => Services.Contracts/Services.Contracts.csproj} (57%) rename Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/{JobFinalizerService => }/JobFinalizerService.cs (86%) create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.csproj rename Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/{JobFinalizerService => }/JobFinalizerService.sln (100%) create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/Services.csproj diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj b/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj index b74677025..d9618ffda 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj @@ -20,7 +20,6 @@ - diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs index 0cadc7f87..0eb12c519 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -4,20 +4,20 @@ using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; using System.Threading.Tasks; +using Services.Contracts; -namespace Hosts.GroupMembershipObtainer +namespace Hosts.JobFinalizer { public class JobStatusUpdaterFunction { private readonly ILoggingRepository _loggingRepository; - private readonly SGMembershipCalculator _membershipCalculator; + private readonly IJobFinalizerService _jobFinalizerService; - public JobStatusUpdaterFunction(ILoggingRepository loggingRepository, SGMembershipCalculator membershipCalculator) + public JobStatusUpdaterFunction(ILoggingRepository loggingRepository, IJobFinalizerService jobFinalizerService) { _loggingRepository = loggingRepository; - _membershipCalculator = membershipCalculator; + _jobFinalizerService = jobFinalizerService; } [FunctionName(nameof(JobStatusUpdaterFunction))] @@ -26,7 +26,7 @@ public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest if (request.SyncJob != null) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobStatusUpdaterFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); - await _membershipCalculator.UpdateSyncJobStatusAsync(request.SyncJob, request.Status); + await _jobFinalizerService.UpdateSyncJobStatusAsync(request.SyncJob, request.Status); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobStatusUpdaterFunction)} function completed", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); } } diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs index 867eb42a3..7d35d436e 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs @@ -3,7 +3,7 @@ using Models; using System; -namespace Hosts.GroupMembershipObtainer +namespace Hosts.JobFinalizer { public class JobStatusUpdaterRequest { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj index b741ed2d8..bf59b61b7 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj @@ -1,18 +1,30 @@ - + net6.0 v4 - - + + + + + + + + + + + + + + PreserveNewest - PreserveNewest + Always Never diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln index 1dd222d48..162c7b713 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln @@ -3,7 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.10.35027.167 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobFinalizer", "JobFinalizer.csproj", "{D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobFinalizer", "JobFinalizer.csproj", "{D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.FunctionBase", "..\..\..\Hosts.FunctionBase\Hosts.FunctionBase.csproj", "{A54477AB-BD0E-492F-91F1-C70EB4FEDB11}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobFinalizerService", "..\Services\JobFinalizerService.csproj", "{B84FE7BA-3798-4287-8356-6FAD64166217}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Contracts", "..\Services.Contracts\Services.Contracts.csproj", "{9D98B058-0BF5-40C3-86FD-075E1A2E38BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Contracts", "..\..\..\Repositories.Contracts\Repositories.Contracts.csproj", "{DC27F5A3-305C-4B89-B339-5058490DC870}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +23,22 @@ Global {D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}.Debug|Any CPU.Build.0 = Debug|Any CPU {D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}.Release|Any CPU.Build.0 = Release|Any CPU + {A54477AB-BD0E-492F-91F1-C70EB4FEDB11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A54477AB-BD0E-492F-91F1-C70EB4FEDB11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A54477AB-BD0E-492F-91F1-C70EB4FEDB11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A54477AB-BD0E-492F-91F1-C70EB4FEDB11}.Release|Any CPU.Build.0 = Release|Any CPU + {B84FE7BA-3798-4287-8356-6FAD64166217}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B84FE7BA-3798-4287-8356-6FAD64166217}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B84FE7BA-3798-4287-8356-6FAD64166217}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B84FE7BA-3798-4287-8356-6FAD64166217}.Release|Any CPU.Build.0 = Release|Any CPU + {9D98B058-0BF5-40C3-86FD-075E1A2E38BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D98B058-0BF5-40C3-86FD-075E1A2E38BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D98B058-0BF5-40C3-86FD-075E1A2E38BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D98B058-0BF5-40C3-86FD-075E1A2E38BB}.Release|Any CPU.Build.0 = Release|Any CPU + {DC27F5A3-305C-4B89-B339-5058490DC870}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC27F5A3-305C-4B89-B339-5058490DC870}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC27F5A3-305C-4B89-B339-5058490DC870}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC27F5A3-305C-4B89-B339-5058490DC870}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs index 3ebea9a08..112584bbc 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using Repositories.Contracts.InjectConfig; using Models.Notifications; +using Hosts.JobFInalizer; namespace Hosts.JobFinalizer { @@ -25,7 +26,7 @@ public class OrchestratorFunction private readonly ILoggingRepository _log; public OrchestratorFunction( - ILoggingRepository loggingRepository, + ILoggingRepository loggingRepository ) { _log = loggingRepository; @@ -41,17 +42,18 @@ public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrat var syncJob = mainRequest.SyncJob; var runId = syncJob.RunId.GetValueOrDefault(Guid.Empty); var syncjobStatus = mainRequest.Status; + SyncStatus status; if (!string.Equals(syncjobStatus, "unknown", StringComparison.OrdinalIgnoreCase)) { - SyncStatus status = (SyncStatus)Enum.Parse(typeof(SyncStatus), syncjobStatus, true); + status = (SyncStatus)Enum.Parse(typeof(SyncStatus), syncjobStatus, true); } else { await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { SyncJob = syncJob, Status = SyncStatus.Error }); if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { RunId = runId, Message = $"{syncJob.TargetOfficeGroupId} pass an unknown status. Marking job as {SyncStatus.Error}."}, VerbosityLevel.DEBUG); - return + return; } if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { Message = $"{nameof(OrchestratorFunction)} function started", RunId = runId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs index fd3ee3722..fa775357f 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs @@ -3,7 +3,7 @@ using Models; -namespace Hosts.GroupMembershipObtainer +namespace Hosts.JobFInalizer { public class OrchestratorRequest { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs index 5556c2abc..fbc2e7263 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Azure.Messaging.ServiceBus; +using Hosts.JobFInalizer; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; @@ -8,6 +9,7 @@ using Repositories.Contracts.InjectConfig; using System; using System.Text; +using System.Text.Json; using System.Threading.Tasks; namespace Hosts.JobFinalizer @@ -26,7 +28,7 @@ public async Task RunAsync( [ServiceBusTrigger("%serviceBusSyncJobTopic%", "SyncJobStatus", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, [DurableClient] IDurableOrchestrationClient starter) { - var syncJob = JsonSerializer.DeserializeObject(Encoding.UTF8.GetString(message.Body)); + var syncJob = JsonSerializer.Deserialize(Encoding.UTF8.GetString(message.Body)); var runId = syncJob.RunId.GetValueOrDefault(Guid.Empty); _loggingRepository.SetSyncJobProperties(runId, syncJob.ToDictionary()); diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs index ea3b306ac..b99a2defc 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs @@ -7,12 +7,7 @@ using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Repositories.BlobStorage; using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphGroups; -using Repositories.ServiceBusQueue; // see https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection [assembly: FunctionsStartup(typeof(Hosts.JobFinalizer.Startup))] @@ -28,14 +23,13 @@ public override void Configure(IFunctionsHostBuilder builder) { base.Configure(builder); - .AddScoped(services => + builder.Services.AddScoped(services => { return new JobFinalizerService( services.GetRequiredService(), - services.GetRequiredService(), - + services.GetRequiredService() ); - }) + }); } } } diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/IJobFInalizerService.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/IJobFInalizerService.cs new file mode 100644 index 000000000..78e566574 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/IJobFInalizerService.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; +using System; +using System.Threading.Tasks; + +namespace Services.Contracts +{ + public interface IJobFinalizerService + { + Task UpdateSyncJobStatusAsync(SyncJob job, SyncStatus status); + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/Services.Contracts.csproj similarity index 57% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.csproj rename to Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/Services.Contracts.csproj index 132c02c59..1b3fbe831 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/Services.Contracts.csproj @@ -2,8 +2,10 @@ net6.0 - enable - enable + + + + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.cs similarity index 86% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.cs rename to Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.cs index 07ca65675..78719efd6 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.cs @@ -1,10 +1,9 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; using Models.Entities; using Models.Notifications; using Models.ServiceBus; -using Newtonsoft.Json; using Polly; using Polly.Retry; using Repositories.Contracts; @@ -14,19 +13,20 @@ using System.Net.Sockets; using System.Threading.Tasks; using Models.Helpers; +using Services.Contracts; namespace Hosts.JobFinalizer { - public class JobFinalizerService + public class JobFinalizerService : IJobFinalizerService { private readonly ILoggingRepository _log; private readonly IDatabaseSyncJobsRepository _databaseSyncJobsRepository; - public SGMembershipCalculator( + public JobFinalizerService( IDatabaseSyncJobsRepository databaseSyncJobsRepository, - ILoggingRepository logging, + ILoggingRepository logging ) { _log = logging; diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.csproj new file mode 100644 index 000000000..7fcd9bcb5 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.sln b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.sln similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService/JobFinalizerService.sln rename to Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.sln diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/Services.csproj new file mode 100644 index 000000000..b048e3b8d --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/Services.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + + + + + + + + + + + + + + PreserveNewest + + + + From d8849a772c44d977b7165165cc2beaf277df3c25 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 16 Jul 2024 13:56:26 -0700 Subject: [PATCH 0203/1479] Modifiy the script --- .../Set-AppConfigurationManagedIdentityRoles.ps1 | 2 +- .../Set-StorageAccountContainerManagedIdentityRoles.ps1 | 2 +- vsts-cicd.yml | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 index d83e815ab..f53402a27 100644 --- a/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 @@ -35,7 +35,7 @@ function Set-AppConfigurationManagedIdentityRoles [string] $ErrorActionPreference = $Stop ) - $apps = @("WebApi","GraphUpdater","MembershipAggregator","GroupMembershipObtainer","SqlMembershipObtainer","PlaceMembershipObtainer","AzureMaintenance","AzureUserReader","JobScheduler","JobTrigger","NonProdService","Notifier","TeamsChannelMembershipObtainer","GroupOwnershipObtainer", "TeamsChannelUpdater", "DestinationAttributesUpdater") + $apps = @("WebApi","GraphUpdater","MembershipAggregator","GroupMembershipObtainer","SqlMembershipObtainer","PlaceMembershipObtainer","AzureMaintenance","AzureUserReader","JobScheduler","JobTrigger","NonProdService","Notifier","TeamsChannelMembershipObtainer","GroupOwnershipObtainer", "TeamsChannelUpdater", "DestinationAttributesUpdater", "JobFinalizer") $resourceGroupName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation"; if($DataResourceGroupName) diff --git a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 index 7ae8d1430..407eb49fa 100644 --- a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 @@ -36,7 +36,7 @@ function Set-StorageAccountContainerManagedIdentityRoles [string] $ErrorActionPreference = $Stop ) - $functionApps = @("GroupMembershipObtainer","SqlMembershipObtainer","PlaceMembershipObtainer","MembershipAggregator","GraphUpdater","TeamsChannelMembershipObtainer","GroupOwnershipObtainer","TeamsChannelUpdater", "DestinationAttributesUpdater") + $functionApps = @("GroupMembershipObtainer","SqlMembershipObtainer","PlaceMembershipObtainer","MembershipAggregator","GraphUpdater","TeamsChannelMembershipObtainer","GroupOwnershipObtainer","TeamsChannelUpdater", "DestinationAttributesUpdater", "JobFinalizer") foreach ($functionApp in $functionApps) { diff --git a/vsts-cicd.yml b/vsts-cicd.yml index 6941c5bdc..44bd21187 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -99,6 +99,9 @@ stages: - function: name: 'GroupOwnershipObtainer' coverageThreshold: 80 + - function: + name: 'JobFinalizer' + coverageThreshold: 0 - template: yaml/build-release-package.yml parameters: From f9c16421eb1a836217e4a633cbbe808860ae4acd Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 16 Jul 2024 14:02:24 -0700 Subject: [PATCH 0204/1479] Add function in the deployment file --- Deployment/computeResources.bicep | 33 ++- .../Hosts/JobFinalizer/Function/.gitignore | 264 ------------------ .../JobFinalizer/Function/local.settings.json | 7 + 3 files changed, 39 insertions(+), 265 deletions(-) delete mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/.gitignore create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/local.settings.json diff --git a/Deployment/computeResources.bicep b/Deployment/computeResources.bicep index f8194e9a1..7007e1e36 100644 --- a/Deployment/computeResources.bicep +++ b/Deployment/computeResources.bicep @@ -24,7 +24,7 @@ param pipeline string var prereqsResourceGroupName = isManagedApplication ? managedResourceGroupName : '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' var dataResourceGroupName = isManagedApplication ? managedResourceGroupName : '${solutionAbbreviation}-data-${environmentAbbreviation}' var computeResourceGroupName = isManagedApplication ? managedResourceGroupName : '${solutionAbbreviation}-compute-${environmentAbbreviation}' -// TODO: Add the new function here + // function resources // ----------------- JobTrigger module jobTriggerDataResources '../Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/data/template.bicep' = { @@ -467,6 +467,37 @@ module jobSchedulerComputeResources '../Service/GroupMembershipManagement/Hosts/ ] } +// ----------------- JobFinalizer +module jobFinalizerDataResources '../Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep' = { + name: 'jobFinalizerDataResourcesTemplate' + scope: resourceGroup(dataResourceGroupName) + params: { + location: location + environmentAbbreviation: environmentAbbreviation + solutionAbbreviation: solutionAbbreviation + tenantId: tenantId + storageAccountName: 'notused' + } +} + +module jobFinalizerComputeResources '../Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep' = { + name: 'jobFinalizerComputeResourcesTemplate' + scope: resourceGroup(computeResourceGroupName) + params: { + location: location + environmentAbbreviation: environmentAbbreviation + solutionAbbreviation: solutionAbbreviation + tenantId: tenantId + storageAccountName: 'notUsed' + prereqsKeyVaultResourceGroup: prereqsResourceGroupName + dataKeyVaultResourceGroup: dataResourceGroupName + setRBACPermissions: setRBACPermissions + } + dependsOn: [ + jobFinalizerDataResources + ] +} + // web api module webApiDataResources '../Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/data/template.bicep' = { name: 'webApiDataResourcesTemplate' diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/.gitignore b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/.gitignore deleted file mode 100644 index ff5b00c50..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/.gitignore +++ /dev/null @@ -1,264 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# Azure Functions localsettings file -local.settings.json - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -#*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/local.settings.json new file mode 100644 index 000000000..7b93cb670 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "APPINSIGHTS_INSTRUMENTATIONKEY": "", + "AzureWebJobsStorage": "UseDevelopmentStorage=true" + } +} \ No newline at end of file From 3b6756db7b350bab899e2caef824a6bc78bc1939 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 16 Jul 2024 15:27:08 -0700 Subject: [PATCH 0205/1479] Add project reference --- .../Hosts.FunctionBase/Hosts.FunctionBase.csproj | 1 + .../Hosts/JobFinalizer/Function/JobFinalizer.sln | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj b/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj index d9618ffda..529ea4d0e 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/Hosts.FunctionBase.csproj @@ -28,6 +28,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln index 162c7b713..4138328a3 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Contracts", "..\Se EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Contracts", "..\..\..\Repositories.Contracts\Repositories.Contracts.csproj", "{DC27F5A3-305C-4B89-B339-5058490DC870}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\Models.csproj", "{F7C85A82-C663-4E0A-9016-693DE824A0E7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {DC27F5A3-305C-4B89-B339-5058490DC870}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC27F5A3-305C-4B89-B339-5058490DC870}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC27F5A3-305C-4B89-B339-5058490DC870}.Release|Any CPU.Build.0 = Release|Any CPU + {F7C85A82-C663-4E0A-9016-693DE824A0E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7C85A82-C663-4E0A-9016-693DE824A0E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7C85A82-C663-4E0A-9016-693DE824A0E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7C85A82-C663-4E0A-9016-693DE824A0E7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From e6a5c7824e4136f2868d857e1a0306e3824a8e19 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 30 Jul 2024 11:01:06 -0700 Subject: [PATCH 0206/1479] Reuse service plan --- .../Infrastructure/compute/servicePlan.bicep | 25 --------------- .../Infrastructure/compute/template.bicep | 31 ++++++------------- .../Infrastructure/compute/template.bicep | 2 +- 3 files changed, 11 insertions(+), 47 deletions(-) delete mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/servicePlan.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/servicePlan.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/servicePlan.bicep deleted file mode 100644 index 48d575c91..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/servicePlan.bicep +++ /dev/null @@ -1,25 +0,0 @@ -@description('Service plan name.') -@minLength(1) -param name string - -@description('Service plan sku.') -param sku string = 'Y1' - -@description('Service plan location.') -param location string - -@description('Maximum elastic worker count.') -param maximumElasticWorkerCount int = 1 - -resource servicePlan 'Microsoft.Web/serverfarms@2018-02-01' = { - name: name - location: location - properties: { - maximumElasticWorkerCount: maximumElasticWorkerCount - targetWorkerCount: maximumElasticWorkerCount - } - sku: { - name: sku - tier: 'Dynamic' - } -} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep index fafa70177..72182f5d4 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep @@ -20,7 +20,7 @@ param environmentAbbreviation string param tenantId string @description('Service plan name.') -param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'JobFinalizer'),0,8)}' +param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'NotifierJobFinalizer'),0,8)}' @description('Service plan sku') param servicePlanSku string = 'Y1' @@ -96,16 +96,6 @@ var jobFinalizerStorageAccountStaging = resourceId(subscription().subscriptionId var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') -module servicePlanTemplate 'servicePlan.bicep' = { - name: 'servicePlanTemplate-JobFinalizer' - params: { - name: servicePlanName - sku: servicePlanSku - location: location - maximumElasticWorkerCount: maximumElasticWorkerCount - } -} - var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 @@ -146,7 +136,7 @@ var appSettings = { var stagingSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}GroupMembershipObtainerStaging' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobFinalizerStaging' 'AzureWebJobs.StarterFunction.Disabled': 1 'AzureWebJobs.OrchestratorFunction.Disabled': 1 'AzureWebJobs.SubOrchestratorFunction.Disabled': 1 @@ -164,7 +154,7 @@ var stagingSettings = { 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled': 1 'AzureWebJobs.UsersReaderFunction.Disabled': 1 'AzureWebJobs.UsersSenderFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'GroupMembershipObtainerStaging' + AzureFunctionsWebHost__hostid: 'JobFinalizerStaging' } var productionSettings = { @@ -203,7 +193,7 @@ module existingLogAnalyticsWorkspace 'logAnalyticsWorkspace.bicep' = { } } -module functionAppTemplate_GroupMembershipObtainer 'functionApp.bicep' = { +module functionAppTemplate_JobFinalizer 'functionApp.bicep' = { name: 'functionAppTemplate-JobFinalizer' params: { name: '${functionAppName}-JobFinalizer' @@ -217,13 +207,12 @@ module functionAppTemplate_GroupMembershipObtainer 'functionApp.bicep' = { logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId } dependsOn: [ - servicePlanTemplate graphUAMI existingLogAnalyticsWorkspace ] } -module functionAppSlotTemplate_GroupMembershipObtainer 'functionAppSlot.bicep' = { +module functionAppSlotTemplate_JobFinalizer 'functionAppSlot.bicep' = { name: 'functionAppSlotTemplate-JobFinalizer' params: { name: '${functionAppName}-JobFinalizer/staging' @@ -237,7 +226,7 @@ module functionAppSlotTemplate_GroupMembershipObtainer 'functionAppSlot.bicep' = logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId } dependsOn: [ - functionAppTemplate_GroupMembershipObtainer + functionAppTemplate_JobFinalizer ] } @@ -250,12 +239,12 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultName: dataKeyVaultName dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions - productionSlotPrincipalId: functionAppTemplate_GroupMembershipObtainer.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_GroupMembershipObtainer.outputs.msi + productionSlotPrincipalId: functionAppTemplate_JobFinalizer.outputs.msi + stagingSlotPrincipalId: functionAppSlotTemplate_JobFinalizer.outputs.msi } dependsOn: [ - functionAppTemplate_GroupMembershipObtainer - functionAppSlotTemplate_GroupMembershipObtainer + functionAppTemplate_JobFinalizer + functionAppSlotTemplate_JobFinalizer ] } diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep index fb3a1c2a9..02d870b6f 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep @@ -26,7 +26,7 @@ param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmen param prereqsKeyVaultResourceGroup string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' @description('Service plan name.') -param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'Notifier'),0,8)}' +param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'NotifierJobFinalizer'),0,8)}' @description('App service name.') param appServiceName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-webapi' From 9eab272951c7edac590adec14a19f081e8bdbf5b Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 31 Jul 2024 10:23:47 -0700 Subject: [PATCH 0207/1479] Delete slot template --- .../compute/functionAppRBAC.bicep | 22 ----- .../compute/functionAppSlot.bicep | 86 ------------------- .../Infrastructure/compute/template.bicep | 70 --------------- 3 files changed, 178 deletions(-) delete mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppSlot.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppRBAC.bicep index f7819597b..12898c117 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppRBAC.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppRBAC.bicep @@ -16,9 +16,6 @@ param setRBACPermissions bool @description('The principalId of the function app for the production slot.') param productionSlotPrincipalId string -@description('The principalId of the function app for the staging slot.') -param stagingSlotPrincipalId string - param functionName string module functionAppPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { @@ -41,22 +38,3 @@ module functionAppDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { } } -module functionAppSlotPrereqsRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'prereqsKV-rbac-${functionName}Slot' - scope: resourceGroup(prereqsKeyVaultResourceGroup) - params: { - keyVaultName: prereqsKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} - -module functionAppSlotDataRBAC 'keyvaultRBAC.bicep' = if (setRBACPermissions) { - name: 'dataKV-rbac-${functionName}Slot' - scope: resourceGroup(dataKeyVaultResourceGroup) - params: { - keyVaultName: dataKeyVaultName - principalId: stagingSlotPrincipalId - roleName: 'Key Vault Secrets User' - } -} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppSlot.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppSlot.bicep deleted file mode 100644 index 424a2c56c..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppSlot.bicep +++ /dev/null @@ -1,86 +0,0 @@ -@description('Function app name.') -@minLength(1) -param name string - -@description('Function app kind.') -@allowed([ - 'functionapp' - 'linux' - 'container' -]) -param kind string = 'functionapp' - -@description('Function app location.') -param location string - -@description('Service plan name.') -@minLength(1) -param servicePlanName string - -@description('app settings') -param secretSettings object - -@description('User assigned managed identities. Single or list of user assigned managed identities. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') -param userManagedIdentities object = {} - -var deployUserManagedIdentity = userManagedIdentities != null && userManagedIdentities != {} - -@description('Log Analytics Workspace Id.') -param logAnalyticsWorkspaceId string - -resource functionAppSlot 'Microsoft.Web/sites/slots@2018-11-01' = { - name: name - kind: kind - location: location - properties: { - clientAffinityEnabled: true - enabled: true - httpsOnly: true - serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) - siteConfig: { - use32BitWorkerProcess : false - appSettings: secretSettings - ftpsState: 'Disabled' - } - } - identity: { - type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' - userAssignedIdentities: deployUserManagedIdentity ? userManagedIdentities : null - } -} - -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'functionAppSlot-diagnostics' - scope: functionAppSlot - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'FunctionAppLogs' - enabled: true - retentionPolicy: { - days: 0 - enabled: false - } - } - ] - } -} - -resource functionAppSlot_ftp 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'ftp' - properties: { - allow: false - } -} - -resource functionAppSlot_scm 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01' = { - parent: functionAppSlot - name: 'scm' - properties: { - allow: false - } -} - -output msi string = functionAppSlot.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep index 72182f5d4..90a8fb5bb 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep @@ -22,9 +22,6 @@ param tenantId string @description('Service plan name.') param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'NotifierJobFinalizer'),0,8)}' -@description('Service plan sku') -param servicePlanSku string = 'Y1' - @description('Resource location.') param location string @@ -39,21 +36,6 @@ param functionAppName string = '${solutionAbbreviation}-${resourceGroupClassific ]) param functionAppKind string = 'functionapp' -@description('Maximum elastic worker count.') -param maximumElasticWorkerCount int = 1 - -@description('Enter application insights name.') -param appInsightsName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Resource group where Application Insights is located.') -param appInsightsResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - -@description('Enter storage account name.') -param storageAccountName string - -@description('Resource group where storage account is located.') -param storageAccountResourceGroup string = '${solutionAbbreviation}-data-${environmentAbbreviation}' - @description('Name of the resource group where the \'prereqs\' key vault is located.') param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' @@ -92,7 +74,6 @@ var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKe var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') var jobFinalizerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobFinalizerStorageAccountProd') -var jobFinalizerStorageAccountStaging = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobFinalizerStorageAccountStaging') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') @@ -134,29 +115,6 @@ var appSettings = { serviceBusNotificationsQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusNotificationsQueue, '2019-09-01').secretUriWithVersion})' } -var stagingSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountStaging, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobFinalizerStaging' - 'AzureWebJobs.StarterFunction.Disabled': 1 - 'AzureWebJobs.OrchestratorFunction.Disabled': 1 - 'AzureWebJobs.SubOrchestratorFunction.Disabled': 1 - 'AzureWebJobs.DeltaUsersReaderFunction.Disabled': 1 - 'AzureWebJobs.DeltaUsersSenderFunction.Disabled': 1 - 'AzureWebJobs.EmailSenderFunction.Disabled': 1 - 'AzureWebJobs.FileDownloaderFunction.Disabled': 1 - 'AzureWebJobs.GroupsReaderFunction.Disabled': 1 - 'AzureWebJobs.GroupValidatorFunction.Disabled': 1 - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled': 1 - 'AzureWebJobs.MembersReaderFunction.Disabled': 1 - 'AzureWebJobs.SourceGroupsReaderFunction.Disabled': 1 - 'AzureWebJobs.SubsequentDeltaUsersReaderFunction.Disabled': 1 - 'AzureWebJobs.SubsequentMembersReaderFunction.Disabled': 1 - 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled': 1 - 'AzureWebJobs.UsersReaderFunction.Disabled': 1 - 'AzureWebJobs.UsersSenderFunction.Disabled': 1 - AzureFunctionsWebHost__hostid: 'JobFinalizerStaging' -} - var productionSettings = { AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountProd, '2019-09-01').secretUriWithVersion})' AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobFinalizer' @@ -212,23 +170,6 @@ module functionAppTemplate_JobFinalizer 'functionApp.bicep' = { ] } -module functionAppSlotTemplate_JobFinalizer 'functionAppSlot.bicep' = { - name: 'functionAppSlotTemplate-JobFinalizer' - params: { - name: '${functionAppName}-JobFinalizer/staging' - kind: functionAppKind - location: location - servicePlanName: servicePlanName - secretSettings: commonSettings - userManagedIdentities:{ - '${graphUAMI.id}' : {} - } - logAnalyticsWorkspaceId: existingLogAnalyticsWorkspace.outputs.workspaceId - } - dependsOn: [ - functionAppTemplate_JobFinalizer - ] -} module functionAppRBAC 'functionAppRBAC.bicep' = { name: 'functionAppsRBAC-JobFinalizer' @@ -240,11 +181,9 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions productionSlotPrincipalId: functionAppTemplate_JobFinalizer.outputs.msi - stagingSlotPrincipalId: functionAppSlotTemplate_JobFinalizer.outputs.msi } dependsOn: [ functionAppTemplate_JobFinalizer - functionAppSlotTemplate_JobFinalizer ] } @@ -257,12 +196,3 @@ resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { ] } -resource functionAppStagingSettings 'Microsoft.Web/sites/slots/config@2022-09-01' = { - name: '${functionAppName}-JobFinalizer/staging/appsettings' - kind: 'string' - properties: union(commonSettings, appSettings, stagingSettings) - dependsOn: [ - functionAppRBAC - functionAppSettings - ] -} From 79b433ec768433643756693d1117503935763da0 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 31 Jul 2024 11:32:41 -0700 Subject: [PATCH 0208/1479] Adjust parameters --- .../Infrastructure/compute/template.bicep | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep index 90a8fb5bb..a0ca71ca1 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep @@ -16,6 +16,9 @@ param resourceGroupClassification string = 'compute' @maxLength(6) param environmentAbbreviation string +@description('Enter storage account name.') +param storageAccountName string + @description('Tenant id.') param tenantId string @@ -58,7 +61,6 @@ var logAnalyticsCustomerId = resourceId(subscription().subscriptionId, dataKeyVa var logAnalyticsPrimarySharedKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'logAnalyticsPrimarySharedKey') var serviceBusFQN = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusFQN') var serviceBusSyncJobTopic = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusSyncJobTopic') -var serviceBusMembershipAggregatorQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusMembershipAggregatorQueue') var graphAppClientId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppClientId') var graphAppClientSecret = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppClientSecret') var graphAppCertificateName = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppCertificateName') @@ -67,8 +69,7 @@ var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultRe var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') -var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') -var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') +var jobFinalizerStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierProviderId') var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') @@ -90,7 +91,6 @@ var appSettings = { APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountProd, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTSHARE: toLower('functionApp-JobFinalizer') - serviceBusMembershipAggregatorQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusMembershipAggregatorQueue, '2019-09-01').secretUriWithVersion})' serviceBusSyncJobTopic: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusSyncJobTopic, '2019-09-01').secretUriWithVersion})' gmmServiceBus__fullyQualifiedNamespace: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusFQN, '2019-09-01').secretUriWithVersion})' 'graphCredentials:ClientCertificateName': '@Microsoft.KeyVault(SecretUri=${reference(graphAppCertificateName, '2019-09-01').secretUriWithVersion})' @@ -107,8 +107,7 @@ var appSettings = { senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' - membershipStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(membershipStorageAccountName, '2019-09-01').secretUriWithVersion})' - membershipContainerName: '@Microsoft.KeyVault(SecretUri=${reference(membershipContainerName, '2019-09-01').secretUriWithVersion})' + jobFinalizerStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountName, '2019-09-01').secretUriWithVersion})' appConfigurationEndpoint: appConfigurationEndpoint actionableEmailProviderId: '@Microsoft.KeyVault(SecretUri=${reference(actionableEmailProviderId, '2019-09-01').secretUriWithVersion})' 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' From c10f4765e139671319844f98c2cce122405a97b0 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 5 Aug 2024 11:59:33 -0700 Subject: [PATCH 0209/1479] Change starter function --- .../Hosts/JobFinalizer/Function/Starter/StarterFunction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs index fbc2e7263..2dd9e211d 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs @@ -25,7 +25,7 @@ public StarterFunction(ILoggingRepository loggingRepository) [FunctionName(nameof(StarterFunction))] public async Task RunAsync( - [ServiceBusTrigger("%serviceBusSyncJobTopic%", "SyncJobStatus", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, + [ServiceBusTrigger("%serviceBusJobFinalizerQueue", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, [DurableClient] IDurableOrchestrationClient starter) { var syncJob = JsonSerializer.Deserialize(Encoding.UTF8.GetString(message.Body)); From b1778d633c21917fd84b445fb9e9f2a60e0e684b Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 5 Aug 2024 13:30:33 -0700 Subject: [PATCH 0210/1479] Correct a typo --- .../JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs | 2 +- .../JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs | 2 +- .../Hosts/JobFinalizer/Function/Starter/StarterFunction.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs index 112584bbc..8ffeb61c0 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs @@ -17,7 +17,7 @@ using System.Threading.Tasks; using Repositories.Contracts.InjectConfig; using Models.Notifications; -using Hosts.JobFInalizer; +using Hosts.JobFinalizer; namespace Hosts.JobFinalizer { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs index fa775357f..d6e194043 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs @@ -3,7 +3,7 @@ using Models; -namespace Hosts.JobFInalizer +namespace Hosts.JobFinalizer { public class OrchestratorRequest { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs index 2dd9e211d..2bb46285a 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Azure.Messaging.ServiceBus; -using Hosts.JobFInalizer; +using Hosts.JobFinalizer; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; From 18fd807b285198ef988cb37c64eda93f90b2926c Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 7 Aug 2024 10:58:39 -0700 Subject: [PATCH 0211/1479] Change to net 8.0 --- .../JobFinalizer/Function/JobFinalizer.csproj | 10 ++++---- .../JobFinalizer/Function/JobFinalizer.sln | 16 ++++++++++++ .../Infrastructure/compute/template.bicep | 1 + .../Infrastructure/data/template.bicep | 12 ++++----- .../Services.Contracts.csproj | 2 +- .../Services/JobFinalizerService.csproj | 4 +-- .../Services/JobFinalizerService.sln | 25 ------------------- 7 files changed, 31 insertions(+), 39 deletions(-) delete mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.sln diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj index bf59b61b7..13963d3d1 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj @@ -1,15 +1,15 @@  - net6.0 + net8.0 v4 - + - - - + + + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln index 4138328a3..146dd4ea8 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln @@ -45,6 +45,22 @@ Global {F7C85A82-C663-4E0A-9016-693DE824A0E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7C85A82-C663-4E0A-9016-693DE824A0E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7C85A82-C663-4E0A-9016-693DE824A0E7}.Release|Any CPU.Build.0 = Release|Any CPU + {962EC314-B08E-45F4-A6D3-735F3649FF49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {962EC314-B08E-45F4-A6D3-735F3649FF49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {962EC314-B08E-45F4-A6D3-735F3649FF49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {962EC314-B08E-45F4-A6D3-735F3649FF49}.Release|Any CPU.Build.0 = Release|Any CPU + {0BCD28A7-F6FE-4EED-8DEB-49B3D5E1CC1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0BCD28A7-F6FE-4EED-8DEB-49B3D5E1CC1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0BCD28A7-F6FE-4EED-8DEB-49B3D5E1CC1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0BCD28A7-F6FE-4EED-8DEB-49B3D5E1CC1A}.Release|Any CPU.Build.0 = Release|Any CPU + {5902F33F-B6ED-4255-BDED-8BDB6807E711}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5902F33F-B6ED-4255-BDED-8BDB6807E711}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5902F33F-B6ED-4255-BDED-8BDB6807E711}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5902F33F-B6ED-4255-BDED-8BDB6807E711}.Release|Any CPU.Build.0 = Release|Any CPU + {45769BA7-C56B-49DE-AF9D-773790AB1E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45769BA7-C56B-49DE-AF9D-773790AB1E56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45769BA7-C56B-49DE-AF9D-773790AB1E56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45769BA7-C56B-49DE-AF9D-773790AB1E56}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep index a0ca71ca1..51cd8ff6c 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep @@ -84,6 +84,7 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet' FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep index 010696f83..2d7dfbc76 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep @@ -20,11 +20,11 @@ param storageAccountSku string = 'Standard_LRS' param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' -var prodStorageAccountName = substring('gmo${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('gmo${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) +var prodStorageAccountName = substring('jf${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) +var stagingStorageAccountName = substring('jf${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) -module gmoStorageAccountProd 'storageAccount.bicep' = { - name: 'gmoProdstorageAccountTemplate' +module jfStorageAccountProd 'storageAccount.bicep' = { + name: 'jfProdstorageAccountTemplate' params: { name: prodStorageAccountName sku: storageAccountSku @@ -34,8 +34,8 @@ module gmoStorageAccountProd 'storageAccount.bicep' = { } } -module gmoStorageAccountStaging 'storageAccount.bicep' = { - name: 'gmoStagingstorageAccountTemplate' +module jfStorageAccountStaging 'storageAccount.bicep' = { + name: 'jfStagingstorageAccountTemplate' params: { name: stagingStorageAccountName sku: storageAccountSku diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/Services.Contracts.csproj index 1b3fbe831..1b9c7c652 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/Services.Contracts.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/Services.Contracts.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.csproj index 7fcd9bcb5..48aa835c6 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.sln b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.sln deleted file mode 100644 index d262312dd..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.10.35027.167 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobFinalizerService", "JobFinalizerService.csproj", "{AC7A4643-5D81-494C-AC70-893FAC822B1F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AC7A4643-5D81-494C-AC70-893FAC822B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AC7A4643-5D81-494C-AC70-893FAC822B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AC7A4643-5D81-494C-AC70-893FAC822B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AC7A4643-5D81-494C-AC70-893FAC822B1F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {D9018227-C4CC-4159-99DE-055034687C1E} - EndGlobalSection -EndGlobal From 4e290fad7ae5e318029369dd13f03115fa60421a Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 7 Aug 2024 15:08:58 -0700 Subject: [PATCH 0212/1479] Change the template --- .../Infrastructure/compute/functionApp.bicep | 29 ------------------- .../Infrastructure/compute/template.bicep | 17 +++++++---- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionApp.bicep index 9162c014a..11625bfb4 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionApp.bicep @@ -82,33 +82,4 @@ resource snFtpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@ } } -resource functionAppSlotConfig 'Microsoft.Web/sites/config@2021-03-01' = { - name: 'slotConfigNames' - parent: functionApp - properties: { - appSettingNames: [ - 'AzureFunctionsJobHost__extensions__durableTask__hubName' - 'AzureWebJobs.StarterFunction.Disabled' - 'AzureWebJobs.OrchestratorFunction.Disabled' - 'AzureWebJobs.SubOrchestratorFunction.Disabled' - 'AzureWebJobs.DeltaUsersReaderFunction.Disabled' - 'AzureWebJobs.DeltaUsersSenderFunction.Disabled' - 'AzureWebJobs.EmailSenderFunction.Disabled' - 'AzureWebJobs.FileDownloaderFunction.Disabled' - 'AzureWebJobs.GroupsReaderFunction.Disabled' - 'AzureWebJobs.GroupValidatorFunction.Disabled' - 'AzureWebJobs.JobStatusUpdaterFunction.Disabled' - 'AzureWebJobs.MembersReaderFunction.Disabled' - 'AzureWebJobs.SourceGroupsReaderFunction.Disabled' - 'AzureWebJobs.SubsequentDeltaUsersReaderFunction.Disabled' - 'AzureWebJobs.SubsequentMembersReaderFunction.Disabled' - 'AzureWebJobs.SubsequentUsersReaderFunction.Disabled' - 'AzureWebJobs.UsersReaderFunction.Disabled' - 'AzureWebJobs.UsersSenderFunction.Disabled' - 'AzureWebJobsStorage' - 'AzureFunctionsWebHost__hostid' - ] - } -} - output msi string = functionApp.identity.principalId diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep index 51cd8ff6c..900d37ea2 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep @@ -88,6 +88,9 @@ var commonSettings = { } var appSettings = { + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobFinalizer' + AzureFunctionsWebHost__hostid: 'JobFinalizer' 'AzureFunctionsJobHost:extensions:durableTask:extendedSessionsEnabled': toLower(environmentAbbreviation) == 'prodv2' ? 'True' : 'False' APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountProd, '2019-09-01').secretUriWithVersion})' @@ -115,12 +118,16 @@ var appSettings = { serviceBusNotificationsQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusNotificationsQueue, '2019-09-01').secretUriWithVersion})' } -var productionSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobFinalizer' - AzureFunctionsWebHost__hostid: 'JobFinalizer' +var activityFunctionSettings = { + 'AzureWebJobs.StarterFunction.Disabled': 0 + 'AzureWebJobs.OrchestratorFunction.Disabled': 0 + 'AzureWebJobs.AttributeCacheUpdaterFunction.Disabled': 0 + 'AzureWebJobs.AttributeReaderFunction.Disabled': 0 + 'AzureWebJobs.DestinationReaderFunction.Disabled': 0 + 'AzureWebJobs.LoggerFunction.Disabled': 0 } + resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { name: dataKeyVaultName scope: resourceGroup(dataKeyVaultResourceGroup) @@ -190,7 +197,7 @@ module functionAppRBAC 'functionAppRBAC.bicep' = { resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { name: '${functionAppName}-JobFinalizer/appsettings' kind: 'string' - properties: union(commonSettings, appSettings, productionSettings) + properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ functionAppRBAC ] From ba601edf30081a708befd369bebd0d243d6bbad3 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 11:53:07 -0700 Subject: [PATCH 0213/1479] Add log acitivities --- .../Activity/Logger/LoggerFunction.cs | 28 +++++++++++++++++++ .../Function/Activity/Logger/LoggerRequest.cs | 16 +++++++++++ .../Orchestrator/OrchestratorFunction.cs | 14 +++++++--- .../Function/Starter/StarterFunction.cs | 2 +- 4 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerFunction.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerRequest.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerFunction.cs new file mode 100644 index 000000000..84e4f5136 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerFunction.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Repositories.Contracts; +using System; +using System.Threading.Tasks; + +namespace Hosts.JobFinalizer +{ + public class LoggerFunction + { + private readonly ILoggingRepository _loggingRepository; + + public LoggerFunction(ILoggingRepository loggingRepository) + { + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + } + + [FunctionName(nameof(LoggerFunction))] + public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.RunId },request.Verbosity); + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerRequest.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerRequest.cs new file mode 100644 index 000000000..8644ee86f --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerRequest.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Repositories.Contracts; +using System; + +namespace Hosts.JobFinalizer +{ + public class LoggerRequest + { + public string Message { get; set; } + public Guid RunId { get; set; } + public VerbosityLevel Verbosity { get; set; } = VerbosityLevel.INFO; + + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs index 8ffeb61c0..5e0bad834 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs @@ -43,6 +43,13 @@ public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrat var runId = syncJob.RunId.GetValueOrDefault(Guid.Empty); var syncjobStatus = mainRequest.Status; SyncStatus status; + await context.CallActivityAsync(nameof(LoggerFunction), + new LoggerRequest + { + RunId = runId, + Message = $"{nameof(OrchestratorFunction)} function started at: {context.CurrentUtcDateTime}", + Verbosity = VerbosityLevel.DEBUG + }); if (!string.Equals(syncjobStatus, "unknown", StringComparison.OrdinalIgnoreCase)) { @@ -52,16 +59,15 @@ public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrat else { await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { SyncJob = syncJob, Status = SyncStatus.Error }); - if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { RunId = runId, Message = $"{syncJob.TargetOfficeGroupId} pass an unknown status. Marking job as {SyncStatus.Error}."}, VerbosityLevel.DEBUG); + await context.CallActivityAsync(nameof(LoggerFunction), new LoggerRequest { RunId = runId, Message = $"{syncJob.TargetOfficeGroupId} pass an unknown status. Marking job as {SyncStatus.Error}.", Verbosity = VerbosityLevel.DEBUG }); return; } - if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { Message = $"{nameof(OrchestratorFunction)} function started", RunId = runId }, VerbosityLevel.DEBUG); await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { SyncJob = syncJob, Status = status }); - if (!context.IsReplaying) - _ = _log.LogMessageAsync(new LogMessage { Message = $"{nameof(OrchestratorFunction)} function completed", RunId = runId, DynamicProperties = syncJob.ToDictionary() }, VerbosityLevel.DEBUG); + + await context.CallActivityAsync(nameof(LoggerFunction), new LoggerRequest { RunId = runId, Message = $"{nameof(OrchestratorFunction)} function completed", Verbosity = VerbosityLevel.DEBUG }); } } } diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs index 2bb46285a..3e77beade 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs @@ -25,7 +25,7 @@ public StarterFunction(ILoggingRepository loggingRepository) [FunctionName(nameof(StarterFunction))] public async Task RunAsync( - [ServiceBusTrigger("%serviceBusJobFinalizerQueue", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, + [ServiceBusTrigger("%serviceBusJobFinalizerQueue%", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, [DurableClient] IDurableOrchestrationClient starter) { var syncJob = JsonSerializer.Deserialize(Encoding.UTF8.GetString(message.Body)); From eb2064390c095ea3e65446347c500134b1c41063 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 11:54:22 -0700 Subject: [PATCH 0214/1479] Code cleaning --- .../Function/Orchestrator/OrchestratorFunction.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs index 5e0bad834..fd4a5f3ce 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs @@ -50,7 +50,6 @@ await context.CallActivityAsync(nameof(LoggerFunction), Message = $"{nameof(OrchestratorFunction)} function started at: {context.CurrentUtcDateTime}", Verbosity = VerbosityLevel.DEBUG }); - if (!string.Equals(syncjobStatus, "unknown", StringComparison.OrdinalIgnoreCase)) { status = (SyncStatus)Enum.Parse(typeof(SyncStatus), syncjobStatus, true); @@ -62,11 +61,7 @@ await context.CallActivityAsync(nameof(LoggerFunction), await context.CallActivityAsync(nameof(LoggerFunction), new LoggerRequest { RunId = runId, Message = $"{syncJob.TargetOfficeGroupId} pass an unknown status. Marking job as {SyncStatus.Error}.", Verbosity = VerbosityLevel.DEBUG }); return; } - - await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { SyncJob = syncJob, Status = status }); - - await context.CallActivityAsync(nameof(LoggerFunction), new LoggerRequest { RunId = runId, Message = $"{nameof(OrchestratorFunction)} function completed", Verbosity = VerbosityLevel.DEBUG }); } } From 0a4e4783f086c734808588fbf4f583a4634c9e2b Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 12:53:36 -0700 Subject: [PATCH 0215/1479] Change logging --- .../Function/Orchestrator/OrchestratorFunction.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs index fd4a5f3ce..0b64b55b7 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs @@ -23,14 +23,8 @@ namespace Hosts.JobFinalizer { public class OrchestratorFunction { - private readonly ILoggingRepository _log; - - public OrchestratorFunction( - ILoggingRepository loggingRepository - ) + public OrchestratorFunction() { - _log = loggingRepository; - } [FunctionName(nameof(OrchestratorFunction))] From ad033aa6c67fa8c3ceef03422c995a2b4b4b42b1 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 22 Aug 2024 11:22:02 -0700 Subject: [PATCH 0216/1479] Remove staging account --- .../JobFinalizer/Infrastructure/data/template.bicep | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep index 2d7dfbc76..126ad1366 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep @@ -21,7 +21,6 @@ param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' var prodStorageAccountName = substring('jf${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -var stagingStorageAccountName = substring('jf${solutionAbbreviation}${environmentAbbreviation}staging${uniqueString(resourceGroup().id)}',0,23) module jfStorageAccountProd 'storageAccount.bicep' = { name: 'jfProdstorageAccountTemplate' @@ -34,14 +33,4 @@ module jfStorageAccountProd 'storageAccount.bicep' = { } } -module jfStorageAccountStaging 'storageAccount.bicep' = { - name: 'jfStagingstorageAccountTemplate' - params: { - name: stagingStorageAccountName - sku: storageAccountSku - keyVaultName: keyVaultName - location: location - storageAccountConnectionStringSettingName: 'jobFinalizerStorageAccountStaging' - } -} From 336fdc4cc7a425b9f5f9cf2c9696fd29dee2b0e2 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 5 Aug 2024 13:29:03 -0700 Subject: [PATCH 0217/1479] Add test cases --- Infrastructure/data/template.bicep | 19 +++++ .../JobFinalizer/Function/JobFinalizer.sln | 2 + .../Function/Starter/StarterFunction.cs | 1 - .../Hosts/JobFinalizer/Function/Startup.cs | 9 +++ .../JobFinalizer/Function/local.settings.json | 30 +++++++- .../Infrastructure/compute/template.bicep | 2 + .../JobFinalizerServiceTests.cs | 39 +++++++++++ .../Mocks/MockLoggingRepository.cs | 42 +++++++++++ .../OrchestratorTests.cs | 70 +++++++++++++++++++ .../SampleDataHelper.cs | 48 +++++++++++++ .../Services.JobFinalizer.Tests.csproj | 31 ++++++++ 11 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/JobFinalizerServiceTests.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Mocks/MockLoggingRepository.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/OrchestratorTests.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/SampleDataHelper.cs create mode 100644 Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj diff --git a/Infrastructure/data/template.bicep b/Infrastructure/data/template.bicep index 5d1ab10bf..157b3286c 100644 --- a/Infrastructure/data/template.bicep +++ b/Infrastructure/data/template.bicep @@ -104,6 +104,9 @@ param serviceBusNotificationsQueue string = 'notifications' @description('Enter notifications service bus queue name') param serviceBusFailedNotificationsQueue string = 'failedNotifications' +@description('Enter job finalizer service bus queue name') +param serviceBusJobFinalizerQueue string = 'jobFinalizer' + @description('Enter storage account name.') @minLength(1) @maxLength(24) @@ -545,6 +548,18 @@ module failedNotificationsQueue 'serviceBusQueue.bicep' = { ] } +module jobFinalizerQueue 'serviceBusQueue.bicep' = { + name: 'jobFinalizerQueue' + params: { + queueName: serviceBusJobFinalizerQueue + serviceBusName: serviceBusName + requiresSession: false + maxDeliveryCount: 5 + } + dependsOn:[ + serviceBusTemplate + ] +} module storageAccountTemplate 'storageAccount.bicep' = { name: 'storageAccountTemplate' params: { @@ -686,6 +701,10 @@ module secretsTemplate 'keyVaultSecrets.bicep' = { name: 'serviceBusFailedNotificationsQueue' value: serviceBusFailedNotificationsQueue } + { + name: 'serviceBusJobFinalizerQueue' + value: serviceBusJobFinalizerQueue + } { name: 'graphUserAssignedManagedIdentityName' value: graphUserAssignedManagedIdentityName diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln index 146dd4ea8..97485bbb0 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Contracts", ". EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\Models.csproj", "{F7C85A82-C663-4E0A-9016-693DE824A0E7}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.JobFinalizer.Tests", "..\Services.JobFinalizer.Tests\Services.JobFinalizer.Tests.csproj", "{5B1C1870-46C3-41D0-9D00-D08CB8EDF57E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs index 3e77beade..835ed3430 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Azure.Messaging.ServiceBus; -using Hosts.JobFinalizer; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs index b99a2defc..158855e14 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Repositories.Contracts; +using Repositories.ServiceBusQueue; // see https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection [assembly: FunctionsStartup(typeof(Hosts.JobFinalizer.Startup))] @@ -30,6 +31,14 @@ public override void Configure(IFunctionsHostBuilder builder) services.GetRequiredService() ); }); + builder.Services.AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var JobFinalizerQueue = configuration["serviceBusJobFinalizerQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(JobFinalizerQueue); + return new ServiceBusQueueRepository(sender); + }); } } } diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/local.settings.json index 7b93cb670..297c1d2a0 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/local.settings.json @@ -2,6 +2,34 @@ "IsEncrypted": false, "Values": { "APPINSIGHTS_INSTRUMENTATIONKEY": "", - "AzureWebJobsStorage": "UseDevelopmentStorage=true" + "AzureWebJobsStorage": "", + "graphCredentials:UserAssignedManagedIdentityClientId": "", + "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "", + "gmmServiceBus__fullyQualifiedNamespace": "", + "actionableEmailProviderId": "", + "supportEmailAddresses": "", + "AzureFunctionsWebHost__hostid": "", + "graphCredentials:TenantId": "", + "jobFinalizerStorageAccountName": "", + "senderAddress": "", + "WEBSITE_CONTENTSHARE": "", + "AzureWebJobsStorage": "", + "graphCredentials:KeyVaultName": "", + "AzureFunctionsJobHost__extensions__durableTask__hubName": "", + "logAnalyticsPrimarySharedKey": "", + "syncDisabledCCEmailAddresses": "", + "ConnectionStrings:JobsContext": "", + "graphCredentials:ClientCertificateName": "", + "logAnalyticsCustomerId": "", + "serviceBusSyncJobTopic": "", + "appConfigurationEndpoint": "", + "graphCredentials:ClientId": "", + "graphCredentials:ClientSecret": "", + "graphCredentials:KeyVaultTenantId": "", + "ConnectionStrings:JobsContextReadOnly": "", + "senderPassword": "", + "serviceBusNotificationsQueue": "", + "serviceBusJobFinalizerQueue": "", + "APPINSIGHTS_INSTRUMENTATIONKEY": "" } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep index 900d37ea2..4eb015a4b 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep @@ -77,6 +77,7 @@ var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, d var jobFinalizerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobFinalizerStorageAccountProd') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') +var serviceBusJobFinalizerQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusJobFinalizerQueue') var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 @@ -116,6 +117,7 @@ var appSettings = { actionableEmailProviderId: '@Microsoft.KeyVault(SecretUri=${reference(actionableEmailProviderId, '2019-09-01').secretUriWithVersion})' 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' serviceBusNotificationsQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusNotificationsQueue, '2019-09-01').secretUriWithVersion})' + serviceBusJobFinalizerQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusJobFinalizerQueue, '2019-09-01').secretUriWithVersion})' } var activityFunctionSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/JobFinalizerServiceTests.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/JobFinalizerServiceTests.cs new file mode 100644 index 000000000..9f40256bb --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/JobFinalizerServiceTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using System; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Models; +using Repositories.Contracts; +using Hosts.JobFinalizer; +using Services.Tests; +using Services.Notifier.Tests.Mocks; + +namespace Services.Tests +{ + [TestClass] + public class JobFinalizerServiceTests + { + private Mock _mockDatabaseSyncJobsRepository; + private MockLoggingRepository _mockLoggingRepository; + private JobFinalizerService _jobFinalizerService; + + [TestInitialize] + public void Setup() + { + _mockDatabaseSyncJobsRepository = new Mock(); + _mockLoggingRepository = new MockLoggingRepository(); + _jobFinalizerService = new JobFinalizerService(_mockDatabaseSyncJobsRepository.Object, _mockLoggingRepository); + } + + [TestMethod] + public async Task UpdateSyncJobStatus() + { + var syncJob = new SyncJob { Id = Guid.NewGuid() }; + var status = SyncStatus.Idle; + await _jobFinalizerService.UpdateSyncJobStatusAsync(syncJob, status); + _mockDatabaseSyncJobsRepository.Verify(repo => repo.UpdateSyncJobStatusAsync(It.Is(jobs => jobs.Length == 1 && jobs[0] == syncJob), status), Times.Once); + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Mocks/MockLoggingRepository.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Mocks/MockLoggingRepository.cs new file mode 100644 index 000000000..5c81ec25c --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Mocks/MockLoggingRepository.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Models; +using Repositories.Contracts; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Services.Notifier.Tests.Mocks +{ + public class MockLoggingRepository : ILoggingRepository + { + public List MessagesLogged { get; set; } = new List(); + public int MessagesLoggedCount => MessagesLogged.Count; + public Dictionary SyncJobProperties { get; private set; } = new Dictionary(); + public bool DryRun { get; set; } + + public Task LogMessageAsync(LogMessage logMessage, VerbosityLevel verbosityLevel = VerbosityLevel.INFO, [CallerMemberName] string caller = "", [CallerFilePath] string file = "") + { + MessagesLogged.Add(logMessage); + return Task.CompletedTask; + } + + public Task LogPIIMessageAsync(LogMessage logMessage, [CallerMemberName] string caller = "", [CallerFilePath] string file = "") + { + MessagesLogged.Add(logMessage); + return Task.CompletedTask; + } + + public void RemoveSyncJobProperties(Guid key) + { + throw new NotImplementedException(); + } + + public void SetSyncJobProperties(Guid key, Dictionary properties) + { + throw new NotImplementedException(); + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/OrchestratorTests.cs new file mode 100644 index 000000000..f5cba924b --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/OrchestratorTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Models; +using Repositories.Contracts; +using Hosts.JobFinalizer; +using Services.Notifier.Tests.Mocks; + +namespace Services.Tests +{ + [TestClass] + public class OrchestratorFunctionTests + { + private Mock _mockContext; + private MockLoggingRepository _mockLogger; + private OrchestratorFunction _orchestratorFunction; + private const string GroupMembership = "GroupMembership"; + + [TestInitialize] + public void Setup() + { + _mockContext = new Mock(); + _mockLogger = new MockLoggingRepository(); + _orchestratorFunction = new OrchestratorFunction(_mockLogger); + } + + [TestMethod] + public async Task RunOrchestratorTest() + { + var syncJob = SampleDataHelper.CreateSampleSyncJobs(1, GroupMembership).First(); + var orchestratorRequest = new OrchestratorRequest + { + SyncJob = syncJob, + Status = SyncStatus.Idle.ToString() + }; + + _mockContext.Setup(c => c.GetInput()).Returns(orchestratorRequest); + + await _orchestratorFunction.RunOrchestratorAsync(_mockContext.Object, null); + _mockContext.Verify(context => context.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny()), Times.Once); + Assert.AreEqual(2, _mockLogger.MessagesLoggedCount, "Expected two log messages for valid sync job."); + } + + [TestMethod] + public async Task RunOrchestratorTestWithUnknowStatus() + { + var syncJob = SampleDataHelper.CreateSampleSyncJobs(1, GroupMembership).First(); + var orchestratorRequest = new OrchestratorRequest + { + SyncJob = syncJob, + Status = "Unknown" + }; + + _mockContext.Setup(c => c.GetInput()).Returns(orchestratorRequest); + + await _orchestratorFunction.RunOrchestratorAsync(_mockContext.Object, null); + + Assert.IsTrue(_mockLogger.MessagesLogged.Any(log => log.Message.Contains("unknown status")), "Expected an error log message for unknown status."); + _mockContext.Verify(context => context.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny()), Times.Once); + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/SampleDataHelper.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/SampleDataHelper.cs new file mode 100644 index 000000000..6276545a3 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/SampleDataHelper.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Models; +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Linq; + +namespace Services.Tests +{ + static class SampleDataHelper + { + public static List CreateSampleSyncJobs(int numberOfJobs, string syncType, int period = 1, DateTime? startDateBase = null, DateTime? lastRunTime = null) + { + var jobs = new List(); + + for (int i = 0; i < numberOfJobs; i++) + { + var job = new SyncJob + { + Requestor = $"requestor_{i}@email.com", + Id = Guid.NewGuid(), + PartitionKey = DateTime.UtcNow.ToString("MMddyyyy"), + RowKey = Guid.NewGuid().ToString(), + Period = period, + Query = GetJobQuery(syncType, new[] { Guid.NewGuid().ToString() }), + StartDate = startDateBase ?? DateTime.UtcNow.AddDays(-1), + Status = SyncStatus.Idle.ToString(), + TargetOfficeGroupId = Guid.NewGuid(), + LastRunTime = lastRunTime ?? SqlDateTime.MinValue.Value, + RunId = Guid.NewGuid(), + Destination = $"[{{\"type\":\"GroupMembership\",\"value\":{{\"objectId\":\"{Guid.NewGuid()}\"}}}}]" + }; + + jobs.Add(job); + } + + return jobs; + } + + public static string GetJobQuery(string syncType, string[] groupIds) + { + var individualQueries = groupIds.Select(x => $"{{\"type\":\"{syncType}\",\"source\": \"{x}\"}}"); + var query = $"[{string.Join(",", individualQueries)}]"; + return query; + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj new file mode 100644 index 000000000..206b2b157 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + From 2054742ec6cc8756f53dc4a6e3fd3d8039adabbc Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 7 Aug 2024 11:36:40 -0700 Subject: [PATCH 0218/1479] Change to net 8.0 --- .../JobFinalizer/Function/JobFinalizer.sln | 22 +++++-------------- .../Services.JobFinalizer.Tests.csproj | 14 ++++++------ vsts-cicd.yml | 4 ++-- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln index 97485bbb0..29707f450 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln @@ -15,7 +15,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Contracts", ". EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\Models.csproj", "{F7C85A82-C663-4E0A-9016-693DE824A0E7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.JobFinalizer.Tests", "..\Services.JobFinalizer.Tests\Services.JobFinalizer.Tests.csproj", "{5B1C1870-46C3-41D0-9D00-D08CB8EDF57E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.JobFinalizer.Tests", "..\Services.JobFinalizer.Tests\Services.JobFinalizer.Tests.csproj", "{DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -47,22 +47,10 @@ Global {F7C85A82-C663-4E0A-9016-693DE824A0E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7C85A82-C663-4E0A-9016-693DE824A0E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7C85A82-C663-4E0A-9016-693DE824A0E7}.Release|Any CPU.Build.0 = Release|Any CPU - {962EC314-B08E-45F4-A6D3-735F3649FF49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {962EC314-B08E-45F4-A6D3-735F3649FF49}.Debug|Any CPU.Build.0 = Debug|Any CPU - {962EC314-B08E-45F4-A6D3-735F3649FF49}.Release|Any CPU.ActiveCfg = Release|Any CPU - {962EC314-B08E-45F4-A6D3-735F3649FF49}.Release|Any CPU.Build.0 = Release|Any CPU - {0BCD28A7-F6FE-4EED-8DEB-49B3D5E1CC1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0BCD28A7-F6FE-4EED-8DEB-49B3D5E1CC1A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0BCD28A7-F6FE-4EED-8DEB-49B3D5E1CC1A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0BCD28A7-F6FE-4EED-8DEB-49B3D5E1CC1A}.Release|Any CPU.Build.0 = Release|Any CPU - {5902F33F-B6ED-4255-BDED-8BDB6807E711}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5902F33F-B6ED-4255-BDED-8BDB6807E711}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5902F33F-B6ED-4255-BDED-8BDB6807E711}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5902F33F-B6ED-4255-BDED-8BDB6807E711}.Release|Any CPU.Build.0 = Release|Any CPU - {45769BA7-C56B-49DE-AF9D-773790AB1E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {45769BA7-C56B-49DE-AF9D-773790AB1E56}.Debug|Any CPU.Build.0 = Debug|Any CPU - {45769BA7-C56B-49DE-AF9D-773790AB1E56}.Release|Any CPU.ActiveCfg = Release|Any CPU - {45769BA7-C56B-49DE-AF9D-773790AB1E56}.Release|Any CPU.Build.0 = Release|Any CPU + {DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj index 206b2b157..c37e5375f 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false @@ -11,13 +11,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/vsts-cicd.yml b/vsts-cicd.yml index 44bd21187..0fc0c9186 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -45,7 +45,7 @@ stages: repoToCheckout: self checkoutPath: '$(Build.BuildNumber)' buildRelease: ${{variables.buildRelease}} -#TODO: Modify the function here + - template: yaml/build-functionapps.yml parameters: dependsOn: Build_Common @@ -101,7 +101,7 @@ stages: coverageThreshold: 80 - function: name: 'JobFinalizer' - coverageThreshold: 0 + coverageThreshold: 40 - template: yaml/build-release-package.yml parameters: From 74824df898a4c4ba51a9a0ad7525aa65f84e0706 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 8 Aug 2024 11:40:49 -0700 Subject: [PATCH 0219/1479] Remove sender --- .../Hosts/JobFinalizer/Function/Startup.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs index 158855e14..16082f84c 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs @@ -31,14 +31,6 @@ public override void Configure(IFunctionsHostBuilder builder) services.GetRequiredService() ); }); - builder.Services.AddSingleton(services => - { - var configuration = services.GetRequiredService(); - var JobFinalizerQueue = configuration["serviceBusJobFinalizerQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(JobFinalizerQueue); - return new ServiceBusQueueRepository(sender); - }); } } } From 985bf47a2c1dcdfdc125e0669056d26b17aab163 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 12:51:50 -0700 Subject: [PATCH 0220/1479] Change logging test --- .../JobFinalizerServiceTests.cs | 2 +- .../Mocks/MockLoggingRepository.cs | 2 +- .../OrchestratorTests.cs | 18 ++++++++++++------ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/JobFinalizerServiceTests.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/JobFinalizerServiceTests.cs index 9f40256bb..14c2531de 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/JobFinalizerServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/JobFinalizerServiceTests.cs @@ -8,7 +8,7 @@ using Repositories.Contracts; using Hosts.JobFinalizer; using Services.Tests; -using Services.Notifier.Tests.Mocks; +using Services.JobFinalizer.Tests.Mocks; namespace Services.Tests { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Mocks/MockLoggingRepository.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Mocks/MockLoggingRepository.cs index 5c81ec25c..976e68f91 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Mocks/MockLoggingRepository.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Mocks/MockLoggingRepository.cs @@ -8,7 +8,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -namespace Services.Notifier.Tests.Mocks +namespace Services.JobFinalizer.Tests.Mocks { public class MockLoggingRepository : ILoggingRepository { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/OrchestratorTests.cs index f5cba924b..827c21532 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/OrchestratorTests.cs @@ -12,7 +12,7 @@ using Models; using Repositories.Contracts; using Hosts.JobFinalizer; -using Services.Notifier.Tests.Mocks; +using Services.JobFinalizer.Tests.Mocks; namespace Services.Tests { @@ -29,7 +29,7 @@ public void Setup() { _mockContext = new Mock(); _mockLogger = new MockLoggingRepository(); - _orchestratorFunction = new OrchestratorFunction(_mockLogger); + _orchestratorFunction = new OrchestratorFunction(); } [TestMethod] @@ -46,11 +46,11 @@ public async Task RunOrchestratorTest() await _orchestratorFunction.RunOrchestratorAsync(_mockContext.Object, null); _mockContext.Verify(context => context.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny()), Times.Once); - Assert.AreEqual(2, _mockLogger.MessagesLoggedCount, "Expected two log messages for valid sync job."); + _mockContext.Verify(context => context.CallActivityAsync(nameof(LoggerFunction), It.IsAny()), Times.Exactly(2)); } [TestMethod] - public async Task RunOrchestratorTestWithUnknowStatus() + public async Task RunOrchestratorTestWithUnknownStatus() { var syncJob = SampleDataHelper.CreateSampleSyncJobs(1, GroupMembership).First(); var orchestratorRequest = new OrchestratorRequest @@ -58,13 +58,19 @@ public async Task RunOrchestratorTestWithUnknowStatus() SyncJob = syncJob, Status = "Unknown" }; + var loggerRequests = new List(); + _mockContext + .Setup(context => context.CallActivityAsync(nameof(LoggerFunction), It.IsAny())) + .Callback((name, request) => loggerRequests.Add((LoggerRequest)request)) + .Returns(Task.CompletedTask); _mockContext.Setup(c => c.GetInput()).Returns(orchestratorRequest); await _orchestratorFunction.RunOrchestratorAsync(_mockContext.Object, null); - - Assert.IsTrue(_mockLogger.MessagesLogged.Any(log => log.Message.Contains("unknown status")), "Expected an error log message for unknown status."); + _mockContext.Verify(context => context.CallActivityAsync(nameof(LoggerFunction), It.IsAny()), Times.Exactly(2), "Expected LoggerFunction to be called exactly twice."); + Assert.IsTrue(loggerRequests.Any(req => req.Message.Contains("unknown status")), "Expected an error log message for unknown status."); _mockContext.Verify(context => context.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny()), Times.Once); } + } } From b1a3a8ca9db1783a5bc4521b838211089ad36cff Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 13 Aug 2024 14:53:15 -0700 Subject: [PATCH 0221/1479] Rename JobFinalizer into SyncJobUpdater --- Deployment/computeResources.bicep | 12 +++--- Infrastructure/data/template.bicep | 12 +++--- ...t-AppConfigurationManagedIdentityRoles.ps1 | 2 +- ...geAccountContainerManagedIdentityRoles.ps1 | 2 +- .../JobStatusUpdaterFunction.cs | 10 ++--- .../JobStatusUpdaterRequest.cs | 2 +- .../JobFinalizer/Function/JobFinalizer.csproj | 2 +- .../JobFinalizer/Function/JobFinalizer.sln | 6 +-- .../Orchestrator/OrchestratorFunction.cs | 6 +-- .../Orchestrator/OrchrestratorRequest.cs | 2 +- .../Function/Starter/StarterFunction.cs | 4 +- .../Hosts/JobFinalizer/Function/Startup.cs | 10 ++--- .../Infrastructure/compute/template.bicep | 40 +++++++++---------- .../Infrastructure/data/template.bicep | 2 +- .../IJobFInalizerService.cs | 2 +- .../Services.JobFinalizer.Tests.csproj | 2 +- .../Services/JobFinalizerService.cs | 6 +-- .../Infrastructure/compute/template.bicep | 2 +- vsts-cicd.yml | 2 +- 19 files changed, 63 insertions(+), 63 deletions(-) diff --git a/Deployment/computeResources.bicep b/Deployment/computeResources.bicep index 7007e1e36..0a61442e4 100644 --- a/Deployment/computeResources.bicep +++ b/Deployment/computeResources.bicep @@ -467,9 +467,9 @@ module jobSchedulerComputeResources '../Service/GroupMembershipManagement/Hosts/ ] } -// ----------------- JobFinalizer -module jobFinalizerDataResources '../Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep' = { - name: 'jobFinalizerDataResourcesTemplate' +// ----------------- SyncJobUpdater +module syncJobUpdaterDataResources '../Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/template.bicep' = { + name: 'syncJobUpdaterDataResourcesTemplate' scope: resourceGroup(dataResourceGroupName) params: { location: location @@ -480,8 +480,8 @@ module jobFinalizerDataResources '../Service/GroupMembershipManagement/Hosts/Job } } -module jobFinalizerComputeResources '../Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep' = { - name: 'jobFinalizerComputeResourcesTemplate' +module syncJobUpdaterComputeResources '../Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep' = { + name: 'syncJobUpdaterComputeResourcesTemplate' scope: resourceGroup(computeResourceGroupName) params: { location: location @@ -494,7 +494,7 @@ module jobFinalizerComputeResources '../Service/GroupMembershipManagement/Hosts/ setRBACPermissions: setRBACPermissions } dependsOn: [ - jobFinalizerDataResources + syncJobUpdaterDataResources ] } diff --git a/Infrastructure/data/template.bicep b/Infrastructure/data/template.bicep index 157b3286c..c3de20a11 100644 --- a/Infrastructure/data/template.bicep +++ b/Infrastructure/data/template.bicep @@ -105,7 +105,7 @@ param serviceBusNotificationsQueue string = 'notifications' param serviceBusFailedNotificationsQueue string = 'failedNotifications' @description('Enter job finalizer service bus queue name') -param serviceBusJobFinalizerQueue string = 'jobFinalizer' +param serviceBusSyncJobUpdaterQueue string = 'syncJobUpdater' @description('Enter storage account name.') @minLength(1) @@ -548,10 +548,10 @@ module failedNotificationsQueue 'serviceBusQueue.bicep' = { ] } -module jobFinalizerQueue 'serviceBusQueue.bicep' = { - name: 'jobFinalizerQueue' +module syncJobUpdaterQueue 'serviceBusQueue.bicep' = { + name: 'syncJobUpdaterQueue' params: { - queueName: serviceBusJobFinalizerQueue + queueName: serviceBusSyncJobUpdaterQueue serviceBusName: serviceBusName requiresSession: false maxDeliveryCount: 5 @@ -702,8 +702,8 @@ module secretsTemplate 'keyVaultSecrets.bicep' = { value: serviceBusFailedNotificationsQueue } { - name: 'serviceBusJobFinalizerQueue' - value: serviceBusJobFinalizerQueue + name: 'serviceBusSyncJobUpdaterQueue' + value: serviceBusSyncJobUpdaterQueue } { name: 'graphUserAssignedManagedIdentityName' diff --git a/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 index f53402a27..6fe013453 100644 --- a/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 @@ -35,7 +35,7 @@ function Set-AppConfigurationManagedIdentityRoles [string] $ErrorActionPreference = $Stop ) - $apps = @("WebApi","GraphUpdater","MembershipAggregator","GroupMembershipObtainer","SqlMembershipObtainer","PlaceMembershipObtainer","AzureMaintenance","AzureUserReader","JobScheduler","JobTrigger","NonProdService","Notifier","TeamsChannelMembershipObtainer","GroupOwnershipObtainer", "TeamsChannelUpdater", "DestinationAttributesUpdater", "JobFinalizer") + $apps = @("WebApi","GraphUpdater","MembershipAggregator","GroupMembershipObtainer","SqlMembershipObtainer","PlaceMembershipObtainer","AzureMaintenance","AzureUserReader","JobScheduler","JobTrigger","NonProdService","Notifier","TeamsChannelMembershipObtainer","GroupOwnershipObtainer", "TeamsChannelUpdater", "DestinationAttributesUpdater", "SyncJobUpdater") $resourceGroupName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation"; if($DataResourceGroupName) diff --git a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 index 407eb49fa..cb693da85 100644 --- a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 @@ -36,7 +36,7 @@ function Set-StorageAccountContainerManagedIdentityRoles [string] $ErrorActionPreference = $Stop ) - $functionApps = @("GroupMembershipObtainer","SqlMembershipObtainer","PlaceMembershipObtainer","MembershipAggregator","GraphUpdater","TeamsChannelMembershipObtainer","GroupOwnershipObtainer","TeamsChannelUpdater", "DestinationAttributesUpdater", "JobFinalizer") + $functionApps = @("GroupMembershipObtainer","SqlMembershipObtainer","PlaceMembershipObtainer","MembershipAggregator","GraphUpdater","TeamsChannelMembershipObtainer","GroupOwnershipObtainer","TeamsChannelUpdater", "DestinationAttributesUpdater", "SyncJobUpdater") foreach ($functionApp in $functionApps) { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs index 0eb12c519..487ab1d00 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -7,17 +7,17 @@ using System.Threading.Tasks; using Services.Contracts; -namespace Hosts.JobFinalizer +namespace Hosts.SyncJobUpdater { public class JobStatusUpdaterFunction { private readonly ILoggingRepository _loggingRepository; - private readonly IJobFinalizerService _jobFinalizerService; + private readonly ISyncJobUpdaterService _syncJobUpdaterService; - public JobStatusUpdaterFunction(ILoggingRepository loggingRepository, IJobFinalizerService jobFinalizerService) + public JobStatusUpdaterFunction(ILoggingRepository loggingRepository, ISyncJobUpdaterService syncJobUpdaterService) { _loggingRepository = loggingRepository; - _jobFinalizerService = jobFinalizerService; + _syncJobUpdaterService = syncJobUpdaterService; } [FunctionName(nameof(JobStatusUpdaterFunction))] @@ -26,7 +26,7 @@ public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest if (request.SyncJob != null) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobStatusUpdaterFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); - await _jobFinalizerService.UpdateSyncJobStatusAsync(request.SyncJob, request.Status); + await _syncJobUpdaterService.UpdateSyncJobStatusAsync(request.SyncJob, request.Status); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobStatusUpdaterFunction)} function completed", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); } } diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs index 7d35d436e..bf152bfeb 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs @@ -3,7 +3,7 @@ using Models; using System; -namespace Hosts.JobFinalizer +namespace Hosts.SyncJobUpdater { public class JobStatusUpdaterRequest { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj index 13963d3d1..d485a124f 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj @@ -17,7 +17,7 @@ - + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln index 29707f450..c622caa4a 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln @@ -3,11 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.10.35027.167 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobFinalizer", "JobFinalizer.csproj", "{D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SyncJobUpdater", "SyncJobUpdater.csproj", "{D6DC947C-D41D-4DB5-A712-5CDF8DA827A0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.FunctionBase", "..\..\..\Hosts.FunctionBase\Hosts.FunctionBase.csproj", "{A54477AB-BD0E-492F-91F1-C70EB4FEDB11}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobFinalizerService", "..\Services\JobFinalizerService.csproj", "{B84FE7BA-3798-4287-8356-6FAD64166217}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SyncJobUpdaterService", "..\Services\SyncJobUpdaterService.csproj", "{B84FE7BA-3798-4287-8356-6FAD64166217}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Contracts", "..\Services.Contracts\Services.Contracts.csproj", "{9D98B058-0BF5-40C3-86FD-075E1A2E38BB}" EndProject @@ -15,7 +15,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Contracts", ". EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\Models.csproj", "{F7C85A82-C663-4E0A-9016-693DE824A0E7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.JobFinalizer.Tests", "..\Services.JobFinalizer.Tests\Services.JobFinalizer.Tests.csproj", "{DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.SyncJobUpdater.Tests", "..\Services.SyncJobUpdater.Tests\Services.SyncJobUpdater.Tests.csproj", "{DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs index 0b64b55b7..9b04cbfb4 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs @@ -9,7 +9,7 @@ using Models.Helpers; using Newtonsoft.Json; using Repositories.Contracts; -using Hosts.JobFinalizer; +using Hosts.SyncJobUpdater; using System; using System.Collections.Generic; using System.Net; @@ -17,9 +17,9 @@ using System.Threading.Tasks; using Repositories.Contracts.InjectConfig; using Models.Notifications; -using Hosts.JobFinalizer; +using Hosts.SyncJobUpdater; -namespace Hosts.JobFinalizer +namespace Hosts.SyncJobUpdater { public class OrchestratorFunction { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs index d6e194043..57d1eb627 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs @@ -3,7 +3,7 @@ using Models; -namespace Hosts.JobFinalizer +namespace Hosts.SyncJobUpdater { public class OrchestratorRequest { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs index 835ed3430..e95a1e987 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs @@ -11,7 +11,7 @@ using System.Text.Json; using System.Threading.Tasks; -namespace Hosts.JobFinalizer +namespace Hosts.SyncJobUpdater { public class StarterFunction { @@ -24,7 +24,7 @@ public StarterFunction(ILoggingRepository loggingRepository) [FunctionName(nameof(StarterFunction))] public async Task RunAsync( - [ServiceBusTrigger("%serviceBusJobFinalizerQueue%", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, + [ServiceBusTrigger("%serviceBusSyncJobUpdaterQueue%", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, [DurableClient] IDurableOrchestrationClient starter) { var syncJob = JsonSerializer.Deserialize(Encoding.UTF8.GetString(message.Body)); diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs index 16082f84c..f292ec3fc 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs @@ -11,14 +11,14 @@ using Repositories.ServiceBusQueue; // see https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection -[assembly: FunctionsStartup(typeof(Hosts.JobFinalizer.Startup))] +[assembly: FunctionsStartup(typeof(Hosts.SyncJobUpdater.Startup))] -namespace Hosts.JobFinalizer +namespace Hosts.SyncJobUpdater { public class Startup : CommonStartup { - protected override string FunctionName => nameof(JobFinalizer); - protected override string DryRunSettingName => "JobFinalizer:IsDryRunEnabled"; + protected override string FunctionName => nameof(SyncJobUpdater); + protected override string DryRunSettingName => "SyncJobUpdater:IsDryRunEnabled"; public override void Configure(IFunctionsHostBuilder builder) { @@ -26,7 +26,7 @@ public override void Configure(IFunctionsHostBuilder builder) builder.Services.AddScoped(services => { - return new JobFinalizerService( + return new SyncJobUpdaterService( services.GetRequiredService(), services.GetRequiredService() ); diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep index 4eb015a4b..0c20ff54c 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep @@ -23,7 +23,7 @@ param storageAccountName string param tenantId string @description('Service plan name.') -param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'NotifierJobFinalizer'),0,8)}' +param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'NotifierSyncJobUpdater'),0,8)}' @description('Resource location.') param location string @@ -69,15 +69,15 @@ var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultRe var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') -var jobFinalizerStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') +var syncJobUpdaterStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierProviderId') var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'replicaJobsMSIConnectionString') -var jobFinalizerStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobFinalizerStorageAccountProd') +var syncJobUpdaterStorageAccountProd = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'syncJobUpdaterStorageAccountProd') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') var serviceBusNotificationsQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusNotificationsQueue') -var serviceBusJobFinalizerQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusJobFinalizerQueue') +var serviceBusSyncJobUpdaterQueue = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusSyncJobUpdaterQueue') var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 @@ -89,13 +89,13 @@ var commonSettings = { } var appSettings = { - AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}JobFinalizer' - AzureFunctionsWebHost__hostid: 'JobFinalizer' + AzureWebJobsStorage: '@Microsoft.KeyVault(SecretUri=${reference(syncJobUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' + AzureFunctionsJobHost__extensions__durableTask__hubName: '${solutionAbbreviation}compute${environmentAbbreviation}SyncJobUpdater' + AzureFunctionsWebHost__hostid: 'SyncJobUpdater' 'AzureFunctionsJobHost:extensions:durableTask:extendedSessionsEnabled': toLower(environmentAbbreviation) == 'prodv2' ? 'True' : 'False' APPINSIGHTS_INSTRUMENTATIONKEY: '@Microsoft.KeyVault(SecretUri=${reference(appInsightsInstrumentationKey, '2019-09-01').secretUriWithVersion})' - WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountProd, '2019-09-01').secretUriWithVersion})' - WEBSITE_CONTENTSHARE: toLower('functionApp-JobFinalizer') + WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=${reference(syncJobUpdaterStorageAccountProd, '2019-09-01').secretUriWithVersion})' + WEBSITE_CONTENTSHARE: toLower('functionApp-SyncJobUpdater') serviceBusSyncJobTopic: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusSyncJobTopic, '2019-09-01').secretUriWithVersion})' gmmServiceBus__fullyQualifiedNamespace: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusFQN, '2019-09-01').secretUriWithVersion})' 'graphCredentials:ClientCertificateName': '@Microsoft.KeyVault(SecretUri=${reference(graphAppCertificateName, '2019-09-01').secretUriWithVersion})' @@ -112,12 +112,12 @@ var appSettings = { senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' - jobFinalizerStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(jobFinalizerStorageAccountName, '2019-09-01').secretUriWithVersion})' + syncJobUpdaterStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(syncJobUpdaterStorageAccountName, '2019-09-01').secretUriWithVersion})' appConfigurationEndpoint: appConfigurationEndpoint actionableEmailProviderId: '@Microsoft.KeyVault(SecretUri=${reference(actionableEmailProviderId, '2019-09-01').secretUriWithVersion})' 'graphCredentials:UserAssignedManagedIdentityClientId': '@Microsoft.KeyVault(SecretUri=${reference(graphUserAssignedManagedIdentityClientId, '2019-09-01').secretUriWithVersion})' serviceBusNotificationsQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusNotificationsQueue, '2019-09-01').secretUriWithVersion})' - serviceBusJobFinalizerQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusJobFinalizerQueue, '2019-09-01').secretUriWithVersion})' + serviceBusSyncJobUpdaterQueue: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusSyncJobUpdaterQueue, '2019-09-01').secretUriWithVersion})' } var activityFunctionSettings = { @@ -136,7 +136,7 @@ resource dataKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { } module userAssignedManagedIdentityNameReader 'keyVaultReader.bicep' = { - name: 'uamiNameReader-JobFinalizer' + name: 'uamiNameReader-SyncJobUpdater' params: { value: dataKeyVault.getSecret('graphUserAssignedManagedIdentityName') } @@ -160,10 +160,10 @@ module existingLogAnalyticsWorkspace 'logAnalyticsWorkspace.bicep' = { } } -module functionAppTemplate_JobFinalizer 'functionApp.bicep' = { - name: 'functionAppTemplate-JobFinalizer' +module functionAppTemplate_SyncJobUpdater 'functionApp.bicep' = { + name: 'functionAppTemplate-SyncJobUpdater' params: { - name: '${functionAppName}-JobFinalizer' + name: '${functionAppName}-SyncJobUpdater' kind: functionAppKind location: location servicePlanName: servicePlanName @@ -181,23 +181,23 @@ module functionAppTemplate_JobFinalizer 'functionApp.bicep' = { module functionAppRBAC 'functionAppRBAC.bicep' = { - name: 'functionAppsRBAC-JobFinalizer' + name: 'functionAppsRBAC-SyncJobUpdater' params: { - functionName: 'JobFinalizer' + functionName: 'SyncJobUpdater' prereqsKeyVaultName: prereqsKeyVaultName prereqsKeyVaultResourceGroup: prereqsKeyVaultResourceGroup dataKeyVaultName: dataKeyVaultName dataKeyVaultResourceGroup: dataKeyVaultResourceGroup setRBACPermissions: setRBACPermissions - productionSlotPrincipalId: functionAppTemplate_JobFinalizer.outputs.msi + productionSlotPrincipalId: functionAppTemplate_SyncJobUpdater.outputs.msi } dependsOn: [ - functionAppTemplate_JobFinalizer + functionAppTemplate_SyncJobUpdater ] } resource functionAppSettings 'Microsoft.Web/sites/config@2022-09-01' = { - name: '${functionAppName}-JobFinalizer/appsettings' + name: '${functionAppName}-SyncJobUpdater/appsettings' kind: 'string' properties: union(commonSettings, appSettings, activityFunctionSettings) dependsOn: [ diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep index 126ad1366..3725070f5 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep @@ -29,7 +29,7 @@ module jfStorageAccountProd 'storageAccount.bicep' = { sku: storageAccountSku keyVaultName: keyVaultName location: location - storageAccountConnectionStringSettingName: 'jobFinalizerStorageAccountProd' + storageAccountConnectionStringSettingName: 'syncJobUpdaterStorageAccountProd' } } diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/IJobFInalizerService.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/IJobFInalizerService.cs index 78e566574..357d834dc 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/IJobFInalizerService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/IJobFInalizerService.cs @@ -7,7 +7,7 @@ namespace Services.Contracts { - public interface IJobFinalizerService + public interface ISyncJobUpdaterService { Task UpdateSyncJobStatusAsync(SyncJob job, SyncStatus status); } diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj index c37e5375f..848a4f81e 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj @@ -25,7 +25,7 @@ - + diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.cs b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.cs index 78719efd6..29377c5b3 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.cs @@ -15,16 +15,16 @@ using Models.Helpers; using Services.Contracts; -namespace Hosts.JobFinalizer +namespace Hosts.SyncJobUpdater { - public class JobFinalizerService : IJobFinalizerService + public class SyncJobUpdaterService : ISyncJobUpdaterService { private readonly ILoggingRepository _log; private readonly IDatabaseSyncJobsRepository _databaseSyncJobsRepository; - public JobFinalizerService( + public SyncJobUpdaterService( IDatabaseSyncJobsRepository databaseSyncJobsRepository, ILoggingRepository logging ) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep index 02d870b6f..d0fb6be36 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep @@ -26,7 +26,7 @@ param prereqsKeyVaultName string = '${solutionAbbreviation}-prereqs-${environmen param prereqsKeyVaultResourceGroup string = '${solutionAbbreviation}-prereqs-${environmentAbbreviation}' @description('Service plan name.') -param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'NotifierJobFinalizer'),0,8)}' +param servicePlanName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-${substring(uniqueString(subscription().id,'NotifierSyncJobUpdater'),0,8)}' @description('App service name.') param appServiceName string = '${solutionAbbreviation}-${resourceGroupClassification}-${environmentAbbreviation}-webapi' diff --git a/vsts-cicd.yml b/vsts-cicd.yml index 0fc0c9186..d2e04f6c0 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -100,7 +100,7 @@ stages: name: 'GroupOwnershipObtainer' coverageThreshold: 80 - function: - name: 'JobFinalizer' + name: 'SyncJobUpdater' coverageThreshold: 40 - template: yaml/build-release-package.yml From 9ef7853f6d5c6941c9cfd8e490436ad9e4e8dfde Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 13 Aug 2024 14:57:15 -0700 Subject: [PATCH 0222/1479] Rename File name from JobFinalizer into SyncJobUpdater --- .../Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs | 0 .../Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs | 0 .../Function/Orchestrator/OrchestratorFunction.cs | 0 .../Function/Orchestrator/OrchrestratorRequest.cs | 0 .../Function/Starter/StarterFunction.cs | 0 .../Hosts/{JobFinalizer => SyncJobUpdater}/Function/Startup.cs | 0 .../Function/SyncJobUpdater.csproj} | 0 .../Function/SyncJobUpdater.sln} | 0 .../Hosts/{JobFinalizer => SyncJobUpdater}/Function/host.json | 0 .../{JobFinalizer => SyncJobUpdater}/Function/local.settings.json | 0 .../Infrastructure/compute/functionApp.bicep | 0 .../Infrastructure/compute/functionAppRBAC.bicep | 0 .../Infrastructure/compute/keyVaultRBAC.bicep | 0 .../Infrastructure/compute/keyVaultReader.bicep | 0 .../Infrastructure/compute/logAnalyticsWorkspace.bicep | 0 .../Infrastructure/compute/parameters/parameters.int.json | 0 .../Infrastructure/compute/parameters/parameters.prodv2.json | 0 .../Infrastructure/compute/parameters/parameters.ua.json | 0 .../Infrastructure/compute/template.bicep | 0 .../Infrastructure/data/keyVaultSecretsSecure.bicep | 0 .../Infrastructure/data/parameters/parameters.int.json | 0 .../Infrastructure/data/parameters/parameters.prodv2.json | 0 .../Infrastructure/data/parameters/parameters.ua.json | 0 .../Infrastructure/data/storageAccount.bicep | 0 .../Infrastructure/data/template.bicep | 0 .../Services.Contracts/ISyncJobUpdaterService.cs} | 0 .../Services.Contracts/Services.Contracts.csproj | 0 .../Services.SyncJobUpdater.Tests}/JobFinalizerServiceTests.cs | 0 .../Services.SyncJobUpdater.Tests}/Mocks/MockLoggingRepository.cs | 0 .../Services.SyncJobUpdater.Tests}/OrchestratorTests.cs | 0 .../Services.SyncJobUpdater.Tests}/SampleDataHelper.cs | 0 .../Services.SyncJobUpdater.Tests.csproj} | 0 .../{JobFinalizer => SyncJobUpdater}/Services/Services.csproj | 0 .../Services/SyncJobUpdaterService.cs} | 0 .../Services/SyncJobUpdaterService.csproj} | 0 35 files changed, 0 insertions(+), 0 deletions(-) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Function/Orchestrator/OrchestratorFunction.cs (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Function/Orchestrator/OrchrestratorRequest.cs (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Function/Starter/StarterFunction.cs (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Function/Startup.cs (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer/Function/JobFinalizer.csproj => SyncJobUpdater/Function/SyncJobUpdater.csproj} (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer/Function/JobFinalizer.sln => SyncJobUpdater/Function/SyncJobUpdater.sln} (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Function/host.json (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Function/local.settings.json (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/compute/functionApp.bicep (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/compute/functionAppRBAC.bicep (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/compute/keyVaultRBAC.bicep (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/compute/keyVaultReader.bicep (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/compute/logAnalyticsWorkspace.bicep (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/compute/parameters/parameters.int.json (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/compute/parameters/parameters.prodv2.json (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/compute/parameters/parameters.ua.json (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/compute/template.bicep (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/data/keyVaultSecretsSecure.bicep (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/data/parameters/parameters.int.json (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/data/parameters/parameters.prodv2.json (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/data/parameters/parameters.ua.json (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/data/storageAccount.bicep (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Infrastructure/data/template.bicep (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer/Services.Contracts/IJobFInalizerService.cs => SyncJobUpdater/Services.Contracts/ISyncJobUpdaterService.cs} (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Services.Contracts/Services.Contracts.csproj (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer/Services.JobFinalizer.Tests => SyncJobUpdater/Services.SyncJobUpdater.Tests}/JobFinalizerServiceTests.cs (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer/Services.JobFinalizer.Tests => SyncJobUpdater/Services.SyncJobUpdater.Tests}/Mocks/MockLoggingRepository.cs (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer/Services.JobFinalizer.Tests => SyncJobUpdater/Services.SyncJobUpdater.Tests}/OrchestratorTests.cs (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer/Services.JobFinalizer.Tests => SyncJobUpdater/Services.SyncJobUpdater.Tests}/SampleDataHelper.cs (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj => SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj} (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Services/Services.csproj (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer/Services/JobFinalizerService.cs => SyncJobUpdater/Services/SyncJobUpdaterService.cs} (100%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer/Services/JobFinalizerService.csproj => SyncJobUpdater/Services/SyncJobUpdaterService.csproj} (100%) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Orchestrator/OrchestratorFunction.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchestratorFunction.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Orchestrator/OrchestratorFunction.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Orchestrator/OrchrestratorRequest.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Orchestrator/OrchrestratorRequest.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Orchestrator/OrchrestratorRequest.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Starter/StarterFunction.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Starter/StarterFunction.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Starter/StarterFunction.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Startup.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Startup.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.csproj rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.sln similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/JobFinalizer.sln rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.sln diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/host.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/host.json similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/host.json rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/host.json diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/local.settings.json similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/local.settings.json rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/local.settings.json diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/functionApp.bicep similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionApp.bicep rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/functionApp.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppRBAC.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/functionAppRBAC.bicep similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/functionAppRBAC.bicep rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/functionAppRBAC.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultRBAC.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/keyVaultRBAC.bicep similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultRBAC.bicep rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/keyVaultRBAC.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultReader.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/keyVaultReader.bicep similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/keyVaultReader.bicep rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/keyVaultReader.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/logAnalyticsWorkspace.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/logAnalyticsWorkspace.bicep similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/logAnalyticsWorkspace.bicep rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/logAnalyticsWorkspace.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.int.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/parameters/parameters.int.json similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.int.json rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/parameters/parameters.int.json diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.prodv2.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/parameters/parameters.prodv2.json similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.prodv2.json rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/parameters/parameters.prodv2.json diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.ua.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/parameters/parameters.ua.json similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/parameters/parameters.ua.json rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/parameters/parameters.ua.json diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/compute/template.bicep rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/keyVaultSecretsSecure.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/keyVaultSecretsSecure.bicep similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/keyVaultSecretsSecure.bicep rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/keyVaultSecretsSecure.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.int.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/parameters/parameters.int.json similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.int.json rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/parameters/parameters.int.json diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.prodv2.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/parameters/parameters.prodv2.json similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.prodv2.json rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/parameters/parameters.prodv2.json diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.ua.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/parameters/parameters.ua.json similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/parameters/parameters.ua.json rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/parameters/parameters.ua.json diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/storageAccount.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/storageAccount.bicep similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/storageAccount.bicep rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/storageAccount.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/template.bicep similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Infrastructure/data/template.bicep rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/template.bicep diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/IJobFInalizerService.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.Contracts/ISyncJobUpdaterService.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/IJobFInalizerService.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.Contracts/ISyncJobUpdaterService.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/Services.Contracts.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.Contracts/Services.Contracts.csproj similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.Contracts/Services.Contracts.csproj rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.Contracts/Services.Contracts.csproj diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/JobFinalizerServiceTests.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/JobFinalizerServiceTests.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Mocks/MockLoggingRepository.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Mocks/MockLoggingRepository.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Mocks/MockLoggingRepository.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Mocks/MockLoggingRepository.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/OrchestratorTests.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/OrchestratorTests.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/OrchestratorTests.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/SampleDataHelper.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/SampleDataHelper.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/SampleDataHelper.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/SampleDataHelper.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services.JobFinalizer.Tests/Services.JobFinalizer.Tests.csproj rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/Services.csproj similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/Services.csproj rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/Services.csproj diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/SyncJobUpdaterService.cs similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/SyncJobUpdaterService.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/SyncJobUpdaterService.csproj similarity index 100% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Services/JobFinalizerService.csproj rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/SyncJobUpdaterService.csproj From 2d9f2fac9d68c0b97b11774a7bba73a19983d2d1 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 13 Aug 2024 15:03:23 -0700 Subject: [PATCH 0223/1479] Rename test name from JobFinalizer into SyncJobUpdater --- .../JobFinalizerServiceTests.cs | 10 +++++----- .../Services.SyncJobUpdater.Tests/OrchestratorTests.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs index 14c2531de..746713c7f 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs @@ -6,25 +6,25 @@ using Moq; using Models; using Repositories.Contracts; -using Hosts.JobFinalizer; +using Hosts.SyncJobUpdater; using Services.Tests; using Services.JobFinalizer.Tests.Mocks; namespace Services.Tests { [TestClass] - public class JobFinalizerServiceTests + public class SyncJobUpdaterServiceTests { private Mock _mockDatabaseSyncJobsRepository; private MockLoggingRepository _mockLoggingRepository; - private JobFinalizerService _jobFinalizerService; + private SyncJobUpdaterService _syncJobUpdaterService; [TestInitialize] public void Setup() { _mockDatabaseSyncJobsRepository = new Mock(); _mockLoggingRepository = new MockLoggingRepository(); - _jobFinalizerService = new JobFinalizerService(_mockDatabaseSyncJobsRepository.Object, _mockLoggingRepository); + _syncJobUpdaterService = new SyncJobUpdaterService(_mockDatabaseSyncJobsRepository.Object, _mockLoggingRepository); } [TestMethod] @@ -32,7 +32,7 @@ public async Task UpdateSyncJobStatus() { var syncJob = new SyncJob { Id = Guid.NewGuid() }; var status = SyncStatus.Idle; - await _jobFinalizerService.UpdateSyncJobStatusAsync(syncJob, status); + await _syncJobUpdaterService.UpdateSyncJobStatusAsync(syncJob, status); _mockDatabaseSyncJobsRepository.Verify(repo => repo.UpdateSyncJobStatusAsync(It.Is(jobs => jobs.Length == 1 && jobs[0] == syncJob), status), Times.Once); } } diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/OrchestratorTests.cs index 827c21532..f7cd1d8a3 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/OrchestratorTests.cs @@ -11,8 +11,8 @@ using Moq; using Models; using Repositories.Contracts; -using Hosts.JobFinalizer; -using Services.JobFinalizer.Tests.Mocks; +using Services.SyncJobUpdater.Tests.Mocks; +using Hosts.SyncJobUpdater; namespace Services.Tests { From f944b9098ba07f10e5c9bb6724a2d3c00f73047c Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 13:15:27 -0700 Subject: [PATCH 0224/1479] Change name for logging repo --- .../Function/Activity/Logger/LoggerFunction.cs | 2 +- .../Function/Activity/Logger/LoggerRequest.cs | 2 +- .../Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs | 2 +- .../Mocks/MockLoggingRepository.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Function/Activity/Logger/LoggerFunction.cs (96%) rename Service/GroupMembershipManagement/Hosts/{JobFinalizer => SyncJobUpdater}/Function/Activity/Logger/LoggerRequest.cs (91%) diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/Logger/LoggerFunction.cs similarity index 96% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerFunction.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/Logger/LoggerFunction.cs index 84e4f5136..4ceca5ccc 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/Logger/LoggerFunction.cs @@ -8,7 +8,7 @@ using System; using System.Threading.Tasks; -namespace Hosts.JobFinalizer +namespace Hosts.SyncJobUpdater { public class LoggerFunction { diff --git a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerRequest.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/Logger/LoggerRequest.cs similarity index 91% rename from Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerRequest.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/Logger/LoggerRequest.cs index 8644ee86f..9e1957c68 100644 --- a/Service/GroupMembershipManagement/Hosts/JobFinalizer/Function/Activity/Logger/LoggerRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/Logger/LoggerRequest.cs @@ -4,7 +4,7 @@ using Repositories.Contracts; using System; -namespace Hosts.JobFinalizer +namespace Hosts.SyncJobUpdater { public class LoggerRequest { diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs index 746713c7f..0d6b74a84 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs @@ -8,7 +8,7 @@ using Repositories.Contracts; using Hosts.SyncJobUpdater; using Services.Tests; -using Services.JobFinalizer.Tests.Mocks; +using Services.SyncJobUpdater.Tests.Mocks; namespace Services.Tests { diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Mocks/MockLoggingRepository.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Mocks/MockLoggingRepository.cs index 976e68f91..b707ef1bf 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Mocks/MockLoggingRepository.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Mocks/MockLoggingRepository.cs @@ -8,7 +8,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -namespace Services.JobFinalizer.Tests.Mocks +namespace Services.SyncJobUpdater.Tests.Mocks { public class MockLoggingRepository : ILoggingRepository { From 8a8128bc75574e6436448b2f18c4f6037af636dd Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 22 Aug 2024 14:43:15 -0700 Subject: [PATCH 0225/1479] Update the local setting --- .../Hosts/SyncJobUpdater/Function/local.settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/local.settings.json index 297c1d2a0..39a23ca0e 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/local.settings.json @@ -10,7 +10,7 @@ "supportEmailAddresses": "", "AzureFunctionsWebHost__hostid": "", "graphCredentials:TenantId": "", - "jobFinalizerStorageAccountName": "", + "syncJobUpdaterStorageAccountName": "", "senderAddress": "", "WEBSITE_CONTENTSHARE": "", "AzureWebJobsStorage": "", @@ -29,7 +29,7 @@ "ConnectionStrings:JobsContextReadOnly": "", "senderPassword": "", "serviceBusNotificationsQueue": "", - "serviceBusJobFinalizerQueue": "", + "serviceBusSyncJobUpdaterQueue": "", "APPINSIGHTS_INSTRUMENTATIONKEY": "" } } \ No newline at end of file From 64dd181c332b6087f1ba21a1721ffa36e9aa013e Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 14 Aug 2024 16:14:30 -0700 Subject: [PATCH 0226/1479] Add bicep resources for signalR setup --- .../Infrastructure/compute/template.bicep | 26 +++++++++++++++++++ .../data/keyVaultSecretsSecure.bicep | 10 +++++++ .../WebApi/Infrastructure/data/template.bicep | 20 ++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/data/keyVaultSecretsSecure.bicep diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep index c6310f456..50b84e3e5 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep @@ -80,6 +80,7 @@ var replicaJobsMSIConnectionString = resourceId(subscription().subscriptionId, d var jobsMSIConnectionString = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsMSIConnectionString') var sqlServerMSIConnectionString = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'sqlServerMSIConnectionString') var graphUserAssignedManagedIdentityClientId = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'graphUserAssignedManagedIdentityClientId') +var azureSignalRConnectionString = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'azureSignalRConnectionString') var serviceBusFQN = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusFQN') var serviceBusMembershipAggregatorQueue = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'serviceBusMembershipAggregatorQueue') @@ -178,6 +179,10 @@ var appSettings = [ name: 'Settings:SqlServerConnectionString' value: '@Microsoft.KeyVault(SecretUri=${reference(sqlServerMSIConnectionString, '2019-09-01').secretUriWithVersion})' } + { + name: 'Settings:AzureSignalRConnectionString' + value: '@Microsoft.KeyVault(SecretUri=${reference(azureSignalRConnectionString, '2019-09-01').secretUriWithVersion})' + } { name: 'ADF:Pipeline' value: adfPipeline @@ -244,6 +249,27 @@ resource graphUAMI 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31- scope: resourceGroup(dataResourceGroup) } +resource signalR 'Microsoft.SignalRService/signalR@2021-06-01-preview' = { + name: '${solutionAbbreviation}-compute-${environmentAbbreviation}-signalr' + location: location + sku: { + name: 'Standard_S1' + tier: 'Standard' + capacity: 1 + } + properties: { + cors: { + allowedOrigins: ['https://microsoft.com', 'http://localhost:3000'] + } + features: [ + { + flag: 'ServiceMode' + value: 'Default' + } + ] + } +} + module servicePlanTemplate 'servicePlan.bicep' = { name: 'servicePlanTemplate-WebApi' params: { diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/data/keyVaultSecretsSecure.bicep b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/data/keyVaultSecretsSecure.bicep new file mode 100644 index 000000000..bbb8c8b38 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/data/keyVaultSecretsSecure.bicep @@ -0,0 +1,10 @@ +param keyVaultName string +@secure() +param keyVaultSecrets object + +resource secrets 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = [for secret in keyVaultSecrets.secrets: { + name: '${keyVaultName}/${secret.name}' + properties: { + value: secret.value + } +}] diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/data/template.bicep index c24690fbf..a83ca6078 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/data/template.bicep @@ -13,6 +13,11 @@ param environmentAbbreviation string @maxLength(24) param appConfigurationName string = '${solutionAbbreviation}-appConfig-${environmentAbbreviation}' +@description('Name of the \'data\' key vault.') +param dataKeyVaultName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' + +var azureSignalRConnectionString = 'Endpoint=https://${solutionAbbreviation}-compute-${environmentAbbreviation}-signalr.service.signalr.net;AuthType=azure.msi;Version=1.0;' + param appConfigurationKeyData array = [ { key: 'WebAPI:Settings:Sentinel' @@ -31,3 +36,18 @@ module appConfigurationTemplate 'appConfigurationValues.bicep' = { appConfigurationKeyData: appConfigurationKeyData } } + +module secureKeyvaultSecrets 'keyVaultSecretsSecure.bicep' = { + name: 'secureKeyvaultSecrets' + params: { + keyVaultName: dataKeyVaultName + keyVaultSecrets: { + secrets: [ + { + name: 'azureSignalRConnectionString' + value: azureSignalRConnectionString + } + ] + } + } +} From f97c8afe38723ffc7a301ee6f5f68d1a11ef4d4f Mon Sep 17 00:00:00 2001 From: abgonz Date: Fri, 16 Aug 2024 14:44:42 -0700 Subject: [PATCH 0227/1479] Add signalR service to front-end and back-end --- .../GetServiceStatusHandler.cs | 7 +- .../Services.WebApi/Services.WebApi.csproj | 1 + .../WebApi/Services.WebApi/SignalRService.cs | 19 ++++ .../OperationsBackgroundService.cs | 4 + .../Hosts/WebApi/WebApi/Program.cs | 4 + .../Hosts/WebApi/WebApi/appsettings.json | 1 + UI/web-app/package.json | 1 + UI/web-app/pnpm-lock.yaml | 90 +++++++++++++++++-- .../components/Operation/Operation.base.tsx | 2 +- UI/web-app/src/index.tsx | 1 + .../src/services/signalR/SignalRService.ts | 20 +++++ UI/web-app/src/services/signalR/index.ts | 1 + UI/web-app/src/store/operations.slice.tsx | 4 + 13 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs create mode 100644 UI/web-app/src/services/signalR/SignalRService.ts create mode 100644 UI/web-app/src/services/signalR/index.ts diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs index 4c5b4f3e4..fbf118ce9 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; using Models; using Repositories.Contracts; using Services.Contracts; @@ -13,12 +15,15 @@ public class GetServiceStatusHandler : RequestHandlerBase _hubContext; public GetServiceStatusHandler(ILoggingRepository loggingRepository, - IServiceStatusRepository serviceStatusRepository) : base(loggingRepository) + IServiceStatusRepository serviceStatusRepository, + IHubContext hubContext) : base(loggingRepository) { _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); _serviceStatusRepository = serviceStatusRepository ?? throw new ArgumentNullException(nameof(serviceStatusRepository)); + _hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext)); } protected override async Task ExecuteCoreAsync(GetServiceStatusRequest request) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj index a6af131c9..f728989e8 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/Services.WebApi.csproj @@ -12,6 +12,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs new file mode 100644 index 000000000..48fda244c --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.SignalR; +using System.Text; +using System.Threading.Tasks; + +namespace Services.WebApi +{ + [ExcludeFromCodeCoverage] + public class SignalRService : Hub + { + public async Task NewMessage(long username, string message) => + await Clients.All.SendAsync("messageReceived", username, message); + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs index 6604fc1c2..a75f3a595 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/BackgroundServices/OperationsBackgroundService.cs @@ -5,12 +5,14 @@ using Azure.Messaging.ServiceBus; using Azure.Messaging.ServiceBus.Administration; using Azure.Storage.Queues; +using Microsoft.AspNetCore.SignalR; using Models; using Newtonsoft.Json; using Polly; using Repositories.Contracts; using Services.Contracts; using Services.Entities; +using Services.WebApi; using Services.WebApi.Contracts; using System.Diagnostics.CodeAnalysis; using System.Text; @@ -132,6 +134,8 @@ private async Task SetStatusAsync(ServiceStatuses status, Guid requestorId) { var scopedStatusRepository = scope.ServiceProvider.GetRequiredService(); await scopedStatusRepository.SetServiceStatusAsync(status, requestorId); + var signalRHubContext = scope.ServiceProvider.GetRequiredService>(); + await signalRHubContext.Clients.All.SendAsync("ServiceStatusChanged", status); } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs index 77f68f2af..893a510f8 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs @@ -325,6 +325,8 @@ public static void Main(string[] args) return settings.Value; }); + builder.Services.AddSignalR(); + builder.WebHost.ConfigureServices(services => { services.AddHostedService(); @@ -380,6 +382,8 @@ public static void Main(string[] args) app.UseStaticFiles(); app.UseRouting(); + app.MapHub("/hub"); + var allowedOrigins = new[] { "https://*.microsoft.com", "http://localhost:3000" }; app.UseCors(x => x .SetIsOriginAllowedToAllowWildcardSubdomains() diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json index 9f299d75c..8a809c3f3 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/appsettings.json @@ -12,6 +12,7 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform "jobsStorageAccountConnectionString": "", "ActionableEmailProviderId": "00000000-0000-0000-0000-000000000000", "ApiHostname": "api..gmm.microsoft.com", + "AzureSignalRConnectionString": "", "SqlServerConnectionString": "", "GraphCredentials": { "clientId": "", diff --git a/UI/web-app/package.json b/UI/web-app/package.json index c3c13db28..3198a1a79 100644 --- a/UI/web-app/package.json +++ b/UI/web-app/package.json @@ -9,6 +9,7 @@ "@fluentui/react-icons-mdl2": "^1.3.50", "@microsoft/applicationinsights-react-js": "17.0.2", "@microsoft/applicationinsights-web": "3.0.5", + "@microsoft/signalr": "^8.0.7", "@reduxjs/toolkit": "^1.9.7", "@testing-library/react": "13.4.0", "@testing-library/user-event": "13.5.0", diff --git a/UI/web-app/pnpm-lock.yaml b/UI/web-app/pnpm-lock.yaml index 5a1fe6786..4cbd6bf4c 100644 --- a/UI/web-app/pnpm-lock.yaml +++ b/UI/web-app/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@azure/msal-browser': specifier: 3.3.0 @@ -19,6 +23,9 @@ dependencies: '@microsoft/applicationinsights-web': specifier: 3.0.5 version: 3.0.5(tslib@2.6.2) + '@microsoft/signalr': + specifier: ^8.0.7 + version: 8.0.7 '@reduxjs/toolkit': specifier: ^1.9.7 version: 1.9.7(react-redux@8.1.3)(react@18.2.0) @@ -63,13 +70,13 @@ dependencies: version: 18.2.0(react@18.2.0) react-redux: specifier: ^8.1.3 - version: 8.1.3(@types/react-dom@18.0.10)(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0) + version: 8.1.3(@types/react-dom@18.0.10)(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) react-router-dom: specifier: ^6.17.0 version: 6.17.0(react-dom@18.2.0)(react@18.2.0) react-scripts: specifier: 5.0.1 - version: 5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(react@18.2.0)(typescript@4.9.4) + version: 5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.42.0)(react@18.2.0)(typescript@4.9.4) react-string-format: specifier: ^1.0.1 version: 1.0.1 @@ -2138,6 +2145,7 @@ packages: /@humanwhocodes/config-array@0.11.10: resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 1.2.1 debug: 4.3.4 @@ -2153,6 +2161,7 @@ packages: /@humanwhocodes/object-schema@1.2.1: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + deprecated: Use @eslint/object-schema instead dev: false /@istanbuljs/load-nyc-config@1.1.0: @@ -2612,6 +2621,20 @@ packages: resolution: {integrity: sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==} dev: false + /@microsoft/signalr@8.0.7: + resolution: {integrity: sha512-PHcdMv8v5hJlBkRHAuKG5trGViQEkPYee36LnJQx4xHOQ5LL4X0nEWIxOp5cCtZ7tu+30quz5V3k0b1YNuc6lw==} + dependencies: + abort-controller: 3.0.0 + eventsource: 2.0.2 + fetch-cookie: 2.2.0 + node-fetch: 2.7.0 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + dev: false + /@nevware21/ts-async@0.3.0: resolution: {integrity: sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==} dependencies: @@ -2702,7 +2725,7 @@ packages: dependencies: immer: 9.0.21 react: 18.2.0 - react-redux: 8.1.3(@types/react-dom@18.0.10)(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0) + react-redux: 8.1.3(@types/react-dom@18.0.10)(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) redux: 4.2.1 redux-thunk: 2.4.2(redux@4.2.1) reselect: 4.1.8 @@ -3561,6 +3584,13 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} dev: false + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -5583,6 +5613,11 @@ packages: engines: {node: '>= 0.6'} dev: false + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: false @@ -5592,6 +5627,11 @@ packages: engines: {node: '>=0.8.x'} dev: false + /eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + dev: false + /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -5715,6 +5755,13 @@ packages: bser: 2.1.1 dev: false + /fetch-cookie@2.2.0: + resolution: {integrity: sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==} + dependencies: + set-cookie-parser: 2.7.0 + tough-cookie: 4.1.2 + dev: false + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -7790,6 +7837,18 @@ packages: semver: 5.7.1 dev: true + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} @@ -9169,7 +9228,7 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - /react-redux@8.1.3(@types/react-dom@18.0.10)(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0): + /react-redux@8.1.3(@types/react-dom@18.0.10)(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1): resolution: {integrity: sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==} peerDependencies: '@types/react': ^16.8 || ^17.0 || ^18.0 @@ -9199,6 +9258,7 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-is: 18.2.0 + redux: 4.2.1 use-sync-external-store: 1.2.0(react@18.2.0) dev: false @@ -9230,11 +9290,12 @@ packages: react: 18.2.0 dev: false - /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(react@18.2.0)(typescript@4.9.4): + /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.42.0)(react@18.2.0)(typescript@4.9.4): resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} engines: {node: '>=14.0.0'} hasBin: true peerDependencies: + eslint: '*' react: '>= 16' typescript: ^3.2.1 || ^4 peerDependenciesMeta: @@ -9801,6 +9862,10 @@ packages: - supports-color dev: false + /set-cookie-parser@2.7.0: + resolution: {integrity: sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==} + dev: false + /setprototypeof@1.1.0: resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} dev: false @@ -10374,6 +10439,10 @@ packages: url-parse: 1.5.10 dev: false + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} dependencies: @@ -10683,6 +10752,10 @@ packages: resolution: {integrity: sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==} dev: false + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: false @@ -10861,6 +10934,13 @@ packages: resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} dev: false + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index 3f2246c04..c2a0bda4e 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -51,7 +51,7 @@ export const OperationBase: React.FunctionComponent = (props: Op handleOperation(Operations.Start)} - disabled={isButtonDisabled || displayStatus == ServiceStatuses.Starting} + disabled={isButtonDisabled || displayStatus === ServiceStatuses.Starting} className={classNames.button} /> ) : ( diff --git a/UI/web-app/src/index.tsx b/UI/web-app/src/index.tsx index 37ac18953..a5eb27d44 100644 --- a/UI/web-app/src/index.tsx +++ b/UI/web-app/src/index.tsx @@ -12,6 +12,7 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { App } from './App'; import { AdminConfig, JobsPage, JobDetails, OwnerPage, ManageMembership, NotFound } from './pages'; import { store } from './store'; +import './services/signalR/SignalRService'; const connectionString = process.env.REACT_APP_APPINSIGHTS_CONNECTIONSTRING; if (!connectionString || connectionString === '') { diff --git a/UI/web-app/src/services/signalR/SignalRService.ts b/UI/web-app/src/services/signalR/SignalRService.ts new file mode 100644 index 000000000..d2d4d284c --- /dev/null +++ b/UI/web-app/src/services/signalR/SignalRService.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as signalR from '@microsoft/signalr'; +import { store } from '../../store'; +import { updateServiceStatus } from '../../store/operations.slice'; +import { ServiceStatuses } from '../../models'; + +const connection = new signalR.HubConnectionBuilder() + .withUrl("https://localhost:7224/hub") // Change this back once done testing locally + .build(); + +connection.start() + .then(() => console.log('SignalR Connected')) + .catch((err: any) => console.error('SignalR Connection Error:', err)); + +connection.on("ServiceStatusChanged", (status: ServiceStatuses) => { + console.log("Service status changed:", status); + store.dispatch(updateServiceStatus(status)); +}); diff --git a/UI/web-app/src/services/signalR/index.ts b/UI/web-app/src/services/signalR/index.ts new file mode 100644 index 000000000..21374e303 --- /dev/null +++ b/UI/web-app/src/services/signalR/index.ts @@ -0,0 +1 @@ +export * from './SignalRService'; \ No newline at end of file diff --git a/UI/web-app/src/store/operations.slice.tsx b/UI/web-app/src/store/operations.slice.tsx index 5fb946cda..9f3fca6cc 100644 --- a/UI/web-app/src/store/operations.slice.tsx +++ b/UI/web-app/src/store/operations.slice.tsx @@ -27,6 +27,9 @@ const operationsSlice = createSlice({ resetError(state) { state.error = null; }, + updateServiceStatus: (state, action) => { + state.status = action.payload; + }, }, extraReducers: (builder) => { builder @@ -79,6 +82,7 @@ const operationsSlice = createSlice({ }); export const { resetError } = operationsSlice.actions; +export const { updateServiceStatus } = operationsSlice.actions; export const selectOperationStatus = (state: RootState) => state.operations.status; export const selectOperationDisplayStatus = (state: RootState) => state.operations.displayStatus; From 47989619677fb8daaf837af8f89f758a708b7a22 Mon Sep 17 00:00:00 2001 From: abgonz Date: Fri, 16 Aug 2024 15:21:24 -0700 Subject: [PATCH 0228/1479] Change to Azure SignalR service instead of self-hosted version --- .../WebApi/Services.WebApi/GetServiceStatusHandler.cs | 3 +++ .../Hosts/WebApi/WebApi/Program.cs | 9 ++++++--- .../Hosts/WebApi/WebApi/WebApi.csproj | 1 + UI/web-app/src/services/signalR/SignalRService.ts | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs index fbf118ce9..911137726 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; +using Microsoft.Graph.Models; using Models; using Repositories.Contracts; using Services.Contracts; @@ -33,6 +34,8 @@ await _loggingRepository.LogMessageAsync(new LogMessage Message = $"Retrieving service status." }); + await _hubContext.Clients.All.SendAsync("ServiceStatusChanged", "test"); + var currentStatus = await _serviceStatusRepository.GetCurrentServiceStatusAsync(); await _loggingRepository.LogMessageAsync(new LogMessage diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs index 893a510f8..075168666 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs @@ -325,7 +325,7 @@ public static void Main(string[] args) return settings.Value; }); - builder.Services.AddSignalR(); + builder.Services.AddSignalR().AddAzureSignalR(builder.Configuration["Settings:AzureSignalRConnectionString"]); builder.WebHost.ConfigureServices(services => { @@ -382,8 +382,6 @@ public static void Main(string[] args) app.UseStaticFiles(); app.UseRouting(); - app.MapHub("/hub"); - var allowedOrigins = new[] { "https://*.microsoft.com", "http://localhost:3000" }; app.UseCors(x => x .SetIsOriginAllowedToAllowWildcardSubdomains() @@ -398,6 +396,11 @@ public static void Main(string[] args) app.UseAuthentication(); app.UseAuthorization(); + app.UseEndpoints(routes => + { + routes.MapHub("/hub"); + }); + app.MapControllers(); app.Run(); diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj index 8193afbfd..67f66aec1 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/WebApi.csproj @@ -19,6 +19,7 @@ + diff --git a/UI/web-app/src/services/signalR/SignalRService.ts b/UI/web-app/src/services/signalR/SignalRService.ts index d2d4d284c..452f67679 100644 --- a/UI/web-app/src/services/signalR/SignalRService.ts +++ b/UI/web-app/src/services/signalR/SignalRService.ts @@ -7,7 +7,7 @@ import { updateServiceStatus } from '../../store/operations.slice'; import { ServiceStatuses } from '../../models'; const connection = new signalR.HubConnectionBuilder() - .withUrl("https://localhost:7224/hub") // Change this back once done testing locally + .withUrl("https://gmm-compute-ag-signalr.service.signalr.net") // Change this to be configurable .build(); connection.start() From 7aa0124ebd5fda4ae029804debc12d7038b186d9 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 16 Aug 2024 16:41:03 -0700 Subject: [PATCH 0229/1479] Updated signalr template Updated hub name --- .../WebApi/Infrastructure/compute/template.bicep | 12 ++++++++++-- .../Services.WebApi/GetServiceStatusHandler.cs | 10 +--------- .../Hosts/WebApi/Services.WebApi/SignalRService.cs | 8 ++------ .../Hosts/WebApi/WebApi/Program.cs | 2 +- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep index 50b84e3e5..021a1dbfe 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/template.bicep @@ -64,6 +64,9 @@ param adfPipeline string @description('Flag to indicate if the deployment should set RBAC permissions.') param setRBACPermissions bool = false +@description('Allowed origins for the SignalR service.') +param signalrCORS array = ['https://microsoft.com'] + var subscriptionId = subscription().subscriptionId var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var webapiClientId = resourceId(subscription().subscriptionId, prereqsResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'webapiClientId') @@ -249,7 +252,7 @@ resource graphUAMI 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31- scope: resourceGroup(dataResourceGroup) } -resource signalR 'Microsoft.SignalRService/signalR@2021-06-01-preview' = { +resource signalR 'Microsoft.SignalRService/signalR@2023-08-01-preview' = { name: '${solutionAbbreviation}-compute-${environmentAbbreviation}-signalr' location: location sku: { @@ -257,10 +260,15 @@ resource signalR 'Microsoft.SignalRService/signalR@2021-06-01-preview' = { tier: 'Standard' capacity: 1 } + kind: 'SignalR' + identity: { + type: 'SystemAssigned' + } properties: { cors: { - allowedOrigins: ['https://microsoft.com', 'http://localhost:3000'] + allowedOrigins: signalrCORS } + disableLocalAuth: true features: [ { flag: 'ServiceMode' diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs index 911137726..4c5b4f3e4 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetServiceStatusHandler.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.AspNetCore.SignalR; -using Microsoft.EntityFrameworkCore; -using Microsoft.Graph.Models; using Models; using Repositories.Contracts; using Services.Contracts; @@ -16,15 +13,12 @@ public class GetServiceStatusHandler : RequestHandlerBase _hubContext; public GetServiceStatusHandler(ILoggingRepository loggingRepository, - IServiceStatusRepository serviceStatusRepository, - IHubContext hubContext) : base(loggingRepository) + IServiceStatusRepository serviceStatusRepository) : base(loggingRepository) { _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); _serviceStatusRepository = serviceStatusRepository ?? throw new ArgumentNullException(nameof(serviceStatusRepository)); - _hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext)); } protected override async Task ExecuteCoreAsync(GetServiceStatusRequest request) @@ -34,8 +28,6 @@ await _loggingRepository.LogMessageAsync(new LogMessage Message = $"Retrieving service status." }); - await _hubContext.Clients.All.SendAsync("ServiceStatusChanged", "test"); - var currentStatus = await _serviceStatusRepository.GetCurrentServiceStatusAsync(); await _loggingRepository.LogMessageAsync(new LogMessage diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs index 48fda244c..f47b5c46a 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs @@ -1,12 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.SignalR; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; namespace Services.WebApi { @@ -14,6 +10,6 @@ namespace Services.WebApi public class SignalRService : Hub { public async Task NewMessage(long username, string message) => - await Clients.All.SendAsync("messageReceived", username, message); + await Clients.All.SendAsync("messageReceived", username, message); } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs index 075168666..8f1ccbbb2 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs @@ -398,7 +398,7 @@ public static void Main(string[] args) app.UseEndpoints(routes => { - routes.MapHub("/hub"); + routes.MapHub("/servicestatus"); }); app.MapControllers(); From 109cff905fbe22e2beaea98336b914e58f7260e7 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 19 Aug 2024 13:43:26 -0700 Subject: [PATCH 0230/1479] Updated SignalRService --- .../Hosts/WebApi/Services.WebApi/SignalRService.cs | 2 -- .../GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs index f47b5c46a..7e80a7faa 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/SignalRService.cs @@ -9,7 +9,5 @@ namespace Services.WebApi [ExcludeFromCodeCoverage] public class SignalRService : Hub { - public async Task NewMessage(long username, string message) => - await Clients.All.SendAsync("messageReceived", username, message); } } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs index 8f1ccbbb2..145a834b6 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Program.cs @@ -396,10 +396,7 @@ public static void Main(string[] args) app.UseAuthentication(); app.UseAuthorization(); - app.UseEndpoints(routes => - { - routes.MapHub("/servicestatus"); - }); + app.MapHub("/servicestatus"); app.MapControllers(); From 991fe6eba46e8714fffb68bc56ad8322d6c5af60 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 20 Aug 2024 11:15:50 -0700 Subject: [PATCH 0231/1479] Added SignalR to wait for reponse --- UI/web-app/.env | 3 +- .../components/Operation/Operation.base.tsx | 7 +++- UI/web-app/src/index.tsx | 1 - .../src/services/signalR/ISignalRService.ts | 7 ++++ .../src/services/signalR/SignalRService.ts | 20 ---------- .../services/signalR/SignalRStatusService.ts | 40 +++++++++++++++++++ UI/web-app/src/services/signalR/index.ts | 2 +- UI/web-app/src/store/operations.slice.tsx | 1 + 8 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 UI/web-app/src/services/signalR/ISignalRService.ts delete mode 100644 UI/web-app/src/services/signalR/SignalRService.ts create mode 100644 UI/web-app/src/services/signalR/SignalRStatusService.ts diff --git a/UI/web-app/.env b/UI/web-app/.env index 9827b14a6..1bde5ee10 100644 --- a/UI/web-app/.env +++ b/UI/web-app/.env @@ -4,4 +4,5 @@ REACT_APP_AAD_APP_TENANT_ID= REACT_APP_AAD_APP_SERVICE_BASE_URI= REACT_APP_APPINSIGHTS_CONNECTIONSTRING= REACT_APP_VERSION_NUMBER= -REACT_APP_ENVIRONMENT_ABBREVIATION= \ No newline at end of file +REACT_APP_ENVIRONMENT_ABBREVIATION= +REACT_APP_SIGNALR_SERVICE_BASE_URI= \ No newline at end of file diff --git a/UI/web-app/src/components/Operation/Operation.base.tsx b/UI/web-app/src/components/Operation/Operation.base.tsx index c2a0bda4e..95181dc15 100644 --- a/UI/web-app/src/components/Operation/Operation.base.tsx +++ b/UI/web-app/src/components/Operation/Operation.base.tsx @@ -8,6 +8,9 @@ import { AppDispatch } from '../../store'; import { ServiceStatuses } from '../../models/ServiceStatuses'; import { Operations } from '../../models/Operations'; import type { OperationProps, OperationStyles, OperationStyleProps } from './Operation.types'; +import { SignalRStatusService } from '../../services/signalR/SignalRStatusService'; + +const signalRStatusService = new SignalRStatusService(); export const getClassNames = classNamesFunction(); @@ -29,8 +32,10 @@ export const OperationBase: React.FunctionComponent = (props: Op }, [dispatch]); const handleOperation = (operation: Operations) => { - dispatch(processOperation(operation)) + signalRStatusService.startConnection().then(() => { + dispatch(processOperation(operation)) .then(() => dispatch(fetchServiceStatus())); + }); }; useEffect(() => { diff --git a/UI/web-app/src/index.tsx b/UI/web-app/src/index.tsx index a5eb27d44..37ac18953 100644 --- a/UI/web-app/src/index.tsx +++ b/UI/web-app/src/index.tsx @@ -12,7 +12,6 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { App } from './App'; import { AdminConfig, JobsPage, JobDetails, OwnerPage, ManageMembership, NotFound } from './pages'; import { store } from './store'; -import './services/signalR/SignalRService'; const connectionString = process.env.REACT_APP_APPINSIGHTS_CONNECTIONSTRING; if (!connectionString || connectionString === '') { diff --git a/UI/web-app/src/services/signalR/ISignalRService.ts b/UI/web-app/src/services/signalR/ISignalRService.ts new file mode 100644 index 000000000..960d2d3e2 --- /dev/null +++ b/UI/web-app/src/services/signalR/ISignalRService.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export interface ISignalRService { + startConnection: () => void; + stopConnection: () => void; +} \ No newline at end of file diff --git a/UI/web-app/src/services/signalR/SignalRService.ts b/UI/web-app/src/services/signalR/SignalRService.ts deleted file mode 100644 index 452f67679..000000000 --- a/UI/web-app/src/services/signalR/SignalRService.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import * as signalR from '@microsoft/signalr'; -import { store } from '../../store'; -import { updateServiceStatus } from '../../store/operations.slice'; -import { ServiceStatuses } from '../../models'; - -const connection = new signalR.HubConnectionBuilder() - .withUrl("https://gmm-compute-ag-signalr.service.signalr.net") // Change this to be configurable - .build(); - -connection.start() - .then(() => console.log('SignalR Connected')) - .catch((err: any) => console.error('SignalR Connection Error:', err)); - -connection.on("ServiceStatusChanged", (status: ServiceStatuses) => { - console.log("Service status changed:", status); - store.dispatch(updateServiceStatus(status)); -}); diff --git a/UI/web-app/src/services/signalR/SignalRStatusService.ts b/UI/web-app/src/services/signalR/SignalRStatusService.ts new file mode 100644 index 000000000..8113eeb4c --- /dev/null +++ b/UI/web-app/src/services/signalR/SignalRStatusService.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as signalR from '@microsoft/signalr'; +import { ISignalRService } from './ISignalRService'; +import { store } from '../../store'; +import { updateServiceStatus } from '../../store/operations.slice'; +import { ServiceStatuses } from '../../models'; + +export class SignalRStatusService implements ISignalRService { + private _connection: signalR.HubConnection | null = null; + + public startConnection() { + if (!this._connection) { + this._connection = new signalR.HubConnectionBuilder() + .withUrl(`${process.env.REACT_APP_SIGNALR_SERVICE_BASE_URI}/servicestatus`) + .withAutomaticReconnect() + .build(); + + const startPromise = this._connection.start() + .then(() => console.log('SignalR Connected')) + .catch((err: any) => console.error('SignalR Connection Error:', err)); + + this._connection.on("ServiceStatusChanged", (status: ServiceStatuses) => { + console.log("Service status changed:", status); + store.dispatch(updateServiceStatus(status)); + }); + + return startPromise; + } else + return Promise.resolve(); + } + + public stopConnection() { + if (this._connection) { + this._connection.stop(); + this._connection = null; + } + } +} diff --git a/UI/web-app/src/services/signalR/index.ts b/UI/web-app/src/services/signalR/index.ts index 21374e303..759816cb0 100644 --- a/UI/web-app/src/services/signalR/index.ts +++ b/UI/web-app/src/services/signalR/index.ts @@ -1 +1 @@ -export * from './SignalRService'; \ No newline at end of file +export * from './SignalRStatusService'; \ No newline at end of file diff --git a/UI/web-app/src/store/operations.slice.tsx b/UI/web-app/src/store/operations.slice.tsx index 9f3fca6cc..edbfde567 100644 --- a/UI/web-app/src/store/operations.slice.tsx +++ b/UI/web-app/src/store/operations.slice.tsx @@ -29,6 +29,7 @@ const operationsSlice = createSlice({ }, updateServiceStatus: (state, action) => { state.status = action.payload; + state.displayStatus = action.payload; }, }, extraReducers: (builder) => { From f737b3b16b83f9d99760b3269c9b1f2cf0fa217e Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 20 Aug 2024 11:19:56 -0700 Subject: [PATCH 0232/1479] Added SignalR endpoint setting to UI --- yaml/deploy-webapp.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/yaml/deploy-webapp.yml b/yaml/deploy-webapp.yml index 9f43c986d..a337327db 100644 --- a/yaml/deploy-webapp.yml +++ b/yaml/deploy-webapp.yml @@ -79,6 +79,8 @@ steps: REACT_APP_SHAREPOINTDOMAIN: $(sharepoint_domain) REACT_APP_DOMAINNAME: $(tenant_domain) REACT_APP_ENVIRONMENT_ABBREVIATION: ${{parameters.environmentAbbreviation}} + REACT_APP_SIGNALR_SERVICE_BASE_URI: https://${{parameters.solutionAbbreviation}}-compute-${{parameters.environmentAbbreviation}}-signalr.service.signalr.net + - task: AzureStaticWebApp@0 name: DeployStaticWebApp From 1344228d2dea825b29dcc21688d7a41acb0fd94d Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 20 Aug 2024 13:25:57 -0700 Subject: [PATCH 0233/1479] Added new postdeployment script --- .../Set-PostDeploymentRoles.ps1 | 8 ++ ...geAccountContainerManagedIdentityRoles.ps1 | 23 ---- .../PostDeployment/Set-WebAPIAccessRoles.ps1 | 111 ++++++++++++++++++ 3 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 Scripts/PostDeployment/Set-WebAPIAccessRoles.ps1 diff --git a/Scripts/PostDeployment/Set-PostDeploymentRoles.ps1 b/Scripts/PostDeployment/Set-PostDeploymentRoles.ps1 index e85fde25e..29e2123d6 100644 --- a/Scripts/PostDeployment/Set-PostDeploymentRoles.ps1 +++ b/Scripts/PostDeployment/Set-PostDeploymentRoles.ps1 @@ -82,4 +82,12 @@ function Set-PostDeploymentRoles { -SolutionAbbreviation $SolutionAbbreviation ` -EnvironmentAbbreviation $EnvironmentAbbreviation ` -Verbose + + . ($scriptsDirectory + '\PostDeployment\Set-WebAPIAccessRoles.ps1') + Set-WebAPIAccessRoles ` + -SolutionAbbreviation $SolutionAbbreviation ` + -EnvironmentAbbreviation $EnvironmentAbbreviation ` + -ComputeResourceGroupName $ComputeResourceGroupName ` + -DataResourceGroupName $DataResourceGroupName ` + -Verbose } \ No newline at end of file diff --git a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 index cb693da85..aab788210 100644 --- a/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-StorageAccountContainerManagedIdentityRoles.ps1 @@ -104,28 +104,5 @@ function Set-StorageAccountContainerManagedIdentityRoles } } - $currentSubscription = (Get-AzContext).Subscription - $resourceGroups = @("$SolutionAbbreviation-data-$EnvironmentAbbreviation","$SolutionAbbreviation-compute-$EnvironmentAbbreviation") - - foreach ($resourceGroup in $resourceGroups) { - $scope = "/subscriptions/$($currentSubscription.Id)/resourceGroups/$resourceGroup" - if ($null -eq (Get-AzRoleAssignment -ObjectId $webApiSP -Scope $scope -RoleDefinitionName "Reader")) { - New-AzRoleAssignment -ObjectId $webApiSP -Scope $scope -RoleDefinitionName "Reader"; - Write-Host "Added role assignment Reader to $($webApi.Name) with scope $scope."; - } - else { - Write-Host "$($webApi.Name) can already Reader with scope $scope."; - } - } - - $scope = "/subscriptions/$($currentSubscription.Id)/resourceGroups/$SolutionAbbreviation-compute-$EnvironmentAbbreviation" - if ($null -eq (Get-AzRoleAssignment -ObjectId $webApiSP -Scope $scope -RoleDefinitionName "Website Contributor")) { - New-AzRoleAssignment -ObjectId $webApiSP -Scope $scope -RoleDefinitionName "Website Contributor"; - Write-Host "Added role assignment Website Contributor to $($webApi.Name) with scope $scope."; - } - else { - Write-Host "$($webApi.Name) can already Website Contributor with scope $scope."; - } - Write-Host "Done attempting to add Storage role assignments."; } \ No newline at end of file diff --git a/Scripts/PostDeployment/Set-WebAPIAccessRoles.ps1 b/Scripts/PostDeployment/Set-WebAPIAccessRoles.ps1 new file mode 100644 index 000000000..8d6cbafc1 --- /dev/null +++ b/Scripts/PostDeployment/Set-WebAPIAccessRoles.ps1 @@ -0,0 +1,111 @@ +<# +.SYNOPSIS +Grants the Web API access to the necessary resources. + +.DESCRIPTION +This should be run by an owner on the subscription after the storage account and app service have been set up. + +.PARAMETER SolutionAbbreviation +The abbreviation for your solution. + +.PARAMETER EnvironmentAbbreviation +A 2-6 character abbreviation for your environment. + +.PARAMETER ErrorActionPreference +Parameter description + +.EXAMPLE +Set-WebAPIAccessRoles -SolutionAbbreviation "" ` + -EnvironmentAbbreviation "" ` + -Verbose +#> + +function Set-WebAPIAccessRoles { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True)] + [string] $SolutionAbbreviation, + [Parameter(Mandatory = $True)] + [string] $EnvironmentAbbreviation, + [Parameter(Mandatory = $False)] + [string] $DataResourceGroupName = $null, + [Parameter(Mandatory = $False)] + [string] $ComputeResourceGroupName = $null, + [Parameter(Mandatory = $False)] + [string] $ErrorActionPreference = $Stop + ) + + Write-Host "Granting RBACs to WebAPI"; + + if ([string]::IsNullOrEmpty($ComputeResourceGroupName)) { + $ComputeResourceGroupName = "$SolutionAbbreviation-compute-$EnvironmentAbbreviation"; + } + + if ([string]::IsNullOrEmpty($DataResourceGroupName)) { + $DataResourceGroupName = "$SolutionAbbreviation-data-$EnvironmentAbbreviation"; + } + + $currentSubscription = (Get-AzContext).Subscription + $webApi = Get-AzWebApp -ResourceGroupName $ComputeResourceGroupName -Name "$ComputeResourceGroupName-webapi" + + if ($webApi) { + $webApiServicePrincipal = Get-AzADServicePrincipal -DisplayName $webApi.Name + + if ($webApiServicePrincipal) { + + Set-RoleAssignment ` + -ObjectId $webApiServicePrincipal.Id ` + -DisplayName $webApi.Name ` + -Scope "/subscriptions/$($currentSubscription.Id)/resourceGroups/$ComputeResourceGroupName" ` + -RoleDefinitionName "Website Contributor" + + Set-RoleAssignment ` + -ObjectId $webApiServicePrincipal.Id ` + -DisplayName $webApi.Name ` + -Scope "/subscriptions/$($currentSubscription.Id)/resourceGroups/$DataResourceGroupName" ` + -RoleDefinitionName "Reader" + + Set-RoleAssignment ` + -ObjectId $webApiServicePrincipal.Id ` + -DisplayName $webApi.Name ` + -Scope "/subscriptions/$($currentSubscription.Id)/resourceGroups/$ComputeResourceGroupName" ` + -RoleDefinitionName "Reader" + + $signalRResource = Get-AzResource -ResourceGroupName $ComputeResourceGroupName -ResourceType "Microsoft.SignalRService/SignalR" -Name "$ComputeResourceGroupName-signalr" + Set-RoleAssignment ` + -ObjectId $webApiServicePrincipal.Id ` + -DisplayName $webApi.Name ` + -Scope $signalRResource.Id ` + -RoleDefinitionName "SignalR App Server" + + + } + elseif ($null -eq $webApiServicePrincipal) { + Write-Host "Web API $($webApi.Name) was not found!" + } + } + + Write-Host "Done."; +} + +function Set-RoleAssignment { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True)] + [string] $ObjectId, + [Parameter(Mandatory = $True)] + [string] $DisplayName, + [Parameter(Mandatory = $True)] + [string] $Scope, + [Parameter(Mandatory = $True)] + [string] $RoleDefinitionName + ) + + if ($null -eq (Get-AzRoleAssignment -ObjectId $ObjectId -Scope $Scope -RoleDefinitionName $RoleDefinitionName)) { + New-AzRoleAssignment -ObjectId $ObjectId -Scope $Scope -RoleDefinitionName $RoleDefinitionName; + Write-Host "Added role $RoleDefinitionName to $DisplayName to $Scope."; + } + else { + Write-Host "$DisplayName already has $RoleDefinitionName role for $Scope."; + } +} \ No newline at end of file From 4097f23839d89c75b9672e67cc17b1e748632b3b Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 20 Aug 2024 14:08:26 -0700 Subject: [PATCH 0234/1479] Added signalr base uri to script --- Deployment/Deploy-Resources.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Deployment/Deploy-Resources.ps1 b/Deployment/Deploy-Resources.ps1 index 7451899cd..91bf02c23 100644 --- a/Deployment/Deploy-Resources.ps1 +++ b/Deployment/Deploy-Resources.ps1 @@ -1149,6 +1149,7 @@ function Set-PublishUICode { $envContent += "REACT_APP_ENVIRONMENT_ABBREVIATION=$EnvironmentAbbreviation`n" $envContent += "AZURE_SUBSCRIPTION_ID=$SubscriptionId`n" $envContent += "AZURE_TENANT_ID=$MainTenantId`n" + $envContent += "REACT_APP_SIGNALR_SERVICE_BASE_URI=https://$SolutionAbbreviation-compute-$EnvironmentAbbreviation-signalr.service.signalr.net`n" Set-Content -Path "$WebAppDirectory\.env" -Value $envContent -Force $currentLocation = Get-Location From 915eb16659a1bafd1fa7b29701937a2ae039cf71 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 21 Aug 2024 10:32:28 -0700 Subject: [PATCH 0235/1479] Updated pnpm run build task --- yaml/deploy-webapp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yaml/deploy-webapp.yml b/yaml/deploy-webapp.yml index a337327db..e846956e3 100644 --- a/yaml/deploy-webapp.yml +++ b/yaml/deploy-webapp.yml @@ -67,7 +67,7 @@ steps: displayName: 'pnpm run build' inputs: targetType: 'inline' - script: pnpm install && pnpm run build + script: pnpm install --no-frozen-lockfile && pnpm run build workingDirectory: '${{ parameters.root }}/webapp_package/web-app' env: REACT_APP_AAD_UI_APP_CLIENT_ID: $(ui_app_client_id) From ffcfb000ebfde404b117d818db8ce41e1522948e Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 21 Aug 2024 13:37:52 -0700 Subject: [PATCH 0236/1479] Removed REACT_APP_SIGNALR_SERVICE_BASE_URI --- Deployment/Deploy-Resources.ps1 | 1 - UI/web-app/.env | 3 +-- UI/web-app/src/services/signalR/SignalRStatusService.ts | 6 ++++-- yaml/deploy-webapp.yml | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Deployment/Deploy-Resources.ps1 b/Deployment/Deploy-Resources.ps1 index 91bf02c23..7451899cd 100644 --- a/Deployment/Deploy-Resources.ps1 +++ b/Deployment/Deploy-Resources.ps1 @@ -1149,7 +1149,6 @@ function Set-PublishUICode { $envContent += "REACT_APP_ENVIRONMENT_ABBREVIATION=$EnvironmentAbbreviation`n" $envContent += "AZURE_SUBSCRIPTION_ID=$SubscriptionId`n" $envContent += "AZURE_TENANT_ID=$MainTenantId`n" - $envContent += "REACT_APP_SIGNALR_SERVICE_BASE_URI=https://$SolutionAbbreviation-compute-$EnvironmentAbbreviation-signalr.service.signalr.net`n" Set-Content -Path "$WebAppDirectory\.env" -Value $envContent -Force $currentLocation = Get-Location diff --git a/UI/web-app/.env b/UI/web-app/.env index 1bde5ee10..9827b14a6 100644 --- a/UI/web-app/.env +++ b/UI/web-app/.env @@ -4,5 +4,4 @@ REACT_APP_AAD_APP_TENANT_ID= REACT_APP_AAD_APP_SERVICE_BASE_URI= REACT_APP_APPINSIGHTS_CONNECTIONSTRING= REACT_APP_VERSION_NUMBER= -REACT_APP_ENVIRONMENT_ABBREVIATION= -REACT_APP_SIGNALR_SERVICE_BASE_URI= \ No newline at end of file +REACT_APP_ENVIRONMENT_ABBREVIATION= \ No newline at end of file diff --git a/UI/web-app/src/services/signalR/SignalRStatusService.ts b/UI/web-app/src/services/signalR/SignalRStatusService.ts index 8113eeb4c..38791d257 100644 --- a/UI/web-app/src/services/signalR/SignalRStatusService.ts +++ b/UI/web-app/src/services/signalR/SignalRStatusService.ts @@ -11,9 +11,11 @@ export class SignalRStatusService implements ISignalRService { private _connection: signalR.HubConnection | null = null; public startConnection() { - if (!this._connection) { + if (!this._connection + || (this._connection && (this._connection.state !== signalR.HubConnectionState.Connected + && this._connection.state !== signalR.HubConnectionState.Connecting))) { this._connection = new signalR.HubConnectionBuilder() - .withUrl(`${process.env.REACT_APP_SIGNALR_SERVICE_BASE_URI}/servicestatus`) + .withUrl(`${process.env.REACT_APP_AAD_APP_SERVICE_BASE_URI}/servicestatus`) .withAutomaticReconnect() .build(); diff --git a/yaml/deploy-webapp.yml b/yaml/deploy-webapp.yml index e846956e3..a22e3e7ae 100644 --- a/yaml/deploy-webapp.yml +++ b/yaml/deploy-webapp.yml @@ -79,7 +79,6 @@ steps: REACT_APP_SHAREPOINTDOMAIN: $(sharepoint_domain) REACT_APP_DOMAINNAME: $(tenant_domain) REACT_APP_ENVIRONMENT_ABBREVIATION: ${{parameters.environmentAbbreviation}} - REACT_APP_SIGNALR_SERVICE_BASE_URI: https://${{parameters.solutionAbbreviation}}-compute-${{parameters.environmentAbbreviation}}-signalr.service.signalr.net - task: AzureStaticWebApp@0 From b50b85fd89c12beaa052ea956b5fa94452cbda85 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 21 Aug 2024 15:21:40 -0700 Subject: [PATCH 0237/1479] Updated WebAPI to support CORS credentials --- .../Hosts/WebApi/Infrastructure/compute/appService.bicep | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/appService.bicep b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/appService.bicep index 61eddae56..d015e3c86 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/appService.bicep +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Infrastructure/compute/appService.bicep @@ -42,6 +42,11 @@ resource websiteTemplate 'Microsoft.Web/sites@2022-03-01' = { httpsOnly: true reserved: false serverFarmId: resourceId('Microsoft.Web/serverfarms', servicePlanName) + siteConfig: { + cors: { + supportCredentials: true + } + } } identity: { type: deployUserManagedIdentity ? 'SystemAssigned, UserAssigned' : 'SystemAssigned' From 41aca92384a00311c0bc9e0fbc6966af3914e91e Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 21 Aug 2024 15:37:09 -0700 Subject: [PATCH 0238/1479] Updated documentation --- UI/Documentation/UISetup.md | 1 + 1 file changed, 1 insertion(+) diff --git a/UI/Documentation/UISetup.md b/UI/Documentation/UISetup.md index e3e4edcd2..98d94a3f7 100644 --- a/UI/Documentation/UISetup.md +++ b/UI/Documentation/UISetup.md @@ -48,6 +48,7 @@ Add the following variables to your environment parameter file located in `/Serv * Go to the static web app in your compute resource group. You should see a URL in the Overview page. Copy that URL. * Go to ``-ui-`` application in Microsoft Entra ID -> `Authentication` -> Add that URL as a Redirect URI -> Click `Save` * Go to ``-compute-``-webapi in your compute resource group -> CORS under API -> Add that URL as `Allowed Origins` -> Click `Save` + * Go to the SignalR resource ``-compute-``-signalr, it is located in your compute resource group, under the `Settings` -> `CORS` -> Add that URL as `Allowed Origins` -> Click `Save`. ### Run UI locally From 196a98bbd734bf5ea864b62c1eaeacd171a73b2d Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 21 Aug 2024 15:55:03 -0700 Subject: [PATCH 0239/1479] Updated deployment script --- Deployment/Deploy-Resources.ps1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Deployment/Deploy-Resources.ps1 b/Deployment/Deploy-Resources.ps1 index 7451899cd..73d0d1ceb 100644 --- a/Deployment/Deploy-Resources.ps1 +++ b/Deployment/Deploy-Resources.ps1 @@ -1043,7 +1043,7 @@ function Set-ConfigureWebApps { try { $customDomain = Get-AzStaticWebAppCustomDomain -Name $UIWebAppName -ResourceGroupName $ComputeResourceGroup if (-not [string]::IsNullOrEmpty($customDomain)) { - $allowedOrigins += $customDomain.HostName + $allowedOrigins += "https://$($customDomain.DomainName)" } } catch { @@ -1053,6 +1053,17 @@ function Set-ConfigureWebApps { $staticWebApp = Get-AzStaticWebApp -Name $UIWebAppName -ResourceGroupName $ComputeResourceGroup $allowedOrigins += "https://$($staticWebApp.DefaultHostname)" + # Set CORS for SignalR service + try { + Update-AzSignalR ` + -ResourceGroupName $ComputeResourceGroup ` + -Name "$ComputeResourceGroup-signalr" ` + -AllowedOrigin $allowedOrigins + } + catch { + Write-Output "Unable to update SignalR service CORS settings." + } + $webApi = Get-AzWebApp -ResourceGroupName $ComputeResourceGroup -Name $WebApiName $currentCORs = $webApi.SiteConfig.Cors.AllowedOrigins $newCORs = @() From 5f951bcb6ab150145a3302dc31cab5a4762f8207 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 28 Aug 2024 11:05:42 -0700 Subject: [PATCH 0240/1479] Fix syncjobupdaer global build --- .../Function/SyncJobUpdater.csproj | 3 ++- .../Function/SyncJobUpdater.sln | 12 +++++----- .../Services.SyncJobUpdater.Tests.csproj | 3 ++- ...Tests.cs => SyncJobUpdaterServiceTests.cs} | 4 ++-- .../SyncJobUpdater/Services/Services.csproj | 8 ++----- .../Services/SyncJobUpdaterService.csproj | 22 ------------------- 6 files changed, 14 insertions(+), 38 deletions(-) rename Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/{JobFinalizerServiceTests.cs => SyncJobUpdaterServiceTests.cs} (98%) delete mode 100644 Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/SyncJobUpdaterService.csproj diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj index d485a124f..06aee19cb 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj @@ -17,7 +17,7 @@ - + @@ -29,3 +29,4 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.sln b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.sln index c622caa4a..061fd3552 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.sln +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SyncJobUpdater", "SyncJobUp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.FunctionBase", "..\..\..\Hosts.FunctionBase\Hosts.FunctionBase.csproj", "{A54477AB-BD0E-492F-91F1-C70EB4FEDB11}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SyncJobUpdaterService", "..\Services\SyncJobUpdaterService.csproj", "{B84FE7BA-3798-4287-8356-6FAD64166217}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Contracts", "..\Services.Contracts\Services.Contracts.csproj", "{9D98B058-0BF5-40C3-86FD-075E1A2E38BB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Contracts", "..\..\..\Repositories.Contracts\Repositories.Contracts.csproj", "{DC27F5A3-305C-4B89-B339-5058490DC870}" @@ -17,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\M EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.SyncJobUpdater.Tests", "..\Services.SyncJobUpdater.Tests\Services.SyncJobUpdater.Tests.csproj", "{DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services", "..\Services\Services.csproj", "{1C0B2B77-6100-493A-ADE3-58C0234C9908}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,10 +31,6 @@ Global {A54477AB-BD0E-492F-91F1-C70EB4FEDB11}.Debug|Any CPU.Build.0 = Debug|Any CPU {A54477AB-BD0E-492F-91F1-C70EB4FEDB11}.Release|Any CPU.ActiveCfg = Release|Any CPU {A54477AB-BD0E-492F-91F1-C70EB4FEDB11}.Release|Any CPU.Build.0 = Release|Any CPU - {B84FE7BA-3798-4287-8356-6FAD64166217}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B84FE7BA-3798-4287-8356-6FAD64166217}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B84FE7BA-3798-4287-8356-6FAD64166217}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B84FE7BA-3798-4287-8356-6FAD64166217}.Release|Any CPU.Build.0 = Release|Any CPU {9D98B058-0BF5-40C3-86FD-075E1A2E38BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9D98B058-0BF5-40C3-86FD-075E1A2E38BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {9D98B058-0BF5-40C3-86FD-075E1A2E38BB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -51,6 +47,10 @@ Global {DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}.Debug|Any CPU.Build.0 = Debug|Any CPU {DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}.Release|Any CPU.ActiveCfg = Release|Any CPU {DFDF0FF1-54E0-4F16-B1A2-7FD0474FB2C8}.Release|Any CPU.Build.0 = Release|Any CPU + {1C0B2B77-6100-493A-ADE3-58C0234C9908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C0B2B77-6100-493A-ADE3-58C0234C9908}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C0B2B77-6100-493A-ADE3-58C0234C9908}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C0B2B77-6100-493A-ADE3-58C0234C9908}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj index 848a4f81e..4f3024ef0 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj @@ -26,6 +26,7 @@ + + - diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/SyncJobUpdaterServiceTests.cs similarity index 98% rename from Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs rename to Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/SyncJobUpdaterServiceTests.cs index 0d6b74a84..c5b0fbd69 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/JobFinalizerServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/SyncJobUpdaterServiceTests.cs @@ -6,9 +6,9 @@ using Moq; using Models; using Repositories.Contracts; -using Hosts.SyncJobUpdater; -using Services.Tests; using Services.SyncJobUpdater.Tests.Mocks; +using Services.Contracts; +using Hosts.SyncJobUpdater; namespace Services.Tests { diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/Services.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/Services.csproj index b048e3b8d..c5cf0f1b4 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/Services.csproj @@ -1,16 +1,12 @@  - net6.0 + net8.0 - - - - - + diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/SyncJobUpdaterService.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/SyncJobUpdaterService.csproj deleted file mode 100644 index 48aa835c6..000000000 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services/SyncJobUpdaterService.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - net8.0 - - - - - - - - - - - - - - PreserveNewest - - - - From 1dcec59fe80ece7d973b12f39adb3336dcb35802 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 28 Aug 2024 11:14:24 -0700 Subject: [PATCH 0241/1479] Remove reference --- .../Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj | 3 +-- .../Services.SyncJobUpdater.Tests.csproj | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj index 06aee19cb..14766ab9d 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj @@ -28,5 +28,4 @@ Never - - + \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj index 4f3024ef0..1436d35b4 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/Services.SyncJobUpdater.Tests.csproj @@ -26,7 +26,6 @@ - From 90a88f705cc51414f7d121f6592e6594cec40219 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 26 Aug 2024 13:27:59 -0700 Subject: [PATCH 0242/1479] Removed all references to syncDisabledCCAddresses and syncCompletedCCAddresses so that only supportEmailAddresses is used across all GMM emails --- Deployment/commonResources.bicep | 4 -- Deployment/localTemplate.bicep | 4 -- Deployment/parameters.json | 14 +------ Deployment/prereqResources.bicep | 10 ----- Scripts/Set-SenderRecipientCredentials.ps1 | 42 ------------------- .../DIConcreteTypes/EmailSenderRecipient.cs | 12 ++---- .../Hosts.FunctionBase/CommonProgramBase.cs | 4 -- .../Hosts.FunctionBase/CommonStartup.cs | 4 -- .../Infrastructure/compute/template.bicep | 2 - .../GraphUpdater/Function/local.settings.json | 2 - .../Infrastructure/compute/template.bicep | 4 -- .../Function/local.settings.json | 1 - .../Infrastructure/compute/template.bicep | 2 - .../Function/local.settings.json | 1 - .../Infrastructure/compute/template.bicep | 2 - .../JobTrigger/Function/local.settings.json | 1 - .../Infrastructure/compute/template.bicep | 2 - .../Services.Tests/JobTriggerServiceTests.cs | 12 ++---- .../Function/local.settings.json | 2 - .../Infrastructure/compute/template.bicep | 4 -- .../Function/local.settings.json | 1 - .../Infrastructure/compute/template.bicep | 2 - .../Infrastructure/compute/template.bicep | 2 - .../Function/local.settings.json | 3 -- .../Infrastructure/compute/template.bicep | 2 - .../Function/local.settings.json | 1 - .../Infrastructure/compute/template.bicep | 2 - .../Infrastructure/compute/template.bicep | 2 - .../InjectConfig/IEmailSenderRecipient.cs | 2 - .../SetSenderAddressForEmailNotification.md | 4 -- .../Repositories.Mocks/MockMailRepository.cs | 4 -- 31 files changed, 9 insertions(+), 145 deletions(-) diff --git a/Deployment/commonResources.bicep b/Deployment/commonResources.bicep index e1087c8ca..db792b404 100644 --- a/Deployment/commonResources.bicep +++ b/Deployment/commonResources.bicep @@ -18,8 +18,6 @@ param isMailApplicationPermissionGranted bool param senderPassword string param senderUsername string param supportEmailAddresses string -param syncDisabledCCEmailAddresses string -param syncCompletedCCEmailAddresses string param teamsChannelServiceAccountObjectId string @secure() param teamsChannelServiceAccountPassword string @@ -72,8 +70,6 @@ module prereqResources 'prereqResources.bicep' = { senderPassword: senderPassword senderUsername: senderUsername supportEmailAddresses: supportEmailAddresses - syncCompletedCCEmailAddresses: syncCompletedCCEmailAddresses - syncDisabledCCEmailAddresses: syncDisabledCCEmailAddresses teamsChannelServiceAccountObjectId: teamsChannelServiceAccountObjectId teamsChannelServiceAccountPassword: teamsChannelServiceAccountPassword teamsChannelServiceAccountUsername: teamsChannelServiceAccountUsername diff --git a/Deployment/localTemplate.bicep b/Deployment/localTemplate.bicep index 639949b91..1738b5342 100644 --- a/Deployment/localTemplate.bicep +++ b/Deployment/localTemplate.bicep @@ -26,8 +26,6 @@ param certificateName string = 'not-set' param senderPassword string param senderUsername string param supportEmailAddresses string -param syncDisabledCCEmailAddresses string -param syncCompletedCCEmailAddresses string param teamsChannelServiceAccountObjectId string @secure() param teamsChannelServiceAccountPassword string @@ -89,8 +87,6 @@ module gmmResources 'commonResources.bicep' = { senderPassword: senderPassword senderUsername: senderUsername supportEmailAddresses: supportEmailAddresses - syncDisabledCCEmailAddresses: syncDisabledCCEmailAddresses - syncCompletedCCEmailAddresses: syncCompletedCCEmailAddresses teamsChannelServiceAccountObjectId: teamsChannelServiceAccountObjectId teamsChannelServiceAccountPassword: teamsChannelServiceAccountPassword teamsChannelServiceAccountUsername: teamsChannelServiceAccountUsername diff --git a/Deployment/parameters.json b/Deployment/parameters.json index 999a47468..06778ddee 100644 --- a/Deployment/parameters.json +++ b/Deployment/parameters.json @@ -5,7 +5,7 @@ "skipMailNotifications": { "value": false, "metadata": { - "description": "If you don't want to send email notifications set this to true. When this is true, 'senderUsername','senderPassword','supportEmailAddresses','syncDisabledCCEmailAddresses','syncCompletedCCEmailAddresses' should be set to 'not-set'." + "description": "If you don't want to send email notifications set this to true. When this is true, 'senderUsername','senderPassword','supportEmailAddresses' should be set to 'not-set'." } }, "isMailApplicationPermissionGranted": { @@ -32,18 +32,6 @@ "description": "The email addresses of the users that will receive support emails." } }, - "syncDisabledCCEmailAddresses": { - "value": "", - "metadata": { - "description": "The email addresses of the users that will receive emails when a sync job is disabled." - } - }, - "syncCompletedCCEmailAddresses": { - "value": "", - "metadata": { - "description": "The email addresses of the users that will receive emails when a sync job is completed." - } - }, "teamsChannelServiceAccountObjectId": { "value": "", "metadata": { diff --git a/Deployment/prereqResources.bicep b/Deployment/prereqResources.bicep index cd53468c7..ef3d84275 100644 --- a/Deployment/prereqResources.bicep +++ b/Deployment/prereqResources.bicep @@ -9,8 +9,6 @@ param tenantId string param senderPassword string param senderUsername string param supportEmailAddresses string -param syncDisabledCCEmailAddresses string -param syncCompletedCCEmailAddresses string param teamsChannelServiceAccountObjectId string @secure() param teamsChannelServiceAccountPassword string @@ -46,14 +44,6 @@ module prereqsScretsTemplate '../Infrastructure/data/keyVaultSecretsSecure.bicep name: 'supportEmailAddresses' value: supportEmailAddresses } - { - name: 'syncDisabledCCEmailAddresses' - value: syncDisabledCCEmailAddresses - } - { - name: 'syncCompletedCCEmailAddresses' - value: syncCompletedCCEmailAddresses - } { name: 'teamsChannelServiceAccountObjectId' value: teamsChannelServiceAccountObjectId diff --git a/Scripts/Set-SenderRecipientCredentials.ps1 b/Scripts/Set-SenderRecipientCredentials.ps1 index e148f9ff1..b8f2c2a25 100644 --- a/Scripts/Set-SenderRecipientCredentials.ps1 +++ b/Scripts/Set-SenderRecipientCredentials.ps1 @@ -18,12 +18,6 @@ Sender Username .PARAMETER SenderPassword Sender Password -.PARAMETER SyncCompletedCCEmailAddresses -Comma separated list of email addresseses of secondary recipients to an email when the sync is complete, eg: abc@tenant.com, def@tenant.com - -.PARAMETER SyncDisabledCCEmailAddresses -Comma separated list of email addresseses of secondary recipients to an email when the sync is disabled, eg: abc@tenant.com, def@tenant.com - .PARAMETER SupportEmailAddresses Comma separated list of email addresseses of secondary recipients providing technical support, eg: abc@tenant.com, def@tenant.com @@ -31,8 +25,6 @@ Comma separated list of email addresseses of secondary recipients providing tech $secureSenderUsername = ConvertTo-SecureString -AsPlainText -Force "" $secureSecurePassword = ConvertTo-SecureString -AsPlainText -Force "" -$secureSyncCompletedCCEmailAddresses = ConvertTo-SecureString -AsPlainText -Force "" -$secureSyncDisabledCCEmailAddresses = ConvertTo-SecureString -AsPlainText -Force "" $secureSupportEmailAddresses = ConvertTo-SecureString -AsPlainText -Force "" Set-SenderRecipientCredentials -SubscriptionName "" ` @@ -40,8 +32,6 @@ Set-SenderRecipientCredentials -SubscriptionName "" ` -EnvironmentAbbreviation "" ` -SecureSenderUsername $secureSenderUsername ` -SecureSenderPassword $secureSecurePassword ` - -SecureSyncCompletedCCEmailAddresses $secureSyncCompletedCCEmailAddresses ` - -SecureSyncDisabledCCEmailAddresses $secureSyncDisabledCCEmailAddresses ` -SecureSupportEmailAddresses $secureSupportEmailAddresses ` -GmmGraphAppHasMailApplicationPermissions $false ` -Verbose @@ -69,8 +59,6 @@ function Skip-SenderRecipientCredentials { -EnvironmentAbbreviation $EnvironmentAbbreviation ` -SecureSenderUsername $defaultSecret ` -SecureSenderPassword $defaultSecret ` - -SecureSyncCompletedCCEmailAddresses $defaultValue ` - -SecureSyncDisabledCCEmailAddresses $defaultValue ` -SecureSupportEmailAddresses $defaultValue ` -GmmGraphAppHasMailApplicationPermissions $false @@ -101,10 +89,6 @@ function Set-SenderRecipientCredentials { [Parameter(Mandatory=$True)] [SecureString] $SecureSenderPassword, [Parameter(Mandatory=$False)] - [SecureString] $SecureSyncCompletedCCEmailAddresses, - [Parameter(Mandatory=$False)] - [SecureString] $SecureSyncDisabledCCEmailAddresses, - [Parameter(Mandatory=$False)] [SecureString] $SecureSupportEmailAddresses, [Parameter(Mandatory=$False)] [boolean] $GmmGraphAppHasMailApplicationPermissions, @@ -147,32 +131,6 @@ function Set-SenderRecipientCredentials { -SecretValue $SecureSenderPassword Write-Verbose "$senderPasswordKeyVaultSecretName added to vault..." - if(!($SyncCompletedCCEmailAddresses)) - { - $SyncCompletedCCEmailAddresses = "admin@$tenantName.onmicrosoft.com" - } - - #region Store SyncCompletedCCEmailAddresses secret in KeyVault - $syncCompletedCCKeyVaultSecretName = "syncCompletedCCEmailAddresses" - Set-AzKeyVaultSecret -VaultName $keyVault.VaultName ` - -Name $syncCompletedCCKeyVaultSecretName ` - -SecretValue $SecureSyncCompletedCCEmailAddresses - Write-Verbose "$syncCompletedCCKeyVaultSecretName added to vault..." - - if(!($SyncDisabledCCEmailAddresses)) - { - $SyncDisabledCCEmailAddresses = "admin@$tenantName.onmicrosoft.com" - } - - #region Store SyncDisabledCCEmailAddresses secret in KeyVault - $syncDisabledCCKeyVaultSecretName = "syncDisabledCCEmailAddresses" - Set-AzKeyVaultSecret -VaultName $keyVault.VaultName ` - -Name $syncDisabledCCKeyVaultSecretName ` - -SecretValue $SecureSyncDisabledCCEmailAddresses - Write-Verbose "$syncDisabledCCKeyVaultSecretName added to vault..." - - #endregion - if(!($SupportEmailAddresses)) { $SupportEmailAddresses = "admin@$tenantName.onmicrosoft.com" diff --git a/Service/GroupMembershipManagement/DIConcreteTypes/EmailSenderRecipient.cs b/Service/GroupMembershipManagement/DIConcreteTypes/EmailSenderRecipient.cs index 4451e9f9a..a8388be09 100644 --- a/Service/GroupMembershipManagement/DIConcreteTypes/EmailSenderRecipient.cs +++ b/Service/GroupMembershipManagement/DIConcreteTypes/EmailSenderRecipient.cs @@ -8,17 +8,13 @@ public class EmailSenderRecipient : IEmailSenderRecipient { public string SenderAddress { get; set; } public string SenderPassword { get; set; } - public string SyncCompletedCCAddresses { get; set; } - public string SyncDisabledCCAddresses { get; set; } public string SupportEmailAddresses { get; set; } - public EmailSenderRecipient(string senderAddress, string senderPassword, string syncCompletedCCAddresses, string syncDisabledCCAddresses, string supportEmailAddresses) + public EmailSenderRecipient(string senderAddress, string senderPassword, string supportEmailAddresses) { - this.SenderAddress = senderAddress; - this.SenderPassword = senderPassword; - this.SyncCompletedCCAddresses = syncCompletedCCAddresses; - this.SyncDisabledCCAddresses = syncDisabledCCAddresses; - this.SupportEmailAddresses = supportEmailAddresses; + SenderAddress = senderAddress; + SenderPassword = senderPassword; + SupportEmailAddresses = supportEmailAddresses; } public EmailSenderRecipient() diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs index 9d49828ce..f7f3386f6 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonProgramBase.cs @@ -147,8 +147,6 @@ public static void ConfigureCommonServices(IServiceCollection services, IConfigu { settings.SenderAddress = configuration.GetValue("senderAddress"); settings.SenderPassword = configuration.GetValue("senderPassword"); - settings.SyncDisabledCCAddresses = configuration.GetValue("syncDisabledCCEmailAddresses"); - settings.SyncCompletedCCAddresses = configuration.GetValue("syncCompletedCCEmailAddresses"); settings.SupportEmailAddresses = configuration.GetValue("supportEmailAddresses"); }); @@ -158,8 +156,6 @@ public static void ConfigureCommonServices(IServiceCollection services, IConfigu return new EmailSenderRecipient( creds.Value.SenderAddress, creds.Value.SenderPassword, - creds.Value.SyncCompletedCCAddresses, - creds.Value.SyncDisabledCCAddresses, creds.Value.SupportEmailAddresses); }); diff --git a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs index f3ad7861b..b2081f87d 100644 --- a/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs +++ b/Service/GroupMembershipManagement/Hosts.FunctionBase/CommonStartup.cs @@ -159,8 +159,6 @@ public override void Configure(IFunctionsHostBuilder builder) { settings.SenderAddress = configuration.GetValue("senderAddress"); settings.SenderPassword = configuration.GetValue("senderPassword"); - settings.SyncDisabledCCAddresses = configuration.GetValue("syncDisabledCCEmailAddresses"); - settings.SyncCompletedCCAddresses = configuration.GetValue("syncCompletedCCEmailAddresses"); settings.SupportEmailAddresses = configuration.GetValue("supportEmailAddresses"); }); @@ -170,8 +168,6 @@ public override void Configure(IFunctionsHostBuilder builder) return new EmailSenderRecipient( creds.Value.SenderAddress, creds.Value.SenderPassword, - creds.Value.SyncCompletedCCAddresses, - creds.Value.SyncDisabledCCAddresses, creds.Value.SupportEmailAddresses); }); diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep index 3b136ec96..3663193c5 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep @@ -95,7 +95,6 @@ var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultRe var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') var teamsChannelServiceAccountUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'teamsChannelServiceAccountUsername') var teamsChannelServiceAccountPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'teamsChannelServiceAccountPassword') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierProviderId') @@ -147,7 +146,6 @@ var appSettings = { senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' teamsChannelServiceAccountUsername: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelServiceAccountUsername, '2019-09-01').secretUriWithVersion})' teamsChannelServiceAccountPassword: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelServiceAccountPassword, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' appConfigurationEndpoint: appConfigurationEndpoint actionableEmailProviderId: '@Microsoft.KeyVault(SecretUri=${reference(actionableEmailProviderId, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json index 305ecec8c..6221e6c98 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json @@ -15,8 +15,6 @@ "logAnalyticsPrimarySharedKey": "", "senderAddress": "", "senderPassword": "", - "syncCompletedCCEmailAddresses": "", - "syncDisabledCCEmailAddresses": "", "supportEmailAddresses": "", "membershipStorageAccountName": "", "membershipContainerName": "", diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep index 77da07e10..727254922 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep @@ -71,8 +71,6 @@ var graphAppCertificateName = resourceId(subscription().subscriptionId, prereqsK var graphAppTenantId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppTenantId') var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderUsername') var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') -var syncCompletedCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncCompletedCCEmailAddresses') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') @@ -124,8 +122,6 @@ var appSettings = { logAnalyticsPrimarySharedKey: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsPrimarySharedKey, '2019-09-01').secretUriWithVersion})' senderAddress: '@Microsoft.KeyVault(SecretUri=${reference(senderUsername, '2019-09-01').secretUriWithVersion})' senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' - syncCompletedCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncCompletedCCEmailAddresses, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' appConfigurationEndpoint: appConfigurationEndpoint membershipStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(membershipStorageAccountName, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json index ed8980971..ab28ad63a 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json @@ -17,7 +17,6 @@ "ConnectionStrings:JobsContextReadOnly": "", "senderAddress": "", "senderPassword": "", - "syncDisabledCCEmailAddresses": "", "supportEmailAddresses": "", "membershipStorageAccountName": "", "membershipContainerName": "", diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep index 4660dad13..c29666415 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep @@ -74,7 +74,6 @@ var graphAppCertificateName = resourceId(subscription().subscriptionId, prereqsK var graphAppTenantId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppTenantId') var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderUsername') var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') @@ -128,7 +127,6 @@ var appSettings = { logAnalyticsPrimarySharedKey: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsPrimarySharedKey, '2019-09-01').secretUriWithVersion})' senderAddress: '@Microsoft.KeyVault(SecretUri=${reference(senderUsername, '2019-09-01').secretUriWithVersion})' senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' membershipStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(membershipStorageAccountName, '2019-09-01').secretUriWithVersion})' membershipContainerName: '@Microsoft.KeyVault(SecretUri=${reference(membershipContainerName, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/local.settings.json index 654393612..8be6158e4 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Function/local.settings.json @@ -18,7 +18,6 @@ "ConnectionStrings:JobsContextReadOnly": "", "senderAddress": "", "senderPassword": "", - "syncDisabledCCEmailAddresses": "", "supportEmailAddresses": "", "membershipStorageAccountName": "", "membershipContainerName": "", diff --git a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep index 9b4909316..24d109449 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupOwnershipObtainer/Infrastructure/compute/template.bicep @@ -74,7 +74,6 @@ var graphAppCertificateName = resourceId(subscription().subscriptionId, prereqsK var graphAppTenantId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppTenantId') var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderUsername') var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') @@ -127,7 +126,6 @@ var appSettings = { logAnalyticsPrimarySharedKey: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsPrimarySharedKey, '2019-09-01').secretUriWithVersion})' senderAddress: '@Microsoft.KeyVault(SecretUri=${reference(senderUsername, '2019-09-01').secretUriWithVersion})' senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' membershipStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(membershipStorageAccountName, '2019-09-01').secretUriWithVersion})' membershipContainerName: '@Microsoft.KeyVault(SecretUri=${reference(membershipContainerName, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/local.settings.json index 297891b08..e6ff3ec3b 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/local.settings.json @@ -10,7 +10,6 @@ "gmmServiceBus__fullyQualifiedNamespace": ".servicebus.windows.net", "serviceBusSyncJobTopic": "", "jobTriggerSchedule": "0 */1 * * * *", - "syncDisabledCCEmailAddresses": "", "supportEmailAddresses": "", "senderAddress": "", "senderPassword": "", diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep index eb9c23d2a..3100907c1 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep @@ -96,7 +96,6 @@ var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultRe var teamsChannelServiceAccountUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'teamsChannelServiceAccountUsername') var teamsChannelServiceAccountPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'teamsChannelServiceAccountPassword') var teamsChannelServiceAccountObjectId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'teamsChannelServiceAccountObjectId') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierProviderId') @@ -150,7 +149,6 @@ var appSettings = { teamsChannelServiceAccountUsername: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelServiceAccountUsername, '2019-09-01').secretUriWithVersion})' teamsChannelServiceAccountPassword: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelServiceAccountPassword, '2019-09-01').secretUriWithVersion})' teamsChannelServiceAccountObjectId: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelServiceAccountObjectId, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' appConfigurationEndpoint: appConfigurationEndpoint actionableEmailProviderId: '@Microsoft.KeyVault(SecretUri=${reference(actionableEmailProviderId, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/JobTriggerServiceTests.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/JobTriggerServiceTests.cs index 9e77ea33c..4620248ea 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/JobTriggerServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/JobTriggerServiceTests.cs @@ -379,9 +379,9 @@ public async Task VerifyJobStatusIsUpdatedToInProgress() _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(getDestinationObjectId(x))); _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(getDestinationObjectId(x))); - + foreach (var job in _syncJobRepository.Jobs) - { + { var canWriteToGroup = await _jobTriggerService.DestinationExistsAndGMMCanWriteToItAsync(job); await _jobTriggerService.UpdateSyncJobAsync(canWriteToGroup == DestinationVerifierResult.Success ? SyncStatus.InProgress : SyncStatus.NotOwnerOfDestinationGroup, job); Assert.AreEqual(job.Status, SyncStatus.InProgress.ToString()); @@ -397,7 +397,7 @@ public async Task VerifyJobStatusIsUpdatedToStuckInProgress() _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsThatExist.Add(getDestinationObjectId(x))); _syncJobRepository.Jobs.ForEach(x => _graphGroupRepository.GroupsGMMOwns.Add(getDestinationObjectId(x))); - + foreach (var job in _syncJobRepository.Jobs) { job.Status = SyncStatus.InProgress.ToString(); @@ -582,7 +582,7 @@ public async Task VerifyJobsAreProcessedWithMissingMailLicenses() _mockTeamsChannelRepository.Object, new MockKeyVaultSecret(), new MockKeyVaultSecret(), - new MockEmail(), + new MockEmail(), _serviceBusQueueRepository.Object, _gMMResources, _jobTriggerConfig, @@ -653,10 +653,6 @@ private class MockEmail : IEmailSenderRecipient public string SenderPassword => ""; - public string SyncCompletedCCAddresses => ""; - - public string SyncDisabledCCAddresses => ""; - public string SupportEmailAddresses => ""; } diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json index 689fd3e0f..ac7479840 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json @@ -20,8 +20,6 @@ "graphCredentials:TenantId": "", "senderAddress": "", "senderPassword": "", - "syncCompletedCCEmailAddresses": "", - "syncDisabledCCEmailAddresses": "", "supportEmailAddresses": "", "actionableEmailProviderId": "", "serviceBusMembershipAggregatorQueue": "", diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep index 15aec00f7..7f6ff5701 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep @@ -95,8 +95,6 @@ var graphAppCertificateName = resourceId(subscription().subscriptionId, prereqsK var graphAppTenantId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppTenantId') var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderUsername') var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') -var syncCompletedCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncCompletedCCEmailAddresses') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') var actionableEmailProviderId = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'notifierProviderId') @@ -151,8 +149,6 @@ var appSettings = { 'ConnectionStrings:JobsContextReadOnly': '@Microsoft.KeyVault(SecretUri=${reference(replicaJobsMSIConnectionString, '2019-09-01').secretUriWithVersion})' senderAddress: '@Microsoft.KeyVault(SecretUri=${reference(senderUsername, '2019-09-01').secretUriWithVersion})' senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' - syncCompletedCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncCompletedCCEmailAddresses, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' actionableEmailProviderId: '@Microsoft.KeyVault(SecretUri=${reference(actionableEmailProviderId, '2019-09-01').secretUriWithVersion})' gmmServiceBus__fullyQualifiedNamespace: '@Microsoft.KeyVault(SecretUri=${reference(serviceBusFQN, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/local.settings.json index 4fa1e650e..68ca90451 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/local.settings.json @@ -15,7 +15,6 @@ "logAnalyticsPrimarySharedKey": "", "senderAddress": "", "senderPassword": "", - "syncDisabledCCEmailAddresses": "", "supportEmailAddresses": "", "membershipStorageAccountName": "", "membershipContainerName": "", diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep index 086c2f6dc..7836dc5a7 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep @@ -74,7 +74,6 @@ var graphAppCertificateName = resourceId(subscription().subscriptionId, prereqsK var graphAppTenantId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppTenantId') var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderUsername') var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') @@ -126,7 +125,6 @@ var appSettings = { logAnalyticsPrimarySharedKey: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsPrimarySharedKey, '2019-09-01').secretUriWithVersion})' senderAddress: '@Microsoft.KeyVault(SecretUri=${reference(senderUsername, '2019-09-01').secretUriWithVersion})' senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' membershipStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(membershipStorageAccountName, '2019-09-01').secretUriWithVersion})' membershipContainerName: '@Microsoft.KeyVault(SecretUri=${reference(membershipContainerName, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep index 2a7132e7f..ec97c41bf 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep @@ -100,7 +100,6 @@ var graphAppTenantId = resourceId(subscription().subscriptionId, prereqsKeyVault var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderUsername') var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') @@ -162,7 +161,6 @@ var appSettings = { senderAddress: '@Microsoft.KeyVault(SecretUri=${reference(senderUsername, '2019-09-01').secretUriWithVersion})' senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' membershipStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(membershipStorageAccountName, '2019-09-01').secretUriWithVersion})' membershipContainerName: '@Microsoft.KeyVault(SecretUri=${reference(membershipContainerName, '2019-09-01').secretUriWithVersion})' actionableEmailProviderId: '@Microsoft.KeyVault(SecretUri=${reference(actionableEmailProviderId, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/local.settings.json index 39a23ca0e..c4dbc8f59 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/local.settings.json @@ -13,11 +13,9 @@ "syncJobUpdaterStorageAccountName": "", "senderAddress": "", "WEBSITE_CONTENTSHARE": "", - "AzureWebJobsStorage": "", "graphCredentials:KeyVaultName": "", "AzureFunctionsJobHost__extensions__durableTask__hubName": "", "logAnalyticsPrimarySharedKey": "", - "syncDisabledCCEmailAddresses": "", "ConnectionStrings:JobsContext": "", "graphCredentials:ClientCertificateName": "", "logAnalyticsCustomerId": "", @@ -30,6 +28,5 @@ "senderPassword": "", "serviceBusNotificationsQueue": "", "serviceBusSyncJobUpdaterQueue": "", - "APPINSIGHTS_INSTRUMENTATIONKEY": "" } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep index 0c20ff54c..5fe7bff65 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep @@ -67,7 +67,6 @@ var graphAppCertificateName = resourceId(subscription().subscriptionId, prereqsK var graphAppTenantId = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'graphAppTenantId') var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderUsername') var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var syncJobUpdaterStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var appInsightsInstrumentationKey = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'appInsightsInstrumentationKey') @@ -110,7 +109,6 @@ var appSettings = { logAnalyticsPrimarySharedKey: '@Microsoft.KeyVault(SecretUri=${reference(logAnalyticsPrimarySharedKey, '2019-09-01').secretUriWithVersion})' senderAddress: '@Microsoft.KeyVault(SecretUri=${reference(senderUsername, '2019-09-01').secretUriWithVersion})' senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' syncJobUpdaterStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(syncJobUpdaterStorageAccountName, '2019-09-01').secretUriWithVersion})' appConfigurationEndpoint: appConfigurationEndpoint diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/local.settings.json index 01eac4188..7274df164 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Function/local.settings.json @@ -27,7 +27,6 @@ "serviceBusMembershipAggregatorQueue": "", "gmmServiceBus__fullyQualifiedNamespace": ".servicebus.windows.net", "supportEmailAddresses": "", - "syncDisabledCCEmailAddresses": "", "TeamsChannelMembershipObtainer:IsDryRunEnabled": "", "WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG": "", "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "", diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep index 14c778323..4a97b3965 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelMembershipObtainer/Infrastructure/compute/template.bicep @@ -76,7 +76,6 @@ var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultRe var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') var teamsChannelServiceAccountUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'teamsChannelServiceAccountUsername') var teamsChannelServiceAccountPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'teamsChannelServiceAccountPassword') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') @@ -130,7 +129,6 @@ var appSettings = { senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' teamsChannelServiceAccountUsername: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelServiceAccountUsername, '2019-09-01').secretUriWithVersion})' teamsChannelServiceAccountPassword: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelServiceAccountPassword, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' membershipStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(membershipStorageAccountName, '2019-09-01').secretUriWithVersion})' membershipContainerName: '@Microsoft.KeyVault(SecretUri=${reference(membershipContainerName, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep index 69cca29c2..17181846a 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep @@ -75,7 +75,6 @@ var senderUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultRe var senderPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'senderPassword') var teamsChannelServiceAccountUsername = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'teamsChannelServiceAccountUsername') var teamsChannelServiceAccountPassword = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'teamsChannelServiceAccountPassword') -var syncDisabledCCEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'syncDisabledCCEmailAddresses') var supportEmailAddresses = resourceId(subscription().subscriptionId, prereqsKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', prereqsKeyVaultName, 'supportEmailAddresses') var membershipStorageAccountName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'jobsStorageAccountName') var membershipContainerName = resourceId(subscription().subscriptionId, dataKeyVaultResourceGroup, 'Microsoft.KeyVault/vaults/secrets', dataKeyVaultName, 'membershipContainerName') @@ -126,7 +125,6 @@ var appSettings = { senderPassword: '@Microsoft.KeyVault(SecretUri=${reference(senderPassword, '2019-09-01').secretUriWithVersion})' teamsChannelServiceAccountUsername: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelServiceAccountUsername, '2019-09-01').secretUriWithVersion})' teamsChannelServiceAccountPassword: '@Microsoft.KeyVault(SecretUri=${reference(teamsChannelServiceAccountPassword, '2019-09-01').secretUriWithVersion})' - syncDisabledCCEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(syncDisabledCCEmailAddresses, '2019-09-01').secretUriWithVersion})' supportEmailAddresses: '@Microsoft.KeyVault(SecretUri=${reference(supportEmailAddresses, '2019-09-01').secretUriWithVersion})' membershipStorageAccountName: '@Microsoft.KeyVault(SecretUri=${reference(membershipStorageAccountName, '2019-09-01').secretUriWithVersion})' membershipContainerName: '@Microsoft.KeyVault(SecretUri=${reference(membershipContainerName, '2019-09-01').secretUriWithVersion})' diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/InjectConfig/IEmailSenderRecipient.cs b/Service/GroupMembershipManagement/Repositories.Contracts/InjectConfig/IEmailSenderRecipient.cs index df1087679..da3a7efb5 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts/InjectConfig/IEmailSenderRecipient.cs +++ b/Service/GroupMembershipManagement/Repositories.Contracts/InjectConfig/IEmailSenderRecipient.cs @@ -6,8 +6,6 @@ public interface IEmailSenderRecipient { string SenderAddress { get; } string SenderPassword { get; } - string SyncCompletedCCAddresses { get; } - string SyncDisabledCCAddresses { get; } string SupportEmailAddresses { get; } } } diff --git a/Service/GroupMembershipManagement/Repositories.Mail/Documentation/SetSenderAddressForEmailNotification.md b/Service/GroupMembershipManagement/Repositories.Mail/Documentation/SetSenderAddressForEmailNotification.md index c525fe493..1cec63b8c 100644 --- a/Service/GroupMembershipManagement/Repositories.Mail/Documentation/SetSenderAddressForEmailNotification.md +++ b/Service/GroupMembershipManagement/Repositories.Mail/Documentation/SetSenderAddressForEmailNotification.md @@ -25,8 +25,6 @@ Follow the steps below to set the sender's credentials that will be used to send ``` $secureSenderUsername = ConvertTo-SecureString -AsPlainText -Force "" $secureSecurePassword = ConvertTo-SecureString -AsPlainText -Force "" - $secureSyncCompletedCCEmailAddresses = ConvertTo-SecureString -AsPlainText -Force "" - $secureSyncDisabledCCEmailAddresses = ConvertTo-SecureString -AsPlainText -Force "" $secureSupportEmailAddresses = ConvertTo-SecureString -AsPlainText -Force "" Set-SenderRecipientCredentials -SubscriptionName "" ` @@ -34,8 +32,6 @@ Follow the steps below to set the sender's credentials that will be used to send -EnvironmentAbbreviation "" ` -SecureSenderUsername $secureSenderUsername ` -SecureSenderPassword $secureSecurePassword ` - -SecureSyncCompletedCCEmailAddresses $secureSyncCompletedCCEmailAddresses ` - -SecureSyncDisabledCCEmailAddresses $secureSyncDisabledCCEmailAddresses ` -SecureSupportEmailAddresses $secureSupportEmailAddresses ` -GmmGraphAppHasMailApplicationPermissions $false ` -Verbose diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/MockMailRepository.cs b/Service/GroupMembershipManagement/Repositories.Mocks/MockMailRepository.cs index e213525c7..009c6c654 100644 --- a/Service/GroupMembershipManagement/Repositories.Mocks/MockMailRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Mocks/MockMailRepository.cs @@ -31,10 +31,6 @@ public class MockEmail : IEmailSenderRecipient public string SenderPassword => ""; - public string SyncCompletedCCAddresses => ""; - - public string SyncDisabledCCAddresses => ""; - public string SupportEmailAddresses => ""; } } From af77353a21a2ade8683eec2d5f7c954a5b45c640 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 26 Aug 2024 13:29:51 -0700 Subject: [PATCH 0243/1479] Removed use of different CC address for Disabled emails --- .../GraphUpdaterServiceTests.cs | 24 +++++++-------- .../GroupUpdaterServiceTests.cs | 4 +-- .../Services.Tests/OrchestratorTests.cs | 30 +++++++------------ .../Services.Notifier/NotifierService.cs | 5 +--- 4 files changed, 25 insertions(+), 38 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GraphUpdaterServiceTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GraphUpdaterServiceTests.cs index 7a9b6c367..54652f68d 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GraphUpdaterServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GraphUpdaterServiceTests.cs @@ -27,7 +27,7 @@ public async Task GetFirstMembersPageTest() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new Mock(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); @@ -56,7 +56,7 @@ public async Task GetNextMembersPageTest() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new Mock(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); @@ -83,7 +83,7 @@ public async Task GroupExistsTest() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new MockGraphGroupRepository(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); @@ -108,7 +108,7 @@ public async Task SendEmailTest() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new MockGraphGroupRepository(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); @@ -134,7 +134,7 @@ public async Task VerifyEmailNotSentIfDisabled() { var mockLogs = new MockLoggingRepository(); var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var lastRunTime = DateTime.UtcNow.AddDays(-1); var job = new SyncJob { Id = Guid.NewGuid(), Status = SyncStatus.Idle.ToString(), LastRunTime = lastRunTime }; @@ -168,7 +168,7 @@ public async Task VerifyEmailNotSentIfGloballyDisabled() { var mockLogs = new MockLoggingRepository(); var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var runId = Guid.NewGuid(); var lastRunTime = DateTime.UtcNow.AddDays(-1); @@ -198,7 +198,7 @@ public async Task UpdateSyncJobStatusTest() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new MockGraphGroupRepository(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); @@ -227,7 +227,7 @@ public async Task UpdateSyncJobStatusDryRunModeTest() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new MockGraphGroupRepository(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); @@ -255,7 +255,7 @@ public async Task GetSyncJobStatusTest() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new MockGraphGroupRepository(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); @@ -280,7 +280,7 @@ public async Task GetGroupNameTest() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new Mock(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); @@ -324,7 +324,7 @@ public async Task IsEmailRecipientOwnerOfGroupAsync_ShouldReturnTrue_WhenRecipie var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new Mock(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); @@ -346,7 +346,7 @@ public async Task IsEmailRecipientOwnerOfGroupAsync_ShouldReturnFalse_WhenRecipi var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new Mock(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GroupUpdaterServiceTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GroupUpdaterServiceTests.cs index 7c00c9372..8c567f0ec 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GroupUpdaterServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/GroupUpdaterServiceTests.cs @@ -27,7 +27,7 @@ public async Task AddUsersToGroupInNormalMode() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new MockGraphGroupRepository(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); @@ -60,7 +60,7 @@ public async Task RemoveUsersToGroupInNormalMode() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var mockGraphGroup = new MockGraphGroupRepository(); var mockMail = new MockMailRepository(); - var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + var mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); var mockSyncJobs = new MockDatabaseSyncJobRepository(); var mockNotificationType = new MockNotificationTypesRepository(); var mockJobNotification = new MockJobNotificationRepository(); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs index 9f6df7827..ae98b4344 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs @@ -62,8 +62,7 @@ public async Task TestMsalTransientExceptionAsync() mockGraphUpdaterService = new MockGraphUpdaterService(mockServiceBusQueueRepository.Object); dryRun = new DryRunValue(false); thresholdConfig = new ThresholdConfig(5, 3, 3, 10); - mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", - "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); mockGroupRepo = new MockGraphGroupRepository(); @@ -156,8 +155,7 @@ public async Task RunOrchestratorValidSyncTest() mockGraphUpdaterService = new MockGraphUpdaterService(mockServiceBusQueueRepository.Object); dryRun = new DryRunValue(false); thresholdConfig = new ThresholdConfig(5, 3, 3, 10); - mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", - "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); mockGroupRepo = new MockGraphGroupRepository(); mockSyncJobRepo = new MockDatabaseSyncJobRepository(); @@ -267,8 +265,7 @@ public async Task RunOrchestratorInitialSyncTest() mockGraphUpdaterService = new MockGraphUpdaterService(mockServiceBusQueueRepository.Object); dryRun = new DryRunValue(false); thresholdConfig = new ThresholdConfig(5, 3, 3, 10); - mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", - "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); mockGroupRepo = new MockGraphGroupRepository(); @@ -392,8 +389,7 @@ public async Task TestHttpTransientExceptionAsync() mockGraphUpdaterService = new MockGraphUpdaterService(mockServiceBusQueueRepository.Object); dryRun = new DryRunValue(false); thresholdConfig = new ThresholdConfig(5, 3, 3, 10); - mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", - "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); mockGroupRepo = new MockGraphGroupRepository(); @@ -490,8 +486,7 @@ public async Task RunOrchestratorExceptionTest() mockGraphUpdaterService = new MockGraphUpdaterService(mockServiceBusQueueRepository.Object); dryRun = new DryRunValue(false); thresholdConfig = new ThresholdConfig(5, 3, 3, 10); - mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", - "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); mockGroupRepo = new MockGraphGroupRepository(); @@ -580,8 +575,7 @@ public async Task RunSyncJobNotFoundTest() mockGraphUpdaterService = new MockGraphUpdaterService(mockServiceBusQueueRepository.Object); dryRun = new DryRunValue(false); thresholdConfig = new ThresholdConfig(5, 3, 3, 10); - mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", - "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); mockGroupRepo = new MockGraphGroupRepository(); @@ -640,8 +634,7 @@ public async Task RunOrchestratorFileNotFoundExceptionTest() mockGraphUpdaterService = new MockGraphUpdaterService(mockServiceBusQueueRepository.Object); dryRun = new DryRunValue(false); thresholdConfig = new ThresholdConfig(5, 3, 3, 10); - mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", - "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); mockGroupRepo = new MockGraphGroupRepository(); @@ -720,8 +713,7 @@ public async Task RunOrchestratorMissingGroupTest() mockGraphUpdaterService = new MockGraphUpdaterService(mockServiceBusQueueRepository.Object); dryRun = new DryRunValue(false); thresholdConfig = new ThresholdConfig(5, 3, 3, 10); - mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", - "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); mockGroupRepo = new MockGraphGroupRepository(); mockSyncJobRepo = new MockDatabaseSyncJobRepository(); @@ -804,8 +796,7 @@ public async Task RunOrchestratorGuestUserErrorTest() mockGraphUpdaterService = new MockGraphUpdaterService(mockServiceBusQueueRepository.Object); dryRun = new DryRunValue(false); thresholdConfig = new ThresholdConfig(5, 3, 3, 10); - mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", - "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); mockGroupRepo = new MockGraphGroupRepository(); @@ -924,8 +915,7 @@ public async Task RunCacheUserUpdaterSubOrchestratorFunctionTest() mockGraphUpdaterService = new MockGraphUpdaterService(mockServiceBusQueueRepository.Object); dryRun = new DryRunValue(false); thresholdConfig = new ThresholdConfig(5, 3, 3, 10); - mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", - "recipient@domain.com", "recipient@domain.com", "recipient@domain.com"); + mailSenders = new EmailSenderRecipient("sender@domain.com", "fake_pass", "recipient@domain.com"); mockGroupRepo = new MockGraphGroupRepository(); mockSyncJobRepo = new MockDatabaseSyncJobRepository(); diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index 302a61023..2da4490e1 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -234,9 +234,6 @@ await _loggingRepository.LogMessageAsync(new LogMessage ownerEmails = string.Join(";", owners.Where(x => !string.IsNullOrWhiteSpace(x.Mail)).Select(x => x.Mail)); } - if (contentTemplate.Contains("disabled", StringComparison.InvariantCultureIgnoreCase)) - ccAddress = _emailSenderAndRecipients.SyncDisabledCCAddresses; - var message = new EmailMessage { Subject = subjectTemplate, @@ -423,7 +420,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage contentTemplate = thresholdEmail.ContentTemplate; additionalContent = thresholdEmail.AdditionalContent; - var recipients = _emailSenderAndRecipients.SupportEmailAddresses ?? _emailSenderAndRecipients.SyncDisabledCCAddresses; + var recipients = _emailSenderAndRecipients.SupportEmailAddresses; if (!string.IsNullOrWhiteSpace(job.Requestor)) { From 99f8694ae006cbcc1e83304b11e66fe5cff6da91 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Thu, 29 Aug 2024 09:18:51 -0700 Subject: [PATCH 0244/1479] Removed unneeded property from additionalContentParams of JobTrigger not owner email --- .../SubOrchestratorFunction.cs | 1 - .../LocalizationRepository.en-US.resx | 58 +++++++++---------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs index 1e8f20596..2e6f214a6 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs @@ -232,7 +232,6 @@ await context.CallActivityAsync(nameof(EmailSenderFunction), AdditionalContentParams = new[] { destinationObject.Value.ObjectId.ToString(), - _emailSenderAndRecipients.SyncDisabledCCAddresses, destinationName } }); diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 977cef947..202b9fcf8 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -1,17 +1,17 @@  - @@ -1057,9 +1057,9 @@ Please reach out to support to update the source definition and re-enable the me - You received this notification because you are listed as an owner of **{2}**. + You received this notification because you are listed as an owner of **{1}**. - Membership syncs are **disabled** because **GMM is not an owner** of **{2}** with object Id {0}. + Membership syncs are **disabled** because **GMM is not an owner** of **{1}** with object Id {0}. Contact support if you would like to re-enable GMM. From 5b9760bcb4ebf63581c9d440869242541ce1c3b7 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Thu, 29 Aug 2024 09:52:26 -0700 Subject: [PATCH 0245/1479] Removed unneeded additionalContentParam from GroupVaildator failure email --- .../Function/Activity/GroupValidator/GroupValidatorFunction.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupValidator/GroupValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupValidator/GroupValidatorFunction.cs index 33be6994d..f149ec1fe 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupValidator/GroupValidatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupValidator/GroupValidatorFunction.cs @@ -55,8 +55,7 @@ await _calculator.SendEmailAsync(request.SyncJob, { request.SyncJob.TargetOfficeGroupId.ToString(), targetGroupName, - request.ObjectId.ToString(), - _emailSenderAndRecipients.SyncDisabledCCAddresses + request.ObjectId.ToString() }); } else if (groupExistsResult.FaultType == FaultType.ExceptionHandledByThisPolicy) From 470b4468f9b14c75bebd8ef7c33a40c9a86073fa Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 26 Aug 2024 13:37:10 -0700 Subject: [PATCH 0246/1479] Added fallback HTML content using simple message plus a message about fallback HTML --- .../Repositories.Mail/MailRepository.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Repositories.Mail/MailRepository.cs b/Service/GroupMembershipManagement/Repositories.Mail/MailRepository.cs index 74b2d6ac2..a8b4cacb4 100644 --- a/Service/GroupMembershipManagement/Repositories.Mail/MailRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Mail/MailRepository.cs @@ -232,6 +232,9 @@ public async Task GetAdaptiveCardMessage(EmailMessage emailMessage) var template = new AdaptiveCardTemplate(adaptiveCardJson); var adaptiveCard = template.Expand(cardData); + var simpleMessage = GetSimpleMessage(emailMessage); + var fallbackHTMLContent = simpleMessage.Body.Content; + var htmlTemplate = @" @@ -240,10 +243,13 @@ public async Task GetAdaptiveCardMessage(EmailMessage emailMessage) +

Warning: Group Membership Management (GMM) notifications are powered by Outlook Actionable Messages. The following is a fallback message that you will see if the Actionable Message fails to render.

+

Original Message

+
{1}
"; - var htmlContent = string.Format(htmlTemplate, adaptiveCard); + var htmlContent = string.Format(htmlTemplate, adaptiveCard, fallbackHTMLContent); var message = new Message { From 0eb369de8877da72273063bcd1f12edf2c6f36c2 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 28 Aug 2024 09:49:46 -0700 Subject: [PATCH 0247/1479] Made a fix for query parameters when getting user from Graph by using Mail / UPN as input, no need to check if id equals Guid --- .../Repositories.GraphGroups/GraphGroupMembershipReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupMembershipReader.cs b/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupMembershipReader.cs index 789d747c0..df0ca147e 100644 --- a/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupMembershipReader.cs +++ b/Service/GroupMembershipManagement/Repositories.GraphGroups/GraphGroupMembershipReader.cs @@ -390,7 +390,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage await _graphServiceClient.Users.GetAsync(requestConfiguration => { - requestConfiguration.QueryParameters.Filter = $"Mail eq '{userIdentifier}' or UserPrincipalName eq '{userIdentifier}' or id eq '{userIdentifier}'"; + requestConfiguration.QueryParameters.Filter = $"Mail eq '{userIdentifier}' or UserPrincipalName eq '{userIdentifier}'"; requestConfiguration.Options.Add(new ResponseHandlerOption { ResponseHandler = nativeResponseHandler }); }); From 6832fcffb0bfbce14bb90f6fcd991783ea13b09e Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 4 Sep 2024 11:06:41 -0700 Subject: [PATCH 0248/1479] Update global.json version to 8.0.400 --- Service/GroupMembershipManagement/global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/global.json b/Service/GroupMembershipManagement/global.json index fd07d882a..65af5e6b4 100644 --- a/Service/GroupMembershipManagement/global.json +++ b/Service/GroupMembershipManagement/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.303" + "version": "8.0.400" } } From 174ee686be887f6d5766a2805c6877fc8baa4150 Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 4 Sep 2024 15:08:54 -0700 Subject: [PATCH 0249/1479] use dotnet sdk version from global json --- Service/GroupMembershipManagement/global.json | 2 +- yaml/build-release-package.yml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/global.json b/Service/GroupMembershipManagement/global.json index 65af5e6b4..fd07d882a 100644 --- a/Service/GroupMembershipManagement/global.json +++ b/Service/GroupMembershipManagement/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.400" + "version": "8.0.303" } } diff --git a/yaml/build-release-package.yml b/yaml/build-release-package.yml index 46c73ea96..e4b57f68d 100644 --- a/yaml/build-release-package.yml +++ b/yaml/build-release-package.yml @@ -33,6 +33,12 @@ stages: - checkout: ${{ parameters.repoToCheckout }} path: ${{ parameters.checkoutPath }} + - task: UseDotNet@2 + displayName: "Install .NET SDK" + inputs: + packageType: sdk + useGlobalJson: true + - task: PowerShell@2 displayName: 'Build json templates' inputs: From 6eba1069e1f406f1f3c8d04bfce50c5b94e02d2e Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 3 Sep 2024 15:07:41 -0700 Subject: [PATCH 0250/1479] Fallback html for threshold notification --- .../Notifier/Services.Notifier/NotifierService.cs | 12 +++++++++++- .../Resources/LocalizationRepository.en-US.resx | 7 +++++++ .../Services.Contracts/NotificationConstants.cs | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index 2da4490e1..b17d8c9d8 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -93,6 +93,13 @@ await _loggingRepository.LogMessageAsync(new LogMessage var ownerEmails = string.Join(";", owners.Where(x => !string.IsNullOrWhiteSpace(x.Mail)).Select(x => x.Mail)); var adaptiveCard = await _thresholdNotificationService.CreateNotificationCardAsync(notification); + + var fallbackHTMLContent = _localizationRepository.TranslateSetting(NotificationConstants.ThresholdNotificationFallbackBody, + groupName, + notification.TargetOfficeGroupId.ToString(), + notification.ThresholdPercentageForAdditions.ToString(), + notification.ThresholdPercentageForRemovals.ToString()); + var htmlTemplate = @" @@ -101,6 +108,9 @@ await _loggingRepository.LogMessageAsync(new LogMessage +

Warning: Group Membership Management (GMM) notifications are powered by Outlook Actionable Messages. The following is a fallback message that you will see if the Actionable Message fails to render.

+

Fallback Message

+
{1}
"; @@ -115,7 +125,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage var message = new EmailMessage { Subject = subject, - Content = string.Format(htmlTemplate, adaptiveCard), + Content = string.Format(htmlTemplate, adaptiveCard, fallbackHTMLContent), SenderAddress = _emailSenderAndRecipients.SenderAddress, SenderPassword = _emailSenderAndRecipients.SenderPassword, ToEmailAddresses = ownerEmails, diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 202b9fcf8..82774d586 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -1155,4 +1155,11 @@ The group **{1}** with Id: {0} **has been set to the status "GuestUsersCannotBeA Even though GMM couldn't add the guest users, it still **added the remaining {2} users** and **removed the expected {3} users** from the destination.
+ + The most recent attempt to update the membership of your GMM managed group '**{0}**' with Id: {1} exceeded the configured alert threshold. + +**{2}% increase** and **{3}% decrease** alerts are configured for this group. + +Please forward this message to the CC'ed email for support and they will guide you through the actions. +
\ No newline at end of file diff --git a/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs b/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs index 722f7cfd4..3dec1d295 100644 --- a/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs +++ b/Service/GroupMembershipManagement/Services.Contracts/NotificationConstants.cs @@ -27,5 +27,6 @@ public static class NotificationConstants public const string SyncPurgedForInactivityEmailBody = "SyncPurgedForInactivityEmailBody"; public const string SyncPurgedForInactivityEmailSubject = "SyncPurgedForInactivityEmailSubject"; public const string GuestUserFailureEmailBody = "GuestUserFailureEmailBody"; + public const string ThresholdNotificationFallbackBody = "ThresholdNotificationFallbackBody"; } } From 66778831f082b6ca7f5f5ad0c171623528a9f679 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 4 Sep 2024 13:03:17 -0700 Subject: [PATCH 0251/1479] Updated Adaptive Card templates to have hideOriginalBody attribute set to 'true' --- .../Resources/LocalizationRepository.en-US.resx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 82774d586..5c67d11ad 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -122,6 +122,7 @@ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.0", + "hideOriginalBody": true, "originator": "${$root.providerId}", "createdUtc": "${$root.cardCreatedTime}", "body": [ @@ -273,6 +274,7 @@ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.0", + "hideOriginalBody": true, "originator": "${$root.providerId}", "createdUtc": "${$root.cardCreatedTime}", "body": [ @@ -424,6 +426,7 @@ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.0", + "hideOriginalBody": true, "createdUtc": "${$root.cardCreatedTime}", "body": [ { @@ -525,6 +528,7 @@ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.0", + "hideOriginalBody": true, "createdUtc": "${$root.cardCreatedTime}", "body": [ { @@ -673,6 +677,7 @@ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.0", + "hideOriginalBody": true, "createdUtc": "${$root.cardCreatedTime}", "body": [ { @@ -773,6 +778,7 @@ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.0", + "hideOriginalBody": true, "createdUtc": "${$root.cardCreatedTime}", "body": [ { @@ -880,6 +886,7 @@ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.4", + "hideOriginalBody": true, "createdUtc": "${$root.cardCreatedTime}", "body": [ { From e51fcaafa65d0d85f8caf3c579f73e677ee6358d Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 26 Aug 2024 14:55:31 -0700 Subject: [PATCH 0252/1479] Updated cache deletion logic --- .../Services.Tests/Mocks/MockBlobStorageRepository.cs | 5 +++++ .../Activity/FileDeleter/FileDeleterFunction.cs | 3 +-- .../SubOrchestrator/SubOrchestratorFunction.cs | 11 +++++++++-- .../Services.Tests/Mocks/MockBlobStorageRepository.cs | 5 +++++ .../Services.Tests/SubOrchestratorFunctionTests.cs | 2 +- .../Repositories.BlobStorage/BlobStorageRepository.cs | 11 +++++++++++ .../Repositories.Contracts/IBlobStorageRepository.cs | 1 + 7 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Mocks/MockBlobStorageRepository.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Mocks/MockBlobStorageRepository.cs index fce57c70a..f3e7249fa 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Mocks/MockBlobStorageRepository.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/Mocks/MockBlobStorageRepository.cs @@ -64,5 +64,10 @@ public Task CommitFileAsync(string path, List blockIds) { throw new NotImplementedException(); } + + public Task DeleteFilesByPrefixAsync(string prefix, bool excludeLatest = false) + { + throw new NotImplementedException(); + } } } diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDeleter/FileDeleterFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDeleter/FileDeleterFunction.cs index c3940cbd9..f4815522e 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDeleter/FileDeleterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDeleter/FileDeleterFunction.cs @@ -3,7 +3,6 @@ using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; -using Models.Helpers; using Repositories.Contracts; using System; using System.Threading.Tasks; @@ -25,7 +24,7 @@ public FileDeleterFunction(ILoggingRepository loggingRepository, IBlobStorageRep public async Task DeleteFileAsync([ActivityTrigger] FileDeleterRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Deleting file {request.FilePath}", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); - await _blobStorageRepository.DeleteFilesAsync(request.FilePath); + await _blobStorageRepository.DeleteFilesByPrefixAsync(request.FilePath, true); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Deleted file {request.FilePath}", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); } } diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs index 544b300b0..29ee8c81c 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs @@ -155,6 +155,7 @@ public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurabl if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { RunId = request.RunId, Message = $"Run delta query using delta link for group {request.SourceGroup.ObjectId}" }); var compressedDeltaResponse = await GetDeltaUsersReaderFunction(context, deltaFileContent, request); var deltaResponse = JsonConvert.DeserializeObject(TextCompressor.Decompress(compressedDeltaResponse)); + var shouldClearCache = false; deltaUsersToAdd.AddRange(deltaResponse.UsersToAdd); deltaUsersToRemove = deltaResponse.UsersToRemove; @@ -177,8 +178,7 @@ public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurabl if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { RunId = request.RunId, Message = $"{request.SourceGroup.ObjectId} has {countOfUsersFromAADGroup} users but cache has {countOfUsersFromCache} users. Running delta query..." }); // clear cache - await ClearCacheFunction(context, filePath, request.SyncJob); - await ClearCacheFunction(context, deltaFilePath, request.SyncJob); + shouldClearCache = true; var compressedResponse = await GetUsersReaderFunction(context, request); var response = JsonConvert.DeserializeObject(TextCompressor.Decompress(compressedResponse)); @@ -195,6 +195,13 @@ public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurabl } await GetDeltaUsersSenderFunction(context, request, allUsers, deltaResponse.DeltaUrl); + + if (shouldClearCache) + { + // delete old cache files, only after new cache file is created + await ClearCacheFunction(context, filePath, request.SyncJob); + await ClearCacheFunction(context, deltaFilePath, request.SyncJob); + } } catch (Exception e) when (e is KeyNotFoundException || e is ServiceException) { diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs index f5b77cc6d..24f165113 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs @@ -53,5 +53,10 @@ public Task CommitFileAsync(string path, List blockIds) { throw new NotImplementedException(); } + + public Task DeleteFilesByPrefixAsync(string prefix, bool excludeLatest = false) + { + throw new NotImplementedException(); + } } } diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs index 860440263..8620467b9 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs @@ -430,7 +430,7 @@ public async Task ProcessDeltaSinglePageRequestWithExtraUserTestAsync() _graphGroupRepository.Verify(x => x.GetGroupsCountAsync(It.IsAny()), Times.Once); _graphGroupRepository.Verify(x => x.GroupExists(It.IsAny()), Times.Once); _graphGroupRepository.Verify(x => x.GetFirstUsersPageAsync(It.IsAny()), Times.Once); - _blobStorageRepository.Verify(x => x.DeleteFilesAsync(It.IsAny()), Times.Exactly(2)); + _blobStorageRepository.Verify(x => x.DeleteFilesByPrefixAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains($"read {_userCount} users")), diff --git a/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs b/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs index 1d5631191..7e3613ed8 100644 --- a/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs @@ -58,6 +58,17 @@ public async Task DeleteFilesAsync(string path) } } + public async Task DeleteFilesByPrefixAsync(string prefix, bool excludeLatest = false) + { + var blobs = _containerClient.GetBlobs(prefix: prefix).OrderByDescending(m => m.Properties.LastModified); + foreach (var blob in blobs) + { + if (excludeLatest) continue; + var blobClient = _containerClient.GetBlobClient(blob.Name); + await blobClient.DeleteIfExistsAsync(); + } + } + public async Task DownloadCacheFileAsync(string path) { var latest = _containerClient.GetBlobs(prefix: path).OrderByDescending(m => m.Properties.LastModified).FirstOrDefault(); diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/IBlobStorageRepository.cs b/Service/GroupMembershipManagement/Repositories.Contracts/IBlobStorageRepository.cs index 55f73a2a4..589199e49 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts/IBlobStorageRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Contracts/IBlobStorageRepository.cs @@ -11,6 +11,7 @@ public interface IBlobStorageRepository public Task UploadFileAsync(string path, string content, Dictionary metadata = null); Task UploadFileBlockAsync(string path, string content, Dictionary metadata = null); public Task DeleteFileAsync(string path); + public Task DeleteFilesByPrefixAsync(string prefix, bool excludeLatest = false); public Task DownloadFileAsync(string path); public Task DownloadCacheFileAsync(string path); public Task DeleteFilesAsync(string path); From 4109555046e981cc000727cc246b7915d2be0d09 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 4 Sep 2024 15:40:55 -0700 Subject: [PATCH 0253/1479] Fix bug. Added unit tests --- .../BlobStorageRepositoryTests.cs | 59 +++++++++++++ .../Mocks/MockBlobStorageRepository.cs | 87 ++++++++++++++++++- .../BlobStorageRepository.cs | 7 +- 3 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/BlobStorageRepositoryTests.cs diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/BlobStorageRepositoryTests.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/BlobStorageRepositoryTests.cs new file mode 100644 index 000000000..780fbed34 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/BlobStorageRepositoryTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; +using System.Threading.Tasks; +using Tests.FunctionApps.Mocks; + +namespace Tests.Services +{ + [TestClass] + public class BlobStorageRepositoryTests + { + private MockBlobStorageRepository _mockBlobStorageRepository; + + [TestInitialize] + public void Initialize() + { + _mockBlobStorageRepository = new MockBlobStorageRepository(); + } + + [TestMethod] + public async Task DeleteFilesByPrefixExcludeLatestAsync() + { + var prefix = "even"; + var latestBlob = _mockBlobStorageRepository.Blobs + .Where(x => x.Name.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase)) + .MaxBy(x => x.Properties.LastModified); + + var otherBlobs = _mockBlobStorageRepository.Blobs + .Count(x => !x.Name.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase)); + + await _mockBlobStorageRepository.DeleteFilesByPrefixAsync(prefix, excludeLatest: true); + + Assert.IsNotNull(_mockBlobStorageRepository.Blobs.FirstOrDefault(x => x.Name == latestBlob.Name)); + Assert.AreEqual(1, _mockBlobStorageRepository.Blobs.Count(x => x.Name.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase))); + Assert.AreEqual(otherBlobs, _mockBlobStorageRepository.Blobs.Count(x => !x.Name.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase))); + } + + [TestMethod] + public async Task DeleteFilesByPrefixIncludeLatestAsync() + { + var prefix = "even"; + var latestBlob = _mockBlobStorageRepository.Blobs + .Where(x => x.Name.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase)) + .MaxBy(x => x.Properties.LastModified); + + var otherBlobs = _mockBlobStorageRepository.Blobs + .Count(x => !x.Name.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase)); + + await _mockBlobStorageRepository.DeleteFilesByPrefixAsync(prefix, excludeLatest: false); + + Assert.IsNull(_mockBlobStorageRepository.Blobs.FirstOrDefault(x => x.Name == latestBlob.Name)); + Assert.AreEqual(0, _mockBlobStorageRepository.Blobs.Count(x => x.Name.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase))); + Assert.AreEqual(otherBlobs, _mockBlobStorageRepository.Blobs.Count(x => !x.Name.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase))); + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs index 24f165113..5c54ff635 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Mocks/MockBlobStorageRepository.cs @@ -1,11 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Entities; +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; using Models; using Repositories.Contracts; using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Tests.FunctionApps.Mocks @@ -13,6 +17,23 @@ namespace Tests.FunctionApps.Mocks public class MockBlobStorageRepository : IBlobStorageRepository { public List<(string Path, string Content)> Sent { get; set; } = new List<(string Path, string Content)>(); + + public List Blobs { get; set; } = new List(); + + public MockBlobContainerClient ContainerClient { get; set; } + + public MockBlobStorageRepository() + { + for (int i = 0; i < 10; i++) + { + var prefix = i % 2 == 0 ? "even" : "odd"; + var blobProperties = BlobsModelFactory.BlobItemProperties(false, lastModified: DateTimeOffset.UtcNow.AddDays(-i)); + Blobs.Add(BlobsModelFactory.BlobItem(name: $"{prefix}_{i}.json", properties: blobProperties)); + } + + ContainerClient = new MockBlobContainerClient(Blobs); + } + public Task DeleteFileAsync(string path) { throw new NotImplementedException(); @@ -54,9 +75,69 @@ public Task CommitFileAsync(string path, List blockIds) throw new NotImplementedException(); } - public Task DeleteFilesByPrefixAsync(string prefix, bool excludeLatest = false) + public async Task DeleteFilesByPrefixAsync(string prefix, bool excludeLatest = false) { - throw new NotImplementedException(); + var blobs = ContainerClient.GetBlobs(prefix: prefix).OrderByDescending(m => m.Properties.LastModified); + foreach (var blob in blobs) + { + if (excludeLatest) + { + excludeLatest = false; + continue; + } + + var blobClient = ContainerClient.GetBlobClient(blob.Name); + await blobClient.DeleteIfExistsAsync(); + } + } + } + + public class MockBlobContainerClient : BlobContainerClient + { + private List _blobs; + + public MockBlobContainerClient(List blobs) + { + _blobs = blobs; + } + + public override Pageable GetBlobs(BlobTraits traits = BlobTraits.None, BlobStates states = BlobStates.None, string prefix = null, CancellationToken cancellationToken = default) + { + var filteredBlobs = prefix != null + ? _blobs.Where(x => x.Name.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase)).ToList() + : _blobs; + + var page = Azure.Page.FromValues(filteredBlobs, null, null); + return Pageable.FromPages([page]); + } + + public override BlobClient GetBlobClient(string blobName) + { + return new MockBlobClient(_blobs, blobName); + } + } + + public class MockBlobClient : BlobClient + { + private List _blobs; + private string _blobName; + + public MockBlobClient(List blobs, string blobName) + { + _blobs = blobs; + _blobName = blobName; + } + + public override Task> DeleteIfExistsAsync(DeleteSnapshotsOption snapshotsOption = DeleteSnapshotsOption.None, BlobRequestConditions conditions = null, CancellationToken cancellationToken = default) + { + var blob = _blobs.Where(x => x.Name == _blobName).FirstOrDefault(); + if (blob != null) + { + _blobs.Remove(blob); + return Task.FromResult(Response.FromValue(true, null)); + } + + return Task.FromResult(Response.FromValue(false, null)); } } } diff --git a/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs b/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs index 7e3613ed8..582b436f4 100644 --- a/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.BlobStorage/BlobStorageRepository.cs @@ -63,7 +63,12 @@ public async Task DeleteFilesByPrefixAsync(string prefix, bool excludeLatest = f var blobs = _containerClient.GetBlobs(prefix: prefix).OrderByDescending(m => m.Properties.LastModified); foreach (var blob in blobs) { - if (excludeLatest) continue; + if (excludeLatest) + { + excludeLatest = false; + continue; + } + var blobClient = _containerClient.GetBlobClient(blob.Name); await blobClient.DeleteIfExistsAsync(); } From 3f9b644dd0b5b79425e4c3dd575cb041cbb53b22 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Fri, 6 Sep 2024 03:43:32 +0000 Subject: [PATCH 0254/1479] Fixed storage account name for SyncJobUpdater --- .../Hosts/SyncJobUpdater/Infrastructure/data/template.bicep | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/template.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/template.bicep index 3725070f5..74e62547a 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/data/template.bicep @@ -20,10 +20,10 @@ param storageAccountSku string = 'Standard_LRS' param location string var keyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' -var prodStorageAccountName = substring('jf${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) +var prodStorageAccountName = substring('sju${solutionAbbreviation}${environmentAbbreviation}prod${uniqueString(resourceGroup().id)}',0,23) -module jfStorageAccountProd 'storageAccount.bicep' = { - name: 'jfProdstorageAccountTemplate' +module sjuStorageAccountProd 'storageAccount.bicep' = { + name: 'sjuProdstorageAccountTemplate' params: { name: prodStorageAccountName sku: storageAccountSku From 545d636258127529958dc00eb51d54e7d1af0f39 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Tue, 17 Sep 2024 21:28:26 -0700 Subject: [PATCH 0255/1479] Added overload method for CreateAuthProviderFromCertificate. --- .../Common.DependencyInjection/FunctionAppDI.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Service/GroupMembershipManagement/Common.DependencyInjection/FunctionAppDI.cs b/Service/GroupMembershipManagement/Common.DependencyInjection/FunctionAppDI.cs index 12a834f2a..8316bdf4f 100644 --- a/Service/GroupMembershipManagement/Common.DependencyInjection/FunctionAppDI.cs +++ b/Service/GroupMembershipManagement/Common.DependencyInjection/FunctionAppDI.cs @@ -43,6 +43,11 @@ private static TokenCredential CreateAuthProviderFromCertificate(GraphCredential return new ClientCertificateCredential(creds.TenantId, creds.ClientId, GetCertificate(creds.ClientCertificateName, creds.KeyVaultName)); } + public static TokenCredential CreateAuthProviderFromCertificate(string tenantId, string clientId, string certificateName, string KeyVaultName) + { + return new ClientCertificateCredential(tenantId, clientId, GetCertificate(certificateName, KeyVaultName)); + } + public static TokenCredential CreateServiceAccountAuthProvider(GraphCredentials creds) { return new UsernamePasswordCredential(creds.ServiceAccountUserName, creds.ServiceAccountPassword, creds.TenantId, creds.ClientId); From f12cdc3335722b6e051bfd51e0c4c3071c31da68 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Mon, 23 Sep 2024 10:10:25 -0700 Subject: [PATCH 0256/1479] Added FederatedCredential to the AuthenticationType enum. --- .../Common.DependencyInjection/AuthenticationType.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Common.DependencyInjection/AuthenticationType.cs b/Service/GroupMembershipManagement/Common.DependencyInjection/AuthenticationType.cs index 2b9b26cb1..fdbe3e28b 100644 --- a/Service/GroupMembershipManagement/Common.DependencyInjection/AuthenticationType.cs +++ b/Service/GroupMembershipManagement/Common.DependencyInjection/AuthenticationType.cs @@ -8,6 +8,7 @@ public enum AuthenticationType Unknown = 0, ClientSecret = 1, Certificate = 2, - UserAssignedManagedIdentity = 3 + UserAssignedManagedIdentity = 3, + FederatedIdentity = 4 } } From e39b51fbdfec94256e4f55ec827484f7e4a7505f Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 29 Aug 2024 10:55:37 -0700 Subject: [PATCH 0257/1479] Add azure maintance isolated worker model --- .../BackUpInactiveJobsFunction.cs | 5 +- .../EmailSender/EmailSenderFunction.cs | 5 +- .../ExpireNotificationsFunction.cs | 5 +- .../Activity/Logger/LoggerFunction.cs | 5 +- .../ReadGroupName/ReadGroupNameFunction.cs | 5 +- .../ReadSyncJobs/ReadSyncJobsFunction.cs | 5 +- .../RemoveBackUps/RemoveBackUpsFunction.cs | 5 +- .../RemoveInactiveJobsFunction.cs | 5 +- .../Function/AzureMaintenance.csproj | 9 +- .../Orchestrator/OrchestratorFunction.cs | 8 +- .../AzureMaintenance/Function/Program.cs | 104 ++++++++++++++++++ .../Function/Starter/StarterFunction.cs | 10 +- .../AzureMaintenance/Function/Startup.cs | 100 ----------------- .../Hosts/AzureMaintenance/Function/host.json | 3 +- 14 files changed, 138 insertions(+), 136 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/BackUpInactiveJobs/BackUpInactiveJobsFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/BackUpInactiveJobs/BackUpInactiveJobsFunction.cs index 7504db97c..511638fed 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/BackUpInactiveJobs/BackUpInactiveJobsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/BackUpInactiveJobs/BackUpInactiveJobsFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureMaintenance { @@ -21,7 +20,7 @@ public BackUpInactiveJobsFunction(ILoggingRepository loggingRepository, IAzureMa _azureMaintenanceService = azureMaintenanceService ?? throw new ArgumentNullException(nameof(azureMaintenanceService)); } - [FunctionName(nameof(BackUpInactiveJobsFunction))] + [Function(nameof(BackUpInactiveJobsFunction))] public async Task BackupInactiveJobsAsync([ActivityTrigger] List syncJobs) { int countOfBackUpJobs = 0; diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs index cbcc5652d..055d44019 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/EmailSender/EmailSenderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureMaintenance { @@ -22,7 +21,7 @@ public EmailSenderFunction(ILoggingRepository loggingRepository, _azureMaintenanceService = azureMaintenanceService ?? throw new ArgumentNullException(nameof(azureMaintenanceService)); } - [FunctionName(nameof(EmailSenderFunction))] + [Function(nameof(EmailSenderFunction))] public async Task SendEmailAsync([ActivityTrigger] EmailSenderRequest request) { if (request.SyncJob != null) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ExpireNotifications/ExpireNotificationsFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ExpireNotifications/ExpireNotificationsFunction.cs index 02ef3020b..30134edd5 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ExpireNotifications/ExpireNotificationsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ExpireNotifications/ExpireNotificationsFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureMaintenance { @@ -21,7 +20,7 @@ public ExpireNotificationsFunction(ILoggingRepository loggingRepository, IAzureM _azureMaintenanceService = azureMaintenanceService ?? throw new ArgumentNullException(nameof(azureMaintenanceService)); } - [FunctionName(nameof(ExpireNotificationsFunction))] + [Function(nameof(ExpireNotificationsFunction))] public async Task ExpireNotificationsAsync([ActivityTrigger] List syncJobs) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(ExpireNotificationsFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/Logger/LoggerFunction.cs index a7715fc3c..cbd94cb21 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/Logger/LoggerFunction.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureMaintenance { @@ -18,7 +17,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.RunId }, request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ReadGroupName/ReadGroupNameFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ReadGroupName/ReadGroupNameFunction.cs index 721cd57e0..29d6b66a0 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ReadGroupName/ReadGroupNameFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ReadGroupName/ReadGroupNameFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureMaintenance { @@ -20,7 +19,7 @@ public ReadGroupNameFunction(ILoggingRepository loggingRepository, IAzureMainten _azureMaintenanceService = azureMaintenanceService ?? throw new ArgumentNullException(nameof(azureMaintenanceService)); ; } - [FunctionName(nameof(ReadGroupNameFunction))] + [Function(nameof(ReadGroupNameFunction))] public async Task GetGroupNameAsync([ActivityTrigger] SyncJob syncJob) { var group = new SyncJobGroup(); diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ReadSyncJobs/ReadSyncJobsFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ReadSyncJobs/ReadSyncJobsFunction.cs index f74c44d0f..76c7c5e8e 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ReadSyncJobs/ReadSyncJobsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/ReadSyncJobs/ReadSyncJobsFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureMaintenance { @@ -21,7 +20,7 @@ public ReadSyncJobsFunction(ILoggingRepository loggingRepository, IAzureMaintena _azureMaintenanceService = azureMaintenanceService ?? throw new ArgumentNullException(nameof(azureMaintenanceService)); } - [FunctionName(nameof(ReadSyncJobsFunction))] + [Function(nameof(ReadSyncJobsFunction))] public async Task> GetSyncJobsAsync([ActivityTrigger] object obj) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(ReadSyncJobsFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/RemoveBackUps/RemoveBackUpsFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/RemoveBackUps/RemoveBackUpsFunction.cs index e9ed0b7a6..3d35dc866 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/RemoveBackUps/RemoveBackUpsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/RemoveBackUps/RemoveBackUpsFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureMaintenance { @@ -20,7 +19,7 @@ public RemoveBackUpsFunction(ILoggingRepository loggingRepository, IAzureMainten _azureMaintenanceService = azureMaintenanceService ?? throw new ArgumentNullException(nameof(azureMaintenanceService)); } - [FunctionName(nameof(RemoveBackUpsFunction))] + [Function(nameof(RemoveBackUpsFunction))] public async Task RemoveBackUpsAsync([ActivityTrigger] object obj) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(RemoveBackUpsFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/RemoveInactiveJobs/RemoveInactiveJobsFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/RemoveInactiveJobs/RemoveInactiveJobsFunction.cs index 3b23214fa..31cbc782a 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/RemoveInactiveJobs/RemoveInactiveJobsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Activity/RemoveInactiveJobs/RemoveInactiveJobsFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureMaintenance { @@ -21,7 +20,7 @@ public RemoveInactiveJobsFunction(ILoggingRepository loggingRepository, IAzureMa _azureMaintenanceService = azureMaintenanceService ?? throw new ArgumentNullException(nameof(azureMaintenanceService)); } - [FunctionName(nameof(RemoveInactiveJobsFunction))] + [Function(nameof(RemoveInactiveJobsFunction))] public async Task RemoveInactiveJobsAsync([ActivityTrigger] List syncJobs) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(RemoveInactiveJobsFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj index 61a978707..ac5850695 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj @@ -7,9 +7,14 @@ - - + + + + + + + diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs index 9c30f0d19..d19c0653b 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Orchestrator/OrchestratorFunction.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using System.Collections.Generic; using System.Threading.Tasks; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using Models; using Services.Contracts; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.AzureMaintenance { @@ -27,9 +27,9 @@ public OrchestratorFunction(IHandleInactiveJobsConfig handleInactiveJobsConfig, _azureMaintenanceService = azureMaintenanceService; } - [FunctionName(nameof(OrchestratorFunction))] + [Function(nameof(OrchestratorFunction))] public async Task RunOrchestrator( - [OrchestrationTrigger] IDurableOrchestrationContext context) + [OrchestrationTrigger] TaskOrchestrationContext context) { var runId = context.NewGuid(); diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Program.cs new file mode 100644 index 000000000..ad4f88b61 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Program.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.Extensions.Hosting; +using Azure.Core; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using DIConcreteTypes; +using Hosts.FunctionBase; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.GraphGroups; +using Repositories.ServiceBusQueue; +using Services; +using Services.Contracts; +using System; +using Azure.Identity; +using Repositories.EntityFramework; +using Repositories.NotificationsRepository; + +namespace Hosts.AzureMaintenance +{ + + public class Program + { + public static void Main (string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var SCHEMA_DIRECTORY = "JsonSchemas"; + var functionName = "AzureMaintenance"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + services.AddScoped(); + + services.AddOptions().Configure((settings, configuration) => + { + settings.HandleInactiveJobsEnabled = CommonServices.GetBoolSettingBase(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); + settings.NumberOfDaysBeforeDeletion = CommonServices.GetIntSettingBase(configuration, "AzureMaintenance:NumberOfDaysBeforeDeletion", 0); + }); + services.AddSingleton(services => + { + return new HandleInactiveJobsConfig( + services.GetService>().Value.HandleInactiveJobsEnabled, + services.GetService>().Value.NumberOfDaysBeforeDeletion); + }); + + services.AddOptions().Configure((settings, configuration) => + { + settings.IsThresholdNotificationEnabled = CommonServices.GetBoolSettingBase(configuration, "ThresholdNotification:IsThresholdNotificationEnabled", false); + }); + services.AddSingleton(services => + { + return new ThresholdNotificationConfig( + services.GetService>().Value.IsThresholdNotificationEnabled); + }); + + services.AddSingleton(); + + + services + .AddGraphAPIClient() + .AddScoped(); + + services.AddScoped(services => + { + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + var notificationsQueueRepository = new ServiceBusQueueRepository(sender); + + return new AzureMaintenanceService(services.GetService(), + services.GetService(), + services.GetService(), + services.GetService(), + services.GetService(), + notificationsQueueRepository, + services.GetService()); + }); + }) + . Build(); + host.Run(); + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Starter/StarterFunction.cs index 755619d88..86fa62a36 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Starter/StarterFunction.cs @@ -3,10 +3,10 @@ using System; using System.Threading.Tasks; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Logging; using Repositories.Contracts; +using Microsoft.DurableTask.Client; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureMaintenance { @@ -19,15 +19,15 @@ public StarterFunction(ILoggingRepository loggingRepository) } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task Run( [TimerTrigger("%maintenanceTriggerSchedule%")] TimerInfo myTimer, - [DurableClient] IDurableOrchestrationClient starter, + [DurableClient] DurableTaskClient client, ILogger log) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started" }, VerbosityLevel.DEBUG); - await starter.StartNewAsync(nameof(OrchestratorFunction), null); + await client.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), null); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed" }, VerbosityLevel.DEBUG); } diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs deleted file mode 100644 index 959598f79..000000000 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Startup.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using Azure.Messaging.ServiceBus; -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.AzureMaintenance; -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.EntityFramework; -using Repositories.GraphGroups; -using Repositories.NotificationsRepository; -using Repositories.ServiceBusQueue; -using Services; -using Services.Contracts; - -[assembly: FunctionsStartup(typeof(Startup))] - -namespace Hosts.AzureMaintenance -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(AzureMaintenance); - protected override string DryRunSettingName => string.Empty; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddScoped(); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - settings.HandleInactiveJobsEnabled = GetBoolSetting(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); - settings.NumberOfDaysBeforeDeletion = GetIntSetting(configuration, "AzureMaintenance:NumberOfDaysBeforeDeletion", 0); - }); - builder.Services.AddSingleton(services => - { - return new HandleInactiveJobsConfig( - services.GetService>().Value.HandleInactiveJobsEnabled, - services.GetService>().Value.NumberOfDaysBeforeDeletion); - }); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - settings.IsThresholdNotificationEnabled = GetBoolSetting(configuration, "ThresholdNotification:IsThresholdNotificationEnabled", false); - }); - builder.Services.AddSingleton(services => - { - return new ThresholdNotificationConfig( - services.GetService>().Value.IsThresholdNotificationEnabled); - }); - - builder.Services.AddSingleton(); - - - builder.Services - .AddGraphAPIClient() - .AddScoped(); - - builder.Services.AddScoped(services => - { - var configuration = services.GetRequiredService(); - var notificationsQueue = configuration["serviceBusNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(notificationsQueue); - var notificationsQueueRepository = new ServiceBusQueueRepository(sender); - - return new AzureMaintenanceService(services.GetService(), - services.GetService(), - services.GetService(), - services.GetService(), - services.GetService(), - notificationsQueueRepository, - services.GetService()); - }); - } - - private bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) - { - var checkParse = bool.TryParse(configuration[settingName], out bool value); - if (checkParse) - return value; - return defaultValue; - } - - private int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) - { - var checkParse = int.TryParse(configuration[settingName], out int value); - if (checkParse) - return value; - return defaultValue; - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/host.json b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/host.json index 3870ba50f..4cb56f151 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/host.json @@ -5,7 +5,8 @@ "applicationInsights": { "samplingExcludedTypes": "Request", "samplingSettings": { - "isEnabled": true + "isEnabled": true, + "excludedTypes": "Request" } } } From c74d172596c1aacb274324bef7fee490a376b50a Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 29 Aug 2024 12:15:13 -0700 Subject: [PATCH 0258/1479] add runtime --- .../Function/AzureMaintenance.csproj | 6 +- .../AzureMaintenance/Function/Program.cs | 83 +++++++++---------- .../Infrastructure/compute/template.bicep | 2 +- 3 files changed, 45 insertions(+), 46 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj index ac5850695..8ffa4f112 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj @@ -2,8 +2,8 @@ net8.0 v4 - Hosts.AzureMaintenance - Hosts.AzureMaintenance + Exe + enabled @@ -18,8 +18,8 @@ - + diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Program.cs index ad4f88b61..81dfcf41c 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/Program.cs @@ -44,60 +44,59 @@ public static void Main (string[] args) .ConfigureServices((context, services) => { var configuration = context.Configuration; - var SCHEMA_DIRECTORY = "JsonSchemas"; var functionName = "AzureMaintenance"; var dryRunSettingName = string.Empty; var rootPath = context.HostingEnvironment.ContentRootPath; CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); - services.AddScoped(); + services.AddScoped(); - services.AddOptions().Configure((settings, configuration) => - { - settings.HandleInactiveJobsEnabled = CommonServices.GetBoolSettingBase(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); - settings.NumberOfDaysBeforeDeletion = CommonServices.GetIntSettingBase(configuration, "AzureMaintenance:NumberOfDaysBeforeDeletion", 0); - }); - services.AddSingleton(services => - { - return new HandleInactiveJobsConfig( - services.GetService>().Value.HandleInactiveJobsEnabled, - services.GetService>().Value.NumberOfDaysBeforeDeletion); - }); + services.AddOptions().Configure((settings, configuration) => + { + settings.HandleInactiveJobsEnabled = CommonServices.GetBoolSettingBase(configuration, "AzureMaintenance:HandleInactiveJobsEnabled", false); + settings.NumberOfDaysBeforeDeletion = CommonServices.GetIntSettingBase(configuration, "AzureMaintenance:NumberOfDaysBeforeDeletion", 0); + }); + services.AddSingleton(services => + { + return new HandleInactiveJobsConfig( + services.GetService>().Value.HandleInactiveJobsEnabled, + services.GetService>().Value.NumberOfDaysBeforeDeletion); + }); - services.AddOptions().Configure((settings, configuration) => - { - settings.IsThresholdNotificationEnabled = CommonServices.GetBoolSettingBase(configuration, "ThresholdNotification:IsThresholdNotificationEnabled", false); - }); - services.AddSingleton(services => - { - return new ThresholdNotificationConfig( - services.GetService>().Value.IsThresholdNotificationEnabled); - }); + services.AddOptions().Configure((settings, configuration) => + { + settings.IsThresholdNotificationEnabled = CommonServices.GetBoolSettingBase(configuration, "ThresholdNotification:IsThresholdNotificationEnabled", false); + }); + services.AddSingleton(services => + { + return new ThresholdNotificationConfig( + services.GetService>().Value.IsThresholdNotificationEnabled); + }); - services.AddSingleton(); + services.AddSingleton(); - services - .AddGraphAPIClient() - .AddScoped(); + services + .AddGraphAPIClient() + .AddScoped(); - services.AddScoped(services => - { - var configuration = services.GetRequiredService(); - var notificationsQueue = configuration["serviceBusNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(notificationsQueue); - var notificationsQueueRepository = new ServiceBusQueueRepository(sender); + services.AddScoped(services => + { + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + var notificationsQueueRepository = new ServiceBusQueueRepository(sender); - return new AzureMaintenanceService(services.GetService(), - services.GetService(), - services.GetService(), - services.GetService(), - services.GetService(), - notificationsQueueRepository, - services.GetService()); - }); + return new AzureMaintenanceService(services.GetService(), + services.GetService(), + services.GetService(), + services.GetService(), + services.GetService(), + notificationsQueueRepository, + services.GetService()); + }); }) - . Build(); + .Build(); host.Run(); } } diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep index 2b3602706..cc453f68e 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep @@ -95,7 +95,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } From 03605865736b740be17484effeb5445223e82844 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 29 Aug 2024 12:19:57 -0700 Subject: [PATCH 0259/1479] add namespace --- .../Hosts/AzureMaintenance/Function/AzureMaintenance.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj index 8ffa4f112..21299f125 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Function/AzureMaintenance.csproj @@ -2,6 +2,8 @@ net8.0 v4 + Hosts.AzureMaintenance + Hosts.AzureMaintenance Exe enabled From 49e7a562fd5eb3b5c11c6a814f13329d5616658d Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 23 Aug 2024 14:34:21 -0700 Subject: [PATCH 0260/1479] Migrated JobScheduler to isolated model --- .../BatchUpdateJobsFunction.cs | 10 +-- .../CheckJobSchedulerStatusFunction.cs | 6 +- .../DistributeJobs/DistributeJobsFunction.cs | 14 ++- .../Activity/GetJobs/GetJobsFunction.cs | 12 ++- .../Activity/Logger/LoggerFunction.cs | 5 +- .../PostCallback/PostCallbackFunction.cs | 5 +- .../Activity/ResetJobs/ResetJobsFunction.cs | 14 ++- .../GetJobsSubOrchestratorFunction.cs | 8 +- .../JobScheduler/Function/JobScheduler.csproj | 14 ++- .../Orchestrator/OrchestratorFunction.cs | 8 +- .../Hosts/JobScheduler/Function/Program.cs | 87 +++++++++++++++++++ .../PipelineInvocationStarterFunction.cs | 41 +++++---- .../Function/Starter/StarterFunction.cs | 16 ++-- .../Hosts/JobScheduler/Function/Startup.cs | 67 -------------- .../StatusCallbackOrchestratorFunction.cs | 13 ++- .../StatusCallbackOrchestratorRequest.cs | 2 +- .../UpdateJobsSubOrchestratorFunction.cs | 10 +-- .../Hosts/JobScheduler/Function/host.json | 4 +- .../JobScheduler/Function/local.settings.json | 2 +- .../Infrastructure/compute/template.bicep | 2 +- 20 files changed, 178 insertions(+), 162 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/BatchUpdateJobs/BatchUpdateJobsFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/BatchUpdateJobs/BatchUpdateJobsFunction.cs index 8a03a8e78..fe105fb63 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/BatchUpdateJobs/BatchUpdateJobsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/BatchUpdateJobs/BatchUpdateJobsFunction.cs @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Repositories.Contracts; +using Services.Contracts; using System; -using System.Collections.Generic; using System.Threading.Tasks; -using Services.Contracts; -using Repositories.Contracts; namespace Hosts.JobScheduler { @@ -22,7 +20,7 @@ public BatchUpdateJobsFunction(IJobSchedulingService jobSchedulingService, ILogg _jobSchedulingService = jobSchedulingService ?? throw new ArgumentNullException(nameof(jobSchedulingService)); } - [FunctionName(nameof(BatchUpdateJobsFunction))] + [Function(nameof(BatchUpdateJobsFunction))] public async Task BatchUpdateJobsAsync([ActivityTrigger] BatchUpdateJobsRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(BatchUpdateJobsFunction)} function started at: {DateTime.UtcNow}" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/CheckJobSchedulerStatus/CheckJobSchedulerStatusFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/CheckJobSchedulerStatus/CheckJobSchedulerStatusFunction.cs index 625ba6872..c29791b62 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/CheckJobSchedulerStatus/CheckJobSchedulerStatusFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/CheckJobSchedulerStatus/CheckJobSchedulerStatusFunction.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask.Client; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Newtonsoft.Json; using Repositories.Contracts; using System; @@ -24,7 +24,7 @@ public CheckJobSchedulerStatusFunction(IHttpClientFactory httpClientFactory, ILo _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(CheckJobSchedulerStatusFunction))] + [Function(nameof(CheckJobSchedulerStatusFunction))] public async Task CheckStatusAsync([ActivityTrigger] CheckJobSchedulerStatusRequest request) { var completed = false; diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/DistributeJobs/DistributeJobsFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/DistributeJobs/DistributeJobsFunction.cs index 802605a39..c0fb8568d 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/DistributeJobs/DistributeJobsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/DistributeJobs/DistributeJobsFunction.cs @@ -1,15 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using System; -using System.Threading.Tasks; -using Services.Contracts; +using Microsoft.Azure.Functions.Worker; +using Models; using Repositories.Contracts; +using Services.Contracts; +using System; using System.Collections.Generic; -using Models; +using System.Threading.Tasks; namespace Hosts.JobScheduler { @@ -23,7 +21,7 @@ public DistributeJobsFunction(IJobSchedulingService jobSchedulingService, ILoggi _jobSchedulingService = jobSchedulingService ?? throw new ArgumentNullException(nameof(jobSchedulingService)); } - [FunctionName(nameof(DistributeJobsFunction))] + [Function(nameof(DistributeJobsFunction))] public async Task> DistributeJobsAsync([ActivityTrigger] DistributeJobsRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(DistributeJobsFunction)} function started at: {DateTime.UtcNow}" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/GetJobs/GetJobsFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/GetJobs/GetJobsFunction.cs index 33e425d00..d6cfef74d 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/GetJobs/GetJobsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/GetJobs/GetJobsFunction.cs @@ -1,15 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using System; -using System.Threading.Tasks; -using Services.Contracts; using Repositories.Contracts; +using Services.Contracts; +using System; using System.Linq; -using Entities; +using System.Threading.Tasks; namespace Hosts.JobScheduler { @@ -23,7 +21,7 @@ public GetJobsFunction(IJobSchedulingService jobSchedulingService, ILoggingRepos _jobSchedulingService = jobSchedulingService ?? throw new ArgumentNullException(nameof(jobSchedulingService)); } - [FunctionName(nameof(GetJobsFunction))] + [Function(nameof(GetJobsFunction))] public async Task GetJobsToUpdateAsync([ActivityTrigger] object request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GetJobsFunction)} function started at: {DateTime.UtcNow}" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/Logger/LoggerFunction.cs index a0a8dacae..b563e4de4 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/Logger/LoggerFunction.cs @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; @@ -19,7 +18,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.RunId },request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/PostCallback/PostCallbackFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/PostCallback/PostCallbackFunction.cs index f0fcb9e4b..4435bbfb7 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/PostCallback/PostCallbackFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/PostCallback/PostCallbackFunction.cs @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Net.Http; @@ -24,7 +23,7 @@ public PostCallbackFunction(IHttpClientFactory httpClientFactory, ILoggingReposi _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(PostCallbackFunction))] + [Function(nameof(PostCallbackFunction))] public async Task PostCallbackAsync([ActivityTrigger] PostCallbackRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(PostCallbackFunction)} function started at: {DateTime.UtcNow}" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/ResetJobs/ResetJobsFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/ResetJobs/ResetJobsFunction.cs index 2b5c5eb69..050cc4de3 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/ResetJobs/ResetJobsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/ResetJobs/ResetJobsFunction.cs @@ -1,15 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using System; -using System.Threading.Tasks; -using Services.Contracts; +using Microsoft.Azure.Functions.Worker; +using Models; using Repositories.Contracts; +using Services.Contracts; +using System; using System.Collections.Generic; -using Models; +using System.Threading.Tasks; namespace Hosts.JobScheduler { @@ -23,7 +21,7 @@ public ResetJobsFunction(IJobSchedulingService jobSchedulingService, ILoggingRep _jobSchedulingService = jobSchedulingService ?? throw new ArgumentNullException(nameof(jobSchedulingService)); } - [FunctionName(nameof(ResetJobsFunction))] + [Function(nameof(ResetJobsFunction))] public async Task> ResetJobsAsync([ActivityTrigger] ResetJobsRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(ResetJobsFunction)} function started at: {DateTime.UtcNow}" }); diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/GetJobsSubOrchestrator/GetJobsSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/GetJobsSubOrchestrator/GetJobsSubOrchestratorFunction.cs index 647272111..a7288e32f 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/GetJobsSubOrchestrator/GetJobsSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/GetJobsSubOrchestrator/GetJobsSubOrchestratorFunction.cs @@ -1,8 +1,8 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; using Models; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; @@ -20,8 +20,8 @@ public GetJobsSubOrchestratorFunction(IJobSchedulerConfig jobSchedulerConfig) _jobSchedulerConfig = jobSchedulerConfig; } - [FunctionName(nameof(GetJobsSubOrchestratorFunction))] - public async Task> RunSubOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(GetJobsSubOrchestratorFunction))] + public async Task> RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { await context.CallActivityAsync(nameof(LoggerFunction), new LoggerRequest diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj index e90d3711f..0634628c6 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj @@ -2,15 +2,18 @@ net8.0 v4 - Hosts.AzureMaintenance - Hosts.AzureMaintenance + Exe + enabled - - + + + + + @@ -27,4 +30,7 @@ Never + + + diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Orchestrator/OrchestratorFunction.cs index 93f8b69c2..8dff57f22 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Orchestrator/OrchestratorFunction.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; using Models; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; @@ -21,9 +21,9 @@ public OrchestratorFunction(IJobSchedulerConfig jobSchedulerConfig) _jobSchedulerConfig = jobSchedulerConfig ?? throw new ArgumentNullException(nameof(jobSchedulerConfig)); } - [FunctionName(nameof(OrchestratorFunction))] + [Function(nameof(OrchestratorFunction))] public async Task RunOrchestratorAsync( - [OrchestrationTrigger] IDurableOrchestrationContext context) + [OrchestrationTrigger] TaskOrchestrationContext context) { var runId = context.NewGuid(); diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Program.cs new file mode 100644 index 000000000..dd68c302d --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Program.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Azure.Identity; +using Azure.Monitor.Query; +using Hosts.FunctionBase; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Services; +using Services.Contracts; +using System; + +namespace Hosts.JobScheduler +{ + public class Program + { + public static void Main(string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var jobSchedulerConfigSettingName = "JobScheduler:JobSchedulerConfiguration"; + var configuration = context.Configuration; + var functionName = nameof(JobScheduler); + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + + services.AddOptions().Configure((settings, configuration) => + { + if (!string.IsNullOrEmpty(jobSchedulerConfigSettingName)) + { + settings.Value = configuration[jobSchedulerConfigSettingName]; + } + }); + + services.AddScoped(services => + { + var jsonString = services.GetService>().Value.Value; + var jobSchedulerConfig = JsonConvert.DeserializeObject(jsonString); + jobSchedulerConfig.WorkspaceId = CommonServices.GetValueOrThrowBase("logAnalyticsCustomerId"); + return jobSchedulerConfig; + }); + + services.AddScoped(services => + { + var config = services.GetService(); + return config.GetRunTimeFromLogs + ? new LogsRuntimeRetrievalService(config, new LogsQueryClient(new DefaultAzureCredential())) + : new DefaultRuntimeRetrievalService(config.DefaultRuntimeSeconds); + }); + + services.AddScoped(services => + { + return new JobSchedulingService( + services.GetService(), + services.GetService(), + services.GetService() + ); + }); + + services.AddHttpClient(); + + }).Build(); + + host.Run(); + } + } +} + diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/PipelineInvocationStarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/PipelineInvocationStarterFunction.cs index 769bb38eb..2235d1ed8 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/PipelineInvocationStarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/PipelineInvocationStarterFunction.cs @@ -1,19 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask.Client; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Extensions.Http; using Newtonsoft.Json; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; -using Services.Contracts; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; namespace Hosts.JobScheduler { @@ -28,35 +27,41 @@ public PipelineInvocationStarterFunction(IJobSchedulerConfig jobSchedulerConfig, _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(PipelineInvocationStarterFunction))] - public async Task HttpStart( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestMessage req, - [DurableClient] IDurableOrchestrationClient starter) + [Function(nameof(PipelineInvocationStarterFunction))] + public async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, + [DurableClient] DurableTaskClient starter) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(PipelineInvocationStarterFunction)} function started" }, VerbosityLevel.DEBUG); - var requestBody = JsonConvert.DeserializeObject>(await req.Content.ReadAsStringAsync()); + var requestBody = JsonConvert.DeserializeObject>(await req.ReadAsStringAsync()); var delayForDeploymentInMinutes = int.Parse(requestBody.GetValueOrDefault("DelayForDeploymentInMinutes")); - var instanceId = await starter.StartNewAsync(nameof(OrchestratorFunction), + var instanceId = await starter.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), new OrchestratorRequest { StartTimeDelayMinutes = delayForDeploymentInMinutes }); var response = starter.CreateCheckStatusResponse(req, instanceId); + var responseBody = string.Empty; + + using (var reader = new StreamReader(response.Body)) + { + responseBody = await reader.ReadToEndAsync(); + } - var responseDict = JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + var responseDict = JsonConvert.DeserializeObject>(responseBody); var statusQueryGetUri = responseDict.GetValueOrDefault("statusQueryGetUri"); if (req.Headers.Contains("PlanUrl")) - await starter.StartNewAsync(nameof(StatusCallbackOrchestratorFunction), GetCallbackRequest(req, statusQueryGetUri)); + await starter.ScheduleNewOrchestrationInstanceAsync(nameof(StatusCallbackOrchestratorFunction), GetCallbackRequest(req, statusQueryGetUri)); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(PipelineInvocationStarterFunction)} function completed" }, VerbosityLevel.DEBUG); return response; } - private StatusCallbackOrchestratorRequest GetCallbackRequest(HttpRequestMessage req, string statusUrl) + private StatusCallbackOrchestratorRequest GetCallbackRequest(HttpRequestData req, string statusUrl) { var url = req.Headers.GetValues("PlanUrl").FirstOrDefault("NULL"); var projectId = req.Headers.GetValues("ProjectId").FirstOrDefault("NULL"); diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/StarterFunction.cs index dd557527e..bae32fd7f 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/StarterFunction.cs @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System; -using System.Net.Http; -using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask.Client; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Extensions.Http; using Repositories.Contracts; +using System; +using System.Threading.Tasks; namespace Hosts.JobScheduler { @@ -21,13 +19,13 @@ public StarterFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task RunAsync( [TimerTrigger("%jobSchedulerSchedule%")] TimerInfo myTimer, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient starter) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started" }, VerbosityLevel.DEBUG); - await starter.StartNewAsync(nameof(OrchestratorFunction), null); + await starter.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction)); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed" }, VerbosityLevel.DEBUG); } } diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Startup.cs deleted file mode 100644 index 197c9281b..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Startup.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection; -using Repositories.Contracts; -using Hosts.JobScheduler; -using Services; -using Services.Contracts; -using Repositories.Contracts.InjectConfig; -using Newtonsoft.Json; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; -using Azure.Identity; -using Azure.Monitor.Query; - -[assembly: FunctionsStartup(typeof(Startup))] - -namespace Hosts.JobScheduler -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(JobScheduler); - protected override string DryRunSettingName => string.Empty; - protected string JobSchedulerConfigSettingName => "JobScheduler:JobSchedulerConfiguration"; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - if (!string.IsNullOrEmpty(JobSchedulerConfigSettingName)) - { - settings.Value = configuration[JobSchedulerConfigSettingName]; - } - }); - - builder.Services.AddScoped(services => - { - var jsonString = services.GetService>().Value.Value; - var jobSchedulerConfig = JsonConvert.DeserializeObject(jsonString); - jobSchedulerConfig.WorkspaceId = GetValueOrThrow("logAnalyticsCustomerId"); - return jobSchedulerConfig; - }); - - builder.Services.AddScoped(services => - { - var config = services.GetService(); - return config.GetRunTimeFromLogs - ? new LogsRuntimeRetrievalService(config, new LogsQueryClient(new DefaultAzureCredential())) - : new DefaultRuntimeRetrievalService(config.DefaultRuntimeSeconds); - }); - - builder.Services.AddScoped(services => - { - return new JobSchedulingService( - services.GetService(), - services.GetService(), - services.GetService() - ); - }); - - builder.Services.AddHttpClient(); - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorFunction.cs index 2ba75c3be..815cb31c2 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using System; -using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; using Repositories.Contracts; +using System; using System.Threading; -using Repositories.Contracts.InjectConfig; +using System.Threading.Tasks; namespace Hosts.JobScheduler { @@ -20,8 +19,8 @@ public StatusCallbackOrchestratorFunction() { } - [FunctionName(nameof(StatusCallbackOrchestratorFunction))] - public async Task RunStatusCallbackOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(StatusCallbackOrchestratorFunction))] + public async Task RunStatusCallbackOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { await context.CallActivityAsync(nameof(LoggerFunction), new LoggerRequest diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorRequest.cs index 2b7f1b631..f21189355 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorRequest.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs.Extensions.DurableTask; + using System.Net.Http; namespace Hosts.JobScheduler diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/UpdateJobsSubOrchestrator/UpdateJobsSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/UpdateJobsSubOrchestrator/UpdateJobsSubOrchestratorFunction.cs index 8a74a12fd..74cc849ef 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/UpdateJobsSubOrchestrator/UpdateJobsSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/UpdateJobsSubOrchestrator/UpdateJobsSubOrchestratorFunction.cs @@ -1,12 +1,10 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; using Repositories.Contracts; using System.Threading.Tasks; -using System.Collections.Generic; -using System.Linq; namespace Hosts.JobScheduler { @@ -16,8 +14,8 @@ public UpdateJobsSubOrchestratorFunction() { } - [FunctionName(nameof(UpdateJobsSubOrchestratorFunction))] - public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(UpdateJobsSubOrchestratorFunction))] + public async Task RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { await context.CallActivityAsync(nameof(LoggerFunction), new LoggerRequest diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/host.json b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/host.json index bb3b8dadd..c7ded769c 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/host.json @@ -2,9 +2,9 @@ "version": "2.0", "logging": { "applicationInsights": { - "samplingExcludedTypes": "Request", "samplingSettings": { - "isEnabled": true + "isEnabled": true, + "excludedTypes": "Request" } } } diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/local.settings.json index d975e0bae..936699308 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/local.settings.json @@ -2,7 +2,7 @@ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "logAnalyticsCustomerId": "", "logAnalyticsPrimarySharedKey": "", "ConnectionStrings:JobsContext": "", diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep index 4ddbb788f..ef987e2d5 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep @@ -104,7 +104,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } From 1e2a505b08b4445e2433b6eb8e53043c3ebe4915 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 26 Aug 2024 09:42:21 -0700 Subject: [PATCH 0261/1479] Updated function requests --- .../Hosts/AzureUserReader/Function/Program.cs | 59 +++++++++++++++++++ .../BatchUpdateJobs/BatchUpdateJobsRequest.cs | 2 +- .../DistributeJobs/DistributeJobsRequest.cs | 6 +- .../Activity/ResetJobs/ResetJobsRequest.cs | 4 +- .../JobScheduler/Function/JobScheduler.csproj | 2 +- .../Orchestrator/OrchestratorRequest.cs | 2 +- .../PipelineInvocationStarterFunction.cs | 4 +- .../StatusCallbackOrchestratorRequest.cs | 3 - .../UpdateJobsSubOrchestratorRequest.cs | 2 +- 9 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs new file mode 100644 index 000000000..547c917f7 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Azure.Identity; +using Common.DependencyInjection; +using DIConcreteTypes; +using Hosts.FunctionBase; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.GraphAzureADUsers; +using Services; +using Services.Contracts; +using System; + +namespace Hosts.AzureUserReader +{ + public class Program + { + public static void Main(string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = nameof(AzureUserReader); + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + + services.AddGraphAPIClient(); + services.AddSingleton(services => new StorageAccountSecret(CommonServices.GetValueOrThrowBase("storageAccountConnectionString"))); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + + }).Build(); + + host.Run(); + } + } + + +} diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/BatchUpdateJobs/BatchUpdateJobsRequest.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/BatchUpdateJobs/BatchUpdateJobsRequest.cs index f0d796efc..8b11e9900 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/BatchUpdateJobs/BatchUpdateJobsRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/BatchUpdateJobs/BatchUpdateJobsRequest.cs @@ -8,6 +8,6 @@ namespace Hosts.JobScheduler { public class BatchUpdateJobsRequest { - public IEnumerable SyncJobBatch; + public IEnumerable SyncJobBatch { get; set; } } } diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/DistributeJobs/DistributeJobsRequest.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/DistributeJobs/DistributeJobsRequest.cs index d7008e62f..e4a1c10e9 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/DistributeJobs/DistributeJobsRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/DistributeJobs/DistributeJobsRequest.cs @@ -8,8 +8,8 @@ namespace Hosts.JobScheduler { public class DistributeJobsRequest { - public List JobsToDistribute; - public int StartTimeDelayMinutes; - public int DelayBetweenSyncsSeconds; + public List JobsToDistribute { get; set; } + public int StartTimeDelayMinutes { get; set; } + public int DelayBetweenSyncsSeconds { get; set; } } } diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/ResetJobs/ResetJobsRequest.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/ResetJobs/ResetJobsRequest.cs index c84c549f3..6511a062a 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/ResetJobs/ResetJobsRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Activity/ResetJobs/ResetJobsRequest.cs @@ -8,7 +8,7 @@ namespace Hosts.JobScheduler { public class ResetJobsRequest { - public List JobsToReset; - public int DaysToAddForReset; + public List JobsToReset { get; set; } + public int DaysToAddForReset { get; set; } } } diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj index 0634628c6..ccad4fe98 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/JobScheduler.csproj @@ -13,7 +13,7 @@ - + diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Orchestrator/OrchestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Orchestrator/OrchestratorRequest.cs index d40758928..085965644 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Orchestrator/OrchestratorRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Orchestrator/OrchestratorRequest.cs @@ -5,6 +5,6 @@ namespace Hosts.JobScheduler { public class OrchestratorRequest { - public int StartTimeDelayMinutes; + public int StartTimeDelayMinutes { get; set; } } } diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/PipelineInvocationStarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/PipelineInvocationStarterFunction.cs index 2235d1ed8..bf58af02e 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/PipelineInvocationStarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/Starter/PipelineInvocationStarterFunction.cs @@ -42,9 +42,11 @@ public async Task HttpStart( { StartTimeDelayMinutes = delayForDeploymentInMinutes }); + var response = starter.CreateCheckStatusResponse(req, instanceId); - var responseBody = string.Empty; + response.Body.Position = 0; + var responseBody = string.Empty; using (var reader = new StreamReader(response.Body)) { responseBody = await reader.ReadToEndAsync(); diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorRequest.cs index f21189355..37fd242ed 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/StatusCallbackOrchestrator/StatusCallbackOrchestratorRequest.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. - -using System.Net.Http; - namespace Hosts.JobScheduler { public class StatusCallbackOrchestratorRequest diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/UpdateJobsSubOrchestrator/UpdateJobsSubOrchestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/UpdateJobsSubOrchestrator/UpdateJobsSubOrchestratorRequest.cs index 3a1abd04f..e4e979dde 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/UpdateJobsSubOrchestrator/UpdateJobsSubOrchestratorRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Function/UpdateJobsSubOrchestrator/UpdateJobsSubOrchestratorRequest.cs @@ -8,6 +8,6 @@ namespace Hosts.JobScheduler { public class UpdateJobsSubOrchestratorRequest { - public List JobsToUpdate; + public List JobsToUpdate { get; set; } } } From fded083390d83cc35cd85176157bacfd6688e823 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 26 Aug 2024 09:44:36 -0700 Subject: [PATCH 0262/1479] Updated functions code coverage exclusion filter --- yaml/build-functionapps.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yaml/build-functionapps.yml b/yaml/build-functionapps.yml index 1f1f87d91..f5b13e993 100644 --- a/yaml/build-functionapps.yml +++ b/yaml/build-functionapps.yml @@ -66,7 +66,8 @@ stages: /p:thresholdType=line /p:thresholdStat=total /p:CoverletOutput=$(Build.SourcesDirectory)\TestResults\Coverage\${{ app.function.name }}\ - /p:Exclude=[Repositories.*]*%2c[Entities]*%2c[*.Entities]*%2c[Models]*%2c[*.Models]*%2c[*.Tests]*%2c[*.Mocks]*%2c[Common.DependencyInjection]*%2c[Hosts.FunctionBase]*%2c[DIConcreteTypes]*%2c[*]Hosts.*.Startup' + /p:Exclude=[Repositories.*]*%2c[Entities]*%2c[*.Entities]*%2c[Models]*%2c[*.Models]*%2c[*.Tests]*%2c[*.Mocks]*%2c[Common.DependencyInjection]*%2c[Hosts.FunctionBase]*%2c[DIConcreteTypes]*%2c[*]Hosts.*.Startup*%2c[*]*.*.Program + /p:ExcludeByAttribute=Obsolete%2cGeneratedCodeAttribute%2cCompilerGeneratedAttribute' condition: and(succeeded(), eq(variables['BuildConfiguration'], 'debug')) - task: PublishCodeCoverageResults@1 From 50f124e113fefcfdbde5ea497b785c600b05ed53 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 26 Aug 2024 10:13:09 -0700 Subject: [PATCH 0263/1479] Removed unused file --- .../Hosts/AzureUserReader/Function/Program.cs | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs deleted file mode 100644 index 547c917f7..000000000 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using Azure.Identity; -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.FunctionBase; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphAzureADUsers; -using Services; -using Services.Contracts; -using System; - -namespace Hosts.AzureUserReader -{ - public class Program - { - public static void Main(string[] args) - { - var host = new HostBuilder() - .ConfigureFunctionsWorkerDefaults() - .ConfigureAppConfiguration((context, config) => - { - var settings = config.Build(); - var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); - - config.AddAzureAppConfiguration(options => - { - options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) - .UseFeatureFlags(); - }); - }) - .ConfigureServices((context, services) => - { - var configuration = context.Configuration; - var functionName = nameof(AzureUserReader); - var dryRunSettingName = string.Empty; - var rootPath = context.HostingEnvironment.ContentRootPath; - CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); - - services.AddGraphAPIClient(); - services.AddSingleton(services => new StorageAccountSecret(CommonServices.GetValueOrThrowBase("storageAccountConnectionString"))); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - - }).Build(); - - host.Run(); - } - } - - -} From ad0eb8a2b0e356167b48e5a117feff4510cd7769 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Thu, 29 Aug 2024 16:25:00 -0700 Subject: [PATCH 0264/1479] Migrated MembershipAggregator to isolated model --- .../DeltaCalculatorFunction.cs | 6 +- .../EmailSender/EmailSenderFunction.cs | 9 +- .../FileDownloader/FileDownloaderFunction.cs | 7 +- .../FileUploader/FileUploaderFunction.cs | 6 +- .../GroupNameReaderFunction.cs | 6 +- .../Activity/JobReader/JobReaderFunction.cs | 4 +- .../JobStatusUpdaterFunction.cs | 6 +- .../Activity/JobTracker/JobTrackerEntity.cs | 8 +- .../Activity/Logger/LoggerFunction.cs | 4 +- .../TelemetryTrackerFunction.cs | 6 +- .../TopicMessageSenderFunction.cs | 4 +- .../Function/MembershipAggregator.csproj | 13 +- .../MembershipSubOrchestratorFunction.cs | 17 +-- .../MembershipSubOrchestratorRequest.cs | 4 +- .../Orchestrator/OrchestratorFunction.cs | 30 ++-- .../MembershipAggregator/Function/Program.cs | 141 ++++++++++++++++++ .../Function/Starter/StarterFunction.cs | 9 +- .../MembershipAggregator/Function/Startup.cs | 124 --------------- .../Function/local.settings.json | 1 + .../Infrastructure/compute/template.bicep | 2 +- .../MembershipSubOrchestratorTests.cs | 55 +++---- .../Services.Tests/OrchestratorTests.cs | 76 +++++----- .../Services.Tests/StarterFunctionTests.cs | 16 +- 23 files changed, 287 insertions(+), 267 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs index 9dafdb720..16b01e6cc 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.WebJobs; using Models; using Models.Helpers; using Models.ServiceBus; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Newtonsoft.Json; using Repositories.Contracts; using Services.Contracts; @@ -30,7 +30,7 @@ public DeltaCalculatorFunction( _blobStorageRepository = blobStorageRepository; } - [FunctionName(nameof(DeltaCalculatorFunction))] + [Function(nameof(DeltaCalculatorFunction))] public async Task CalculateDeltaAsync([ActivityTrigger] DeltaCalculatorRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(DeltaCalculatorFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/EmailSender/EmailSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/EmailSender/EmailSenderFunction.cs index 987dbe17b..d692d6aab 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/EmailSender/EmailSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/EmailSender/EmailSenderFunction.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Models; using MembershipAggregator.Activity.EmailSender; +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Models; using Repositories.Contracts; using Services.Contracts; using System; @@ -22,7 +22,7 @@ public EmailSenderFunction(ILoggingRepository loggingRepository, IGraphAPIServic _graphAPIService = graphAPIService ?? throw new ArgumentNullException(nameof(graphAPIService)); ; } - [FunctionName(nameof(EmailSenderFunction))] + [Function(nameof(EmailSenderFunction))] public async Task SendEmailAsync([ActivityTrigger] EmailSenderRequest request) { var job = request.SyncJob; @@ -33,5 +33,4 @@ public async Task SendEmailAsync([ActivityTrigger] EmailSenderRequest request) } } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderFunction.cs index 7c910a6c8..c43394c5b 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderFunction.cs @@ -1,10 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.WebJobs; using Models; using Models.Helpers; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Newtonsoft.Json; using Repositories.Contracts; using System; using System.IO; @@ -23,7 +22,7 @@ public FileDownloaderFunction(ILoggingRepository loggingRepository, IBlobStorage _blobStorageRepository = blobStorageRepository ?? throw new ArgumentNullException(nameof(blobStorageRepository)); } - [FunctionName(nameof(FileDownloaderFunction))] + [Function(nameof(FileDownloaderFunction))] public async Task<(string FilePath, string Content)> DownloadFileAsync([ActivityTrigger] FileDownloaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Downloading file {request.FilePath}", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileUploader/FileUploaderFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileUploader/FileUploaderFunction.cs index 6e78ef504..667ca950c 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileUploader/FileUploaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileUploader/FileUploaderFunction.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.WebJobs; using Models; using Models.Helpers; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; @@ -21,7 +21,7 @@ public FileUploaderFunction(ILoggingRepository loggingRepository, IBlobStorageRe _blobStorageRepository = blobStorageRepository ?? throw new ArgumentNullException(nameof(blobStorageRepository)); } - [FunctionName(nameof(FileUploaderFunction))] + [Function(nameof(FileUploaderFunction))] public async Task UploadFileAsync([ActivityTrigger] FileUploaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Uploading file {request.FilePath}", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs index 2080726b0..d494fc2f9 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; @@ -20,11 +20,11 @@ public GroupNameReaderFunction(ILoggingRepository loggingRepository, IGraphAPISe _graphAPIService = graphAPIService ?? throw new ArgumentNullException(nameof(graphAPIService)); ; } - [FunctionName(nameof(GroupNameReaderFunction))] + [Function(nameof(GroupNameReaderFunction))] public async Task GetGroupNameAsync([ActivityTrigger] SyncJob syncJob) { var group = new SyncJobGroup(); - + if (syncJob != null) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupNameReaderFunction)} function started", RunId = syncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobReader/JobReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobReader/JobReaderFunction.cs index 599468f9e..07488f872 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobReader/JobReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobReader/JobReaderFunction.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; @@ -20,7 +20,7 @@ public JobReaderFunction(ILoggingRepository loggingRepository, IDatabaseSyncJobs _syncJobRepository = syncJobRepository ?? throw new ArgumentNullException(nameof(syncJobRepository)); } - [FunctionName(nameof(JobReaderFunction))] + [Function(nameof(JobReaderFunction))] public async Task GetSyncJobAsync([ActivityTrigger] JobReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs index 35cde3899..e06370215 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Models; +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; @@ -20,7 +20,7 @@ public JobStatusUpdaterFunction(ILoggingRepository loggingRepository, IDatabaseS _syncJobRepository = syncJobRespository ?? throw new ArgumentNullException(nameof(syncJobRespository)); } - [FunctionName(nameof(JobStatusUpdaterFunction))] + [Function(nameof(JobStatusUpdaterFunction))] public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobStatusUpdaterFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs index f686a649b..00ba97539 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs @@ -1,7 +1,7 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using System.Threading.Tasks; namespace Hosts.MembershipAggregator @@ -45,12 +45,12 @@ public Task SetTotalParts(int totalParts) public virtual async Task Delete() { - Entity.Current.DeleteState(); + JobState = null; await Task.CompletedTask; } - [FunctionName(nameof(JobTrackerEntity))] - public static Task RunAsync([EntityTrigger] IDurableEntityContext ctx) + [Function(nameof(JobTrackerEntity))] + public static Task RunAsync([EntityTrigger] TaskEntityDispatcher ctx) { return ctx.DispatchAsync(); } diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/Logger/LoggerFunction.cs index 312da654a..5b3cef66e 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/Logger/LoggerFunction.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; @@ -17,7 +17,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(request.Message, request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs index 414d79b60..9c2ced137 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Models; using Microsoft.ApplicationInsights; +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Models; using Repositories.Contracts; using System; using System.Collections.Generic; @@ -22,7 +22,7 @@ public TelemetryTrackerFunction(ILoggingRepository loggingRepository, TelemetryC _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - [FunctionName(nameof(TelemetryTrackerFunction))] + [Function(nameof(TelemetryTrackerFunction))] public async Task TrackEventAsync([ActivityTrigger] TelemetryTrackerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TelemetryTrackerFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/TopicMessageSender/TopicMessageSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/TopicMessageSender/TopicMessageSenderFunction.cs index 48cfa8278..9d08b802f 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/TopicMessageSender/TopicMessageSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/TopicMessageSender/TopicMessageSenderFunction.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Models.ServiceBus; using Newtonsoft.Json; @@ -23,7 +23,7 @@ public TopicMessageSenderFunction(ILoggingRepository loggingRepository, IService _serviceBusTopicsRepository = serviceBusTopicsRepository ?? throw new ArgumentNullException(nameof(serviceBusTopicsRepository)); } - [FunctionName(nameof(TopicMessageSenderFunction))] + [Function(nameof(TopicMessageSenderFunction))] public async Task SendMessageAsync([ActivityTrigger] MembershipHttpRequest request) { diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj index 707326d89..12f5bc11e 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj @@ -2,12 +2,14 @@ net8.0 v4 + Exe + enabled - - - - + + + + @@ -29,4 +31,7 @@ Never + + + diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs index 1d49247fd..9c364ab6f 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs @@ -3,8 +3,8 @@ using MembershipAggregator.Activity.EmailSender; using MembershipAggregator.Helpers; using Microsoft.ApplicationInsights; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; using Models; using Models.Helpers; using Models.Notifications; @@ -36,13 +36,12 @@ public MembershipSubOrchestratorFunction(IThresholdConfig thresholdConfig, IGrap _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - [FunctionName(nameof(MembershipSubOrchestratorFunction))] - public async Task RunMembershipSubOrchestratorFunctionAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(MembershipSubOrchestratorFunction))] + public async Task RunMembershipSubOrchestratorFunctionAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var request = context.GetInput(); var runId = request.SyncJob.RunId.GetValueOrDefault(Guid.Empty); - var proxy = context.CreateEntityProxy(request.EntityId); - var state = await proxy.GetState(); + var state = await context.Entities.CallEntityAsync(request.EntityId, "GetState"); var downloadFileTasks = new List>(); foreach (var part in state.CompletedParts) @@ -299,7 +298,7 @@ await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), return (sourceGroupMembership, destinationGroupMembership); } - private FileUploaderRequest CreateAggregatedFileUploaderRequest(GroupMembership membership, DeltaCalculatorResponse deltaResponse, SyncJob syncJob, IDurableOrchestrationContext context) + private FileUploaderRequest CreateAggregatedFileUploaderRequest(GroupMembership membership, DeltaCalculatorResponse deltaResponse, SyncJob syncJob, TaskOrchestrationContext context) { var membersToAdd = JsonConvert.DeserializeObject>(TextCompressor.Decompress(deltaResponse.CompressedMembersToAddJSON)); var membersToRemove = JsonConvert.DeserializeObject>(TextCompressor.Decompress(deltaResponse.CompressedMembersToRemoveJSON)); @@ -321,13 +320,13 @@ private FileUploaderRequest CreateAggregatedFileUploaderRequest(GroupMembership return new FileUploaderRequest { FilePath = filePath, Content = content, SyncJob = syncJob }; } - private string GenerateFileName(SyncJob syncJob, string suffix, IDurableOrchestrationContext context) + private string GenerateFileName(SyncJob syncJob, string suffix, TaskOrchestrationContext context) { var timeStamp = context.CurrentUtcDateTime.ToString("MMddyyyy-HHmm"); return $"/{syncJob.TargetOfficeGroupId}/{timeStamp}_{syncJob.RunId}_{suffix}.json"; } - private void TrackSyncCompleteEvent(IDurableOrchestrationContext context, SyncJob syncJob, SyncCompleteCustomEvent syncCompleteEvent, string successStatus) + private void TrackSyncCompleteEvent(TaskOrchestrationContext context, SyncJob syncJob, SyncCompleteCustomEvent syncCompleteEvent, string successStatus) { var timeElapsedForJob = (context.CurrentUtcDateTime - syncJob.LastSuccessfulStartTime).TotalSeconds; _telemetryClient.TrackMetric(nameof(Services.Entities.Metric.SyncJobTimeElapsedSeconds), timeElapsedForJob); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorRequest.cs index 3eea378ae..06c4597b8 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorRequest.cs @@ -1,7 +1,7 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask.Entities; using Models; namespace Hosts.MembershipAggregator @@ -9,6 +9,6 @@ namespace Hosts.MembershipAggregator public class MembershipSubOrchestratorRequest { public SyncJob SyncJob { get; set; } - public EntityId EntityId { get; set; } + public EntityInstanceId EntityId { get; set; } } } diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Orchestrator/OrchestratorFunction.cs index 3b585912a..6ad08f1f2 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Orchestrator/OrchestratorFunction.cs @@ -1,19 +1,14 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Azure; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Primitives; using Models; -using Newtonsoft.Json; using Repositories.Contracts; using Services.Entities; using System; -using System.Collections.Generic; using System.IO; -using System.Net; -using System.Net.Http; using System.Threading.Tasks; namespace Hosts.MembershipAggregator @@ -29,26 +24,25 @@ public OrchestratorFunction(IConfiguration configuration, ILoggingRepository log _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(OrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(OrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var request = context.GetInput(); var runId = request.SyncJob.RunId.GetValueOrDefault(Guid.Empty); - var entityId = new EntityId(nameof(JobTrackerEntity), $"{request.SyncJob.TargetOfficeGroupId}_{runId}"); - var proxy = context.CreateEntityProxy(entityId); + var entityId = new EntityInstanceId(nameof(JobTrackerEntity), $"{request.SyncJob.TargetOfficeGroupId}_{runId}"); var hasSourceCompleted = false; var errorOccurred = false; try { - using (await context.LockAsync(entityId)) + await using (await context.Entities.LockEntitiesAsync(entityId)) { - await proxy.SetTotalParts(request.PartsCount); - await proxy.AddCompletedPart(request.FilePath); - hasSourceCompleted = await proxy.IsComplete(); + await context.Entities.CallEntityAsync(entityId, "SetTotalParts", request.PartsCount); + await context.Entities.CallEntityAsync(entityId, "AddCompletedPart", request.FilePath); + hasSourceCompleted = await context.Entities.CallEntityAsync(entityId, "IsComplete"); if (request.IsDestinationPart) - await proxy.SetDestinationPart(request.FilePath); + await context.Entities.CallEntityAsync(entityId, "SetDestinationPart", request.FilePath); } if (hasSourceCompleted) @@ -142,7 +136,7 @@ await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), { if (hasSourceCompleted || errorOccurred) { - await proxy.Delete(); + await context.Entities.CallEntityAsync(entityId, "Delete"); } _loggingRepository.RemoveSyncJobProperties(runId); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Program.cs new file mode 100644 index 000000000..14fcd2081 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Program.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Azure.Identity; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using DIConcreteTypes; +using Hosts.FunctionBase; +using Microsoft.ApplicationInsights; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Repositories.BlobStorage; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.GraphGroups; +using Repositories.ServiceBusQueue; +using Repositories.ServiceBusTopics; +using Services; +using Services.Contracts; +using System; + +namespace Hosts.MembershipAggregator +{ + public class Program + { + public static void Main(string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = nameof(MembershipAggregator); + var dryRunSettingName = "MembershipAggregator:IsMembershipAggregatorDryRunEnabled"; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + + services.AddOptions().Configure((settings, configuration) => + { + settings.MaximumNumberOfThresholdRecipients = CommonServices.GetIntSettingBase(configuration, "MaximumNumberOfThresholdRecipients", 10); + settings.NumberOfThresholdViolationsToNotify = CommonServices.GetIntSettingBase(configuration, "NumberOfThresholdViolationsToNotify", 3); + settings.NumberOfThresholdViolationsFollowUps = CommonServices.GetIntSettingBase(configuration, "NumberOfThresholdViolationsFollowUps", 3); + settings.NumberOfThresholdViolationsToDisableJob = CommonServices.GetIntSettingBase(configuration, "NumberOfThresholdViolationsToDisableJob", 10); + }); + + services.AddGraphAPIClient() + .AddScoped() + .AddScoped((services) => + { + var loggingRepository = services.GetRequiredService(); + var graphGroupRepository = services.GetRequiredService(); + + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + var notificationsQueueRepository = new ServiceBusQueueRepository(sender); + + return new GraphAPIService( + loggingRepository, + graphGroupRepository, + notificationsQueueRepository + ); + }) + .AddSingleton(services => + { + return new ThresholdConfig + ( + services.GetService>().Value.MaximumNumberOfThresholdRecipients, + services.GetService>().Value.NumberOfThresholdViolationsToNotify, + services.GetService>().Value.NumberOfThresholdViolationsFollowUps, + services.GetService>().Value.NumberOfThresholdViolationsToDisableJob + ); + }) + .AddSingleton((s) => + { + var configuration = s.GetService(); + var storageAccountName = configuration["membershipStorageAccountName"]; + var containerName = configuration["membershipContainerName"]; + + return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); + }) + .AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var membershipAggregatorQueue = configuration["serviceBusMembershipUpdatersTopic"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(membershipAggregatorQueue); + return new ServiceBusTopicsRepository(sender); + }) + .AddScoped((services) => + { + var syncJobRepository = services.GetRequiredService(); + var loggingRepository = services.GetRequiredService(); + var graphAPIService = services.GetRequiredService(); + var dryRun = services.GetRequiredService(); + var telemetryClient = services.GetRequiredService(); + var thresholdConfig = services.GetRequiredService(); + var thresholdNotificationConfig = services.GetRequiredService(); + var notificationRepository = services.GetRequiredService(); + + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + var notificationsQueueRepository = new ServiceBusQueueRepository(sender); + + return new DeltaCalculatorService( + syncJobRepository, + loggingRepository, + graphAPIService, + dryRun, + thresholdConfig, + thresholdNotificationConfig, + notificationRepository, + notificationsQueueRepository, + telemetryClient + ); + }); + + + }).Build(); + + host.Run(); + } + } +} + diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Starter/StarterFunction.cs index d2c317e2f..e5fcb8b67 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Starter/StarterFunction.cs @@ -1,8 +1,9 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. using Azure.Messaging.ServiceBus; +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask.Client; using Models; using Newtonsoft.Json; using Repositories.Contracts; @@ -21,10 +22,10 @@ public StarterFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName("ServiceBusStarterFunction")] + [Function("ServiceBusStarterFunction")] public async Task ProcessServiceBusMessageAsync( [ServiceBusTrigger("%serviceBusMembershipAggregatorQueue%", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient starter) { var request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(message.Body)); var runId = request.SyncJob.RunId.GetValueOrDefault(Guid.Empty); @@ -33,7 +34,7 @@ public async Task ProcessServiceBusMessageAsync( await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started", RunId = runId }, VerbosityLevel.DEBUG); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Processing message {message.MessageId}", RunId = runId }, VerbosityLevel.INFO); - var instanceId = await starter.StartNewAsync(nameof(OrchestratorFunction), request); + var instanceId = await starter.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), request); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"InstanceId: {instanceId}", RunId = runId }, VerbosityLevel.DEBUG); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed", RunId = runId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Startup.cs deleted file mode 100644 index fdc437c31..000000000 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Startup.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Azure.Messaging.ServiceBus; -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Repositories.BlobStorage; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphGroups; -using Repositories.ServiceBusQueue; -using Repositories.ServiceBusTopics; -using Services; -using Services.Contracts; -using Microsoft.ApplicationInsights; - -[assembly: FunctionsStartup(typeof(Hosts.MembershipAggregator.Startup))] - -namespace Hosts.MembershipAggregator -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(MembershipAggregator); - protected override string DryRunSettingName => "MembershipAggregator:IsMembershipAggregatorDryRunEnabled"; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - settings.MaximumNumberOfThresholdRecipients = GetIntSetting(configuration, "MaximumNumberOfThresholdRecipients", 10); - settings.NumberOfThresholdViolationsToNotify = GetIntSetting(configuration, "NumberOfThresholdViolationsToNotify", 3); - settings.NumberOfThresholdViolationsFollowUps = GetIntSetting(configuration, "NumberOfThresholdViolationsFollowUps", 3); - settings.NumberOfThresholdViolationsToDisableJob = GetIntSetting(configuration, "NumberOfThresholdViolationsToDisableJob", 10); - }); - - builder.Services.AddGraphAPIClient() - .AddScoped() - .AddScoped((services) => - { - var loggingRepository = services.GetRequiredService(); - var graphGroupRepository = services.GetRequiredService(); - - var configuration = services.GetRequiredService(); - var notificationsQueue = configuration["serviceBusNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(notificationsQueue); - var notificationsQueueRepository = new ServiceBusQueueRepository(sender); - - return new GraphAPIService( - loggingRepository, - graphGroupRepository, - notificationsQueueRepository - ); - }) - .AddSingleton(services => - { - return new ThresholdConfig - ( - services.GetService>().Value.MaximumNumberOfThresholdRecipients, - services.GetService>().Value.NumberOfThresholdViolationsToNotify, - services.GetService>().Value.NumberOfThresholdViolationsFollowUps, - services.GetService>().Value.NumberOfThresholdViolationsToDisableJob - ); - }) - .AddSingleton((s) => - { - var configuration = s.GetService(); - var storageAccountName = configuration["membershipStorageAccountName"]; - var containerName = configuration["membershipContainerName"]; - - return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); - }) - .AddSingleton(services => - { - var configuration = services.GetRequiredService(); - var membershipAggregatorQueue = configuration["serviceBusMembershipUpdatersTopic"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(membershipAggregatorQueue); - return new ServiceBusTopicsRepository(sender); - }) - .AddScoped((services) => - { - var syncJobRepository = services.GetRequiredService(); - var loggingRepository = services.GetRequiredService(); - var graphAPIService = services.GetRequiredService(); - var dryRun = services.GetRequiredService(); - var telemetryClient = services.GetRequiredService(); - var thresholdConfig = services.GetRequiredService(); - var thresholdNotificationConfig = services.GetRequiredService(); - var notificationRepository = services.GetRequiredService(); - - var configuration = services.GetRequiredService(); - var notificationsQueue = configuration["serviceBusNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(notificationsQueue); - var notificationsQueueRepository = new ServiceBusQueueRepository(sender); - - return new DeltaCalculatorService( - syncJobRepository, - loggingRepository, - graphAPIService, - dryRun, - thresholdConfig, - thresholdNotificationConfig, - notificationRepository, - notificationsQueueRepository, - telemetryClient - ); - }); - } - - private int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) - { - var isParsed = int.TryParse(configuration[settingName], out var maximumNumberOfThresholdRecipients); - return isParsed ? maximumNumberOfThresholdRecipients : defaultValue; - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json index ac7479840..9b5d194c8 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/local.settings.json @@ -1,6 +1,7 @@ { "IsEncrypted": false, "Values": { + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "APPINSIGHTS_INSTRUMENTATIONKEY": "", "AzureWebJobsStorage": "UseDevelopmentStorage=true", "appConfigurationEndpoint": "https://-appconfig-.azconfig.io", diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep index 7f6ff5701..83ef8ffe8 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep @@ -121,7 +121,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs index 11b356a8a..86e326ab4 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs @@ -5,19 +5,17 @@ using MembershipAggregator.Activity.EmailSender; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Graph.Models.Security; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Models.Notifications; using Models.ServiceBus; -using Models.ThresholdNotifications; using Moq; using Newtonsoft.Json; using Polly; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; -using Repositories.ServiceBusQueue; using Services.Contracts; using Services.Entities; using System; @@ -56,7 +54,7 @@ public class MembershipSubOrchestratorTests private Mock _loggingRepository; private Mock _syncJobRepository; private Mock _emailSenderRecipient; - private Mock _durableContext; + private Mock _durableContext; private Mock _blobStorageRepository; private Mock _localizationRepository; private Mock _notificationRepository; @@ -69,7 +67,7 @@ public void SetupTest() _thresholdNotificationConfig = new Mock(); _loggingRepository = new Mock(); _syncJobRepository = new Mock(); - _durableContext = new Mock(); + _durableContext = new Mock(); _blobStorageRepository = new Mock(); _graphAPIService = new Mock(); _notificationRepository = new Mock(); @@ -126,7 +124,7 @@ public void SetupTest() }; _membershipSubOrchestratorRequest = new MembershipSubOrchestratorRequest { - EntityId = new EntityId(), + EntityId = new EntityInstanceId(), SyncJob = _syncJob }; @@ -229,53 +227,56 @@ public void SetupTest() _graphAPIService.Setup(x => x.GetGroupNameAsync(It.IsAny())) .ReturnsAsync(() => "GroupName"); - _durableContext.Setup(x => x.CallActivityAsync(It.Is(s => s == nameof(GroupNameReaderFunction)), It.IsAny())) + _durableContext.Setup(x => x.CallActivityAsync(It.Is(s => s.Name == nameof(GroupNameReaderFunction)), It.IsAny(), It.IsAny())) .ReturnsAsync(_groupInformation); _durableContext.Setup(x => x.GetInput()) .Returns(() => _membershipSubOrchestratorRequest); - - _durableContext.Setup(x => x.CreateEntityProxy(It.IsAny())) - .Returns(() => _jobTrackerEntity); - - _durableContext.Setup(x => x.CallActivityAsync<(string FilePath, string Content)>(It.Is(x => x == nameof(FileDownloaderFunction)), It.IsAny())) - .Callback(async (name, request) => + + _durableContext.Setup(x => x.CallActivityAsync<(string FilePath, string Content)>(It.Is(x => x.Name == nameof(FileDownloaderFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _downloaderResponse = await CallFileDownloaderFunctionAsync(request as FileDownloaderRequest); }) .ReturnsAsync(() => _downloaderResponse); - _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(DeltaCalculatorFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x.Name == nameof(DeltaCalculatorFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _deltaCalculatorResponse = await CallDeltaCalculatorFunctionAsync(request as DeltaCalculatorRequest); }) .ReturnsAsync(() => _deltaCalculatorResponse); - _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(FileUploaderFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x.Name == nameof(FileUploaderFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallFileUploaderFunctionAsync(request as FileUploaderRequest); }); - _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(LoggerFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x.Name == nameof(LoggerFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallLoggerFunctionAsync(request as LoggerRequest); }); - _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(JobStatusUpdaterFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x.Name == nameof(JobStatusUpdaterFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallJobStatusUpdaterFunctionAsync(request as JobStatusUpdaterRequest); }); - _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(EmailSenderFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x.Name == nameof(EmailSenderFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallEmailSenderFunctionAsync(request as EmailSenderRequest); }); - _durableContext.Setup(x => x.CallActivityAsync(nameof(JobReaderFunction), It.IsAny())).ReturnsAsync(() => _syncJob); + _durableContext.Setup(x => x.CallActivityAsync(nameof(JobReaderFunction), It.IsAny(), It.IsAny())).ReturnsAsync(() => _syncJob); + + var entitiesMock = new Mock(); + entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), "GetState", null, null)) + .ReturnsAsync(() => _jobState); + + _durableContext.Setup(x => x.Entities).Returns(entitiesMock.Object); } [TestMethod] @@ -568,7 +569,7 @@ public async Task ProcessMembershipFromFilesForLargeSyncsAsync() _syncJob.ThresholdPercentageForRemovals = -1; _numberOfUsersForSourcePart = 50000; - var contextMock = new Mock(); + var contextMock = new Mock(); _membersPerFile.Add(GenerateFileName(_syncJob, "SourceMembership", contextMock.Object), 100000); _membersPerFile.Add(GenerateFileName(_syncJob, "DestinationMembership", contextMock.Object), 0); @@ -858,7 +859,7 @@ private async Task CallDeltaCalculatorFunctionAsync(Del return await function.CalculateDeltaAsync(request); } - private string GenerateFileName(SyncJob syncJob, string suffix, IDurableOrchestrationContext context) + private string GenerateFileName(SyncJob syncJob, string suffix, TaskOrchestrationContext context) { var timeStamp = context.CurrentUtcDateTime.ToString("MMddyyyy-HHmm"); return $"/{syncJob.TargetOfficeGroupId}/{timeStamp}_{syncJob.RunId}_{suffix}.json"; diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs index c6763c142..3e217bade 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs @@ -4,7 +4,7 @@ using Hosts.MembershipAggregator; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; + using Microsoft.Extensions.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -15,6 +15,8 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; namespace Services.Tests { @@ -27,20 +29,18 @@ public class OrchestratorTests private TelemetryClient _telemetryClient; private Mock _configuration; - private Mock _jobTrackerEntity; private Mock _loggingRepository; private Mock _syncJobRepository; - private Mock _durableContext; + private Mock _durableContext; private Mock _serviceBusTopicsRepository; [TestInitialize] public void SetupTest() { _configuration = new Mock(); - _jobTrackerEntity = new Mock(); _loggingRepository = new Mock(); _syncJobRepository = new Mock(); - _durableContext = new Mock(); + _durableContext = new Mock(); _telemetryClient = new TelemetryClient(new TelemetryConfiguration()); _serviceBusTopicsRepository = new Mock(); @@ -84,25 +84,22 @@ public void SetupTest() _durableContext.Setup(x => x.GetInput()) .Returns(() => _membershipAggregatorHttpRequest); - _durableContext.Setup(x => x.CreateEntityProxy(It.IsAny())) - .Returns(() => _jobTrackerEntity.Object); - - _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(TelemetryTrackerFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x.Name == nameof(TelemetryTrackerFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var telemetryRequest = request as TelemetryTrackerRequest; await CallTelemetryTrackerFunctionAsync(telemetryRequest); }); - _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(LoggerFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x.Name == nameof(LoggerFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var loggerRequest = request as LoggerRequest; await CallLoggerFunctionAsync(loggerRequest); }); - _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(JobStatusUpdaterFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x.Name == nameof(JobStatusUpdaterFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var updateRequest = request as JobStatusUpdaterRequest; await CallJobStatusUpdaterFunctionAsync(updateRequest); @@ -110,17 +107,23 @@ public void SetupTest() _durableContext.Setup(x => x.CallSubOrchestratorAsync ( - It.Is(x => x == nameof(MembershipSubOrchestratorFunction)), - It.IsAny()) + It.Is(x => x.Name == nameof(MembershipSubOrchestratorFunction)), + It.IsAny(), + It.IsAny()) ) .ReturnsAsync(() => _membershipSubOrchestratorResponse); - _durableContext.Setup(x => x.CallActivityAsync(nameof(TopicMessageSenderFunction), It.IsAny())) - .Callback(async (name, request) => + _durableContext.Setup(x => x.CallActivityAsync(nameof(TopicMessageSenderFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var membershipRequest = request as MembershipHttpRequest; await CallTopicMessageSenderFunctionAsync(membershipRequest); }); + + var entitiesMock = new Mock(); + entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), It.Is(x => x == "IsComplete"), null, null)) + .ReturnsAsync(() => true); + _durableContext.Setup(x => x.Entities).Returns(entitiesMock.Object); } [TestMethod] @@ -129,24 +132,25 @@ public async Task TestJobWithSinglePartAsync() var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); await orchestratorFunction.RunOrchestratorAsync(_durableContext.Object); - Assert.IsNull(_jobTrackerEntity.Object.JobState.DestinationPart); _loggingRepository.Verify(x => x.LogMessageAsync(It.Is(m => m.Message.StartsWith("Sent message")), VerbosityLevel.INFO, It.IsAny(), It.IsAny())); _syncJobRepository.Verify(x => x.UpdateSyncJobsAsync(It.IsAny>(), It.IsAny()), Times.Never()); - _jobTrackerEntity.Verify(x => x.Delete(), Times.Once()); } [TestMethod] public async Task TestMissingPartAsync() { + var entitiesMock = new Mock(); + entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), It.Is(x => x == "IsComplete"), null, null)) + .ReturnsAsync(() => false); + _durableContext.Setup(x => x.Entities).Returns(entitiesMock.Object); + _membershipAggregatorHttpRequest.PartsCount = 2; var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); await orchestratorFunction.RunOrchestratorAsync(_durableContext.Object); - Assert.IsNull(_jobTrackerEntity.Object.JobState.DestinationPart); _loggingRepository.Verify(x => x.LogMessageAsync(It.Is(m => m.Message.StartsWith("Sent message")), VerbosityLevel.INFO, It.IsAny(), It.IsAny()), Times.Never()); _syncJobRepository.Verify(x => x.UpdateSyncJobsAsync(It.IsAny>(), It.IsAny()), Times.Never()); - _jobTrackerEntity.Verify(x => x.Delete(), Times.Never()); } [TestMethod] @@ -157,11 +161,8 @@ public async Task TestDestinationPartAsync() var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); await orchestratorFunction.RunOrchestratorAsync(_durableContext.Object); - Assert.IsNotNull(_jobTrackerEntity.Object.JobState.DestinationPart); - Assert.AreEqual(_membershipAggregatorHttpRequest.FilePath, _jobTrackerEntity.Object.JobState.DestinationPart); _loggingRepository.Verify(x => x.LogMessageAsync(It.Is(m => m.Message.StartsWith("Sent message")), VerbosityLevel.INFO, It.IsAny(), It.IsAny())); _syncJobRepository.Verify(x => x.UpdateSyncJobsAsync(It.IsAny>(), It.IsAny()), Times.Never()); - _jobTrackerEntity.Verify(x => x.Delete(), Times.Once()); } [TestMethod] @@ -175,7 +176,6 @@ public async Task TestNotSuccessMembershipDeltaStatusAsync() _loggingRepository.Verify(x => x.LogMessageAsync(It.Is(m => m.Message == "Calling GraphUpdater"), VerbosityLevel.INFO, It.IsAny(), It.IsAny()), Times.Never); _loggingRepository.Verify(x => x.LogMessageAsync(It.Is(m => m.Message.StartsWith("GraphUpdater response Code")), VerbosityLevel.INFO, It.IsAny(), It.IsAny()), Times.Never); _syncJobRepository.Verify(x => x.UpdateSyncJobsAsync(It.IsAny>(), It.IsAny()), Times.Never()); - _jobTrackerEntity.Verify(x => x.Delete(), Times.Once()); } [TestMethod] @@ -183,8 +183,9 @@ public async Task HandleFileNotFoundAsync() { _durableContext.Setup(x => x.CallSubOrchestratorAsync ( - It.Is(x => x == nameof(MembershipSubOrchestratorFunction)), - It.IsAny()) + nameof(MembershipSubOrchestratorFunction), + It.IsAny(), + It.IsAny()) ) .Throws(); @@ -194,12 +195,11 @@ public async Task HandleFileNotFoundAsync() _loggingRepository.Verify(x => x.LogMessageAsync(It.Is(m => m.Message == "Calling GraphUpdater"), VerbosityLevel.INFO, It.IsAny(), It.IsAny()), Times.Never()); _loggingRepository.Verify(x => x.LogMessageAsync(It.Is(m => m.Message.StartsWith("GraphUpdater response Code")), VerbosityLevel.INFO, It.IsAny(), It.IsAny()), Times.Never()); _durableContext.Verify(x => x.CallActivityAsync( - It.Is(x => x == nameof(JobStatusUpdaterFunction)), - It.Is(x => x.Status == SyncStatus.FileNotFound) + It.Is(x => x.Name == nameof(JobStatusUpdaterFunction)), + It.Is(x => x.Status == SyncStatus.FileNotFound), + It.IsAny() ) , Times.Once()); - - _jobTrackerEntity.Verify(x => x.Delete(), Times.Once()); } [TestMethod] @@ -207,8 +207,9 @@ public async Task HandleUnexpectedExceptionAsync() { _durableContext.Setup(x => x.CallSubOrchestratorAsync ( - It.Is(x => x == nameof(MembershipSubOrchestratorFunction)), - It.IsAny()) + It.Is(x => x.Name == nameof(MembershipSubOrchestratorFunction)), + It.IsAny(), + It.IsAny()) ) .Throws(); @@ -219,12 +220,11 @@ public async Task HandleUnexpectedExceptionAsync() _loggingRepository.Verify(x => x.LogMessageAsync(It.Is(m => m.Message.StartsWith("GraphUpdater response Code")), VerbosityLevel.INFO, It.IsAny(), It.IsAny()), Times.Never()); _loggingRepository.Verify(x => x.LogMessageAsync(It.Is(m => m.Message.StartsWith("Unexpected exception")), VerbosityLevel.INFO, It.IsAny(), It.IsAny()), Times.Once()); _durableContext.Verify(x => x.CallActivityAsync( - It.Is(x => x == nameof(JobStatusUpdaterFunction)), - It.Is(x => x.Status == SyncStatus.Error) + It.Is(x => x.Name == nameof(JobStatusUpdaterFunction)), + It.Is(x => x.Status == SyncStatus.Error), + It.IsAny() ) , Times.Once()); - - _jobTrackerEntity.Verify(x => x.Delete(), Times.Once()); } private async Task CallTelemetryTrackerFunctionAsync(TelemetryTrackerRequest request) diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/StarterFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/StarterFunctionTests.cs index 73859fb8d..cccf9acc1 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/StarterFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/StarterFunctionTests.cs @@ -3,16 +3,16 @@ using Azure.Messaging.ServiceBus; using Hosts.MembershipAggregator; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Moq; using Newtonsoft.Json; using Repositories.Contracts; using System; -using System.Net; -using System.Net.Http; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Services.Tests @@ -23,14 +23,14 @@ public class StarterFunctionTests private SyncJob _syncJob; private string _instanceId; private Mock _loggingRepository; - private Mock _durableClient; + private Mock _durableClient; [TestInitialize] public void SetupTest() { _instanceId = "1234567890"; _loggingRepository = new Mock(); - _durableClient = new Mock(); + _durableClient = new Mock("test"); _syncJob = new SyncJob { Id = Guid.NewGuid(), @@ -44,7 +44,11 @@ public void SetupTest() }; _durableClient - .Setup(x => x.StartNewAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ScheduleNewOrchestrationInstanceAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + )) .ReturnsAsync(_instanceId); } From 9917e4a2c94acdf744518970c0fce530ff01ee8a Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 30 Aug 2024 10:55:52 -0700 Subject: [PATCH 0265/1479] Updated tests and test coverage for MA --- .../Activity/JobTracker/JobTrackerEntity.cs | 1 - .../MembershipSubOrchestratorTests.cs | 6 +-- .../Services.Tests/OrchestratorTests.cs | 39 ++++++++++++++++--- vsts-cicd.yml | 2 +- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs index 00ba97539..76e5cd8f3 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs @@ -1,7 +1,6 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.WebJobs; using System.Threading.Tasks; namespace Hosts.MembershipAggregator diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs index 86e326ab4..a19fde9ba 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs @@ -232,7 +232,7 @@ public void SetupTest() _durableContext.Setup(x => x.GetInput()) .Returns(() => _membershipSubOrchestratorRequest); - + _durableContext.Setup(x => x.CallActivityAsync<(string FilePath, string Content)>(It.Is(x => x.Name == nameof(FileDownloaderFunction)), It.IsAny(), It.IsAny())) .Callback(async (name, request, options) => { @@ -274,8 +274,8 @@ public void SetupTest() var entitiesMock = new Mock(); entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), "GetState", null, null)) - .ReturnsAsync(() => _jobState); - + .Returns(async () => await _jobTrackerEntity.GetState()); + _durableContext.Setup(x => x.Entities).Returns(entitiesMock.Object); } diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs index 3e217bade..cefa3f282 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs @@ -4,19 +4,18 @@ using Hosts.MembershipAggregator; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; - +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; using Models; +using Moq; using Repositories.Contracts; using Services.Entities; using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using Microsoft.DurableTask; -using Microsoft.DurableTask.Entities; namespace Services.Tests { @@ -24,6 +23,7 @@ namespace Services.Tests public class OrchestratorTests { private SyncJob _syncJob; + private JobTrackerEntity _jobTrackerEntity; private MembershipAggregatorHttpRequest _membershipAggregatorHttpRequest; private MembershipSubOrchestratorResponse _membershipSubOrchestratorResponse; private TelemetryClient _telemetryClient; @@ -43,6 +43,7 @@ public void SetupTest() _durableContext = new Mock(); _telemetryClient = new TelemetryClient(new TelemetryConfiguration()); _serviceBusTopicsRepository = new Mock(); + _jobTrackerEntity = new JobTrackerEntity(); var targetOfficeGroupId = Guid.NewGuid(); _syncJob = new SyncJob @@ -121,8 +122,34 @@ public void SetupTest() }); var entitiesMock = new Mock(); + + entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), "SetTotalParts", It.IsAny(), null)) + .Callback(async (entityId, operationName, request, options) => + { + await _jobTrackerEntity.SetTotalParts((int)request); + }); + + entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), "AddCompletedPart", It.IsAny(), null)) + .Callback(async (entityId, operationName, request, options) => + { + await _jobTrackerEntity.AddCompletedPart((string)request); + }); + + entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), "SetDestinationPart", It.IsAny(), null)) + .Callback(async (entityId, operationName, request, options) => + { + await _jobTrackerEntity.SetDestinationPart((string)request); + }); + + entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), "Delete", null, null)) + .Callback(async (entityId, operationName, request, options) => + { + await _jobTrackerEntity.Delete(); + }); + + entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), It.Is(x => x == "IsComplete"), null, null)) - .ReturnsAsync(() => true); + .Returns(async () => await _jobTrackerEntity.IsComplete()); _durableContext.Setup(x => x.Entities).Returns(entitiesMock.Object); } @@ -184,7 +211,7 @@ public async Task HandleFileNotFoundAsync() _durableContext.Setup(x => x.CallSubOrchestratorAsync ( nameof(MembershipSubOrchestratorFunction), - It.IsAny(), + It.IsAny(), It.IsAny()) ) .Throws(); diff --git a/vsts-cicd.yml b/vsts-cicd.yml index d2e04f6c0..ad891bc4f 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -92,7 +92,7 @@ stages: coverageThreshold: 48 - function: name: 'MembershipAggregator' - coverageThreshold: 85 + coverageThreshold: 82 - function: name: 'Notifier' coverageThreshold: 75 From ad1e8a1f28a9777bc6759a59cd09448673e9d99e Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 3 Sep 2024 13:22:57 -0700 Subject: [PATCH 0266/1479] Updated DurableEntity logic --- .../FileDownloader/FileDownloaderFunction.cs | 9 ++++-- .../FileDownloader/FileDownloaderResponse.cs | 11 +++++++ .../Activity/JobTracker/IJobTracker.cs | 1 - .../Activity/JobTracker/JobTrackerEntity.cs | 30 +++++++++---------- .../MembershipSubOrchestratorFunction.cs | 13 ++++---- .../Function/Starter/StarterFunction.cs | 1 - .../Services.Entities/Metric.cs | 2 +- .../Services.Entities.csproj | 1 + .../SyncCompleteCustomEvent.cs | 2 +- .../Services.Tests/JobTrackerEntityFake.cs | 15 ++++++++++ .../MembershipSubOrchestratorTests.cs | 16 +++++----- .../Services.Tests/OrchestratorTests.cs | 17 +++++------ 12 files changed, 71 insertions(+), 47 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderResponse.cs create mode 100644 Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/JobTrackerEntityFake.cs diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderFunction.cs index c43394c5b..40e7a5be3 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderFunction.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.WebJobs; using Models; using Models.Helpers; using Repositories.Contracts; @@ -23,7 +22,7 @@ public FileDownloaderFunction(ILoggingRepository loggingRepository, IBlobStorage } [Function(nameof(FileDownloaderFunction))] - public async Task<(string FilePath, string Content)> DownloadFileAsync([ActivityTrigger] FileDownloaderRequest request) + public async Task DownloadFileAsync([ActivityTrigger] FileDownloaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Downloading file {request.FilePath}", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); @@ -37,7 +36,11 @@ public FileDownloaderFunction(ILoggingRepository loggingRepository, IBlobStorage var compressedContent = TextCompressor.Compress(content); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Downloaded file {request.FilePath}", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); - return (request.FilePath, compressedContent); + return new FileDownloaderResponse + { + FilePath = request.FilePath, + Content = compressedContent + }; } } } diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderResponse.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderResponse.cs new file mode 100644 index 000000000..dfde07512 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/FileDownloader/FileDownloaderResponse.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Hosts.MembershipAggregator +{ + public class FileDownloaderResponse + { + public string FilePath { get; set; } + public string Content { get; set; } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/IJobTracker.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/IJobTracker.cs index 88ecf6e1d..a43cde7e3 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/IJobTracker.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/IJobTracker.cs @@ -11,6 +11,5 @@ public interface IJobTracker Task GetState(); Task IsComplete(); Task SetTotalParts(int totalParts); - Task Delete(); } } diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs index 76e5cd8f3..aa4258017 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobTracker/JobTrackerEntity.cs @@ -1,57 +1,55 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask.Entities; using System.Threading.Tasks; namespace Hosts.MembershipAggregator { - public class JobTrackerEntity : IJobTracker + public class JobTrackerEntity : TaskEntity, IJobTracker { - public JobState JobState { get; set; } = new JobState(); - public Task AddCompletedPart(string filePath) { - if (!JobState.CompletedParts.Contains(filePath)) - JobState.CompletedParts.Add(filePath); + if (!State.CompletedParts.Contains(filePath)) + State.CompletedParts.Add(filePath); return Task.CompletedTask; } public Task SetDestinationPart(string filePath) { - JobState.DestinationPart = filePath; + State.DestinationPart = filePath; return Task.CompletedTask; } public Task GetState() { - return Task.FromResult(JobState); + return Task.FromResult(State); } public Task IsComplete() { - var allPartsCompleted = JobState.TotalParts > 0 - && JobState.CompletedParts.Count == JobState.TotalParts; + var allPartsCompleted = State.TotalParts > 0 + && State.CompletedParts.Count == State.TotalParts; return Task.FromResult(allPartsCompleted); } public Task SetTotalParts(int totalParts) { - JobState.TotalParts = totalParts; + State.TotalParts = totalParts; return Task.CompletedTask; } - public virtual async Task Delete() + [Function(nameof(JobTrackerEntity))] + public static Task Run([EntityTrigger] TaskEntityDispatcher ctx) { - JobState = null; - await Task.CompletedTask; + return ctx.DispatchAsync(); } - [Function(nameof(JobTrackerEntity))] - public static Task RunAsync([EntityTrigger] TaskEntityDispatcher ctx) + protected override JobState InitializeState(TaskEntityOperation entityOperation) { - return ctx.DispatchAsync(); + return new JobState(); } } } diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs index 9c364ab6f..d71623d1c 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using MembershipAggregator.Activity.EmailSender; using MembershipAggregator.Helpers; +using MembershipAggretatorEntities = MembershipAggregator.Services.Entities; using Microsoft.ApplicationInsights; using Microsoft.Azure.Functions.Worker; using Microsoft.DurableTask; @@ -42,12 +43,12 @@ public async Task RunMembershipSubOrchestrato var request = context.GetInput(); var runId = request.SyncJob.RunId.GetValueOrDefault(Guid.Empty); var state = await context.Entities.CallEntityAsync(request.EntityId, "GetState"); - var downloadFileTasks = new List>(); + var downloadFileTasks = new List>(); foreach (var part in state.CompletedParts) { var downloadRequest = new FileDownloaderRequest { FilePath = part, SyncJob = request.SyncJob }; - downloadFileTasks.Add(context.CallActivityAsync<(string FilePath, string Content)>(nameof(FileDownloaderFunction), downloadRequest)); + downloadFileTasks.Add(context.CallActivityAsync(nameof(FileDownloaderFunction), downloadRequest)); } var completedDownloadTasks = await Task.WhenAll(downloadFileTasks); @@ -252,7 +253,7 @@ await context.CallActivityAsync(nameof(LoggerFunction), if (!context.IsReplaying) TrackSyncCompleteEvent(context, dbSyncJob, syncCompleteEvent, "Success"); - + await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), new JobStatusUpdaterRequest { @@ -270,7 +271,7 @@ await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), } private (GroupMembership SourceMembership, GroupMembership DestinationMembership) - ExtractMembershipInformationAsync((string FilePath, string Content)[] allGroupMemberships, string destinationPath) + ExtractMembershipInformationAsync(FileDownloaderResponse[] allGroupMemberships, string destinationPath) { var sourceGroupsMemberships = allGroupMemberships .Where(x => x.FilePath != destinationPath) @@ -329,7 +330,7 @@ private string GenerateFileName(SyncJob syncJob, string suffix, TaskOrchestratio private void TrackSyncCompleteEvent(TaskOrchestrationContext context, SyncJob syncJob, SyncCompleteCustomEvent syncCompleteEvent, string successStatus) { var timeElapsedForJob = (context.CurrentUtcDateTime - syncJob.LastSuccessfulStartTime).TotalSeconds; - _telemetryClient.TrackMetric(nameof(Services.Entities.Metric.SyncJobTimeElapsedSeconds), timeElapsedForJob); + _telemetryClient.TrackMetric(nameof(MembershipAggretatorEntities.Metric.SyncJobTimeElapsedSeconds), timeElapsedForJob); syncCompleteEvent.SyncJobTimeElapsedSeconds = timeElapsedForJob.ToString(); syncCompleteEvent.Result = successStatus; @@ -338,7 +339,7 @@ private void TrackSyncCompleteEvent(TaskOrchestrationContext context, SyncJob sy .GetProperties(BindingFlags.Instance | BindingFlags.Public) .ToDictionary(prop => prop.Name, prop => (string)prop.GetValue(syncCompleteEvent, null)); - _telemetryClient.TrackEvent(nameof(Services.Entities.Metric.SyncComplete), syncCompleteDict); + _telemetryClient.TrackEvent(nameof(MembershipAggretatorEntities.Metric.SyncComplete), syncCompleteDict); } } } diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Starter/StarterFunction.cs index e5fcb8b67..9b646890a 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Starter/StarterFunction.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Azure.Messaging.ServiceBus; using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.WebJobs; using Microsoft.DurableTask.Client; using Models; using Newtonsoft.Json; diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Metric.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Metric.cs index 92a917f08..174a5c7ee 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Metric.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Metric.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -namespace Services.Entities +namespace MembershipAggregator.Services.Entities { public enum Metric { diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Services.Entities.csproj b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Services.Entities.csproj index 902a7e1ea..3a71fbcc6 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Services.Entities.csproj +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/Services.Entities.csproj @@ -2,6 +2,7 @@ net8.0 + MembershipAggregator.Services.Entities diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/SyncCompleteCustomEvent.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/SyncCompleteCustomEvent.cs index 8ae81ed0d..ee3fe0a36 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/SyncCompleteCustomEvent.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/SyncCompleteCustomEvent.cs @@ -2,7 +2,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license.using System; -namespace Services.Entities +namespace MembershipAggregator.Services.Entities { public class SyncCompleteCustomEvent { diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/JobTrackerEntityFake.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/JobTrackerEntityFake.cs new file mode 100644 index 000000000..31cc8c109 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/JobTrackerEntityFake.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Hosts.MembershipAggregator; + +namespace Services.Tests +{ + public class JobTrackerEntityFake : JobTrackerEntity + { + public void SetState(JobState state) + { + State = state; + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs index a19fde9ba..24031c607 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs @@ -36,12 +36,12 @@ public class MembershipSubOrchestratorTests private int _numberOfUsersForSourcePart; private int _numberOfUsersForSourcePartOne; private int _numberOfUsersForSourcePartTwo; - private JobTrackerEntity _jobTrackerEntity; + private JobTrackerEntityFake _jobTrackerEntity; private int _numberOfUsersForDestinationPart; private Dictionary _membersPerFile; private DeltaCalculatorService _deltaCalculatorService; private DeltaCalculatorResponse _deltaCalculatorResponse; - private (string FilePath, string Content) _downloaderResponse; + private FileDownloaderResponse _downloaderResponse; private MembershipSubOrchestratorRequest _membershipSubOrchestratorRequest; private TelemetryClient _telemetryClient; private SyncJobGroup _groupInformation; @@ -140,12 +140,10 @@ public void SetupTest() TotalParts = 3 }; - _jobTrackerEntity = new JobTrackerEntity - { - JobState = _jobState - }; + _jobTrackerEntity = new JobTrackerEntityFake(); + _jobTrackerEntity.SetState(_jobState); - _downloaderResponse = (null, null); + _downloaderResponse = new FileDownloaderResponse(); _blobStorageRepository.Setup(x => x.DownloadFileAsync(It.Is(x => x.StartsWith("http://file-path")))) .Callback(path => @@ -233,7 +231,7 @@ public void SetupTest() _durableContext.Setup(x => x.GetInput()) .Returns(() => _membershipSubOrchestratorRequest); - _durableContext.Setup(x => x.CallActivityAsync<(string FilePath, string Content)>(It.Is(x => x.Name == nameof(FileDownloaderFunction)), It.IsAny(), It.IsAny())) + _durableContext.Setup(x => x.CallActivityAsync(It.Is(x => x.Name == nameof(FileDownloaderFunction)), It.IsAny(), It.IsAny())) .Callback(async (name, request, options) => { _downloaderResponse = await CallFileDownloaderFunctionAsync(request as FileDownloaderRequest); @@ -823,7 +821,7 @@ public async Task TestNoMembershipChangesAsync() Assert.AreEqual(MembershipDeltaStatus.NoChanges, response.MembershipDeltaStatus); } - private async Task<(string FilePath, string Content)> CallFileDownloaderFunctionAsync(FileDownloaderRequest request) + private async Task CallFileDownloaderFunctionAsync(FileDownloaderRequest request) { var function = new FileDownloaderFunction(_loggingRepository.Object, _blobStorageRepository.Object); return await function.DownloadFileAsync(request); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs index cefa3f282..662b76a0b 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs @@ -19,11 +19,13 @@ namespace Services.Tests { + [TestClass] public class OrchestratorTests { private SyncJob _syncJob; - private JobTrackerEntity _jobTrackerEntity; + private JobState _jobState; + private JobTrackerEntityFake _jobTrackerEntity; private MembershipAggregatorHttpRequest _membershipAggregatorHttpRequest; private MembershipSubOrchestratorResponse _membershipSubOrchestratorResponse; private TelemetryClient _telemetryClient; @@ -43,7 +45,10 @@ public void SetupTest() _durableContext = new Mock(); _telemetryClient = new TelemetryClient(new TelemetryConfiguration()); _serviceBusTopicsRepository = new Mock(); - _jobTrackerEntity = new JobTrackerEntity(); + _jobState = new JobState(); + _jobTrackerEntity = new JobTrackerEntityFake(); + _jobTrackerEntity.SetState(_jobState); + var targetOfficeGroupId = Guid.NewGuid(); _syncJob = new SyncJob @@ -141,15 +146,9 @@ public void SetupTest() await _jobTrackerEntity.SetDestinationPart((string)request); }); - entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), "Delete", null, null)) - .Callback(async (entityId, operationName, request, options) => - { - await _jobTrackerEntity.Delete(); - }); - - entitiesMock.Setup(x => x.CallEntityAsync(It.IsAny(), It.Is(x => x == "IsComplete"), null, null)) .Returns(async () => await _jobTrackerEntity.IsComplete()); + _durableContext.Setup(x => x.Entities).Returns(entitiesMock.Object); } From 41de2b2ab9797f204c547336543391d96ab4ef03 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 3 Sep 2024 14:06:01 -0700 Subject: [PATCH 0267/1479] Updated namespace for MA entities --- .../Activity/DeltaCalculator/DeltaCalculatorResponse.cs | 2 +- .../Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs | 3 ++- .../Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs | 2 +- .../Function/MembershipAggregator.csproj | 1 - .../MembershipSubOrchestratorFunction.cs | 4 ++-- .../MembershipSubOrchestratorResponse.cs | 2 +- .../Function/Orchestrator/OrchestratorFunction.cs | 2 +- .../Services.Contracts/IDeltaCalculatorService.cs | 2 +- .../MembershipAggregator/Services.Entities/DeltaResponse.cs | 2 +- .../Services.Entities/MembershipDeltaStatus.cs | 2 +- .../Services.Tests/MembershipSubOrchestratorTests.cs | 1 + .../MembershipAggregator/Services.Tests/OrchestratorTests.cs | 4 ++-- .../MembershipAggregator/Services/DeltaCalculatorService.cs | 5 ++--- vsts-cicd.yml | 2 +- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorResponse.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorResponse.cs index 16636b24c..651ea6425 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorResponse.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorResponse.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Services.Entities; +using MembershipAggregator.Services.Entities; namespace Hosts.MembershipAggregator { diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs index e06370215..2ca51f622 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using MembershipAggregator.Services.Entities; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; using Models; @@ -38,7 +39,7 @@ public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest { syncJob.LastRunTime = currentDate; - if (request.DeltaStatus == Services.Entities.MembershipDeltaStatus.NoChanges) + if (request.DeltaStatus == MembershipDeltaStatus.NoChanges) { if (syncJob.IgnoreThresholdOnce) syncJob.IgnoreThresholdOnce = false; diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs index d623af25f..9f613e6f9 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/JobStatusUpdater/JobStatusUpdaterRequest.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using MembershipAggregator.Services.Entities; using Models; -using Services.Entities; namespace Hosts.MembershipAggregator { diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj index 12f5bc11e..391785ee7 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj @@ -19,7 +19,6 @@ - diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs index d71623d1c..aaef59493 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorFunction.cs @@ -13,13 +13,13 @@ using Newtonsoft.Json; using Repositories.Contracts.InjectConfig; using Services.Contracts; -using Services.Entities; using System; using System.Collections.Generic; using System.Data.SqlTypes; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using MembershipAggregator.Services.Entities; namespace Hosts.MembershipAggregator { @@ -220,7 +220,7 @@ await context.CallActivityAsync(nameof(LoggerFunction), var sourceTypeCounts = JsonParser.GetQueryTypes(request.SyncJob.Query); var destination = JsonParser.GetDestination(request.SyncJob.Destination); - var syncCompleteEvent = new SyncCompleteCustomEvent + var syncCompleteEvent = new MembershipAggretatorEntities.SyncCompleteCustomEvent { Type = destination.Type.ToString(), SourceTypesCounts = sourceTypeCounts, diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorResponse.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorResponse.cs index a9d7543a1..54e2e26ab 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorResponse.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipSubOrchestrator/MembershipSubOrchestratorResponse.cs @@ -1,6 +1,6 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Services.Entities; +using MembershipAggregator.Services.Entities; namespace Hosts.MembershipAggregator { diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Orchestrator/OrchestratorFunction.cs index 6ad08f1f2..889166d4c 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Orchestrator/OrchestratorFunction.cs @@ -1,12 +1,12 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. +using MembershipAggregator.Services.Entities; using Microsoft.Azure.Functions.Worker; using Microsoft.DurableTask; using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Configuration; using Models; using Repositories.Contracts; -using Services.Entities; using System; using System.IO; using System.Threading.Tasks; diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Contracts/IDeltaCalculatorService.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Contracts/IDeltaCalculatorService.cs index f3e33d739..92eace27e 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Contracts/IDeltaCalculatorService.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Contracts/IDeltaCalculatorService.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models.ServiceBus; -using Services.Entities; +using MembershipAggregator.Services.Entities; using System; using System.Threading.Tasks; diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/DeltaResponse.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/DeltaResponse.cs index e9a83521c..26a347fd2 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/DeltaResponse.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/DeltaResponse.cs @@ -3,7 +3,7 @@ using Models; using System.Collections.Generic; -namespace Services.Entities +namespace MembershipAggregator.Services.Entities { public class DeltaResponse { diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/MembershipDeltaStatus.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/MembershipDeltaStatus.cs index 621bc145c..aea5dfe82 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/MembershipDeltaStatus.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Entities/MembershipDeltaStatus.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -namespace Services.Entities +namespace MembershipAggregator.Services.Entities { public enum MembershipDeltaStatus { diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs index 24031c607..9b9a32e44 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/MembershipSubOrchestratorTests.cs @@ -3,6 +3,7 @@ using Hosts.MembershipAggregator; using MembershipAggregator.Activity.EmailSender; +using MembershipAggregator.Services.Entities; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.DurableTask; diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs index 662b76a0b..5ee342434 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services.Tests/OrchestratorTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Hosts.MembershipAggregator; +using MembershipAggregator.Services.Entities; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.DurableTask; @@ -11,7 +12,6 @@ using Models; using Moq; using Repositories.Contracts; -using Services.Entities; using System; using System.Collections.Generic; using System.IO; @@ -194,7 +194,7 @@ public async Task TestDestinationPartAsync() [TestMethod] public async Task TestNotSuccessMembershipDeltaStatusAsync() { - _membershipSubOrchestratorResponse.MembershipDeltaStatus = Entities.MembershipDeltaStatus.Error; + _membershipSubOrchestratorResponse.MembershipDeltaStatus = MembershipDeltaStatus.Error; var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); await orchestratorFunction.RunOrchestratorAsync(_durableContext.Object); diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/DeltaCalculatorService.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/DeltaCalculatorService.cs index 91888fb3a..7fb6e28cc 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/DeltaCalculatorService.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Services/DeltaCalculatorService.cs @@ -7,7 +7,6 @@ using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using Services.Contracts; -using Services.Entities; using System; using System.Collections.Generic; using System.Diagnostics; @@ -15,6 +14,7 @@ using System.Threading.Tasks; using Microsoft.ApplicationInsights; using System.Data.SqlTypes; +using MembershipAggregator.Services.Entities; namespace Services { @@ -313,7 +313,6 @@ private async Task SendThresholdNotification(ThresholdResult threshold, SyncJob : NotificationMessageType.NormalThresholdNotification; var messageId = $"{job.Id}_{job.RunId}_{messageType}"; - var message = new ServiceBusMessage { MessageId = messageId, @@ -326,7 +325,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage Message = $"Sent message {message.MessageId} to service bus notifications queue ", RunId = job.RunId }); - } + } private async Task CloseUnresolvedThresholdNotificationAsync(SyncJob job) { if (_thresholdNotificationConfig.IsThresholdNotificationEnabled) diff --git a/vsts-cicd.yml b/vsts-cicd.yml index ad891bc4f..45bc8c36c 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -92,7 +92,7 @@ stages: coverageThreshold: 48 - function: name: 'MembershipAggregator' - coverageThreshold: 82 + coverageThreshold: 81 - function: name: 'Notifier' coverageThreshold: 75 From a227a0a75814e13f77878acdb7837eca0d356b0b Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Thu, 29 Aug 2024 14:38:18 -0700 Subject: [PATCH 0268/1479] Migrated NonProdService to isolated model --- .../GroupCreatorAndRetrieverFunction.cs | 4 +- .../GroupUpdater/GroupUpdaterFunction.cs | 6 +- .../LoadTestingGroupCalculatorFunction.cs | 9 ++- .../LoadTestingSyncJobCreatorFunction.cs | 8 +-- .../LoadTestingSyncJobRetrieverFunction.cs | 5 +- .../Activity/Logger/LoggerFunction.cs | 6 +- .../TenantUserCountFunction.cs | 4 +- .../TenantUserReaderFunction.cs | 4 +- .../GroupUpdaterSubOrchestratorFunction.cs | 17 +++-- ...ationTestingPrepSubOrchestratorFunction.cs | 7 +- .../LoadTestingPrepSubOrchestratorFunction.cs | 7 +- .../Function/NonProdService.csproj | 14 ++-- .../Orchestrator/OrchestratorFunction.cs | 7 +- .../Hosts/NonProdService/Function/Program.cs | 66 +++++++++++++++++++ .../Function/Starter/StarterFunction.cs | 15 ++--- .../Hosts/NonProdService/Function/Startup.cs | 51 -------------- .../Function/local.settings.json | 2 +- .../Infrastructure/compute/template.bicep | 2 +- 18 files changed, 127 insertions(+), 107 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/NonProdService/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/NonProdService/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs index 5fc89dadb..53deeec62 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupCreatorAndRetriever/GroupCreatorAndRetrieverFunction.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Logging; using Models; using Repositories.Contracts; @@ -21,7 +21,7 @@ public GroupCreatorAndRetrieverFunction(ILoggingRepository loggingRepository, IG _graphGroupRepository = graphGroupRepository ?? throw new ArgumentNullException(nameof(graphGroupRepository)); } - [FunctionName(nameof(GroupCreatorAndRetrieverFunction))] + [Function(nameof(GroupCreatorAndRetrieverFunction))] public async Task GenerateGroup([ActivityTrigger] GroupCreatorAndRetrieverRequest request, ILogger log) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupCreatorAndRetrieverFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupUpdater/GroupUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupUpdater/GroupUpdaterFunction.cs index f55fd0816..926282de9 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupUpdater/GroupUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/GroupUpdater/GroupUpdaterFunction.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Models; +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; @@ -22,7 +22,7 @@ public GroupUpdaterFunction( _graphGroupRepository = graphGroupRepository ?? throw new ArgumentNullException(nameof(graphGroupRepository)); } - [FunctionName(nameof(GroupUpdaterFunction))] + [Function(nameof(GroupUpdaterFunction))] public async Task UpdateGroupAsync([ActivityTrigger] GroupUpdaterRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupUpdaterFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingGroupCalculator/LoadTestingGroupCalculatorFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingGroupCalculator/LoadTestingGroupCalculatorFunction.cs index 98c88c0e5..349e9960d 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingGroupCalculator/LoadTestingGroupCalculatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingGroupCalculator/LoadTestingGroupCalculatorFunction.cs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.Amqp.Framing; +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Logging; using Models; using Repositories.Contracts; @@ -27,13 +26,13 @@ public LoadTestingGroupCalculatorFunction(ILoggingRepository loggingRepository) /// Creates a dictionary of group sizes and the number of groups of that size to create. /// It attempts to create many more smaller groups than larger groups to more closely resemble production usage. /// - [FunctionName(nameof(LoadTestingGroupCalculatorFunction))] + [Function(nameof(LoadTestingGroupCalculatorFunction))] public async Task GenerateGroup([ActivityTrigger] LoadTestingGroupCalculatorRequest request, ILogger log) { var runId = request.RunId; var numberOfUsers = request.NumberOfUsers; var numberOfGroups = request.NumberOfGroups; - + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(LoadTestingGroupCalculatorFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); var groupSizesAndCounts = new Dictionary(); @@ -58,7 +57,7 @@ public async Task GenerateGroup([ActivityTri if (groupCount > 0) { totalGroupCount += groupCount; groupSizesAndCounts.Add(_groupSizes[maxGroupSizeIndex-i], groupCount); - } + } } // Smallest group size gets the remaining number of groups that need to be created to match the requested number of groups diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingSyncJobCreator/LoadTestingSyncJobCreatorFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingSyncJobCreator/LoadTestingSyncJobCreatorFunction.cs index 480058ba9..350197204 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingSyncJobCreator/LoadTestingSyncJobCreatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingSyncJobCreator/LoadTestingSyncJobCreatorFunction.cs @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Models; using NonProdService.Activity.LoadTestingSyncJobCreator; using Repositories.Contracts; using System; +using System.Data.SqlTypes; using System.Linq; using System.Threading.Tasks; -using System.Data.SqlTypes; namespace Hosts.NonProdService { @@ -27,7 +27,7 @@ public LoadTestingSyncJobCreatorFunction(ILoggingRepository loggingRepository, I _options = options ?? throw new ArgumentNullException(nameof(options)); } - [FunctionName(nameof(LoadTestingSyncJobCreatorFunction))] + [Function(nameof(LoadTestingSyncJobCreatorFunction))] public async Task CreateLoadTestingSyncJobs([ActivityTrigger] LoadTestingSyncJobCreatorRequest request, ILogger log) { var runId = request.RunId; @@ -36,7 +36,7 @@ public async Task CreateLoadTestingSyncJobs([ActivityTrigger] LoadTestingSyncJob var options = _options.Value; await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(LoadTestingSyncJobCreatorFunction)} function started", RunId = runId }, VerbosityLevel.DEBUG); - + // spread out jobs evenly across 1 day var totalJobsToCreate = groupSizesAndIds.Keys.Sum(groupSize => groupSizesAndIds[groupSize].Count); var minutesInADay = 60 * 24; diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingSyncJobRetriever/LoadTestingSyncJobRetrieverFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingSyncJobRetriever/LoadTestingSyncJobRetrieverFunction.cs index 36ba3c9e0..6d42300c0 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingSyncJobRetriever/LoadTestingSyncJobRetrieverFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/LoadTestingSyncJobRetriever/LoadTestingSyncJobRetrieverFunction.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Logging; using Models; using Repositories.Contracts; -using Repositories.EntityFramework; using System; using System.Threading.Tasks; @@ -22,7 +21,7 @@ public LoadTestingSyncJobRetrieverFunction(ILoggingRepository loggingRepository, _databaseSyncJobsRepository = databaseSyncJobsRepository ?? throw new ArgumentNullException(nameof(databaseSyncJobsRepository)); } - [FunctionName(nameof(LoadTestingSyncJobRetrieverFunction))] + [Function(nameof(LoadTestingSyncJobRetrieverFunction))] public async Task GenerateGroup([ActivityTrigger] LoadTestingSyncJobRetrieverRequest request, ILogger log) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(LoadTestingSyncJobRetrieverFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/Logger/LoggerFunction.cs index 4adf4e985..345a5a136 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/Logger/LoggerFunction.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Models; +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; @@ -18,7 +18,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.RunId }, request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/TenantUserCount/TenantUserCountFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/TenantUserCount/TenantUserCountFunction.cs index e83f8d14f..0286bc101 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/TenantUserCount/TenantUserCountFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/TenantUserCount/TenantUserCountFunction.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Logging; using Models; using Repositories.Contracts; @@ -22,7 +22,7 @@ public TenantUserCountFunction(ILoggingRepository loggingRepository, IGraphUserR _graphUserRepository = graphUserRepository ?? throw new ArgumentNullException(nameof(graphUserRepository)); } - [FunctionName(nameof(TenantUserCountFunction))] + [Function(nameof(TenantUserCountFunction))] public async Task GetTenantUsersAsync([ActivityTrigger] TenantUserCountRequest request, ILogger log) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TenantUserCountFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/TenantUserReader/TenantUserReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/TenantUserReader/TenantUserReaderFunction.cs index 0cdbe0d95..da1a7e876 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/TenantUserReader/TenantUserReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Activity/TenantUserReader/TenantUserReaderFunction.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Logging; using Models; using Repositories.Contracts; @@ -22,7 +22,7 @@ public TenantUserReaderFunction(ILoggingRepository loggingRepository, IGraphGrou _graphGroupRepository = graphGroupRepository ?? throw new ArgumentNullException(nameof(graphGroupRepository)); } - [FunctionName(nameof(TenantUserReaderFunction))] + [Function(nameof(TenantUserReaderFunction))] public async Task> GetTenantUsersAsync([ActivityTrigger] TenantUserReaderRequest request, ILogger log) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TenantUserReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/GroupUpdaterSubOrchestrator/GroupUpdaterSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/GroupUpdaterSubOrchestrator/GroupUpdaterSubOrchestratorFunction.cs index 528c1bc1b..c0de98670 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/GroupUpdaterSubOrchestrator/GroupUpdaterSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/GroupUpdaterSubOrchestrator/GroupUpdaterSubOrchestratorFunction.cs @@ -1,14 +1,13 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using System.Threading.Tasks; -using System.Linq; -using System.Collections.Generic; -using System; -using Microsoft.ApplicationInsights; -using Repositories.Contracts; +using Microsoft.DurableTask; using Models; +using Repositories.Contracts; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace Hosts.NonProdService { @@ -18,8 +17,8 @@ public GroupUpdaterSubOrchestratorFunction() { } - [FunctionName(nameof(GroupUpdaterSubOrchestratorFunction))] - public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(GroupUpdaterSubOrchestratorFunction))] + public async Task RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var skip = 0; var batchSize = 100; diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/IntegrationTestingPrepSubOrchestrator/IntegrationTestingPrepSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/IntegrationTestingPrepSubOrchestrator/IntegrationTestingPrepSubOrchestratorFunction.cs index c6e57f5db..3798a78d3 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/IntegrationTestingPrepSubOrchestrator/IntegrationTestingPrepSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/IntegrationTestingPrepSubOrchestrator/IntegrationTestingPrepSubOrchestratorFunction.cs @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; @@ -41,8 +42,8 @@ public IntegrationTestingPrepSubOrchestratorFunction(INonProdService nonProdServ _nonProdService = nonProdService ?? throw new ArgumentNullException(nameof(nonProdService)); } - [FunctionName(nameof(IntegrationTestingPrepSubOrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(IntegrationTestingPrepSubOrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var request = context.GetInput(); var runId = request.RunId; diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/LoadTestingPrepSubOrchestrator/LoadTestingPrepSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/LoadTestingPrepSubOrchestrator/LoadTestingPrepSubOrchestratorFunction.cs index e0041b5ea..0ab2bec14 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/LoadTestingPrepSubOrchestrator/LoadTestingPrepSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/LoadTestingPrepSubOrchestrator/LoadTestingPrepSubOrchestratorFunction.cs @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; using Microsoft.Extensions.Options; using Models; using NonProdService.LoadTestingPrepSubOrchestrator; @@ -21,8 +22,8 @@ public LoadTestingPrepSubOrchestratorFunction(IOptions(); var runId = request.RunId; diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj index b711a7d66..dec934ed1 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/NonProdService.csproj @@ -3,13 +3,16 @@ net8.0 v4 41fcfc96-9608-4426-9cd8-181202f026a4 + Exe + enabled - - - - + + + + + @@ -33,4 +36,7 @@ Never + + + diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Orchestrator/OrchestratorFunction.cs index 8350abec6..0461d4a26 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Orchestrator/OrchestratorFunction.cs @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; using Repositories.Contracts; using Services.Contracts; using Services.Entities; @@ -16,8 +17,8 @@ public OrchestratorFunction(INonProdService nonProdService) { } - [FunctionName(nameof(OrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(OrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var runId = context.NewGuid(); diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Program.cs new file mode 100644 index 000000000..dd96e58a6 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Program.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Azure.Identity; +using Common.DependencyInjection; +using Hosts.FunctionBase; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NonProdService.Activity.LoadTestingSyncJobCreator; +using NonProdService.LoadTestingPrepSubOrchestrator; +using Repositories.Contracts; +using Repositories.GraphAzureADUsers; +using Repositories.GraphGroups; +using Services.Contracts; +using System; + +namespace Hosts.NonProdService +{ + public class Program + { + public static void Main(string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()).UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = nameof(NonProdService); + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + + services.AddOptions().Configure((settings, configuration) => + { + settings.DestinationGroupOwnerId = configuration.GetValue("graphCredentials:ClientId"); + settings.GroupCount = configuration.GetValue("NonProdService:LoadTesting:JobCount"); + }); + + services.AddOptions().Configure((settings, configuration) => + { + configuration.GetSection("NonProdService:LoadTesting").Bind(settings); + }); + + services + .AddGraphAPIClient() + .AddScoped() + .AddScoped() + .AddSingleton(); + + }).Build(); + + host.Run(); + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Starter/StarterFunction.cs index ffcceef34..2e80fc9ed 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Starter/StarterFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask.Client; using Models; using Repositories.Contracts; using System; -using System.Net.Http; using System.Threading.Tasks; namespace Hosts.NonProdService @@ -20,13 +19,13 @@ public StarterFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(StarterFunction))] - public async Task HttpStart([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestMessage req, - [DurableClient] IDurableOrchestrationClient starter) + [Function(nameof(StarterFunction))] + public async Task HttpStart([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, + [DurableClient] DurableTaskClient starter) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started" }, VerbosityLevel.DEBUG); - var instanceId = await starter.StartNewAsync(nameof(OrchestratorFunction), null); + var instanceId = await starter.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction)); var response = starter.CreateCheckStatusResponse(req, instanceId); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Startup.cs deleted file mode 100644 index c78f5415d..000000000 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/Startup.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using NonProdService.Activity.LoadTestingSyncJobCreator; -using NonProdService.LoadTestingPrepSubOrchestrator; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphAzureADUsers; -using Repositories.GraphGroups; -using Services.Contracts; -using System; - -[assembly: FunctionsStartup(typeof(Hosts.NonProdService.Startup))] - -namespace Hosts.NonProdService -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(NonProdService); - protected override string DryRunSettingName => string.Empty; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services - .AddGraphAPIClient() - .AddScoped() - .AddScoped(); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - settings.DestinationGroupOwnerId = configuration.GetValue("graphCredentials:ClientId"); - settings.GroupCount = configuration.GetValue("NonProdService:LoadTesting:JobCount"); - }); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - configuration.GetSection("NonProdService:LoadTesting").Bind(settings); - }); - - builder.Services.AddSingleton(); - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/local.settings.json index 63a41fb3b..b756d01ab 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Function/local.settings.json @@ -2,7 +2,7 @@ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "logAnalyticsCustomerId": "", "logAnalyticsPrimarySharedKey": "", "graphCredentials:ClientId": "", diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep index d6e4dae0d..d25fa8f60 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep @@ -172,7 +172,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } From 73c1cbf11bb7fe8ed5299fd6fa46549d1ce22592 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Mon, 26 Aug 2024 11:03:40 -0700 Subject: [PATCH 0269/1479] Isolated worker model for syncjobupdater --- .../JobStatusUpdaterFunction.cs | 5 +- .../Activity/Logger/LoggerFunction.cs | 5 +- .../Orchestrator/OrchestratorFunction.cs | 8 +-- .../Hosts/SyncJobUpdater/Function/Program.cs | 58 +++++++++++++++++++ .../Function/Starter/StarterFunction.cs | 10 ++-- .../Hosts/SyncJobUpdater/Function/Startup.cs | 36 ------------ .../Function/SyncJobUpdater.csproj | 11 ++-- .../Hosts/SyncJobUpdater/Function/host.json | 4 +- .../Infrastructure/compute/template.bicep | 2 +- .../OrchestratorTests.cs | 21 +++---- 10 files changed, 92 insertions(+), 68 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs index 487ab1d00..dcbb4512a 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System.Threading.Tasks; using Services.Contracts; +using Microsoft.Azure.Functions.Worker; namespace Hosts.SyncJobUpdater { @@ -20,7 +19,7 @@ public JobStatusUpdaterFunction(ILoggingRepository loggingRepository, ISyncJobUp _syncJobUpdaterService = syncJobUpdaterService; } - [FunctionName(nameof(JobStatusUpdaterFunction))] + [Function(nameof(JobStatusUpdaterFunction))] public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest request) { if (request.SyncJob != null) diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/Logger/LoggerFunction.cs index 4ceca5ccc..0d77860bd 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Activity/Logger/LoggerFunction.cs @@ -2,11 +2,10 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.SyncJobUpdater { @@ -19,7 +18,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.RunId },request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Orchestrator/OrchestratorFunction.cs index 9b04cbfb4..86109b0f8 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Orchestrator/OrchestratorFunction.cs @@ -1,7 +1,5 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; using Microsoft.Graph; @@ -18,6 +16,8 @@ using Repositories.Contracts.InjectConfig; using Models.Notifications; using Hosts.SyncJobUpdater; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.SyncJobUpdater { @@ -27,8 +27,8 @@ public OrchestratorFunction() { } - [FunctionName(nameof(OrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context, ExecutionContext executionContext) + [Function(nameof(OrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var mainRequest = context.GetInput(); if (mainRequest != null && mainRequest.SyncJob != null) diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Program.cs new file mode 100644 index 000000000..6babeb420 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Program.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.Extensions.Hosting; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using DIConcreteTypes; +using Hosts.FunctionBase; +using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Repositories.Contracts; +using Repositories.ServiceBusQueue; +using Azure.Identity; +using System; + +namespace Hosts.SyncJobUpdater +{ + + public class Program + { + public static void Main (string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = "SyncJobUpdater"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + + services.AddScoped(services => + { + return new SyncJobUpdaterService( + services.GetRequiredService(), + services.GetRequiredService() + ); + }); + }) + .Build(); + + host.Run(); + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Starter/StarterFunction.cs index e95a1e987..24ade4b41 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Starter/StarterFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Azure.Messaging.ServiceBus; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; @@ -10,6 +8,8 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask.Client; namespace Hosts.SyncJobUpdater { @@ -22,10 +22,10 @@ public StarterFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository; } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task RunAsync( [ServiceBusTrigger("%serviceBusSyncJobUpdaterQueue%", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient client) { var syncJob = JsonSerializer.Deserialize(Encoding.UTF8.GetString(message.Body)); var runId = syncJob.RunId.GetValueOrDefault(Guid.Empty); @@ -42,7 +42,7 @@ public async Task RunAsync( Status = syncjobStatus }; - var instanceId = await starter.StartNewAsync(nameof(OrchestratorFunction), request); + var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), request); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"InstanceId: {instanceId} for job Id: {syncJob.Id} ", RunId = runId }); diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Startup.cs deleted file mode 100644 index f292ec3fc..000000000 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/Startup.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Azure.Messaging.ServiceBus; -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Repositories.Contracts; -using Repositories.ServiceBusQueue; - -// see https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection -[assembly: FunctionsStartup(typeof(Hosts.SyncJobUpdater.Startup))] - -namespace Hosts.SyncJobUpdater -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(SyncJobUpdater); - protected override string DryRunSettingName => "SyncJobUpdater:IsDryRunEnabled"; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddScoped(services => - { - return new SyncJobUpdaterService( - services.GetRequiredService(), - services.GetRequiredService() - ); - }); - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj index 14766ab9d..f1f91c8be 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/SyncJobUpdater.csproj @@ -2,14 +2,17 @@ net8.0 v4 + Exe + enabled - - - - + + + + + diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/host.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/host.json index b715b753b..3c45f5fad 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/host.json @@ -16,9 +16,9 @@ "Host.Concurrency": "Trace" }, "applicationInsights": { - "samplingExcludedTypes": "Request", "samplingSettings": { - "isEnabled": true + "isEnabled": true, + "excludedTypes": "Request" } } } diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep index 5fe7bff65..9c0d558c7 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep @@ -82,7 +82,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/OrchestratorTests.cs index f7cd1d8a3..4d2be4bf5 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Services.SyncJobUpdater.Tests/OrchestratorTests.cs @@ -13,13 +13,14 @@ using Repositories.Contracts; using Services.SyncJobUpdater.Tests.Mocks; using Hosts.SyncJobUpdater; +using Microsoft.DurableTask; namespace Services.Tests { [TestClass] public class OrchestratorFunctionTests { - private Mock _mockContext; + private Mock _mockContext; private MockLoggingRepository _mockLogger; private OrchestratorFunction _orchestratorFunction; private const string GroupMembership = "GroupMembership"; @@ -27,7 +28,7 @@ public class OrchestratorFunctionTests [TestInitialize] public void Setup() { - _mockContext = new Mock(); + _mockContext = new Mock(); _mockLogger = new MockLoggingRepository(); _orchestratorFunction = new OrchestratorFunction(); } @@ -44,9 +45,9 @@ public async Task RunOrchestratorTest() _mockContext.Setup(c => c.GetInput()).Returns(orchestratorRequest); - await _orchestratorFunction.RunOrchestratorAsync(_mockContext.Object, null); - _mockContext.Verify(context => context.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny()), Times.Once); - _mockContext.Verify(context => context.CallActivityAsync(nameof(LoggerFunction), It.IsAny()), Times.Exactly(2)); + await _orchestratorFunction.RunOrchestratorAsync(_mockContext.Object); + _mockContext.Verify(context => context.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny(), null), Times.Once); + _mockContext.Verify(context => context.CallActivityAsync(nameof(LoggerFunction), It.IsAny(), null), Times.Exactly(2)); } [TestMethod] @@ -60,16 +61,16 @@ public async Task RunOrchestratorTestWithUnknownStatus() }; var loggerRequests = new List(); _mockContext - .Setup(context => context.CallActivityAsync(nameof(LoggerFunction), It.IsAny())) - .Callback((name, request) => loggerRequests.Add((LoggerRequest)request)) + .Setup(context => context.CallActivityAsync(It.Is(x => x.ToString() == nameof(LoggerFunction)), It.IsAny(), It.IsAny())) + .Callback((name, request, options) => loggerRequests.Add((LoggerRequest)request)) .Returns(Task.CompletedTask); _mockContext.Setup(c => c.GetInput()).Returns(orchestratorRequest); - await _orchestratorFunction.RunOrchestratorAsync(_mockContext.Object, null); - _mockContext.Verify(context => context.CallActivityAsync(nameof(LoggerFunction), It.IsAny()), Times.Exactly(2), "Expected LoggerFunction to be called exactly twice."); + await _orchestratorFunction.RunOrchestratorAsync(_mockContext.Object); + _mockContext.Verify(context => context.CallActivityAsync(nameof(LoggerFunction), It.IsAny(), null), Times.Exactly(2), "Expected LoggerFunction to be called exactly twice."); Assert.IsTrue(loggerRequests.Any(req => req.Message.Contains("unknown status")), "Expected an error log message for unknown status."); - _mockContext.Verify(context => context.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny()), Times.Once); + _mockContext.Verify(context => context.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny(), null), Times.Once); } } From 23a650be440645cbd9550304d39f721e26e4b5c7 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 5 Sep 2024 16:34:01 -0700 Subject: [PATCH 0270/1479] Remove extension session --- .../Hosts/SyncJobUpdater/Function/host.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/host.json b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/host.json index 3c45f5fad..28c22fd92 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Function/host.json @@ -5,12 +5,6 @@ "dynamicConcurrencyEnabled": true, "snapshotPersistenceEnabled": true }, - "extensions": { - "durableTask": { - "extendedSessionsEnabled": true, - "extendedSessionIdleTimeoutInSeconds": 30 - } - }, "logging": { "logLevel": { "Host.Concurrency": "Trace" From 784367f980e14f3cf7cd7ff462c9ce6262550643 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Wed, 4 Sep 2024 17:16:17 -0700 Subject: [PATCH 0271/1479] Updated the Value ComboBox to display codes for each description. --- .../HRQuerySource/HRQuerySource.base.tsx | 20 ++++++++++++++++++- .../HRQuerySource/HRQuerySource.styles.ts | 9 ++++++++- .../HRQuerySource/HRQuerySource.types.ts | 2 ++ .../src/services/localization/IStrings.ts | 1 + .../i18n/locales/en/translations.ts | 3 ++- .../i18n/locales/es/translations.ts | 3 ++- 6 files changed, 34 insertions(+), 4 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 2946d6c1a..7c0be120e 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT license. import React, { useEffect, useState } from 'react'; -import { classNamesFunction, Stack, type IProcessedStyleSet, IStackTokens, Label, IconButton, TooltipHost, ChoiceGroup, IChoiceGroupOption, SpinButton, NormalPeoplePicker, DirectionalHint, IDropdownOption, ActionButton, DetailsList, DetailsListLayoutMode, Dropdown, Selection, IColumn, ComboBox, IComboBoxOption, IComboBox, Separator} from '@fluentui/react'; +import { classNamesFunction, Stack, type IProcessedStyleSet, IStackTokens, Label, IconButton, TooltipHost, Text, ChoiceGroup, IChoiceGroupOption, SpinButton, NormalPeoplePicker, DirectionalHint, IDropdownOption, ActionButton, DetailsList, DetailsListLayoutMode, Dropdown, Selection, IColumn, ComboBox, IComboBoxOption, IComboBox, Separator, ISelectableOption} from '@fluentui/react'; import { useTheme } from '@fluentui/react/lib/Theme'; import { TextField } from '@fluentui/react/lib/TextField'; import { IPersonaProps } from '@fluentui/react/lib/Persona'; @@ -1170,6 +1170,23 @@ const checkType = (value: string, type: string | undefined): string => { setItemsBasedOnGroups(newGroups); } + const onRenderValueComboBoxOptions = (props?: ISelectableOption, defaultRender?: (props?: ISelectableOption) => JSX.Element | null): JSX.Element | null => { + return ( +
+
+ + {props?.text} + +
+
+ + {strings.HROnboarding.valueComboBoxOptionCodeLabel + props?.key} + +
+
+ ); + } + const onRenderItemColumn = (items: IFilterPart[], item?: any, index?: number, column?: IColumn, groupIndex?: number): JSX.Element => { if (typeof index !== 'undefined' && items[index]) { switch (column?.key) { @@ -1202,6 +1219,7 @@ const checkType = (value: string, type: string | undefined): string => { options={filteredValueOptions[index] || getValueOptions(attributeMappings[items[index].attribute].mappings)} onInputValueChange={(text) => onAttributeValueChange(text, index)} onChange={(event, option) => handleAttributeValueChange(item.attribute, event, option, index)} + onRenderOption={onRenderValueComboBoxOptions} allowFreeInput autoComplete="off" useComboBoxAsMenuWidth={false} diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.styles.ts b/UI/web-app/src/components/HRQuerySource/HRQuerySource.styles.ts index 5cabff3ad..58bc15c88 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.styles.ts +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.styles.ts @@ -127,6 +127,13 @@ export const getStyles = (props: HRQuerySourceStyleProps): HRQuerySourceStyles = lineHeight: 16, fontFamily: 'Segoe UI', color: theme.semanticColors.errorText - } + }, + comboBoxOptionCodeText: { + fontStyle: 'italic', + }, + comboBoxOptionContainer: { + paddingTop: 3, + paddingBottom: 3, + }, }; }; \ No newline at end of file diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.types.ts b/UI/web-app/src/components/HRQuerySource/HRQuerySource.types.ts index 7cbcb0cce..8f19f85df 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.types.ts +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.types.ts @@ -28,6 +28,8 @@ export type HRQuerySourceStyles = { separator: IStyle; cardHeader: IStyle; cardTitle: IStyle; + comboBoxOptionCodeText: IStyle; + comboBoxOptionContainer: IStyle; }; export type HRQuerySourceStyleProps = { diff --git a/UI/web-app/src/services/localization/IStrings.ts b/UI/web-app/src/services/localization/IStrings.ts index 9435fec67..77f108bc3 100644 --- a/UI/web-app/src/services/localization/IStrings.ts +++ b/UI/web-app/src/services/localization/IStrings.ts @@ -47,6 +47,7 @@ export type IStrings = { orgLeaderMissingErrorMessage: string; source: string; invalidInputErrorMessage: string; + valueComboBoxOptionCodeLabel: string; }, Components: { AppHeader: { diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index da0788e76..0d1ab4302 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -49,7 +49,8 @@ export const strings: IStrings = { customOrgLeaderMissingErrorMessage: " doesn't exist in the ", source: " source", orgLeaderMissingErrorMessage: " doesn't exist in the HR Data source", - invalidInputErrorMessage: "Invalid input. Please enter only numbers." + invalidInputErrorMessage: "Invalid input. Please enter only numbers.", + valueComboBoxOptionCodeLabel: "Code: ", }, Components: { AppHeader: { diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index 4cdff8e83..6273f533d 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -50,7 +50,8 @@ export const strings: IStrings = { customOrgLeaderMissingErrorMessage: " no existe en ", orgLeaderMissingErrorMessage: "Este usuario no existe en la fuente de datos de RR.HH.", source: " fuente", - invalidInputErrorMessage: "Entrada inválida. Por favor ingrese solo números." + invalidInputErrorMessage: "Entrada inválida. Por favor ingrese solo números.", + valueComboBoxOptionCodeLabel: "Codigo: ", }, Components: { AppHeader: { From c294092194c8d034dd877d0977c7d912f7eb7f20 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Thu, 5 Sep 2024 11:41:04 -0700 Subject: [PATCH 0272/1479] Updated the Value and Attribute ComboBoxes to have a max width and height. --- .../HRQuerySource/HRQuerySource.base.tsx | 14 +++++++++++++- .../HRQuerySource/HRQuerySource.styles.ts | 3 +++ .../HRQuerySource/HRQuerySource.types.ts | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 7c0be120e..d29d6ee35 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT license. import React, { useEffect, useState } from 'react'; -import { classNamesFunction, Stack, type IProcessedStyleSet, IStackTokens, Label, IconButton, TooltipHost, Text, ChoiceGroup, IChoiceGroupOption, SpinButton, NormalPeoplePicker, DirectionalHint, IDropdownOption, ActionButton, DetailsList, DetailsListLayoutMode, Dropdown, Selection, IColumn, ComboBox, IComboBoxOption, IComboBox, Separator, ISelectableOption} from '@fluentui/react'; +import { classNamesFunction, Stack, type IProcessedStyleSet, IStackTokens, Label, IconButton, TooltipHost, Text, ChoiceGroup, IChoiceGroupOption, SpinButton, NormalPeoplePicker, DirectionalHint, IDropdownOption, ActionButton, DetailsList, DetailsListLayoutMode, Dropdown, Selection, IColumn, ComboBox, IComboBoxOption, IComboBox, Separator, ISelectableOption, ISelectableDroppableTextProps} from '@fluentui/react'; import { useTheme } from '@fluentui/react/lib/Theme'; import { TextField } from '@fluentui/react/lib/TextField'; import { IPersonaProps } from '@fluentui/react/lib/Persona'; @@ -1187,6 +1187,14 @@ const checkType = (value: string, type: string | undefined): string => { ); } + const onRenderValueComboBoxList = (props?: ISelectableDroppableTextProps, defaultRender?: (props?: ISelectableDroppableTextProps) => JSX.Element | null): JSX.Element | null => { + return ( +
+ {defaultRender!(props)} +
+ ); + } + const onRenderItemColumn = (items: IFilterPart[], item?: any, index?: number, column?: IColumn, groupIndex?: number): JSX.Element => { if (typeof index !== 'undefined' && items[index]) { switch (column?.key) { @@ -1201,9 +1209,11 @@ const checkType = (value: string, type: string | undefined): string => { options={filteredOptions[index] || getOptions(attributes)} onInputValueChange={(text) => onAttributeChange(text, index)} onChange={(event, option) => handleAttributeChange(event, option, index, groupIndex)} + onRenderList={onRenderValueComboBoxList} allowFreeInput autoComplete="off" useComboBoxAsMenuWidth={true} + dropdownMaxWidth={500} />; case 'equalityOperator': return { onInputValueChange={(text) => onAttributeValueChange(text, index)} onChange={(event, option) => handleAttributeValueChange(item.attribute, event, option, index)} onRenderOption={onRenderValueComboBoxOptions} + onRenderList={onRenderValueComboBoxList} allowFreeInput autoComplete="off" useComboBoxAsMenuWidth={false} + dropdownMaxWidth={500} /> } else { return Date: Thu, 5 Sep 2024 11:44:09 -0700 Subject: [PATCH 0273/1479] Updated the Value ComboBox search to search on both descriptions and codes. --- UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index d29d6ee35..5b67a0ea3 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -1018,7 +1018,7 @@ const checkType = (value: string, type: string | undefined): string => { newFilteredValueOptions[index] = getValueOptions(currentAttributeMappings); } else { let valueOptions = getValueOptions(currentAttributeMappings); - newFilteredValueOptions[index] = valueOptions.filter(opt => opt.text.toLowerCase().startsWith(text.toLowerCase())); + newFilteredValueOptions[index] = valueOptions.filter(opt => opt.text.toLowerCase().startsWith(text.toLowerCase()) || opt.key.toString().toLowerCase().startsWith(text.toLowerCase())); } setFilteredValueOptions(newFilteredValueOptions); } From 55ddbb4165d447229568d10dd7f5ae8868f53258 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 27 Aug 2024 13:29:27 -0700 Subject: [PATCH 0274/1479] GMO isolated worker model --- .../DeltaUsersReaderFunction.cs | 5 +- .../DeltaUsersSenderFunction.cs | 5 +- .../DestinationNameReaderFunction.cs | 5 +- .../EmailSender/EmailSenderFunction.cs | 5 +- .../FeatureFlagReader/FeatureFlagFunction.cs | 5 +- .../FileDeleter/FileDeleterFunction.cs | 5 +- .../FileDownloader/FileDownloaderFunction.cs | 5 +- .../GetTransitiveGroupCountFunction.cs | 5 +- .../GetUserCount/GetUserCountFunction.cs | 5 +- .../GroupReader/GroupReaderFunction.cs | 5 +- .../GroupValidator/GroupValidatorFunction.cs | 5 +- .../JobStatusUpdaterFunction.cs | 5 +- .../MembersReader/MembersReaderFunction.cs | 5 +- .../QueueMessageSenderFunction.cs | 5 +- .../SchemaValidatorFunction.cs | 5 +- .../SubsequentDeltaUsersReaderFunction.cs | 5 +- .../SubsequentMembersReaderFunction.cs | 5 +- .../SubsequentUsersReaderFunction.cs | 5 +- .../TelemetryTrackerFunction.cs | 5 +- .../UsersReader/UsersReaderFunction.cs | 5 +- .../UsersSender/UsersSenderFunction.cs | 5 +- .../Function/GroupMembershipObtainer.csproj | 13 +- .../Orchestrator/OrchestratorFunction.cs | 10 +- .../Function/Program.cs | 96 +++++++++ .../Function/Starter/StarterFunction.cs | 11 +- .../Function/Startup.cs | 85 -------- .../SubOrchestratorFunction.cs | 22 +- .../Function/host.json | 4 +- .../Function/local.settings.json | 5 +- .../Services.Tests/OrchestratorTests.cs | 115 +++++----- .../Services.Tests/StarterTests.cs | 19 +- .../SubOrchestratorFunctionTests.cs | 202 +++++++++--------- .../Services.Tests/Tests.Services.csproj | 1 + .../MockDurableTaskClient.cs | 91 ++++++++ .../Repositories.Mocks.csproj | 2 + 35 files changed, 444 insertions(+), 337 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Startup.cs create mode 100644 Service/GroupMembershipManagement/Repositories.Mocks/MockDurableTaskClient.cs diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DeltaUsersReader/DeltaUsersReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DeltaUsersReader/DeltaUsersReaderFunction.cs index fe101495f..65b94dbcb 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DeltaUsersReader/DeltaUsersReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DeltaUsersReader/DeltaUsersReaderFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Models; using Repositories.Contracts; @@ -10,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -24,7 +23,7 @@ public DeltaUsersReaderFunction(ILoggingRepository loggingRepository, SGMembersh _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); } - [FunctionName(nameof(DeltaUsersReaderFunction))] + [Function(nameof(DeltaUsersReaderFunction))] public async Task GetDeltaUsersAsync([ActivityTrigger] DeltaUsersReaderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(DeltaUsersReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DeltaUsersSender/DeltaUsersSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DeltaUsersSender/DeltaUsersSenderFunction.cs index 4e681e5d0..59211273c 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DeltaUsersSender/DeltaUsersSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DeltaUsersSender/DeltaUsersSenderFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models.Helpers; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Models; using Newtonsoft.Json; @@ -11,6 +9,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -25,7 +24,7 @@ public DeltaUsersSenderFunction(ILoggingRepository loggingRepository, SGMembersh _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); } - [FunctionName(nameof(DeltaUsersSenderFunction))] + [Function(nameof(DeltaUsersSenderFunction))] public async Task SendUsersAsync([ActivityTrigger] DeltaUsersSenderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(DeltaUsersSenderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DestinationNameReader/DestinationNameReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DestinationNameReader/DestinationNameReaderFunction.cs index 597484cc2..a1f5dc363 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DestinationNameReader/DestinationNameReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/DestinationNameReader/DestinationNameReaderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -21,7 +20,7 @@ public DestinationNameReaderFunction(ILoggingRepository loggingRepository, SGMem _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); ; } - [FunctionName(nameof(DestinationNameReaderFunction))] + [Function(nameof(DestinationNameReaderFunction))] public async Task GetDestinationNameAsync([ActivityTrigger] SyncJob syncJob) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(DestinationNameReaderFunction)} function started", RunId = syncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/EmailSender/EmailSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/EmailSender/EmailSenderFunction.cs index ab42aa5f2..a765e9f15 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/EmailSender/EmailSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/EmailSender/EmailSenderFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -21,7 +20,7 @@ public EmailSenderFunction(ILoggingRepository loggingRepository, SGMembershipCal _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); } - [FunctionName(nameof(EmailSenderFunction))] + [Function(nameof(EmailSenderFunction))] public async Task SendEmailAsync([ActivityTrigger] EmailSenderRequest request) { if (request.SyncJob != null) diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FeatureFlagReader/FeatureFlagFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FeatureFlagReader/FeatureFlagFunction.cs index aebb9f38d..43bc9bd2d 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FeatureFlagReader/FeatureFlagFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FeatureFlagReader/FeatureFlagFunction.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -22,7 +21,7 @@ public FeatureFlagFunction( _featureFlagRepository = featureFlagRepository ?? throw new ArgumentNullException(nameof(featureFlagRepository)); } - [FunctionName(nameof(FeatureFlagFunction))] + [Function(nameof(FeatureFlagFunction))] public async Task CheckFeatureFlagStateAsync([ActivityTrigger] FeatureFlagRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(FeatureFlagFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDeleter/FileDeleterFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDeleter/FileDeleterFunction.cs index f4815522e..fa4f0bb8c 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDeleter/FileDeleterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDeleter/FileDeleterFunction.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -20,7 +19,7 @@ public FileDeleterFunction(ILoggingRepository loggingRepository, IBlobStorageRep _blobStorageRepository = blobStorageRepository ?? throw new ArgumentNullException(nameof(blobStorageRepository)); } - [FunctionName(nameof(FileDeleterFunction))] + [Function(nameof(FileDeleterFunction))] public async Task DeleteFileAsync([ActivityTrigger] FileDeleterRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Deleting file {request.FilePath}", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDownloader/FileDownloaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDownloader/FileDownloaderFunction.cs index c2c17d54c..00c5db2c9 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDownloader/FileDownloaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/FileDownloader/FileDownloaderFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Models.Helpers; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -26,7 +25,7 @@ public FileDownloaderFunction(ILoggingRepository loggingRepository, IBlobStorage /// /// /// Compressed file content - [FunctionName(nameof(FileDownloaderFunction))] + [Function(nameof(FileDownloaderFunction))] public async Task DownloadFileAsync([ActivityTrigger] FileDownloaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Downloading file {request.FilePath}", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GetTransitiveGroupCount/GetTransitiveGroupCountFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GetTransitiveGroupCount/GetTransitiveGroupCountFunction.cs index 31cc5be7d..dfab8b132 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GetTransitiveGroupCount/GetTransitiveGroupCountFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GetTransitiveGroupCount/GetTransitiveGroupCountFunction.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -23,7 +22,7 @@ public GetTransitiveGroupCountFunction(ILoggingRepository loggingRepository, SGM _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); } - [FunctionName(nameof(GetTransitiveGroupCountFunction))] + [Function(nameof(GetTransitiveGroupCountFunction))] public async Task GetGroupsAsync([ActivityTrigger] GetTransitiveGroupCountRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(GetTransitiveGroupCountFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GetUserCount/GetUserCountFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GetUserCount/GetUserCountFunction.cs index 550cd677c..7836a8b5b 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GetUserCount/GetUserCountFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GetUserCount/GetUserCountFunction.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -23,7 +22,7 @@ public GetUserCountFunction(ILoggingRepository loggingRepository, SGMembershipCa _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); } - [FunctionName(nameof(GetUserCountFunction))] + [Function(nameof(GetUserCountFunction))] public async Task GetUserCountAsync([ActivityTrigger] GetUserCountRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(GetUserCountFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderFunction.cs index 96b51f098..675c10c24 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Newtonsoft.Json.Linq; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -22,7 +21,7 @@ public GroupReaderFunction(ILoggingRepository loggingRepository, SGMembershipCal _membershipCalculator = membershipCalculator; } - [FunctionName(nameof(GroupReaderFunction))] + [Function(nameof(GroupReaderFunction))] public async Task<(AzureADGroup, string)> GetGroupAsync([ActivityTrigger] GroupReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupValidator/GroupValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupValidator/GroupValidatorFunction.cs index f149ec1fe..7d83a6a62 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupValidator/GroupValidatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupValidator/GroupValidatorFunction.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Identity.Client; using Models; using Models.Notifications; @@ -11,6 +9,7 @@ using System; using System.Threading.Tasks; using static System.Net.WebRequestMethods; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -28,7 +27,7 @@ public GroupValidatorFunction(ILoggingRepository loggingRepository, SGMembership _emailSenderAndRecipients = emailSenderAndRecipients; } - [FunctionName(nameof(GroupValidatorFunction))] + [Function(nameof(GroupValidatorFunction))] public async Task ValidateGroupAsync([ActivityTrigger] GroupValidatorRequest request) { bool isExistingGroup = false; diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs index 0cadc7f87..d8b783c8c 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -20,7 +19,7 @@ public JobStatusUpdaterFunction(ILoggingRepository loggingRepository, SGMembersh _membershipCalculator = membershipCalculator; } - [FunctionName(nameof(JobStatusUpdaterFunction))] + [Function(nameof(JobStatusUpdaterFunction))] public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest request) { if (request.SyncJob != null) diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/MembersReader/MembersReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/MembersReader/MembersReaderFunction.cs index a0b5399d3..0c0b04f3b 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/MembersReader/MembersReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/MembersReader/MembersReaderFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Models; using Repositories.Contracts; @@ -10,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -24,7 +23,7 @@ public MembersReaderFunction(ILoggingRepository loggingRepository, SGMembershipC _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); } - [FunctionName(nameof(MembersReaderFunction))] + [Function(nameof(MembersReaderFunction))] public async Task GetMembersAsync([ActivityTrigger] MembersReaderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(MembersReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs index 537621438..f246c62fd 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Models.ServiceBus; using Newtonsoft.Json; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -23,7 +22,7 @@ public QueueMessageSenderFunction( _serviceBusQueueRepository = serviceBusQueueRepository ?? throw new ArgumentNullException(nameof(serviceBusQueueRepository)); } - [FunctionName(nameof(QueueMessageSenderFunction))] + [Function(nameof(QueueMessageSenderFunction))] public async Task SendMessageAsync([ActivityTrigger] MembershipAggregatorHttpRequest request) { diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs index 089a2bcb4..e71b08608 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs @@ -1,12 +1,11 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; using NJsonSchema; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -21,7 +20,7 @@ public SchemaValidatorFunction(ILoggingRepository loggingRepository, SchemaProvi _schemaProvider = schemaProvider ?? throw new ArgumentNullException(nameof(schemaProvider)); } - [FunctionName(nameof(SchemaValidatorFunction))] + [Function(nameof(SchemaValidatorFunction))] public async Task ValidateSchemasAsync([ActivityTrigger] SchemaValidatorRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentDeltaUsersReader/SubsequentDeltaUsersReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentDeltaUsersReader/SubsequentDeltaUsersReaderFunction.cs index 2035c2229..79a18f8cb 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentDeltaUsersReader/SubsequentDeltaUsersReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentDeltaUsersReader/SubsequentDeltaUsersReaderFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Models; using Repositories.Contracts; @@ -10,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -24,7 +23,7 @@ public SubsequentDeltaUsersReaderFunction(ILoggingRepository loggingRepository, _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); } - [FunctionName(nameof(SubsequentDeltaUsersReaderFunction))] + [Function(nameof(SubsequentDeltaUsersReaderFunction))] public async Task GetDeltaUsersAsync([ActivityTrigger] SubsequentDeltaUsersReaderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(SubsequentDeltaUsersReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentMembersReader/SubsequentMembersReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentMembersReader/SubsequentMembersReaderFunction.cs index 9cdca96e7..5a2edcc04 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentMembersReader/SubsequentMembersReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentMembersReader/SubsequentMembersReaderFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Models; using Repositories.Contracts; @@ -10,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -24,7 +23,7 @@ public SubsequentMembersReaderFunction(ILoggingRepository loggingRepository, SGM _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); } - [FunctionName(nameof(SubsequentMembersReaderFunction))] + [Function(nameof(SubsequentMembersReaderFunction))] public async Task GetMembersAsync([ActivityTrigger] SubsequentMembersReaderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(SubsequentMembersReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentUsersReader/SubsequentUsersReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentUsersReader/SubsequentUsersReaderFunction.cs index 53c6a037f..aaaa853b7 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentUsersReader/SubsequentUsersReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/SubsequentUsersReader/SubsequentUsersReaderFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Models; using Repositories.Contracts; @@ -10,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -24,7 +23,7 @@ public SubsequentUsersReaderFunction(ILoggingRepository loggingRepository, SGMem _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); } - [FunctionName(nameof(SubsequentUsersReaderFunction))] + [Function(nameof(SubsequentUsersReaderFunction))] public async Task GetUsersAsync([ActivityTrigger] SubsequentUsersReaderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(SubsequentUsersReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs index 1c3eedee2..d14c2ef61 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs @@ -2,13 +2,12 @@ // Licensed under the MIT license. using Models; using Microsoft.ApplicationInsights; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -23,7 +22,7 @@ public TelemetryTrackerFunction(ILoggingRepository loggingRepository, TelemetryC _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - [FunctionName(nameof(TelemetryTrackerFunction))] + [Function(nameof(TelemetryTrackerFunction))] public async Task TrackEventAsync([ActivityTrigger] TelemetryTrackerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TelemetryTrackerFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/UsersReader/UsersReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/UsersReader/UsersReaderFunction.cs index 0ddf8c066..6e684abab 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/UsersReader/UsersReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/UsersReader/UsersReaderFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Models; using Repositories.Contracts; @@ -10,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -24,7 +23,7 @@ public UsersReaderFunction(ILoggingRepository loggingRepository, SGMembershipCal _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); } - [FunctionName(nameof(UsersReaderFunction))] + [Function(nameof(UsersReaderFunction))] public async Task GetUsersAsync([ActivityTrigger] UsersReaderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(UsersReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/UsersSender/UsersSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/UsersSender/UsersSenderFunction.cs index 0cc4f826c..af1d4c5c7 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/UsersSender/UsersSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/UsersSender/UsersSenderFunction.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models.Helpers; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Newtonsoft.Json; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GroupMembershipObtainer { @@ -23,7 +22,7 @@ public UsersSenderFunction(ILoggingRepository loggingRepository, SGMembershipCal _calculator = calculator; } - [FunctionName(nameof(UsersSenderFunction))] + [Function(nameof(UsersSenderFunction))] public async Task SendUsersAsync([ActivityTrigger] UsersSenderRequest request) { string filePath = null; diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj index f53efe6b4..f6192c98a 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/GroupMembershipObtainer.csproj @@ -2,15 +2,18 @@ net8.0 v4 + Exe + enabled - - - - + + + + + @@ -32,4 +35,4 @@ Never - + \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs index fd287c085..02cd9b185 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs @@ -1,7 +1,5 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; using Microsoft.Graph; @@ -18,6 +16,8 @@ using Repositories.Contracts.InjectConfig; using Models.Notifications; using Newtonsoft.Json.Linq; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.GroupMembershipObtainer { @@ -41,8 +41,8 @@ IEmailSenderRecipient emailSenderRecipient _emailSenderRecipient = emailSenderRecipient; } - [FunctionName(nameof(OrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context, ExecutionContext executionContext) + [Function(nameof(OrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var mainRequest = context.GetInput(); if (mainRequest != null && mainRequest.SyncJob != null) @@ -87,7 +87,7 @@ public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrat } var additionalContentParams = new[] { - destinationName.ToString(), + destinationName.ToString(), syncJob.TargetOfficeGroupId.ToString(), sourceGroupId.ToString(), }; diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Program.cs new file mode 100644 index 000000000..c90a5a13a --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Program.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.Extensions.Hosting; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using DIConcreteTypes; +using Hosts.FunctionBase; +using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Repositories.BlobStorage; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.GraphGroups; +using Repositories.ServiceBusQueue; +using System; +using Azure.Identity; +namespace Hosts.GroupMembershipObtainer +{ + + public class Program + { + public static void Main (string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = "GroupMembershipObtainer"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + services.AddOptions().Configure((settings, configuration) => + { + settings.DeltaCacheEnabled = CommonServices.GetBoolSettingBase(configuration, "GroupMembershipObtainer:IsDeltaCacheEnabled", false); + }); + services.AddSingleton(services => + { + return new DeltaCachingConfig(services.GetService>().Value.DeltaCacheEnabled); + }); + services.AddGraphAPIClient() + .AddScoped() + .AddSingleton((s) => + { + var configuration = s.GetService(); + var storageAccountName = configuration["membershipStorageAccountName"]; + var containerName = configuration["membershipContainerName"]; + + return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); + }) + .AddScoped(services => + { + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + var notificationsQueueRepository = new ServiceBusQueueRepository(sender); + + return new SGMembershipCalculator( + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + notificationsQueueRepository, + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService() + ); + }) + .AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var membershipAggregatorQueue = configuration["serviceBusMembershipAggregatorQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(membershipAggregatorQueue); + return new ServiceBusQueueRepository(sender); + }); + }) + .Build(); + host.Run(); + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Starter/StarterFunction.cs index bf797f6d3..c29adbd6e 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Starter/StarterFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Azure.Messaging.ServiceBus; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Newtonsoft.Json; using Repositories.Contracts; @@ -10,6 +8,9 @@ using System; using System.Text; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace Hosts.GroupMembershipObtainer { @@ -26,10 +27,10 @@ public StarterFunction(ILoggingRepository loggingRepository, IDatabaseSyncJobsRe _isGroupMembershipDryRunEnabled = dryRun.DryRunEnabled; } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task RunAsync( [ServiceBusTrigger("%serviceBusSyncJobTopic%", "GroupMembership", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient client) { var syncJob = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(message.Body)); var runId = syncJob.RunId.GetValueOrDefault(Guid.Empty); @@ -53,7 +54,7 @@ public async Task RunAsync( IsDestinationPart = message.ApplicationProperties.ContainsKey("IsDestinationPart") ? Convert.ToBoolean(message.ApplicationProperties["IsDestinationPart"]) : false, }; - var instanceId = await starter.StartNewAsync(nameof(OrchestratorFunction), request); + var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), request); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"InstanceId: {instanceId} for job Id: {syncJob.Id} ", RunId = runId }); } diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Startup.cs deleted file mode 100644 index c44f176f7..000000000 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Startup.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Azure.Messaging.ServiceBus; -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Repositories.BlobStorage; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphGroups; -using Repositories.ServiceBusQueue; - -// see https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection -[assembly: FunctionsStartup(typeof(Hosts.GroupMembershipObtainer.Startup))] - -namespace Hosts.GroupMembershipObtainer -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(GroupMembershipObtainer); - protected override string DryRunSettingName => "GroupMembershipObtainer:IsDryRunEnabled"; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - settings.DeltaCacheEnabled = GetBoolSetting(configuration, "GroupMembershipObtainer:IsDeltaCacheEnabled", false); - }); - builder.Services.AddSingleton(services => - { - return new DeltaCachingConfig(services.GetService>().Value.DeltaCacheEnabled); - }); - builder.Services.AddGraphAPIClient() - .AddScoped() - .AddSingleton((s) => - { - var configuration = s.GetService(); - var storageAccountName = configuration["membershipStorageAccountName"]; - var containerName = configuration["membershipContainerName"]; - - return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); - }) - .AddScoped(services => - { - var configuration = services.GetRequiredService(); - var notificationsQueue = configuration["serviceBusNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(notificationsQueue); - var notificationsQueueRepository = new ServiceBusQueueRepository(sender); - - return new SGMembershipCalculator( - services.GetRequiredService(), - services.GetRequiredService(), - services.GetRequiredService(), - notificationsQueueRepository, - services.GetRequiredService(), - services.GetRequiredService(), - services.GetRequiredService() - ); - }) - .AddSingleton(services => - { - var configuration = services.GetRequiredService(); - var membershipAggregatorQueue = configuration["serviceBusMembershipAggregatorQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(membershipAggregatorQueue); - return new ServiceBusQueueRepository(sender); - }); - } - - private bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) - { - var checkParse = bool.TryParse(configuration[settingName], out bool value); - if (checkParse) - return value; - return defaultValue; - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs index 29ee8c81c..0c544123b 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs @@ -1,8 +1,6 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System.Threading.Tasks; using System.Collections.Generic; @@ -17,6 +15,8 @@ using Models; using Hosts.GroupMembershipObtainer; using System.Net.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.GroupMembershipObtainer { @@ -42,8 +42,8 @@ public SubOrchestratorFunction( /// /// /// Compressed serialized SubOrchestratorResponse - [FunctionName(nameof(SubOrchestratorFunction))] - public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(SubOrchestratorFunction))] + public async Task RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var request = context.GetInput(); var allUsers = new List(); @@ -259,7 +259,7 @@ private void TrackCachedUsersEvent(Guid runId, int cachedUsersCount, Guid groupI _telemetryClient.TrackEvent("UsersInCacheCount", cachedUsersEvent); } - public async Task GetFileDownloaderFunction(IDurableOrchestrationContext context, string filePath, SyncJob syncJob) + public async Task GetFileDownloaderFunction(TaskOrchestrationContext context, string filePath, SyncJob syncJob) { return await context.CallActivityAsync(nameof(FileDownloaderFunction), new FileDownloaderRequest @@ -269,7 +269,7 @@ public async Task GetFileDownloaderFunction(IDurableOrchestrationContext }); } - public async Task ClearCacheFunction(IDurableOrchestrationContext context, string filePath, SyncJob syncJob) + public async Task ClearCacheFunction(TaskOrchestrationContext context, string filePath, SyncJob syncJob) { await context.CallActivityAsync(nameof(FileDeleterFunction), new FileDeleterRequest @@ -279,7 +279,7 @@ await context.CallActivityAsync(nameof(FileDeleterFunction), }); } - public async Task GetUsersCountFunction(IDurableOrchestrationContext context, Guid groupId, Guid runId) + public async Task GetUsersCountFunction(TaskOrchestrationContext context, Guid groupId, Guid runId) { return await context.CallActivityAsync(nameof(GetUserCountFunction), new GetUserCountRequest @@ -289,7 +289,7 @@ public async Task GetUsersCountFunction(IDurableOrchestrationContext contex }); } - public async Task GetDeltaUsersSenderFunction(IDurableOrchestrationContext context, GroupMembershipRequest request, List allUsers, string deltaUrl) + public async Task GetDeltaUsersSenderFunction(TaskOrchestrationContext context, GroupMembershipRequest request, List allUsers, string deltaUrl) { var compressedUsers = TextCompressor.Compress(JsonConvert.SerializeObject(allUsers)); @@ -311,7 +311,7 @@ await context.CallActivityAsync(nameof(DeltaUsersSenderFunction), /// /// /// Compressed serialized MembersReaderResponse - public async Task GetMembersReaderFunction(IDurableOrchestrationContext context, GroupMembershipRequest request) + public async Task GetMembersReaderFunction(TaskOrchestrationContext context, GroupMembershipRequest request) { var allUsers = new List(); var allNonUserGraphObjects = new Dictionary(); @@ -350,7 +350,7 @@ public async Task GetMembersReaderFunction(IDurableOrchestrationContext /// /// Compressed serialized UsersReaderResponse public async Task GetUsersReaderFunction( - IDurableOrchestrationContext context, + TaskOrchestrationContext context, GroupMembershipRequest request) { var allUsers = new List(); @@ -380,7 +380,7 @@ public async Task GetUsersReaderFunction( /// /// Compressed serialized DeltaUserReaderResponse public async Task GetDeltaUsersReaderFunction( - IDurableOrchestrationContext context, + TaskOrchestrationContext context, string fileContent, GroupMembershipRequest request) { diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/host.json b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/host.json index b715b753b..3c45f5fad 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/host.json @@ -16,9 +16,9 @@ "Host.Concurrency": "Trace" }, "applicationInsights": { - "samplingExcludedTypes": "Request", "samplingSettings": { - "isEnabled": true + "isEnabled": true, + "excludedTypes": "Request" } } } diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json index ab28ad63a..9e0733840 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json @@ -7,7 +7,7 @@ "graphCredentials:KeyVaultTenantId": "", "appConfigurationEndpoint": "", "serviceBusSyncJobTopic": "", - "gmmServiceBus__fullyQualifiedNamespace": ".servicebus.windows.net", + "gmmServiceBus__fullyQualifiedNamespace": "\u003Cservice_bus_namespace\u003E.servicebus.windows.net", "graphCredentials:ClientSecret": "", "graphCredentials:ClientId": "", "graphCredentials:TenantId": "", @@ -24,6 +24,7 @@ "graphUpdaterFunctionKey": "", "actionableEmailProviderId": "", "serviceBusMembershipAggregatorQueue": "", - "serviceBusNotificationsQueue": "" + "serviceBusNotificationsQueue": "", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs index 6c1bb2200..eb86a4163 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs @@ -3,7 +3,6 @@ using Hosts.GroupMembershipObtainer; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -21,6 +20,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.DurableTask; +using Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http; namespace Tests.Services { @@ -36,7 +37,7 @@ public class OrchestratorTests private Mock _emailSenderRecipient; private Mock _blobStorageRepository; private Mock _serviceBusQueueRepository; - private Mock _durableOrchestrationContext; + private Mock _durableOrchestrationContext; private Mock _configurationRefresherProvider; private Mock _databaseDestinationAttributesRepository; private Mock _teamsChannelRepository; @@ -62,7 +63,7 @@ public void Setup() _graphGroupRepository = new Mock(); _emailSenderRecipient = new Mock(); _blobStorageRepository = new Mock(); - _durableOrchestrationContext = new Mock(); + _durableOrchestrationContext = new Mock(); _configurationRefresherProvider = new Mock(); _executionContext = new Mock(); _telemetryClient = new TelemetryClient(new TelemetryConfiguration()); @@ -110,34 +111,46 @@ public void Setup() _durableOrchestrationContext.Setup(x => x.CurrentUtcDateTime).Returns(DateTime.UtcNow); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(),It.IsAny())) + .Callback(async (name, request, options) => { await CallJobStatusUpdaterFunctionAsync(request as JobStatusUpdaterRequest); }); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(DestinationNameReaderFunction), It.IsAny())) + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(DestinationNameReaderFunction)), It.IsAny(),It.IsAny())) .ReturnsAsync("ExpectedDestinationName"); AzureADGroup sourceGroup = null; string id = null; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(TelemetryTrackerFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(),It.IsAny())) + .Callback(async (name, request, options) => { var telemetryRequest = request as TelemetryTrackerRequest; await CallTelemetryTrackerFunctionAsync(telemetryRequest); }); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync<(AzureADGroup, string)>(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - (sourceGroup, id) = await CallSourceGroupsReaderFunctionAsync(request as GroupReaderRequest); - }). - ReturnsAsync(() => (sourceGroup, id)); - + _durableOrchestrationContext.Setup(x => x.CallActivityAsync<(AzureADGroup, string)>( + It.Is(x => x.ToString() == nameof(GroupReaderFunction)), + It.IsAny(), + It.IsAny()) + ) + .Callback(async (name, request, options) => + { + // Retrieve the result asynchronously + var result = await CallSourceGroupsReaderFunctionAsync(request as GroupReaderRequest); + + // Assign the result values to sourceGroup and id + sourceGroup = result.Item1; + id = result.Item2; + }) + .Returns((TaskName name, object request, TaskOptions options) => + { + // Return the result as a completed Task + return Task.FromResult((sourceGroup, id)); + }); _subOrchestratorResponseStatus = SyncStatus.InProgress; - _durableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny())) + _durableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), It.IsAny())) .ReturnsAsync(() => { var users = new List(); @@ -155,31 +168,31 @@ public void Setup() }); string _filePath = null; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _filePath = await CallUsersSenderFunctionAsync(request as UsersSenderRequest); }) .ReturnsAsync(() => _filePath); _membershipAgregatorResponse = new DurableHttpResponse(System.Net.HttpStatusCode.NoContent); - _durableOrchestrationContext.Setup(x => x.CallHttpAsync(It.IsAny())).ReturnsAsync(() => _membershipAgregatorResponse); + _durableOrchestrationContext.Setup(r => r.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(_membershipAgregatorResponse); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallEmailSenderFunctionAsync(request as EmailSenderRequest); }); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(QueueMessageSenderFunction), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(QueueMessageSenderFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallQueueMessageSenderFunctionAsync(request as MembershipAggregatorHttpRequest); }); _schemaProvider = SchemaProviderFactory.CreateJsonSchemaProvider(); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(SchemaValidatorFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); }) @@ -199,7 +212,7 @@ public async Task TestInvalidCurrentPartAsync() _emailSenderRecipient.Object ); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains("Found invalid value for CurrentPart or TotalParts")), @@ -228,7 +241,7 @@ public async Task TestInvalidQuerySourceAsync() _emailSenderRecipient.Object ); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains($"Marking job as {SyncStatus.QueryNotValid}")), @@ -261,7 +274,7 @@ public async Task TestEmptySourceGroupsAsync() _emailSenderRecipient.Object ); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains($"Marking job as {SyncStatus.QueryNotValid}")), @@ -293,8 +306,8 @@ public async Task TestGetGroupsNameAsync() _emailSenderRecipient.Object ); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); - _durableOrchestrationContext.Verify(x => x.CallActivityAsync(nameof(DestinationNameReaderFunction), _orchestratorRequest.SyncJob), Times.Once()); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); + _durableOrchestrationContext.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(DestinationNameReaderFunction)), _orchestratorRequest.SyncJob, It.IsAny()), Times.Once()); } @@ -310,7 +323,7 @@ public async Task TestGroupMembershipNotFoundAsync() _emailSenderRecipient.Object ); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); _syncJobRepository.Verify(x => x.UpdateSyncJobStatusAsync( It.IsAny>(), @@ -321,7 +334,7 @@ public async Task TestGroupMembershipNotFoundAsync() [TestMethod] public async Task TestUnhandledExceptionAsync() { - _durableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny())) + _durableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), It.IsAny())) .Throws(); var orchestratorFunction = new OrchestratorFunction( @@ -331,7 +344,7 @@ public async Task TestUnhandledExceptionAsync() _emailSenderRecipient.Object ); - await Assert.ThrowsExceptionAsync(async () => await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object)); + await Assert.ThrowsExceptionAsync(async () => await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object)); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.StartsWith("Caught unexpected exception")), @@ -351,7 +364,7 @@ public async Task TestGraphAPITimeoutExceptionAsync() { var exception = new Exception("The request timed out"); - _durableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny())) + _durableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), It.IsAny())) .Throws(exception); var orchestratorFunction = new OrchestratorFunction( @@ -361,7 +374,7 @@ public async Task TestGraphAPITimeoutExceptionAsync() _emailSenderRecipient.Object ); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.StartsWith("Rescheduling job at")), @@ -389,7 +402,7 @@ public async Task TestValidPartRequestAsync() _emailSenderRecipient.Object ); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains($"Read {_usersToReturn} users from source groups")), @@ -431,8 +444,8 @@ public async Task TestMissingSchemasAsync() { _schemaProvider = new SchemaProvider(); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(SchemaValidatorFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); }) @@ -445,7 +458,7 @@ public async Task TestMissingSchemasAsync() _emailSenderRecipient.Object ); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message == $"No json schemas have been loaded. Skipping schema validation."), @@ -460,8 +473,8 @@ public async Task TestMissingGroupMembershipSchemaAsync() { _schemaProvider = SchemaProviderFactory.CreateMissingGroupMembershipSchemaProvider(); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(SchemaValidatorFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); }) @@ -474,7 +487,7 @@ public async Task TestMissingGroupMembershipSchemaAsync() _emailSenderRecipient.Object ); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message == $"No GroupMembership schema has been loaded. Skipping schema validation."), @@ -494,16 +507,16 @@ public async Task TestInvalidSchemaAsync() _emailSenderRecipient.Object ); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(SchemaValidatorFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); }) .ReturnsAsync(() => false); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); - _durableOrchestrationContext.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), - It.Is(x => x.Status == SyncStatus.SchemaError)), Times.Once()); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); + _durableOrchestrationContext.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(JobStatusUpdaterFunction)), + It.Is(x => x.Status == SyncStatus.SchemaError), It.IsAny()), Times.Once()); } [TestMethod] @@ -511,7 +524,7 @@ public async Task TestJsonReaderExceptionAsync() { _schemaProvider = SchemaProviderFactory.CreateMissingGroupMembershipSchemaProvider(); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(SchemaValidatorFunction)), It.IsAny(), It.IsAny())) .ThrowsAsync(new JsonReaderException()); var orchestratorFunction = new OrchestratorFunction( @@ -521,7 +534,7 @@ public async Task TestJsonReaderExceptionAsync() _emailSenderRecipient.Object ); - await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains("Source query is not valid for job")), @@ -530,8 +543,8 @@ public async Task TestJsonReaderExceptionAsync() It.IsAny() ), Times.Once); - _durableOrchestrationContext.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), - It.Is(x => x.Status == SyncStatus.QueryNotValid)), Times.Once()); + _durableOrchestrationContext.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(JobStatusUpdaterFunction)), + It.Is(x => x.Status == SyncStatus.QueryNotValid), It.IsAny()), Times.Once()); } private async Task CallTelemetryTrackerFunctionAsync(TelemetryTrackerRequest request) diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/StarterTests.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/StarterTests.cs index b20b707b7..466269b35 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/StarterTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/StarterTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Azure.Messaging.ServiceBus; using Hosts.GroupMembershipObtainer; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Models; @@ -14,6 +13,10 @@ using System.Text; using System.Threading.Tasks; using System.Collections.Concurrent; +using Microsoft.DurableTask; +using System.Threading; +using System.Linq; +using Repositories.Mocks; namespace Tests.Services { @@ -23,7 +26,7 @@ public class StarterTests private Mock _dryRunValue; private Mock _loggingRepository; private Mock _syncJobRepository; - private Mock _durableOrchestrationClient; + private Mock _durableOrchestrationClient; private SyncJob _syncJob; [TestInitialize] @@ -32,7 +35,7 @@ public void Setup() _dryRunValue = new Mock(); _loggingRepository = new Mock(); _syncJobRepository = new Mock(); - _durableOrchestrationClient = new Mock(); + _durableOrchestrationClient = new Mock(); _syncJob = new SyncJob { @@ -58,9 +61,9 @@ public async Task TestRegularSyncJobRun() var starterFunction = new StarterFunction(_loggingRepository.Object, _syncJobRepository.Object, _dryRunValue.Object); await starterFunction.RunAsync(message, _durableOrchestrationClient.Object); - _durableOrchestrationClient.Verify(x => x.StartNewAsync( - It.IsAny(), - It.Is(r => r.CurrentPart == 1 && r.TotalParts == 3) + _durableOrchestrationClient.Verify(x => x.ScheduleNewOrchestrationInstanceAsync( + It.IsAny(), + It.Is(r => r.CurrentPart == 1 && r.TotalParts == 3), null, It.IsAny() ), Times.Once); _loggingRepository.Verify(x => x.LogMessageAsync( @@ -96,7 +99,7 @@ public async Task TestDryRunSyncJobRun() var starterFunction = new StarterFunction(_loggingRepository.Object, _syncJobRepository.Object, _dryRunValue.Object); await starterFunction.RunAsync(message, _durableOrchestrationClient.Object); - _durableOrchestrationClient.Verify(x => x.StartNewAsync(It.IsAny(), It.IsAny()), Times.Never); + _durableOrchestrationClient.Verify(x => x.ScheduleNewOrchestrationInstanceAsync(It.IsAny(), It.IsAny(), null, It.IsAny()), Times.Never); _syncJobRepository.Verify(x => x.UpdateSyncJobStatusAsync(It.IsAny>(), It.Is(s => s == SyncStatus.Idle)), Times.Once); _loggingRepository.Verify(x => x.LogMessageAsync( @@ -121,4 +124,6 @@ public async Task TestDryRunSyncJobRun() ), Times.Once); } } + } + diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs index 8620467b9..6eed41db4 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs @@ -3,7 +3,7 @@ using Hosts.GroupMembershipObtainer; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Models.Helpers; @@ -34,7 +34,7 @@ public class SubOrchestratorFunctionTests private Mock _graphGroupRepository; private Mock _emailSenderRecipient; private Mock _blobStorageRepository; - private Mock _durableOrchestrationContext; + private Mock _durableOrchestrationContext; private Mock _serviceBusQueueRepository; private Mock _destinationAttributesRepository; private Mock _teamsChannelRepository; @@ -64,7 +64,7 @@ public void Setup() _graphGroupRepository = new Mock(); _emailSenderRecipient = new Mock(); _blobStorageRepository = new Mock(); - _durableOrchestrationContext = new Mock(); + _durableOrchestrationContext = new Mock(); _telemetryClient = new TelemetryClient(new TelemetryConfiguration()); _serviceBusQueueRepository = new Mock(); _teamsChannelRepository = new Mock(); @@ -140,75 +140,75 @@ public void Setup() _durableOrchestrationContext.Setup(x => x.GetInput()).Returns(() => _groupMembershipRequest); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallJobStatusUpdaterFunctionAsync(request as JobStatusUpdaterRequest); }); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _groupExists = await CallGroupValidatorFunctionAsync(request as GroupValidatorRequest); }) .ReturnsAsync(() => _groupExists); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallDeltaUsersSenderFunctionAsync(request as DeltaUsersSenderRequest); }); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallFileDeleterFunctionAsync(request as FileDeleterRequest); }); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), - It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), + It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _deltaUsersReaderResponse = await CallDeltaUsersReaderFunctionAsync(request as DeltaUsersReaderRequest); }) .ReturnsAsync(() => _deltaUsersReaderResponse); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), - It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), + It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _deltaUsersReaderResponse = await CallSubsequentDeltaUsersReaderFunctionAsync(request as SubsequentDeltaUsersReaderRequest); }) .ReturnsAsync(() => _deltaUsersReaderResponse); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), - It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), + It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _membersReaderResponse = await CallMembersReaderFunctionAsync(request as MembersReaderRequest); }) .ReturnsAsync(() => _membersReaderResponse); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), - It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), + It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _membersReaderResponse = await CallSubsequentMembersReaderFunctionAsync(request as SubsequentMembersReaderRequest); }) .ReturnsAsync(() => _membersReaderResponse); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), - It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), + It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _usersReaderResponse = await CallUsersReaderFunctionAsync(request as UsersReaderRequest); }) .ReturnsAsync(() => _usersReaderResponse); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), - It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), + It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _usersReaderResponse = await CallSubsequentUsersReaderFunctionAsync(request as SubsequentUsersReaderRequest); }) @@ -307,15 +307,15 @@ public async Task ProcessDeltaSinglePageRequestTestAsync() { string content = null; _groupCount = 0; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var fileDownloaderRequest = request as FileDownloaderRequest; @@ -374,23 +374,23 @@ public async Task ProcessDeltaSinglePageRequestWithExtraUserTestAsync() string content = null; _groupCount = 0; _blobStorageRepository.Setup(x => x.DownloadCacheFileAsync(It.IsAny())).ReturnsAsync(() => _extraUserBlobResult); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _userCount = await CallUsersReaderFunctionAsync(request as GetUserCountRequest); }) .ReturnsAsync(() => _userCount); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var fileDownloaderRequest = request as FileDownloaderRequest; @@ -399,10 +399,10 @@ public async Task ProcessDeltaSinglePageRequestWithExtraUserTestAsync() }) .ReturnsAsync(() => content); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - var fileDeleterRequest = request as FileDeleterRequest; + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + var fileDeleterRequest = request as FileDeleterRequest; await CallFileDeleterFunctionAsync(fileDeleterRequest); }); @@ -456,21 +456,21 @@ public async Task VerifyCountTestAsync() { _groupCount = 0; _userCount = 0; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _userCount = await CallUsersReaderFunctionAsync(request as GetUserCountRequest); }) .ReturnsAsync(() => _userCount); _deltaUrl = "http://delta-url"; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _deltaUrl = await CallFileDownloaderFunctionAsync(request as FileDownloaderRequest); }) @@ -514,16 +514,16 @@ public async Task VerifyCountTestAsync() public async Task ProcessDeltaLinkSinglePageRequestTestAsync() { _groupCount = 0; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); _deltaUrl = "http://delta-url"; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _deltaUrl = await CallFileDownloaderFunctionAsync(request as FileDownloaderRequest); }) @@ -575,21 +575,21 @@ public async Task ProcessDeltaLinkSinglePageRequestTestAsync() public async Task HandleDeltaLinkExceptionAsync() { _groupCount = 0; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); _deltaUrl = "http://delta-url"; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _deltaUrl = await CallFileDownloaderFunctionAsync(request as FileDownloaderRequest); }) .ReturnsAsync(() => _deltaUrl); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _deltaUsersReaderResponse = await CallDeltaUsersReaderFunctionAsync(request as DeltaUsersReaderRequest); }) @@ -637,8 +637,8 @@ public async Task HandleDeltaLinkExceptionAsync() public async Task ProcessTMSinglePageRequestTestAsync() { _groupCount = 2; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) @@ -693,17 +693,17 @@ public async Task ProcessDeltaMultiplePageRequestTestAsync() _usersReaderNextPageUrl = "http://next-page-url"; _groupCount = 0; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - var fileDownloaderRequest = request as FileDownloaderRequest; + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + var fileDownloaderRequest = request as FileDownloaderRequest; if (fileDownloaderRequest.FilePath.StartsWith("cache/delta_")) content = string.Empty; @@ -758,24 +758,24 @@ public async Task ProcessDeltaMultiplePageRequestTestAsync() public async Task ProcessDeltaLinkMultiplePageRequestTestAsync() { _groupCount = 0; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); _deltaUrl = "http://delta-url"; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _deltaUrl = await CallFileDownloaderFunctionAsync(request as FileDownloaderRequest); }) .ReturnsAsync(() => _deltaUrl); _deltaUrl = "http://delta-url"; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _deltaUrl = await CallFileDownloaderFunctionAsync(request as FileDownloaderRequest); }) @@ -821,10 +821,10 @@ public async Task ProcessTMMultiplePageRequestTestAsync() _usersReaderNextPageUrl = "http://next-page-url"; _deltaUrl = "http://delta-url"; _groupCount = 2; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); @@ -878,17 +878,17 @@ public async Task ProcessDeltaMultiplePagesRequestTestAsync() _usersReaderNextPageUrl = "http://next-page-url"; _deltaUrl = "http://delta-url"; _groupCount = 0; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - var fileDownloaderRequest = request as FileDownloaderRequest; + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + var fileDownloaderRequest = request as FileDownloaderRequest; if (fileDownloaderRequest.FilePath.StartsWith("cache/delta_")) content = string.Empty; @@ -923,10 +923,10 @@ public async Task ProcessDeltaMultiplePagesRequestTestAsync() public async Task ProcessDeltaLinkMultiplePagesRequestTestAsync() { _groupCount = 0; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); @@ -945,8 +945,8 @@ public async Task ProcessDeltaLinkMultiplePagesRequestTestAsync() }); _deltaUrl = "http://delta-url"; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _deltaUrl = await CallFileDownloaderFunctionAsync(request as FileDownloaderRequest); }) @@ -967,10 +967,10 @@ public async Task ProcessTMMultiplePagesRequestTestAsync() _usersReaderNextPageUrl = "http://next-page-url"; _deltaUrl = "http://delta-url"; _groupCount = 2; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + _groupCount = await CallGroupsReaderFunctionAsync(request as GetTransitiveGroupCountRequest); }) .ReturnsAsync(() => _groupCount); @@ -1037,7 +1037,7 @@ public async Task GroupDoesNotExistTestAsync() [TestMethod] public async Task TestTransientExceptionAsync() { - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(); var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); @@ -1062,7 +1062,7 @@ public async Task TestTransientExceptionAsync() [TestMethod] public async Task TestOtherExceptionAsync() { - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(); var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Tests.Services.csproj b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Tests.Services.csproj index b4dc80bef..0a163c686 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Tests.Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/Tests.Services.csproj @@ -19,6 +19,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/MockDurableTaskClient.cs b/Service/GroupMembershipManagement/Repositories.Mocks/MockDurableTaskClient.cs new file mode 100644 index 000000000..385d56434 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.Mocks/MockDurableTaskClient.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Repositories.Mocks +{ + public class MockDurableTaskClient : DurableTaskClient + { + public MockDurableTaskClient() : base("mock") + { + } + + public override Task ScheduleNewOrchestrationInstanceAsync(TaskName orchestratorName, object input = null, StartOrchestrationOptions options = null, + CancellationToken cancellation = new()) + { + return Task.FromResult(options?.InstanceId ?? Guid.NewGuid().ToString()); + } + + public override Task RaiseEventAsync(string instanceId, string eventName, object eventPayload = null, CancellationToken cancellation = new()) + { + return Task.CompletedTask; + } + + public override Task WaitForInstanceStartAsync(string instanceId, bool getInputsAndOutputs = false, + CancellationToken cancellation = new()) + { + return Task.FromResult(new OrchestrationMetadata(Guid.NewGuid().ToString(), instanceId)); + } + + public override Task WaitForInstanceCompletionAsync(string instanceId, bool getInputsAndOutputs = false, + CancellationToken cancellation = new()) + { + return Task.FromResult(new OrchestrationMetadata(Guid.NewGuid().ToString(), instanceId)); + } + + public override Task TerminateInstanceAsync(string instanceId, object output = null, CancellationToken cancellation = new()) + { + return Task.CompletedTask; + } + + public override Task SuspendInstanceAsync(string instanceId, string reason = null, CancellationToken cancellation = new()) + { + return Task.CompletedTask; + } + + public override Task ResumeInstanceAsync(string instanceId, string reason = null, CancellationToken cancellation = new()) + { + return Task.CompletedTask; + } + + public override Task GetInstancesAsync(string instanceId, bool getInputsAndOutputs = false, + CancellationToken cancellation = new()) + { + return Task.FromResult(new OrchestrationMetadata(Guid.NewGuid().ToString(), instanceId)); + } + + public override AsyncPageable GetAllInstancesAsync(OrchestrationQuery filter = null) + { + return new MockOrchestrationMetadataAsyncPageable(); + } + + public override Task PurgeInstanceAsync(string instanceId, CancellationToken cancellation = new()) + { + return Task.FromResult(new PurgeResult(1)); + } + + public override Task PurgeAllInstancesAsync(PurgeInstancesFilter filter, CancellationToken cancellation = new()) + { + return Task.FromResult(new PurgeResult(Random.Shared.Next())); + } + + public override ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } + + } + internal class MockOrchestrationMetadataAsyncPageable : AsyncPageable + { + public override IAsyncEnumerable> AsPages(string continuationToken = null, int? pageSizeHint = null) + { + return AsyncEnumerable.Empty>(); + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj b/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj index 19a85e2cf..acbc319b2 100644 --- a/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj +++ b/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj @@ -7,7 +7,9 @@ + + From 1aeb63ae90eeeffc896d47eb0b0d94f490d0a33f Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 27 Aug 2024 13:40:00 -0700 Subject: [PATCH 0275/1479] Update template for isolate worker model --- .../Infrastructure/compute/template.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep index c29666415..96c406a10 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep @@ -99,7 +99,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } From 6435ee2f9f73cb567db7d237aa74d10808866af4 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 29 Aug 2024 10:01:16 -0700 Subject: [PATCH 0276/1479] Change acitivity response --- .../Activity/GroupReader/GroupReaderFunction.cs | 8 ++++++-- .../Activity/GroupReader/GroupReaderResponse.cs | 7 +++++++ .../Orchestrator/OrchestratorFunction.cs | 4 +++- .../GroupMembershipObtainer/Function/Program.cs | 7 ++++--- .../Services.Tests/OrchestratorTests.cs | 17 +++++++++-------- 5 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderResponse.cs diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderFunction.cs index 675c10c24..f34db1bb1 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderFunction.cs @@ -22,7 +22,7 @@ public GroupReaderFunction(ILoggingRepository loggingRepository, SGMembershipCal } [Function(nameof(GroupReaderFunction))] - public async Task<(AzureADGroup, string)> GetGroupAsync([ActivityTrigger] GroupReaderRequest request) + public async Task GetGroupAsync([ActivityTrigger] GroupReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); if (request.IsDestinationPart) @@ -60,7 +60,11 @@ await _loggingRepository.LogMessageAsync(new LogMessage } await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupReaderFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); - return (azureAdGroup, groupId); + return new GroupReaderResponse + { + SourceGroup = azureAdGroup, + SourceGroupId = groupId + }; } public (AzureADGroup Group, string GroupId) GetSourceGroup(GroupReaderRequest request) diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderResponse.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderResponse.cs new file mode 100644 index 000000000..f67df3747 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Activity/GroupReader/GroupReaderResponse.cs @@ -0,0 +1,7 @@ +using Models; + +public class GroupReaderResponse +{ + public AzureADGroup SourceGroup { get; set; } + public string SourceGroupId { get; set; } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs index 02cd9b185..9b70395bf 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs @@ -62,7 +62,7 @@ public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationC } if (!context.IsReplaying) _ = _log.LogMessageAsync(new LogMessage { Message = $"{nameof(OrchestratorFunction)} function started", RunId = runId }, VerbosityLevel.DEBUG); - var (sourceGroup, sourceGroupId) = await context.CallActivityAsync<(AzureADGroup, string)>(nameof(GroupReaderFunction), + var groupReaderResult = await context.CallActivityAsync(nameof(GroupReaderFunction), new GroupReaderRequest { SyncJob = syncJob, @@ -70,6 +70,8 @@ public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationC IsDestinationPart = mainRequest.IsDestinationPart, RunId = runId }); + var sourceGroup = groupReaderResult.SourceGroup; + var sourceGroupId = groupReaderResult.SourceGroupId; if (sourceGroup.ObjectId == Guid.Empty) { diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Program.cs index c90a5a13a..a4b56e641 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/Program.cs @@ -1,14 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Extensions.Hosting; + +using Azure.Identity; using Azure.Messaging.ServiceBus; using Common.DependencyInjection; using DIConcreteTypes; using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Repositories.BlobStorage; using Repositories.Contracts; @@ -16,7 +17,7 @@ using Repositories.GraphGroups; using Repositories.ServiceBusQueue; using System; -using Azure.Identity; + namespace Hosts.GroupMembershipObtainer { diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs index eb86a4163..4a775ce5d 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs @@ -122,7 +122,7 @@ public void Setup() AzureADGroup sourceGroup = null; string id = null; - + GroupReaderResponse groupReaderResponse = null; _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(),It.IsAny())) .Callback(async (name, request, options) => { @@ -130,24 +130,25 @@ public void Setup() await CallTelemetryTrackerFunctionAsync(telemetryRequest); }); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync<(AzureADGroup, string)>( + _durableOrchestrationContext.Setup(x => x.CallActivityAsync( It.Is(x => x.ToString() == nameof(GroupReaderFunction)), It.IsAny(), It.IsAny()) ) .Callback(async (name, request, options) => { - // Retrieve the result asynchronously var result = await CallSourceGroupsReaderFunctionAsync(request as GroupReaderRequest); - // Assign the result values to sourceGroup and id - sourceGroup = result.Item1; - id = result.Item2; + groupReaderResponse = new GroupReaderResponse + { + SourceGroup = result.Item1, + SourceGroupId = result.Item2 + }; + }) .Returns((TaskName name, object request, TaskOptions options) => { - // Return the result as a completed Task - return Task.FromResult((sourceGroup, id)); + return Task.FromResult(groupReaderResponse); }); _subOrchestratorResponseStatus = SyncStatus.InProgress; _durableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), It.IsAny())) From 99284149e2c449512399726fe28ed38f352711d3 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 29 Aug 2024 10:03:42 -0700 Subject: [PATCH 0277/1479] Change test --- .../Services.Tests/OrchestratorTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs index 4a775ce5d..8e2414b5a 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Services.Tests/OrchestratorTests.cs @@ -47,7 +47,7 @@ public class OrchestratorTests private OrchestratorRequest _orchestratorRequest; private SyncStatus _subOrchestratorResponseStatus; private SGMembershipCalculator _membershipCalculator; - private DurableHttpResponse _membershipAgregatorResponse; + private DurableHttpResponse _membershipAggregatorResponse; private TelemetryClient _telemetryClient; SchemaProvider _schemaProvider; private bool _isValid = true; @@ -176,8 +176,8 @@ public void Setup() }) .ReturnsAsync(() => _filePath); - _membershipAgregatorResponse = new DurableHttpResponse(System.Net.HttpStatusCode.NoContent); - _durableOrchestrationContext.Setup(r => r.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(_membershipAgregatorResponse); + _membershipAggregatorResponse = new DurableHttpResponse(System.Net.HttpStatusCode.NoContent); + _durableOrchestrationContext.Setup(r => r.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(_membershipAggregatorResponse); _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback(async (name, request, options) => From af8ebf9764f8468fd24e6f3d1e9dd7fd9f80875a Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 3 Sep 2024 10:25:59 -0700 Subject: [PATCH 0278/1479] Change test coverage --- yaml/build-functionapps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yaml/build-functionapps.yml b/yaml/build-functionapps.yml index f5b13e993..bbd479058 100644 --- a/yaml/build-functionapps.yml +++ b/yaml/build-functionapps.yml @@ -65,8 +65,8 @@ stages: /p:threshold=${{ app.function.coverageThreshold }} /p:thresholdType=line /p:thresholdStat=total - /p:CoverletOutput=$(Build.SourcesDirectory)\TestResults\Coverage\${{ app.function.name }}\ - /p:Exclude=[Repositories.*]*%2c[Entities]*%2c[*.Entities]*%2c[Models]*%2c[*.Models]*%2c[*.Tests]*%2c[*.Mocks]*%2c[Common.DependencyInjection]*%2c[Hosts.FunctionBase]*%2c[DIConcreteTypes]*%2c[*]Hosts.*.Startup*%2c[*]*.*.Program + /p:CoverletOutput=$(Build.SourcesDirectory)\TestResults\Coverage\${{ app.function.name }} + /p:Exclude=[Repositories.*]*%2c[Entities]*%2c[*.Entities]*%2c[Models]*%2c[*.Models]*%2c[*.Tests]*%2c[*.Mocks]*%2c[Common.DependencyInjection]*%2c[Hosts.FunctionBase]*%2c[DIConcreteTypes]*%2c[*]*.*.Startup*%2c[*]*.*.Program /p:ExcludeByAttribute=Obsolete%2cGeneratedCodeAttribute%2cCompilerGeneratedAttribute' condition: and(succeeded(), eq(variables['BuildConfiguration'], 'debug')) From f5dcb8fb9f07127fe444b54c81c7ce93af3556d8 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 3 Sep 2024 13:19:21 -0700 Subject: [PATCH 0279/1479] Change test coverage of gmo --- vsts-cicd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsts-cicd.yml b/vsts-cicd.yml index 45bc8c36c..d169cd097 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -71,7 +71,7 @@ stages: coverageThreshold: 64 - function: name: 'GroupMembershipObtainer' - coverageThreshold: 89 + coverageThreshold: 83 - function: name: 'SqlMembershipObtainer' coverageThreshold: 50 From 0186e34504013cb3d42fc0ccb87b1bfce32bf99b Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 3 Sep 2024 13:20:38 -0700 Subject: [PATCH 0280/1479] Change local setting of GMO --- .../Hosts/GroupMembershipObtainer/Function/local.settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json index 9e0733840..dcc6ae02b 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/local.settings.json @@ -7,7 +7,7 @@ "graphCredentials:KeyVaultTenantId": "", "appConfigurationEndpoint": "", "serviceBusSyncJobTopic": "", - "gmmServiceBus__fullyQualifiedNamespace": "\u003Cservice_bus_namespace\u003E.servicebus.windows.net", + "gmmServiceBus__fullyQualifiedNamespace": ".servicebus.windows.net", "graphCredentials:ClientSecret": "", "graphCredentials:ClientId": "", "graphCredentials:TenantId": "", From fbe4ec8046abc99b5b8b5434347458565287e6cb Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 5 Sep 2024 16:32:39 -0700 Subject: [PATCH 0281/1479] Remove extension session --- .../Hosts/GraphUpdater/Function/host.json | 2 -- .../Hosts/GroupMembershipObtainer/Function/host.json | 6 ------ 2 files changed, 8 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/host.json b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/host.json index d08057a2e..11846e1c9 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/host.json @@ -3,8 +3,6 @@ "functionTimeout": "00:10:00", "extensions": { "durableTask": { - "extendedSessionsEnabled": true, - "extendedSessionIdleTimeoutInSeconds": 30, "maxConcurrentActivityFunctions": 10, "maxConcurrentOrchestratorFunctions": 5 } diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/host.json b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/host.json index 3c45f5fad..28c22fd92 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Function/host.json @@ -5,12 +5,6 @@ "dynamicConcurrencyEnabled": true, "snapshotPersistenceEnabled": true }, - "extensions": { - "durableTask": { - "extendedSessionsEnabled": true, - "extendedSessionIdleTimeoutInSeconds": 30 - } - }, "logging": { "logLevel": { "Host.Concurrency": "Trace" From d19ee993a29c93e57ead7eaca80a9dad601e9d65 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 6 Sep 2024 13:12:14 -0700 Subject: [PATCH 0282/1479] Migrated SqlMembershipObtainer to isolated worker model --- .../ChildEntitiesFilterFunction.cs | 9 +- .../FeatureFlagReader/FeatureFlagFunction.cs | 13 +- .../GroupMembershipSenderFunction.cs | 15 +- .../JobStatusUpdaterFunction.cs | 7 +- .../Activity/Logger/LoggerFunction.cs | 5 +- .../ManagerOrgReaderFunction.cs | 9 +- .../QueueMessageSenderFunction.cs | 5 +- .../SchemaValidatorFunction.cs | 7 +- .../TableNameReaderFunction.cs | 5 +- .../TelemetryTrackerFunction.cs | 7 +- .../Orchestrator/OrchestratorFunction.cs | 35 +++-- .../Orchestrator/OrchestratorResponse.cs | 13 ++ .../SqlMembershipObtainer/Function/Program.cs | 84 +++++++++++ .../Function/SqlMembershipObtainer.csproj | 15 +- .../Function/Starter/StarterFunction.cs | 17 ++- .../SqlMembershipObtainer/Function/Startup.cs | 65 --------- .../OrganizationProcessorFunction.cs | 12 +- .../Function/local.settings.json | 2 +- .../Infrastructure/compute/template.bicep | 2 +- .../OrchestratorFunctionTests.cs | 132 ++++++++++++------ .../OrganizationProcessorFunctionTests.cs | 41 +++--- 21 files changed, 286 insertions(+), 214 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorResponse.cs create mode 100644 Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ChildEntitiesFilter/ChildEntitiesFilterFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ChildEntitiesFilter/ChildEntitiesFilterFunction.cs index 499f25e48..eb3b1a1ca 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ChildEntitiesFilter/ChildEntitiesFilterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ChildEntitiesFilter/ChildEntitiesFilterFunction.cs @@ -1,13 +1,12 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Models.Helpers; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; using Models; +using Models.Helpers; using Newtonsoft.Json; -using SqlMembershipObtainer.SubOrchestrator; using Repositories.Contracts; using Services.Contracts; +using SqlMembershipObtainer.SubOrchestrator; using System; using System.Linq; using System.Threading.Tasks; @@ -25,7 +24,7 @@ public ChildEntitiesFilterFunction(ISqlMembershipObtainerService sqlMembershipOb _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(ChildEntitiesFilterFunction))] + [Function(nameof(ChildEntitiesFilterFunction))] public async Task FilterChildEntities([ActivityTrigger] ChildEntitiesFilterRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(ChildEntitiesFilterFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/FeatureFlagReader/FeatureFlagFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/FeatureFlagReader/FeatureFlagFunction.cs index 2a9ff7e8b..4d5a2daf4 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/FeatureFlagReader/FeatureFlagFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/FeatureFlagReader/FeatureFlagFunction.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; using Models; using Repositories.Contracts; using System; @@ -12,22 +11,22 @@ namespace SqlMembershipObtainer public class FeatureFlagFunction { private readonly ILoggingRepository _loggingRepository; - private readonly IFeatureFlagRepository _featureFlagRespository; + private readonly IFeatureFlagRepository _featureFlagRepository; public FeatureFlagFunction( ILoggingRepository loggingRepository, - IFeatureFlagRepository featureFlagRespository) + IFeatureFlagRepository featureFlagRepository) { _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); - _featureFlagRespository = featureFlagRespository ?? throw new ArgumentNullException(nameof(featureFlagRespository)); + _featureFlagRepository = featureFlagRepository ?? throw new ArgumentNullException(nameof(featureFlagRepository)); } - [FunctionName(nameof(FeatureFlagFunction))] + [Function(nameof(FeatureFlagFunction))] public async Task CheckFeatureFlagStateAsync([ActivityTrigger] FeatureFlagRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(FeatureFlagFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); - var isFlagEnabled = await _featureFlagRespository.IsFeatureFlagEnabledAsync(request.FeatureFlagName, request.RefreshAppConfigurationValues, request.RunId); + var isFlagEnabled = await _featureFlagRepository.IsFeatureFlagEnabledAsync(request.FeatureFlagName, request.RefreshAppConfigurationValues, request.RunId); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(FeatureFlagFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); return isFlagEnabled; diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/GroupMembershipSender/GroupMembershipSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/GroupMembershipSender/GroupMembershipSenderFunction.cs index 4a5746f1c..f6b2c8206 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/GroupMembershipSender/GroupMembershipSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/GroupMembershipSender/GroupMembershipSenderFunction.cs @@ -1,9 +1,8 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Models.Helpers; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; using Models; +using Models.Helpers; using Newtonsoft.Json; using Repositories.Contracts; using Services.Contracts; @@ -24,8 +23,8 @@ public GroupMembershipSenderFunction(ISqlMembershipObtainerService sqlMembership _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(GroupMembershipSenderFunction))] - public async Task<(SyncStatus Status, string FilePath)> SendGroupMembershipAsync([ActivityTrigger] GroupMembershipSenderRequest request) + [Function(nameof(GroupMembershipSenderFunction))] + public async Task SendGroupMembershipAsync([ActivityTrigger] GroupMembershipSenderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupMembershipSenderFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); @@ -34,7 +33,11 @@ public GroupMembershipSenderFunction(ISqlMembershipObtainerService sqlMembership await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupMembershipSenderFunction)} function completed", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); - return response; + return new OrchestratorResponse + { + Status = response.Status, + FilePath = response.FilePath + }; } } } diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs index 0c96adf04..47dde0bf5 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -1,11 +1,8 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Entities; +using Microsoft.Azure.Functions.Worker; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -25,7 +22,7 @@ public JobStatusUpdaterFunction( _syncJobRepository = syncJobRepository ?? throw new ArgumentNullException(nameof(syncJobRepository)); } - [FunctionName(nameof(JobStatusUpdaterFunction))] + [Function(nameof(JobStatusUpdaterFunction))] public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobStatusUpdaterFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/Logger/LoggerFunction.cs index 91fc0632e..c77b05ed5 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/Logger/LoggerFunction.cs @@ -1,8 +1,7 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; @@ -18,7 +17,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.SyncJob?.RunId }, request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ManagerOrgReader/ManagerOrgReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ManagerOrgReader/ManagerOrgReaderFunction.cs index 84d704a0c..fd6a4d963 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ManagerOrgReader/ManagerOrgReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/ManagerOrgReader/ManagerOrgReaderFunction.cs @@ -1,13 +1,12 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Models.Helpers; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; using Models; +using Models.Helpers; using Newtonsoft.Json; -using SqlMembershipObtainer.SubOrchestrator; using Repositories.Contracts; using Services.Contracts; +using SqlMembershipObtainer.SubOrchestrator; using System; using System.Linq; using System.Threading.Tasks; @@ -25,7 +24,7 @@ public ManagerOrgReaderFunction(ISqlMembershipObtainerService sqlMembershipObtai _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(ManagerOrgReaderFunction))] + [Function(nameof(ManagerOrgReaderFunction))] public async Task ReadUsersAsync([ActivityTrigger] ManagerOrgReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(ManagerOrgReaderFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs index a1951f235..ba0630092 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; using Models; using Models.ServiceBus; using Newtonsoft.Json; @@ -23,7 +22,7 @@ public QueueMessageSenderFunction( _serviceBusQueueRepository = serviceBusQueueRepository ?? throw new ArgumentNullException(nameof(serviceBusQueueRepository)); } - [FunctionName(nameof(QueueMessageSenderFunction))] + [Function(nameof(QueueMessageSenderFunction))] public async Task SendMessageAsync([ActivityTrigger] MembershipAggregatorHttpRequest request) { diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs index a1d98c8cb..8ba20f0b4 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs @@ -1,12 +1,11 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; using Models; +using NJsonSchema; using Repositories.Contracts; using System; using System.Threading.Tasks; -using NJsonSchema; namespace SqlMembershipObtainer { @@ -21,7 +20,7 @@ public SchemaValidatorFunction(ILoggingRepository loggingRepository, SchemaProvi _schemaProvider = schemaProvider ?? throw new ArgumentNullException(nameof(schemaProvider)); } - [FunctionName(nameof(SchemaValidatorFunction))] + [Function(nameof(SchemaValidatorFunction))] public async Task ValidateSchemasAsync([ActivityTrigger] SchemaValidatorRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TableNameReader/TableNameReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TableNameReader/TableNameReaderFunction.cs index 453693f09..a19db3c7b 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TableNameReader/TableNameReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TableNameReader/TableNameReaderFunction.cs @@ -1,8 +1,7 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.Azure.Functions.Worker; using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services.Contracts; using System; @@ -21,7 +20,7 @@ public TableNameReaderFunction(ISqlMembershipObtainerService sqlMembershipObtain _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(TableNameReaderFunction))] + [Function(nameof(TableNameReaderFunction))] public async Task GetSqlMembershipTableName([ActivityTrigger] SyncJob syncJob) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TableNameReaderFunction)} function started", RunId = syncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs index 5e09aa857..c05ddfbe6 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs @@ -1,9 +1,8 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Models; using Microsoft.ApplicationInsights; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Models; using Repositories.Contracts; using System; using System.Collections.Generic; @@ -22,7 +21,7 @@ public TelemetryTrackerFunction(ILoggingRepository loggingRepository, TelemetryC _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - [FunctionName(nameof(TelemetryTrackerFunction))] + [Function(nameof(TelemetryTrackerFunction))] public async Task TrackEventAsync([ActivityTrigger] TelemetryTrackerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TelemetryTrackerFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs index 20b82f7aa..31767b86a 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs @@ -1,24 +1,21 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Newtonsoft.Json; -using SqlMembershipObtainer.Entities; -using System.Net.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Data.SqlClient; +using Microsoft.DurableTask; using Microsoft.Extensions.Configuration; -using System.Net; -using Newtonsoft.Json.Linq; -using Microsoft.Extensions.Primitives; -using Repositories.Contracts; using Microsoft.Graph; using Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Repositories.Contracts; +using SqlMembershipObtainer.Entities; using SqlMembershipObtainer.SubOrchestrator; -using Microsoft.Data.SqlClient; +using System; +using System.IO; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; namespace SqlMembershipObtainer { @@ -32,9 +29,9 @@ public OrchestratorFunction(IConfiguration configuration, ILoggingRepository log _loggingRepository = loggingRepository; } - [FunctionName(nameof(OrchestratorFunction))] + [Function(nameof(OrchestratorFunction))] public async Task RunOrchestratorAsync( - [OrchestrationTrigger] IDurableOrchestrationContext context, ExecutionContext executionContext) + [OrchestrationTrigger] TaskOrchestrationContext context) { var mainRequest = context.GetInput(); if (mainRequest == null || mainRequest.SyncJob == null) { return; } @@ -112,7 +109,7 @@ await context.CallActivityAsync( Message = $"Retrieved {graphProfilesResponse.GraphProfileCount} total profiles from SqlMembershipObtainer", }); - var senderResponse = await context.CallActivityAsync<(SyncStatus Status, string FilePath)>( + var senderResponse = await context.CallActivityAsync( nameof(GroupMembershipSenderFunction), new GroupMembershipSenderRequest { @@ -120,7 +117,7 @@ await context.CallActivityAsync( Profiles = graphProfilesResponse.GraphProfiles, CurrentPart = mainRequest.CurrentPart, Exclusionary = mainRequest.Exclusionary, - AdaptiveCardTemplateDirectory = executionContext.FunctionAppDirectory + AdaptiveCardTemplateDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) }); if (senderResponse.Status != SyncStatus.InProgress) diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorResponse.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorResponse.cs new file mode 100644 index 000000000..c4d38c0ae --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorResponse.cs @@ -0,0 +1,13 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. + +using Models; + +namespace SqlMembershipObtainer +{ + public class OrchestratorResponse + { + public SyncStatus Status { get; set; } + public string FilePath { get; set; } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Program.cs new file mode 100644 index 000000000..6d95fcd5c --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Program.cs @@ -0,0 +1,84 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. + +using Azure.Identity; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using DIConcreteTypes; +using Hosts.FunctionBase; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Repositories.BlobStorage; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.DataFactory; +using Repositories.ServiceBusQueue; +using Repositories.SqlMembershipRepository; +using Services; +using Services.Contracts; +using System; + +namespace SqlMembershipObtainer +{ + public class Program + { + public static void Main(string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()).UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = nameof(SqlMembershipObtainer); + var dryRunSettingName = "SqlMembershipObtainer:IsMembershipAggregatorDryRunEnabled"; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + + services.AddSingleton((s) => + { + var configuration = s.GetService(); + var storageAccountName = configuration["membershipStorageAccountName"]; + var containerName = configuration["membershipContainerName"]; + + return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); + }); + + services.AddSingleton>(services => new KeyVaultSecret(CommonServices.GetValueOrDefaultBase("sqlServerMSIConnectionString"))); + services.AddSingleton(); + + services.AddSingleton>(new DataFactorySecrets( + CommonServices.GetValueOrDefaultBase("pipeline"), + CommonServices.GetValueOrDefaultBase("dataFactoryName"), + CommonServices.GetValueOrDefaultBase("subscriptionId"), + CommonServices.GetValueOrDefaultBase("dataResourceGroup"))); + + services.AddSingleton(); + services.AddGraphAPIClient(); + services.AddSingleton(); + services.AddScoped(); + services.AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var membershipAggregatorQueue = configuration["serviceBusMembershipAggregatorQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(membershipAggregatorQueue); + return new ServiceBusQueueRepository(sender); + }); + + }).Build(); + + host.Run(); + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj index 862e51602..aa736a41e 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SqlMembershipObtainer.csproj @@ -4,6 +4,8 @@ v4 SqlMembershipObtainer SqlMembershipObtainer + Exe + enabled @@ -11,11 +13,11 @@ - - - - - + + + + + @@ -42,4 +44,7 @@ + + + diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Starter/StarterFunction.cs index cfbb8fbb0..27786665e 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Starter/StarterFunction.cs @@ -1,17 +1,16 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using System; -using System.Text; -using System.Threading.Tasks; using Azure.Messaging.ServiceBus; -using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask.Client; using Models; using Newtonsoft.Json; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using Services.Contracts; +using System; +using System.Text; +using System.Threading.Tasks; namespace SqlMembershipObtainer { @@ -28,10 +27,10 @@ public StarterFunction(ILoggingRepository loggingRepository, ISqlMembershipObtai _isSqlMembershipDryRunEnabled = dryRun.DryRunEnabled; } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task RunAsync( [ServiceBusTrigger("%serviceBusTopicName%", "SqlMembership", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient starter) { var syncJob = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(message.Body)); var runId = syncJob.RunId.GetValueOrDefault(Guid.Empty); @@ -55,7 +54,7 @@ public async Task RunAsync( TotalParts = message.ApplicationProperties.ContainsKey("TotalParts") ? Convert.ToInt32(message.ApplicationProperties["TotalParts"]) : 1, }; - var instanceId = await starter.StartNewAsync(nameof(OrchestratorFunction), request); + var instanceId = await starter.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), request); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"InstanceId: {instanceId} for job Id: {syncJob.Id} ", RunId = runId }); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed", RunId = runId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Startup.cs deleted file mode 100644 index 7d5c15fb4..000000000 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Startup.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright(c) Microsoft Corporation. -// Licensed under the MIT license. -using Azure.Messaging.ServiceBus; -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Repositories.BlobStorage; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.DataFactory; -using Repositories.ServiceBusQueue; -using Repositories.SqlMembershipRepository; -using Services; -using Services.Contracts; - - -// see https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection -[assembly: FunctionsStartup(typeof(SqlMembershipObtainer.Startup))] - -namespace SqlMembershipObtainer -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(SqlMembershipObtainer); - protected override string DryRunSettingName => "SqlMembershipObtainer:IsDryRunEnabled"; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddSingleton((s) => - { - var configuration = s.GetService(); - var storageAccountName = configuration["membershipStorageAccountName"]; - var containerName = configuration["membershipContainerName"]; - - return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); - }); - - builder.Services.AddSingleton>(services => new KeyVaultSecret(GetValueOrThrow("sqlServerMSIConnectionString"))); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton>(new DataFactorySecrets(GetValueOrThrow("pipeline"), GetValueOrThrow("dataFactoryName"), GetValueOrThrow("subscriptionId"), GetValueOrThrow("dataResourceGroup"))); - - builder.Services.AddSingleton(); - - builder.Services.AddGraphAPIClient(); - - builder.Services.AddSingleton(); - builder.Services.AddScoped(); - - builder.Services.AddSingleton(services => - { - var configuration = services.GetRequiredService(); - var membershipAggregatorQueue = configuration["serviceBusMembershipAggregatorQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(membershipAggregatorQueue); - return new ServiceBusQueueRepository(sender); - }); - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SubOrchestrator/OrganizationProcessor/OrganizationProcessorFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SubOrchestrator/OrganizationProcessor/OrganizationProcessorFunction.cs index ed9a0c6ca..2f7e24b78 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SubOrchestrator/OrganizationProcessor/OrganizationProcessorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/SubOrchestrator/OrganizationProcessor/OrganizationProcessorFunction.cs @@ -1,15 +1,15 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Models.Helpers; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; using Models; +using Models.Helpers; using Newtonsoft.Json; +using Repositories.Contracts; using SqlMembershipObtainer.SubOrchestrator; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Repositories.Contracts; namespace SqlMembershipObtainer { @@ -19,8 +19,8 @@ public OrganizationProcessorFunction() { } - [FunctionName(nameof(OrganizationProcessorFunction))] - public async Task ProcessQueryAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(OrganizationProcessorFunction))] + public async Task ProcessQueryAsync([OrchestrationTrigger] TaskOrchestrationContext context) { List graphProfileInformation = null; var response = new GraphProfileInformationResponse(); diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/local.settings.json index 0502780be..d5fd3ebf2 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/local.settings.json @@ -2,7 +2,7 @@ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "gmmServiceBus__fullyQualifiedNamespace": ".servicebus.windows.net", "pipeline": "", "dataFactoryName": "gmm-data--adf", diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep index ec97c41bf..2b34a1b0d 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep @@ -122,7 +122,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs index 17a9f6b01..1755652df 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs @@ -2,25 +2,23 @@ // Licensed under the MIT license. using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Data.SqlClient; +using Microsoft.DurableTask; using Microsoft.Extensions.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Models.Helpers; -using Models.ServiceBus; using Moq; using Newtonsoft.Json; -using SqlMembershipObtainer; -using SqlMembershipObtainer.SubOrchestrator; using Repositories.Contracts; using Services.Contracts; using Services.Tests.Helpers; +using SqlMembershipObtainer; +using SqlMembershipObtainer.SubOrchestrator; using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading.Tasks; -using Microsoft.Data.SqlClient; namespace Services.Tests { @@ -31,14 +29,13 @@ public class OrchestratorFunctionTests private List _profiles; private Mock _configuration; private Mock _loggingRepository; - private Mock _context; + private Mock _context; private Mock _executionContext; private SyncJob _syncJob; private OrchestratorRequest _mainRequest; private GraphProfileInformationResponse _graphProfileInformationResponse; private SyncStatus _senderResponseStatus = SyncStatus.InProgress; private string _senderResponseFilePath = "file-path"; - private DurableHttpResponse _membershipAggregatorResponse; private TelemetryClient _telemetryClient; private Mock _sqlMembershipObtainerService; private Mock _serviceBusQueueRepository; @@ -51,7 +48,7 @@ public void Setup() _configuration = new Mock(); _loggingRepository = new Mock(); _sqlMembershipObtainerService = new Mock(); - _context = new Mock(); + _context = new Mock(); _executionContext = new Mock(); _telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); _serviceBusQueueRepository = new Mock(); @@ -82,48 +79,49 @@ public void Setup() GraphProfileCount = _profiles.Count }; - _membershipAggregatorResponse = new DurableHttpResponse(HttpStatusCode.NoContent); - _context.Setup(x => x.GetInput()).Returns(() => _mainRequest); - _context.Setup(x => x.CallActivityAsync(nameof(LoggerFunction), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(nameof(LoggerFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallLoggerFunctionAsync(request as LoggerRequest); }); _context.Setup(x => x.CallSubOrchestratorAsync( nameof(OrganizationProcessorFunction), - It.IsAny() + It.IsAny(), + It.IsAny() )) .ReturnsAsync(() => _graphProfileInformationResponse); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(TelemetryTrackerFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(nameof(TelemetryTrackerFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var telemetryRequest = request as TelemetryTrackerRequest; await CallTelemetryTrackerFunctionAsync(telemetryRequest); }); - _context.Setup(x => x.CallActivityAsync<(SyncStatus Status, string FilePath)>( + _context.Setup(x => x.CallActivityAsync( nameof(GroupMembershipSenderFunction), - It.IsAny())) - .Callback(async (name, request) => + It.IsAny(), + It.IsAny())) + .Callback(async (name, request, options) => { await CallGroupMembershipSenderFunctionAsync(request as GroupMembershipSenderRequest); }) - .ReturnsAsync(() => (_senderResponseStatus, _senderResponseFilePath)); + .ReturnsAsync(() => new OrchestratorResponse + { Status = _senderResponseStatus, + FilePath = _senderResponseFilePath + }); - _context.Setup(x => x.CallHttpAsync(It.IsAny())).ReturnsAsync(() => _membershipAggregatorResponse); - - _context.Setup(x => x.CallActivityAsync(nameof(QueueMessageSenderFunction), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(nameof(QueueMessageSenderFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallQueueMessageSenderFunctionAsync(request as MembershipAggregatorHttpRequest); }); _schemaProvider = SchemaProviderFactory.CreateJsonSchemaProvider(); - _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); }) @@ -147,7 +145,7 @@ public async Task TestValidSqlMembershipQueryAsync() .ReturnsAsync(() => (_senderResponseStatus, _senderResponseFilePath)); var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); - await orchestratorFunction.RunOrchestratorAsync(_context.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.StartsWith($"Retrieved {_profilesCount} total profiles from SqlMembershipObtainer")), @@ -173,7 +171,7 @@ public async Task TestEmptySqlMembershipQueryAsync(string query) _syncJob.Query = query; var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); - await orchestratorFunction.RunOrchestratorAsync(_context.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains($"The job Id:{_syncJob.Id} Part#{_mainRequest.CurrentPart} does not have a valid query")), @@ -188,7 +186,7 @@ public async Task TestInvalidSqlMembershipQueryAsync() _syncJob.Query = "some-invalid-query"; var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); - await orchestratorFunction.RunOrchestratorAsync(_context.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains($"The job Id:{_syncJob.Id} Part#{_mainRequest.CurrentPart} does not have a valid query")), @@ -197,7 +195,7 @@ public async Task TestInvalidSqlMembershipQueryAsync() It.IsAny()), Times.Once()); _context.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), - It.Is(x => x.Status == SyncStatus.QueryNotValid)), Times.Once()); + It.Is(x => x.Status == SyncStatus.QueryNotValid), It.IsAny()), Times.Once()); } [TestMethod] @@ -205,15 +203,15 @@ public async Task TestInvalidSchemaAsync() { _syncJob.Query = "[{\"type\":\"SqlMembership\",\"source\":{\"manager\":{\"id\":[1, 2]},\"filter\":\"Attribute = 'Value'\"}}]"; - _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); }) .ReturnsAsync(() => false); var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); - await orchestratorFunction.RunOrchestratorAsync(_context.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains($"Query not valid")), @@ -222,7 +220,53 @@ public async Task TestInvalidSchemaAsync() It.IsAny()), Times.Once()); _context.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), - It.Is(x => x.Status == SyncStatus.SchemaError)), Times.Once()); + It.Is(x => x.Status == SyncStatus.SchemaError), It.IsAny()), Times.Once()); + } + + [TestMethod] + public async Task TestNoSchemasLoadedAsync() + { + var result = false; + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + result = await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => result); + + _schemaProvider.Schemas.Clear(); + + var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); + + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message.Contains($"No json schemas have been loaded.")), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once()); + } + + [TestMethod] + public async Task TestNoSqlSchemasLoadedAsync() + { + var result = false; + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => + { + result = await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => result); + + _schemaProvider.Schemas.Remove(Schema.SqlMembershipSchema); + + var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); + + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message.Contains($"No SqlMembership schema has been loaded.")), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once()); } [TestMethod] @@ -231,7 +275,7 @@ public async Task TestInvalidFilePathFailureAsync() _senderResponseFilePath = null; var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); - await orchestratorFunction.RunOrchestratorAsync(_context.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.StartsWith($"Retrieved {_profilesCount} total profiles from SqlMembershipObtainer")), @@ -246,7 +290,7 @@ public async Task TestInvalidFilePathFailureAsync() It.IsAny()), Times.Once()); _context.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), - It.Is(x => x.Status == SyncStatus.FilePathNotValid)), Times.Once()); + It.Is(x => x.Status == SyncStatus.FilePathNotValid), It.IsAny()), Times.Once()); } [TestMethod] @@ -262,12 +306,13 @@ public async Task TestFailJobOnFrameworkDataProviderErrorAsync() _context.Setup(x => x.CurrentUtcDateTime).Returns(originalStartDate); _context.Setup(x => x.CallSubOrchestratorAsync( nameof(OrganizationProcessorFunction), - It.IsAny() + It.IsAny(), + It.IsAny() )) .ThrowsAsync(new Exception("Internal .NET Framework Data Provider error 6")); var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); - await orchestratorFunction.RunOrchestratorAsync(_context.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.StartsWith($"Rescheduling job at")), @@ -276,7 +321,7 @@ public async Task TestFailJobOnFrameworkDataProviderErrorAsync() It.IsAny()), Times.Never()); _context.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), - It.Is(x => x.Status == SyncStatus.Error)), Times.Once()); + It.Is(x => x.Status == SyncStatus.Error), It.IsAny()), Times.Once()); } [TestMethod] @@ -285,12 +330,13 @@ public async Task TestFailJobOnSqlExceptionAsync() { _context.Setup(x => x.CallSubOrchestratorAsync( nameof(OrganizationProcessorFunction), - It.IsAny() + It.IsAny(), + It.IsAny() )) .ThrowsAsync(MakeSqlException()); var orchestratorFunction = new OrchestratorFunction(_configuration.Object, _loggingRepository.Object); - await orchestratorFunction.RunOrchestratorAsync(_context.Object, _executionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_context.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.StartsWith($"Caught SqlException")), @@ -299,7 +345,7 @@ public async Task TestFailJobOnSqlExceptionAsync() It.IsAny()), Times.Once()); _context.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), - It.Is(x => x.Status == SyncStatus.Error)), Times.Once()); + It.Is(x => x.Status == SyncStatus.Error), It.IsAny()), Times.Once()); } public static SqlException MakeSqlException() @@ -329,7 +375,7 @@ private async Task CallLoggerFunctionAsync(LoggerRequest request) await function.LogMessageAsync(request); } - private async Task<(SyncStatus Status, string FilePath)> CallGroupMembershipSenderFunctionAsync(GroupMembershipSenderRequest request) + private async Task CallGroupMembershipSenderFunctionAsync(GroupMembershipSenderRequest request) { var function = new GroupMembershipSenderFunction(_sqlMembershipObtainerService.Object, _loggingRepository.Object); return await function.SendGroupMembershipAsync(request); diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrganizationProcessorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrganizationProcessorFunctionTests.cs index a4eb99725..b8f297a81 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrganizationProcessorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Services.Tests/OrganizationProcessorFunctionTests.cs @@ -1,20 +1,19 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Entities; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; using Models; +using Moq; +using Repositories.Contracts; +using Services.Contracts; +using Services.Tests.Helpers; using SqlMembershipObtainer; using SqlMembershipObtainer.Entities; -using Services.Tests.Helpers; +using SqlMembershipObtainer.SubOrchestrator; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using SqlMembershipObtainer.SubOrchestrator; -using Services.Contracts; -using Repositories.Contracts; namespace Services.Tests { @@ -52,7 +51,7 @@ public void Setup() [TestMethod] public async Task ProcessQueryWithOrgLeadersTest() { - var context = new Mock(); + var context = new Mock(); var queryFunction = new OrganizationProcessorFunction(); var organization = new OrganizationCreator().GenerateOrganizationHierarchy(); @@ -84,19 +83,21 @@ public async Task ProcessQueryWithOrgLeadersTest() context.Setup(x => x.GetInput()).Returns(organizationProcessorRequest); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())); - context.Setup(x => x.CallActivityAsync(nameof(TableNameReaderFunction), It.IsAny())).ReturnsAsync("tbl112233445566"); + context.Setup(x => x.CallActivityAsync(nameof(TableNameReaderFunction), It.IsAny(), It.IsAny())).ReturnsAsync("tbl112233445566"); context.Setup(x => x.CallSubOrchestratorAsync( - It.Is(x => x == nameof(ManagerOrgReaderFunction)), - It.IsAny())) + nameof(ManagerOrgReaderFunction), + It.IsAny(), + It.IsAny())) .ReturnsAsync(() => managerOrgProcessorResponse); context.Setup(x => x.CallActivityAsync( - It.Is(x => x == nameof(ManagerOrgReaderFunction)), - It.IsAny())) - .Callback(async (name, requestObject) => + nameof(ManagerOrgReaderFunction), + It.IsAny(), + It.IsAny())) + .Callback(async (name, requestObject, options) => { var request = requestObject as ManagerOrgReaderRequest; managerOrgReaderResponse = await ManagerOrgReaderFunctionAsync(request); @@ -111,7 +112,7 @@ public async Task ProcessQueryWithOrgLeadersTest() [TestMethod] public async Task ProcessQueryWithNoOrgLeadersTest() { - var context = new Mock(); + var context = new Mock(); var queryFunction = new OrganizationProcessorFunction(); var organization = new OrganizationCreator().GenerateOrganizationHierarchy(); @@ -140,12 +141,12 @@ public async Task ProcessQueryWithNoOrgLeadersTest() context.Setup(x => x.GetInput()).Returns(organizationProcessorRequest); context.Setup(x => x.GetInput()).Returns(() => managerOrgReaderRequest); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())); - context.Setup(x => x.CallActivityAsync(nameof(TableNameReaderFunction), It.IsAny())).ReturnsAsync("tbl112233445566"); + context.Setup(x => x.CallActivityAsync(nameof(TableNameReaderFunction), It.IsAny(), It.IsAny())).ReturnsAsync("tbl112233445566"); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, requestObject) => + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, requestObject, options) => { var request = requestObject as ChildEntitiesFilterRequest; childEntitiesFilterResponse = await CallChildEntitiesFilterFunctionAsync(request); From 4e8773e29ccff2c66a55523e2150eb6a6048e0f6 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Fri, 6 Sep 2024 13:29:04 -0700 Subject: [PATCH 0283/1479] Updated code coverage for SqlMembershipObtainer --- vsts-cicd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsts-cicd.yml b/vsts-cicd.yml index d169cd097..773b8e61a 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -74,7 +74,7 @@ stages: coverageThreshold: 83 - function: name: 'SqlMembershipObtainer' - coverageThreshold: 50 + coverageThreshold: 40 - function: name: 'PlaceMembershipObtainer' coverageThreshold: 45 From efae3a399b1944cc9a31b54550e645733cc0c740 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Fri, 6 Sep 2024 15:43:48 -0700 Subject: [PATCH 0284/1479] Updated the GetDefaultAttributes endpoint to properly handle updates to column's hasMapping attribute. --- .../GetDefaultSqlMembershipSourceAttributesHandler.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributesHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributesHandler.cs index 6c351026d..6bbc14b03 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributesHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributesHandler.cs @@ -41,7 +41,7 @@ protected override async Task E if (storedAttributeSettings != null) { - storedAttributeSettings.RemoveAll(attribute => !sqlFilterAttributes.Any(t => t.Name == attribute.Name && t.Type == attribute.Type)); + storedAttributeSettings.RemoveAll(attribute => !sqlFilterAttributes.Any(t => t.Name == attribute.Name)); await _databaseSqlMembershipSourcesRepository.UpdateDefaultSourceAttributesAsync(storedAttributeSettings); } @@ -49,10 +49,12 @@ protected override async Task E var attributesToReturn = sqlFilterAttributes.Select(sqlAttribute => { var storedAttribute = storedAttributeSettings?.FirstOrDefault(attribute => - attribute.Name == sqlAttribute.Name && attribute.Type == sqlAttribute.Type + attribute.Name == sqlAttribute.Name ); - return storedAttribute ?? sqlAttribute; + sqlAttribute.CustomLabel = (storedAttribute != null) ? storedAttribute.CustomLabel : ""; + + return sqlAttribute; }).ToList(); From 3c5a6d55605da568730598bd9614d6cd984874a6 Mon Sep 17 00:00:00 2001 From: abgonz Date: Thu, 5 Sep 2024 16:35:48 -0700 Subject: [PATCH 0285/1479] Add retry policy to TableNameReaderFunction --- .../TableNameReader/TableNameReaderFunction.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TableNameReader/TableNameReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TableNameReader/TableNameReaderFunction.cs index a19db3c7b..92d8f9b47 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TableNameReader/TableNameReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Activity/TableNameReader/TableNameReaderFunction.cs @@ -6,6 +6,9 @@ using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Data.SqlClient; +using Polly.Retry; +using Polly; namespace SqlMembershipObtainer { @@ -25,11 +28,20 @@ public async Task GetSqlMembershipTableName([ActivityTrigger] SyncJob sy { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TableNameReaderFunction)} function started", RunId = syncJob.RunId }, VerbosityLevel.DEBUG); - var sqlMembershipObtainerTableName = await _sqlMembershipObtainerService.GetTableNameAsync(syncJob.RunId, syncJob.TargetOfficeGroupId); + string sqlMembershipObtainerTableName = null; + + await _retryPolicy.ExecuteAsync(async () => + { + sqlMembershipObtainerTableName = await _sqlMembershipObtainerService.GetTableNameAsync(syncJob.RunId, syncJob.TargetOfficeGroupId); + }); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TableNameReaderFunction)} function completed", RunId = syncJob.RunId }, VerbosityLevel.DEBUG); return sqlMembershipObtainerTableName; } + + private readonly AsyncRetryPolicy _retryPolicy = Policy + .Handle(ex => ex.Number == -2) // SQL timeout exception number + .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); } } From 547215025a56ec1a8174d8fd6a62c4da58baa19a Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Fri, 30 Aug 2024 10:51:48 -0700 Subject: [PATCH 0286/1479] Add isolated worker model for graph updater --- .../CacheUpdater/CacheUpdaterFunction.cs | 5 +- .../EmailSender/EmailSenderFunction.cs | 5 +- .../FileDownloader/FileDownloaderFunction.cs | 5 +- .../GroupNameReaderFunction.cs | 5 +- .../GroupOwnersReaderFunction.cs | 5 +- .../GroupUpdater/GroupUpdaterFunction.cs | 5 +- .../GroupValidator/GroupValidatorFunction.cs | 5 +- .../Activity/JobReader/JobReaderFunction.cs | 5 +- .../JobStatusUpdaterFunction.cs | 5 +- .../Activity/Logger/LoggerFunction.cs | 5 +- .../MessageReader/MessageReaderFunction.cs | 5 +- .../TelemetryTrackerFunction.cs | 5 +- .../GraphUpdater/Function/GraphUpdater.csproj | 10 +- .../Orchestrator/OrchestratorFunction.cs | 13 +- .../Hosts/GraphUpdater/Function/Program.cs | 97 +++++++++ .../QueueMessageOrchestratorFunction.cs | 9 +- .../Function/Starter/StarterFunction.cs | 13 +- .../Hosts/GraphUpdater/Function/Startup.cs | 91 -------- ...CacheUserUpdaterSubOrchestratorFunction.cs | 8 +- .../GroupUpdaterSubOrchestratorFunction.cs | 8 +- .../GraphUpdater/Function/local.settings.json | 7 +- .../Infrastructure/compute/template.bicep | 2 +- .../Services.Tests/OrchestratorTests.cs | 195 +++++++++--------- .../QueueMessageOrchestratorTests.cs | 31 +-- .../Services.Tests/StarterFunctionTests.cs | 19 +- .../SubOrchestratorFunctionTests.cs | 18 +- .../MockDurableTaskClient.cs | 5 +- .../Repositories.Mocks.csproj | 29 +-- 28 files changed, 312 insertions(+), 303 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/CacheUpdater/CacheUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/CacheUpdater/CacheUpdaterFunction.cs index 7255ede0d..583f6fb88 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/CacheUpdater/CacheUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/CacheUpdater/CacheUpdaterFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services.Contracts; using System; @@ -14,6 +12,7 @@ using Repositories.BlobStorage; using System.Collections.Generic; using System.Linq; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -28,7 +27,7 @@ public CacheUpdaterFunction(ILoggingRepository loggingRepository, IBlobStorageRe _blobStorageRepository = blobStorageRepository ?? throw new ArgumentNullException(nameof(blobStorageRepository)); } - [FunctionName(nameof(CacheUpdaterFunction))] + [Function(nameof(CacheUpdaterFunction))] public async Task UpdateCacheAsync ([ActivityTrigger] CacheUpdaterRequest request) { diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs index f2dfa44c6..e0e866c09 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -21,7 +20,7 @@ public EmailSenderFunction(ILoggingRepository loggingRepository, IGraphUpdaterSe _graphUpdaterService = graphUpdaterService ?? throw new ArgumentNullException(nameof(graphUpdaterService)); ; } - [FunctionName(nameof(EmailSenderFunction))] + [Function(nameof(EmailSenderFunction))] public async Task SendEmailAsync([ActivityTrigger] EmailSenderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(EmailSenderFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/FileDownloader/FileDownloaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/FileDownloader/FileDownloaderFunction.cs index 315f2e1c1..a3bbcc79d 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/FileDownloader/FileDownloaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/FileDownloader/FileDownloaderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.IO; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -22,7 +21,7 @@ public FileDownloaderFunction(ILoggingRepository loggingRepository, IBlobStorage _blobStorageRepository = blobStorageRepository ?? throw new ArgumentNullException(nameof(blobStorageRepository)); } - [FunctionName(nameof(FileDownloaderFunction))] + [Function(nameof(FileDownloaderFunction))] public async Task DownloadFileAsync([ActivityTrigger] FileDownloaderRequest request) { var blobResult = new BlobResult { BlobStatus = BlobStatus.NotFound }; diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs index b9120448c..c541f2bf3 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -21,7 +20,7 @@ public GroupNameReaderFunction(ILoggingRepository loggingRepository, IGraphUpdat _graphUpdaterService = graphUpdaterService ?? throw new ArgumentNullException(nameof(graphUpdaterService)); } - [FunctionName(nameof(GroupNameReaderFunction))] + [Function(nameof(GroupNameReaderFunction))] public async Task GetGroupNameAsync([ActivityTrigger] GroupNameReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupNameReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderFunction.cs index 7ea18f048..e7bb6c762 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupOwnersReader/GroupOwnersReaderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -22,7 +21,7 @@ public GroupOwnersReaderFunction(ILoggingRepository loggingRepository, IGraphUpd _graphUpdaterService = graphUpdaterService ?? throw new ArgumentNullException(nameof(graphUpdaterService)); } - [FunctionName(nameof(GroupOwnersReaderFunction))] + [Function(nameof(GroupOwnersReaderFunction))] public async Task> GetGroupOwnersAsync([ActivityTrigger] GroupOwnersReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupOwnersReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupUpdater/GroupUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupUpdater/GroupUpdaterFunction.cs index b9cb5ac2e..7501a356f 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupUpdater/GroupUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupUpdater/GroupUpdaterFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using GraphUpdater.Helpers; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; @@ -10,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -26,7 +25,7 @@ public GroupUpdaterFunction( _graphUpdaterService = graphUpdaterService ?? throw new ArgumentNullException(nameof(graphUpdaterService)); } - [FunctionName(nameof(GroupUpdaterFunction))] + [Function(nameof(GroupUpdaterFunction))] public async Task UpdateGroupAsync([ActivityTrigger] GroupUpdaterRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupUpdaterFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupValidator/GroupValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupValidator/GroupValidatorFunction.cs index 8aca7e256..58621d1d8 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupValidator/GroupValidatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/GroupValidator/GroupValidatorFunction.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using Services.Contracts; using System; using System.Threading.Tasks; using Models.Notifications; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -26,7 +25,7 @@ public GroupValidatorFunction(ILoggingRepository loggingRepository, IGraphUpdate _emailSenderAndRecipients = emailSenderAndRecipients ?? throw new ArgumentNullException(nameof(emailSenderAndRecipients)); } - [FunctionName(nameof(GroupValidatorFunction))] + [Function(nameof(GroupValidatorFunction))] public async Task ValidateGroupAsync([ActivityTrigger] GroupValidatorRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupValidatorFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/JobReader/JobReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/JobReader/JobReaderFunction.cs index a929f2f2f..4d8577172 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/JobReader/JobReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/JobReader/JobReaderFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -21,7 +20,7 @@ public JobReaderFunction(ILoggingRepository loggingRepository, IGraphUpdaterServ _graphUpdaterService = graphUpdaterService ?? throw new ArgumentNullException(nameof(graphUpdaterService)); } - [FunctionName(nameof(JobReaderFunction))] + [Function(nameof(JobReaderFunction))] public async Task GetSyncJobAsync([ActivityTrigger] JobReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs index c82361dc1..154e4c21f 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -23,7 +22,7 @@ public JobStatusUpdaterFunction( _graphUpdaterService = graphUpdaterService ?? throw new ArgumentNullException(nameof(graphUpdaterService)); } - [FunctionName(nameof(JobStatusUpdaterFunction))] + [Function(nameof(JobStatusUpdaterFunction))] public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobStatusUpdaterFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/Logger/LoggerFunction.cs index 9a2c00788..84bcacebb 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/Logger/LoggerFunction.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -18,7 +17,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.SyncJob?.RunId }, request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/MessageReader/MessageReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/MessageReader/MessageReaderFunction.cs index 03128265d..f2d8d7682 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/MessageReader/MessageReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/MessageReader/MessageReaderFunction.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Azure.Messaging.ServiceBus; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Newtonsoft.Json; using Repositories.Contracts; using System; using System.Text; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -23,7 +22,7 @@ public MessageReaderFunction(ILoggingRepository loggingRepository, ServiceBusRec _serviceBusReceiver = serviceBusReceiver ?? throw new ArgumentNullException(nameof(serviceBusReceiver)); } - [FunctionName(nameof(MessageReaderFunction))] + [Function(nameof(MessageReaderFunction))] public async Task GetSyncJobAsync([ActivityTrigger] object input) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(MessageReaderFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs index 1e2d8b8bd..1173729b5 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs @@ -2,12 +2,11 @@ // Licensed under the MIT license. using Models; using Microsoft.ApplicationInsights; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.GraphUpdater { @@ -22,7 +21,7 @@ public TelemetryTrackerFunction(ILoggingRepository loggingRepository, TelemetryC _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - [FunctionName(nameof(TelemetryTrackerFunction))] + [Function(nameof(TelemetryTrackerFunction))] public async Task TrackEventAsync([ActivityTrigger] TelemetryTrackerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TelemetryTrackerFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj index ffb56a312..29684fda0 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj @@ -2,13 +2,19 @@ net8.0 v4 + Exe + enabled - - + + + + + + diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Orchestrator/OrchestratorFunction.cs index 46f17306f..0cb77b172 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Orchestrator/OrchestratorFunction.cs @@ -3,8 +3,6 @@ using GraphUpdater.Entities; using GraphUpdater.Helpers; using Microsoft.ApplicationInsights; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Identity.Client; using Models; using Models.Notifications; @@ -22,6 +20,9 @@ using System.Reflection; using System.Threading.Tasks; using static System.Net.WebRequestMethods; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace Hosts.GraphUpdater { @@ -56,8 +57,8 @@ public OrchestratorFunction( _deltaCachingConfig = deltaCachingConfig ?? throw new ArgumentNullException(nameof(deltaCachingConfig)); } - [FunctionName(nameof(OrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context, ExecutionContext executionContext) + [Function(nameof(OrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { GroupMembership groupMembership = null; MembershipHttpRequest graphRequest = null; @@ -283,7 +284,7 @@ await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), } } - public async Task UpdateCachesAsync(IDurableOrchestrationContext context, + public async Task UpdateCachesAsync(TaskOrchestrationContext context, List sourceUsersNotFound, List destinationUsersNotFound, SyncJob syncJob, @@ -354,7 +355,7 @@ private void TrackUsersNotFoundEvent(Guid? runId, int usersNotFoundCount, Guid g _telemetryClient.TrackEvent("UsersNotFoundCount", usersNotFoundEvent); } - private void TrackSyncCompleteEvent(IDurableOrchestrationContext context, SyncJob syncJob, SyncCompleteCustomEvent syncCompleteEvent, string successStatus) + private void TrackSyncCompleteEvent(TaskOrchestrationContext context, SyncJob syncJob, SyncCompleteCustomEvent syncCompleteEvent, string successStatus) { var timeElapsedForJob = (context.CurrentUtcDateTime - syncJob.LastSuccessfulStartTime).TotalSeconds; _telemetryClient.TrackMetric(nameof(Metric.SyncJobTimeElapsedSeconds), timeElapsedForJob); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Program.cs new file mode 100644 index 000000000..ab9214d9a --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Program.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +using Azure.Identity; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using DIConcreteTypes; +using GraphUpdater.Entities; +using Hosts.FunctionBase; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Repositories.BlobStorage; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.GraphGroups; +using Repositories.ServiceBusQueue; +using Services; +using Services.Contracts; +using System; + +namespace Hosts.GraphUpdater +{ + + public class Program + { + public static void Main (string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = "GraphUpdater"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + services.AddOptions().Configure((settings, configuration) => + { + settings.DeltaCacheEnabled = CommonServices.GetBoolSettingBase(configuration, "GraphUpdater:IsDeltaCacheEnabled", false); + }); + services.AddSingleton(services => + { + return new DeltaCachingConfig(services.GetService>().Value.DeltaCacheEnabled); + }); + + services.AddGraphAPIClient() + + .AddScoped() + .AddScoped() + .AddSingleton((s) => + { + var configuration = s.GetService(); + var storageAccountName = configuration["membershipStorageAccountName"]; + var containerName = configuration["membershipContainerName"]; + + return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); + }) + .AddSingleton((s) => + { + var configuration = s.GetService(); + return new GraphUpdaterBatchSize { BatchSize = CommonServices.GetIntSettingBase(configuration, "GraphUpdater:UpdateBatchSize", 100) }; + }) + .AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + return new ServiceBusQueueRepository(sender); + }) + .AddSingleton(services => + { + var client = services.GetRequiredService(); + var serviceBusMembershipUpdatersTopic = CommonServices.GetValueOrThrowBase("serviceBusMembershipUpdatersTopic"); + var receiver = client.CreateReceiver(serviceBusMembershipUpdatersTopic, "GraphUpdater"); + return receiver; + }); + }) + .Build(); + host.Run(); + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/QueueMessageOrchestrator/QueueMessageOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/QueueMessageOrchestrator/QueueMessageOrchestratorFunction.cs index 994a114c1..f4b937bc2 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/QueueMessageOrchestrator/QueueMessageOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/QueueMessageOrchestrator/QueueMessageOrchestratorFunction.cs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace Hosts.GraphUpdater { @@ -18,8 +19,8 @@ public QueueMessageOrchestratorFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(QueueMessageOrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(QueueMessageOrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { try { diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Starter/StarterFunction.cs index 38ddaeba8..4d71f47ea 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Starter/StarterFunction.cs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Azure.Messaging.ServiceBus; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace Hosts.GraphUpdater { @@ -21,15 +22,15 @@ public StarterFunction(ILoggingRepository loggingRepository, ServiceBusReceiver _serviceBusReceiver = serviceBusReceiver ?? throw new ArgumentNullException(nameof(serviceBusReceiver)); } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task RunAsync( [TimerTrigger("%triggerSchedule%")] TimerInfo myTimer, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient client) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started" }, VerbosityLevel.DEBUG); var instanceId = nameof(QueueMessageOrchestratorFunction); - var orchestratorStatus = await starter.GetStatusAsync(instanceId); + var orchestratorStatus = await client.GetInstanceAsync(instanceId); var isRunning = orchestratorStatus != null && orchestratorStatus.RuntimeStatus != OrchestrationRuntimeStatus.Completed && orchestratorStatus.RuntimeStatus != OrchestrationRuntimeStatus.Terminated @@ -38,7 +39,7 @@ public async Task RunAsync( if (!isRunning) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Calling {instanceId}" }, VerbosityLevel.INFO); - await starter.StartNewAsync(instanceId, instanceId, (object)null); + await client.ScheduleNewOrchestrationInstanceAsync(instanceId, instanceId); } await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Startup.cs deleted file mode 100644 index 0eb399ef0..000000000 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/Startup.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Azure.Messaging.ServiceBus; -using Common.DependencyInjection; -using DIConcreteTypes; -using GraphUpdater.Entities; -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Repositories.BlobStorage; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphGroups; -using Repositories.ServiceBusQueue; -using Services; -using Services.Contracts; - -// see https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection -[assembly: FunctionsStartup(typeof(Hosts.GraphUpdater.Startup))] - -namespace Hosts.GraphUpdater -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(GraphUpdater); - protected override string DryRunSettingName => string.Empty; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - settings.DeltaCacheEnabled = GetBoolSetting(configuration, "GraphUpdater:IsDeltaCacheEnabled", false); - }); - builder.Services.AddSingleton(services => - { - return new DeltaCachingConfig(services.GetService>().Value.DeltaCacheEnabled); - }); - - builder.Services.AddGraphAPIClient() - - .AddScoped() - .AddScoped() - .AddSingleton((s) => - { - var configuration = s.GetService(); - var storageAccountName = configuration["membershipStorageAccountName"]; - var containerName = configuration["membershipContainerName"]; - - return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); - }) - .AddSingleton((s) => - { - var configuration = s.GetService(); - return new GraphUpdaterBatchSize { BatchSize = GetIntSetting(configuration, "GraphUpdater:UpdateBatchSize", 100) }; - }) - .AddSingleton(services => - { - var configuration = services.GetRequiredService(); - var notificationsQueue = configuration["serviceBusNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(notificationsQueue); - return new ServiceBusQueueRepository(sender); - }) - .AddSingleton(services => - { - var client = services.GetRequiredService(); - var serviceBusMembershipUpdatersTopic = GetValueOrThrow("serviceBusMembershipUpdatersTopic"); - var receiver = client.CreateReceiver(serviceBusMembershipUpdatersTopic, "GraphUpdater"); - return receiver; - }); - } - - private int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) - { - var isParsed = int.TryParse(configuration[settingName], out var maximumNumberOfThresholdRecipients); - return isParsed ? maximumNumberOfThresholdRecipients : defaultValue; - } - - private bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) - { - var checkParse = bool.TryParse(configuration[settingName], out bool value); - if (checkParse) - return value; - return defaultValue; - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/SubOrchestrator/CacheUserUpdaterSubOrchrestrator/CacheUserUpdaterSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/SubOrchestrator/CacheUserUpdaterSubOrchrestrator/CacheUserUpdaterSubOrchestratorFunction.cs index d8250e1da..e70804189 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/SubOrchestrator/CacheUserUpdaterSubOrchrestrator/CacheUserUpdaterSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/SubOrchestrator/CacheUserUpdaterSubOrchrestrator/CacheUserUpdaterSubOrchestratorFunction.cs @@ -4,8 +4,6 @@ using GraphUpdater.Entities; using Hosts.GraphUpdater; using Microsoft.ApplicationInsights; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -16,6 +14,8 @@ using System.Linq; using System.Threading.Tasks; using Models.Helpers; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.GraphUpdater { @@ -30,8 +30,8 @@ public CacheUserUpdaterSubOrchestratorFunction(ILoggingRepository loggingReposit _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - [FunctionName(nameof(CacheUserUpdaterSubOrchestratorFunction))] - public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(CacheUserUpdaterSubOrchestratorFunction))] + public async Task RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var request = context.GetInput(); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/SubOrchestrator/GroupUpdaterSubOrchrestrator/GroupUpdaterSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/SubOrchestrator/GroupUpdaterSubOrchrestrator/GroupUpdaterSubOrchestratorFunction.cs index 1b392cec4..2e9da4c5a 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/SubOrchestrator/GroupUpdaterSubOrchrestrator/GroupUpdaterSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/SubOrchestrator/GroupUpdaterSubOrchrestrator/GroupUpdaterSubOrchestratorFunction.cs @@ -1,7 +1,5 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using System.Threading.Tasks; using System.Linq; using System.Collections.Generic; @@ -11,6 +9,8 @@ using Models; using Repositories.Contracts; using Services.Entities; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.GraphUpdater { @@ -25,8 +25,8 @@ public GroupUpdaterSubOrchestratorFunction(TelemetryClient telemetryClient, Grap _batchSize = batchSize.BatchSize; } - [FunctionName(nameof(GroupUpdaterSubOrchestratorFunction))] - public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(GroupUpdaterSubOrchestratorFunction))] + public async Task RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var skip = 0; var request = context.GetInput(); diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json index 6221e6c98..95d4df1ce 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json @@ -19,9 +19,10 @@ "membershipStorageAccountName": "", "membershipContainerName": "", "actionableEmailProviderId": "", - "gmmServiceBus__fullyQualifiedNamespace": ".servicebus.windows.net", + "gmmServiceBus__fullyQualifiedNamespace": "\u003Cservice_bus_namespace\u003E.servicebus.windows.net", "serviceBusMembershipUpdatersTopic": "", "serviceBusNotificationsQueue": "", - "triggerSchedule": "0,30 * * * * *" + "triggerSchedule": "0,30 * * * * *", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" } -} +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep index 727254922..9056d4b38 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep @@ -98,7 +98,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs index ae98b4344..d36ed36ad 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs @@ -4,7 +4,8 @@ using Hosts.GraphUpdater; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; using Microsoft.Graph.Models; using Microsoft.Identity.Client; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -100,26 +101,26 @@ public async Task TestMsalTransientExceptionAsync() blobStorageRepository.Files.Add(input.FilePath, JsonConvert.SerializeObject(groupMembership)); - var context = new Mock(); + var context = new Mock(); var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(syncJob); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(),null)).ReturnsAsync(syncJob); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .ReturnsAsync(await DownloadFileAsync(fileDownloaderRequest, mockLoggingRepo, blobStorageRepository)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .ThrowsAsync(new MsalClientException("MULTIPLE_MATCHING_TOKENS_DETECTED", "MULTIPLE_MATCHING_TOKENS_DETECTED")); JobStatusUpdaterRequest updateJobRequest = null; - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback((name, request) => + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((name, request, options) => { updateJobRequest = request as JobStatusUpdaterRequest; }); var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); - await Assert.ThrowsExceptionAsync(async () => await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object)); + await Assert.ThrowsExceptionAsync(async () => await orchestrator.RunOrchestratorAsync(context.Object)); Assert.IsFalse(mockLoggingRepo.MessagesLogged.Any(x => x.Message == nameof(OrchestratorFunction) + " function completed")); Assert.IsTrue(mockLoggingRepo.MessagesLogged.Any(x => x.Message.Contains("Caught MsalClientException, marking sync job status as transient error."))); @@ -203,38 +204,38 @@ public async Task RunOrchestratorValidSyncTest() mockGraphUpdaterService.Groups.Add(groupMembership.Destination.ObjectId, new Group { Id = groupMembership.Destination.ObjectId.ToString() }); blobStorageRepository.Files.Add(input.FilePath, JsonConvert.SerializeObject(groupMembership)); - var context = new Mock(); + var context = new Mock(); var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); - context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(TelemetryTrackerFunction)), It.IsAny())) - .Callback(async (name, request) => + context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var telemetryRequest = request as TelemetryTrackerRequest; await CallTelemetryTrackerFunctionAsync(telemetryRequest, mockLoggingRepo); }); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(),null)) .Returns(async () => await RunJobReaderFunctionAsync(mockLoggingRepo, mockGraphUpdaterService, jobReaderRequest)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .ReturnsAsync(await DownloadFileAsync(fileDownloaderRequest, mockLoggingRepo, blobStorageRepository)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .Returns(async () => await CheckIfGroupExistsAsync(groupMembership, mockLoggingRepo, mockGraphUpdaterService, mailSenders)); - context.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny(), null)) .Returns(() => Task.FromResult(new GroupUpdaterSubOrchestratorResponse() { SuccessCount = 1, UsersNotFound = new List(), UsersAlreadyExist = new List() })); - context.Setup(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny())) - .Callback(async (name, request) => + context.Setup(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny(), null)) + .Callback(async (name, request, options) => { await CallJobStatusUpdaterFunctionAsync(mockLoggingRepo, mockGraphUpdaterService, request as JobStatusUpdaterRequest); }); var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); - var response = await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object); + var response = await orchestrator.RunOrchestratorAsync(context.Object); Assert.IsTrue(response == OrchestrationRuntimeStatus.Completed); Assert.IsTrue(mockLoggingRepo.MessagesLogged.Any(x => x.Message == nameof(OrchestratorFunction) + " function completed")); - context.Verify(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + context.Verify(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny(), null), Times.Exactly(2)); var logProperties = mockLoggingRepo.SyncJobPropertiesHistory[syncJob.RunId.Value].Properties; @@ -322,31 +323,31 @@ public async Task RunOrchestratorInitialSyncTest() Mock graphUpdaterService = new Mock(); graphUpdaterService.Setup(x => x.GetGroupOwnersAsync(It.IsAny(), It.IsAny())).ReturnsAsync(owners); - var context = new Mock(); + var context = new Mock(); var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(syncJob); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(JsonConvert.SerializeObject(groupMembership)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)).ReturnsAsync(syncJob); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)).ReturnsAsync(JsonConvert.SerializeObject(groupMembership)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) + .Callback(async (name, request, options) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .Returns(async () => await CheckIfGroupExistsAsync(groupMembership, mockLoggingRepo, mockGraphUpdaterService, mailSenders)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .Returns(async () => await CallGroupNameReaderFunctionAsync(mockLoggingRepo, mockGraphUpdaterService, groupNameReaderRequest)); - context.Setup(x => x.CallActivityAsync>(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync>(It.IsAny(), It.IsAny(), null)) .Returns(async () => await CallGroupOwnersReaderFunctionAsync(mockLoggingRepo, graphUpdaterService.Object, groupOwnersReaderRequest)); - context.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny(), null)) .Returns(() => Task.FromResult(new GroupUpdaterSubOrchestratorResponse() { SuccessCount = 1, UsersNotFound = new List(), UsersAlreadyExist = new List() })); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var emailSenderFunction = new EmailSenderFunction(mockLoggingRepo, mockGraphUpdaterService); await emailSenderFunction.SendEmailAsync((EmailSenderRequest)request); }); var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); - var response = await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object); + var response = await orchestrator.RunOrchestratorAsync(context.Object); Assert.IsTrue(response == OrchestrationRuntimeStatus.Completed); Assert.IsTrue(mockLoggingRepo.MessagesLogged.Any(x => x.Message == nameof(OrchestratorFunction) + " function completed")); @@ -362,7 +363,7 @@ public async Task RunOrchestratorInitialSyncTest() msg.ApplicationProperties["MessageType"].ToString() == NotificationMessageType.SyncCompletedNotification.ToString())), Times.Exactly(1)); - context.Verify(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + context.Verify(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny(), null), Times.Exactly(2)); } [TestMethod] @@ -427,29 +428,29 @@ public async Task TestHttpTransientExceptionAsync() blobStorageRepository.Files.Add(input.FilePath, JsonConvert.SerializeObject(groupMembership)); - var context = new Mock(); + var context = new Mock(); var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(syncJob); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)).ReturnsAsync(syncJob); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .ReturnsAsync(await DownloadFileAsync(fileDownloaderRequest, mockLoggingRepo, blobStorageRepository)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .ReturnsAsync(true); JobStatusUpdaterRequest updateJobRequest = null; - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback((name, request) => + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((name, request, options) => { updateJobRequest = request as JobStatusUpdaterRequest; }); - context.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny(), null)) .Throws(); var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); - await Assert.ThrowsExceptionAsync(async () => await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object)); + await Assert.ThrowsExceptionAsync(async () => await orchestrator.RunOrchestratorAsync(context.Object)); Assert.IsFalse(mockLoggingRepo.MessagesLogged.Any(x => x.Message == nameof(OrchestratorFunction) + " function completed")); Assert.IsTrue(mockLoggingRepo.MessagesLogged.Any(x => x.Message.Contains("Caught HttpRequestException, marking sync job status as transient error."))); @@ -524,22 +525,22 @@ public async Task RunOrchestratorExceptionTest() blobStorageRepository.Files.Add(input.FilePath, JsonConvert.SerializeObject(groupMembership)); - var context = new Mock(); + var context = new Mock(); var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(syncJob); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)).ReturnsAsync(syncJob); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); JobStatusUpdaterRequest updateJobRequest = null; - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback((name, request) => + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((name, request, options) => { updateJobRequest = request as JobStatusUpdaterRequest; }); var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); - await Assert.ThrowsExceptionAsync(async () => await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object)); + await Assert.ThrowsExceptionAsync(async () => await orchestrator.RunOrchestratorAsync(context.Object)); Assert.IsFalse(mockLoggingRepo.MessagesLogged.Any(x => x.Message == nameof(OrchestratorFunction) + " function completed")); Assert.IsTrue(mockLoggingRepo.MessagesLogged.Any(x => x.Message.Contains("Caught unexpected exception, marking sync job as errored."))); @@ -594,15 +595,15 @@ public async Task RunSyncJobNotFoundTest() blobStorageRepository.Files.Add(input.FilePath, JsonConvert.SerializeObject(groupMembership)); - var context = new Mock(); + var context = new Mock(); var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(syncJob); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)).ReturnsAsync(syncJob); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); - await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object); + await orchestrator.RunOrchestratorAsync(context.Object); Assert.IsFalse(mockLoggingRepo.MessagesLogged.Any(x => x.Message == nameof(OrchestratorFunction) + " function completed")); Assert.IsTrue(mockLoggingRepo.MessagesLogged.Any(x => x.Message.Contains("Caught unexpected exception, marking sync job as errored."))); @@ -671,22 +672,22 @@ public async Task RunOrchestratorFileNotFoundExceptionTest() blobStorageRepository.Files.Add(input.FilePath, JsonConvert.SerializeObject(groupMembership)); - var context = new Mock(); + var context = new Mock(); var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(syncJob); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)).ReturnsAsync(syncJob); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .Returns(async () => await DownloadFileAsync(fileDownloaderRequest, mockLoggingRepo, blobStorageRepository)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .Returns(async () => await CheckIfGroupExistsAsync(groupMembership, mockLoggingRepo, mockGraphUpdaterService, mailSenders)); mockGraphUpdaterService.Groups.Add(groupMembership.Destination.ObjectId, new Group { Id = groupMembership.Destination.ObjectId.ToString() }); mockSyncJobRepo.Jobs.Add(syncJob); var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); - await Assert.ThrowsExceptionAsync(async () => await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object)); + await Assert.ThrowsExceptionAsync(async () => await orchestrator.RunOrchestratorAsync(context.Object)); } [TestMethod] @@ -742,25 +743,25 @@ public async Task RunOrchestratorMissingGroupTest() SyncJob = syncJob }; - var context = new Mock(); + var context = new Mock(); var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(syncJob); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(JsonConvert.SerializeObject(groupMembership)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)).ReturnsAsync(syncJob); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)).ReturnsAsync(JsonConvert.SerializeObject(groupMembership)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .Returns(async () => await CheckIfGroupExistsAsync(groupMembership, mockLoggingRepo, mockGraphUpdaterService, mailSenders)); JobStatusUpdaterRequest updateJobRequest = null; - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback((name, request) => + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((name, request, options) => { updateJobRequest = request as JobStatusUpdaterRequest; }); var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); - var response = await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object); + var response = await orchestrator.RunOrchestratorAsync(context.Object); Assert.AreEqual(SyncStatus.DestinationGroupNotFound, updateJobRequest.Status); Assert.IsTrue(response == OrchestrationRuntimeStatus.Completed); @@ -854,30 +855,30 @@ public async Task RunOrchestratorGuestUserErrorTest() graphUpdaterService.Setup(x => x.GetGroupOwnersAsync(It.IsAny(), It.IsAny())).ReturnsAsync(owners); graphUpdaterService.Setup(x => x.GetSyncJobAsync(It.IsAny())).ReturnsAsync(new SyncJob()); - var context = new Mock(); + var context = new Mock(); var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(syncJob); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(JsonConvert.SerializeObject(groupMembership)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)).ReturnsAsync(syncJob); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)).ReturnsAsync(JsonConvert.SerializeObject(groupMembership)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .Returns(async () => await CheckIfGroupExistsAsync(groupMembership, mockLoggingRepo, mockGraphUpdaterService, mailSenders)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .Returns(async () => await CallGroupNameReaderFunctionAsync(mockLoggingRepo, mockGraphUpdaterService, groupNameReaderRequest)); - context.Setup(x => x.CallActivityAsync>(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync>(It.IsAny(), It.IsAny(), null)) .Returns(async () => await CallGroupOwnersReaderFunctionAsync(mockLoggingRepo, graphUpdaterService.Object, groupOwnersReaderRequest)); - context.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny(), null)) .Returns(() => Task.FromResult(new GroupUpdaterSubOrchestratorResponse() { Status = Entities.GraphUpdaterStatus.GuestError, SuccessCount = 1, UsersNotFound = new List(), UsersAlreadyExist = new List() })); - context.Setup(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny())) - .Callback(async (name, request) => + context.Setup(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallJobStatusUpdaterFunctionAsync(mockLoggingRepo, graphUpdaterService.Object, request as JobStatusUpdaterRequest); }); var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); - var response = await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object); + var response = await orchestrator.RunOrchestratorAsync(context.Object); Assert.IsTrue(response == OrchestrationRuntimeStatus.Completed); Assert.IsTrue(mockLoggingRepo.MessagesLogged.Any(x => x.Message == nameof(OrchestratorFunction) + " function completed")); @@ -889,7 +890,7 @@ public async Task RunOrchestratorGuestUserErrorTest() Assert.AreEqual(logProperties["Id"], syncJob.Id.ToString()); graphUpdaterService.Verify(x => x.UpdateSyncJobStatusAsync(It.IsAny(), SyncStatus.GuestUsersCannotBeAddedToUnifiedGroup, false, It.IsAny())); - context.Verify(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + context.Verify(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny(), null), Times.Exactly(2)); } [TestMethod] @@ -978,16 +979,16 @@ public async Task RunCacheUserUpdaterSubOrchestratorFunctionTest() mockGraphUpdaterService.Groups.Add(groupMembership.Destination.ObjectId, new Group { Id = groupMembership.Destination.ObjectId.ToString() }); blobStorageRepository.Files.Add(input.FilePath, JsonConvert.SerializeObject(groupMembership)); - var context = new Mock(); + var context = new Mock(); var executionContext = new Mock(); context.Setup(x => x.GetInput()).Returns(input); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .Returns(async () => await RunJobReaderFunctionAsync(mockLoggingRepo, mockGraphUpdaterService, jobReaderRequest)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .ReturnsAsync(await DownloadFileAsync(fileDownloaderRequest, mockLoggingRepo, blobStorageRepository)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => await CallLogMessageFunctionAsync((LoggerRequest)request, mockLoggingRepo)); + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) .Returns(async () => await CheckIfGroupExistsAsync(groupMembership, mockLoggingRepo, mockGraphUpdaterService, mailSenders)); var usersAlreadyExist = new List(); @@ -996,8 +997,8 @@ public async Task RunCacheUserUpdaterSubOrchestratorFunctionTest() { new AzureADUser { ObjectId = users[0].ObjectId, MembershipAction = MembershipAction.Remove } }; - context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x == nameof(GroupUpdaterSubOrchestratorFunction)), - It.IsAny())).ReturnsAsync(() => new GroupUpdaterSubOrchestratorResponse + context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(GroupUpdaterSubOrchestratorFunction)), + It.IsAny(), null)).ReturnsAsync(() => new GroupUpdaterSubOrchestratorResponse { Type = RequestType.Add, SuccessCount = 1, @@ -1006,9 +1007,9 @@ public async Task RunCacheUserUpdaterSubOrchestratorFunctionTest() }); var orchestrator = new OrchestratorFunction(mockTelemetryClient, mockGraphUpdaterService, mailSenders, _gmmResources, mockLoggingRepo, mockDeltaCachingConfig); - await orchestrator.RunOrchestratorAsync(context.Object, executionContext.Object); + await orchestrator.RunOrchestratorAsync(context.Object); - context.Verify(x => x.CallSubOrchestratorAsync(nameof(CacheUserUpdaterSubOrchestratorFunction), It.IsAny()), Times.Exactly(3)); + context.Verify(x => x.CallSubOrchestratorAsync(nameof(CacheUserUpdaterSubOrchestratorFunction), It.IsAny(), null), Times.Exactly(3)); } private async Task RunJobReaderFunctionAsync(MockLoggingRepository loggingRepository, MockGraphUpdaterService graphUpdaterService, JobReaderRequest request) diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/QueueMessageOrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/QueueMessageOrchestratorTests.cs index 66ea709f8..2c40d33d3 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/QueueMessageOrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/QueueMessageOrchestratorTests.cs @@ -2,7 +2,8 @@ // Licensed under the MIT license. using Azure.Messaging.ServiceBus; using Hosts.GraphUpdater; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Moq; @@ -21,7 +22,7 @@ public class QueueMessageOrchestratorTests { private MembershipHttpRequest _request; private MockLoggingRepository _loggerMock; - private Mock _context; + private Mock _context; private Mock _serviceBusReceiverMock; [TestInitialize] @@ -39,11 +40,11 @@ public void SetupTest() }; _loggerMock = new MockLoggingRepository(); - _context = new Mock(); + _context = new Mock(); _serviceBusReceiverMock = new Mock(); MembershipHttpRequest response = null; - _context.Setup(x => x.CallActivityAsync(nameof(MessageReaderFunction), (object)null)) + _context.Setup(x => x.CallActivityAsync(nameof(MessageReaderFunction),null)) .Callback(async () => response = await CallMessageReaderFunctionAsync()) .ReturnsAsync(() => response); @@ -64,14 +65,14 @@ public async Task RunOrchestratorWithMessagesInQueueAsync() await orchestrator.RunOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == "There are no more messages to process at this time.")), Times.Never()); + It.Is(r => r.Message == "There are no more messages to process at this time."),null), Times.Never()); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == $"Processing message for group {_request.SyncJob.TargetOfficeGroupId}")), Times.Once()); + It.Is(r => r.Message == $"Processing message for group {_request.SyncJob.TargetOfficeGroupId}"), null), Times.Once()); - _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny()), Times.Once()); + _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(),null), Times.Once()); - _context.Verify(x => x.ContinueAsNew((object)null, false), Times.Once()); + _context.Verify(x => x.ContinueAsNew((object)null,true), Times.Once()); } [TestMethod] @@ -83,30 +84,30 @@ public async Task RunOrchestratorWithNoMessagesInQueueAsync() await orchestrator.RunOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == "There are no more messages to process at this time.")), Times.Once()); + It.Is(r => r.Message == "There are no more messages to process at this time."),null), Times.Once()); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message.StartsWith("Processing message for group"))), Times.Never()); + It.Is(r => r.Message.StartsWith("Processing message for group")),null), Times.Never()); - _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny()), Times.Never()); + _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(), null), Times.Never()); } [TestMethod] public async Task MainOrchestratorFailsAsync() { - _context.Setup(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny())) + _context.Setup(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(), null)) .Throws(new Exception("Main orchestrator failed.")); var orchestrator = new QueueMessageOrchestratorFunction(_loggerMock); await orchestrator.RunOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == "There are no more messages to process at this time.")), Times.Never()); + It.Is(r => r.Message == "There are no more messages to process at this time."), null), Times.Never()); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == $"Processing message for group {_request.SyncJob.TargetOfficeGroupId}")), Times.Once()); + It.Is(r => r.Message == $"Processing message for group {_request.SyncJob.TargetOfficeGroupId}"),null), Times.Once()); - _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny()), Times.Once()); + _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(),null), Times.Once()); } private async Task CallMessageReaderFunctionAsync() diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/StarterFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/StarterFunctionTests.cs index 559874b27..b1e72ead0 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/StarterFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/StarterFunctionTests.cs @@ -2,14 +2,15 @@ // Licensed under the MIT license. using Azure.Messaging.ServiceBus; using Hosts.GraphUpdater; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Moq; using Repositories.Mocks; using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Services.Tests @@ -19,7 +20,7 @@ public class StarterFunctionTests { private string _instanceId; private MockLoggingRepository _loggerMock; - private Mock _durableClientMock; + private Mock _durableOrchestrationClient; private SyncJob _syncJob; private Mock _serviceBusReceiverMock; @@ -27,7 +28,7 @@ public class StarterFunctionTests public void SetupTest() { _instanceId = "1234567890"; - _durableClientMock = new Mock(); + _durableOrchestrationClient = new Mock(); _loggerMock = new MockLoggingRepository(); _serviceBusReceiverMock = new Mock(); _syncJob = new SyncJob @@ -46,18 +47,18 @@ public void SetupTest() [TestMethod] public async Task ProcessValidRequestTest() { - _durableClientMock - .Setup(x => x.StartNewAsync(It.IsAny(), It.IsAny(), (object)null)) + _durableOrchestrationClient + .Setup(x => x.ScheduleNewOrchestrationInstanceAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(_instanceId); var instanceId = nameof(QueueMessageOrchestratorFunction); var starterFunction = new StarterFunction(_loggerMock, _serviceBusReceiverMock.Object); - var timer = new TimerInfo(null, null); + var timer = new TimerInfo(); - await starterFunction.RunAsync(timer, _durableClientMock.Object); + await starterFunction.RunAsync(timer, _durableOrchestrationClient.Object); Assert.IsNotNull(_loggerMock.MessagesLogged.Single(x => x.Message.Contains("function started"))); - _durableClientMock.Verify(x => x.StartNewAsync(instanceId, instanceId, (object)null), Times.Once()); + _durableOrchestrationClient.Verify(x => x.ScheduleNewOrchestrationInstanceAsync(It.IsAny(), instanceId, null,It.IsAny()), Times.Once()); Assert.IsNotNull(_loggerMock.MessagesLogged.Single(x => x.Message == $"Calling {instanceId}")); Assert.IsNotNull(_loggerMock.MessagesLogged.Single(x => x.Message.Contains("function complete"))); } diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/SubOrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/SubOrchestratorFunctionTests.cs index 557e1c2fe..199de77a7 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/SubOrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/SubOrchestratorFunctionTests.cs @@ -5,7 +5,6 @@ using Hosts.GraphUpdater; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -19,6 +18,7 @@ using System.Threading.Tasks; using System.Text.Json.Serialization; using Newtonsoft.Json; +using Microsoft.DurableTask; namespace Services.Tests { @@ -29,7 +29,7 @@ public class SubOrchestratorFunctionTests private Mock _loggingRepository; private Mock _graphGroupRepository; private Mock _blobStorageRepository; - private Mock _durableOrchestrationContext; + private Mock _durableOrchestrationContext; private int _userCount; private BlobResult _blobResult; @@ -43,7 +43,7 @@ public void Setup() _loggingRepository = new Mock(); _graphGroupRepository = new Mock(); _blobStorageRepository = new Mock(); - _durableOrchestrationContext = new Mock(); + _durableOrchestrationContext = new Mock(); _userCount = 10; @@ -101,21 +101,21 @@ public void Setup() public async Task DownloadCacheFileAsync() { _cacheUrl = "http://cache-url"; - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(),It.IsAny())) + .Callback(async (name, request,options) => { _cacheUrl = await CallFileDownloaderFunctionAsync(request as FileDownloaderRequest); }) .ReturnsAsync(() => _cacheUrl); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(CacheUpdaterFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(CacheUpdaterFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallCacheUpdaterFunctionAsync(request as CacheUpdaterRequest); }); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(LoggerFunction)), It.IsAny())) - .Callback(async (name, request) => + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(LoggerFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallLoggerFunctionAsync(request as LoggerRequest); }); diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/MockDurableTaskClient.cs b/Service/GroupMembershipManagement/Repositories.Mocks/MockDurableTaskClient.cs index 385d56434..a53b891e1 100644 --- a/Service/GroupMembershipManagement/Repositories.Mocks/MockDurableTaskClient.cs +++ b/Service/GroupMembershipManagement/Repositories.Mocks/MockDurableTaskClient.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Azure; using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using System; @@ -60,7 +61,7 @@ public override Task GetInstancesAsync(string instanceId, return Task.FromResult(new OrchestrationMetadata(Guid.NewGuid().ToString(), instanceId)); } - public override AsyncPageable GetAllInstancesAsync(OrchestrationQuery filter = null) + public override Microsoft.DurableTask.AsyncPageable GetAllInstancesAsync(OrchestrationQuery filter = null) { return new MockOrchestrationMetadataAsyncPageable(); } @@ -81,7 +82,7 @@ public override ValueTask DisposeAsync() } } - internal class MockOrchestrationMetadataAsyncPageable : AsyncPageable + internal class MockOrchestrationMetadataAsyncPageable : Microsoft.DurableTask.AsyncPageable { public override IAsyncEnumerable> AsPages(string continuationToken = null, int? pageSizeHint = null) { diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj b/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj index acbc319b2..a6bd9346f 100644 --- a/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj +++ b/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj @@ -1,21 +1,22 @@ - - net8.0 + + net8.0 - false - + false + - - - - - + + + + + - - - - - + + + + + + From da2acb026562662daa8c6a219159549bed845ae5 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 5 Sep 2024 15:47:58 -0700 Subject: [PATCH 0287/1479] Update host config --- .../Hosts/GraphUpdater/Function/GraphUpdater.csproj | 6 ++++-- .../Hosts/GraphUpdater/Function/GraphUpdater.sln | 6 ++++++ .../Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj index 29684fda0..cff6b39c5 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.csproj @@ -1,4 +1,4 @@ - + net8.0 v4 @@ -7,7 +7,6 @@ - @@ -38,4 +37,7 @@ Never + + + diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.sln b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.sln index 7352bc226..013e2586a 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.sln +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/GraphUpdater.sln @@ -50,6 +50,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.ServiceBusQueu EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqlMembershipObtainer.Entities", "..\..\SqlMembershipObtainer.Common\SqlMembershipObtainer.Entities\SqlMembershipObtainer.Entities.csproj", "{C6C0138E-6961-4C3C-84A4-C684C9D98DFD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.RetryPolicyProvider", "..\..\..\Repositories.RetryPolicyProvider\Repositories.RetryPolicyProvider.csproj", "{8149985F-0230-40CA-A00E-0CBACE49F81E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -152,6 +154,10 @@ Global {C6C0138E-6961-4C3C-84A4-C684C9D98DFD}.Debug|Any CPU.Build.0 = Debug|Any CPU {C6C0138E-6961-4C3C-84A4-C684C9D98DFD}.Release|Any CPU.ActiveCfg = Release|Any CPU {C6C0138E-6961-4C3C-84A4-C684C9D98DFD}.Release|Any CPU.Build.0 = Release|Any CPU + {8149985F-0230-40CA-A00E-0CBACE49F81E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8149985F-0230-40CA-A00E-0CBACE49F81E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8149985F-0230-40CA-A00E-0CBACE49F81E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8149985F-0230-40CA-A00E-0CBACE49F81E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs index d36ed36ad..fe9acc904 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Services.Tests/OrchestratorTests.cs @@ -25,7 +25,7 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using ExecutionContext = Microsoft.Azure.WebJobs.ExecutionContext; +using ExecutionContext = Microsoft.DurableTask.TaskOrchestrationContext; namespace Services.Tests { From 72f06472f2072ea0d1f0c75280a08bc316e9cb9c Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 5 Sep 2024 16:26:13 -0700 Subject: [PATCH 0288/1479] Update local setting --- .../Hosts/GraphUpdater/Function/local.settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json index 95d4df1ce..dbc4e867d 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Function/local.settings.json @@ -19,7 +19,7 @@ "membershipStorageAccountName": "", "membershipContainerName": "", "actionableEmailProviderId": "", - "gmmServiceBus__fullyQualifiedNamespace": "\u003Cservice_bus_namespace\u003E.servicebus.windows.net", + "gmmServiceBus__fullyQualifiedNamespace": ".servicebus.windows.net", "serviceBusMembershipUpdatersTopic": "", "serviceBusNotificationsQueue": "", "triggerSchedule": "0,30 * * * * *", From 5ac289d9316bbbfa43202a9e95a649af0e8bf784 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Fri, 6 Sep 2024 10:11:24 -0700 Subject: [PATCH 0289/1479] Change the test coverage --- yaml/build-functionapps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yaml/build-functionapps.yml b/yaml/build-functionapps.yml index bbd479058..ac42c721c 100644 --- a/yaml/build-functionapps.yml +++ b/yaml/build-functionapps.yml @@ -65,9 +65,9 @@ stages: /p:threshold=${{ app.function.coverageThreshold }} /p:thresholdType=line /p:thresholdStat=total - /p:CoverletOutput=$(Build.SourcesDirectory)\TestResults\Coverage\${{ app.function.name }} + /p:CoverletOutput=$(Build.SourcesDirectory)\TestResults\Coverage\${{ app.function.name }}\ /p:Exclude=[Repositories.*]*%2c[Entities]*%2c[*.Entities]*%2c[Models]*%2c[*.Models]*%2c[*.Tests]*%2c[*.Mocks]*%2c[Common.DependencyInjection]*%2c[Hosts.FunctionBase]*%2c[DIConcreteTypes]*%2c[*]*.*.Startup*%2c[*]*.*.Program - /p:ExcludeByAttribute=Obsolete%2cGeneratedCodeAttribute%2cCompilerGeneratedAttribute' + /p:ExcludeByAttribute=Obsolete%2cGeneratedCodeAttribute%2cCompilerGeneratedAttribute' condition: and(succeeded(), eq(variables['BuildConfiguration'], 'debug')) - task: PublishCodeCoverageResults@1 From b4e05874875baa6fe812df5ac2323c3a0d47bb71 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 15 Aug 2024 13:24:18 -0700 Subject: [PATCH 0290/1479] Migrate job trigger into the isolated worker model --- .../DestinationNameReaderFunction.cs | 5 +- .../DestinationVerifierFunction.cs | 5 +- .../EmailSender/EmailSenderFunction.cs | 5 +- .../Activity/GetJobs/GetJobsFunction.cs | 5 +- .../Activity/JobTracker/JobTrackerFunction.cs | 5 +- .../Activity/JobUpdater/JobUpdaterFunction.cs | 5 +- .../Activity/Logger/LoggerFunction.cs | 5 +- .../ParseAndValidateDestinationFunction.cs | 5 +- .../SchemaValidatorFunction.cs | 5 +- .../TelemetryTrackerFunction.cs | 5 +- .../TopicMessageSenderFunction.cs | 5 +- .../JobTrigger/Function/JobTrigger.csproj | 15 +- .../Orchestrator/OrchestratorFunction.cs | 10 +- .../Hosts/JobTrigger/Function/Program.cs | 153 +++++++++++++++ .../Function/Starter/StarterFunction.cs | 11 +- .../SubOrchestratorFunction.cs | 8 +- .../Hosts/JobTrigger/Function/host.json | 12 +- .../OrchestratorFunctionTests.cs | 36 ++-- .../SubOrchestratorFunctionTests.cs | 174 +++++++++--------- .../Function/MembershipAggregator.csproj | 2 +- 20 files changed, 323 insertions(+), 153 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/DestinationNameReader/DestinationNameReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/DestinationNameReader/DestinationNameReaderFunction.cs index e310bee33..9f10be35f 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/DestinationNameReader/DestinationNameReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/DestinationNameReader/DestinationNameReaderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.JobTrigger { @@ -21,7 +20,7 @@ public DestinationNameReaderFunction(ILoggingRepository loggingRepository, IJobT _jobTriggerService = jobTriggerService ?? throw new ArgumentNullException(nameof(jobTriggerService)); ; } - [FunctionName(nameof(DestinationNameReaderFunction))] + [Function(nameof(DestinationNameReaderFunction))] public async Task GetDestinationNameAsync([ActivityTrigger] SyncJob syncJob) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(DestinationNameReaderFunction)} function started", RunId = syncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/DestinationVerifier/DestinationVerifierFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/DestinationVerifier/DestinationVerifierFunction.cs index 04fb02854..c91096ddc 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/DestinationVerifier/DestinationVerifierFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/DestinationVerifier/DestinationVerifierFunction.cs @@ -2,12 +2,11 @@ // Licensed under the MIT license. using Models; using Microsoft.ApplicationInsights; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.JobTrigger { @@ -24,7 +23,7 @@ public DestinationVerifierFunction(ILoggingRepository loggingRepository, IJobTri _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - [FunctionName(nameof(DestinationVerifierFunction))] + [Function(nameof(DestinationVerifierFunction))] public async Task VerifyDestinationAsync([ActivityTrigger] SyncJob syncJob) { var verifierResult = DestinationVerifierResult.NotFound; diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/EmailSender/EmailSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/EmailSender/EmailSenderFunction.cs index ce97a0511..b0704effc 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/EmailSender/EmailSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/EmailSender/EmailSenderFunction.cs @@ -2,12 +2,11 @@ // Licensed under the MIT license. using Models; using JobTrigger.Activity.EmailSender; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.JobTrigger { @@ -21,7 +20,7 @@ public EmailSenderFunction(ILoggingRepository loggingRepository, IJobTriggerServ _jobTriggerService = jobTriggerService ?? throw new ArgumentNullException(nameof(jobTriggerService)); ; } - [FunctionName(nameof(EmailSenderFunction))] + [Function(nameof(EmailSenderFunction))] public async Task SendEmailAsync([ActivityTrigger] EmailSenderRequest request) { var job = request.SyncJob; diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/GetJobs/GetJobsFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/GetJobs/GetJobsFunction.cs index 621f132be..d0990a29b 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/GetJobs/GetJobsFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/GetJobs/GetJobsFunction.cs @@ -2,14 +2,13 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using System; using System.Threading.Tasks; using Services.Contracts; using Repositories.Contracts; using System.Collections.Generic; using System.Linq; +using Microsoft.Azure.Functions.Worker; namespace Hosts.JobTrigger { @@ -23,7 +22,7 @@ public GetJobsFunction(IJobTriggerService jobTriggerService, ILoggingRepository _jobTriggerService = jobTriggerService ?? throw new ArgumentNullException(nameof(jobTriggerService)); } - [FunctionName(nameof(GetJobsFunction))] + [Function(nameof(GetJobsFunction))] public async Task> GetJobsToUpdateAsync([ActivityTrigger] object obj) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GetJobsFunction)} function started at: {DateTime.UtcNow}" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/JobTracker/JobTrackerFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/JobTracker/JobTrackerFunction.cs index 07c80e341..1b247dff2 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/JobTracker/JobTrackerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/JobTracker/JobTrackerFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Data.SqlTypes; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.JobTrigger { @@ -19,7 +18,7 @@ public JobTrackerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(JobTrackerFunction))] + [Function(nameof(JobTrackerFunction))] public async Task TrackJobFrequencyAsync([ActivityTrigger] SyncJob syncJob) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobTrackerFunction)} function started", RunId = syncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/JobUpdater/JobUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/JobUpdater/JobUpdaterFunction.cs index 573fdf09d..0634340f3 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/JobUpdater/JobUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/JobUpdater/JobUpdaterFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.JobTrigger { @@ -20,7 +19,7 @@ public JobUpdaterFunction(ILoggingRepository loggingRepository, IJobTriggerServi _jobTriggerService = jobTriggerService ?? throw new ArgumentNullException(nameof(jobTriggerService)); ; } - [FunctionName(nameof(JobUpdaterFunction))] + [Function(nameof(JobUpdaterFunction))] public async Task UpdateJobAsync([ActivityTrigger] JobUpdaterRequest request) { if (request.SyncJob != null) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/Logger/LoggerFunction.cs index cca59457e..6825300f7 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/Logger/LoggerFunction.cs @@ -2,11 +2,10 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.JobTrigger { @@ -19,7 +18,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.RunId },request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/ParseAndValidateDestination/ParseAndValidateDestinationFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/ParseAndValidateDestination/ParseAndValidateDestinationFunction.cs index 72e1dea9e..239a93dfc 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/ParseAndValidateDestination/ParseAndValidateDestinationFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/ParseAndValidateDestination/ParseAndValidateDestinationFunction.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Newtonsoft.Json.Linq; using Repositories.Contracts; @@ -9,6 +7,7 @@ using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.JobTrigger { @@ -22,7 +21,7 @@ public ParseAndValidateDestinationFunction(ILoggingRepository loggingRepository, _jobTriggerService = jobTriggerService ?? throw new ArgumentNullException(nameof(jobTriggerService)); ; } - [FunctionName(nameof(ParseAndValidateDestinationFunction))] + [Function(nameof(ParseAndValidateDestinationFunction))] public async Task<(bool IsValid, string DestinationObject)> ParseAndValidateDestinationAsync([ActivityTrigger] SyncJob syncJob) { diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs index 0287910fa..ab29e363b 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Hosts.JobTrigger; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Newtonsoft.Json; using NJsonSchema; @@ -11,6 +9,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace JobTrigger.Activity.SchemaValidator { @@ -30,7 +29,7 @@ public SchemaValidatorFunction( _schemaProvider = jsonSchemaProvider ?? throw new ArgumentNullException(nameof(jsonSchemaProvider)); } - [FunctionName(nameof(SchemaValidatorFunction))] + [Function(nameof(SchemaValidatorFunction))] public async Task ValidateSchemasAsync([ActivityTrigger] SyncJob syncJob) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function started", RunId = syncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs index 3d4644e31..292dd49ee 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Microsoft.ApplicationInsights; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.JobTrigger { @@ -22,7 +21,7 @@ public TelemetryTrackerFunction(ILoggingRepository loggingRepository, TelemetryC _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - [FunctionName(nameof(TelemetryTrackerFunction))] + [Function(nameof(TelemetryTrackerFunction))] public async Task TrackEventAsync([ActivityTrigger] TelemetryTrackerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TelemetryTrackerFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/TopicMessageSender/TopicMessageSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/TopicMessageSender/TopicMessageSenderFunction.cs index da9220a17..4eaacb177 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/TopicMessageSender/TopicMessageSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/TopicMessageSender/TopicMessageSenderFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.JobTrigger { @@ -20,7 +19,7 @@ public TopicMessageSenderFunction(ILoggingRepository loggingRepository, IJobTrig _jobTriggerService = jobTriggerService ?? throw new ArgumentNullException(nameof(jobTriggerService)); } - [FunctionName(nameof(TopicMessageSenderFunction))] + [Function(nameof(TopicMessageSenderFunction))] public async Task SendMessageAsync([ActivityTrigger] SyncJob syncJob) { if (syncJob != null) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj index 4fe1a8b70..74eaa1319 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj @@ -3,13 +3,19 @@ net8.0 v4 41fcfc96-9608-4426-9cd8-181202f026a4 + Exe + enabled - - + + + + + + @@ -36,4 +42,7 @@ Never - + + + + \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Orchestrator/OrchestratorFunction.cs index 0d476cbbe..e057d6f22 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Orchestrator/OrchestratorFunction.cs @@ -1,12 +1,12 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.JobTrigger { @@ -19,8 +19,8 @@ public OrchestratorFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(OrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(OrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var runId = context.NewGuid(); @@ -33,7 +33,7 @@ await context.CallActivityAsync(nameof(LoggerFunction), Verbosity = VerbosityLevel.DEBUG }); - var syncJobs = await context.CallActivityAsync>(nameof(GetJobsFunction), null); + var syncJobs = await context.CallActivityAsync>(new TaskName(nameof(GetJobsFunction)), null); await context.CallActivityAsync(nameof(LoggerFunction), new LoggerRequest diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs new file mode 100644 index 000000000..bfb47a416 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs @@ -0,0 +1,153 @@ +using Microsoft.Extensions.Hosting; +using Azure.Core; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using DIConcreteTypes; +using Hosts.FunctionBase; +using Microsoft.ApplicationInsights; +using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Graph; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.GraphGroups; +using Repositories.ServiceBusQueue; +using Repositories.ServiceBusTopics; +using Repositories.TeamsChannel; +using Services; +using Services.Contracts; +using System; +using System.IO; +using System.Runtime.CompilerServices; +using Azure.Identity; +using Hosts.JobTrigger; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = GetValueOrThrow("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var SCHEMA_DIRECTORY = "JsonSchemas"; + var functionName = "JobTrigger"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, "JobTrigger", dryRunSettingName, rootPath); + services.AddOptions().Configure((settings, configuration) => + { + settings.GMMHasGroupReadWriteAllPermissions = GetBoolSetting(configuration, "JobTrigger:IsGroupReadWriteAllGranted", false); + settings.GMMHasChannelReadWriteAllPermissions = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); + settings.JobCountThreshold = GetIntSetting(configuration, "JobTrigger:JobCountThreshold", 10); + settings.JobPerMilleThreshold = GetIntSetting(configuration, "JobTrigger:JobPerMilleThreshold", 10); + }); + + services.AddSingleton(services => services.GetService>().Value); + + services.AddSingleton>(services => + { + var graphCredentials = services.GetService>().Value; + return new KeyVaultSecret(graphCredentials.GMMOwnerAppId); + }) + .AddSingleton>(services => + { + var configuration = services.GetService(); + var serviceAccountObjectId = string.IsNullOrWhiteSpace(configuration["teamsChannelServiceAccountObjectId"]) ? Guid.Empty : Guid.Parse(configuration["teamsChannelServiceAccountObjectId"]); + return new KeyVaultSecret(serviceAccountObjectId); + }); + + services.AddGraphAPIClient(); + services.AddScoped(); + + services.AddTransient((services) => + { + var loggingRepository = services.GetRequiredService(); + var telemetryClient = services.GetRequiredService(); + + var configuration = services.GetService(); + var graphCredentials = services.GetService>().Value; + + var channelReadWriteApplicationPermissionGranted = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); + + TokenCredential graphTokenCredential; + + if (channelReadWriteApplicationPermissionGranted) + { + graphTokenCredential = FunctionAppDI.CreateAuthProviderFromSecret(graphCredentials); + } + else + { + graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; + graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; + + graphTokenCredential = FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials); + } + var graphServiceClient = new GraphServiceClient(graphTokenCredential); + + return new TeamsChannelRepository(loggingRepository, graphServiceClient, telemetryClient); + }); + + services.AddSingleton(services => + { + var serviceBusSyncJobTopic = GetValueOrThrow("serviceBusSyncJobTopic"); + var client = services.GetRequiredService(); + var sender = client.CreateSender(serviceBusSyncJobTopic); + return new ServiceBusTopicsRepository(sender); + }); + + services.AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + return new ServiceBusQueueRepository(sender); + }); + + services.AddScoped(); + + var jsonSchemasPath = Path.Combine(rootPath, SCHEMA_DIRECTORY); + var schemaProvider = new JsonSchemaProvider(); + if (Directory.Exists(jsonSchemasPath)) + { + var files = Directory.EnumerateFiles(jsonSchemasPath); + foreach (var file in files) + { + schemaProvider.Schemas.Add(Path.GetFileNameWithoutExtension(file), File.ReadAllText(file)); + } + } + + services.AddSingleton(schemaProvider); + }) + .Build(); + +host.Run(); + +static bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) +{ + var checkParse = bool.TryParse(configuration[settingName], out bool value); + return checkParse ? value : defaultValue; +} +static int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) +{ + var checkParse = int.TryParse(configuration[settingName], out int value); + return checkParse ? value : defaultValue; +} +static string GetValueOrThrow(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) +{ + var value = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException($"Could not start because of missing configuration option: {key}. Requested by file {callerFile}:{callerLine}."); + return value; +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Starter/StarterFunction.cs index eb42e5583..edc97eebf 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Starter/StarterFunction.cs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace Hosts.JobTrigger { @@ -18,13 +19,13 @@ public StarterFunction(ILoggingRepository loggingRepository) } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task Run( [TimerTrigger("%jobTriggerSchedule%")] TimerInfo myTimer, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient client) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started" }, VerbosityLevel.DEBUG); - await starter.StartNewAsync(nameof(OrchestratorFunction), null); + await client.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), null); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed" }, VerbosityLevel.DEBUG); } } diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs index 2e6f214a6..f4b7eeff4 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs @@ -3,8 +3,6 @@ using JobTrigger.Activity.EmailSender; using JobTrigger.Activity.SchemaValidator; using Microsoft.ApplicationInsights; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Models.Helpers; using Models.Notifications; @@ -19,6 +17,8 @@ using System.Text.Json; using System.Threading.Tasks; using JsonSerializer = System.Text.Json.JsonSerializer; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.JobTrigger { @@ -41,8 +41,8 @@ public SubOrchestratorFunction(ILoggingRepository loggingRepository, _emailSenderAndRecipients = emailSenderAndRecipients; } - [FunctionName(nameof(SubOrchestratorFunction))] - public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context, ExecutionContext executionContext) + [Function(nameof(SubOrchestratorFunction))] + public async Task RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context, ExecutionContext executionContext) { var syncJob = context.GetInput(); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/host.json b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/host.json index 911052841..1e6a74f99 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/host.json @@ -1,4 +1,12 @@ { - "version": "2.0", - "functionTimeout": "00:10:00" + "version": "2.0", + "functionTimeout": "00:10:00", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs index 9575dc75d..4767862bc 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Hosts.JobTrigger; using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Moq; @@ -11,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Services.Tests @@ -26,7 +28,7 @@ public async Task ValidOrchestratorRunAsync() var jobTriggerService = new Mock(); var jobTriggerServiceInProgress = new Mock(); var jobTriggerServiceStuckInProgress = new Mock(); - var context = new Mock(); + var context = new Mock(); var syncJobs = SampleDataHelper.CreateSampleSyncJobs(10, "GroupMembership"); var loggerJobProperties = new Dictionary(); @@ -39,13 +41,13 @@ public async Task ValidOrchestratorRunAsync() context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(GetJobsFunction)), It.IsAny())) .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); - context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x == nameof(SubOrchestratorFunction)), It.IsAny())); + context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), null)); var orchestrator = new OrchestratorFunction(loggingRepository.Object); await orchestrator.RunOrchestratorAsync(context.Object); Assert.IsTrue(syncJobs.All(x => x.RunId.HasValue)); - context.Verify(x => x.CallSubOrchestratorAsync(It.Is(x => x == nameof(SubOrchestratorFunction)), It.IsAny()), + context.Verify(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), null), Times.Exactly(syncJobs.Count)); } @@ -56,7 +58,7 @@ public async Task ZeroJobsRetrieved() var loggingRepository = new Mock(); var graphRepository = new Mock(); var jobTriggerService = new Mock(); - var context = new Mock(); + var context = new Mock(); var syncJobs = SampleDataHelper.CreateSampleSyncJobs(0, "GroupMembership"); var emptySyncJobsList = new List(); var loggerJobProperties = new Dictionary(); @@ -69,11 +71,11 @@ public async Task ZeroJobsRetrieved() context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(GetJobsFunction)), It.IsAny())) .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); - context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x == nameof(SubOrchestratorFunction)), It.IsAny())); + context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), null)); var orchestrator = new OrchestratorFunction(loggingRepository.Object); await orchestrator.RunOrchestratorAsync(context.Object); - context.Verify(x => x.CallSubOrchestratorAsync(nameof(SubOrchestratorFunction), It.IsAny()), + context.Verify(x => x.CallSubOrchestratorAsync(nameof(SubOrchestratorFunction), It.IsAny(), null), Times.Exactly(syncJobs.Count)); } @@ -83,7 +85,7 @@ public async Task NoContinuationTokenRetrieved() var loggingRepository = new Mock(); var graphRepository = new Mock(); var jobTriggerService = new Mock(); - var context = new Mock(); + var context = new Mock(); var syncJobs = SampleDataHelper.CreateSampleSyncJobs(10, "GroupMembership"); var emptySyncJobsList = new List(); var loggerJobProperties = new Dictionary(); @@ -95,14 +97,16 @@ public async Task NoContinuationTokenRetrieved() jobTriggerService.Setup(x => x.GetSyncJobsAsync()) .ReturnsAsync((syncJobs)); - context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(GetJobsFunction)), It.IsAny())) - .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); + context.Setup(x => x.CallActivityAsync>( + It.Is(x => x.ToString() == nameof(GetJobsFunction)), + null)) + .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); - context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x == nameof(SubOrchestratorFunction)), It.IsAny())); + context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), null)); var orchestrator = new OrchestratorFunction(loggingRepository.Object); await orchestrator.RunOrchestratorAsync(context.Object); - context.Verify(x => x.CallSubOrchestratorAsync(nameof(SubOrchestratorFunction), It.IsAny()), + context.Verify(x => x.CallSubOrchestratorAsync(nameof(SubOrchestratorFunction), It.IsAny(), null), Times.Exactly(syncJobs.Count)); } @@ -112,7 +116,7 @@ public async Task MultipleBatchesRetrieved() var loggingRepository = new Mock(); var graphRepository = new Mock(); var jobTriggerService = new Mock(); - var context = new Mock(); + var context = new Mock(); var syncJobs1 = SampleDataHelper.CreateSampleSyncJobs(10, "GroupMembership"); var syncJobs2 = SampleDataHelper.CreateSampleSyncJobs(10, "GroupMembership"); var emptySyncJobsList = new List(); @@ -125,7 +129,7 @@ public async Task MultipleBatchesRetrieved() jobTriggerService.Setup(x => x.GetSyncJobsAsync()) .ReturnsAsync(() => (syncJobs1.Concat(syncJobs2).ToList() )); - context.SetupSequence(x => x.CallActivityAsync>(nameof(GetJobsFunction), It.IsAny())) + context.SetupSequence(x => x.CallActivityAsync>(nameof(GetJobsFunction),null)) .ReturnsAsync(() => syncJobs1 ) @@ -133,14 +137,14 @@ public async Task MultipleBatchesRetrieved() syncJobs2 ); - context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(GetJobsFunction)), It.IsAny())) + context.Setup(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(GetJobsFunction)), null)) .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); - context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x == nameof(SubOrchestratorFunction)), It.IsAny())); + context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), null)); var orchestrator = new OrchestratorFunction(loggingRepository.Object); await orchestrator.RunOrchestratorAsync(context.Object); - context.Verify(x => x.CallSubOrchestratorAsync(nameof(SubOrchestratorFunction), It.IsAny()), + context.Verify(x => x.CallSubOrchestratorAsync(nameof(SubOrchestratorFunction), It.IsAny(), null), Times.Exactly(syncJobs1.Count + syncJobs2.Count)); } diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs index 46fdf863d..fb8cb1a63 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs @@ -27,6 +27,7 @@ using Models.Helpers; using System.Text.Json; using JsonSerializer = System.Text.Json.JsonSerializer; +using Microsoft.DurableTask; namespace Services.Tests { @@ -35,8 +36,8 @@ public class SubOrchestratorFunctionTests { Mock _jobTriggerService; Mock _loggingRespository; - Mock _context; - Mock _executionContext; + Mock _context; + ExecutionContext _executionContext; Mock _emailSenderAndRecipients; Mock _gmmResources; SyncStatus? _syncStatus = SyncStatus.Idle; @@ -57,8 +58,8 @@ public void Setup() _destinationVerifierResult = DestinationVerifierResult.Success; _gmmResources = new Mock(); _jobTriggerService = new Mock(); - _context = new Mock(); - _executionContext = new Mock(); + _context = new Mock(); + _executionContext = ExecutionContext.Capture(); _loggingRespository = new Mock(); _emailSenderAndRecipients = new Mock(); _telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); @@ -90,61 +91,64 @@ public void Setup() _syncStatus = status; }); - _context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.IsAny(), null)) + .Callback(async (name, request) => { _frequency = await CallJobTrackerFunctionAsync(request as SyncJob, DateTime.UtcNow); }) .ReturnsAsync(() => _frequency); - _context.Setup(x => x.CallActivityAsync<(bool IsValid, string DestinationObject)>(It.Is(x => x == nameof(ParseAndValidateDestinationFunction)), It.IsAny())) + _context.Setup(x => x.CallActivityAsync<(bool IsValid, string DestinationObject)>(It.Is(x => x.ToString() == nameof(ParseAndValidateDestinationFunction)), It.IsAny(), null)) .Returns(async () => await CallParseAndValidateDestinationFunction()); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(TelemetryTrackerFunction)), It.IsAny())) - .Callback(async (name, request) => - { - var telemetryRequest = request as TelemetryTrackerRequest; - await CallTelemetryTrackerFunctionAsync(telemetryRequest); - }); + _context.Setup(x => x.CallActivityAsync( + It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), + It.IsAny(), + It.IsAny())) + .Callback(async (name, request, options) => + { + var telemetryRequest = request as TelemetryTrackerRequest; + await CallTelemetryTrackerFunctionAsync(telemetryRequest); + }); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(JobUpdaterFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(JobUpdaterFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var updateRequest = request as JobUpdaterRequest; await CallJobStatusUpdaterFunctionAsync(updateRequest); _syncStatus = updateRequest.Status; }); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(DestinationVerifierFunction)), It.IsAny())) + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(DestinationVerifierFunction)), It.IsAny(), It.IsAny())) .Returns(async () => await CallDestinationVerifierFunctionAsync()); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(DestinationNameReaderFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(DestinationNameReaderFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _destinationName = await CallDestinationNameReaderFunctionAsync(); }) .ReturnsAsync(() => _destinationName); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(EmailSenderFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(EmailSenderFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallEmailSenderFunctionAsync(request as EmailSenderRequest); }); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(TopicMessageSenderFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TopicMessageSenderFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallTopicMessageSenderFunctionAsync(); }); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(LoggerFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(LoggerFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { await CallLoggerFunctionAsync(request as LoggerRequest); }); - _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _jsonValidationResult = await CallSchemaValidatorFunctionAsync(request as SyncJob); }).ReturnsAsync(() => _jsonValidationResult); @@ -162,7 +166,7 @@ public async Task HandleInvalidDestinationQueryException() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.Is(s => s == SyncStatus.DestinationQueryNotValid), It.IsAny()), Times.Once()); @@ -185,7 +189,7 @@ public async Task HandleInvalidDestinationQuery() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.Is(s => s == SyncStatus.DestinationQueryNotValid), It.IsAny()), Times.Once()); @@ -207,7 +211,7 @@ public async Task HandleInvalidQueryJson() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.Is(s => s == SyncStatus.QueryNotValid), It.IsAny()), Times.Once()); @@ -222,8 +226,8 @@ public async Task HandleInvalidQueryJson() [TestMethod] public async Task HandleInvalidJson() { - _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var job = request as SyncJob; job.Query = "{invalid-json}"; @@ -236,7 +240,7 @@ public async Task HandleInvalidJson() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.Is(s => s == SyncStatus.SchemaError), It.IsAny()), Times.Once()); @@ -259,7 +263,7 @@ public async Task HandleNoSchemasLoaded() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _loggingRespository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains("No json schemas have been loaded")), @@ -280,7 +284,7 @@ public async Task HandleSchemasForUnknowProperty() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _loggingRespository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains("Skipping schema validation for property")), @@ -300,7 +304,7 @@ public async Task HandleValidQueryJsonButWrongSchema() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(SyncStatus.SchemaError, It.IsAny()), Times.Once()); @@ -322,11 +326,12 @@ public async Task HandleStuckInProgressJobs() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(SyncStatus.ErroredDueToStuckInProgress, It.IsAny()), Times.Once()); } [TestMethod] + public async Task HandleEmptySourceQuery() { _syncJob.Query = null; @@ -336,7 +341,7 @@ public async Task HandleEmptySourceQuery() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.Is(s => s == SyncStatus.QueryNotValid), It.IsAny()), Times.Once()); @@ -354,8 +359,8 @@ public async Task ProcessValidSourceAndDestinationQueries() _context.Setup(x => x.GetInput()).Returns(_syncJob); - _context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _frequency = await CallJobTrackerFunctionAsync(request as SyncJob, SqlDateTime.MinValue.Value); }) @@ -365,7 +370,7 @@ public async Task ProcessValidSourceAndDestinationQueries() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _loggingRespository.Verify(x => x.LogMessageAsync( It.Is(m => !m.Message.Contains("Source query is not valid") && !m.Message.Contains("Source query is empty for job")), @@ -379,10 +384,10 @@ public async Task ProcessValidSourceAndDestinationQueries() It.IsAny(), It.IsAny())); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(JobTrackerFunction)), It.IsAny()), Times.Once()); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(DestinationNameReaderFunction)), It.IsAny()), Times.Once()); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(EmailSenderFunction)), It.IsAny()), Times.Once()); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(TopicMessageSenderFunction)), It.IsAny()), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(JobTrackerFunction)), It.IsAny(), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(DestinationNameReaderFunction)), It.IsAny(), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(EmailSenderFunction)), It.IsAny(), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TopicMessageSenderFunction)), It.IsAny(), null), Times.Once()); _jobTriggerService.Verify(x => x.GetDestinationNameAsync(It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -399,8 +404,8 @@ public async Task ProcessIdleJob() var serviceBusSender = new Mock(); _context.Setup(x => x.GetInput()).Returns(_syncJob); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(TopicMessageSenderFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TopicMessageSenderFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var gmmResources = new Mock(); var jobTriggerConfig = new Mock(); @@ -436,12 +441,12 @@ public async Task ProcessIdleJob() await CallTopicMessageSenderFunctionAsync(jobTriggerService: jobTriggerService); }); - _context.Setup(x => x.CallActivityAsync(nameof(JobTrackerFunction), It.IsAny())).ReturnsAsync(2); + _context.Setup(x => x.CallActivityAsync(nameof(JobTrackerFunction), It.IsAny(), null)).ReturnsAsync(2); var suborchestrator = new SubOrchestratorFunction(_loggingRespository.Object, _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _loggingRespository.Verify(x => x.LogMessageAsync( It.Is(m => !m.Message.Contains("Source query is not valid") && !m.Message.Contains("Source query is empty for job")), @@ -449,10 +454,10 @@ public async Task ProcessIdleJob() It.IsAny(), It.IsAny())); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(JobTrackerFunction)), It.IsAny()), Times.Once()); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(DestinationNameReaderFunction)), It.IsAny()), Times.Once()); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(EmailSenderFunction)), It.IsAny()), Times.Once()); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(TopicMessageSenderFunction)), It.IsAny()), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(JobTrackerFunction)), It.IsAny(), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(DestinationNameReaderFunction)), It.IsAny(), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(EmailSenderFunction)), It.IsAny(), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TopicMessageSenderFunction)), It.IsAny(), null), Times.Once()); _jobTriggerService.Verify(x => x.GetDestinationNameAsync(It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -481,8 +486,8 @@ public async Task ProcessInProgressJob() _syncJob.Status = SyncStatus.InProgress.ToString(); _context.Setup(x => x.GetInput()).Returns(_syncJob); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(TopicMessageSenderFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TopicMessageSenderFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { var gmmResources = new Mock(); var jobTriggerConfig = new Mock(); @@ -518,12 +523,12 @@ public async Task ProcessInProgressJob() await CallTopicMessageSenderFunctionAsync(jobTriggerService: jobTriggerService); }); - _context.Setup(x => x.CallActivityAsync(nameof(JobTrackerFunction), It.IsAny())).ReturnsAsync(2); + _context.Setup(x => x.CallActivityAsync(nameof(JobTrackerFunction), It.IsAny(), null)).ReturnsAsync(2); var suborchestrator = new SubOrchestratorFunction(_loggingRespository.Object, _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); + await suborchestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); _loggingRespository.Verify(x => x.LogMessageAsync( It.Is(m => !m.Message.Contains("Source query is not valid") && !m.Message.Contains("Source query is empty for job")), @@ -531,10 +536,10 @@ public async Task ProcessInProgressJob() It.IsAny(), It.IsAny())); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(JobTrackerFunction)), It.IsAny()), Times.Once()); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(DestinationNameReaderFunction)), It.IsAny()), Times.Once()); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(EmailSenderFunction)), It.IsAny()), Times.Once()); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(TopicMessageSenderFunction)), It.IsAny()), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(JobTrackerFunction)), It.IsAny(), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(DestinationNameReaderFunction)), It.IsAny(), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(EmailSenderFunction)), It.IsAny(), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TopicMessageSenderFunction)), It.IsAny(), null), Times.Once()); _jobTriggerService.Verify(x => x.GetDestinationNameAsync(It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -558,42 +563,42 @@ public async Task ProcessInProgressJob() public async Task DestinationNotFound() { _context.Setup(x => x.GetInput()).Returns(_syncJob); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(DestinationVerifierFunction)), It.IsAny())) + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(DestinationVerifierFunction)), It.IsAny(), null)) .ReturnsAsync(DestinationVerifierResult.NotFound); var suborchrestrator = new SubOrchestratorFunction(_loggingRespository.Object, _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(TelemetryTrackerFunction)), It.IsAny()), Times.Once()); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(),null), Times.Once()); Assert.AreEqual(SyncStatus.DestinationGroupNotFound, _syncStatus); - _context.Verify(x => x.CallActivityAsync(nameof(EmailSenderFunction), It.Is(r => r.NotificationType == NotificationMessageType.DestinationNotExistNotification)), Times.Once()); + _context.Verify(x => x.CallActivityAsync(nameof(EmailSenderFunction), It.Is(r => r.NotificationType == NotificationMessageType.DestinationNotExistNotification), null), Times.Once()); } [TestMethod] public async Task DestinationNotOwnedByGMM() { _context.Setup(x => x.GetInput()).Returns(_syncJob); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(DestinationVerifierFunction)), It.IsAny())) + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(DestinationVerifierFunction)), It.IsAny(), null)) .ReturnsAsync(DestinationVerifierResult.NotOwnedByGMM); var suborchrestrator = new SubOrchestratorFunction(_loggingRespository.Object, _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(TelemetryTrackerFunction)), It.IsAny()), Times.Once()); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(), null), Times.Once()); Assert.AreEqual(SyncStatus.NotOwnerOfDestinationGroup, _syncStatus); - _context.Verify(x => x.CallActivityAsync(nameof(EmailSenderFunction), It.Is(r => r.NotificationType == NotificationMessageType.NotOwnerNotification)), Times.Once()); + _context.Verify(x => x.CallActivityAsync(nameof(EmailSenderFunction), It.Is(r => r.NotificationType == NotificationMessageType.NotOwnerNotification), null), Times.Once()); } [TestMethod] public async Task TrackTelemetry() { _context.Setup(x => x.GetInput()).Returns(_syncJob); - _context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _frequency = await CallJobTrackerFunctionAsync(request as SyncJob, DateTime.UtcNow.AddDays(-1)); }) @@ -605,8 +610,8 @@ public async Task TrackTelemetry() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(TelemetryTrackerFunction)), It.IsAny()), Times.Once()); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(), null), Times.Once()); Assert.AreEqual(SyncStatus.NotOwnerOfDestinationGroup, _syncStatus); _destinationVerifierResult = DestinationVerifierResult.NotOwnedByGMM; @@ -616,8 +621,8 @@ public async Task TrackTelemetry() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(TelemetryTrackerFunction)), It.IsAny()), Times.Exactly(2)); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(), null), Times.Exactly(2)); Assert.AreEqual(SyncStatus.QueryNotValid, _syncStatus); } @@ -625,21 +630,22 @@ public async Task TrackTelemetry() public async Task HandleUnexpectedExceptionInSubOrchestrator() { _context.Setup(x => x.GetInput()).Returns(_syncJob); - _context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Throws(new Exception("Unexpected exception triggered for testing")); + _context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(),null)) + .Throws(new Exception("Unexpected exception triggered for testing")); var suborchrestrator = new SubOrchestratorFunction(_loggingRespository.Object, _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext.Object); - - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(req => req.Message.Contains("Caught unexpected exception in SubOrchestratorFunction, marking sync job as errored. "))), Times.Once()); - _context.Verify(x => x.CallActivityAsync(nameof(JobUpdaterFunction), - It.Is(req => req.Status == SyncStatus.Error)), Times.Once()); - _context.Verify(x => x.CallActivityAsync(nameof(TelemetryTrackerFunction), - It.Is(req => req.JobStatus == SyncStatus.Error && req.ResultStatus == ResultStatus.Failure)), Times.Once()); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + + + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(LoggerFunction)), + It.Is(req => req.Message.Contains("Caught unexpected exception in SubOrchestratorFunction, marking sync job as errored. ")), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(JobUpdaterFunction)), + It.Is(req => req.Status == SyncStatus.Error), null), Times.Once()); + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), + It.Is(req => req.JobStatus == SyncStatus.Error && req.ResultStatus == ResultStatus.Failure), null), Times.Once()); } private async Task<(bool IsValid, string DestinationObject)> CallParseAndValidateDestinationFunction() diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj index 391785ee7..323f70fa9 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj @@ -1,6 +1,6 @@ - net8.0 + net6.0 v4 Exe enabled From 46bbd519c0f6b9ac3f097b951abceba2779a790e Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 15 Aug 2024 13:41:50 -0700 Subject: [PATCH 0291/1479] Revert .net version --- .../MembershipAggregator/Function/MembershipAggregator.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj index 323f70fa9..391785ee7 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/MembershipAggregator.csproj @@ -1,6 +1,6 @@ - net6.0 + net8.0 v4 Exe enabled From 4746cb047106ffeb0d9a1427bb3b8d231ce7e6a4 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Fri, 16 Aug 2024 13:42:26 -0700 Subject: [PATCH 0292/1479] Change the code coverage exlusion --- .../Hosts/JobTrigger/Function/Startup.cs | 138 ------------------ 1 file changed, 138 deletions(-) delete mode 100644 Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs deleted file mode 100644 index ebbd25d5f..000000000 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Startup.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Azure.Core; -using Azure.Messaging.ServiceBus; -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.FunctionBase; -using Microsoft.ApplicationInsights; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.Graph; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphGroups; -using Repositories.ServiceBusQueue; -using Repositories.ServiceBusTopics; -using Repositories.TeamsChannel; -using Services; -using Services.Contracts; -using System; -using System.IO; - -[assembly: FunctionsStartup(typeof(Hosts.JobTrigger.Startup))] - -namespace Hosts.JobTrigger -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(JobTrigger); - protected override string DryRunSettingName => string.Empty; - - private const string SCHEMA_DIRECTORY = "JsonSchemas"; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddOptions().Configure((settings, configuration) => - { - settings.GMMHasGroupReadWriteAllPermissions = GetBoolSetting(configuration, "JobTrigger:IsGroupReadWriteAllGranted", false); - settings.GMMHasChannelReadWriteAllPermissions = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); - settings.JobCountThreshold = GetIntSetting(configuration, "JobTrigger:JobCountThreshold", 10); - settings.JobPerMilleThreshold = GetIntSetting(configuration, "JobTrigger:JobPerMilleThreshold", 10); - }); - - builder.Services.AddSingleton(services => services.GetService>().Value); - - builder.Services.AddSingleton>(services => - { - var graphCredentials = services.GetService>().Value; - return new KeyVaultSecret(graphCredentials.GMMOwnerAppId); - }) - .AddSingleton>(services => - { - var configuration = services.GetService(); - var serviceAccountObjectId = string.IsNullOrWhiteSpace(configuration["teamsChannelServiceAccountObjectId"]) ? Guid.Empty : Guid.Parse(configuration["teamsChannelServiceAccountObjectId"]); - return new KeyVaultSecret(serviceAccountObjectId); - }); - - builder.Services.AddGraphAPIClient(); - builder.Services.AddScoped(); - - builder.Services.AddTransient((services) => - { - var loggingRepository = services.GetRequiredService(); - var telemetryClient = services.GetRequiredService(); - - var configuration = services.GetService(); - var graphCredentials = services.GetService>().Value; - - var channelReadWriteApplicationPermissionGranted = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); - - TokenCredential graphTokenCredential; - - if (channelReadWriteApplicationPermissionGranted) - { - graphTokenCredential = FunctionAppDI.CreateAuthProviderFromSecret(graphCredentials); - } - else - { - graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; - graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; - - graphTokenCredential = FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials); - } - var graphServiceClient = new GraphServiceClient(graphTokenCredential); - - return new TeamsChannelRepository(loggingRepository, graphServiceClient, telemetryClient); - }); - - builder.Services.AddSingleton(services => - { - var serviceBusSyncJobTopic = GetValueOrThrow("serviceBusSyncJobTopic"); - var client = services.GetRequiredService(); - var sender = client.CreateSender(serviceBusSyncJobTopic); - return new ServiceBusTopicsRepository(sender); - }); - - builder.Services.AddSingleton(services => - { - var configuration = services.GetRequiredService(); - var notificationsQueue = configuration["serviceBusNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(notificationsQueue); - return new ServiceBusQueueRepository(sender); - }); - - builder.Services.AddScoped(); - - var rootPath = builder.GetContext().ApplicationRootPath; - var jsonSchemasPath = Path.Combine(rootPath, SCHEMA_DIRECTORY); - var schemaProvider = new JsonSchemaProvider(); - if (Directory.Exists(jsonSchemasPath)) - { - var files = Directory.EnumerateFiles(jsonSchemasPath); - foreach (var file in files) - { - schemaProvider.Schemas.Add(Path.GetFileNameWithoutExtension(file), File.ReadAllText(file)); - } - } - - builder.Services.AddSingleton(schemaProvider); - } - - private bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) - { - var checkParse = bool.TryParse(configuration[settingName], out bool value); - return checkParse ? value : defaultValue; - } - private int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) - { - var checkParse = int.TryParse(configuration[settingName], out int value); - return checkParse ? value : defaultValue; - } - } -} From 47527b6f8967dbc9d77f7fe3e6f3c4403d4a5ca1 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 10:31:57 -0700 Subject: [PATCH 0293/1479] Add program class --- .../Hosts/JobTrigger/Function/Program.cs | 243 +++++++++--------- 1 file changed, 127 insertions(+), 116 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs index bfb47a416..a49aeeac1 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs @@ -22,132 +22,143 @@ using System.IO; using System.Runtime.CompilerServices; using Azure.Identity; -using Hosts.JobTrigger; -var host = new HostBuilder() - .ConfigureFunctionsWorkerDefaults() - .ConfigureAppConfiguration((context, config) => - { - var settings = config.Build(); - var appConfigEndpoint = GetValueOrThrow("appConfigurationEndpoint"); +namespace Hosts.JobTrigger +{ - config.AddAzureAppConfiguration(options => - { - options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) - .UseFeatureFlags(); - }); - }) - .ConfigureServices((context, services) => + public class Program { - var configuration = context.Configuration; - var SCHEMA_DIRECTORY = "JsonSchemas"; - var functionName = "JobTrigger"; - var dryRunSettingName = string.Empty; - var rootPath = context.HostingEnvironment.ContentRootPath; - CommonServices.ConfigureCommonServices(services, configuration, "JobTrigger", dryRunSettingName, rootPath); - services.AddOptions().Configure((settings, configuration) => - { - settings.GMMHasGroupReadWriteAllPermissions = GetBoolSetting(configuration, "JobTrigger:IsGroupReadWriteAllGranted", false); - settings.GMMHasChannelReadWriteAllPermissions = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); - settings.JobCountThreshold = GetIntSetting(configuration, "JobTrigger:JobCountThreshold", 10); - settings.JobPerMilleThreshold = GetIntSetting(configuration, "JobTrigger:JobPerMilleThreshold", 10); - }); - - services.AddSingleton(services => services.GetService>().Value); - - services.AddSingleton>(services => - { - var graphCredentials = services.GetService>().Value; - return new KeyVaultSecret(graphCredentials.GMMOwnerAppId); - }) - .AddSingleton>(services => + public static void Main (string[] args) { - var configuration = services.GetService(); - var serviceAccountObjectId = string.IsNullOrWhiteSpace(configuration["teamsChannelServiceAccountObjectId"]) ? Guid.Empty : Guid.Parse(configuration["teamsChannelServiceAccountObjectId"]); - return new KeyVaultSecret(serviceAccountObjectId); - }); - - services.AddGraphAPIClient(); - services.AddScoped(); - - services.AddTransient((services) => - { - var loggingRepository = services.GetRequiredService(); - var telemetryClient = services.GetRequiredService(); - - var configuration = services.GetService(); - var graphCredentials = services.GetService>().Value; - - var channelReadWriteApplicationPermissionGranted = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); - - TokenCredential graphTokenCredential; - - if (channelReadWriteApplicationPermissionGranted) + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => { - graphTokenCredential = FunctionAppDI.CreateAuthProviderFromSecret(graphCredentials); - } - else + var settings = config.Build(); + var appConfigEndpoint = GetValueOrThrow("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => { - graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; - graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; - - graphTokenCredential = FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials); - } - var graphServiceClient = new GraphServiceClient(graphTokenCredential); - - return new TeamsChannelRepository(loggingRepository, graphServiceClient, telemetryClient); - }); - - services.AddSingleton(services => + var configuration = context.Configuration; + var SCHEMA_DIRECTORY = "JsonSchemas"; + var functionName = "JobTrigger"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, "JobTrigger", dryRunSettingName, rootPath); + services.AddOptions().Configure((settings, configuration) => + { + settings.GMMHasGroupReadWriteAllPermissions = GetBoolSetting(configuration, "JobTrigger:IsGroupReadWriteAllGranted", false); + settings.GMMHasChannelReadWriteAllPermissions = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); + settings.JobCountThreshold = GetIntSetting(configuration, "JobTrigger:JobCountThreshold", 10); + settings.JobPerMilleThreshold = GetIntSetting(configuration, "JobTrigger:JobPerMilleThreshold", 10); + }); + + services.AddSingleton(services => services.GetService>().Value); + + services.AddSingleton>(services => + { + var graphCredentials = services.GetService>().Value; + return new KeyVaultSecret(graphCredentials.GMMOwnerAppId); + }) + .AddSingleton>(services => + { + var configuration = services.GetService(); + var serviceAccountObjectId = string.IsNullOrWhiteSpace(configuration["teamsChannelServiceAccountObjectId"]) ? Guid.Empty : Guid.Parse(configuration["teamsChannelServiceAccountObjectId"]); + return new KeyVaultSecret(serviceAccountObjectId); + }); + + services.AddGraphAPIClient(); + services.AddScoped(); + + services.AddTransient((services) => + { + var loggingRepository = services.GetRequiredService(); + var telemetryClient = services.GetRequiredService(); + + var configuration = services.GetService(); + var graphCredentials = services.GetService>().Value; + + var channelReadWriteApplicationPermissionGranted = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); + + TokenCredential graphTokenCredential; + + if (channelReadWriteApplicationPermissionGranted) + { + graphTokenCredential = FunctionAppDI.CreateAuthProviderFromSecret(graphCredentials); + } + else + { + graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; + graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; + + graphTokenCredential = FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials); + } + var graphServiceClient = new GraphServiceClient(graphTokenCredential); + + return new TeamsChannelRepository(loggingRepository, graphServiceClient, telemetryClient); + }); + + services.AddSingleton(services => + { + var serviceBusSyncJobTopic = GetValueOrThrow("serviceBusSyncJobTopic"); + var client = services.GetRequiredService(); + var sender = client.CreateSender(serviceBusSyncJobTopic); + return new ServiceBusTopicsRepository(sender); + }); + + services.AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + return new ServiceBusQueueRepository(sender); + }); + + services.AddScoped(); + + var jsonSchemasPath = Path.Combine(rootPath, SCHEMA_DIRECTORY); + var schemaProvider = new JsonSchemaProvider(); + if (Directory.Exists(jsonSchemasPath)) + { + var files = Directory.EnumerateFiles(jsonSchemasPath); + foreach (var file in files) + { + schemaProvider.Schemas.Add(Path.GetFileNameWithoutExtension(file), File.ReadAllText(file)); + } + } + + services.AddSingleton(schemaProvider); + }) + .Build(); + + host.Run(); + } + static bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) { - var serviceBusSyncJobTopic = GetValueOrThrow("serviceBusSyncJobTopic"); - var client = services.GetRequiredService(); - var sender = client.CreateSender(serviceBusSyncJobTopic); - return new ServiceBusTopicsRepository(sender); - }); - - services.AddSingleton(services => + var checkParse = bool.TryParse(configuration[settingName], out bool value); + return checkParse ? value : defaultValue; + } + static int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) { - var configuration = services.GetRequiredService(); - var notificationsQueue = configuration["serviceBusNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(notificationsQueue); - return new ServiceBusQueueRepository(sender); - }); - - services.AddScoped(); - - var jsonSchemasPath = Path.Combine(rootPath, SCHEMA_DIRECTORY); - var schemaProvider = new JsonSchemaProvider(); - if (Directory.Exists(jsonSchemasPath)) + var checkParse = int.TryParse(configuration[settingName], out int value); + return checkParse ? value : defaultValue; + } + static string GetValueOrThrow(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) { - var files = Directory.EnumerateFiles(jsonSchemasPath); - foreach (var file in files) - { - schemaProvider.Schemas.Add(Path.GetFileNameWithoutExtension(file), File.ReadAllText(file)); - } + var value = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException($"Could not start because of missing configuration option: {key}. Requested by file {callerFile}:{callerLine}."); + return value; } + } +} - services.AddSingleton(schemaProvider); - }) - .Build(); -host.Run(); -static bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) -{ - var checkParse = bool.TryParse(configuration[settingName], out bool value); - return checkParse ? value : defaultValue; -} -static int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) -{ - var checkParse = int.TryParse(configuration[settingName], out int value); - return checkParse ? value : defaultValue; -} -static string GetValueOrThrow(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) -{ - var value = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentNullException($"Could not start because of missing configuration option: {key}. Requested by file {callerFile}:{callerLine}."); - return value; -} \ No newline at end of file From 6517eac104f0b56ff24f27938d22e13c3ca08448 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 10:33:56 -0700 Subject: [PATCH 0294/1479] Change code coverage From 2bc780d18c5cadecf7a6480646931347ee7801b1 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 14:08:56 -0700 Subject: [PATCH 0295/1479] Add main class and change test --- .../Hosts/JobTrigger/Function/Program.cs | 34 ++++++------------- .../OrchestratorFunctionTests.cs | 5 ++- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs index a49aeeac1..b7e84f173 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + using Microsoft.Extensions.Hosting; using Azure.Core; using Azure.Messaging.ServiceBus; @@ -35,7 +38,7 @@ public static void Main (string[] args) .ConfigureAppConfiguration((context, config) => { var settings = config.Build(); - var appConfigEndpoint = GetValueOrThrow("appConfigurationEndpoint"); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); config.AddAzureAppConfiguration(options => { @@ -53,10 +56,10 @@ public static void Main (string[] args) CommonServices.ConfigureCommonServices(services, configuration, "JobTrigger", dryRunSettingName, rootPath); services.AddOptions().Configure((settings, configuration) => { - settings.GMMHasGroupReadWriteAllPermissions = GetBoolSetting(configuration, "JobTrigger:IsGroupReadWriteAllGranted", false); - settings.GMMHasChannelReadWriteAllPermissions = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); - settings.JobCountThreshold = GetIntSetting(configuration, "JobTrigger:JobCountThreshold", 10); - settings.JobPerMilleThreshold = GetIntSetting(configuration, "JobTrigger:JobPerMilleThreshold", 10); + settings.GMMHasGroupReadWriteAllPermissions = CommonServices.GetBoolSettingBase(configuration, "JobTrigger:IsGroupReadWriteAllGranted", false); + settings.GMMHasChannelReadWriteAllPermissions = CommonServices.GetBoolSettingBase(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); + settings.JobCountThreshold = CommonServices.GetIntSettingBase(configuration, "JobTrigger:JobCountThreshold", 10); + settings.JobPerMilleThreshold = CommonServices.GetIntSettingBase(configuration, "JobTrigger:JobPerMilleThreshold", 10); }); services.AddSingleton(services => services.GetService>().Value); @@ -84,7 +87,7 @@ public static void Main (string[] args) var configuration = services.GetService(); var graphCredentials = services.GetService>().Value; - var channelReadWriteApplicationPermissionGranted = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); + var channelReadWriteApplicationPermissionGranted = CommonServices.GetBoolSettingBase(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); TokenCredential graphTokenCredential; @@ -106,7 +109,7 @@ public static void Main (string[] args) services.AddSingleton(services => { - var serviceBusSyncJobTopic = GetValueOrThrow("serviceBusSyncJobTopic"); + var serviceBusSyncJobTopic = CommonServices.GetValueOrThrowBase("serviceBusSyncJobTopic"); var client = services.GetRequiredService(); var sender = client.CreateSender(serviceBusSyncJobTopic); return new ServiceBusTopicsRepository(sender); @@ -140,23 +143,6 @@ public static void Main (string[] args) host.Run(); } - static bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) - { - var checkParse = bool.TryParse(configuration[settingName], out bool value); - return checkParse ? value : defaultValue; - } - static int GetIntSetting(IConfiguration configuration, string settingName, int defaultValue) - { - var checkParse = int.TryParse(configuration[settingName], out int value); - return checkParse ? value : defaultValue; - } - static string GetValueOrThrow(string key, [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) - { - var value = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentNullException($"Could not start because of missing configuration option: {key}. Requested by file {callerFile}:{callerLine}."); - return value; - } } } diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs index 4767862bc..e3fcbc169 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs @@ -38,9 +38,8 @@ public async Task ValidOrchestratorRunAsync() int maxJobsAllowed = syncJobs.Count; jobTriggerService.Setup(x => x.GetSyncJobsAsync()) .ReturnsAsync(syncJobs); - context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(GetJobsFunction)), It.IsAny())) + context.Setup(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(GetJobsFunction)),null)) .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); - context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), null)); var orchestrator = new OrchestratorFunction(loggingRepository.Object); @@ -68,7 +67,7 @@ public async Task ZeroJobsRetrieved() int maxJobsAllowed = syncJobs.Count; jobTriggerService.Setup(x => x.GetSyncJobsAsync()) .ReturnsAsync((syncJobs)); - context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(GetJobsFunction)), It.IsAny())) + context.Setup(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(GetJobsFunction)), null)) .Returns(() => CallGetSyncJobsAsync(loggingRepository.Object, jobTriggerService.Object)); context.Setup(x => x.CallSubOrchestratorAsync(It.Is(x => x.ToString() == nameof(SubOrchestratorFunction)), It.IsAny(), null)); From 40276633560c1056b828620be3a50b45532b97b5 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 22 Aug 2024 11:38:35 -0700 Subject: [PATCH 0296/1479] Change function name --- .../Hosts/JobTrigger/Function/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs index b7e84f173..23b6bd181 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Program.cs @@ -53,7 +53,7 @@ public static void Main (string[] args) var functionName = "JobTrigger"; var dryRunSettingName = string.Empty; var rootPath = context.HostingEnvironment.ContentRootPath; - CommonServices.ConfigureCommonServices(services, configuration, "JobTrigger", dryRunSettingName, rootPath); + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); services.AddOptions().Configure((settings, configuration) => { settings.GMMHasGroupReadWriteAllPermissions = CommonServices.GetBoolSettingBase(configuration, "JobTrigger:IsGroupReadWriteAllGranted", false); From d38ba39752dc3165c69a2202ec2bc0be49641a81 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 22 Aug 2024 14:52:13 -0700 Subject: [PATCH 0297/1479] Update commonsetting --- .../Hosts/JobTrigger/Infrastructure/compute/template.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep index 3100907c1..b6784d6b4 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep @@ -119,7 +119,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } From a8dc1ae95cb20b50fd29ae46c6f8d393664941e8 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 22 Aug 2024 15:27:37 -0700 Subject: [PATCH 0298/1479] Remove unused reference --- .../JobTrigger/Function/JobTrigger.csproj | 4 --- .../SubOrchestratorFunction.cs | 2 +- .../OrchestratorFunctionTests.cs | 1 - .../SubOrchestratorFunctionTests.cs | 36 +++++++++---------- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj index 74eaa1319..b1887f6e3 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/JobTrigger.csproj @@ -9,7 +9,6 @@ - @@ -42,7 +41,4 @@ Never - - - \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs index f4b7eeff4..791f4fa01 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs @@ -42,7 +42,7 @@ public SubOrchestratorFunction(ILoggingRepository loggingRepository, } [Function(nameof(SubOrchestratorFunction))] - public async Task RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context, ExecutionContext executionContext) + public async Task RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var syncJob = context.GetInput(); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs index e3fcbc169..3e6155d47 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/OrchestratorFunctionTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Hosts.JobTrigger; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs index fb8cb1a63..0f6c86409 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs @@ -6,7 +6,6 @@ using JobTrigger.Activity.SchemaValidator; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Moq; @@ -16,7 +15,6 @@ using Repositories.ServiceBusTopics; using Services.Contracts; using Services.Tests.Helpers; -using Repositories.ServiceBusQueue; using System; using System.Collections.Generic; using System.Data.SqlTypes; @@ -166,7 +164,7 @@ public async Task HandleInvalidDestinationQueryException() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.Is(s => s == SyncStatus.DestinationQueryNotValid), It.IsAny()), Times.Once()); @@ -189,7 +187,7 @@ public async Task HandleInvalidDestinationQuery() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.Is(s => s == SyncStatus.DestinationQueryNotValid), It.IsAny()), Times.Once()); @@ -211,7 +209,7 @@ public async Task HandleInvalidQueryJson() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.Is(s => s == SyncStatus.QueryNotValid), It.IsAny()), Times.Once()); @@ -240,7 +238,7 @@ public async Task HandleInvalidJson() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.Is(s => s == SyncStatus.SchemaError), It.IsAny()), Times.Once()); @@ -263,7 +261,7 @@ public async Task HandleNoSchemasLoaded() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _loggingRespository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains("No json schemas have been loaded")), @@ -284,7 +282,7 @@ public async Task HandleSchemasForUnknowProperty() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _loggingRespository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains("Skipping schema validation for property")), @@ -304,7 +302,7 @@ public async Task HandleValidQueryJsonButWrongSchema() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(SyncStatus.SchemaError, It.IsAny()), Times.Once()); @@ -326,7 +324,7 @@ public async Task HandleStuckInProgressJobs() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(SyncStatus.ErroredDueToStuckInProgress, It.IsAny()), Times.Once()); } @@ -341,7 +339,7 @@ public async Task HandleEmptySourceQuery() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny()), Times.Once()); _jobTriggerService.Verify(x => x.UpdateSyncJobAsync(It.Is(s => s == SyncStatus.QueryNotValid), It.IsAny()), Times.Once()); @@ -370,7 +368,7 @@ public async Task ProcessValidSourceAndDestinationQueries() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _loggingRespository.Verify(x => x.LogMessageAsync( It.Is(m => !m.Message.Contains("Source query is not valid") && !m.Message.Contains("Source query is empty for job")), @@ -446,7 +444,7 @@ public async Task ProcessIdleJob() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchestrator.RunSubOrchestratorAsync(_context.Object); _loggingRespository.Verify(x => x.LogMessageAsync( It.Is(m => !m.Message.Contains("Source query is not valid") && !m.Message.Contains("Source query is empty for job")), @@ -528,7 +526,7 @@ public async Task ProcessInProgressJob() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchestrator.RunSubOrchestratorAsync(_context.Object); _loggingRespository.Verify(x => x.LogMessageAsync( It.Is(m => !m.Message.Contains("Source query is not valid") && !m.Message.Contains("Source query is empty for job")), @@ -570,7 +568,7 @@ public async Task DestinationNotFound() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(),null), Times.Once()); Assert.AreEqual(SyncStatus.DestinationGroupNotFound, _syncStatus); _context.Verify(x => x.CallActivityAsync(nameof(EmailSenderFunction), It.Is(r => r.NotificationType == NotificationMessageType.DestinationNotExistNotification), null), Times.Once()); @@ -587,7 +585,7 @@ public async Task DestinationNotOwnedByGMM() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(), null), Times.Once()); Assert.AreEqual(SyncStatus.NotOwnerOfDestinationGroup, _syncStatus); _context.Verify(x => x.CallActivityAsync(nameof(EmailSenderFunction), It.Is(r => r.NotificationType == NotificationMessageType.NotOwnerNotification), null), Times.Once()); @@ -610,7 +608,7 @@ public async Task TrackTelemetry() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(), null), Times.Once()); Assert.AreEqual(SyncStatus.NotOwnerOfDestinationGroup, _syncStatus); @@ -621,7 +619,7 @@ public async Task TrackTelemetry() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(TelemetryTrackerFunction)), It.IsAny(), null), Times.Exactly(2)); Assert.AreEqual(SyncStatus.QueryNotValid, _syncStatus); } @@ -637,7 +635,7 @@ public async Task HandleUnexpectedExceptionInSubOrchestrator() _telemetryClient, _emailSenderAndRecipients.Object, _gmmResources.Object); - await suborchrestrator.RunSubOrchestratorAsync(_context.Object, _executionContext); + await suborchrestrator.RunSubOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(LoggerFunction)), From 53dcdc1c188b60bee18cdf2fc8d3db2255704a7b Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Fri, 30 Aug 2024 11:59:52 -0700 Subject: [PATCH 0299/1479] Add isolated worker model for DestinationAttributesUpdater --- .../AttributeCacheUpdaterFunction.cs | 5 +- .../AttributeReaderFunction.cs | 5 +- .../DestinationReaderFunction.cs | 6 +- .../Activity/Logger/LoggerFunction.cs | 5 +- .../DestinationAttributesUpdater.csproj | 10 ++- .../Orchestrator/OrchestratorFunction.cs | 8 +- .../Function/Program.cs | 73 +++++++++++++++++++ .../Function/Starter/StarterFunction.cs | 10 +-- .../Function/Startup.cs | 53 -------------- .../Function/host.json | 10 ++- .../Function/local.settings.json | 4 +- .../Infrastructure/compute/template.bicep | 2 +- .../OrchestratorFunctionTests.cs | 32 ++++---- 13 files changed, 128 insertions(+), 95 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeCacheUpdater/AttributeCacheUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeCacheUpdater/AttributeCacheUpdaterFunction.cs index 3fb9b403e..c48e8ea59 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeCacheUpdater/AttributeCacheUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeCacheUpdater/AttributeCacheUpdaterFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.DestinationAttributesUpdater { @@ -22,7 +21,7 @@ public AttributeCacheUpdaterFunction(ILoggingRepository loggingRepository, IDest _destinationAttributeUpdaterService = destinationAttributeUpdater ?? throw new ArgumentNullException(nameof(destinationAttributeUpdater)); } - [FunctionName(nameof(AttributeCacheUpdaterFunction))] + [Function(nameof(AttributeCacheUpdaterFunction))] public async Task UpdateAttributesAsync([ActivityTrigger] DestinationAttributes destinationAttributes) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(AttributeCacheUpdaterFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeReader/AttributeReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeReader/AttributeReaderFunction.cs index 386874819..51d6b2294 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeReader/AttributeReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeReader/AttributeReaderFunction.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.DestinationAttributesUpdater { @@ -23,7 +22,7 @@ public AttributeReaderFunction(ILoggingRepository loggingRepository, IDestinatio _destinationAttributeUpdaterService = destinationAttributeUpdater ?? throw new ArgumentNullException(nameof(destinationAttributeUpdater)); } - [FunctionName(nameof(AttributeReaderFunction))] + [Function(nameof(AttributeReaderFunction))] public async Task> GetAttributesAsync([ActivityTrigger] AttributeReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(AttributeReaderFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/DestinationReader/DestinationReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/DestinationReader/DestinationReaderFunction.cs index e18dc2d08..f5caee478 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/DestinationReader/DestinationReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/DestinationReader/DestinationReaderFunction.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using System.Linq; namespace Hosts.DestinationAttributesUpdater { @@ -23,7 +23,7 @@ public DestinationReaderFunction(ILoggingRepository loggingRepository, IDestinat _destinationAttributeUpdater = destinationAttributeUpdater ?? throw new ArgumentNullException(nameof(destinationAttributeUpdater)); } - [FunctionName(nameof(DestinationReaderFunction))] + [Function(nameof(DestinationReaderFunction))] public async Task> GetDestinationsAsync([ActivityTrigger] string destinationType) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(DestinationReaderFunction)} function started"}, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/Logger/LoggerFunction.cs index c5af0c861..004c244fd 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/Logger/LoggerFunction.cs @@ -2,11 +2,10 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.DestinationAttributesUpdater { @@ -19,7 +18,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.RunId },request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/DestinationAttributesUpdater.csproj b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/DestinationAttributesUpdater.csproj index cb2a7c277..df7b37b21 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/DestinationAttributesUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/DestinationAttributesUpdater.csproj @@ -2,11 +2,17 @@ net8.0 v4 + Exe + enabled - - + + + + + + diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Orchestrator/OrchestratorFunction.cs index ab1c2363c..06bbe16cd 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Orchestrator/OrchestratorFunction.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.DestinationAttributesUpdater { @@ -21,8 +21,8 @@ public OrchestratorFunction() } - [FunctionName(nameof(OrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(OrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { await context.CallActivityAsync(nameof(LoggerFunction), diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Program.cs new file mode 100644 index 000000000..3ca5245e7 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Program.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +using Azure.Identity; +using Common.DependencyInjection; +using Hosts.FunctionBase; +using Microsoft.ApplicationInsights; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Graph; +using Repositories.Contracts; +using Repositories.GraphGroups; +using Repositories.TeamsChannel; +using Services; +using Services.Contracts; +using System; + +namespace Hosts.DestinationAttributesUpdater +{ + + public class Program + { + public static void Main(string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = "DestinationAttributesUpdater"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + services + .AddGraphAPIClient() + .AddScoped(); + + services.AddTransient((services) => + { + var loggingRepository = services.GetRequiredService(); + var telemetryClient = services.GetRequiredService(); + + var configuration = services.GetService(); + var graphCredentials = services.GetService>().Value; + graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; + graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; + var graphServiceClient = new GraphServiceClient(FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials)); + + return new TeamsChannelRepository(loggingRepository, graphServiceClient, telemetryClient); + }); + + services.AddScoped(); + }) + .Build(); + + host.Run(); + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Starter/StarterFunction.cs index 6a8b4e6e4..e52a03192 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Starter/StarterFunction.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask.Client; namespace Hosts.DestinationAttributesUpdater { @@ -19,13 +19,13 @@ public StarterFunction(ILoggingRepository loggingRepository) } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task Run( [TimerTrigger("%destinationAttributesUpdaterSchedule%")] TimerInfo myTimer, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient client) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started" }, VerbosityLevel.DEBUG); - await starter.StartNewAsync(nameof(OrchestratorFunction), null); + await client.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), null); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed" }, VerbosityLevel.DEBUG); } } diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Startup.cs deleted file mode 100644 index 3a5214bc0..000000000 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Startup.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.FunctionBase; -using Microsoft.ApplicationInsights; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.Graph; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphGroups; -using Repositories.TeamsChannel; -using Services; -using Services.Contracts; - -[assembly: FunctionsStartup(typeof(Hosts.DestinationAttributesUpdater.Startup))] - -namespace Hosts.DestinationAttributesUpdater -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(DestinationAttributesUpdater); - protected override string DryRunSettingName => string.Empty; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services - .AddGraphAPIClient() - .AddScoped(); - - builder.Services.AddTransient((services) => - { - var loggingRepository = services.GetRequiredService(); - var telemetryClient = services.GetRequiredService(); - - var configuration = services.GetService(); - var graphCredentials = services.GetService>().Value; - graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; - graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; - var graphServiceClient = new GraphServiceClient(FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials)); - - return new TeamsChannelRepository(loggingRepository, graphServiceClient, telemetryClient); - }); - - builder.Services.AddScoped(); - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/host.json b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/host.json index 34fced982..1e6a74f99 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/host.json @@ -1,4 +1,12 @@ { "version": "2.0", - "functionTimeout": "00:10:00" + "functionTimeout": "00:10:00", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/local.settings.json index 077404aaa..fe7a46746 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/local.settings.json @@ -1,3 +1,5 @@ { - + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" + } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep index 3663193c5..b7908b788 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep @@ -117,7 +117,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs index 46b20b3cc..e218039cc 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Hosts.DestinationAttributesUpdater; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Models.Helpers; @@ -23,7 +23,7 @@ public class OrchestratorFunctionTests private Mock _mockDestinationAttributeUpdaterService; private Mock _mockLoggingRepository; - Mock _context; + Mock _context; private const string GroupMembershipDestinationType = "GroupMembership"; @@ -44,37 +44,37 @@ public void InitializeTest() { _mockLoggingRepository = new Mock(); _mockDestinationAttributeUpdaterService = new Mock(); - _context = new Mock(); + _context = new Mock(); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(AttributeCacheUpdaterFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(AttributeCacheUpdaterFunction)), It.IsAny(),It.IsAny())) + .Callback(async (name, request, options) => { await CallAttributeCacheUpdaterAsync(); }); - _context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(AttributeReaderFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(AttributeReaderFunction)), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { _attributeReaderResponse = await CallAttributeReaderAsync(request as AttributeReaderRequest); }) .ReturnsAsync(() => _attributeReaderResponse); - _context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(DestinationReaderFunction)), It.Is(x => x == GroupMembershipDestinationType))) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(DestinationReaderFunction)), It.Is(x => x == GroupMembershipDestinationType), It.IsAny())) + .Callback(async (name, request, options) => { _destinationReaderResponse = await CallDestinationReaderAsync(request as string); }) .ReturnsAsync(() => _destinationReaderResponse); - _context.Setup(x => x.CallActivityAsync>(It.Is(x => x == nameof(DestinationReaderFunction)), It.Is(x => x == TeamsChannelMemberhsipDestinationType))) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(DestinationReaderFunction)), It.Is(x => x == TeamsChannelMemberhsipDestinationType), It.IsAny())) + .Callback(async (name, request, options) => { _destinationReaderResponse = new List<(string Destination, Guid JobId)>(); }) .ReturnsAsync(() => _destinationReaderResponse); - _context.Setup(x => x.CallActivityAsync(It.Is(x => x == nameof(LoggerFunction)), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(LoggerFunction)), It.IsAny(),null)) + .Callback(async (name, request, options) => { await CallLoggerFunctionAsync(request as LoggerRequest); }); @@ -107,11 +107,11 @@ public async Task TestSuccessfulRun() var orchestrator = new OrchestratorFunction(); await orchestrator.RunOrchestratorAsync(_context.Object); - _context.Verify(x => x.CallActivityAsync>(It.Is(x => x == nameof(DestinationReaderFunction)), It.IsAny()), + _context.Verify(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(DestinationReaderFunction)), It.IsAny(),null), Times.Exactly(2)); - _context.Verify(x => x.CallActivityAsync>(It.Is(x => x == nameof(AttributeReaderFunction)), It.Is(x => DeserializeDestination(x.Destinations[0].Destination).Value.ObjectId == DeserializeDestination(destinations[0].Destination).Value.ObjectId)), + _context.Verify(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(AttributeReaderFunction)), It.Is(x => DeserializeDestination(x.Destinations[0].Destination).Value.ObjectId == DeserializeDestination(destinations[0].Destination).Value.ObjectId),null), Times.Once()); - _context.Verify(x => x.CallActivityAsync(It.Is(x => x == nameof(AttributeCacheUpdaterFunction)), It.Is(x => x == destinationAttributes1)), + _context.Verify(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(AttributeCacheUpdaterFunction)), It.Is(x => x == destinationAttributes1),null), Times.Once()); } From 06e2d2f54f2c67cf9104d1fe955cfd1cbdb62ee5 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 3 Sep 2024 13:12:27 -0700 Subject: [PATCH 0300/1479] Change destination response --- .../AttributeReader/AttributeReaderRequest.cs | 2 +- .../DestinationReaderFunction.cs | 2 +- .../Orchestrator/OrchestratorFunction.cs | 2 +- .../IDestinationAttributesUpdaterService.cs | 4 ++-- .../DestinationAttributesUpdaterServiceTests.cs | 4 ++-- .../Services.Tests/OrchestratorFunctionTests.cs | 16 ++++++++-------- .../DestinationAttributesUpdaterService.cs | 11 +++++------ .../Models/DestinationReaderResponse.cs | 14 ++++++++++++++ 8 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 Service/GroupMembershipManagement/Models/DestinationReaderResponse.cs diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeReader/AttributeReaderRequest.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeReader/AttributeReaderRequest.cs index 50b04e417..78f45155c 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeReader/AttributeReaderRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/AttributeReader/AttributeReaderRequest.cs @@ -9,7 +9,7 @@ namespace Hosts.DestinationAttributesUpdater { public class AttributeReaderRequest { - public List<(string Destination, Guid JobId)> Destinations { get; set; } + public List Destinations { get; set; } public string DestinationType { get; set; } } } diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/DestinationReader/DestinationReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/DestinationReader/DestinationReaderFunction.cs index f5caee478..fd9f31173 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/DestinationReader/DestinationReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Activity/DestinationReader/DestinationReaderFunction.cs @@ -24,7 +24,7 @@ public DestinationReaderFunction(ILoggingRepository loggingRepository, IDestinat } [Function(nameof(DestinationReaderFunction))] - public async Task> GetDestinationsAsync([ActivityTrigger] string destinationType) + public async Task> GetDestinationsAsync([ActivityTrigger] string destinationType) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(DestinationReaderFunction)} function started"}, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Orchestrator/OrchestratorFunction.cs index 06bbe16cd..d8fecefa4 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Function/Orchestrator/OrchestratorFunction.cs @@ -39,7 +39,7 @@ await context.CallActivityAsync(nameof(LoggerFunction), foreach (var destinationType in destinationTypes) { - var destinationsList = await context.CallActivityAsync>(nameof(DestinationReaderFunction), destinationType); + var destinationsList = await context.CallActivityAsync>(nameof(DestinationReaderFunction), destinationType); int index = 0; while (index < destinationsList.Count) diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Contracts/IDestinationAttributesUpdaterService.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Contracts/IDestinationAttributesUpdaterService.cs index 1f597acfd..a31f2543e 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Contracts/IDestinationAttributesUpdaterService.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Contracts/IDestinationAttributesUpdaterService.cs @@ -7,8 +7,8 @@ namespace Services.Contracts { public interface IDestinationAttributesUpdaterService { - Task> GetDestinationsAsync(string destinationType); - Task> GetBulkDestinationAttributesAsync(List<(string Destination, Guid JobId)> destinations, string destinationType); + Task> GetDestinationsAsync(string destinationType); + Task> GetBulkDestinationAttributesAsync(List destinations, string destinationType); Task UpdateAttributes(DestinationAttributes destinationAttributes); } } diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/DestinationAttributesUpdaterServiceTests.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/DestinationAttributesUpdaterServiceTests.cs index 30625dfdd..c20366691 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/DestinationAttributesUpdaterServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/DestinationAttributesUpdaterServiceTests.cs @@ -55,7 +55,7 @@ public async Task TestGetGroupDestinations() var response = await _destinationAttributeUpdaterService.GetDestinationsAsync(GroupMembership); - Assert.AreEqual(response.First().JobId, job.Id); + Assert.AreEqual(response.First().GroupId, job.Id); Assert.AreEqual(response.First().Destination.GetType(), typeof(string)); } @@ -78,7 +78,7 @@ public async Task TestGetBulkDestinationAttributes() _mockGraphGroupRepository.Setup(x => x.GetGroupNamesAsync(It.IsAny>())).ReturnsAsync(new Dictionary() { { destination.Value.ObjectId, "name"} }); _mockGraphGroupRepository.Setup(x => x.GetDestinationOwnersAsync(It.IsAny>())).ReturnsAsync(new Dictionary>() { { destination.Value.ObjectId, new List { owner } } }); - var response = await _destinationAttributeUpdaterService.GetBulkDestinationAttributesAsync(new List<(string Destination, Guid TableId)> { (serializedDestination, tableId) }, GroupMembership); + var response = await _destinationAttributeUpdaterService.GetBulkDestinationAttributesAsync(new List { new DestinationReaderResponse { Destination = serializedDestination, GroupId = tableId } }, GroupMembership); Assert.AreEqual(response.First().Id, tableId); } diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs index e218039cc..b3838d4a9 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs @@ -30,7 +30,7 @@ public class OrchestratorFunctionTests private const string TeamsChannelMemberhsipDestinationType = "TeamsChannelMembership"; private List _attributeReaderResponse; - List<(string Destination, Guid JobId)> _destinationReaderResponse; + List _destinationReaderResponse; private const string EmailSubject = "EmailSubject"; @@ -59,17 +59,17 @@ public void InitializeTest() }) .ReturnsAsync(() => _attributeReaderResponse); - _context.Setup(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(DestinationReaderFunction)), It.Is(x => x == GroupMembershipDestinationType), It.IsAny())) + _context.Setup(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(DestinationReaderFunction)), It.Is(x => x == GroupMembershipDestinationType), It.IsAny())) .Callback(async (name, request, options) => { _destinationReaderResponse = await CallDestinationReaderAsync(request as string); }) .ReturnsAsync(() => _destinationReaderResponse); - _context.Setup(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(DestinationReaderFunction)), It.Is(x => x == TeamsChannelMemberhsipDestinationType), It.IsAny())) + _context.Setup(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(DestinationReaderFunction)), It.Is(x => x == TeamsChannelMemberhsipDestinationType), It.IsAny())) .Callback(async (name, request, options) => { - _destinationReaderResponse = new List<(string Destination, Guid JobId)>(); + _destinationReaderResponse = new List(); }) .ReturnsAsync(() => _destinationReaderResponse); @@ -93,7 +93,7 @@ public async Task TestSuccessfulRun() var destination1 = new DestinationObject() { Type = "GroupMembership", Value = new GroupDestinationValue() { ObjectId = Guid.NewGuid() } }; var serializedDestination1 = SerializeDestination(destination1); var jobId1 = Guid.NewGuid(); - var destinations = new List<(string Destination, Guid JobId)>() { (serializedDestination1, jobId1) }; + var destinations = new List() { new DestinationReaderResponse { Destination = serializedDestination1, GroupId = jobId1 } }; _mockDestinationAttributeUpdaterService.Setup(x => x.GetDestinationsAsync(It.IsAny())).ReturnsAsync(() => destinations); var destinationAttributes1 = new DestinationAttributes @@ -102,12 +102,12 @@ public async Task TestSuccessfulRun() Name = "Name", Owners = new List() { Guid.NewGuid() } }; - _mockDestinationAttributeUpdaterService.Setup(x => x.GetBulkDestinationAttributesAsync(It.IsAny>(), It.IsAny())).ReturnsAsync(() => new List() { destinationAttributes1 }); + _mockDestinationAttributeUpdaterService.Setup(x => x.GetBulkDestinationAttributesAsync(It.IsAny>(), It.IsAny())).ReturnsAsync(() => new List() { destinationAttributes1 }); var orchestrator = new OrchestratorFunction(); await orchestrator.RunOrchestratorAsync(_context.Object); - _context.Verify(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(DestinationReaderFunction)), It.IsAny(),null), + _context.Verify(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(DestinationReaderFunction)), It.IsAny(),null), Times.Exactly(2)); _context.Verify(x => x.CallActivityAsync>(It.Is(x => x.ToString() == nameof(AttributeReaderFunction)), It.Is(x => DeserializeDestination(x.Destinations[0].Destination).Value.ObjectId == DeserializeDestination(destinations[0].Destination).Value.ObjectId),null), Times.Once()); @@ -128,7 +128,7 @@ private async Task> CallAttributeReaderAsync(Attribu return response; } - private async Task> CallDestinationReaderAsync(string destinationType) + private async Task> CallDestinationReaderAsync(string destinationType) { var DestinationReaderFunction = new DestinationReaderFunction(_mockLoggingRepository.Object, _mockDestinationAttributeUpdaterService.Object); var response = await DestinationReaderFunction.GetDestinationsAsync(destinationType); diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/DestinationAttributesUpdaterService.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/DestinationAttributesUpdaterService.cs index 91e34f76d..e7d440326 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/DestinationAttributesUpdaterService.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/DestinationAttributesUpdaterService.cs @@ -33,10 +33,10 @@ public DestinationAttributesUpdaterService( _destinationObjectSerializerOptions = new JsonSerializerOptions { Converters = { new DestinationValueConverter() } }; } - public async Task> GetDestinationsAsync(string destinationType) + public async Task> GetDestinationsAsync(string destinationType) { var jobs = await _databaseSyncJobsRepository.GetSyncJobsByDestinationAsync(destinationType); - var destinations = new List<(string Destination, Guid JobId)>(); + var destinations = new List(); foreach (var job in jobs) { @@ -71,19 +71,18 @@ public DestinationAttributesUpdaterService( } var serializedDestination = JsonSerializer.Serialize(destination, _destinationObjectSerializerOptions); - - destinations.Add((serializedDestination, job.Id)); + destinations.Add(new DestinationReaderResponse { Destination = serializedDestination, GroupId = job.Id }); } return destinations; } - public async Task> GetBulkDestinationAttributesAsync(List<(string Destination, Guid JobId)> destinations, string destinationType) + public async Task> GetBulkDestinationAttributesAsync(List destinations, string destinationType) { var destinationAttributesList = new List(); List<(DestinationObject? Destination, Guid JobId)> destinationObjectsMap = destinations - .Select(d => (JsonSerializer.Deserialize(d.Destination, _destinationObjectSerializerOptions), d.JobId)) + .Select(d => (JsonSerializer.Deserialize(d.Destination, _destinationObjectSerializerOptions), d.GroupId)) .ToList(); var destinationObjects = destinationObjectsMap.Select(d => d.Destination).ToList(); diff --git a/Service/GroupMembershipManagement/Models/DestinationReaderResponse.cs b/Service/GroupMembershipManagement/Models/DestinationReaderResponse.cs new file mode 100644 index 000000000..68135b618 --- /dev/null +++ b/Service/GroupMembershipManagement/Models/DestinationReaderResponse.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Data.SqlTypes; +using System; + +namespace Models +{ + public class DestinationReaderResponse + { + public Guid GroupId { get; set; } + public string Destination { get; set; } + } +} From 08c3390bc040e9e3b8d17cbefeb3f5f494b4eba1 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 10 Sep 2024 13:20:32 -0700 Subject: [PATCH 0301/1479] Using JobId instead of group id in the DestinationReaderResponse --- .../DestinationAttributesUpdaterServiceTests.cs | 4 ++-- .../Services.Tests/OrchestratorFunctionTests.cs | 2 +- .../Services/DestinationAttributesUpdaterService.cs | 4 ++-- .../Models/DestinationReaderResponse.cs | 2 +- vsts-cicd.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/DestinationAttributesUpdaterServiceTests.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/DestinationAttributesUpdaterServiceTests.cs index c20366691..ecedd099d 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/DestinationAttributesUpdaterServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/DestinationAttributesUpdaterServiceTests.cs @@ -55,7 +55,7 @@ public async Task TestGetGroupDestinations() var response = await _destinationAttributeUpdaterService.GetDestinationsAsync(GroupMembership); - Assert.AreEqual(response.First().GroupId, job.Id); + Assert.AreEqual(response.First().JobId, job.Id); Assert.AreEqual(response.First().Destination.GetType(), typeof(string)); } @@ -78,7 +78,7 @@ public async Task TestGetBulkDestinationAttributes() _mockGraphGroupRepository.Setup(x => x.GetGroupNamesAsync(It.IsAny>())).ReturnsAsync(new Dictionary() { { destination.Value.ObjectId, "name"} }); _mockGraphGroupRepository.Setup(x => x.GetDestinationOwnersAsync(It.IsAny>())).ReturnsAsync(new Dictionary>() { { destination.Value.ObjectId, new List { owner } } }); - var response = await _destinationAttributeUpdaterService.GetBulkDestinationAttributesAsync(new List { new DestinationReaderResponse { Destination = serializedDestination, GroupId = tableId } }, GroupMembership); + var response = await _destinationAttributeUpdaterService.GetBulkDestinationAttributesAsync(new List { new DestinationReaderResponse { Destination = serializedDestination, JobId = tableId } }, GroupMembership); Assert.AreEqual(response.First().Id, tableId); } diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs index b3838d4a9..d7c9155cf 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services.Tests/OrchestratorFunctionTests.cs @@ -93,7 +93,7 @@ public async Task TestSuccessfulRun() var destination1 = new DestinationObject() { Type = "GroupMembership", Value = new GroupDestinationValue() { ObjectId = Guid.NewGuid() } }; var serializedDestination1 = SerializeDestination(destination1); var jobId1 = Guid.NewGuid(); - var destinations = new List() { new DestinationReaderResponse { Destination = serializedDestination1, GroupId = jobId1 } }; + var destinations = new List() { new DestinationReaderResponse { Destination = serializedDestination1, JobId = jobId1 } }; _mockDestinationAttributeUpdaterService.Setup(x => x.GetDestinationsAsync(It.IsAny())).ReturnsAsync(() => destinations); var destinationAttributes1 = new DestinationAttributes diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/DestinationAttributesUpdaterService.cs b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/DestinationAttributesUpdaterService.cs index e7d440326..a5c51d654 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/DestinationAttributesUpdaterService.cs +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Services/DestinationAttributesUpdaterService.cs @@ -71,7 +71,7 @@ public async Task> GetDestinationsAsync(string d } var serializedDestination = JsonSerializer.Serialize(destination, _destinationObjectSerializerOptions); - destinations.Add(new DestinationReaderResponse { Destination = serializedDestination, GroupId = job.Id }); + destinations.Add(new DestinationReaderResponse { Destination = serializedDestination, JobId = job.Id }); } return destinations; @@ -82,7 +82,7 @@ public async Task> GetBulkDestinationAttributesAsync var destinationAttributesList = new List(); List<(DestinationObject? Destination, Guid JobId)> destinationObjectsMap = destinations - .Select(d => (JsonSerializer.Deserialize(d.Destination, _destinationObjectSerializerOptions), d.GroupId)) + .Select(d => (JsonSerializer.Deserialize(d.Destination, _destinationObjectSerializerOptions), d.JobId)) .ToList(); var destinationObjects = destinationObjectsMap.Select(d => d.Destination).ToList(); diff --git a/Service/GroupMembershipManagement/Models/DestinationReaderResponse.cs b/Service/GroupMembershipManagement/Models/DestinationReaderResponse.cs index 68135b618..1093087f1 100644 --- a/Service/GroupMembershipManagement/Models/DestinationReaderResponse.cs +++ b/Service/GroupMembershipManagement/Models/DestinationReaderResponse.cs @@ -8,7 +8,7 @@ namespace Models { public class DestinationReaderResponse { - public Guid GroupId { get; set; } + public Guid JobId { get; set; } public string Destination { get; set; } } } diff --git a/vsts-cicd.yml b/vsts-cicd.yml index 773b8e61a..ae496ca9e 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -68,7 +68,7 @@ stages: coverageThreshold: 69 - function: name: 'DestinationAttributesUpdater' - coverageThreshold: 64 + coverageThreshold: 62 - function: name: 'GroupMembershipObtainer' coverageThreshold: 83 From 33fe71f358f248fa78c795e19865427f97e39fb3 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 29 Aug 2024 15:11:16 -0700 Subject: [PATCH 0302/1479] Add isolated worker model for azuer user --- .../Function/AzureUserReader.csproj | 10 ++- .../Function/AzureUserReader.sln | 6 ++ .../Orchestrator/OrchestratorFunction.cs | 8 +- .../PersonnelNumberReaderFunction.cs | 5 +- .../Hosts/AzureUserReader/Function/Program.cs | 60 ++++++++++++++ .../Function/Starter/StarterFunction.cs | 48 ++++++----- .../Hosts/AzureUserReader/Function/Startup.cs | 38 --------- .../UploadUsers/UploadUsersFunction.cs | 5 +- .../UserCreator/AzureUserCreatorFunction.cs | 5 +- .../UserCreatorSubOrchestratorFunction.cs | 8 +- .../UserReader/AzureUserReaderFunction.cs | 5 +- .../UserReaderSubOrchestratorFunction.cs | 8 +- .../Services.Tests/OrchestratorTests.cs | 36 ++++----- .../Services.Tests/Services.Tests.csproj | 1 + .../Services.Tests/StarterFunctionTests.cs | 62 ++++++-------- .../UserCreatorSubOrchestratorTests.cs | 8 +- .../Repositories.Mocks/MockHttpRequestData.cs | 81 +++++++++++++++++++ .../Repositories.Mocks.csproj | 1 + Service/GroupMembershipManagement/global.json | 2 +- 19 files changed, 252 insertions(+), 145 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Startup.cs create mode 100644 Service/GroupMembershipManagement/Repositories.Mocks/MockHttpRequestData.cs diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.csproj b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.csproj index 69b569bf2..6f0c53f7e 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.csproj @@ -4,11 +4,17 @@ v4 Hosts.AzureUserReader Hosts.AzureUserReader + Exe + enabled - - + + + + + + diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.sln b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.sln index 4623ff4ee..964aec201 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.sln +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/AzureUserReader.sln @@ -29,6 +29,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services.Tests", "..\Servic EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\..\..\Models\Models.csproj", "{B0DF872A-A0A8-460B-92FB-12400658D173}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Mocks", "..\..\..\Repositories.Mocks\Repositories.Mocks.csproj", "{FCA1D3E0-1721-418D-8497-D1E7128C1F25}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,6 +89,10 @@ Global {B0DF872A-A0A8-460B-92FB-12400658D173}.Debug|Any CPU.Build.0 = Debug|Any CPU {B0DF872A-A0A8-460B-92FB-12400658D173}.Release|Any CPU.ActiveCfg = Release|Any CPU {B0DF872A-A0A8-460B-92FB-12400658D173}.Release|Any CPU.Build.0 = Release|Any CPU + {FCA1D3E0-1721-418D-8497-D1E7128C1F25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCA1D3E0-1721-418D-8497-D1E7128C1F25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCA1D3E0-1721-418D-8497-D1E7128C1F25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCA1D3E0-1721-418D-8497-D1E7128C1F25}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Orchestrator/OrchestratorFunction.cs index c61e3b1f9..abd7eab4d 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Orchestrator/OrchestratorFunction.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.AzureUserReader { @@ -21,9 +21,9 @@ public OrchestratorFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(OrchestratorFunction))] + [Function(nameof(OrchestratorFunction))] public async Task RunOrchestrator( - [OrchestrationTrigger] IDurableOrchestrationContext context) + [OrchestrationTrigger] TaskOrchestrationContext context) { if (!context.IsReplaying) _ = _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(OrchestratorFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/PersonnelNumberReader/PersonnelNumberReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/PersonnelNumberReader/PersonnelNumberReaderFunction.cs index b07b3846e..d74b959c6 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/PersonnelNumberReader/PersonnelNumberReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/PersonnelNumberReader/PersonnelNumberReaderFunction.cs @@ -2,13 +2,12 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureUserReader { @@ -23,7 +22,7 @@ public PersonnelNumberReaderFunction(IAzureUserReaderService azureUserReaderServ _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(PersonnelNumberReaderFunction))] + [Function(nameof(PersonnelNumberReaderFunction))] public async Task> GetPersonnelNumbersAsync([ActivityTrigger] AzureUserReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(PersonnelNumberReaderFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs new file mode 100644 index 000000000..d15c29fd2 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.Extensions.Hosting; +using Azure.Core; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using DIConcreteTypes; +using Hosts.FunctionBase; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Repositories.Contracts; +using Repositories.Contracts.InjectConfig; +using Repositories.ServiceBusQueue; +using Services; +using Services.Contracts; +using System; +using Azure.Identity; +using Repositories.EntityFramework; +using Repositories.NotificationsRepository; +using Repositories.GraphAzureADUsers; + +namespace Hosts.AzureUserReader +{ + + public class Program + { + public static void Main(string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + services.AddGraphAPIClient(); + + services.AddSingleton(services => + new StorageAccountSecret(CommonServices.GetValueOrThrowBase("storageAccountConnectionString"))); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + }) + .Build(); + host.Run(); + } + } +} + diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Starter/StarterFunction.cs index 5c67f0405..09eaf498f 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Starter/StarterFunction.cs @@ -2,15 +2,17 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Extensions.Http; using Newtonsoft.Json; using Repositories.Contracts; using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask.Client; +using Microsoft.Azure.Functions.Worker.Http; +using System.IO; +using Microsoft.AspNetCore.Http; namespace Hosts.AzureUserReader { @@ -23,24 +25,24 @@ public StarterFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(StarterFunction))] - public async Task HttpStart( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestMessage req, - [DurableClient] IDurableOrchestrationClient starter) + [Function(nameof(StarterFunction))] + public async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, + [DurableClient] DurableTaskClient client) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started" }, VerbosityLevel.DEBUG); - HttpResponseMessage response; + HttpResponseData response; var result = await ValidateRequestAsync(req); if (result.StatusCode == HttpStatusCode.OK) { - var instanceId = await starter.StartNewAsync(nameof(OrchestratorFunction), result.Request); - response = starter.CreateCheckStatusResponse(req, instanceId); + var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), result.Request); + response = client.CreateCheckStatusResponse(req, instanceId); } else { - response = new HttpResponseMessage { StatusCode = result.StatusCode }; + response = req.CreateResponse(result.StatusCode); } await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed" }, VerbosityLevel.DEBUG); @@ -48,17 +50,17 @@ public async Task HttpStart( return response; } - private async Task<(HttpStatusCode StatusCode, AzureUserReaderRequest Request)> ValidateRequestAsync(HttpRequestMessage request) + private async Task<(HttpStatusCode StatusCode, AzureUserReaderRequest Request)> ValidateRequestAsync(HttpRequestData request) { AzureUserReaderRequest userReaderRequest = null; try { - var content = await request.Content.ReadAsStringAsync(); + var content = await new StreamReader(request.Body).ReadToEndAsync(); if (string.IsNullOrWhiteSpace(content)) { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Request body was not provided." }); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = "Request body was not provided." }); return (HttpStatusCode.BadRequest, null); } @@ -66,7 +68,7 @@ public async Task HttpStart( if (string.IsNullOrWhiteSpace(userReaderRequest.ContainerName) || string.IsNullOrWhiteSpace(userReaderRequest.BlobPath)) { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Request body is not valid." }); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = "Request body is not valid." }); return (HttpStatusCode.BadRequest, null); } @@ -75,22 +77,26 @@ public async Task HttpStart( if (userReaderRequest.TenantInformation == null || string.IsNullOrWhiteSpace(userReaderRequest.TenantInformation.TenantDomain) || string.IsNullOrWhiteSpace(userReaderRequest.TenantInformation.EmailPrefix) || - string.IsNullOrWhiteSpace(userReaderRequest.TenantInformation.CountryCode) - ) + string.IsNullOrWhiteSpace(userReaderRequest.TenantInformation.CountryCode)) { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Request body is not valid. TenantInformation is missing." }); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = "Request body is not valid. TenantInformation is missing." }); return (HttpStatusCode.BadRequest, null); } } } - catch (Exception ex) when (ex.GetType() == typeof(JsonReaderException) || ex.GetType() == typeof(JsonSerializationException)) + catch (JsonReaderException) { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Request body is not valid." }); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = "Request body is not valid." }); + return (HttpStatusCode.BadRequest, null); + } + catch (JsonSerializationException) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = "Request body is not valid." }); return (HttpStatusCode.BadRequest, null); } catch (Exception ex) { - await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Unexpected error occured when processing the request.\n{ex}" }); + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Unexpected error occurred when processing the request.\n{ex}" }); return (HttpStatusCode.InternalServerError, null); } diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Startup.cs deleted file mode 100644 index c2599203a..000000000 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Startup.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using Common.DependencyInjection; -using DIConcreteTypes; -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection; -using Repositories.Contracts; -using Repositories.Contracts.InjectConfig; -using Repositories.GraphAzureADUsers; -using Services; -using Services.Contracts; - -[assembly: FunctionsStartup(typeof(Hosts.AzureUserReader.Startup))] - -namespace Hosts.AzureUserReader -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(AzureUserReader); - protected override string DryRunSettingName => string.Empty; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddGraphAPIClient(); - - builder.Services.AddSingleton(services => - new StorageAccountSecret(GetValueOrThrow("storageAccountConnectionString"))); - - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - } - } -} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UploadUsers/UploadUsersFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UploadUsers/UploadUsersFunction.cs index 0c9dc85ef..06e7375f1 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UploadUsers/UploadUsersFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UploadUsers/UploadUsersFunction.cs @@ -2,14 +2,13 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services.Contracts; using Services.Entities; using System; using System.IO; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureUserReader { @@ -24,7 +23,7 @@ public UploadUsersFunction(IAzureUserReaderService azureUserReaderService, ILogg _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(UploadUsersFunction))] + [Function(nameof(UploadUsersFunction))] public async Task UploadUsersMemberIdAsync([ActivityTrigger] UploadUsersRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(UploadUsersFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs index 9a1c639f1..5b33448e5 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreator/AzureUserCreatorFunction.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureUserReader { @@ -23,7 +22,7 @@ public AzureUserCreatorFunction(IGraphUserRepository graphUserRepository, ILoggi _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(AzureUserCreatorFunction))] + [Function(nameof(AzureUserCreatorFunction))] public async Task> AddUsersAsync([ActivityTrigger] AzureUserCreatorRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(AzureUserCreatorFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs index df9043387..b219bd0f1 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserCreatorSubOrchestrator/UserCreatorSubOrchestratorFunction.cs @@ -5,10 +5,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.AzureUserReader { @@ -21,9 +21,9 @@ public UserCreatorSubOrchestratorFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(UserCreatorSubOrchestratorFunction))] + [Function(nameof(UserCreatorSubOrchestratorFunction))] public async Task> CreateUsersAsync( - [OrchestrationTrigger] IDurableOrchestrationContext context) + [OrchestrationTrigger] TaskOrchestrationContext context) { var request = context.GetInput(); var profiles = new List(); diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserReader/AzureUserReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserReader/AzureUserReaderFunction.cs index d95f12e0b..289370a14 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserReader/AzureUserReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserReader/AzureUserReaderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.AzureUserReader { @@ -22,7 +21,7 @@ public AzureUserReaderFunction(IGraphUserRepository graphUserRepository, ILoggin _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(AzureUserReaderFunction))] + [Function(nameof(AzureUserReaderFunction))] public async Task> GetUsersAsync([ActivityTrigger] List personnelNumbers) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(AzureUserReaderFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserReaderSubOrchestrator/UserReaderSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserReaderSubOrchestrator/UserReaderSubOrchestratorFunction.cs index 96f399af5..30a527048 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserReaderSubOrchestrator/UserReaderSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/UserReaderSubOrchestrator/UserReaderSubOrchestratorFunction.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.AzureUserReader { @@ -21,9 +21,9 @@ public UserReaderSubOrchestratorFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(UserReaderSubOrchestratorFunction))] + [Function(nameof(UserReaderSubOrchestratorFunction))] public async Task> RunOrchestrator( - [OrchestrationTrigger] IDurableOrchestrationContext context) + [OrchestrationTrigger] TaskOrchestrationContext context) { if (!context.IsReplaying) _ = _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(UserReaderSubOrchestratorFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/OrchestratorTests.cs index 723b5864c..609e2abd5 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/OrchestratorTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Hosts.AzureUserReader; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Models; @@ -11,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.DurableTask; namespace Services.Tests { @@ -21,7 +21,7 @@ public class OrchestratorTests public async Task RunOrchestratorValidTestAsync() { var loggingRepository = new Mock(); - var context = new Mock(); + var context = new Mock(); var request = new AzureUserReaderRequest { BlobPath = "blob/path/blob.csv", @@ -40,9 +40,9 @@ public async Task RunOrchestratorValidTestAsync() loggingRepository.Setup(x => x.LogMessageAsync(It.IsAny(), VerbosityLevel.DEBUG, It.IsAny(), It.IsAny())); context.Setup(x => x.GetInput()).Returns(request); - context.Setup(x => x.CallActivityAsync>(It.IsAny(), It.IsAny())).ReturnsAsync(personnelNumbers); - context.Setup(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny>())) - .Callback((name, request) => + context.Setup(x => x.CallActivityAsync>(It.IsAny(), It.IsAny(),null)).ReturnsAsync(personnelNumbers); + context.Setup(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny>(), It.IsAny())) + .Callback((name, request, options) => { var profiles = new List(); var personnelNumbers = request as List; @@ -69,8 +69,8 @@ public async Task RunOrchestratorValidTestAsync() var fullPages = (int)Math.Truncate(pages); var totalPages = (pages - fullPages) == 0 ? fullPages : fullPages + 1; - context.Verify(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny>()), Times.Once()); - context.Verify(x => x.CallActivityAsync(It.IsAny(), It.IsAny()), Times.Once()); + context.Verify(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny>(), null), Times.Once()); + context.Verify(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null), Times.Once()); loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.StartsWith($"{nameof(OrchestratorFunction)} function completed")), VerbosityLevel.DEBUG, @@ -82,7 +82,7 @@ public async Task RunOrchestratorValidTestAsync() public async Task CreateNewUsers() { var loggingRepository = new Mock(); - var context = new Mock(); + var context = new Mock(); var request = new AzureUserReaderRequest { BlobPath = "blob/path/blob.csv", @@ -123,9 +123,9 @@ public async Task CreateNewUsers() loggingRepository.Setup(x => x.LogMessageAsync(It.IsAny(), VerbosityLevel.DEBUG, It.IsAny(), It.IsAny())); context.Setup(x => x.GetInput()).Returns(request); - context.Setup(x => x.CallActivityAsync>(It.IsAny(), It.IsAny())).ReturnsAsync(allPersonnelNumbers); - context.Setup(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny>())) - .Callback((name, request) => + context.Setup(x => x.CallActivityAsync>(It.IsAny(), It.IsAny(), null)).ReturnsAsync(allPersonnelNumbers); + context.Setup(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny>(), It.IsAny())) + .Callback((name, request, options) => { var profiles = new List(); var readerRequest = request as List; @@ -149,8 +149,8 @@ public async Task CreateNewUsers() }).ReturnsAsync(() => currentPage); var usersCreatedCurrentPage = default(List); - context.Setup(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny())) - .Callback((name, request) => + context.Setup(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((name, request, options) => { var userCreatorRequest = request as AzureUserCreatorRequest; usersCreatedCurrentPage = userCreatorRequest.PersonnelNumbers.Select(x => @@ -165,8 +165,8 @@ public async Task CreateNewUsers() }) .ReturnsAsync(() => usersCreatedCurrentPage); - context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback((name, request) => + context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((name, request, options) => { usersToUploadRequest = request as UploadUsersRequest; }); @@ -174,9 +174,9 @@ public async Task CreateNewUsers() var orchestrator = new OrchestratorFunction(loggingRepository.Object); await orchestrator.RunOrchestrator(context.Object); - context.Verify(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny>()), Times.Once()); - context.Verify(x => x.CallActivityAsync(It.IsAny(), It.IsAny()), Times.Once()); - context.Verify(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny()), Times.Once()); + context.Verify(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny>(), null), Times.Once()); + context.Verify(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + context.Verify(x => x.CallSubOrchestratorAsync>(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.StartsWith($"{nameof(OrchestratorFunction)} function completed")), VerbosityLevel.DEBUG, diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/Services.Tests.csproj index 1a342dbe3..98ae539b7 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/Services.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/StarterFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/StarterFunctionTests.cs index e3ff534ea..33cf6ce2a 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/StarterFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/StarterFunctionTests.cs @@ -2,14 +2,16 @@ // Licensed under the MIT license. using Hosts.AzureUserReader; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Repositories.Contracts; using System.Net; -using System.Net.Http; -using System.Text; +using System.Threading; using System.Threading.Tasks; +using Repositories.Mocks; namespace Services.Tests { @@ -18,13 +20,13 @@ public class StarterFunctionTests { private string _instanceId; private Mock _loggerMock; - private Mock _durableClientMock; + private Mock _durableClientMock; [TestInitialize] public void SetupTest() { _instanceId = "1234567890"; - _durableClientMock = new Mock(); + _durableClientMock = new Mock(); _loggerMock = new Mock(); } @@ -35,50 +37,36 @@ public void SetupTest() [DataRow("{ 'BlobPath':'folder1/folder2/myfile.csv' }")] public async Task PostInvalidRequest(string content) { - var durableClientMock = new Mock(); + var durableClientMock = new Mock(); var loggerMock = new Mock(); - durableClientMock - .Setup(x => x.StartNewAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(_instanceId); + var mockContext = new Mock(); + var mockRequest = new MockHttpRequestData(mockContext.Object, + content); var starterFunction = new StarterFunction(loggerMock.Object); - var result = await starterFunction.HttpStart( - new HttpRequestMessage() - { - Content = new StringContent(content, Encoding.UTF8, "application/json") - }, - durableClientMock.Object - ); + var result = await starterFunction.HttpStart(mockRequest, durableClientMock.Object); Assert.AreEqual(HttpStatusCode.BadRequest, result.StatusCode); } - [TestMethod] public async Task PostValidRequest() { - _durableClientMock - .Setup(x => x.StartNewAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(_instanceId); + var instanceId = "test-instance-id"; + var durableClientMock = new Mock(); + var loggerMock = new Mock(); + + durableClientMock + .Setup(x => x.ScheduleNewOrchestrationInstanceAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(instanceId); - _durableClientMock - .Setup(x => x.CreateCheckStatusResponse(It.IsAny(), _instanceId, It.IsAny())) - .Returns(new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(string.Empty) - }); + var mockContext = new Mock(); + + var mockRequest = new MockHttpRequestData(mockContext.Object, "{ 'ContainerName':'myContainer','BlobPath':'folder1/folder2/myfile.csv'}"); + + var starterFunction = new StarterFunction(loggerMock.Object); - var starterFunction = new StarterFunction(_loggerMock.Object); - var result = await starterFunction.HttpStart( - new HttpRequestMessage() - { - Content = new StringContent( - "{ 'ContainerName':'myContainer','BlobPath':'folder1/folder2/myfile.csv'}", - Encoding.UTF8, "application/json") - }, - _durableClientMock.Object - ); + var result = await starterFunction.HttpStart(mockRequest, durableClientMock.Object); Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); } diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/UserCreatorSubOrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/UserCreatorSubOrchestratorTests.cs index fb6ce416d..b980f748b 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/UserCreatorSubOrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Services.Tests/UserCreatorSubOrchestratorTests.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Hosts.AzureUserReader; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Moq; @@ -22,7 +22,7 @@ public async Task CreateUsersAsync() { var loggingRepository = new Mock(); var graphUserRepository = new Mock(); - var context = new Mock(); + var context = new Mock(); var personnelNumbers = new List(); for (int i = 1; i <= 10000; i++) @@ -45,8 +45,8 @@ public async Task CreateUsersAsync() context.Setup(x => x.GetInput()).Returns(request); var currentProfilePage = default(List); - context.Setup(x => x.CallActivityAsync>(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => + context.Setup(x => x.CallActivityAsync>(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(async (name, request, options) => { currentProfilePage = await RunAzureUserCreatorFunctionAsync ( diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/MockHttpRequestData.cs b/Service/GroupMembershipManagement/Repositories.Mocks/MockHttpRequestData.cs new file mode 100644 index 000000000..ae03fd9b3 --- /dev/null +++ b/Service/GroupMembershipManagement/Repositories.Mocks/MockHttpRequestData.cs @@ -0,0 +1,81 @@ +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Azure.Functions.Worker; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Moq; +using System.Security.Claims; + +namespace Repositories.Mocks +{ + public class MockHttpRequestData : HttpRequestData + { + public MockHttpRequestData(FunctionContext functionContext, string content) : base(functionContext) + { + Headers = new HttpHeadersCollection(); + Body = new MemoryStream(Encoding.UTF8.GetBytes(content)); + Method = "POST"; + Url = new Uri("http://localhost"); + } + + public override Stream Body { get; } + + public override HttpHeadersCollection Headers { get; } + + public override IReadOnlyCollection Cookies { get; } = new List(); + + public override Uri Url { get; } + + public override string Method { get; } + + public override IEnumerable Identities => throw new NotImplementedException(); + + public override HttpResponseData CreateResponse() + { + var response = new MockHttpResponseData(FunctionContext) + { + StatusCode = HttpStatusCode.OK, + Body = new MemoryStream() + }; + return response; + } + } + public class MockHttpResponseData : HttpResponseData + { + public MockHttpResponseData(FunctionContext functionContext) : base(functionContext) + { + Headers = new HttpHeadersCollection(); + Body = new MemoryStream(); + } + + public override HttpStatusCode StatusCode { get; set; } + public override HttpHeadersCollection Headers { get; set; } + public override Stream Body { get; set; } + public override HttpCookies Cookies => new MockHttpCookies(); + } + public class MockHttpCookies : HttpCookies + { + private readonly Dictionary _cookies = new Dictionary(); + + public override void Append(string name, string value) + { + _cookies[name] = new HttpCookie(name, value); + } + + public override void Append(IHttpCookie cookie) + { + throw new NotImplementedException(); + } + + public override IHttpCookie CreateNew() + { + throw new NotImplementedException(); + } + } +} + + diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj b/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj index a6bd9346f..62bfdf1bb 100644 --- a/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj +++ b/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj @@ -9,6 +9,7 @@ + diff --git a/Service/GroupMembershipManagement/global.json b/Service/GroupMembershipManagement/global.json index fd07d882a..36d37e622 100644 --- a/Service/GroupMembershipManagement/global.json +++ b/Service/GroupMembershipManagement/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.303" + "version": "8.0.401" } } From c829b6c7053f3675e0c0bb4547f1b1f2a96f0e02 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 3 Sep 2024 13:29:10 -0700 Subject: [PATCH 0303/1479] Update template --- .../Hosts/AzureUserReader/Infrastructure/compute/template.bicep | 2 +- Service/GroupMembershipManagement/global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep index f71c44421..887a0e0c6 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep @@ -93,7 +93,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } diff --git a/Service/GroupMembershipManagement/global.json b/Service/GroupMembershipManagement/global.json index 36d37e622..fd07d882a 100644 --- a/Service/GroupMembershipManagement/global.json +++ b/Service/GroupMembershipManagement/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.401" + "version": "8.0.303" } } From ace4668f6a12b1f7e5a2c05a33c8052e50cf0ac4 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Tue, 3 Sep 2024 14:36:56 -0700 Subject: [PATCH 0304/1479] Update test coverage threshold --- vsts-cicd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsts-cicd.yml b/vsts-cicd.yml index ae496ca9e..8f6dea035 100644 --- a/vsts-cicd.yml +++ b/vsts-cicd.yml @@ -86,7 +86,7 @@ stages: coverageThreshold: 95 - function: name: 'AzureUserReader' - coverageThreshold: 62 + coverageThreshold: 56 - function: name: 'JobScheduler' coverageThreshold: 48 From a6a452102c6a7711e606f998a3dfe27cf1519000 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 5 Sep 2024 16:06:13 -0700 Subject: [PATCH 0305/1479] Update host config --- .../Hosts/AzureUserReader/Function/Program.cs | 7 ++++++- .../AzureUserReader/Function/Starter/StarterFunction.cs | 2 -- .../Hosts/AzureUserReader/Function/host.json | 8 +------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs index d15c29fd2..e09f40cf6 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Program.cs @@ -42,7 +42,12 @@ public static void Main(string[] args) }); }) .ConfigureServices((context, services) => - { + { + var configuration = context.Configuration; + var functionName = "AzureUserReader"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); services.AddGraphAPIClient(); services.AddSingleton(services => diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Starter/StarterFunction.cs index 09eaf498f..c5f95f9a0 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/Starter/StarterFunction.cs @@ -6,13 +6,11 @@ using Repositories.Contracts; using System; using System.Net; -using System.Net.Http; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker; using Microsoft.DurableTask.Client; using Microsoft.Azure.Functions.Worker.Http; using System.IO; -using Microsoft.AspNetCore.Http; namespace Hosts.AzureUserReader { diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/host.json b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/host.json index 46d89956e..681721b4a 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Function/host.json @@ -8,11 +8,5 @@ "excludedTypes": "Request" } } - }, - "extensions": { - "durableTask": { - "extendedSessionsEnabled": true, - "extendedSessionIdleTimeoutInSeconds": 30 - } } -} \ No newline at end of file +} \ No newline at end of file From 5cf2e53992d109758eb864a898a086bc16851ef7 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Fri, 6 Sep 2024 16:38:53 -0700 Subject: [PATCH 0306/1479] fix for grouping/ungrouping selected items --- .../HRQuerySource/HRQuerySource.base.tsx | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 5b67a0ea3..c669f1aa3 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -72,6 +72,7 @@ export const HRQuerySourceBase: React.FunctionComponent = (p const [items, setItems] = useState([]); let options: IComboBoxOption[] = []; const [groups, setGroups] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); const [selectedIndices, setSelectedIndices] = useState([]); const [groupingEnabled, setGroupingEnabled] = useState(false); const [filterTextEnabled, setFilterTextEnabled] = useState(false); @@ -1315,9 +1316,8 @@ const checkType = (value: string, type: string | undefined): string => { function onUnGroupClick() { let newGroups = [...groups]; let indices: { selectedItemIndex: number, groupIndex: number; childIndex: number }[] = []; - const selectedItems = items.filter((item, index) => selectedIndices.includes(index)); - - selectedItems.forEach((selectedItem, index) => { + const si = selectedIndices.includes(-1) ? selectedItems : items.filter((item, index) => selectedIndices.includes(index)); + si.forEach((selectedItem, index) => { const ifGroupItem = groups.some(group => isGroupItem(group, selectedItem)); const ifGroupChild = groups.some(group => isGroupChild(group, selectedItem)); @@ -1358,17 +1358,17 @@ const checkType = (value: string, type: string | undefined): string => { const allSameChildIndex = childIndices.every((childIndex, index, array) => childIndex === array[0]); let clonedNewGroups: Group[] = JSON.parse(JSON.stringify(newGroups)); - const childItems = selectedItems; + const childItems = si; if (allSameGroupIndex && allSameChildIndex && childIndices[0] >= 0) { clonedNewGroups[groupIndices[0]].items = [...clonedNewGroups[groupIndices[0]].items, ...childItems]; - clonedNewGroups = filterChildren(clonedNewGroups, selectedItems, groupIndices[0], childIndices[0]); + clonedNewGroups = filterChildren(clonedNewGroups, si, groupIndices[0], childIndices[0]); } else if (allSameGroupIndex && groupIndices[0] > 0 && childIndices[0] === -1) { if (groups[groupIndices[0]] && groups[groupIndices[0]].children && groups[groupIndices[0]].children.length > 0) { return; } clonedNewGroups[0].items = [...clonedNewGroups[0].items, ...childItems]; - clonedNewGroups = filterItems(clonedNewGroups, selectedItems, groupIndices[0]); + clonedNewGroups = filterItems(clonedNewGroups, si, groupIndices[0]); } clonedNewGroups = clonedNewGroups.filter((group: { items: any[]; children: any[]; }) => @@ -1382,6 +1382,7 @@ const checkType = (value: string, type: string | undefined): string => { setGroups(clonedNewGroups); getGroupLabels(clonedNewGroups); setSelectedIndices([]); + setSelectedItems([]); selection.setAllSelected(false); setGroupingEnabled(true); } @@ -1389,15 +1390,15 @@ const checkType = (value: string, type: string | undefined): string => { function onGroupClick() { let newGroups = [...groups]; let indices: { selectedItemIndex: number, groupIndex: number; childIndex: number }[] = []; - const selectedItems = items.filter((item, index) => selectedIndices.includes(index)); - selectedItems.forEach((selectedItem, index) => { + const si = selectedIndices.includes(-1) ? selectedItems : items.filter((item, index) => selectedIndices.includes(index)); + si.forEach((selectedItem, index) => { const groupIndex = groups.findIndex(group => isGroupItem(group, selectedItem)); if (groupIndex >= 0) { indices.push({ selectedItemIndex: index, groupIndex, childIndex: -1 }); } }); - selectedItems.forEach((selectedItem, index) => { + si.forEach((selectedItem, index) => { const ifGroupChild = groups.some(group => isGroupChild(group, selectedItem)); if (ifGroupChild) { return; @@ -1408,14 +1409,14 @@ const checkType = (value: string, type: string | undefined): string => { const allSameGroupIndex = groupIndices.every((groupIndex, index, array) => groupIndex === array[0]); let clonedNewGroups: Group[] = JSON.parse(JSON.stringify(newGroups)); - const childItems = selectedItems.map(selectedItem => ({ attribute: selectedItem.attribute, equalityOperator: selectedItem.equalityOperator, value: selectedItem.value, andOr: selectedItem.andOr })); + const childItems = si.map(selectedItem => ({ attribute: selectedItem.attribute, equalityOperator: selectedItem.equalityOperator, value: selectedItem.value, andOr: selectedItem.andOr })); const filterItems = (groupIndex: number) => { clonedNewGroups[groupIndex].items = clonedNewGroups[groupIndex].items.filter((item: { attribute: string; equalityOperator: string; value: string; andOr: string; }) => !childItems.some(childItem => item.attribute === childItem.attribute && item.equalityOperator === childItem.equalityOperator && item.value === childItem.value && item.andOr === childItem.andOr)); }; if (allSameGroupIndex && groupIndices[0] === 0) { - if (clonedNewGroups[groupIndices[0]].items.length === selectedItems.length) { return; } + if (clonedNewGroups[groupIndices[0]].items.length === si.length) { return; } const newGroup: Group = { name: "", items: childItems, @@ -1431,7 +1432,7 @@ const checkType = (value: string, type: string | undefined): string => { } else if (allSameGroupIndex && groupIndices[0] > 0) { - if (clonedNewGroups[groupIndices[0]].items.length === selectedItems.length) { return; } + if (clonedNewGroups[groupIndices[0]].items.length === si.length) { return; } clonedNewGroups[groupIndices[0]].children = [ ...(clonedNewGroups[groupIndices[0]].children || []), { @@ -1464,6 +1465,7 @@ const checkType = (value: string, type: string | undefined): string => { setGroups(clonedNewGroups); getGroupLabels(clonedNewGroups); setSelectedIndices([]); + setSelectedItems([]); selection.setAllSelected(false); setGroupingEnabled(true); setFilteredOptions({}); @@ -1552,6 +1554,7 @@ const checkType = (value: string, type: string | undefined): string => { item.andOr === selectedItem.andOr); }); setSelectedIndices(selectedIndices); + setSelectedItems(selectedItems); } return ( From 57f549a74eb02ff64b1f76603d1c9d2b194e29c9 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Wed, 11 Sep 2024 13:52:26 -0700 Subject: [PATCH 0307/1479] fixes while parsing query --- .../components/HRQuerySource/QuerySerializer.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts index 76f13f2ea..b2e3c9845 100644 --- a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts +++ b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts @@ -232,14 +232,20 @@ function parseSegment(segment: string, groupOperator?: string): Group { const contentOutsideParentheses = segment.replace(/\s*\([^)]*\)\s*/g, '||').split('||'); if (innerSegments) { innerSegments.forEach((innerSegment, index) => { - const childGroup = parseSegment(innerSegment, contentOutsideParentheses && contentOutsideParentheses.length >= 0 ? contentOutsideParentheses[index+1] : ""); + const childGroup = parseSegment(innerSegment, contentOutsideParentheses && contentOutsideParentheses.length >= 0 ? contentOutsideParentheses[index+1].trim().split(/\s+/)[0] : ""); children.push(childGroup); }); } - - let start = segment.indexOf('('); - let end = segment.lastIndexOf(')'); - let remainingSegment = segment.substring(0, start) + segment.substring(end + 1); + let remainingSegment = ""; + contentOutsideParentheses.forEach((content) => { + const trimmedItem = content.trim(); + if (trimmedItem !== "" && trimmedItem.toLowerCase() !== "or" && trimmedItem.toLowerCase() !== "and") { + if (trimmedItem.toLowerCase().trim().startsWith("or") || trimmedItem.toLowerCase().trim().startsWith("and")) { + remainingSegment = remainingSegment.toLowerCase().trim().endsWith("or") || remainingSegment.toLowerCase().trim().endsWith("and") ? remainingSegment.trim().replace(/(?:Or|And)$/i, '') : remainingSegment; + } + remainingSegment += trimmedItem + " "; + } + }); var matchOperator = remainingSegment.match(/^\s*(Or|And)|\s*(Or|And)\s*$/gi); var operator = matchOperator ? matchOperator[0].trim() : null; remainingSegment = remainingSegment.replace(/^\s*(Or|And)|\s*(Or|And)\s*$/gi, '').trim(); From 560a2f3e45c9a158a0b0aa6799100b1d7cd8fca7 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 12 Sep 2024 11:48:18 -0700 Subject: [PATCH 0308/1479] Chnage parsed and validater fucntion return from tuple to ParsedAndValidateDestinationResponse --- .../ParseAndValidateDestinationFunction.cs | 10 ++++++++-- .../SubOrchestrator/SubOrchestratorFunction.cs | 2 +- .../Services.Contracts/IJobTriggerService.cs | 2 +- .../Services.Tests/SubOrchestratorFunctionTests.cs | 12 ++++++++---- .../Hosts/JobTrigger/Services/JobTriggerService.cs | 13 ++++++++++--- .../Models/ParsedAndValidateDestinationResponse.cs | 10 ++++++++++ Service/GroupMembershipManagement/global.json | 2 +- 7 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 Service/GroupMembershipManagement/Models/ParsedAndValidateDestinationResponse.cs diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/ParseAndValidateDestination/ParseAndValidateDestinationFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/ParseAndValidateDestination/ParseAndValidateDestinationFunction.cs index 239a93dfc..496613c1c 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/ParseAndValidateDestination/ParseAndValidateDestinationFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/Activity/ParseAndValidateDestination/ParseAndValidateDestinationFunction.cs @@ -8,6 +8,8 @@ using System; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker; +using Azure; + namespace Hosts.JobTrigger { @@ -22,10 +24,14 @@ public ParseAndValidateDestinationFunction(ILoggingRepository loggingRepository, } [Function(nameof(ParseAndValidateDestinationFunction))] - public async Task<(bool IsValid, string DestinationObject)> ParseAndValidateDestinationAsync([ActivityTrigger] SyncJob syncJob) + public async Task ParseAndValidateDestinationAsync([ActivityTrigger] SyncJob syncJob) { - if (syncJob == null) return (false, null); + if (syncJob == null) return new ParsedAndValidateDestinationResponse + { + IsValid = false, + DestinationObject = null + }; await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(ParseAndValidateDestinationFunction)} function started", RunId = syncJob.RunId }, VerbosityLevel.DEBUG); _jobTriggerService.RunId = syncJob.RunId ?? Guid.Empty; diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs index 791f4fa01..eeb716e13 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Function/SubOrchestrator/SubOrchestratorFunction.cs @@ -77,7 +77,7 @@ await context.CallActivityAsync(nameof(LoggerFunction), try { - var parsedAndValidatedDestination = await context.CallActivityAsync<(bool IsValid, string DestinationObject)>(nameof(ParseAndValidateDestinationFunction), syncJob); + var parsedAndValidatedDestination = await context.CallActivityAsync(nameof(ParseAndValidateDestinationFunction), syncJob); if (!parsedAndValidatedDestination.IsValid) { diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/IJobTriggerService.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/IJobTriggerService.cs index f7c849e18..5a8e755e0 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/IJobTriggerService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Contracts/IJobTriggerService.cs @@ -13,7 +13,7 @@ public interface IJobTriggerService { public Guid RunId { get; set; } Task> GetSyncJobsAsync(); - Task<(bool IsValid, string DestinationObject)> ParseAndValidateDestinationAsync(SyncJob syncJob); + Task ParseAndValidateDestinationAsync(SyncJob syncJob); Task GetDestinationNameAsync(SyncJob job); Task SendEmailAsync(SyncJob job, NotificationMessageType notificationType, string[] additionalContentParameters); Task DestinationExistsAndGMMCanWriteToItAsync(SyncJob job); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs index 0f6c86409..b5542554d 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services.Tests/SubOrchestratorFunctionTests.cs @@ -81,7 +81,7 @@ public void Setup() var options = new JsonSerializerOptions { Converters = { new DestinationValueConverter() } }; var serializedDestinationObject = JsonSerializer.Serialize(destinationObject, options); - _jobTriggerService.Setup(x => x.ParseAndValidateDestinationAsync(It.IsAny())).ReturnsAsync(() => (true, serializedDestinationObject)); + _jobTriggerService.Setup(x => x.ParseAndValidateDestinationAsync(It.IsAny())).ReturnsAsync(() => new ParsedAndValidateDestinationResponse { IsValid = true, DestinationObject = serializedDestinationObject }); _jobTriggerService.Setup(x => x.UpdateSyncJobAsync(It.IsAny(), It.IsAny())) .Callback((status, job) => @@ -96,7 +96,7 @@ public void Setup() }) .ReturnsAsync(() => _frequency); - _context.Setup(x => x.CallActivityAsync<(bool IsValid, string DestinationObject)>(It.Is(x => x.ToString() == nameof(ParseAndValidateDestinationFunction)), It.IsAny(), null)) + _context.Setup(x => x.CallActivityAsync(It.Is(x => x.ToString() == nameof(ParseAndValidateDestinationFunction)), It.IsAny(), null)) .Returns(async () => await CallParseAndValidateDestinationFunction()); _context.Setup(x => x.CallActivityAsync( @@ -181,7 +181,11 @@ public async Task HandleInvalidDestinationQuery() { _syncJob.Destination = "{invalid destination}"; _context.Setup(x => x.GetInput()).Returns(_syncJob); - _jobTriggerService.Setup(x => x.ParseAndValidateDestinationAsync(It.IsAny())).ReturnsAsync(() => (false, null)); + _jobTriggerService.Setup(x => x.ParseAndValidateDestinationAsync(It.IsAny())).ReturnsAsync(() => new ParsedAndValidateDestinationResponse + { + IsValid = false, + DestinationObject = null + }); var suborchrestrator = new SubOrchestratorFunction(_loggingRespository.Object, _telemetryClient, @@ -646,7 +650,7 @@ public async Task HandleUnexpectedExceptionInSubOrchestrator() It.Is(req => req.JobStatus == SyncStatus.Error && req.ResultStatus == ResultStatus.Failure), null), Times.Once()); } - private async Task<(bool IsValid, string DestinationObject)> CallParseAndValidateDestinationFunction() + private async Task CallParseAndValidateDestinationFunction() { var parseAndValidateDestinationFunction = new ParseAndValidateDestinationFunction(_loggingRespository.Object, _jobTriggerService.Object); return await parseAndValidateDestinationFunction.ParseAndValidateDestinationAsync(new SyncJob()); diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs index f4b46d86a..361149348 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Services/JobTriggerService.cs @@ -221,20 +221,27 @@ public async Task> GetGroupEndpointsAsync(SyncJob job) var destinationObjectId = DestinationParser.ParseDestination(job).Value.ObjectId; return await _graphGroupRepository.GetGroupEndpointsAsync(destinationObjectId); } - public async Task<(bool IsValid, string DestinationObject)> ParseAndValidateDestinationAsync(SyncJob syncJob) + public async Task ParseAndValidateDestinationAsync(SyncJob syncJob) { var destinationObject = DestinationParser.ParseDestination(syncJob); if (destinationObject == null) { - return (false, null); + return new ParsedAndValidateDestinationResponse{ + IsValid = false, + DestinationObject = null + }; } else { var options = new JsonSerializerOptions { Converters = { new DestinationValueConverter() } }; var serializedDestinationObject = JsonSerializer.Serialize(destinationObject, options); - return (true, serializedDestinationObject); + return new ParsedAndValidateDestinationResponse + { + IsValid = true, + DestinationObject = serializedDestinationObject + }; } } private IEnumerable ApplyJobTriggerFilters(IEnumerable jobs) diff --git a/Service/GroupMembershipManagement/Models/ParsedAndValidateDestinationResponse.cs b/Service/GroupMembershipManagement/Models/ParsedAndValidateDestinationResponse.cs new file mode 100644 index 000000000..a0da0cc70 --- /dev/null +++ b/Service/GroupMembershipManagement/Models/ParsedAndValidateDestinationResponse.cs @@ -0,0 +1,10 @@ + +namespace Models +{ + public class ParsedAndValidateDestinationResponse + { + public bool IsValid { get; set; } + public string DestinationObject { get; set; } + } +} + diff --git a/Service/GroupMembershipManagement/global.json b/Service/GroupMembershipManagement/global.json index fd07d882a..36d37e622 100644 --- a/Service/GroupMembershipManagement/global.json +++ b/Service/GroupMembershipManagement/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.303" + "version": "8.0.401" } } From 7b9078d2957b50eba91c6749dcb565150f86a718 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 12 Sep 2024 12:58:41 -0700 Subject: [PATCH 0309/1479] Revert sdk version --- Service/GroupMembershipManagement/global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/global.json b/Service/GroupMembershipManagement/global.json index 36d37e622..fd07d882a 100644 --- a/Service/GroupMembershipManagement/global.json +++ b/Service/GroupMembershipManagement/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.401" + "version": "8.0.303" } } From 56bcbc5e05b848752838a76a5f034cf437068bd8 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Thu, 12 Sep 2024 13:13:36 -0700 Subject: [PATCH 0310/1479] Add copy right --- .../Models/ParsedAndValidateDestinationResponse.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Service/GroupMembershipManagement/Models/ParsedAndValidateDestinationResponse.cs b/Service/GroupMembershipManagement/Models/ParsedAndValidateDestinationResponse.cs index a0da0cc70..35ee4788b 100644 --- a/Service/GroupMembershipManagement/Models/ParsedAndValidateDestinationResponse.cs +++ b/Service/GroupMembershipManagement/Models/ParsedAndValidateDestinationResponse.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. namespace Models { From 37200e91b08d43fad56402f7ae18e316173d95d2 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Wed, 7 Aug 2024 10:45:42 -0700 Subject: [PATCH 0311/1479] Added new GetAttributeValues WebAPI endpoint. --- ...lMembershipSourceAttributeValuesRequest.cs | 19 +++ ...MembershipSourceAttributeValuesResponse.cs | 12 ++ ...lMembershipSourceAttributeValuesHandler.cs | 109 ++++++++++++++++++ .../SqlMembershipSourcesControllerTests.cs | 43 ++++++- .../Configuration/MessageHandlerInjector.cs | 1 + .../SqlMembershipSourcesController.cs | 18 +++ .../ISqlMembershipRepository.cs | 1 + .../SqlMembershipRepository.cs | 47 ++++++++ 8 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetDefaultSqlMembershipSourceAttributeValuesRequest.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetDefaultSqlMembershipSourceAttributeValuesResponse.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributeValuesHandler.cs diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetDefaultSqlMembershipSourceAttributeValuesRequest.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetDefaultSqlMembershipSourceAttributeValuesRequest.cs new file mode 100644 index 000000000..f6275ae2a --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetDefaultSqlMembershipSourceAttributeValuesRequest.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Services.Messages.Contracts.Requests; + +namespace Services.Messages.Requests +{ + public class GetDefaultSqlMembershipSourceAttributeValuesRequest : RequestBase + { + public GetDefaultSqlMembershipSourceAttributeValuesRequest(string attribute, bool hasMapping) + { + Attribute = attribute; + HasMapping = hasMapping; + } + + public string Attribute { get; } + public bool HasMapping { get; } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetDefaultSqlMembershipSourceAttributeValuesResponse.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetDefaultSqlMembershipSourceAttributeValuesResponse.cs new file mode 100644 index 000000000..bbd9f4a8d --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetDefaultSqlMembershipSourceAttributeValuesResponse.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Services.Messages.Contracts.Responses; + +namespace Services.Messages.Responses +{ + public class GetDefaultSqlMembershipSourceAttributeValuesResponse : ResponseBase + { + public List Values { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributeValuesHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributeValuesHandler.cs new file mode 100644 index 000000000..8329b62ac --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetDefaultSqlMembershipSourceAttributeValuesHandler.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.Data.SqlClient; +using Models; +using Repositories.Contracts; +using Services.Contracts; +using Services.Messages.Requests; +using Services.Messages.Responses; + +namespace Services +{ + public class GetDefaultSqlMembershipSourceAttributeValuesHandler : RequestHandlerBase + { + private readonly ILoggingRepository _loggingRepository; + private readonly IDataFactoryRepository _dataFactoryRepository; + private readonly ISqlMembershipRepository _sqlMembershipRepository; + + public GetDefaultSqlMembershipSourceAttributeValuesHandler(ILoggingRepository loggingRepository, + IDataFactoryRepository dataFactoryRepository, + ISqlMembershipRepository sqlMembershipRepository) : base(loggingRepository) + { + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + _dataFactoryRepository = dataFactoryRepository ?? throw new ArgumentNullException(nameof(dataFactoryRepository)); + _sqlMembershipRepository = sqlMembershipRepository ?? throw new ArgumentNullException(nameof(sqlMembershipRepository)); + } + + protected override async Task ExecuteCoreAsync(GetDefaultSqlMembershipSourceAttributeValuesRequest request) + { + try + { + var response = new GetDefaultSqlMembershipSourceAttributeValuesResponse(); + var attributeValues = await GetSqlAttributeValuesAsync(request.Attribute, request.HasMapping); + response.Values = attributeValues; + + return response; + } + catch (Exception ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Unable to retrieve Sql Filter Attribute Values: {ex.Message}" }); + throw ex; + } + } + + private async Task> GetSqlAttributeValuesAsync(string attribute, bool hasMapping) + { + var tableName = await GetTableNameAsync(); + var attributes = await GetAttributeValuesAsync(attribute, hasMapping, tableName); + return attributes; + } + + private async Task> GetAttributeValuesAsync(string attribute, bool hasMapping, string tableName) + { + var attributeValues = new List(); + + try + { + attributeValues = await _sqlMembershipRepository.GetAttributeValuesAsync(attribute, hasMapping, tableName); + } + catch (SqlException ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"An exception was thrown while attempting to get Sql Filter Attribute Values from table '{tableName}': {ex.Message}" }); + throw ex; + } + + return attributeValues; + } + + private async Task GetTableNameAsync() + { + var adfRunId = await GetADFRunIdAsync(); + var tableName = adfRunId.Replace("-", ""); + var tableExists = await CheckIfTableExistsAsync(tableName); + + return tableExists ? tableName : ""; + } + + private async Task CheckIfTableExistsAsync(string tableName) + { + bool tableExists = false; + + try + { + tableExists = await _sqlMembershipRepository.CheckIfTableExistsAsync(tableName); + } + catch (SqlException ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"An exception was thrown while checking if table '{tableName}' exists: {ex.Message}" }); + throw ex; + } + + return tableExists; + } + + private async Task GetADFRunIdAsync() + { + var lastSqlMembershipRunId = await _dataFactoryRepository.GetMostRecentSucceededRunIdAsync(); + + if (string.IsNullOrWhiteSpace(lastSqlMembershipRunId)) + { + var message = $"No SqlMembershipObtainer pipeline run has been found"; + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"An exception was thrown while attempting to get the latest ADF pipeline run: {message}" }); + throw new ArgumentException(message); + } + + return lastSqlMembershipRunId; + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs index 4874c9755..ec002c8ca 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs @@ -27,6 +27,7 @@ public class SqlMembershipSourcesControllerTests private GetDefaultSqlMembershipSourceHandler _getDefaultSqlMembershipSourceHandler = null!; private GetDefaultSqlMembershipSourceAttributesHandler _getDefaultSqlMembershipSourceAttributesHandler = null!; private GetDefaultSqlMembershipSourceAttributeMappingsHandler _getDefaultSqlMembershipSourceAttributeMappingsHandler = null!; + private GetDefaultSqlMembershipSourceAttributeValuesHandler _getDefaultSqlMembershipSourceAttributeValuesHandler = null!; private PatchDefaultSqlMembershipSourceCustomLabelHandler _patchDefaultSqlMembershipSourceCustomLabelHandler = null!; private PatchDefaultSqlMembershipSourceAttributesHandler _patchDefaultSqlMembershipSourceAttributesHandler = null!; private SqlMembershipSourcesController _sqlMembershipSourcesController = null!; @@ -42,10 +43,11 @@ public void Initialize() _getDefaultSqlMembershipSourceHandler = new GetDefaultSqlMembershipSourceHandler(_loggingRepository.Object, _databaseSqlMembershipSourcesRepository.Object); _getDefaultSqlMembershipSourceAttributesHandler = new GetDefaultSqlMembershipSourceAttributesHandler(_loggingRepository.Object, _databaseSqlMembershipSourcesRepository.Object, _dataFactoryRepository.Object, _sqlMembershipRepository.Object); _getDefaultSqlMembershipSourceAttributeMappingsHandler = new GetDefaultSqlMembershipSourceAttributeMappingsHandler(_loggingRepository.Object, _dataFactoryRepository.Object, _sqlMembershipRepository.Object); + _getDefaultSqlMembershipSourceAttributeValuesHandler = new GetDefaultSqlMembershipSourceAttributeValuesHandler(_loggingRepository.Object, _dataFactoryRepository.Object, _sqlMembershipRepository.Object); _patchDefaultSqlMembershipSourceCustomLabelHandler = new PatchDefaultSqlMembershipSourceCustomLabelHandler(_loggingRepository.Object, _databaseSqlMembershipSourcesRepository.Object); _patchDefaultSqlMembershipSourceAttributesHandler = new PatchDefaultSqlMembershipSourceAttributesHandler(_loggingRepository.Object, _databaseSqlMembershipSourcesRepository.Object); - _sqlMembershipSourcesController = new SqlMembershipSourcesController(_getDefaultSqlMembershipSourceHandler, _getDefaultSqlMembershipSourceAttributesHandler, _getDefaultSqlMembershipSourceAttributeMappingsHandler, _patchDefaultSqlMembershipSourceCustomLabelHandler, _patchDefaultSqlMembershipSourceAttributesHandler) + _sqlMembershipSourcesController = new SqlMembershipSourcesController(_getDefaultSqlMembershipSourceHandler, _getDefaultSqlMembershipSourceAttributesHandler, _getDefaultSqlMembershipSourceAttributeMappingsHandler, _getDefaultSqlMembershipSourceAttributeValuesHandler, _patchDefaultSqlMembershipSourceCustomLabelHandler, _patchDefaultSqlMembershipSourceAttributesHandler) { ControllerContext = CreateControllerContext(new List { @@ -74,6 +76,7 @@ public void Initialize() _databaseSqlMembershipSourcesRepository.Setup(x => x.GetDefaultSourceAttributesAsync()).ReturnsAsync(() => _storedAttributeSettings); _sqlMembershipRepository.Setup(x => x.GetColumnDetailsAsync(It.IsAny())).ReturnsAsync(new List<(string Name, string Type)> { ("Name1", "nvarchar"), ("Name2", "int"), ("Name3_Code", "nvarchar") }); _sqlMembershipRepository.Setup(x => x.GetAttributeMappingsAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List<(string Code, string Description)> { ("Code1", "Description1"), ("Code2", "Description2"), ("Code3", "Description3") }); + _sqlMembershipRepository.Setup(x => x.GetAttributeValuesAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new List { "Value1", "Value2", "Value3" }); _sqlMembershipRepository.Setup(x => x.CheckIfTableExistsAsync(It.IsAny())).ReturnsAsync(true); _sqlMembershipRepository.Setup(x => x.CheckIfMappingsTableExistsAsync(It.IsAny())).ReturnsAsync(true); _dataFactoryRepository.Setup(x => x.GetMostRecentSucceededRunIdAsync()).ReturnsAsync("RUN ID"); @@ -301,6 +304,44 @@ public async Task ExceptionGetHRFilterattributeMappingsTestAsync() It.IsAny()), Times.Once()); } + [TestMethod] + public async Task SuccessfulGetHRFilterAttributeValuesTestAsync() + { + var response = await _sqlMembershipSourcesController.GetDefaultSourceAttributeValuesAsync("attribute", false); + Assert.IsNotNull(response); + var okResult = response as OkObjectResult; + + Assert.IsNotNull(okResult); + Assert.IsNotNull(okResult.Value); + + var attributeValues = okResult.Value as List; + Assert.IsNotNull(attributeValues); + Assert.AreEqual(attributeValues.Count, 3); + Assert.AreEqual(attributeValues[0], "Value1"); + } + + [TestMethod] + public async Task ExceptionGetHRFilterAttributeValuesTestAsync() + { + _sqlMembershipRepository.Setup(x => x.CheckIfTableExistsAsync(It.IsAny())).ReturnsAsync(false); + _sqlMembershipRepository.Setup(x => x.GetAttributeValuesAsync(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new Exception("Unexpected exception triggered for testing")); + + var response = await _sqlMembershipSourcesController.GetDefaultSourceAttributeValuesAsync("attribute", false); + + Assert.IsNotNull(response); + + var internalServerErrorResponse = response as StatusCodeResult; + + Assert.IsNotNull(internalServerErrorResponse); + Assert.AreEqual(internalServerErrorResponse.StatusCode, (int)HttpStatusCode.InternalServerError); + + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message.StartsWith("Unable to retrieve Sql Filter Attribute Values")), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once()); + } + private ControllerContext CreateControllerContext(List claims) { return new ControllerContext { HttpContext = CreateHttpContext(claims) }; diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs index c7be83196..b663d93d2 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs @@ -24,6 +24,7 @@ public static IServiceCollection InjectMessageHandlers(this IServiceCollection s services.AddTransient, GetDefaultSqlMembershipSourceHandler>(); services.AddTransient, GetDefaultSqlMembershipSourceAttributesHandler>(); services.AddTransient, GetDefaultSqlMembershipSourceAttributeMappingsHandler>(); + services.AddTransient, GetDefaultSqlMembershipSourceAttributeValuesHandler>(); services.AddTransient, PatchDefaultSqlMembershipSourceCustomLabelHandler>(); services.AddTransient, PatchDefaultSqlMembershipSourceAttributesHandler>(); diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs index 6fa2d938a..ab1f9c3a2 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs @@ -18,6 +18,7 @@ public class SqlMembershipSourcesController : ControllerBase private readonly IRequestHandler _getDefaultSqlMembershipSourceHandler; private readonly IRequestHandler _getDefaultSqlMembershipSourceAttributesHandler; private readonly IRequestHandler _getDefaultSqlMembershipSourceAttributeMappingsHandler; + private readonly IRequestHandler _getDefaultSqlMembershipSourceAttributeValuesHandler; private readonly IRequestHandler _patchDefaultSqlMembershipSourceCustomLabelHandler; private readonly IRequestHandler _patchDefaultSqlMembershipSourceAttributesHandler; @@ -25,12 +26,14 @@ public SqlMembershipSourcesController( IRequestHandler getDefaultSqlMembershipSourceHandler, IRequestHandler getDefaultSqlMembershipSourceAttributesHandler, IRequestHandler getDefaultSqlMembershipSourceAttributeMappingsHandler, + IRequestHandler getDefaultSqlMembershipSourceAttributeValuesHandler, IRequestHandler patchDefaultSqlMembershipSourceCustomLabelHandler, IRequestHandler patchDefaultSqlMembershipSourceAttributesHandler) { _getDefaultSqlMembershipSourceHandler = getDefaultSqlMembershipSourceHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceHandler)); _getDefaultSqlMembershipSourceAttributesHandler = getDefaultSqlMembershipSourceAttributesHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceAttributesHandler)); _getDefaultSqlMembershipSourceAttributeMappingsHandler = getDefaultSqlMembershipSourceAttributeMappingsHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceAttributeMappingsHandler)); + _getDefaultSqlMembershipSourceAttributeValuesHandler = getDefaultSqlMembershipSourceAttributeValuesHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceAttributeValuesHandler)); _patchDefaultSqlMembershipSourceCustomLabelHandler = patchDefaultSqlMembershipSourceCustomLabelHandler ?? throw new ArgumentNullException(nameof(patchDefaultSqlMembershipSourceCustomLabelHandler)); _patchDefaultSqlMembershipSourceAttributesHandler = patchDefaultSqlMembershipSourceAttributesHandler ?? throw new ArgumentNullException(nameof(patchDefaultSqlMembershipSourceAttributesHandler)); } @@ -80,6 +83,21 @@ public async Task GetDefaultSourceAttributeMappingsAsync(string a } } + [Authorize(Roles = Models.Roles.CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR)] + [HttpGet("attributeValues/{attribute}")] + public async Task GetDefaultSourceAttributeValuesAsync([FromRoute] string attribute, [FromQuery] bool hasMapping) + { + try + { + var response = await _getDefaultSqlMembershipSourceAttributeValuesHandler.ExecuteAsync(new GetDefaultSqlMembershipSourceAttributeValuesRequest(attribute, hasMapping)); + return Ok(response.Values); + } + catch (Exception ex) + { + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + [Authorize(Roles = Models.Roles.CUSTOM_MEMBERSHIP_PROVIDER_ADMINISTRATOR)] [HttpPatch("default")] public async Task PatchDefaultSourceCustomLabelAsync([FromBody] string customLabel) diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs index bb9eae352..a0db95d88 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs @@ -18,5 +18,6 @@ public interface ISqlMembershipRepository Task> GetColumnDetailsAsync(string tableName); Task CheckIfMappingsTableExistsAsync(string tableName); Task> GetAttributeMappingsAsync(string attribute, string tableName); + Task> GetAttributeValuesAsync(string attribute, bool hasMapping, string tableName); } } diff --git a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs index baef0abae..0108b9660 100644 --- a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs @@ -452,6 +452,53 @@ await retryPolicy.Execute(async () => return attributeMappings; } + public async Task> GetAttributeValuesAsync(string attribute, bool hasMapping, string tableName) + { + var attributeValues = new List(); + var retryPolicy = GetRetryPolicy(); + + var schema = hasMapping ? "mappings" : "users"; + var column = hasMapping ? "Description" : $"{attribute}"; + var whereClause = hasMapping ? $" WHERE ColumnName = '{attribute}'" : ""; + + try + { + var selectQuery = $"SELECT DISTINCT TOP(10) {column} FROM [{schema}].[{tableName}]" + whereClause; + + await retryPolicy.Execute(async () => + { + using (var conn = new SqlConnection(_sqlServerConnectionString)) + { + await conn.OpenAsync(); + using (var cmd = new SqlCommand(selectQuery, conn)) + { + using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)) + { + int valueOrdinal = reader.GetOrdinal($"{column}"); + + while (reader.Read()) + { + var value = reader.IsDBNull(valueOrdinal) ? null : reader.GetValue(valueOrdinal)?.ToString()?.Trim(); + + if (value != null) + { + attributeValues.Add(value); + } + } + await reader.CloseAsync(); + } + } + await conn.CloseAsync(); + } + }); + } + catch (SqlException ex) + { + throw ex; + } + + return attributeValues; + } private RetryPolicy GetRetryPolicy() { return Policy.Handle() From a8e01a2f9df05b905e4d0c722aeb35e131e8c02e Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Wed, 21 Aug 2024 11:48:04 -0700 Subject: [PATCH 0312/1479] Added the value dropdowns to the admin config custom source page. --- .../ISqlMembershipSourcesApi.ts | 1 + .../SqlMembershipSourcesApi.ts | 6 ++ .../src/models/GetAttributeValuesResponse.ts | 7 ++ .../src/models/SqlMembershipAttribute.ts | 3 +- .../pages/AdminConfig/AdminConfig.base.tsx | 8 +- .../pages/AdminConfig/AdminConfig.styles.ts | 21 ++++- .../pages/AdminConfig/AdminConfig.types.ts | 16 +++- .../pages/AdminConfig/AdminConfig.view.tsx | 85 ++++++++++++++++++- .../src/services/localization/IStrings.ts | 3 + .../i18n/locales/en/translations.ts | 3 + .../i18n/locales/es/translations.ts | 3 + .../src/store/sqlMembershipSources.api.tsx | 17 ++++ .../src/store/sqlMembershipSources.slice.tsx | 10 ++- 13 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 UI/web-app/src/models/GetAttributeValuesResponse.ts diff --git a/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts b/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts index 581bd09a4..a878040f0 100644 --- a/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts +++ b/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts @@ -7,6 +7,7 @@ export interface ISqlMembershipSourcesApi { fetchDefaultSqlMembershipSource(): Promise; fetchDefaultSqlMembershipSourceAttributes(): Promise; fetchDefaultSqlMembershipSourceAttributeMappings(attribute: string): Promise; + fetchDefaultSqlMembershipSourceAttributeValues(attribute: SqlMembershipAttribute): Promise; patchDefaultSqlMembershipSourceCustomLabel(customLabel: string): Promise; patchDefaultSqlMembershipSourceAttributes(attributes: SqlMembershipAttribute[]): Promise; } diff --git a/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts b/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts index 90b08f180..700c308d9 100644 --- a/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts +++ b/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts @@ -26,6 +26,12 @@ export class SqlMembershipSourcesApi extends ApiBase implements ISqlMembershipSo return response.data; } + public async fetchDefaultSqlMembershipSourceAttributeValues(attribute: SqlMembershipAttribute): Promise { + const response = await this.httpClient.get('/attributeValues/' + attribute.name, { params: { hasMapping: attribute.hasMapping } }); + this.ensureSuccessStatusCode(response); + return response.data; + } + public async patchDefaultSqlMembershipSourceCustomLabel(customLabel: string): Promise { const response = await this.httpClient.patch( '/default', diff --git a/UI/web-app/src/models/GetAttributeValuesResponse.ts b/UI/web-app/src/models/GetAttributeValuesResponse.ts new file mode 100644 index 000000000..c6c6b9466 --- /dev/null +++ b/UI/web-app/src/models/GetAttributeValuesResponse.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export interface GetAttributeValuesResponse { + values: string[]; + attribute: string; +} \ No newline at end of file diff --git a/UI/web-app/src/models/SqlMembershipAttribute.ts b/UI/web-app/src/models/SqlMembershipAttribute.ts index f10c87e8d..c1b8271c7 100644 --- a/UI/web-app/src/models/SqlMembershipAttribute.ts +++ b/UI/web-app/src/models/SqlMembershipAttribute.ts @@ -6,5 +6,6 @@ export type SqlMembershipAttribute = { customLabel: string; type: string; hasMapping: boolean; - }; + values: string[]; +}; \ No newline at end of file diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx index 45573e94b..b233e84f9 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.base.tsx @@ -20,7 +20,7 @@ import { SettingKey } from '../../models/SettingKey'; import { setPagingBarVisible } from '../../store/pagingBar.slice'; import { selectSource, selectAttributes, selectIsSourceSaving, selectAreAttributesSaving, setSource, setAttributes } from '../../store/sqlMembershipSources.slice'; import { SqlMembershipAttribute, SqlMembershipSource } from '../../models'; -import { patchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceCustomLabel } from '../../store/sqlMembershipSources.api'; +import { fetchAttributeValues, patchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceCustomLabel } from '../../store/sqlMembershipSources.api'; import { selectIsCustomMembershipProviderAdministrator, selectIsHyperlinkAdministrator, @@ -28,7 +28,6 @@ import { selectIsGeneralSettingsAdministrator, } from '../../store/roles.slice'; - export const AdminConfigBase: React.FunctionComponent = (props: AdminConfigProps) => { // get the store's dispatch function const dispatch = useDispatch(); @@ -68,6 +67,10 @@ export const AdminConfigBase: React.FunctionComponent = (props setSettings(generateSettings()) }, [dashboardUrl, outlookWarningUrl, privacyPolicyUrl, canReviewOwnSubmissions]); + const handleGetValues = (attribute: SqlMembershipAttribute) => { + dispatch(fetchAttributeValues(attribute)); + } + // Create an event handler that should be called when the user clicks the save button. const handleSave = (newSettings: { readonly [key in SettingKey]: string }, newSqlMembershipSource: SqlMembershipSource | undefined, newSqlMembershipAttributes: SqlMembershipAttribute[] | undefined) => { @@ -130,6 +133,7 @@ export const AdminConfigBase: React.FunctionComponent = (props settings={settings} strings={strings} onSave={handleSave} + handleGetValues={handleGetValues} sqlMembershipSource={sqlMembershipSource} sqlMembershipSourceAttributes={sqlMembershipSourceAttributes} isHyperlinkAdmin={isHyperlinkAdmin} diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.styles.ts b/UI/web-app/src/pages/AdminConfig/AdminConfig.styles.ts index a2dac35d5..9fc2398ee 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.styles.ts +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.styles.ts @@ -45,8 +45,8 @@ export const getStyles = (props: AdminConfigStyleProps): AdminConfigStyles => { borderRadius: 4, border: '1px solid', borderColor: theme.palette.neutralQuaternary, - backgroud: theme.palette.white, - width: 150, + background: theme.palette.white, + width: 150 }, defaultColumnSpan: { display: 'flex', @@ -63,7 +63,7 @@ export const getStyles = (props: AdminConfigStyleProps): AdminConfigStyles => { borderRadius: 4, border: '1px solid', borderColor: theme.palette.neutralQuaternary, - backgroud: theme.palette.white, + background: theme.palette.white, width: 266, }, sourceNameDescriptionContainer: { @@ -81,6 +81,21 @@ export const getStyles = (props: AdminConfigStyleProps): AdminConfigStyles => { descriptionText: { fontWeight: 400, color: theme.palette.black + }, + valuesDropdown: { + maxWidth: 150 + }, + valuesDropdownTitle: { + borderRadius: 4, + borderStyle: 'solid', + borderWidth: 1, + borderColor: theme.palette.neutralQuaternary, + backgroud: theme.palette.white, + maxWidth: 150 + }, + valuesDropdownSpinner: { + marginTop: 10, + marginBottom: 10 } diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts b/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts index b25c4bc94..d8f248f6c 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.types.ts @@ -5,6 +5,7 @@ import { IProcessedStyleSet, type IStyle, type IStyleFunctionOrObject, type IThe import type React from 'react'; import type { SettingKey, SqlMembershipAttribute, SqlMembershipSource } from '../../models'; import type { IStrings } from '../../services/localization'; +import { MouseEventHandler } from 'react'; export type AdminConfigStyles = { root: IStyle; @@ -21,6 +22,9 @@ export type AdminConfigStyles = { listOfAttributesTitleDescriptionContainer: IStyle; detailsListContainer: IStyle; descriptionText: IStyle; + valuesDropdown: IStyle; + valuesDropdownTitle: IStyle; + valuesDropdownSpinner: IStyle; }; export type AdminConfigStyleProps = { @@ -43,6 +47,7 @@ export type AdminConfigProps = React.AllHTMLAttributes & { export type AdminConfigViewProps = AdminConfigProps & { isSaving: boolean; onSave: (settings: { readonly [key in SettingKey]: string }, sqlMembershipSource: SqlMembershipSource | undefined, sqlMembershipAttributes: SqlMembershipAttribute[] | undefined) => void; + handleGetValues: (attribute: SqlMembershipAttribute) => void; settings: { readonly [key in SettingKey]: string }; sqlMembershipSource: SqlMembershipSource | undefined; sqlMembershipSourceAttributes: SqlMembershipAttribute[] | undefined; @@ -80,6 +85,7 @@ export type CustomSourceSettingsProps = { strings: IStrings['AdminConfig']; setNewSource: React.Dispatch>; setNewAttributes: React.Dispatch>; + handleGetValues: (attribute: SqlMembershipAttribute) => void; }; export type CustomLabelCellProps = { @@ -87,4 +93,12 @@ export type CustomLabelCellProps = { placeholder: string; onChange: ((event: React.FormEvent, newValue?: string | undefined) => void) | undefined; className: string; -}; \ No newline at end of file +}; + +export type AttributeValuesCellProps = { + values: string[]; + classNames: IProcessedStyleSet; + strings: IStrings['AdminConfig']; + onDropdownClick: MouseEventHandler | undefined; +}; + diff --git a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx index 6431a8fef..7945e1fb6 100644 --- a/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx +++ b/UI/web-app/src/pages/AdminConfig/AdminConfig.view.tsx @@ -2,13 +2,14 @@ // Licensed under the MIT license. import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { classNamesFunction, IProcessedStyleSet, Pivot, PivotItem, PrimaryButton, TextField, Text, IColumn, SelectionMode, ShimmeredDetailsList } from '@fluentui/react'; +import { classNamesFunction, IProcessedStyleSet, Pivot, PivotItem, PrimaryButton, TextField, Text, IColumn, SelectionMode, ShimmeredDetailsList, Dropdown, Spinner, IRenderFunction, ISelectableDroppableTextProps, IDropdown } from '@fluentui/react'; import { useTheme } from '@fluentui/react/lib/Theme'; import { AdminConfigStyleProps, AdminConfigStyles, AdminConfigViewProps, CustomLabelCellProps, + AttributeValuesCellProps, CustomSourceSettingsProps, HyperlinkSettingsProps, OperationsProps, @@ -25,7 +26,7 @@ const getClassNames = classNamesFunction = (props: AdminConfigViewProps) => { // extract props - const { className, isSaving, onSave, settings, sqlMembershipSource, sqlMembershipSourceAttributes, strings, styles, + const { className, isSaving, onSave, handleGetValues, settings, sqlMembershipSource, sqlMembershipSourceAttributes, strings, styles, isHyperlinkAdmin, isCustomMembershipProviderAdmin, isOperationsResetAdministrator, @@ -111,6 +112,7 @@ export const AdminConfigView: React.FunctionComponent = (p sqlMembershipSourceAttributes={sqlMembershipSourceAttributes} setNewAttributes={setNewAttributes} setNewSource={setNewSource} + handleGetValues={handleGetValues} strings={strings} /> } @@ -248,7 +250,7 @@ const HyperlinkSettings: React.FunctionComponent = (prop const CustomSourceSettings: React.FunctionComponent = (props: CustomSourceSettingsProps) => { - const { classNames, sqlMembershipSource, sqlMembershipSourceAttributes, setNewAttributes, setNewSource, strings } = props; + const { classNames, sqlMembershipSource, sqlMembershipSourceAttributes, setNewAttributes, setNewSource, handleGetValues, strings } = props; const [attributeMap, setAttributeMap] = useState<{ [key: string]: SqlMembershipAttribute } | undefined>(undefined); const [isSortedDescending, setIsSortedDescending] = useState(false); @@ -267,7 +269,10 @@ const CustomSourceSettings: React.FunctionComponent = useEffect(() => { const newAttributeMap = attributes?.reduce((acc: { [key: string]: SqlMembershipAttribute }, currentItem: SqlMembershipAttribute) => { const { name } = currentItem; - acc[name] = { ...currentItem }; + acc[name] = { + ...currentItem, + customLabel: (attributeMap && attributeMap[name]?.customLabel) || currentItem.customLabel + }; return acc; }, {}); @@ -306,6 +311,15 @@ const CustomSourceSettings: React.FunctionComponent = [setAttributeMap] ); + const handleDropdownClick = useCallback( + (attribute: SqlMembershipAttribute): any => { + if (attribute && !attribute.values) { + handleGetValues(attribute); + } + }, + [attributeMap] + ); + const onRenderItemColumn = (item?: any, index?: number, column?: IColumn): JSX.Element => { if (!item || !column || !attributeMap) { @@ -326,6 +340,17 @@ const CustomSourceSettings: React.FunctionComponent = className={classNames.customLabelTextField} /> ); + case 'attributeValues': + return ( + { + handleDropdownClick(attributeMap[item.name]); + }} + /> + ); default: return (
@@ -357,6 +382,13 @@ const CustomSourceSettings: React.FunctionComponent = isSorted: sortKey === 'customLabel', isSortedDescending, showSortIconWhenUnsorted: true, + }, + { + key: 'attributeValues', + name: strings.CustomSourceSettings.labels.valuesColumn, + fieldName: 'attributeValues', + minWidth: 160, + maxWidth: 170, } ]; @@ -424,4 +456,49 @@ const CustomLabelCell = React.memo((props: CustomLabelCellProps) => { onChange={onChange} /> ); +}); + +const AttributeValuesCell = React.memo((props: AttributeValuesCellProps) => { + const { classNames, values, onDropdownClick, strings } = props; + + const getDropdownOptions = (values : string[]) => { + if (!values) { + return []; + } + + return values.map((value) => { + return { + key: value, + text: value, + disabled: true, + title: value + } + }); + } + + const onRenderList: IRenderFunction> = (props, defaultRender) => { + + const isLoading = props?.options?.length === 0; + + return ( +
+ {isLoading ? ( + + ) : ( + defaultRender!(props) + )} +
+ ); +}; + + return ( + + ); }); \ No newline at end of file diff --git a/UI/web-app/src/services/localization/IStrings.ts b/UI/web-app/src/services/localization/IStrings.ts index 77f108bc3..f3edf9087 100644 --- a/UI/web-app/src/services/localization/IStrings.ts +++ b/UI/web-app/src/services/localization/IStrings.ts @@ -125,6 +125,9 @@ export type IStrings = { attributeColumn: string; customLabelColumn: string; customLabelInputPlaceHolder: string; + valuesColumn: string; + valuesDropdownSpinnerLabel: string; + valuesDropdownPlaceholder: string; }, }, GeneralSettings: { diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index 0d1ab4302..1546c6a67 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -128,6 +128,9 @@ export const strings: IStrings = { attributeColumn: "Attribute", customLabelColumn: "Custom Label", customLabelInputPlaceHolder: "Enter a custom label", + valuesColumn: "Values", + valuesDropdownSpinnerLabel: "Loading values...", + valuesDropdownPlaceholder: "View values" }, }, GeneralSettings: { diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index 6273f533d..c87da3972 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -130,6 +130,9 @@ export const strings: IStrings = { attributeColumn: "Atributo", customLabelColumn: "Nombre Personalizado", customLabelInputPlaceHolder: "Entre un nombre personalizado", + valuesColumn: "Valores", + valuesDropdownSpinnerLabel: "Cargando valores", + valuesDropdownPlaceholder: "Ver valores" }, }, GeneralSettings: { diff --git a/UI/web-app/src/store/sqlMembershipSources.api.tsx b/UI/web-app/src/store/sqlMembershipSources.api.tsx index f14ee3217..8b50d4344 100644 --- a/UI/web-app/src/store/sqlMembershipSources.api.tsx +++ b/UI/web-app/src/store/sqlMembershipSources.api.tsx @@ -6,6 +6,7 @@ import { ThunkConfig } from './store'; import { SqlMembershipAttribute, SqlMembershipSource } from '../models'; import { GetAttributeMappingsResponse } from '../models/GetAttributeMappingsResponse'; import { GetAttributeMappingsRequest } from '../models/GetAttributeMappingsRequest'; +import { GetAttributeValuesResponse } from '../models/GetAttributeValuesResponse'; export const fetchDefaultSqlMembershipSource = createAsyncThunk( 'sqlMembershipSources/fetchDefaultSqlMembershipSource', @@ -57,6 +58,22 @@ export const fetchAttributeMappings = createAsyncThunk( + 'fetchSqlFilterAttributeValues', + async (attribute, { extra }) => { + const { gmmApi } = extra.apis; + let payload: GetAttributeValuesResponse; + try { + const response = await gmmApi.sqlMembershipSources.fetchDefaultSqlMembershipSourceAttributeValues(attribute); + payload = { values: response, attribute: attribute.name }; + return payload; + } catch (error) { + payload = { values: [], attribute: attribute.name }; + return payload; + } + } +); + export const patchDefaultSqlMembershipSourceCustomLabel = createAsyncThunk( 'sqlMembershipSources/patchDefaultSqlMembershipSourceCustomLabel', async (customLabel, { extra }) => { diff --git a/UI/web-app/src/store/sqlMembershipSources.slice.tsx b/UI/web-app/src/store/sqlMembershipSources.slice.tsx index a2af3b20d..d05c80867 100644 --- a/UI/web-app/src/store/sqlMembershipSources.slice.tsx +++ b/UI/web-app/src/store/sqlMembershipSources.slice.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import { fetchAttributeMappings, fetchDefaultSqlMembershipSource, fetchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceCustomLabel } from './sqlMembershipSources.api'; +import { fetchAttributeMappings, fetchAttributeValues, fetchDefaultSqlMembershipSource, fetchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceAttributes, patchDefaultSqlMembershipSourceCustomLabel } from './sqlMembershipSources.api'; import type { RootState } from './store'; import { SqlMembershipAttribute, SqlMembershipAttributeMapping, SqlMembershipSource } from '../models'; @@ -100,6 +100,14 @@ const sqlMembershipSourcesSlice = createSlice({ state.error = action.error.message; }); + builder.addCase(fetchAttributeValues.fulfilled, (state, action) => { + state.areAttributeMappingsLoading = false; + const { attribute, values} = action.payload; + state.attributes = state.attributes?.map(attr => + attr.name === attribute ? { ...attr, values: values } : attr + ); + }); + builder.addCase(patchDefaultSqlMembershipSourceCustomLabel.pending, (state) => { state.isSourceSaving = true; }); From 71a21e0ad67d76310737086547178b8238b09dca Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Fri, 13 Sep 2024 10:24:56 -0700 Subject: [PATCH 0313/1479] Clean packages in Notifier function --- .../Hosts/Notifier/Function/Notifier.csproj | 2 -- .../Services.Notifier.Tests/Services.Notifier.Tests.csproj | 1 - 2 files changed, 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj index dc27dadcb..5356d0cb2 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Notifier.csproj @@ -2,8 +2,6 @@ net8.0 v4 - Hosts.AzureMaintenance - Hosts.AzureMaintenance Exe enabled diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/Services.Notifier.Tests.csproj b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/Services.Notifier.Tests.csproj index 42b450c75..9a70e7993 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/Services.Notifier.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/Services.Notifier.Tests.csproj @@ -11,7 +11,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - From 94c63da9d192dfcd8f4089da1172e0e5f02604aa Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 11 Sep 2024 13:15:39 -0700 Subject: [PATCH 0314/1479] Add blob status logging in MA --- .../DeltaCalculator/DeltaCalculatorFunction.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs index 16b01e6cc..125541645 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs @@ -43,8 +43,24 @@ public async Task CalculateDeltaAsync([ActivityTrigger] if (request.ReadFromBlobs) { var sourceBlobResult = await _blobStorageRepository.DownloadFileAsync(request.SourceMembershipFilePath); + await _loggingRepository.LogMessageAsync( + new LogMessage + { + Message = $"Source blob download result: {sourceBlobResult.BlobStatus} for path {request.SourceMembershipFilePath}", + RunId = request.RunId + }, + VerbosityLevel.INFO + ); var destinationBlobResult = await _blobStorageRepository.DownloadFileAsync(request.DestinationMembershipFilePath); + await _loggingRepository.LogMessageAsync( + new LogMessage + { + Message = $"Destination blob download result: {destinationBlobResult.BlobStatus} for path {request.DestinationMembershipFilePath}", + RunId = request.RunId + }, + VerbosityLevel.INFO + ); await _blobStorageRepository.DeleteFileAsync(request.SourceMembershipFilePath); await _blobStorageRepository.DeleteFileAsync(request.DestinationMembershipFilePath); From 4c64dce82f5766de72556fc8ceff19fcef1e05cd Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 11 Sep 2024 13:32:58 -0700 Subject: [PATCH 0315/1479] Increase sql db timeout --- Infrastructure/adf/pipeline/azureDataFactory.bicep | 2 +- Infrastructure/data/sqlServer.bicep | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Infrastructure/adf/pipeline/azureDataFactory.bicep b/Infrastructure/adf/pipeline/azureDataFactory.bicep index 8d9cef8d9..f1b276bfa 100644 --- a/Infrastructure/adf/pipeline/azureDataFactory.bicep +++ b/Infrastructure/adf/pipeline/azureDataFactory.bicep @@ -64,7 +64,7 @@ resource linkedService_DestinationDatabase 'Microsoft.DataFactory/factories/link annotations: [] type: 'AzureSqlDatabase' typeProperties: { - connectionString: 'Integrated Security=False;Encrypt=True;Connection Timeout=30;Data Source=${sqlServerUrl};Initial Catalog=${sqlDataBaseName}' + connectionString: 'Integrated Security=False;Encrypt=True;Connection Timeout=90;Data Source=${sqlServerUrl};Initial Catalog=${sqlDataBaseName}' } } dependsOn: [ diff --git a/Infrastructure/data/sqlServer.bicep b/Infrastructure/data/sqlServer.bicep index 8c8699eef..cc9c5c98b 100644 --- a/Infrastructure/data/sqlServer.bicep +++ b/Infrastructure/data/sqlServer.bicep @@ -48,8 +48,8 @@ var sqlServerAdditionalSettings = 'MultipleActiveResultSets=False;Encrypt=True;T var jobsSqlDataBaseName = 'Initial Catalog=${solutionAbbreviation}-data-${environmentAbbreviation};' var replicaJobsSqlDataBaseName = 'Initial Catalog=${solutionAbbreviation}-data-${environmentAbbreviation}-R;' var replicaConnectionString = 'Server=tcp:${replicaSqlServerName}${environment().suffixes.sqlServerHostname},1433;Initial Catalog=${replicaSqlDatabaseName};${sqlServerAdditionalSettings}' -var jobsMSIConnectionString = 'Server=tcp:${sqlServerName}${environment().suffixes.sqlServerHostname},1433;${jobsSqlDataBaseName}Authentication=Active Directory Default;' -var replicaJobsMSIConnectionString = 'Server=tcp:${replicaSqlServerName}${environment().suffixes.sqlServerHostname},1433;${replicaJobsSqlDataBaseName}Authentication=Active Directory Default;' +var jobsMSIConnectionString = 'Server=tcp:${sqlServerName}${environment().suffixes.sqlServerHostname},1433;${jobsSqlDataBaseName}Authentication=Active Directory Default;Connection Timeout=90;' +var replicaJobsMSIConnectionString = 'Server=tcp:${replicaSqlServerName}${environment().suffixes.sqlServerHostname},1433;${replicaJobsSqlDataBaseName}Authentication=Active Directory Default;Connection Timeout=90;' // primary sql server resources resource sqlServer 'Microsoft.Sql/servers@2021-02-01-preview' = { @@ -258,11 +258,11 @@ module secureKeyvaultSecrets 'keyVaultSecretsSecure.bicep' = { } { name: 'sqlServerMSIConnectionString' - value: '${sqlServerUrl}${sqlServerDataBaseName}Authentication=Active Directory Default;TrustServerCertificate=True;Encrypt=True;' + value: '${sqlServerUrl}${sqlServerDataBaseName}Authentication=Active Directory Default;TrustServerCertificate=True;Encrypt=True;Connection Timeout=90;' } { name: 'replicaSqlServerMSIConnectionString' - value: '${sqlServerUrl}${sqlServerDataBaseName}Authentication=Active Directory Default;TrustServerCertificate=True;Encrypt=True;' + value: '${sqlServerUrl}${sqlServerDataBaseName}Authentication=Active Directory Default;TrustServerCertificate=True;Encrypt=True;Connection Timeout=90;' } { name: 'sqlServerName' From c965394f89b96a9f5bbc0f4a5b5f8937417d5d47 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 11 Sep 2024 13:34:58 -0700 Subject: [PATCH 0316/1479] Change verbositylevel to debug --- .../Activity/DeltaCalculator/DeltaCalculatorFunction.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs index 125541645..b64a34df0 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Function/Activity/DeltaCalculator/DeltaCalculatorFunction.cs @@ -49,7 +49,7 @@ await _loggingRepository.LogMessageAsync( Message = $"Source blob download result: {sourceBlobResult.BlobStatus} for path {request.SourceMembershipFilePath}", RunId = request.RunId }, - VerbosityLevel.INFO + VerbosityLevel.DEBUG ); var destinationBlobResult = await _blobStorageRepository.DownloadFileAsync(request.DestinationMembershipFilePath); @@ -59,7 +59,7 @@ await _loggingRepository.LogMessageAsync( Message = $"Destination blob download result: {destinationBlobResult.BlobStatus} for path {request.DestinationMembershipFilePath}", RunId = request.RunId }, - VerbosityLevel.INFO + VerbosityLevel.DEBUG ); await _blobStorageRepository.DeleteFileAsync(request.SourceMembershipFilePath); await _blobStorageRepository.DeleteFileAsync(request.DestinationMembershipFilePath); From 6218adfc8626d32b0e10ff796536a9c9f57c2621 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 25 Sep 2024 14:03:12 -0700 Subject: [PATCH 0317/1479] Delete unused package --- .../Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs index bad8abd68..cf24f7b15 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/OrchestratorTests.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models.Notifications; using Models.ThresholdNotifications; using Repositories.Contracts; From 6535833f4c7feea4724932f61b3bd62bcb73a53a Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Thu, 26 Sep 2024 10:37:26 -0700 Subject: [PATCH 0318/1479] fix if condition --- .../Function/Orchestrator/OrchestratorFunction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs index 31767b86a..cd4c430f3 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs @@ -46,7 +46,7 @@ public async Task RunOrchestratorAsync( var currentQuery = currentPart.Value("source"); var currentQueryAsString = Convert.ToString(currentQuery); - if (string.IsNullOrWhiteSpace(currentQueryAsString) || currentQueryAsString.Contains("ids")) + if (string.IsNullOrWhiteSpace(currentQueryAsString)) { await context.CallActivityAsync( nameof(LoggerFunction), From d17b10228166afbb801856fefd649e38d184ebee Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Mon, 30 Sep 2024 16:16:04 -0700 Subject: [PATCH 0319/1479] Removed the FUNCTIONS_INPROC_NET8_ENABLED setting from functions that have already been converted to isolated model. --- .../Hosts/AzureMaintenance/Infrastructure/compute/template.bicep | 1 - .../Hosts/AzureUserReader/Infrastructure/compute/template.bicep | 1 - .../Infrastructure/compute/template.bicep | 1 - .../Hosts/GraphUpdater/Infrastructure/compute/template.bicep | 1 - .../Infrastructure/compute/template.bicep | 1 - .../Hosts/JobScheduler/Infrastructure/compute/template.bicep | 1 - .../Hosts/JobTrigger/Infrastructure/compute/template.bicep | 1 - .../MembershipAggregator/Infrastructure/compute/template.bicep | 1 - .../Hosts/NonProdService/Infrastructure/compute/template.bicep | 1 - .../Hosts/Notifier/Infrastructure/compute/template.bicep | 1 - .../SqlMembershipObtainer/Infrastructure/compute/template.bicep | 1 - .../Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep | 1 - 12 files changed, 12 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep index cc453f68e..aeb327806 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/template.bicep @@ -97,7 +97,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep index 887a0e0c6..80b4d96cf 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/template.bicep @@ -95,7 +95,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep index b7908b788..4722ee185 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/template.bicep @@ -119,7 +119,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep index 9056d4b38..f01c6540c 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/template.bicep @@ -100,7 +100,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep index 96c406a10..d989e9a27 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/template.bicep @@ -101,7 +101,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep index ef987e2d5..7cb0d73e1 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/template.bicep @@ -106,7 +106,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep index b6784d6b4..785f477d0 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/template.bicep @@ -121,7 +121,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep index 83ef8ffe8..668410b22 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/template.bicep @@ -123,7 +123,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep index d25fa8f60..48e8a8b2a 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/template.bicep @@ -174,7 +174,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep index d0fb6be36..7d810342b 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/template.bicep @@ -124,7 +124,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep index 2b34a1b0d..00f7bb61a 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/template.bicep @@ -124,7 +124,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep index 9c0d558c7..3db9e370c 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/template.bicep @@ -84,7 +84,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { From f2ae1cebc855b897c62609194eaad8c8cf93c349 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 25 Sep 2024 15:55:28 -0700 Subject: [PATCH 0320/1479] Updated servicebus templates --- .../data/serviceBusSubscription.bicep | 17 ++- Infrastructure/data/template.bicep | 136 ++++++++---------- 2 files changed, 68 insertions(+), 85 deletions(-) diff --git a/Infrastructure/data/serviceBusSubscription.bicep b/Infrastructure/data/serviceBusSubscription.bicep index 4af18b40d..10c2cffda 100644 --- a/Infrastructure/data/serviceBusSubscription.bicep +++ b/Infrastructure/data/serviceBusSubscription.bicep @@ -1,17 +1,20 @@ -@minLength(1) -param serviceBusName string +type topicSubscription = { + topicName: string + subscriptionName: string + ruleName: string + ruleSqlExpression: string +} @minLength(1) -param topicName string +param serviceBusName string @metadata({ description: 'Topic\'s subscriptions' - sample: '\\[{\'name\': \'subscriptionOne\', \'ruleName\': \'ruleOne\', \'ruleSqlExpression\': \'Property = \\\'value\\\'\'}]\\]' }) -param topicSubscriptions array +param topicSubscriptions topicSubscription[] resource serviceBusNameSubscription 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2017-04-01' = [for item in topicSubscriptions: { - name: '${serviceBusName}/${topicName}/${item.name}' + name: '${serviceBusName}/${item.topicName}/${item.subscriptionName}' properties: { maxDeliveryCount: 5 lockDuration: 'PT5M' @@ -19,7 +22,7 @@ resource serviceBusNameSubscription 'Microsoft.ServiceBus/namespaces/topics/subs }] resource serviceBusNameSubscriptionRules 'Microsoft.ServiceBus/namespaces/topics/subscriptions/Rules@2017-04-01' = [for item in topicSubscriptions: { - name: '${serviceBusName}/${topicName}/${item.name}/${item.ruleName}' + name: '${serviceBusName}/${item.topicName}/${item.subscriptionName}/${item.ruleName}' properties: { filterType: 'SqlFilter' sqlFilter: { diff --git a/Infrastructure/data/template.bicep b/Infrastructure/data/template.bicep index c3de20a11..48dfdc4aa 100644 --- a/Infrastructure/data/template.bicep +++ b/Infrastructure/data/template.bicep @@ -1,3 +1,10 @@ +type topicSubscription = { + topicName: string + subscriptionName: string + ruleName: string + ruleSqlExpression: string +} + @description('Enter an abbreviation for the solution.') @minLength(2) @maxLength(3) @@ -75,25 +82,8 @@ param serviceBusName string = '${solutionAbbreviation}-${resourceGroupClassifica ]) param serviceBusSku string = 'Standard' -@description('Enter service bus topic name.') -param serviceBusTopicName string = 'syncJobs' - @description('Enter service bus topic\'s subscriptions.') -param serviceBusTopicSubscriptions array = [ - { - name: 'GroupMembership' - ruleName: 'syncType' - ruleSqlExpression: 'Type = \'GroupMembership\'' - } - { - name: 'PlaceMembership' - ruleName: 'syncType' - ruleSqlExpression: 'Type = \'PlaceMembership\'' - } -] - -@description('Enter service bus membership updaters topic\'s and subscriptions details.') -param serviceBusMembershipUpdatersTopicSubscriptions object +param serviceBusTopicSubscriptions topicSubscription[] @description('Enter membership aggregator service bus queue name') param serviceBusMembershipAggregatorQueue string = 'membershipAggregator' @@ -397,7 +387,9 @@ param sqlAdministratorsGroupName string @description('Failed notifications alert threshold.') param notificationAlertThreshold int = 10 -module sqlServer 'sqlServer.bicep' = { +var syncJobsTopicName = 'syncJobs' + +module sqlServer 'sqlServer.bicep' = { name: 'sqlServerTemplate' params: { solutionAbbreviation: solutionAbbreviation @@ -412,15 +404,20 @@ module sqlServer 'sqlServer.bicep' = { sqlAdministratorsGroupName: sqlAdministratorsGroupName tenantId: tenantId } - dependsOn:[ + dependsOn: [ dataKeyVaultTemplate ] } -var isDataKVPresent = !empty(existingDataResources) ? !empty(filter(json(existingDataResources), x => x.Name == keyVaultName && x.ResourceType == 'Microsoft.KeyVault/vaults')) : false +var isDataKVPresent = !empty(existingDataResources) + ? !empty(filter( + json(existingDataResources), + x => x.Name == keyVaultName && x.ResourceType == 'Microsoft.KeyVault/vaults' + )) + : false var graphUserAssignedManagedIdentityName = '${solutionAbbreviation}-identity-${environmentAbbreviation}-Graph' -module dataKeyVaultTemplate 'keyVault.bicep' = if(!isDataKVPresent) { +module dataKeyVaultTemplate 'keyVault.bicep' = if (!isDataKVPresent) { name: 'dataKeyVaultTemplate' params: { name: keyVaultName @@ -437,7 +434,7 @@ module graphUserAssignedManagedIdentity 'userAssignedIdentity.bicep' = { identityName: graphUserAssignedManagedIdentityName location: location } - dependsOn:[ + dependsOn: [ dataKeyVaultTemplate ] } @@ -457,55 +454,34 @@ module serviceBusTemplate 'serviceBus.bicep' = { ] } -module serviceBusTopicTemplate 'serviceBusTopic.bicep' = { - name: 'serviceBusTopicTemplate' - params: { - serviceBusName: serviceBusName - topicName: serviceBusTopicName - } - dependsOn: [ - serviceBusTemplate - logAnalyticsTemplate - ] -} - -module serviceBusSubscriptionsTemplate 'serviceBusSubscription.bicep' = { - name: 'serviceBusSubscriptionsTemplate' - params: { - serviceBusName: serviceBusName - topicName: serviceBusTopicName - topicSubscriptions: serviceBusTopicSubscriptions - } - dependsOn: [ - serviceBusTopicTemplate - logAnalyticsTemplate - ] -} +var allTopics = [for topic in serviceBusTopicSubscriptions: topic.topicName ] +var uniqueTopics = union(allTopics, []) -module serviceBusMembershipUpdatersTopicTemplate 'serviceBusTopic.bicep' = { - name: 'serviceBusMembershipUpdatersTopicTemplate' - params: { - serviceBusName: serviceBusName - topicName: serviceBusMembershipUpdatersTopicSubscriptions.topicName +module serviceBusTopicTemplate 'serviceBusTopic.bicep' = [for topic in uniqueTopics: { + name: '${topic.topicName}-serviceBusTopicTemplate' + params: { + serviceBusName: serviceBusName + topicName: topic.topicName + } + dependsOn: [ + serviceBusTemplate + logAnalyticsTemplate + ] } - dependsOn: [ - serviceBusTemplate - logAnalyticsTemplate - ] -} +] -module serviceBusMembershipUpdatersSubscriptionsTemplate 'serviceBusSubscription.bicep' = { - name: 'serviceBusMembershipUpdatersSubscriptionsTemplate' - params: { - serviceBusName: serviceBusName - topicName: serviceBusMembershipUpdatersTopicSubscriptions.topicName - topicSubscriptions: serviceBusMembershipUpdatersTopicSubscriptions.subscriptions - } - dependsOn: [ - serviceBusMembershipUpdatersTopicTemplate - logAnalyticsTemplate - ] -} +module serviceBusSubscriptionsTemplate 'serviceBusSubscription.bicep' = [ for topic in serviceBusTopicSubscriptions: { + name: '${topic.topicName}-${topic.subscriptionName}-serviceBusSubscriptionsTemplate' + params: { + serviceBusName: serviceBusName + topicSubscriptions: serviceBusTopicSubscriptions + } + dependsOn: [ + serviceBusTopicTemplate + logAnalyticsTemplate + ] + } +] module membershipAggregatorQueue 'serviceBusQueue.bicep' = { name: 'membershipAggregatorQueue' @@ -515,7 +491,7 @@ module membershipAggregatorQueue 'serviceBusQueue.bicep' = { requiresSession: false maxDeliveryCount: 5 } - dependsOn:[ + dependsOn: [ serviceBusTemplate logAnalyticsTemplate ] @@ -529,7 +505,7 @@ module notificationsQueue 'serviceBusQueue.bicep' = { requiresSession: false maxDeliveryCount: 5 } - dependsOn:[ + dependsOn: [ serviceBusTemplate logAnalyticsTemplate ] @@ -543,7 +519,7 @@ module failedNotificationsQueue 'serviceBusQueue.bicep' = { requiresSession: false maxDeliveryCount: 5 } - dependsOn:[ + dependsOn: [ serviceBusTemplate ] } @@ -556,7 +532,7 @@ module syncJobUpdaterQueue 'serviceBusQueue.bicep' = { requiresSession: false maxDeliveryCount: 5 } - dependsOn:[ + dependsOn: [ serviceBusTemplate ] } @@ -568,7 +544,7 @@ module storageAccountTemplate 'storageAccount.bicep' = { keyVaultName: keyVaultName location: location } - dependsOn:[ + dependsOn: [ dataKeyVaultTemplate ] } @@ -582,7 +558,7 @@ module jobsStorageAccountTemplate 'storageAccount.bicep' = { addJobsStorageAccountPolicies: true location: location } - dependsOn:[ + dependsOn: [ dataKeyVaultTemplate ] } @@ -675,11 +651,15 @@ module secretsTemplate 'keyVaultSecrets.bicep' = { } { name: 'serviceBusSyncJobTopic' - value: serviceBusTopicName + value: syncJobsTopicName } { name: 'serviceBusMembershipUpdatersTopic' - value: serviceBusMembershipUpdatersTopicSubscriptions.topicName + value: 'membershipUpdaters' + } + { + name: 'serviceBusMessageSplipperTopic' + value: 'messageSplitter' } { name: 'logAnalyticsCustomerId' @@ -759,5 +739,5 @@ module serviceBusQueueAlert 'serviceBusQueueAlert.bicep' = { output storageAccountName string = storageAccountName output serviceBusName string = serviceBusName -output serviceBusTopicName string = serviceBusTopicName +output serviceBusTopicName string = syncJobsTopicName output isDataKVPresent bool = isDataKVPresent From 760b9d067b794515404d5a392ae14b3bd32ffaf3 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 25 Sep 2024 15:55:42 -0700 Subject: [PATCH 0321/1479] update templates and param files --- .../data/parameters/parameters.int.json | 80 +++++++++++++------ .../data/parameters/parameters.prodv2.json | 80 +++++++++++++------ .../data/parameters/parameters.ua.json | 80 +++++++++++++------ Infrastructure/data/template.bicep | 6 +- 4 files changed, 165 insertions(+), 81 deletions(-) diff --git a/Infrastructure/data/parameters/parameters.int.json b/Infrastructure/data/parameters/parameters.int.json index e890ae990..a21ceb2e3 100644 --- a/Infrastructure/data/parameters/parameters.int.json +++ b/Infrastructure/data/parameters/parameters.int.json @@ -5,28 +5,73 @@ "serviceBusTopicSubscriptions": { "value": [ { - "name": "GroupMembership", + "topicName": "membershipUpdaters", + "subscriptionName": "GraphUpdater", + "ruleName": "updaterType", + "ruleSqlExpression": "Type = 'GroupMembership'" + }, + { + "topicName": "membershipUpdaters", + "subscriptionName": "TeamsChannelUpdater", + "ruleName": "updaterType", + "ruleSqlExpression": "Type = 'TeamsChannelMembership'" + }, + { + "topicName": "syncJobs", + "subscriptionName": "PlaceMembership", + "ruleName": "syncType", + "ruleSqlExpression": "Type = 'PlaceMembership'" + }, + { + "topicName": "syncJobs", + "subscriptionName": "GroupMembership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'GroupMembership'" }, { - "name": "TeamsChannelMembership", + "topicName": "syncJobs", + "subscriptionName": "TeamsChannelMembership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'TeamsChannelMembership'" }, { - "name": "GroupOwnership", + "topicName": "syncJobs", + "subscriptionName": "GroupOwnership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'GroupOwnership'" + }, + { + "topicName": "syncJobs", + "subscriptionName": "SqlMembership", + "ruleName": "syncType", + "ruleSqlExpression": "Type = 'SqlMembership'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Small", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Small'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Medium", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Medium'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Large", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Large'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Onboarding", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Onboarding'" } ] }, - "serviceBusTopicName": { - "value": "syncJobs" - }, - "serviceBusQueueName": { - "value": "membership" - }, "notifierProviderId": { "value": "" }, @@ -47,23 +92,6 @@ }, "sqlSkuCapacity": { "value": 4 - }, - "serviceBusMembershipUpdatersTopicSubscriptions": { - "value": { - "topicName": "membershipUpdaters", - "subscriptions": [ - { - "name": "GraphUpdater", - "ruleName": "updaterType", - "ruleSqlExpression": "Type = 'GroupMembership'" - }, - { - "name": "TeamsChannelUpdater", - "ruleName": "updaterType", - "ruleSqlExpression": "Type = 'TeamsChannelMembership'" - } - ] - } } } } \ No newline at end of file diff --git a/Infrastructure/data/parameters/parameters.prodv2.json b/Infrastructure/data/parameters/parameters.prodv2.json index e2b4c63de..36641fb0b 100644 --- a/Infrastructure/data/parameters/parameters.prodv2.json +++ b/Infrastructure/data/parameters/parameters.prodv2.json @@ -8,28 +8,73 @@ "serviceBusTopicSubscriptions": { "value": [ { - "name": "GroupMembership", + "topicName": "membershipUpdaters", + "subscriptionName": "GraphUpdater", + "ruleName": "updaterType", + "ruleSqlExpression": "Type = 'GroupMembership'" + }, + { + "topicName": "membershipUpdaters", + "subscriptionName": "TeamsChannelUpdater", + "ruleName": "updaterType", + "ruleSqlExpression": "Type = 'TeamsChannelMembership'" + }, + { + "topicName": "syncJobs", + "subscriptionName": "PlaceMembership", + "ruleName": "syncType", + "ruleSqlExpression": "Type = 'PlaceMembership'" + }, + { + "topicName": "syncJobs", + "subscriptionName": "GroupMembership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'GroupMembership'" }, { - "name": "TeamsChannelMembership", + "topicName": "syncJobs", + "subscriptionName": "TeamsChannelMembership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'TeamsChannelMembership'" }, { - "name": "GroupOwnership", + "topicName": "syncJobs", + "subscriptionName": "GroupOwnership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'GroupOwnership'" + }, + { + "topicName": "syncJobs", + "subscriptionName": "SqlMembership", + "ruleName": "syncType", + "ruleSqlExpression": "Type = 'SqlMembership'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Small", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Small'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Medium", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Medium'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Large", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Large'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Onboarding", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Onboarding'" } ] }, - "serviceBusTopicName": { - "value": "syncJobs" - }, - "serviceBusQueueName": { - "value": "membership" - }, "notifierProviderId": { "value": "" }, @@ -50,23 +95,6 @@ }, "sqlSkuCapacity": { "value": 4 - }, - "serviceBusMembershipUpdatersTopicSubscriptions": { - "value": { - "topicName": "membershipUpdaters", - "subscriptions": [ - { - "name": "GraphUpdater", - "ruleName": "updaterType", - "ruleSqlExpression": "Type = 'GroupMembership'" - }, - { - "name": "TeamsChannelUpdater", - "ruleName": "updaterType", - "ruleSqlExpression": "Type = 'TeamsChannelMembership'" - } - ] - } } } } \ No newline at end of file diff --git a/Infrastructure/data/parameters/parameters.ua.json b/Infrastructure/data/parameters/parameters.ua.json index e890ae990..a21ceb2e3 100644 --- a/Infrastructure/data/parameters/parameters.ua.json +++ b/Infrastructure/data/parameters/parameters.ua.json @@ -5,28 +5,73 @@ "serviceBusTopicSubscriptions": { "value": [ { - "name": "GroupMembership", + "topicName": "membershipUpdaters", + "subscriptionName": "GraphUpdater", + "ruleName": "updaterType", + "ruleSqlExpression": "Type = 'GroupMembership'" + }, + { + "topicName": "membershipUpdaters", + "subscriptionName": "TeamsChannelUpdater", + "ruleName": "updaterType", + "ruleSqlExpression": "Type = 'TeamsChannelMembership'" + }, + { + "topicName": "syncJobs", + "subscriptionName": "PlaceMembership", + "ruleName": "syncType", + "ruleSqlExpression": "Type = 'PlaceMembership'" + }, + { + "topicName": "syncJobs", + "subscriptionName": "GroupMembership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'GroupMembership'" }, { - "name": "TeamsChannelMembership", + "topicName": "syncJobs", + "subscriptionName": "TeamsChannelMembership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'TeamsChannelMembership'" }, { - "name": "GroupOwnership", + "topicName": "syncJobs", + "subscriptionName": "GroupOwnership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'GroupOwnership'" + }, + { + "topicName": "syncJobs", + "subscriptionName": "SqlMembership", + "ruleName": "syncType", + "ruleSqlExpression": "Type = 'SqlMembership'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Small", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Small'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Medium", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Medium'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Large", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Large'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Onboarding", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Onboarding'" } ] }, - "serviceBusTopicName": { - "value": "syncJobs" - }, - "serviceBusQueueName": { - "value": "membership" - }, "notifierProviderId": { "value": "" }, @@ -47,23 +92,6 @@ }, "sqlSkuCapacity": { "value": 4 - }, - "serviceBusMembershipUpdatersTopicSubscriptions": { - "value": { - "topicName": "membershipUpdaters", - "subscriptions": [ - { - "name": "GraphUpdater", - "ruleName": "updaterType", - "ruleSqlExpression": "Type = 'GroupMembership'" - }, - { - "name": "TeamsChannelUpdater", - "ruleName": "updaterType", - "ruleSqlExpression": "Type = 'TeamsChannelMembership'" - } - ] - } } } } \ No newline at end of file diff --git a/Infrastructure/data/template.bicep b/Infrastructure/data/template.bicep index 48dfdc4aa..4bffcadde 100644 --- a/Infrastructure/data/template.bicep +++ b/Infrastructure/data/template.bicep @@ -458,10 +458,10 @@ var allTopics = [for topic in serviceBusTopicSubscriptions: topic.topicName ] var uniqueTopics = union(allTopics, []) module serviceBusTopicTemplate 'serviceBusTopic.bicep' = [for topic in uniqueTopics: { - name: '${topic.topicName}-serviceBusTopicTemplate' + name: '${topic}-Template' params: { serviceBusName: serviceBusName - topicName: topic.topicName + topicName: topic } dependsOn: [ serviceBusTemplate @@ -471,7 +471,7 @@ module serviceBusTopicTemplate 'serviceBusTopic.bicep' = [for topic in uniqueTop ] module serviceBusSubscriptionsTemplate 'serviceBusSubscription.bicep' = [ for topic in serviceBusTopicSubscriptions: { - name: '${topic.topicName}-${topic.subscriptionName}-serviceBusSubscriptionsTemplate' + name: '${topic.topicName}-${topic.subscriptionName}-Template' params: { serviceBusName: serviceBusName topicSubscriptions: serviceBusTopicSubscriptions From c1d56efbc7c95578fd1658f147653d15596c5c32 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 30 Sep 2024 13:16:32 -0700 Subject: [PATCH 0322/1479] Removed old parameter --- Deployment/commonResources.bicep | 2 - Deployment/dataResources.bicep | 2 - Deployment/localTemplate.bicep | 2 - Deployment/parameters.json | 79 ++++++++++++++++++++------------ 4 files changed, 49 insertions(+), 36 deletions(-) diff --git a/Deployment/commonResources.bicep b/Deployment/commonResources.bicep index db792b404..82c471f5c 100644 --- a/Deployment/commonResources.bicep +++ b/Deployment/commonResources.bicep @@ -25,7 +25,6 @@ param teamsChannelServiceAccountUsername string // data parameters param notifierProviderId string -param serviceBusMembershipUpdatersTopicSubscriptions object param serviceBusTopicSubscriptions array param sqlAdministratorsGroupId string param sqlAdministratorsGroupName string @@ -85,7 +84,6 @@ module dataResources 'dataResources.bicep' = { environmentAbbreviation: environmentAbbreviation solutionAbbreviation: solutionAbbreviation notifierProviderId: notifierProviderId - serviceBusMembershipUpdatersTopicSubscriptions: serviceBusMembershipUpdatersTopicSubscriptions serviceBusTopicSubscriptions: serviceBusTopicSubscriptions sqlAdministratorsGroupId: sqlAdministratorsGroupId sqlAdministratorsGroupName: sqlAdministratorsGroupName diff --git a/Deployment/dataResources.bicep b/Deployment/dataResources.bicep index 4728abd79..5662eec8c 100644 --- a/Deployment/dataResources.bicep +++ b/Deployment/dataResources.bicep @@ -2,7 +2,6 @@ param location string param environmentAbbreviation string param solutionAbbreviation string param notifierProviderId string -param serviceBusMembershipUpdatersTopicSubscriptions object param serviceBusTopicSubscriptions array param sqlAdministratorsGroupId string param sqlAdministratorsGroupName string @@ -23,7 +22,6 @@ module dataInfrastructureTemplate '../Infrastructure/data/template.bicep' = { environmentAbbreviation: environmentAbbreviation solutionAbbreviation: solutionAbbreviation notifierProviderId: notifierProviderId - serviceBusMembershipUpdatersTopicSubscriptions: serviceBusMembershipUpdatersTopicSubscriptions serviceBusTopicSubscriptions: serviceBusTopicSubscriptions sqlAdministratorsGroupId: sqlAdministratorsGroupId sqlAdministratorsGroupName: sqlAdministratorsGroupName diff --git a/Deployment/localTemplate.bicep b/Deployment/localTemplate.bicep index 1738b5342..f5e7ea1cb 100644 --- a/Deployment/localTemplate.bicep +++ b/Deployment/localTemplate.bicep @@ -33,7 +33,6 @@ param teamsChannelServiceAccountUsername string // data parameters param notifierProviderId string -param serviceBusMembershipUpdatersTopicSubscriptions object param serviceBusTopicSubscriptions array param sqlAdministratorsGroupId string param sqlAdministratorsGroupName string @@ -91,7 +90,6 @@ module gmmResources 'commonResources.bicep' = { teamsChannelServiceAccountPassword: teamsChannelServiceAccountPassword teamsChannelServiceAccountUsername: teamsChannelServiceAccountUsername notifierProviderId: notifierProviderId - serviceBusMembershipUpdatersTopicSubscriptions: serviceBusMembershipUpdatersTopicSubscriptions serviceBusTopicSubscriptions: serviceBusTopicSubscriptions sqlAdministratorsGroupId: sqlAdministratorsGroupId sqlAdministratorsGroupName: sqlAdministratorsGroupName diff --git a/Deployment/parameters.json b/Deployment/parameters.json index 06778ddee..55c8fbdd0 100644 --- a/Deployment/parameters.json +++ b/Deployment/parameters.json @@ -101,54 +101,73 @@ "serviceBusTopicSubscriptions": { "value": [ { - "name": "Organization", - "ruleName": "syncType", - "ruleSqlExpression": "Type = 'Organization'" + "topicName": "membershipUpdaters", + "subscriptionName": "GraphUpdater", + "ruleName": "updaterType", + "ruleSqlExpression": "Type = 'GroupMembership'" }, { - "name": "GroupMembership", - "ruleName": "syncType", - "ruleSqlExpression": "Type = 'GroupMembership'" + "topicName": "membershipUpdaters", + "subscriptionName": "TeamsChannelUpdater", + "ruleName": "updaterType", + "ruleSqlExpression": "Type = 'TeamsChannelMembership'" }, { - "name": "PlaceMembership", + "topicName": "syncJobs", + "subscriptionName": "PlaceMembership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'PlaceMembership'" }, { - "name": "TeamsChannelMembership", - "rulename": "syncType", - "rulesqlexpression": "Type = 'TeamsChannelMembership'" + "topicName": "syncJobs", + "subscriptionName": "GroupMembership", + "ruleName": "syncType", + "ruleSqlExpression": "Type = 'GroupMembership'" + }, + { + "topicName": "syncJobs", + "subscriptionName": "TeamsChannelMembership", + "ruleName": "syncType", + "ruleSqlExpression": "Type = 'TeamsChannelMembership'" }, { - "name": "GroupOwnership", + "topicName": "syncJobs", + "subscriptionName": "GroupOwnership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'GroupOwnership'" }, { - "name": "SqlMembership", + "topicName": "syncJobs", + "subscriptionName": "SqlMembership", "ruleName": "syncType", "ruleSqlExpression": "Type = 'SqlMembership'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Small", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Small'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Medium", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Medium'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Large", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Large'" + }, + { + "topicName": "messageSplitter", + "subscriptionName": "Onboarding", + "ruleName": "jobSize", + "ruleSqlExpression": "laneSize = 'Onboarding'" } ] }, - "serviceBusMembershipUpdatersTopicSubscriptions": { - "value": { - "topicName": "membershipUpdaters", - "subscriptions": [ - { - "name": "GraphUpdater", - "ruleName": "updaterType", - "ruleSqlExpression": "Type = 'GroupMembership'" - }, - { - "name": "TeamsChannelUpdater", - "ruleName": "updaterType", - "ruleSqlExpression": "Type = 'TeamsChannelMembership'" - } - ] - } - }, "sharepointDomain": { "value": "m365x00000000.sharepoint.com", "metadata": { @@ -185,7 +204,7 @@ } }, "pipeline": { - "value": "PopulateDestinationPipeline" + "value": "PopulateDestinationPipeline_demo" }, "setRBACPermissions": { "value": true, From 9148dd188bf30174f3e4d785ffebca3a7aac5d17 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Mon, 30 Sep 2024 14:24:23 -0700 Subject: [PATCH 0323/1479] Updated "Build json templates" task to fail in case of errors --- yaml/build-release-package.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/yaml/build-release-package.yml b/yaml/build-release-package.yml index e4b57f68d..83e04c4ff 100644 --- a/yaml/build-release-package.yml +++ b/yaml/build-release-package.yml @@ -45,9 +45,18 @@ stages: targetType: inline workingDirectory: 'Deployment' script: | - $templateFiles = Get-ChildItem -Path . -Filter "*.bicep" - foreach ($templateFile in $templateFiles) { - az bicep build --file $templateFile.FullName + try { + $templateFiles = Get-ChildItem -Path . -Filter "*.bicep" + foreach ($templateFile in $templateFiles) { + az bicep build --file $templateFile.FullName + if ($LASTEXITCODE -ne 0) { + Write-Error "az bicep build failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } + } + } catch { + Write-Error "An error occurred: $_" + exit 1 } pwsh: true From e25ec570b5504f333355806b4416d9058dd8907a Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 1 Oct 2024 10:21:31 -0700 Subject: [PATCH 0324/1479] Fixed typo --- Infrastructure/data/template.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Infrastructure/data/template.bicep b/Infrastructure/data/template.bicep index 4bffcadde..d6f5cf94f 100644 --- a/Infrastructure/data/template.bicep +++ b/Infrastructure/data/template.bicep @@ -658,7 +658,7 @@ module secretsTemplate 'keyVaultSecrets.bicep' = { value: 'membershipUpdaters' } { - name: 'serviceBusMessageSplipperTopic' + name: 'serviceBusMessageSplitterTopic' value: 'messageSplitter' } { From c69536a31f757acc0d4bea45b6fee87a306f4c25 Mon Sep 17 00:00:00 2001 From: yuqingyang Date: Wed, 21 Aug 2024 10:33:56 -0700 Subject: [PATCH 0325/1479] Change code coverage From 88df5f7e147439b56808f4f86e22d5b1eefd54b3 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 11 Sep 2024 14:08:46 -0700 Subject: [PATCH 0326/1479] Updated ADF bicep templates --- Deployment/Deploy-Resources.ps1 | 2 +- Deployment/adfHRResources.bicep | 2 +- .../adf/pipeline/azureDataFactory.bicep | 630 ++++++++++++++++-- Infrastructure/adf/pipeline/template.bicep | 10 +- 4 files changed, 567 insertions(+), 77 deletions(-) diff --git a/Deployment/Deploy-Resources.ps1 b/Deployment/Deploy-Resources.ps1 index 73d0d1ceb..001e728e9 100644 --- a/Deployment/Deploy-Resources.ps1 +++ b/Deployment/Deploy-Resources.ps1 @@ -125,7 +125,7 @@ function Set-Subscription { } function Set-ResourceProviders { - foreach ($namespace in @("Microsoft.ServiceBus", "Microsoft.Insights", "Microsoft.OperationalInsights", "Microsoft.AlertsManagement", "Microsoft.Storage", "Microsoft.AppConfiguration", "Microsoft.Sql", "Microsoft.Web", "Microsoft.DataFactory")) { + foreach ($namespace in @("Microsoft.ServiceBus", "Microsoft.Insights", "Microsoft.OperationalInsights", "Microsoft.AlertsManagement", "Microsoft.Storage", "Microsoft.AppConfiguration", "Microsoft.Sql", "Microsoft.Web", "Microsoft.DataFactory", "Microsoft.SignalRService")) { Write-Host "Checking if the resource provider $namespace is registered..." $provider = Get-AzResourceProvider -ProviderNamespace $namespace diff --git a/Deployment/adfHRResources.bicep b/Deployment/adfHRResources.bicep index 82e17a581..3d8bf79c2 100644 --- a/Deployment/adfHRResources.bicep +++ b/Deployment/adfHRResources.bicep @@ -27,7 +27,7 @@ module adfForHRData '../Infrastructure/adf/pipeline/template.bicep' = { solutionAbbreviation: solutionAbbreviation tenantId: tenantId sqlServerName: sqlServerName - sqlDataBaseName: sqlDataBaseName + sqlDatabaseName: sqlDataBaseName } dependsOn: [ sqlForHRData diff --git a/Infrastructure/adf/pipeline/azureDataFactory.bicep b/Infrastructure/adf/pipeline/azureDataFactory.bicep index f1b276bfa..246a549e7 100644 --- a/Infrastructure/adf/pipeline/azureDataFactory.bicep +++ b/Infrastructure/adf/pipeline/azureDataFactory.bicep @@ -1,6 +1,9 @@ @description('Name of Azure Data Factory') param factoryName string +@description('Resource name suffix') +param resourceSuffix string + @description('Enter an abbreviation for the environment') param environmentAbbreviation string @@ -10,8 +13,8 @@ param location string @description('Name of SQL Server') param sqlServerName string -@description('Name of SQL Server') -param sqlDataBaseName string +@description('Name of SQL Database') +param sqlDatabaseName string @description('AzureUserReader function url.') @secure() @@ -25,11 +28,23 @@ param azureUserReaderFunctionKey string @secure() param storageAccountConnectionString string -var sqlServerUrl = '${sqlServerName}${environment().suffixes.sqlServerHostname}' - +var dataFactoryName = factoryName +var azureBlobStorageLinkedService = 'AzureBlobStorage_${resourceSuffix}' +var destinationDatabaseLinkedService = 'DestinationDatabase_${resourceSuffix}' +var azureUserReaderLinkedService = 'AzureUserReader_${resourceSuffix}' +var populateDestinationPipelineName = 'PopulateDestinationPipeline_${resourceSuffix}' +var destinationTableDataSet = 'DestinationTable_${resourceSuffix}' +var mappingsTableDataSet = 'MappingsTable_${resourceSuffix}' +var jobLevelMapDataSet = 'JobLevelMap_${resourceSuffix}' +var jobTitleMapDataSet = 'JobTitleMap_${resourceSuffix}' +var countryMapDataSet = 'CountryMap_${resourceSuffix}' +var memberIdsDataSet = 'MemberIds_${resourceSuffix}' +var memberHRDataDataSet = 'MemberHRData_${resourceSuffix}' +var populateMappingsTableDataFlow = 'PopulateMappingsTableDataFlow_${resourceSuffix}' +var populateDestinationDataFlow = 'PopulateDestinationDataFlow_${resourceSuffix}' resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' = { - name: factoryName + name: dataFactoryName identity: { type: 'SystemAssigned' } @@ -45,7 +60,8 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' = { } resource linkedService_AzureBlobStorage 'Microsoft.DataFactory/factories/linkedServices@2018-06-01' = { - name: '${factoryName}/AzureBlobStorage' + parent: dataFactory + name: azureBlobStorageLinkedService properties: { annotations: [] type: 'AzureBlobStorage' @@ -53,27 +69,23 @@ resource linkedService_AzureBlobStorage 'Microsoft.DataFactory/factories/linkedS connectionString: storageAccountConnectionString } } - dependsOn: [ - dataFactory - ] } -resource linkedService_DestinationDatabase 'Microsoft.DataFactory/factories/linkedservices@2018-06-01' = { - name: '${factoryName}/DestinationDatabase' +resource linkedService_DestinationDatabase 'Microsoft.DataFactory/factories/linkedServices@2018-06-01' = { + parent: dataFactory + name: destinationDatabaseLinkedService properties: { annotations: [] type: 'AzureSqlDatabase' typeProperties: { - connectionString: 'Integrated Security=False;Encrypt=True;Connection Timeout=90;Data Source=${sqlServerUrl};Initial Catalog=${sqlDataBaseName}' + connectionString: 'Integrated Security=False;Encrypt=True;Connection Timeout=90;Data Source=${sqlServerName}.database.windows.net;Initial Catalog=${sqlDatabaseName}' } } - dependsOn: [ - dataFactory - ] } resource linkedService_AzureUserReader 'Microsoft.DataFactory/factories/linkedServices@2018-06-01' = { - name: '${factoryName}/AzureUserReader' + parent: dataFactory + name: azureUserReaderLinkedService properties: { annotations: [] type: 'AzureFunction' @@ -86,18 +98,17 @@ resource linkedService_AzureUserReader 'Microsoft.DataFactory/factories/linkedSe authentication: 'Anonymous' } } - dependsOn: [ - dataFactory - ] + dependsOn: [] } resource Pipeline_PopulateDestinationPipeline 'Microsoft.DataFactory/factories/pipelines@2018-06-01' = { - name: '${factoryName}/PopulateDestinationPipeline' + parent: dataFactory + name: populateDestinationPipelineName properties: { activities: [ { - name: 'Create users schema' - type: 'Script' + name: 'AzureUserReader' + type: 'AzureFunctionActivity' dependsOn: [] policy: { timeout: '0.12:00:00' @@ -107,62 +118,180 @@ resource Pipeline_PopulateDestinationPipeline 'Microsoft.DataFactory/factories/p secureInput: false } userProperties: [] + typeProperties: { + functionName: { + value: 'StarterFunction' + type: 'Expression' + } + body: { + value: '{"ContainerName":"csvcontainer","BlobPath":"memberids.csv"}' + type: 'Expression' + } + headers: {} + method: 'POST' + } + linkedServiceName: { + referenceName: azureUserReaderLinkedService + type: 'LinkedServiceReference' + } + } + { + name: 'PopulateDestinationDataFlow' + type: 'ExecuteDataFlow' + dependsOn: [ + { + activity: 'CreateSchemas' + dependencyConditions: [ + 'Succeeded' + ] + } + { + activity: 'AzureUserReader' + dependencyConditions: [ + 'Succeeded' + ] + } + ] + policy: { + timeout: '0.12:00:00' + retry: 0 + retryIntervalInSeconds: 30 + secureOutput: false + secureInput: false + } + userProperties: [] + typeProperties: { + dataFlow: { + referenceName: populateDestinationDataFlow + type: 'DataFlowReference' + parameters: {} + datasetParameters: { + memberids: {} + memberHRData: {} + sink: { + TableName: { + value: '@{replace(pipeline().RunId,\'-\',\'\')}' + type: 'Expression' + } + } + } + } + staging: {} + compute: { + coreCount: 8 + computeType: 'General' + } + traceLevel: 'Fine' + } + } + { + name: 'Add Height Column' + type: 'Script' + dependsOn: [ + { + activity: 'PopulateDestinationDataFlow' + dependencyConditions: [ + 'Succeeded' + ] + } + ] + policy: { + timeout: '7.00:00:00' + retry: 0 + retryIntervalInSeconds: 30 + secureOutput: false + secureInput: false + } + userProperties: [] linkedServiceName: { - referenceName: 'DestinationDatabase' + referenceName: destinationDatabaseLinkedService type: 'LinkedServiceReference' } typeProperties: { scripts: [ { - type: 'NonQuery' - text: 'IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = \'users\')\nBEGIN\n EXEC(\'CREATE SCHEMA users\')\nEND' + type: 'Query' + text: { + value: '@concat(\'ALTER TABLE users.[\',replace(pipeline().RunId,\'-\',\'\'),\'] ADD Height int\')' + type: 'Expression' + } } ] scriptBlockExecutionTimeout: '02:00:00' } } { - name: 'AzureUserReader' - type: 'AzureFunctionActivity' + name: 'Calculate Height' + type: 'Script' dependsOn: [ { - activity: 'Create users schema' + activity: 'Add Height Column' dependencyConditions: [ 'Succeeded' ] } ] policy: { - timeout: '0.12:00:00' + timeout: '7.00:00:00' retry: 0 retryIntervalInSeconds: 30 secureOutput: false secureInput: false } userProperties: [] + linkedServiceName: { + referenceName: destinationDatabaseLinkedService + type: 'LinkedServiceReference' + } typeProperties: { - functionName: { - value: 'StarterFunction' - type: 'Expression' - } - method: 'POST' - headers: {} - body: { - value: '{"ContainerName":"csvcontainer","BlobPath":"memberids.csv"}' - type: 'Expression' - } + scripts: [ + { + type: 'Query' + text: { + value: '@concat(\'\n\nWITH DirectReports(Email, RootId, ManagerId, EmployeeId, RelativeEmployeeLevel) AS\n(\n\tSELECT Email, EmployeeId RootId, ManagerId, EmployeeId, 0 AS RelativeEmployeeLevel\n\tFROM users.[\',replace(pipeline().RunId,\'-\',\'\'),\']\t\n\tUNION ALL\n\tSELECT d.Email, d.RootId, e.ManagerId, e.EmployeeId, RelativeEmployeeLevel + 1\n\tFROM users.[\',replace(pipeline().RunId,\'-\',\'\'),\'] AS e\n\t\tINNER JOIN DirectReports AS d\n\t\tON e.ManagerId = d.EmployeeId\t \n), q2 as\n(\n SELECT Email,\n RootId,\n ManagerId,\n EmployeeId,\n RelativeEmployeeLevel,\n max(RelativeEmployeeLevel) over (partition by RootId) - RelativeEmployeeLevel LevelsBelow\n FROM DirectReports\n), ForUpd as\n(SELECT rootId, EmployeeId, LevelsBelow FROM q2 where rootid = EmployeeId)\n\nUPDATE users.[\',replace(pipeline().RunId,\'-\',\'\'),\'] SET Height = B.LevelsBelow\nFROM users.[\',replace(pipeline().RunId,\'-\',\'\'),\'] A\nJOIN ForUpd B ON A.EmployeeId = B.EmployeeId\n\n\')' + type: 'Expression' + } + } + ] + scriptBlockExecutionTimeout: '02:00:00' + } + } + { + name: 'CreateSchemas' + type: 'Script' + dependsOn: [] + policy: { + timeout: '0.12:00:00' + retry: 0 + retryIntervalInSeconds: 30 + secureOutput: false + secureInput: false } + userProperties: [] linkedServiceName: { - referenceName: 'AzureUserReader' + referenceName: destinationDatabaseLinkedService type: 'LinkedServiceReference' } + typeProperties: { + scripts: [ + { + type: 'NonQuery' + text: 'IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = \'users\')\nBEGIN\n EXEC(\'CREATE SCHEMA users\')\nEND' + } + { + type: 'NonQuery' + text: 'IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = \'mappings\')\nBEGIN\n EXEC(\'CREATE SCHEMA mappings\')\nEND' + } + ] + scriptBlockExecutionTimeout: '02:00:00' + } } { - name: 'PopulateDestinationDataFlow' + name: 'PopulateMappingsTableDataFlow' type: 'ExecuteDataFlow' dependsOn: [ { - activity: 'AzureUserReader' + activity: 'CreateSchemas' dependencyConditions: [ 'Succeeded' ] @@ -178,15 +307,16 @@ resource Pipeline_PopulateDestinationPipeline 'Microsoft.DataFactory/factories/p userProperties: [] typeProperties: { dataFlow: { - referenceName: 'PopulateDestinationDataFlow' + referenceName: populateMappingsTableDataFlow type: 'DataFlowReference' parameters: {} datasetParameters: { - memberids: {} - memberHRData: {} - sink: { + CountryMap: {} + JobLevelMap: {} + JobTitleMap: {} + sink1: { TableName: { - value: 'tbl@{replace(pipeline().RunId,\'-\',\'\')}' + value: '@{replace(pipeline().RunId,\'-\',\'\')}' type: 'Expression' } } @@ -207,26 +337,30 @@ resource Pipeline_PopulateDestinationPipeline 'Microsoft.DataFactory/factories/p annotations: [] } dependsOn: [ - dataFactory dataFlow_PopulateDestinationDataFlow - linkedService_AzureUserReader ] } resource dataSet_DestinationTable 'Microsoft.DataFactory/factories/datasets@2018-06-01' = { - name: '${factoryName}/DestinationTable' + parent: dataFactory + name: destinationTableDataSet properties: { linkedServiceName: { - referenceName: 'DestinationDatabase' + referenceName: destinationDatabaseLinkedService type: 'LinkedServiceReference' } + parameters: { + TableName: { + type: 'String' + } + } annotations: [] - type: 'AzureSqlTable' + type: 'SqlServerTable' schema: [] typeProperties: { schema: 'users' table: { - value: '@replace(pipeline().RunId,\'-\',\'\')' + value: '@dataset().TableName' type: 'Expression' } } @@ -236,11 +370,41 @@ resource dataSet_DestinationTable 'Microsoft.DataFactory/factories/datasets@2018 ] } -resource dataSet_DelimitedText 'Microsoft.DataFactory/factories/datasets@2018-06-01' = { - name: '${factoryName}/DelimitedText' +resource dataSet_MappingsTable 'Microsoft.DataFactory/factories/datasets@2018-06-01' = { + parent: dataFactory + name: mappingsTableDataSet properties: { linkedServiceName: { - referenceName: 'AzureBlobStorage' + referenceName: destinationDatabaseLinkedService + type: 'LinkedServiceReference' + } + parameters: { + TableName: { + type: 'string' + } + } + annotations: [] + type: 'AzureSqlTable' + schema: [] + typeProperties: { + schema: 'mappings' + table: { + value: '@dataset().TableName' + type: 'Expression' + } + } + } + dependsOn: [ + linkedService_DestinationDatabase + ] +} + +resource dataSet_JobLevelMap 'Microsoft.DataFactory/factories/datasets@2018-06-01' = { + parent: dataFactory + name: jobLevelMapDataSet + properties: { + linkedServiceName: { + referenceName: azureBlobStorageLinkedService type: 'LinkedServiceReference' } annotations: [] @@ -248,6 +412,7 @@ resource dataSet_DelimitedText 'Microsoft.DataFactory/factories/datasets@2018-06 typeProperties: { location: { type: 'AzureBlobStorageLocation' + fileName: 'jobLevelMap.csv' container: 'csvcontainer' } columnDelimiter: ',' @@ -255,29 +420,347 @@ resource dataSet_DelimitedText 'Microsoft.DataFactory/factories/datasets@2018-06 firstRowAsHeader: true quoteChar: '"' } - schema: [] + schema: [ + { + name: 'JobLevelCode' + type: 'String' + } + { + name: 'JobLevelDesc' + type: 'String' + } + ] } dependsOn: [ linkedService_AzureBlobStorage ] } +resource dataSet_JobTitleMap 'Microsoft.DataFactory/factories/datasets@2018-06-01' = { + parent: dataFactory + name: jobTitleMapDataSet + properties: { + linkedServiceName: { + referenceName: azureBlobStorageLinkedService + type: 'LinkedServiceReference' + } + annotations: [] + type: 'DelimitedText' + typeProperties: { + location: { + type: 'AzureBlobStorageLocation' + fileName: 'jobTitleMap.csv' + container: 'csvcontainer' + } + columnDelimiter: ',' + escapeChar: '\\' + firstRowAsHeader: true + quoteChar: '"' + } + schema: [ + { + name: 'JobTitleCode' + type: 'String' + } + { + name: 'JobTitleDesc' + type: 'String' + } + ] + } + dependsOn: [ + linkedService_AzureBlobStorage + ] +} + +resource dataSet_CountryMap 'Microsoft.DataFactory/factories/datasets@2018-06-01' = { + parent: dataFactory + name: countryMapDataSet + properties: { + linkedServiceName: { + referenceName: azureBlobStorageLinkedService + type: 'LinkedServiceReference' + } + annotations: [] + type: 'DelimitedText' + typeProperties: { + location: { + type: 'AzureBlobStorageLocation' + fileName: 'countryMap.csv' + container: 'csvcontainer' + } + columnDelimiter: ',' + escapeChar: '\\' + firstRowAsHeader: true + quoteChar: '"' + } + schema: [ + { + name: 'CountryCode' + type: 'String' + } + { + name: 'CountryDesc' + type: 'String' + } + ] + } + dependsOn: [ + linkedService_AzureBlobStorage + ] +} + +resource dataSet_MemberIds 'Microsoft.DataFactory/factories/datasets@2018-06-01' = { + parent: dataFactory + name: memberIdsDataSet + properties: { + linkedServiceName: { + referenceName: azureBlobStorageLinkedService + type: 'LinkedServiceReference' + } + annotations: [] + type: 'DelimitedText' + typeProperties: { + location: { + type: 'AzureBlobStorageLocation' + fileName: 'memberids.csv' + container: 'csvcontainer' + } + columnDelimiter: ',' + escapeChar: '\\' + firstRowAsHeader: true + quoteChar: '"' + } + schema: [ + { + name: 'PersonnelNumber' + type: 'String' + } + { + name: 'AzureObjectId' + type: 'String' + } + { + name: 'UserPrincipalName' + type: 'String' + } + ] + } + dependsOn: [ + linkedService_AzureBlobStorage + ] +} + +resource dataSet_MemberHRData 'Microsoft.DataFactory/factories/datasets@2018-06-01' = { + parent: dataFactory + name: memberHRDataDataSet + properties: { + linkedServiceName: { + referenceName: azureBlobStorageLinkedService + type: 'LinkedServiceReference' + } + annotations: [] + type: 'DelimitedText' + typeProperties: { + location: { + type: 'AzureBlobStorageLocation' + fileName: 'memberHRData.csv' + container: 'csvcontainer' + } + columnDelimiter: ',' + escapeChar: '\\' + firstRowAsHeader: true + quoteChar: '"' + } + schema: [ + { + name: 'EmployeeIdentificationNumber' + type: 'String' + } + { + name: 'ManagerIdentificationNumber' + type: 'String' + } + { + name: 'Department' + type: 'String' + } + { + name: 'JobTitleCode' + type: 'String' + } + { + name: 'JobLevelCode' + type: 'String' + } + { + name: 'CountryCode' + type: 'String' + } + ] + } + dependsOn: [ + linkedService_AzureBlobStorage + ] +} + +resource dataFlow_PopulateMappingsTableDataFlow 'Microsoft.DataFactory/factories/dataflows@2018-06-01' = { + parent: dataFactory + name: populateMappingsTableDataFlow + properties: { + type: 'MappingDataFlow' + typeProperties: { + sources: [ + { + dataset: { + referenceName: countryMapDataSet + type: 'DatasetReference' + } + name: 'CountryMap' + description: 'Import data from countryMap.csv' + } + { + dataset: { + referenceName: jobLevelMapDataSet + type: 'DatasetReference' + } + name: 'JobLevelMap' + description: 'Import data from jobLevelMap.csv' + } + { + dataset: { + referenceName: jobTitleMapDataSet + type: 'DatasetReference' + } + name: 'JobTitleMap' + description: 'Import data from jobTitleMap.csv' + } + ] + sinks: [ + { + dataset: { + referenceName: mappingsTableDataSet + type: 'DatasetReference' + } + name: 'sink1' + } + ] + transformations: [ + { + name: 'CreateNewColumns1' + } + { + name: 'CreateNewColumns2' + } + { + name: 'CreateNewColumns3' + } + { + name: 'CountryMapSelect' + } + { + name: 'JobLevelSelect' + } + { + name: 'JobTitleMapSelect' + } + { + name: 'union1' + } + ] + scriptLines: [ + 'source(output(' + ' CountryCode as string,' + ' CountryDesc as string' + ' ),' + ' allowSchemaDrift: true,' + ' validateSchema: false,' + ' ignoreNoFilesFound: false,' + ' wildcardPaths:[\'countryMap.csv\']) ~> CountryMap' + 'source(output(' + ' JobLevelCode as string,' + ' JobLevelDesc as string' + ' ),' + ' allowSchemaDrift: true,' + ' validateSchema: false,' + ' ignoreNoFilesFound: false) ~> JobLevelMap' + 'source(output(' + ' JobTitleCode as string,' + ' JobTitleDesc as string' + ' ),' + ' allowSchemaDrift: true,' + ' validateSchema: false,' + ' ignoreNoFilesFound: false) ~> JobTitleMap' + 'CountryMap derive(ColumnName = \'Country\',' + ' Code = CountryCode,' + ' Description = CountryDesc) ~> CreateNewColumns1' + 'JobLevelMap derive(ColumnName = \'JobLevel\',' + ' Code = JobLevelCode,' + ' Description = JobLevelDesc) ~> CreateNewColumns2' + 'JobTitleMap derive(ColumnName = \'JobTitleMap\',' + ' Code = JobTitleCode,' + ' Description = JobTitleDesc) ~> CreateNewColumns3' + 'CreateNewColumns1 select(mapColumn(' + ' ColumnName,' + ' Code,' + ' Description' + ' ),' + ' skipDuplicateMapInputs: true,' + ' skipDuplicateMapOutputs: true) ~> CountryMapSelect' + 'CreateNewColumns2 select(mapColumn(' + ' ColumnName,' + ' Code,' + ' Description' + ' ),' + ' skipDuplicateMapInputs: true,' + ' skipDuplicateMapOutputs: true) ~> JobLevelSelect' + 'CreateNewColumns3 select(mapColumn(' + ' ColumnName,' + ' Code,' + ' Description' + ' ),' + ' skipDuplicateMapInputs: true,' + ' skipDuplicateMapOutputs: true) ~> JobTitleMapSelect' + 'CountryMapSelect, JobLevelSelect, JobTitleMapSelect union(byName: true)~> union1' + 'union1 sink(allowSchemaDrift: true,' + ' validateSchema: false,' + ' deletable:false,' + ' insertable:true,' + ' updateable:false,' + ' upsertable:false,' + ' format: \'table\',' + ' skipDuplicateMapInputs: true,' + ' skipDuplicateMapOutputs: true,' + ' saveOrder: 1,' + ' errorHandlingOption: \'stopOnFirstError\') ~> sink1' + ] + } + } + dependsOn: [ + dataSet_CountryMap + dataSet_JobLevelMap + dataSet_JobTitleMap + dataSet_MappingsTable + ] +} + resource dataFlow_PopulateDestinationDataFlow 'Microsoft.DataFactory/factories/dataflows@2018-06-01' = { - name: '${factoryName}/PopulateDestinationDataFlow' + parent: dataFactory + name: populateDestinationDataFlow properties: { type: 'MappingDataFlow' typeProperties: { sources: [ { dataset: { - referenceName: 'DelimitedText' + referenceName: memberIdsDataSet type: 'DatasetReference' } name: 'memberids' } { dataset: { - referenceName: 'DelimitedText' + referenceName: memberHRDataDataSet type: 'DatasetReference' } name: 'memberHRData' @@ -286,7 +769,7 @@ resource dataFlow_PopulateDestinationDataFlow 'Microsoft.DataFactory/factories/d sinks: [ { dataset: { - referenceName: 'DestinationTable' + referenceName: destinationTableDataSet type: 'DatasetReference' } name: 'sink' @@ -299,7 +782,7 @@ resource dataFlow_PopulateDestinationDataFlow 'Microsoft.DataFactory/factories/d ] scriptLines: [ 'source(output(' - ' PersonnelNumber as integer,' + ' PersonnelNumber as string,' ' AzureObjectId as string,' ' UserPrincipalName as string' ' ),' @@ -308,11 +791,12 @@ resource dataFlow_PopulateDestinationDataFlow 'Microsoft.DataFactory/factories/d ' ignoreNoFilesFound: false,' ' wildcardPaths:[\'memberids.csv\']) ~> memberids' 'source(output(' - ' EmployeeIdentificationNumber as integer,' - ' ManagerIdentificationNumber as integer,' - ' Position as string,' - ' Level as integer,' - ' Country as string' + ' EmployeeIdentificationNumber as string,' + ' ManagerIdentificationNumber as string,' + ' Department as string,' + ' JobTitleCode as string,' + ' JobLevelCode as string,' + ' CountryCode as string' ' ),' ' allowSchemaDrift: true,' ' validateSchema: false,' @@ -333,18 +817,20 @@ resource dataFlow_PopulateDestinationDataFlow 'Microsoft.DataFactory/factories/d ' mapColumn(' ' AzureObjectId,' ' Email = UserPrincipalName,' - ' EmployeeId = EmployeeIdentificationNumber,' + ' EmployeeId = PersonnelNumber,' ' ManagerId = ManagerIdentificationNumber,' - ' Position,' - ' Level,' - ' Country' + ' JobTitle_Code = JobTitleCode,' + ' JobLevel_Code = JobLevelCode,' + ' Country_Code = CountryCode,' + ' Department' ' )) ~> sink' ] } } dependsOn: [ - dataSet_DelimitedText dataSet_DestinationTable + dataSet_MemberIds + dataSet_MemberHRData ] } diff --git a/Infrastructure/adf/pipeline/template.bicep b/Infrastructure/adf/pipeline/template.bicep index c4331ec31..9bce43fea 100644 --- a/Infrastructure/adf/pipeline/template.bicep +++ b/Infrastructure/adf/pipeline/template.bicep @@ -17,12 +17,15 @@ param tenantId string @description('Name of SQL Server') param sqlServerName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' -@description('Name of SQL Server') -param sqlDataBaseName string = '${solutionAbbreviation}-data-${environmentAbbreviation}-destination' +@description('Name of SQL database name') +param sqlDatabaseName string = '${solutionAbbreviation}-data-${environmentAbbreviation}-adf' @description('Name of Azure Data Factory') param azureDataFactoryName string = '${solutionAbbreviation}-data-${environmentAbbreviation}-adf' +@description('Resource name suffix') +param resourceSuffix string = 'DEMO' + var dataKeyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' resource dataKeyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = { @@ -33,11 +36,12 @@ resource dataKeyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = { module azureDataFactoryTemplate 'azureDataFactory.bicep' = { name: 'azureDataFactoryTemplate' params: { + resourceSuffix: toLower(resourceSuffix) factoryName: azureDataFactoryName environmentAbbreviation: environmentAbbreviation location: location sqlServerName: sqlServerName - sqlDataBaseName: sqlDataBaseName + sqlDatabaseName: sqlDatabaseName azureUserReaderUrl: dataKeyVault.getSecret('azureUserReaderUrl') azureUserReaderFunctionKey: dataKeyVault.getSecret('azureUserReaderKey') storageAccountConnectionString: dataKeyVault.getSecret('adfStorageAccountConnectionString') From b5c61944a20c702253edb7090f17d03e5508db85 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 11 Sep 2024 16:05:32 -0700 Subject: [PATCH 0327/1479] Update Set-AppConfigurationManagedIdentityRoles logic to check for role assignment --- .../PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 b/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 index 6fe013453..f003148c8 100644 --- a/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 +++ b/Scripts/PostDeployment/Set-AppConfigurationManagedIdentityRoles.ps1 @@ -60,7 +60,7 @@ function Set-AppConfigurationManagedIdentityRoles if ($appServicePrincipal) { - if ($null -eq (Get-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $appConfigObject.Id)) + if ($null -eq (Get-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $appConfigObject.Id -RoleDefinitionName "App Configuration Data Reader")) { $assignment = New-AzRoleAssignment -ObjectId $appServicePrincipal.Id -Scope $appConfigObject.Id -RoleDefinitionName "App Configuration Data Reader"; if ($assignment) { From 98934c24df7c4c607f60dbb27c5ce4d9980ba548 Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Wed, 18 Sep 2024 13:57:11 -0700 Subject: [PATCH 0328/1479] Cast columns to right data type --- Infrastructure/adf/pipeline/azureDataFactory.bicep | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Infrastructure/adf/pipeline/azureDataFactory.bicep b/Infrastructure/adf/pipeline/azureDataFactory.bicep index 246a549e7..3c3ac9edf 100644 --- a/Infrastructure/adf/pipeline/azureDataFactory.bicep +++ b/Infrastructure/adf/pipeline/azureDataFactory.bicep @@ -779,6 +779,9 @@ resource dataFlow_PopulateDestinationDataFlow 'Microsoft.DataFactory/factories/d { name: 'join' } + { + name: 'CastColumns' + } ] scriptLines: [ 'source(output(' @@ -807,7 +810,13 @@ resource dataFlow_PopulateDestinationDataFlow 'Microsoft.DataFactory/factories/d ' matchType:\'exact\',' ' ignoreSpaces: false,' ' broadcast: \'auto\')~> join' - 'join sink(allowSchemaDrift: true,' + 'join cast(output(' + ' PersonnelNumber as integer,' + ' EmployeeIdentificationNumber as integer,' + ' ManagerIdentificationNumber as integer' + ' ),' + ' errors: true) ~> CastColumns' + 'CastColumns sink(allowSchemaDrift: true,' ' validateSchema: false,' ' deletable:false,' ' insertable:true,' From 919058ff102abc406b1034fdf6f968138b99477c Mon Sep 17 00:00:00 2001 From: Alberto Rios Date: Tue, 1 Oct 2024 10:30:42 -0700 Subject: [PATCH 0329/1479] Lower case suffix --- Infrastructure/adf/pipeline/template.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Infrastructure/adf/pipeline/template.bicep b/Infrastructure/adf/pipeline/template.bicep index 9bce43fea..2dbb27399 100644 --- a/Infrastructure/adf/pipeline/template.bicep +++ b/Infrastructure/adf/pipeline/template.bicep @@ -24,7 +24,7 @@ param sqlDatabaseName string = '${solutionAbbreviation}-data-${environmentAbbrev param azureDataFactoryName string = '${solutionAbbreviation}-data-${environmentAbbreviation}-adf' @description('Resource name suffix') -param resourceSuffix string = 'DEMO' +param resourceSuffix string = 'demo' var dataKeyVaultName = '${solutionAbbreviation}-data-${environmentAbbreviation}' From 2066d66150ecbfa1fc99f7914f48af7404d718e8 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 30 Sep 2024 13:34:33 -0700 Subject: [PATCH 0330/1479] update retry policy --- .../SqlMembershipRepository.cs | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs index 0108b9660..dccbaa150 100644 --- a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs @@ -24,7 +24,7 @@ public SqlMembershipRepository(IKeyVaultSecret sqlServ public async Task> GetChildEntitiesAsync(string filter, int personnelNumber, string tableName, int depth) { var children = new List(); - var retryPolicy = GetRetryPolicy(); + var retryPolicy = GetRetryPolicyAsync(); try { @@ -45,7 +45,7 @@ FROM [users].[{tableName}] e INNER JOIN emp SELECT * FROM emp e {depthQuery} {filterQuery}"; - await retryPolicy.Execute(async () => + await retryPolicy.ExecuteAsync(async () => { using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -83,7 +83,7 @@ await retryPolicy.Execute(async () => public async Task<(int maxDepth, int id)> GetOrgLeaderDetailsAsync(string azureObjectId, string tableName) { - var retryPolicy = GetRetryPolicy(); + var retryPolicy = GetRetryPolicyAsync(); int maxDepth = 0; int employeeId = 0; @@ -107,7 +107,7 @@ FROM emp e var selectIdQuery = $"SELECT EmployeeId FROM [users].[{tableName}] WHERE AzureObjectId = '{azureObjectId}'"; - await retryPolicy.Execute(async () => + await retryPolicy.ExecuteAsync(async () => { using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -155,12 +155,12 @@ await retryPolicy.Execute(async () => public async Task> FilterChildEntitiesAsync(string query, string tableName) { var filteredChildren = new List(); - var retryPolicy = GetRetryPolicy(); + var retryPolicy = GetRetryPolicyAsync(); try { var selectQuery = $"SELECT EmployeeId, AzureObjectId FROM [users].[{tableName}] WHERE {query}"; - await retryPolicy.Execute(async () => + await retryPolicy.ExecuteAsync(async () => { using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -198,10 +198,10 @@ await retryPolicy.Execute(async () => public async Task CheckIfTableExistsAsync(string tableName) { bool tableExists = false; - var retryPolicy = GetRetryPolicy(); + var retryPolicy = GetRetryPolicyAsync(); try { - await retryPolicy.Execute(async () => + await retryPolicy.ExecuteAsync(async () => { using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -227,12 +227,12 @@ await retryPolicy.Execute(async () => public async Task> GetColumnNamesAsync(string tableName) { var HRColumns = new List(); - var retryPolicy = GetRetryPolicy(); + var retryPolicy = GetRetryPolicyAsync(); try { var selectQuery = $"SELECT name FROM sys.columns WHERE object_id = OBJECT_ID('[users].[{tableName}]') ORDER BY name"; - await retryPolicy.Execute(async () => + await retryPolicy.ExecuteAsync(async () => { using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -265,7 +265,7 @@ await retryPolicy.Execute(async () => public async Task<(int maxDepth, string azureObjectId)> GetOrgLeaderAsync(int employeeId, string tableName) { - var retryPolicy = GetRetryPolicy(); + var retryPolicy = GetRetryPolicyAsync(); int maxDepth = 0; string azureObjectId = ""; @@ -289,7 +289,7 @@ FROM emp e var selectIdQuery = $"SELECT AzureObjectId FROM [users].[{tableName}] WHERE EmployeeId = {employeeId}"; - await retryPolicy.Execute(async () => + await retryPolicy.ExecuteAsync(async () => { using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -337,7 +337,7 @@ await retryPolicy.Execute(async () => public async Task> GetColumnDetailsAsync(string tableName) { var columnDetails = new List<(string Name, string Type)>(); - var retryPolicy = GetRetryPolicy(); + var retryPolicy = GetRetryPolicyAsync(); try { var selectQuery = $@" @@ -347,7 +347,7 @@ FROM sys.columns AS c WHERE c.object_id = OBJECT_ID('[users].[{tableName}]') ORDER BY c.name"; - await retryPolicy.Execute(async () => + await retryPolicy.ExecuteAsync(async () => { using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -383,10 +383,10 @@ await retryPolicy.Execute(async () => public async Task CheckIfMappingsTableExistsAsync(string tableName) { bool tableExists = false; - var retryPolicy = GetRetryPolicy(); + var retryPolicy = GetRetryPolicyAsync(); try { - await retryPolicy.Execute(async () => + await retryPolicy.ExecuteAsync(async () => { using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -412,13 +412,13 @@ await retryPolicy.Execute(async () => public async Task> GetAttributeMappingsAsync(string attribute, string tableName) { var attributeMappings = new List<(string Code, string Description)>(); - var retryPolicy = GetRetryPolicy(); + var retryPolicy = GetRetryPolicyAsync(); try { var selectQuery = $"SELECT Code, Description FROM [mappings].[{tableName}] WHERE ColumnName = '{attribute}'"; - await retryPolicy.Execute(async () => + await retryPolicy.ExecuteAsync(async () => { using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -455,7 +455,7 @@ await retryPolicy.Execute(async () => public async Task> GetAttributeValuesAsync(string attribute, bool hasMapping, string tableName) { var attributeValues = new List(); - var retryPolicy = GetRetryPolicy(); + var retryPolicy = GetRetryPolicyAsync(); var schema = hasMapping ? "mappings" : "users"; var column = hasMapping ? "Description" : $"{attribute}"; @@ -465,7 +465,7 @@ public async Task> GetAttributeValuesAsync(string attribute, bool h { var selectQuery = $"SELECT DISTINCT TOP(10) {column} FROM [{schema}].[{tableName}]" + whereClause; - await retryPolicy.Execute(async () => + await retryPolicy.ExecuteAsync(async () => { using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -499,12 +499,12 @@ await retryPolicy.Execute(async () => return attributeValues; } - private RetryPolicy GetRetryPolicy() + private AsyncRetryPolicy GetRetryPolicyAsync() { return Policy.Handle() - .WaitAndRetry( - 3, - _ => TimeSpan.FromMinutes(1) + .WaitAndRetryAsync( + 5, + attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)) ); } } From 76c4ff95fa0f88c322aa62a340e033fc2b512b21 Mon Sep 17 00:00:00 2001 From: Abril Gonzalez Date: Tue, 1 Oct 2024 12:42:09 -0700 Subject: [PATCH 0331/1479] Clean up ContentContainer to receive an array of actionButtons as props --- .../ContentContainer.base.tsx | 83 +++++++------------ .../ContentContainer.types.ts | 9 +- .../src/pages/JobDetails/JobDetails.base.tsx | 27 +++--- 3 files changed, 49 insertions(+), 70 deletions(-) diff --git a/UI/web-app/src/components/ContentContainer/ContentContainer.base.tsx b/UI/web-app/src/components/ContentContainer/ContentContainer.base.tsx index 40295ecc3..fd764f3cf 100644 --- a/UI/web-app/src/components/ContentContainer/ContentContainer.base.tsx +++ b/UI/web-app/src/components/ContentContainer/ContentContainer.base.tsx @@ -1,70 +1,45 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { classNamesFunction, Icon, type IProcessedStyleSet } from '@fluentui/react'; +import { ActionButton, classNamesFunction, type IProcessedStyleSet } from '@fluentui/react'; import { useTheme } from '@fluentui/react/lib/Theme'; import React from 'react'; -import { ActionButton } from '@fluentui/react/lib/Button'; import { Text } from '@fluentui/react/lib/Text'; import { Separator } from '@fluentui/react/lib/Separator'; -import { Link } from '@fluentui/react/lib/Link'; import { - type IContentContainerProps, - type IContentContainerStyleProps, - type IContentContainerStyles, + type IContentContainerProps, + type IContentContainerStyleProps, + type IContentContainerStyles, } from './ContentContainer.types'; -import { useStrings } from '../../store/hooks'; export const getClassNames = classNamesFunction(); export const ContentContainerBase: React.FunctionComponent = ( - props: IContentContainerProps + props: IContentContainerProps ) => { - const { title, actionOnClick, actionText, actionIcon, className, styles, - children, useLinkButton, linkButtonIconName, hideSeparator, removeButton, editButton } = props; - const classNames: IProcessedStyleSet = getClassNames(styles, { - className, - theme: useTheme(), - }); - const strings = useStrings(); + const { title, actionButtons, className, styles, children, hideSeparator } = props; + const classNames: IProcessedStyleSet = getClassNames(styles, { + className, + theme: useTheme(), + }); - return ( - -
-
- - {title} - - {removeButton === true ? (<>) : - useLinkButton === true ? ( - - {actionText} - {linkButtonIconName ? : <>} - - ) : ( - - {actionText} - - )} - {editButton === true && - ( - - {strings.JobDetails.editButton} - - )} -
- {hideSeparator === true ? <> : } - {children} -
- - ) -}; \ No newline at end of file + return ( +
+
+ {title} + {actionButtons?.map((button, index) => ( + + {button.text} + + ))} +
+ {hideSeparator === true ? <> : } + {children} +
+ ); +}; diff --git a/UI/web-app/src/components/ContentContainer/ContentContainer.types.ts b/UI/web-app/src/components/ContentContainer/ContentContainer.types.ts index 775d934bd..b37f57dcd 100644 --- a/UI/web-app/src/components/ContentContainer/ContentContainer.types.ts +++ b/UI/web-app/src/components/ContentContainer/ContentContainer.types.ts @@ -33,9 +33,7 @@ import { */ styles?: IStyleFunctionOrObject; title: string; - actionOnClick?: () => void; - actionText?: string | null; - actionIcon?: IIconProps; + actionButtons?: IActionButtonProps[]; useLinkButton?: boolean; linkButtonIconName?: string; hideSeparator?: boolean; @@ -44,3 +42,8 @@ import { } + export interface IActionButtonProps { + text: string; + icon?: IIconProps; + onClick: () => void; + } diff --git a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx index cbc383990..55e764194 100644 --- a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx +++ b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx @@ -194,28 +194,29 @@ export const JobDetailsBase: React.FunctionComponent = ( /> } /> } - removeButton={true} - editButton={canEditJob} - actionText={canEditJob ? strings.JobDetails.editButton : ''} - useLinkButton={true} - actionOnClick={openRunConfiguration} + actionButtons={ + canEditJob + ? [{ text: strings.JobDetails.editButton, icon: { iconName: 'Edit' }, onClick: openRunConfiguration }] + : [] + } /> {jobDetails?.source}} - removeButton={isJobWriter} - editButton={canEditJob} - actionText={canEditJob ? strings.JobDetails.editButton : isJobWriter ? strings.JobDetails.viewDetails : ''} - useLinkButton={true} - actionOnClick={openMembershipConfiguration} + actionButtons={ + canEditJob + ? [{ text: strings.JobDetails.editButton, icon: { iconName: 'Edit' }, onClick: openMembershipConfiguration }, + { text: strings.JobDetails.viewDetails, icon: { iconName: 'View' }, onClick: openMembershipConfiguration }] + : [] + } />
{canDeleteJob && From ebc2334224584f19c71d87ef72f94c1da5ddef17 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 2 Oct 2024 10:48:14 -0700 Subject: [PATCH 0332/1479] Enforce consistency for ADF database naming --- Deployment/adfHRResources.bicep | 2 +- Infrastructure/adf/pipeline/template.bicep | 4 ++-- Infrastructure/adf/sql/template.bicep | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Deployment/adfHRResources.bicep b/Deployment/adfHRResources.bicep index 3d8bf79c2..4d670347f 100644 --- a/Deployment/adfHRResources.bicep +++ b/Deployment/adfHRResources.bicep @@ -5,7 +5,7 @@ param solutionAbbreviation string param tenantId string var sqlServerName = '${solutionAbbreviation}-data-${environmentAbbreviation}' -var sqlDataBaseName = '${solutionAbbreviation}-data-${environmentAbbreviation}-hr' +var sqlDataBaseName = '${solutionAbbreviation}-data-${environmentAbbreviation}-adf' module sqlForHRData '../Infrastructure/adf/sql/template.bicep' = { name: 'sqlForHRDataTemplate' diff --git a/Infrastructure/adf/pipeline/template.bicep b/Infrastructure/adf/pipeline/template.bicep index 2dbb27399..440e7e538 100644 --- a/Infrastructure/adf/pipeline/template.bicep +++ b/Infrastructure/adf/pipeline/template.bicep @@ -15,10 +15,10 @@ param location string param tenantId string @description('Name of SQL Server') -param sqlServerName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' +param sqlServerName string @description('Name of SQL database name') -param sqlDatabaseName string = '${solutionAbbreviation}-data-${environmentAbbreviation}-adf' +param sqlDatabaseName string @description('Name of Azure Data Factory') param azureDataFactoryName string = '${solutionAbbreviation}-data-${environmentAbbreviation}-adf' diff --git a/Infrastructure/adf/sql/template.bicep b/Infrastructure/adf/sql/template.bicep index b2405578c..e53f80e40 100644 --- a/Infrastructure/adf/sql/template.bicep +++ b/Infrastructure/adf/sql/template.bicep @@ -30,10 +30,10 @@ param storageAccountSku string = 'Standard_LRS' param storageAccountContainerName string = 'csvcontainer' @description('Name of SQL Server') -param sqlServerName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' +param sqlServerName string @description('Name of ADF SQL Server') -param adfSqlDataBaseName string = '${solutionAbbreviation}-data-${environmentAbbreviation}-destination' +param adfSqlDataBaseName string @description('Name of Jobs SQL Server') param jobsSqlDataBaseName string = '${solutionAbbreviation}-data-${environmentAbbreviation}' From b11f6acb5a1417bb1189dc6b257c35004bf453b0 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Thu, 3 Oct 2024 11:40:47 -0700 Subject: [PATCH 0333/1479] support IS and IS NOT NULL clauses --- .../HRQuerySource/HRQuerySource.base.tsx | 41 ++++++++++++++----- .../HRQuerySource/QuerySerializer.ts | 4 +- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index c669f1aa3..7956d30ee 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -537,6 +537,11 @@ const checkType = (value: string, type: string | undefined): string => { } }; + const nullOptions: IComboBoxOption[] = [ + { key: 'NULL', text: 'NULL' }, + { key: 'NOT NULL', text: 'NOT NULL' } + ]; + const yesNoOptions: IChoiceGroupOption[] = [ { key: 'Yes', text: strings.yes }, { key: 'No', text: strings.no } @@ -637,7 +642,8 @@ const checkType = (value: string, type: string | undefined): string => { { key: '<=', text: '<=' }, { key: '>', text: '>'}, { key: '>=', text: '>=' }, - { key: '<>', text: '<>' } + { key: '<>', text: '<>' }, + { key: 'IS', text: 'IS' } ]; interface UpdateParam { @@ -806,11 +812,10 @@ const checkType = (value: string, type: string | undefined): string => { } }; - const handleAttributeValueChange = (attribute: string, event: React.FormEvent, item?: IComboBoxOption, index?: number): void => { + const handleAttributeValueChange = (attribute: string, event: React.FormEvent, item?: IComboBoxOption, index?: number, operator?: string): void => { if (item) { const selectedValue = item.key.toString(); - const selectedValueAfterConversion = attributeMappings[attribute] ? checkType(selectedValue, attributeMappings[attribute.toString()].type) : selectedValue; - + const selectedValueAfterConversion = operator && operator.toString().toUpperCase() === "IS" ? selectedValue : (attributeMappings[attribute] ? checkType(selectedValue, attributeMappings[attribute.toString()].type) : selectedValue); const updatedItems = items.map((it, idx) => { if (idx === index) { return { ...it, value: selectedValueAfterConversion || selectedValue }; @@ -1218,14 +1223,27 @@ const checkType = (value: string, type: string | undefined): string => { />; case 'equalityOperator': return handleEqualityOperatorChange(event, option, index)} options={equalityOperatorOptions} styles={{root: classNames.root, title: classNames.dropdownTitle}} />; case 'value': - if (attributeMappings && attributeMappings[items[index].attribute] && attributeMappings[items[index].attribute].mappings.length > 0) { - return handleAttributeValueChange(item.attribute, event, option, index, item.equalityOperator)} + allowFreeInput + autoComplete="off" + dropdownMaxWidth={500} + /> + ); + } + else { + if (attributeMappings && attributeMappings[items[index].attribute] && attributeMappings[items[index].attribute].mappings.length > 0) { + return onAttributeValueChange(text, index)} @@ -1236,17 +1254,18 @@ const checkType = (value: string, type: string | undefined): string => { autoComplete="off" useComboBoxAsMenuWidth={false} dropdownMaxWidth={500} - /> - } else { - return + } else { + return handleTAttributeValueChange(item.attribute, event, newValue!, index)} onBlur={(event) => handleBlur(item.attribute, event, index)} styles={{ fieldGroup: classNames.textField }} validateOnLoad={false} validateOnFocusOut={false} - >; + >; } + } case 'andOr': return ( (groups.length <= 0) ? ( diff --git a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts index b2e3c9845..b2cb516c4 100644 --- a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts +++ b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts @@ -2,7 +2,7 @@ import { Group } from "../../models/Group"; import { IFilterPart } from "../../models/IFilterPart"; export function containsSqlExpression(filter: string): boolean { - const sqlExpressions = ['IN', 'BETWEEN', 'NOT', 'LIKE', 'IS NULL', 'IS NOT NULL']; + const sqlExpressions = [' IN ', ' NOT IN ', ' BETWEEN ', ' LIKE ', ' NOT LIKE ']; const regex = new RegExp(`\\b(${sqlExpressions.join('|')})\\b`, 'i'); return regex.test(filter); }; @@ -58,7 +58,7 @@ export function stringifyGroups(groups: Group[]): string { } function parseFilterPart(part: string): IFilterPart { - const operators = ["<=", ">=", "<>", "=", ">", "<"]; + const operators = ["<=", ">=", "<>", "=", ">", "<", "IS"]; let operatorFound = ''; let operatorIndex = -1; From 14e722d4362571da3f881272d571308fb8941473 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Fri, 4 Oct 2024 11:03:45 -0700 Subject: [PATCH 0334/1479] Added the netFrameworkVersion v8 setting to the isolated worker model functions. --- .../AzureMaintenance/Infrastructure/compute/functionApp.bicep | 1 + .../AzureUserReader/Infrastructure/compute/functionApp.bicep | 1 + .../Infrastructure/compute/functionApp.bicep | 1 + .../Hosts/GraphUpdater/Infrastructure/compute/functionApp.bicep | 1 + .../Infrastructure/compute/functionApp.bicep | 1 + .../Hosts/JobScheduler/Infrastructure/compute/functionApp.bicep | 1 + .../Hosts/JobTrigger/Infrastructure/compute/functionApp.bicep | 1 + .../Infrastructure/compute/functionApp.bicep | 1 + .../NonProdService/Infrastructure/compute/functionApp.bicep | 1 + .../Hosts/Notifier/Infrastructure/compute/functionApp.bicep | 1 + .../Infrastructure/compute/functionApp.bicep | 1 + .../SyncJobUpdater/Infrastructure/compute/functionApp.bicep | 1 + 12 files changed, 12 insertions(+) diff --git a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionApp.bicep index 11625bfb4..b9e618d3a 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureMaintenance/Infrastructure/compute/functionApp.bicep @@ -37,6 +37,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionApp.bicep index 1a6c8611f..8cbf088bb 100644 --- a/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/AzureUserReader/Infrastructure/compute/functionApp.bicep @@ -43,6 +43,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionApp.bicep index 11625bfb4..b9e618d3a 100644 --- a/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/DestinationAttributesUpdater/Infrastructure/compute/functionApp.bicep @@ -37,6 +37,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionApp.bicep index d671604ba..c7962308d 100644 --- a/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/GraphUpdater/Infrastructure/compute/functionApp.bicep @@ -43,6 +43,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionApp.bicep index 11625bfb4..b9e618d3a 100644 --- a/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/GroupMembershipObtainer/Infrastructure/compute/functionApp.bicep @@ -37,6 +37,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionApp.bicep index d1a35c0a9..dbe8ae07a 100644 --- a/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobScheduler/Infrastructure/compute/functionApp.bicep @@ -38,6 +38,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionApp.bicep index 11625bfb4..b9e618d3a 100644 --- a/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/JobTrigger/Infrastructure/compute/functionApp.bicep @@ -37,6 +37,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionApp.bicep index 520ef4fc2..36dda0f81 100644 --- a/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/MembershipAggregator/Infrastructure/compute/functionApp.bicep @@ -46,6 +46,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionApp.bicep index a110ee826..910e636eb 100644 --- a/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/NonProdService/Infrastructure/compute/functionApp.bicep @@ -43,6 +43,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionApp.bicep index f19185273..0e446dac5 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Infrastructure/compute/functionApp.bicep @@ -43,6 +43,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionApp.bicep index 11625bfb4..b9e618d3a 100644 --- a/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/SqlMembershipObtainer/Infrastructure/compute/functionApp.bicep @@ -37,6 +37,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' diff --git a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/functionApp.bicep b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/functionApp.bicep index 11625bfb4..b9e618d3a 100644 --- a/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/functionApp.bicep +++ b/Service/GroupMembershipManagement/Hosts/SyncJobUpdater/Infrastructure/compute/functionApp.bicep @@ -37,6 +37,7 @@ resource functionApp 'Microsoft.Web/sites@2018-02-01' = { clientAffinityEnabled: false httpsOnly: true siteConfig: { + netFrameworkVersion: 'v8.0' use32BitWorkerProcess : false appSettings: secretSettings ftpsState: 'Disabled' From 6c018475305e7385d25c1a4c77eabe62db04563d Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 9 Sep 2024 16:07:19 -0700 Subject: [PATCH 0335/1479] Updated WebApi docs for consistency with other documentation --- .../Hosts/WebApi/Documentation/WebApiSetup.md | 55 ++----------------- 1 file changed, 4 insertions(+), 51 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md b/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md index 279fc8beb..4ccebd0e2 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Documentation/WebApiSetup.md @@ -99,9 +99,9 @@ Note: Individual users can be added and granted the proper permission, if you de ## Add trusted client applications -The WebpAPI will be called by the GMM UI. In order to allow it to call the WebAPI, it needs to be added as trusted client application. +The WebAPI will be called by the GMM UI. In order to allow it to call the WebAPI, it needs to be added as trusted client application. -Create UI application by following `UI\Documentation\UISetup.md`. +Create UI application by following [UI\Documentation\UISetup.md](../../../../../UI/Documentation/UISetup.md). 1. From the Azure Portal locate and open "Microsoft Entra ID" 2. On the left menu select "App Registrations" @@ -125,55 +125,8 @@ Make sure to select the Authorized scope. 9. Check "user_impersonation" on and click "Add permissions" 10. Click "Grant admin consent for " -*The following steps need to be completed after a successful deployment* - -## Grant Permissions - -This step needs to be completed after all the resources have been deployed to your Azure tenant. - -See [Post-Deployment tasks](../../../../../README.md#post-deployment-tasks) - -Running the script mentioned in the Post-Deployment tasks section will grant the WebAPI system identity access to the resources it needs. - -To properly setup the WebAPI you will need to configure the parameters in the `WebApi/Infrastructure/compute/parameters` for your environment. -If you have a custom domain, follow the instructions [here](WebApiSetup.md/#setting-up-a-custom-domain). If not, skip on to the instructions [here](WebApiSetup.md/#using-the-default). - -### Grant access to the SQL Server Database - -WebAPI will access the database using its system identity to authenticate with the database to prevent the use of credentials. - -Once the WebAPI is deployed (`-compute--webapi`)and has been created we need to grant it access to the SQL Server DB. - -Server name follows this naming convention `-data-` and `-data--r` for the replica server. -Database name follows this naming convention `-data-` and `-data--r` for the replica database. - -1. Connect to your SQL Server Database using Sql Server Management Studio (SSMS) or Azure Data Studio. -- Server name : `.database.windows.net` -- User name: Use your Azure account. -- Authentication: Microsoft Entra ID - Universal with MFA -- Database name: `` - -2. Run these SQL command - -- This script needs to run only once per database. -- Make sure you are connected to right database. Sometimes SSMS will default to the master database. - -``` -IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = N'-compute--webapi') -BEGIN - CREATE USER [-compute--webapi] FROM EXTERNAL PROVIDER; - ALTER ROLE db_datareader ADD MEMBER [-compute--webapi]; - ALTER ROLE db_datawriter ADD MEMBER [-compute--webapi]; - ALTER ROLE db_ddladmin ADD MEMBER [-compute--webapi]; -END -``` - -Verify it ran successufully by running: -``` -SELECT * FROM sys.database_principals WHERE name = N'-compute--webapi' -``` -You should see one record for your webapi app. -Repeat the steps for both databases. +* To properly setup the WebAPI you will need to configure the parameters in the `WebApi/Infrastructure/compute/parameters` for your environment. +If you have a custom domain, follow the instructions [here](#setting-up-a-custom-domain). If not, skip on to the instructions [here](#using-the-default-domain). ## Setting up a custom domain If you have a custom domain ('contoso.com', for example) and want to use it, you will need to upgrade your App Service Plan. You can set the API custom domain in the `apiHostname` parameter as `api.contoso.com`. From 1988f30ef44b95f9ef01da409ff23ae0cae7056d Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Mon, 9 Sep 2024 15:02:36 -0700 Subject: [PATCH 0336/1479] Created WebApi endpoint for api/v1/sqlMembershipSources/validateFilters to validate array of filters on Sql table --- .../Requests/GetSqlValidationRequest.cs | 17 +++ .../Responses/GetSqlValidationResponse.cs | 13 +++ .../GetSqlValidationHandler.cs | 102 ++++++++++++++++++ .../Configuration/MessageHandlerInjector.cs | 1 + .../SqlMembershipSourcesController.cs | 25 ++++- .../ISqlMembershipRepository.cs | 1 + .../DatabaseSqlMembershipSourcesRepository.cs | 14 +-- .../SqlMembershipRepository.cs | 38 ++++++- 8 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetSqlValidationRequest.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetSqlValidationResponse.cs create mode 100644 Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetSqlValidationHandler.cs diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetSqlValidationRequest.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetSqlValidationRequest.cs new file mode 100644 index 000000000..813301a60 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetSqlValidationRequest.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Services.Messages.Contracts.Requests; + +namespace Services.Messages.Requests +{ + public class GetSqlValidationRequest : RequestBase + { + public string[] SqlFilters { get; } + + public GetSqlValidationRequest(string[] sqlFilters) + { + SqlFilters = sqlFilters; + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetSqlValidationResponse.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetSqlValidationResponse.cs new file mode 100644 index 000000000..6b44216f7 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Responses/GetSqlValidationResponse.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Services.Messages.Contracts.Responses; + +namespace Services.Messages.Responses +{ + public class GetSqlValidationResponse : ResponseBase + { + public bool IsValid { get; set; } + public Dictionary? Errors { get; set; } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetSqlValidationHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetSqlValidationHandler.cs new file mode 100644 index 000000000..ac0ffe2fa --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetSqlValidationHandler.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +using Microsoft.Data.SqlClient; +using Models; +using Repositories.Contracts; +using Services.Contracts; +using Services.Messages.Requests; +using Services.Messages.Responses; + +namespace Services +{ + public class GetSqlValidationHandler : RequestHandlerBase + { + private readonly ILoggingRepository _loggingRepository; + private readonly ISqlMembershipRepository _sqlMembershipRepository; + private readonly IDataFactoryRepository _dataFactoryRepository; + + private SemaphoreSlim _adfRunIdSemaphore = new SemaphoreSlim(1, 1); + + public GetSqlValidationHandler(ILoggingRepository loggingRepository, + ISqlMembershipRepository sqlMembershipRepository, + IDataFactoryRepository dataFactoryRepository) : base(loggingRepository) + { + _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); + _sqlMembershipRepository = sqlMembershipRepository ?? throw new ArgumentNullException(nameof(sqlMembershipRepository)); + _dataFactoryRepository = dataFactoryRepository ?? throw new ArgumentNullException(nameof(dataFactoryRepository)); + } + + protected override async Task ExecuteCoreAsync(GetSqlValidationRequest request) + { + if (request.SqlFilters == null || request.SqlFilters.Length == 0) + { + return new GetSqlValidationResponse + { + IsValid = true, + Errors = null + }; + } + + try + { + var tableName = await GetTableNameAsync(); + var sqlExceptions = await _sqlMembershipRepository.ValidateFiltersAsync(request.SqlFilters, tableName); + + return new GetSqlValidationResponse + { + IsValid = sqlExceptions.Count == 0, + Errors = sqlExceptions.Count == 0 ? null : sqlExceptions + }; + } + catch (Exception ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Unable to validate Sql filter: {ex.Message}" }); + throw; + } + } + + private async Task GetTableNameAsync() + { + var adfRunId = await GetADFRunIdAsync(); + var tableName = adfRunId.Replace("-", ""); + var tableExists = await CheckIfTableExistsAsync(tableName); + + return tableExists ? tableName : ""; + } + + private async Task CheckIfTableExistsAsync(string tableName) + { + bool tableExists = false; + + try + { + tableExists = await _sqlMembershipRepository.CheckIfTableExistsAsync(tableName); + } + catch (SqlException ex) + { + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"An exception was thrown while checking if table '{tableName}' exists: {ex.Message}" }); + throw; + } + + return tableExists; + } + + private async Task GetADFRunIdAsync() + { + await _adfRunIdSemaphore.WaitAsync(); + + var lastSqlMembershipRunId = await _dataFactoryRepository.GetMostRecentSucceededRunIdAsync(); + + _adfRunIdSemaphore.Release(); + + if (string.IsNullOrWhiteSpace(lastSqlMembershipRunId)) + { + var message = $"No SqlMembershipObtainer pipeline run has been found"; + await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"An exception was thrown while attempting to get the laterst ADF pipeline run: {message}" }); + throw new ArgumentException(message); + } + + return lastSqlMembershipRunId; + } + } +} \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs index b663d93d2..0be008f0c 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Configuration/MessageHandlerInjector.cs @@ -27,6 +27,7 @@ public static IServiceCollection InjectMessageHandlers(this IServiceCollection s services.AddTransient, GetDefaultSqlMembershipSourceAttributeValuesHandler>(); services.AddTransient, PatchDefaultSqlMembershipSourceCustomLabelHandler>(); services.AddTransient, PatchDefaultSqlMembershipSourceAttributesHandler>(); + services.AddTransient, GetSqlValidationHandler>(); services.AddTransient, GetJobsHandler>(); services.AddTransient, GetJobDetailsHandler>(); diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs index ab1f9c3a2..89c2a6714 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Azure; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Models; @@ -21,6 +22,7 @@ public class SqlMembershipSourcesController : ControllerBase private readonly IRequestHandler _getDefaultSqlMembershipSourceAttributeValuesHandler; private readonly IRequestHandler _patchDefaultSqlMembershipSourceCustomLabelHandler; private readonly IRequestHandler _patchDefaultSqlMembershipSourceAttributesHandler; + private readonly IRequestHandler _getSqlValidationHandler; public SqlMembershipSourcesController( IRequestHandler getDefaultSqlMembershipSourceHandler, @@ -28,7 +30,8 @@ public SqlMembershipSourcesController( IRequestHandler getDefaultSqlMembershipSourceAttributeMappingsHandler, IRequestHandler getDefaultSqlMembershipSourceAttributeValuesHandler, IRequestHandler patchDefaultSqlMembershipSourceCustomLabelHandler, - IRequestHandler patchDefaultSqlMembershipSourceAttributesHandler) + IRequestHandler patchDefaultSqlMembershipSourceAttributesHandler, + IRequestHandler getSqlValidationHandler) { _getDefaultSqlMembershipSourceHandler = getDefaultSqlMembershipSourceHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceHandler)); _getDefaultSqlMembershipSourceAttributesHandler = getDefaultSqlMembershipSourceAttributesHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceAttributesHandler)); @@ -36,6 +39,7 @@ public SqlMembershipSourcesController( _getDefaultSqlMembershipSourceAttributeValuesHandler = getDefaultSqlMembershipSourceAttributeValuesHandler ?? throw new ArgumentNullException(nameof(getDefaultSqlMembershipSourceAttributeValuesHandler)); _patchDefaultSqlMembershipSourceCustomLabelHandler = patchDefaultSqlMembershipSourceCustomLabelHandler ?? throw new ArgumentNullException(nameof(patchDefaultSqlMembershipSourceCustomLabelHandler)); _patchDefaultSqlMembershipSourceAttributesHandler = patchDefaultSqlMembershipSourceAttributesHandler ?? throw new ArgumentNullException(nameof(patchDefaultSqlMembershipSourceAttributesHandler)); + _getSqlValidationHandler = getSqlValidationHandler ?? throw new ArgumentNullException(nameof(getSqlValidationHandler)); } [Authorize()] @@ -109,7 +113,7 @@ public async Task PatchDefaultSourceCustomLabelAsync([FromBody] s } catch (Exception) { - return StatusCode(500); + return StatusCode((int)HttpStatusCode.InternalServerError); } } @@ -124,7 +128,22 @@ public async Task PatchDefaultSourceAttributesAsync([FromBody] Li } catch (Exception) { - return StatusCode(500); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + + [Authorize(Roles = Models.Roles.JOB_OWNER_WRITER + "," + Models.Roles.JOB_TENANT_WRITER)] + [HttpPost("validateFilters")] + public async Task ValidateSqlFiltersAsync([FromBody] string[] sqlFilters) + { + try + { + var response = await _getSqlValidationHandler.ExecuteAsync(new GetSqlValidationRequest(sqlFilters)); + return Ok(response); + } + catch (Exception) + { + return StatusCode((int)HttpStatusCode.InternalServerError); } } } diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs index a0db95d88..c6709d4bf 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs @@ -19,5 +19,6 @@ public interface ISqlMembershipRepository Task CheckIfMappingsTableExistsAsync(string tableName); Task> GetAttributeMappingsAsync(string attribute, string tableName); Task> GetAttributeValuesAsync(string attribute, bool hasMapping, string tableName); + Task> ValidateFiltersAsync(string[] sqlFilters, string tableName); } } diff --git a/Service/GroupMembershipManagement/Repositories.EntityFramework/DatabaseSqlMembershipSourcesRepository.cs b/Service/GroupMembershipManagement/Repositories.EntityFramework/DatabaseSqlMembershipSourcesRepository.cs index 2940b3fdd..2facfe7de 100644 --- a/Service/GroupMembershipManagement/Repositories.EntityFramework/DatabaseSqlMembershipSourcesRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.EntityFramework/DatabaseSqlMembershipSourcesRepository.cs @@ -31,7 +31,7 @@ public async Task> GetSourcesAsync() return sources; } - public async Task> GetSourceAttributesAsync(string sourceName) + public async Task?> GetSourceAttributesAsync(string sourceName) { var source = await _readContext.SqlMembershipSources.FirstOrDefaultAsync(s => s.Name == sourceName); return source?.Attributes; @@ -40,14 +40,14 @@ public async Task> GetSourceAttributesAsync(string public async Task UpdateSourceAttributesAsync(string sourceName, List attributes) { var source = await _writeContext.SqlMembershipSources.FirstOrDefaultAsync(s => s.Name == sourceName); - source.Attributes = attributes; + source!.Attributes = attributes; await _writeContext.SaveChangesAsync(); } public async Task UpdateSourceCustomLabelAsync(string sourceName, string newCustomLabel) { var source = await _writeContext.SqlMembershipSources.FirstOrDefaultAsync(s => s.Name == sourceName); - source.CustomLabel = newCustomLabel; + source!.CustomLabel = newCustomLabel; await _writeContext.SaveChangesAsync(); } @@ -55,7 +55,7 @@ public async Task GetDefaultSourceAsync() { var source = await _writeContext.SqlMembershipSources.FirstOrDefaultAsync(s => s.Name == "SqlMembership"); return new SqlMembershipSource { - Name = source.Name, + Name = source!.Name, CustomLabel = source.CustomLabel }; } @@ -63,20 +63,20 @@ public async Task GetDefaultSourceAsync() public async Task> GetDefaultSourceAttributesAsync() { var source = await _writeContext.SqlMembershipSources.FirstOrDefaultAsync(s => s.Name == "SqlMembership"); - return source.Attributes; + return source!.Attributes; } public async Task UpdateDefaultSourceCustomLabelAsync(string newCustomLabel) { var source = await _writeContext.SqlMembershipSources.FirstOrDefaultAsync(s => s.Name == "SqlMembership"); - source.CustomLabel = newCustomLabel; + source!.CustomLabel = newCustomLabel; await _writeContext.SaveChangesAsync(); } public async Task UpdateDefaultSourceAttributesAsync(List attributes) { var source = await _writeContext.SqlMembershipSources.FirstOrDefaultAsync(s => s.Name == "SqlMembership"); - source.Attributes = attributes; + source!.Attributes = attributes; await _writeContext.SaveChangesAsync(); } } diff --git a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs index dccbaa150..6be3c3cad 100644 --- a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs @@ -1,5 +1,6 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. +using Azure.Core.Pipeline; using Azure.Identity; using Microsoft.Data.SqlClient; using Polly; @@ -7,6 +8,8 @@ using Repositories.Contracts; using Repositories.Contracts.InjectConfig; using SqlMembershipObtainer.Entities; +using System; +using System.Collections.Concurrent; using System.Data; namespace Repositories.SqlMembershipRepository @@ -341,7 +344,7 @@ await retryPolicy.ExecuteAsync(async () => try { var selectQuery = $@" - SELECT c.name, t.name AS type + SELECT c.name, t.name AS type FROM sys.columns AS c JOIN sys.types AS t ON c.user_type_id = t.user_type_id WHERE c.object_id = OBJECT_ID('[users].[{tableName}]') @@ -483,7 +486,7 @@ await retryPolicy.ExecuteAsync(async () => if (value != null) { attributeValues.Add(value); - } + } } await reader.CloseAsync(); } @@ -499,6 +502,37 @@ await retryPolicy.ExecuteAsync(async () => return attributeValues; } + public async Task> ValidateFiltersAsync(string[] sqlFilters, string tableName) + { + var exceptionsList = new ConcurrentDictionary(); + + var tasks = sqlFilters.Select(async (sqlFilter, index) => + { + try + { + var selectQuery = $@"SET NOEXEC ON; SELECT * FROM [users].[{tableName}] WHERE {sqlFilter}"; + + using (var conn = new SqlConnection(_sqlServerConnectionString)) + { + await conn.OpenAsync(); + + var cmd = new SqlCommand(selectQuery, conn); + await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection); + + await conn.CloseAsync(); + } + } + catch (SqlException ex) + { + exceptionsList.TryAdd(index, ex.Message); + } + }); + + await Task.WhenAll(tasks); + + return exceptionsList.ToDictionary(); + } + private AsyncRetryPolicy GetRetryPolicyAsync() { return Policy.Handle() From a866409dd5eb1cf89912417ab9bbe67ed890154f Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 2 Oct 2024 13:30:59 -0700 Subject: [PATCH 0337/1479] Print unique errors out for SqlValidation --- .../SqlMembershipRepository.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs index 6be3c3cad..94cddb015 100644 --- a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs @@ -524,7 +524,15 @@ public async Task> ValidateFiltersAsync(string[] sqlFilt } catch (SqlException ex) { - exceptionsList.TryAdd(index, ex.Message); + var uniqueErrors = new HashSet(); + + foreach (SqlError error in ex.Errors) + { + uniqueErrors.Add(error.Message); + } + + var errorMessage = string.Join(", ", uniqueErrors); + exceptionsList.TryAdd(index, errorMessage); } }); From 19c74615e6bf1ff8c444d3f0e0676c6b91ac2fad Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 2 Oct 2024 16:56:19 -0700 Subject: [PATCH 0338/1479] Updated the input for the SqlValidation WebApi endpoint to be a Dictionkary --- .../Requests/GetSqlValidationRequest.cs | 4 ++-- .../WebApi/Services.WebApi/GetSqlValidationHandler.cs | 2 +- .../SqlMembershipSourcesController.cs | 2 +- .../Repositories.Contracts/ISqlMembershipRepository.cs | 2 +- .../SqlMembershipRepository.cs | 9 +++++---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetSqlValidationRequest.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetSqlValidationRequest.cs index 813301a60..6b0485e96 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetSqlValidationRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.Messages/Requests/GetSqlValidationRequest.cs @@ -7,9 +7,9 @@ namespace Services.Messages.Requests { public class GetSqlValidationRequest : RequestBase { - public string[] SqlFilters { get; } + public Dictionary SqlFilters { get; } - public GetSqlValidationRequest(string[] sqlFilters) + public GetSqlValidationRequest(Dictionary sqlFilters) { SqlFilters = sqlFilters; } diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetSqlValidationHandler.cs b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetSqlValidationHandler.cs index ac0ffe2fa..4ce6c5fdf 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetSqlValidationHandler.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/Services.WebApi/GetSqlValidationHandler.cs @@ -28,7 +28,7 @@ public GetSqlValidationHandler(ILoggingRepository loggingRepository, protected override async Task ExecuteCoreAsync(GetSqlValidationRequest request) { - if (request.SqlFilters == null || request.SqlFilters.Length == 0) + if (request.SqlFilters == null || request.SqlFilters.Count == 0) { return new GetSqlValidationResponse { diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs index 89c2a6714..0b8dd7529 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi/Controllers/v1/SqlMembershipSources/SqlMembershipSourcesController.cs @@ -134,7 +134,7 @@ public async Task PatchDefaultSourceAttributesAsync([FromBody] Li [Authorize(Roles = Models.Roles.JOB_OWNER_WRITER + "," + Models.Roles.JOB_TENANT_WRITER)] [HttpPost("validateFilters")] - public async Task ValidateSqlFiltersAsync([FromBody] string[] sqlFilters) + public async Task ValidateSqlFilterAsync([FromBody] Dictionary sqlFilters) { try { diff --git a/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs index c6709d4bf..3be894e61 100644 --- a/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.Contracts/ISqlMembershipRepository.cs @@ -19,6 +19,6 @@ public interface ISqlMembershipRepository Task CheckIfMappingsTableExistsAsync(string tableName); Task> GetAttributeMappingsAsync(string attribute, string tableName); Task> GetAttributeValuesAsync(string attribute, bool hasMapping, string tableName); - Task> ValidateFiltersAsync(string[] sqlFilters, string tableName); + Task> ValidateFiltersAsync(Dictionary sqlFilters, string tableName); } } diff --git a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs index 94cddb015..a9a29a90e 100644 --- a/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs +++ b/Service/GroupMembershipManagement/Repositories.SqlMembershipRepository/SqlMembershipRepository.cs @@ -10,6 +10,7 @@ using SqlMembershipObtainer.Entities; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Data; namespace Repositories.SqlMembershipRepository @@ -502,15 +503,15 @@ await retryPolicy.ExecuteAsync(async () => return attributeValues; } - public async Task> ValidateFiltersAsync(string[] sqlFilters, string tableName) + public async Task> ValidateFiltersAsync(Dictionary sqlFilters, string tableName) { var exceptionsList = new ConcurrentDictionary(); - var tasks = sqlFilters.Select(async (sqlFilter, index) => + var tasks = sqlFilters.Select(async sqlFilter => { try { - var selectQuery = $@"SET NOEXEC ON; SELECT * FROM [users].[{tableName}] WHERE {sqlFilter}"; + var selectQuery = $@"SET NOEXEC ON; SELECT * FROM [users].[{tableName}] WHERE {sqlFilter.Value}"; using (var conn = new SqlConnection(_sqlServerConnectionString)) { @@ -532,7 +533,7 @@ public async Task> ValidateFiltersAsync(string[] sqlFilt } var errorMessage = string.Join(", ", uniqueErrors); - exceptionsList.TryAdd(index, errorMessage); + exceptionsList.TryAdd(sqlFilter.Key, errorMessage); } }); From 334d4e8a577eba1d49bfc6c2a1c01b4dc6cf5a38 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Tue, 10 Sep 2024 13:04:11 -0700 Subject: [PATCH 0339/1479] Updated testing for WebApi --- .../SqlMembershipSourcesControllerTests.cs | 95 ++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs index ec002c8ca..b9f430ca7 100644 --- a/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs +++ b/Service/GroupMembershipManagement/Hosts/WebApi/WebApi.Tests/SqlMembershipSourcesControllerTests.cs @@ -7,6 +7,7 @@ using Models; using Moq; using Repositories.Contracts; +using Services.Messages.Responses; using System.Net; using System.Security.Claims; using WebApi.Controllers.v1.Settings; @@ -30,6 +31,7 @@ public class SqlMembershipSourcesControllerTests private GetDefaultSqlMembershipSourceAttributeValuesHandler _getDefaultSqlMembershipSourceAttributeValuesHandler = null!; private PatchDefaultSqlMembershipSourceCustomLabelHandler _patchDefaultSqlMembershipSourceCustomLabelHandler = null!; private PatchDefaultSqlMembershipSourceAttributesHandler _patchDefaultSqlMembershipSourceAttributesHandler = null!; + private GetSqlValidationHandler _getSqlValidationHandler = null!; private SqlMembershipSourcesController _sqlMembershipSourcesController = null!; [TestInitialize] @@ -46,8 +48,15 @@ public void Initialize() _getDefaultSqlMembershipSourceAttributeValuesHandler = new GetDefaultSqlMembershipSourceAttributeValuesHandler(_loggingRepository.Object, _dataFactoryRepository.Object, _sqlMembershipRepository.Object); _patchDefaultSqlMembershipSourceCustomLabelHandler = new PatchDefaultSqlMembershipSourceCustomLabelHandler(_loggingRepository.Object, _databaseSqlMembershipSourcesRepository.Object); _patchDefaultSqlMembershipSourceAttributesHandler = new PatchDefaultSqlMembershipSourceAttributesHandler(_loggingRepository.Object, _databaseSqlMembershipSourcesRepository.Object); - - _sqlMembershipSourcesController = new SqlMembershipSourcesController(_getDefaultSqlMembershipSourceHandler, _getDefaultSqlMembershipSourceAttributesHandler, _getDefaultSqlMembershipSourceAttributeMappingsHandler, _getDefaultSqlMembershipSourceAttributeValuesHandler, _patchDefaultSqlMembershipSourceCustomLabelHandler, _patchDefaultSqlMembershipSourceAttributesHandler) + _getSqlValidationHandler = new GetSqlValidationHandler(_loggingRepository.Object, _sqlMembershipRepository.Object, _dataFactoryRepository.Object); + + _sqlMembershipSourcesController = new SqlMembershipSourcesController(_getDefaultSqlMembershipSourceHandler, + _getDefaultSqlMembershipSourceAttributesHandler, + _getDefaultSqlMembershipSourceAttributeMappingsHandler, + _getDefaultSqlMembershipSourceAttributeValuesHandler, + _patchDefaultSqlMembershipSourceCustomLabelHandler, + _patchDefaultSqlMembershipSourceAttributesHandler, + _getSqlValidationHandler) { ControllerContext = CreateControllerContext(new List { @@ -158,7 +167,7 @@ public async Task TestRemovalOfStaleStoredSettingsAsync() { Name = "Name1", CustomLabel = "CustomLabel1", - Type = "nvarchar", + Type = "nvarchar", HasMapping = false }, new SqlMembershipAttribute @@ -342,6 +351,86 @@ public async Task ExceptionGetHRFilterAttributeValuesTestAsync() It.IsAny()), Times.Once()); } + [TestMethod] + public async Task SuccessfulValidateNoFiltersAsync() + { + var sqlFilters = new Dictionary(); + var response = await _sqlMembershipSourcesController.ValidateSqlFilterAsync(sqlFilters); + + var okResult = response as OkObjectResult; + + Assert.IsNotNull(okResult); + Assert.IsNotNull(okResult.Value); + + var getSqlValidationResponse = okResult.Value as GetSqlValidationResponse; + Assert.IsNotNull(getSqlValidationResponse); + Assert.IsTrue(getSqlValidationResponse.IsValid); + Assert.IsNull(getSqlValidationResponse.Errors); + } + + [TestMethod] + public async Task SuccessfulValidateGoodFiltersAsync() + { + _sqlMembershipRepository.Setup(x => x.ValidateFiltersAsync(It.IsAny>(), "RUN ID")).ReturnsAsync(new Dictionary()); + + var sqlFilters = new Dictionary() { { 0, "filter1" }, { 1, "filter2" } }; + var response = await _sqlMembershipSourcesController.ValidateSqlFilterAsync(sqlFilters); + + var okResult = response as OkObjectResult; + + Assert.IsNotNull(okResult); + Assert.IsNotNull(okResult.Value); + + var getSqlValidationResponse = okResult.Value as GetSqlValidationResponse; + Assert.IsNotNull(getSqlValidationResponse); + Assert.IsTrue(getSqlValidationResponse.IsValid); + Assert.IsNull(getSqlValidationResponse.Errors); + } + + [TestMethod] + public async Task SuccessfulValidateBadFiltersAsync() + { + var exceptionsDictionary = new Dictionary() { { 0, "Sql Error"} }; + _sqlMembershipRepository.Setup(x => x.ValidateFiltersAsync(It.IsAny>(), "RUN ID")).ReturnsAsync(exceptionsDictionary); + + var sqlFilters = new Dictionary() { { 0, "filter1" }, { 1, "filter2" } }; + var response = await _sqlMembershipSourcesController.ValidateSqlFilterAsync(sqlFilters); + + var okResult = response as OkObjectResult; + + Assert.IsNotNull(okResult); + Assert.IsNotNull(okResult.Value); + + var getSqlValidationResponse = okResult.Value as GetSqlValidationResponse; + Assert.IsNotNull(getSqlValidationResponse); + Assert.IsFalse(getSqlValidationResponse.IsValid); + Assert.AreEqual(getSqlValidationResponse.Errors!.Count, 1); + } + + [TestMethod] + public async Task ExceptionValidateFiltersAsync() + { + _sqlMembershipRepository.Setup(x => x.CheckIfTableExistsAsync(It.IsAny())).ReturnsAsync(false); + _sqlMembershipRepository.Setup(x => x.GetAttributeValuesAsync(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new Exception("Unexpected exception triggered for testing")); + + var sqlFilters = new Dictionary() { { 0, "any filter" } }; + + var response = await _sqlMembershipSourcesController.ValidateSqlFilterAsync(sqlFilters); + + Assert.IsNotNull(response); + + var internalServerErrorResponse = response as StatusCodeResult; + + Assert.IsNotNull(internalServerErrorResponse); + Assert.AreEqual(internalServerErrorResponse.StatusCode, (int)HttpStatusCode.InternalServerError); + + _loggingRepository.Verify(x => x.LogMessageAsync( + It.Is(m => m.Message.StartsWith("Unable to validate Sql filter")), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once()); + } + private ControllerContext CreateControllerContext(List claims) { return new ControllerContext { HttpContext = CreateHttpContext(claims) }; From a2094f18817477752e370ccad2e033da53153e65 Mon Sep 17 00:00:00 2001 From: abgonz Date: Tue, 27 Aug 2024 16:41:43 -0700 Subject: [PATCH 0340/1479] Changes to migrate PlaceMembershipObtainer to isolated worker model --- .../JobStatusUpdaterFunction.cs | 5 +- .../QueueMessageSenderFunction.cs | 5 +- .../RoomsReader/RoomsReaderFunction.cs | 5 +- .../SchemaValidatorFunction.cs | 5 +- .../SubsequentUsersReaderFunction.cs | 5 +- .../UsersReader/UsersReaderFunction.cs | 5 +- .../UsersSender/UsersSenderFunction.cs | 5 +- .../WorkSpacesReaderFunction.cs | 5 +- .../Orchestrator/OrchestratorFunction.cs | 8 +- .../Function/PlaceMembershipObtainer.csproj | 15 +++- .../Function/Program.cs | 73 +++++++++++++++++++ .../Function/Starter/StarterFunction.cs | 10 +-- .../Function/Startup.cs | 50 ------------- .../SubOrchestratorFunction.cs | 8 +- .../Function/local.settings.json | 3 +- .../Infrastructure/compute/template.bicep | 2 +- .../OrchestratorFunctionTests.cs | 53 +++++++------- .../Services.Tests/StarterTests.cs | 26 +++++-- .../SubOrchestratorFunctionTests.cs | 60 +++++++-------- .../Services.Tests/Tests.Services.csproj | 1 + .../Repositories.Mocks.csproj | 1 - 21 files changed, 192 insertions(+), 158 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs index 0fb4c1a9c..c208b26a3 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.PlaceMembershipObtainer { @@ -20,7 +19,7 @@ public JobStatusUpdaterFunction(ILoggingRepository loggingRepository, PlaceMembe _membershipProviderService = membershipProviderService; } - [FunctionName(nameof(JobStatusUpdaterFunction))] + [Function(nameof(JobStatusUpdaterFunction))] public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest request) { if (request.SyncJob != null) diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs index 9a65965a7..48b2d5d8a 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/QueueMessageSender/QueueMessageSenderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Models.ServiceBus; using Newtonsoft.Json; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.PlaceMembershipObtainer { @@ -23,7 +22,7 @@ public QueueMessageSenderFunction( _serviceBusQueueRepository = serviceBusQueueRepository ?? throw new ArgumentNullException(nameof(serviceBusQueueRepository)); } - [FunctionName(nameof(QueueMessageSenderFunction))] + [Function(nameof(QueueMessageSenderFunction))] public async Task SendMessageAsync([ActivityTrigger] MembershipAggregatorHttpRequest request) { diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/RoomsReader/RoomsReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/RoomsReader/RoomsReaderFunction.cs index 00d119580..9b988d5ae 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/RoomsReader/RoomsReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/RoomsReader/RoomsReaderFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Models; using Repositories.Contracts; @@ -10,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.PlaceMembershipObtainer { @@ -24,7 +23,7 @@ public RoomsReaderFunction(ILoggingRepository loggingRepository, PlaceMembership _membershipProviderService = membershipProviderService ?? throw new ArgumentNullException(nameof(membershipProviderService)); } - [FunctionName(nameof(RoomsReaderFunction))] + [Function(nameof(RoomsReaderFunction))] public async Task GetRoomsAsync([ActivityTrigger] RoomsReaderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(RoomsReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs index 67bd30ec8..3755bb666 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SchemaValidator/SchemaValidatorFunction.cs @@ -1,12 +1,11 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; using NJsonSchema; +using Microsoft.Azure.Functions.Worker; namespace Hosts.PlaceMembershipObtainer { @@ -21,7 +20,7 @@ public SchemaValidatorFunction(ILoggingRepository loggingRepository, SchemaProvi _schemaProvider = schemaProvider ?? throw new ArgumentNullException(nameof(schemaProvider)); } - [FunctionName(nameof(SchemaValidatorFunction))] + [Function(nameof(SchemaValidatorFunction))] public async Task ValidateSchemasAsync([ActivityTrigger] SchemaValidatorRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(SchemaValidatorFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SubsequentUsersReader/SubsequentUsersReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SubsequentUsersReader/SubsequentUsersReaderFunction.cs index 1be0a4a8c..00df90500 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SubsequentUsersReader/SubsequentUsersReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/SubsequentUsersReader/SubsequentUsersReaderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.PlaceMembershipObtainer { @@ -22,7 +21,7 @@ public SubsequentUsersReaderFunction(ILoggingRepository loggingRepository, Place _membershipProviderService = membershipProviderService ?? throw new ArgumentNullException(nameof(membershipProviderService)); } - [FunctionName(nameof(SubsequentUsersReaderFunction))] + [Function(nameof(SubsequentUsersReaderFunction))] public async Task GetUsersAsync([ActivityTrigger] SubsequentUsersReaderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(SubsequentUsersReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/UsersReader/UsersReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/UsersReader/UsersReaderFunction.cs index fd66c7baa..b87363ebd 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/UsersReader/UsersReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/UsersReader/UsersReaderFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Models; using Repositories.Contracts; @@ -10,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.PlaceMembershipObtainer { @@ -24,7 +23,7 @@ public UsersReaderFunction(ILoggingRepository loggingRepository, PlaceMembership _membershipProviderService = membershipProviderService ?? throw new ArgumentNullException(nameof(membershipProviderService)); } - [FunctionName(nameof(UsersReaderFunction))] + [Function(nameof(UsersReaderFunction))] public async Task GetUsersAsync([ActivityTrigger] UsersReaderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(UsersReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/UsersSender/UsersSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/UsersSender/UsersSenderFunction.cs index fc3edea51..6b968bc6a 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/UsersSender/UsersSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/UsersSender/UsersSenderFunction.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.PlaceMembershipObtainer { @@ -20,7 +19,7 @@ public UsersSenderFunction(ILoggingRepository loggingRepository, PlaceMembership _membershipProviderService = membershipProviderService; } - [FunctionName(nameof(UsersSenderFunction))] + [Function(nameof(UsersSenderFunction))] public async Task SendUsersAsync([ActivityTrigger] UsersSenderRequest request) { string filePath = null; diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/WorkSpacesReader/WorkSpacesReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/WorkSpacesReader/WorkSpacesReaderFunction.cs index 078e7c23c..44e0f5b27 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/WorkSpacesReader/WorkSpacesReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Activity/WorkSpacesReader/WorkSpacesReaderFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Graph; using Models; using Repositories.Contracts; @@ -10,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.PlaceMembershipObtainer { @@ -24,7 +23,7 @@ public WorkSpacesReaderFunction(ILoggingRepository loggingRepository, PlaceMembe _membershipProviderService = membershipProviderService ?? throw new ArgumentNullException(nameof(membershipProviderService)); } - [FunctionName(nameof(WorkSpacesReaderFunction))] + [Function(nameof(WorkSpacesReaderFunction))] public async Task GetWorkSpacesAsync([ActivityTrigger] WorkSpacesReaderRequest request) { await _log.LogMessageAsync(new LogMessage { Message = $"{nameof(WorkSpacesReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs index 94a4d2de1..9ac9d5bf6 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs @@ -1,7 +1,5 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Configuration; using Models; using Newtonsoft.Json; @@ -12,6 +10,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.PlaceMembershipObtainer { @@ -31,8 +31,8 @@ public OrchestratorFunction( _configuration = configuration; } - [FunctionName(nameof(OrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(OrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var mainRequest = context.GetInput(); var syncJob = mainRequest.SyncJob; diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj index 58e304dc3..0b1a0a83d 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/PlaceMembershipObtainer.csproj @@ -2,13 +2,17 @@ net8.0 v4 + Exe + enabled - - - + + + + + @@ -28,4 +32,7 @@ Never - + + + + \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Program.cs new file mode 100644 index 000000000..228e01084 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Program.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.Extensions.Hosting; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using Hosts.FunctionBase; +using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Repositories.BlobStorage; +using Repositories.Contracts; +using Repositories.GraphGroups; +using Repositories.ServiceBusQueue; +using Services; +using System; +using Azure.Identity; + +namespace Hosts.PlaceMembershipObtainer +{ + public class Program + { + public static void Main(string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = "PlaceMembershipObtainer"; + var dryRunSettingName = string.Empty; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + + services.AddGraphAPIClient() + .AddScoped() + .AddScoped(); + + services.AddSingleton((s) => + { + var configuration = s.GetService(); + var storageAccountName = configuration["membershipStorageAccountName"]; + var containerName = configuration["membershipContainerName"]; + + return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); + }); + services.AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var membershipAggregatorQueue = configuration["serviceBusMembershipAggregatorQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(membershipAggregatorQueue); + return new ServiceBusQueueRepository(sender); + }); + + }) + .Build(); + + host.Run(); + } + } +} diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Starter/StarterFunction.cs index 748629729..394646c14 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Starter/StarterFunction.cs @@ -3,13 +3,13 @@ using System; using System.Text; using System.Threading.Tasks; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Newtonsoft.Json; using Repositories.Contracts.InjectConfig; using Repositories.Contracts; using Azure.Messaging.ServiceBus; using Models; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask.Client; namespace Hosts.PlaceMembershipObtainer { @@ -26,10 +26,10 @@ public StarterFunction(ILoggingRepository loggingRepository, IDatabaseSyncJobsRe _isPlaceMembershipObtainerDryRunEnabled = dryRun.DryRunEnabled; } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task RunAsync( [ServiceBusTrigger("%serviceBusSyncJobTopic%", "PlaceMembership", Connection = "gmmServiceBus")] ServiceBusReceivedMessage message, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient client) { var syncJob = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(message.Body)); var runId = syncJob.RunId.GetValueOrDefault(Guid.Empty); @@ -52,7 +52,7 @@ public async Task RunAsync( TotalParts = message.ApplicationProperties.ContainsKey("TotalParts") ? Convert.ToInt32(message.ApplicationProperties["TotalParts"]) : 1 }; - var instanceId = await starter.StartNewAsync(nameof(OrchestratorFunction), request); + var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(OrchestratorFunction), request); await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"InstanceId: {instanceId} for job RowKey: {syncJob.RowKey} ", RunId = runId }); } diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Startup.cs deleted file mode 100644 index df6e1aa7a..000000000 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Startup.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Azure.Messaging.ServiceBus; -using Common.DependencyInjection; -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Repositories.BlobStorage; -using Repositories.Contracts; -using Repositories.GraphGroups; -using Repositories.ServiceBusQueue; -using Services; - -// see https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection -[assembly: FunctionsStartup(typeof(Hosts.PlaceMembershipObtainer.Startup))] - -namespace Hosts.PlaceMembershipObtainer -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(PlaceMembershipObtainer); - protected override string DryRunSettingName => "PlaceMembershipObtainer:IsPlaceMembershipObtainerDryRunEnabled"; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddGraphAPIClient() - .AddScoped() - .AddScoped() - .AddSingleton((s) => - { - var configuration = s.GetService(); - var storageAccountName = configuration["membershipStorageAccountName"]; - var containerName = configuration["membershipContainerName"]; - - return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); - }) - .AddSingleton(services => - { - var configuration = services.GetRequiredService(); - var membershipAggregatorQueue = configuration["serviceBusMembershipAggregatorQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(membershipAggregatorQueue); - return new ServiceBusQueueRepository(sender); - }); - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs index 41e05a6ec..84531552d 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs @@ -1,14 +1,14 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System.Threading.Tasks; using System.Collections.Generic; using System.Linq; using Microsoft.ApplicationInsights; using Models; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.PlaceMembershipObtainer { @@ -23,8 +23,8 @@ public SubOrchestratorFunction(ILoggingRepository loggingRepository, TelemetryCl _telemetryClient = telemetryClient; } - [FunctionName(nameof(SubOrchestratorFunction))] - public async Task<(List Users, SyncStatus Status)> RunSubOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(SubOrchestratorFunction))] + public async Task<(List Users, SyncStatus Status)> RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var request = context.GetInput(); var allUsers = new List(); diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/local.settings.json b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/local.settings.json index 68ca90451..7e546327d 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/local.settings.json +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/local.settings.json @@ -20,6 +20,7 @@ "membershipContainerName": "", "ConnectionStrings:JobsContext": "", "ConnectionStrings:JobsContextReadOnly": "", - "serviceBusMembershipAggregatorQueue": "" + "serviceBusMembershipAggregatorQueue": "", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep index 7836dc5a7..0787f815c 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep @@ -97,7 +97,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs index 7c8a0c878..daf2754ab 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs @@ -3,7 +3,6 @@ using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Extensions.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; @@ -17,6 +16,8 @@ using System.Threading.Tasks; using Services; using Repositories.Contracts.InjectConfig; +using Microsoft.DurableTask; +using Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http; namespace Tests.Services { @@ -27,7 +28,7 @@ public class OrchestratorFunctionTests private List _profiles; private Mock _configuration; private Mock _loggingRepository; - private Mock _context; + private Mock _context; private Mock _executionContext; private SyncJob _syncJob; private QuerySample _querySample; @@ -77,7 +78,7 @@ public void Setup() mockBlobStorageRepository.Object, mockSyncJob.Object, mockDryRunValue.Object); - _context = new Mock(); + _context = new Mock(); _executionContext = new Mock(); _telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); _serviceBusQueueRepository = new Mock(); @@ -100,7 +101,7 @@ public void Setup() _subOrchestratorResponseStatus = SyncStatus.InProgress; - _context.Setup(x => x.CallSubOrchestratorAsync<(List Users, SyncStatus Status)>(It.IsAny(), It.IsAny())) + _context.Setup(x => x.CallSubOrchestratorAsync<(List Users, SyncStatus Status)>(It.IsAny(), It.IsAny(), null)) .ReturnsAsync(() => { var users = new List(); @@ -111,28 +112,28 @@ public void Setup() var response = (Users: users, Status: _subOrchestratorResponseStatus); return response; - }); + }); string _filePath = null; - _context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _filePath = await CallUsersSenderFunctionAsync(request as UsersSenderRequest); - }) - .ReturnsAsync(() => _filePath); + _context.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) + .Callback(async (name, request, options) => + { + _filePath = await CallUsersSenderFunctionAsync(request as UsersSenderRequest); + }) + .ReturnsAsync(() => _filePath); - _context.Setup(x => x.CallHttpAsync(It.IsAny())).ReturnsAsync(() => _membershipAggregatorResponse); + _context.Setup(r => r.CallActivityAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(_membershipAggregatorResponse); - _context.Setup(x => x.CallActivityAsync(nameof(QueueMessageSenderFunction), It.IsAny())) - .Callback(async (name, request) => - { - await CallQueueMessageSenderFunctionAsync(request as MembershipAggregatorHttpRequest); - }); + _context.Setup(x => x.CallActivityAsync(nameof(QueueMessageSenderFunction), It.IsAny(), null)) + .Callback(async (name, request, options) => + { + await CallQueueMessageSenderFunctionAsync(request as MembershipAggregatorHttpRequest); + }); _schemaProvider = SchemaProviderFactory.CreateJsonSchemaProvider(); - _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) - .Callback(async (name, request) => + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny(), null)) + .Callback(async (name, request, options) => { await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); }) @@ -165,17 +166,17 @@ public async Task TestInvalidSchemaAsync() Period = 6 }; - _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny())) - .Callback(async (name, request) => - { - await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); - }) - .ReturnsAsync(() => false); + _context.Setup(x => x.CallActivityAsync(nameof(SchemaValidatorFunction), It.IsAny(), null)) + .Callback(async (taskName, request, options) => + { + await CallSchemaValidatorFunctionAsync(request as SchemaValidatorRequest); + }) + .ReturnsAsync(() => false); var orchestratorFunction = new OrchestratorFunction(_loggingRepository.Object, _placeMembershipObtainerService, _configuration.Object); await orchestratorFunction.RunOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), - It.Is(x => x.Status == SyncStatus.SchemaError)), Times.Once()); + It.Is(x => x.Status == SyncStatus.SchemaError), null), Times.Once()); } private async Task CallUsersSenderFunctionAsync(UsersSenderRequest request) diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/StarterTests.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/StarterTests.cs index 723ed7351..9929d2380 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/StarterTests.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/StarterTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Azure.Messaging.ServiceBus; using Hosts.PlaceMembershipObtainer; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Models; @@ -13,6 +12,9 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; +using Microsoft.DurableTask; +using Repositories.Mocks; +using System.Threading; namespace Tests.Services { @@ -22,7 +24,7 @@ public class StarterTests private Mock _dryRunValue; private Mock _loggingRepository; private Mock _syncJobRepository; - private Mock _durableOrchestrationClient; + private Mock _durableOrchestrationClient; private SyncJob _syncJob; [TestInitialize] @@ -31,7 +33,7 @@ public void Setup() _dryRunValue = new Mock(); _loggingRepository = new Mock(); _syncJobRepository = new Mock(); - _durableOrchestrationClient = new Mock(); + _durableOrchestrationClient = new Mock(); _syncJob = new SyncJob { @@ -57,10 +59,12 @@ public async Task TestRegularSyncJobRun() var starterFunction = new StarterFunction(_loggingRepository.Object, _syncJobRepository.Object, _dryRunValue.Object); await starterFunction.RunAsync(message, _durableOrchestrationClient.Object); - _durableOrchestrationClient.Verify(x => x.StartNewAsync( - It.IsAny(), - It.Is(r => r.CurrentPart == 1 && r.TotalParts == 3) - ), Times.Once); + _durableOrchestrationClient.Verify(x => x.ScheduleNewOrchestrationInstanceAsync( + It.IsAny(), + It.Is(r => r.CurrentPart == 1 && r.TotalParts == 3), + null, + It.IsAny() + ), Times.Once); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.StartsWith("InstanceId")), @@ -95,7 +99,13 @@ public async Task TestDryRunSyncJobRun() var starterFunction = new StarterFunction(_loggingRepository.Object, _syncJobRepository.Object, _dryRunValue.Object); await starterFunction.RunAsync(message, _durableOrchestrationClient.Object); - _durableOrchestrationClient.Verify(x => x.StartNewAsync(It.IsAny(), It.IsAny()), Times.Never); + _durableOrchestrationClient.Verify(x => x.ScheduleNewOrchestrationInstanceAsync( + It.IsAny(), + It.Is(r => r.CurrentPart == 1 && r.TotalParts == 3), + null, + It.IsAny() + ), Times.Never); + _syncJobRepository.Verify(x => x.UpdateSyncJobStatusAsync(It.IsAny>(), It.Is(s => s == SyncStatus.Idle)), Times.Once); _loggingRepository.Verify(x => x.LogMessageAsync( diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs index f81d86204..771c8f699 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs @@ -3,7 +3,6 @@ using Hosts.PlaceMembershipObtainer; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Models.ServiceBus; @@ -16,6 +15,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.DurableTask; namespace Tests.Services { @@ -29,7 +29,7 @@ public class SubOrchestratorFunctionTests private Mock _graphGroupRepository; private Mock _emailSenderRecipient; private Mock _blobStorageRepository; - private Mock _durableOrchestrationContext; + private Mock _durableOrchestrationContext; private int _userCount; private BlobResult _blobResult; @@ -49,7 +49,7 @@ public void Setup() _graphGroupRepository = new Mock(); _emailSenderRecipient = new Mock(); _blobStorageRepository = new Mock(); - _durableOrchestrationContext = new Mock(); + _durableOrchestrationContext = new Mock(); _userCount = 10; @@ -82,33 +82,33 @@ public void Setup() _durableOrchestrationContext.Setup(x => x.GetInput()).Returns(() => _subOrchestratorRequest); - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _placesReaderResponse = await CallRoomsReaderFunctionAsync(request as RoomsReaderRequest); - }) - .ReturnsAsync(() => _placesReaderResponse); - - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _workSpacesReaderResponse = await CallWorkSpacesReaderFunctionAsync(request as WorkSpacesReaderRequest); - }) - .ReturnsAsync(() => _workSpacesReaderResponse); - - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _usersReaderResponse = await CallUsersReaderFunctionAsync(request as UsersReaderRequest); - }) - .ReturnsAsync(() => _usersReaderResponse); - - _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny())) - .Callback(async (name, request) => - { - _usersReaderResponse = await CallSubsequentUsersReaderFunctionAsync(request as SubsequentUsersReaderRequest); - }) - .ReturnsAsync(() => _usersReaderResponse); + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) + .Callback(async (taskName, request, options) => + { + _placesReaderResponse = await CallRoomsReaderFunctionAsync(request as RoomsReaderRequest); + }) + .ReturnsAsync(() => _placesReaderResponse); + + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) + .Callback(async (taskName, request, options) => + { + _workSpacesReaderResponse = await CallWorkSpacesReaderFunctionAsync(request as WorkSpacesReaderRequest); + }) + .ReturnsAsync(() => _workSpacesReaderResponse); + + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) + .Callback(async (taskName, request, options) => + { + _usersReaderResponse = await CallUsersReaderFunctionAsync(request as UsersReaderRequest); + }) + .ReturnsAsync(() => _usersReaderResponse); + + _durableOrchestrationContext.Setup(x => x.CallActivityAsync(It.IsAny(), It.IsAny(), null)) + .Callback(async (taskName, request, options) => + { + _usersReaderResponse = await CallSubsequentUsersReaderFunctionAsync(request as SubsequentUsersReaderRequest); + }) + .ReturnsAsync(() => _usersReaderResponse); _blobStorageRepository.Setup(x => x.DownloadFileAsync(It.IsAny())).ReturnsAsync(() => _blobResult); } diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj index b3b993958..2a189b7f2 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj @@ -24,6 +24,7 @@ + diff --git a/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj b/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj index 62bfdf1bb..8bb45c26b 100644 --- a/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj +++ b/Service/GroupMembershipManagement/Repositories.Mocks/Repositories.Mocks.csproj @@ -20,4 +20,3 @@ - From 0f32bff44823c2209a1ebad0bc3cb09e2a2cd47f Mon Sep 17 00:00:00 2001 From: Abril Gonzalez Date: Tue, 1 Oct 2024 11:00:02 -0700 Subject: [PATCH 0341/1479] Remove FUNCTIONS_INPROC_NET8_ENABLED setting --- .../Infrastructure/compute/template.bicep | 1 - 1 file changed, 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep index 0787f815c..afdac2165 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Infrastructure/compute/template.bicep @@ -99,7 +99,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { From 63347e055db6939048a33841eac72109606318df Mon Sep 17 00:00:00 2001 From: abgonz Date: Mon, 23 Sep 2024 12:00:15 -0700 Subject: [PATCH 0342/1479] Change tuple to SubOrchestratorResponse --- .../Orchestrator/OrchestratorFunction.cs | 2 +- .../SubOrchestratorFunction.cs | 6 ++-- .../SubOrchestratorResponse.cs | 14 ++++++++ .../OrchestratorFunctionTests.cs | 4 +-- .../Services.Tests/StarterTests.cs | 2 +- .../SubOrchestratorFunctionTests.cs | 32 +++++++++---------- .../Services.Tests/Tests.Services.csproj | 2 +- 7 files changed, 38 insertions(+), 24 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorResponse.cs diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs index 9ac9d5bf6..bc5a1f2b3 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/Orchestrator/OrchestratorFunction.cs @@ -88,7 +88,7 @@ public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationC } } - var response = await context.CallSubOrchestratorAsync<(List Users, SyncStatus Status)>(nameof(SubOrchestratorFunction), + var response = await context.CallSubOrchestratorAsync(nameof(SubOrchestratorFunction), new SubOrchestratorRequest { SyncJob = syncJob, Url = currentQueryAsString, RunId = runId }); if (response.Status != SyncStatus.InProgress) diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs index 84531552d..1a0239e82 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorFunction.cs @@ -24,7 +24,7 @@ public SubOrchestratorFunction(ILoggingRepository loggingRepository, TelemetryCl } [Function(nameof(SubOrchestratorFunction))] - public async Task<(List Users, SyncStatus Status)> RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) + public async Task RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var request = context.GetInput(); var allUsers = new List(); @@ -66,12 +66,12 @@ public SubOrchestratorFunction(ILoggingRepository loggingRepository, TelemetryCl else { _ = _log.LogMessageAsync(new LogMessage { RunId = request.RunId, Message = $"Url {request.Url} not supported" }); - return (allUsers, SyncStatus.Error); + return ( new SubOrchestratorResponse { Users = allUsers, Status = SyncStatus.Error }); } _ = _log.LogMessageAsync(new LogMessage { RunId = request.RunId, Message = $"Read {allUsers.Count} users" }); } _ = _log.LogMessageAsync(new LogMessage { Message = $"{nameof(SubOrchestratorFunction)} function completed", RunId = request.RunId }, VerbosityLevel.DEBUG); - return (allUsers, SyncStatus.InProgress); + return (new SubOrchestratorResponse { Users = allUsers, Status = SyncStatus.InProgress }); } } } \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorResponse.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorResponse.cs new file mode 100644 index 000000000..b426d4055 --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Function/SubOrchestrator/SubOrchestratorResponse.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Collections.Generic; + +namespace Models +{ + public class SubOrchestratorResponse + { + public List Users { get; set; } + public SyncStatus Status { get; set; } + } +} + diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs index daf2754ab..e708e9a58 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/OrchestratorFunctionTests.cs @@ -101,7 +101,7 @@ public void Setup() _subOrchestratorResponseStatus = SyncStatus.InProgress; - _context.Setup(x => x.CallSubOrchestratorAsync<(List Users, SyncStatus Status)>(It.IsAny(), It.IsAny(), null)) + _context.Setup(x => x.CallSubOrchestratorAsync(It.IsAny(), It.IsAny(), null)) .ReturnsAsync(() => { var users = new List(); @@ -110,7 +110,7 @@ public void Setup() users.Add(new AzureADUser { ObjectId = Guid.NewGuid() }); } - var response = (Users: users, Status: _subOrchestratorResponseStatus); + var response = new SubOrchestratorResponse { Users = users, Status = _subOrchestratorResponseStatus }; return response; }); diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/StarterTests.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/StarterTests.cs index 9929d2380..11e900d0e 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/StarterTests.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/StarterTests.cs @@ -13,8 +13,8 @@ using System.Text; using System.Threading.Tasks; using Microsoft.DurableTask; -using Repositories.Mocks; using System.Threading; +using Repositories.Mocks; namespace Tests.Services { diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs index 771c8f699..dfb29df79 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/SubOrchestratorFunctionTests.cs @@ -163,7 +163,7 @@ public async Task ProcessRoomsRequestTestAsync() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var subOrchestratorFunction = new SubOrchestratorFunction(_loggingRepository.Object, telemetryClient); - var (Users, Status) = await subOrchestratorFunction.RunSubOrchestratorAsync(_durableOrchestrationContext.Object); + var subOrchestratorResponse = await subOrchestratorFunction.RunSubOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message == $"{nameof(SubOrchestratorFunction)} function started"), @@ -181,9 +181,9 @@ public async Task ProcessRoomsRequestTestAsync() It.IsAny() ), Times.Once); - Assert.IsNotNull(Users); - Assert.AreEqual(_userCount, Users.Count); - Assert.AreEqual(SyncStatus.InProgress, Status); + Assert.IsNotNull(subOrchestratorResponse.Users); + Assert.AreEqual(_userCount, subOrchestratorResponse.Users.Count); + Assert.AreEqual(SyncStatus.InProgress, subOrchestratorResponse.Status); } [TestMethod] @@ -236,7 +236,7 @@ public async Task ProcessWorkSpacesRequestTestAsync() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var subOrchestratorFunction = new SubOrchestratorFunction(_loggingRepository.Object, telemetryClient); - var (Users, Status) = await subOrchestratorFunction.RunSubOrchestratorAsync(_durableOrchestrationContext.Object); + var subOrchestratorResponse = await subOrchestratorFunction.RunSubOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message == $"{nameof(SubOrchestratorFunction)} function started"), @@ -254,9 +254,9 @@ public async Task ProcessWorkSpacesRequestTestAsync() It.IsAny() ), Times.Once); - Assert.IsNotNull(Users); - Assert.AreEqual(_userCount, Users.Count); - Assert.AreEqual(SyncStatus.InProgress, Status); + Assert.IsNotNull(subOrchestratorResponse.Users); + Assert.AreEqual(_userCount, subOrchestratorResponse.Users.Count); + Assert.AreEqual(SyncStatus.InProgress, subOrchestratorResponse.Status); } [TestMethod] @@ -308,7 +308,7 @@ public async Task ProcessUsersRequestTestAsync() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var subOrchestratorFunction = new SubOrchestratorFunction(_loggingRepository.Object, telemetryClient); - var (Users, Status) = await subOrchestratorFunction.RunSubOrchestratorAsync(_durableOrchestrationContext.Object); + var subOrchestratorResponse = await subOrchestratorFunction.RunSubOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message == $"{nameof(SubOrchestratorFunction)} function started"), @@ -326,9 +326,9 @@ public async Task ProcessUsersRequestTestAsync() It.IsAny() ), Times.Once); - Assert.IsNotNull(Users); - Assert.AreEqual(_userCount, Users.Count); - Assert.AreEqual(SyncStatus.InProgress, Status); + Assert.IsNotNull(subOrchestratorResponse.Users); + Assert.AreEqual(_userCount, subOrchestratorResponse.Users.Count); + Assert.AreEqual(SyncStatus.InProgress, subOrchestratorResponse.Status); } [TestMethod] @@ -391,7 +391,7 @@ public async Task ProcessSubsequentUsersRequestTestAsync() var telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); var subOrchestratorFunction = new SubOrchestratorFunction(_loggingRepository.Object, telemetryClient); - var (Users, Status) = await subOrchestratorFunction.RunSubOrchestratorAsync(_durableOrchestrationContext.Object); + var subOrchestratorResponse = await subOrchestratorFunction.RunSubOrchestratorAsync(_durableOrchestrationContext.Object); _loggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message == $"{nameof(SubOrchestratorFunction)} function started"), @@ -410,9 +410,9 @@ public async Task ProcessSubsequentUsersRequestTestAsync() It.IsAny() ), Times.Once); - Assert.IsNotNull(Users); - Assert.AreEqual(_userCount, Users.Count); - Assert.AreEqual(SyncStatus.InProgress, Status); + Assert.IsNotNull(subOrchestratorResponse.Users); + Assert.AreEqual(_userCount, subOrchestratorResponse.Users.Count); + Assert.AreEqual(SyncStatus.InProgress, subOrchestratorResponse.Status); } private async Task CallRoomsReaderFunctionAsync(RoomsReaderRequest request) diff --git a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj index 2a189b7f2..2de46d62f 100644 --- a/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj +++ b/Service/GroupMembershipManagement/Hosts/PlaceMembershipObtainer/Services.Tests/Tests.Services.csproj @@ -24,7 +24,7 @@ - + From 088cceef1047905b8973b60d49aae4d2b76204e2 Mon Sep 17 00:00:00 2001 From: abgonz Date: Wed, 28 Aug 2024 15:35:38 -0700 Subject: [PATCH 0343/1479] Migration to isolated worker model for TeamsChannelUpdater --- .../EmailSender/EmailSenderFunction.cs | 5 +- .../FileDownloader/FileDownloaderFunction.cs | 6 +- .../GroupNameReaderFunction.cs | 5 +- .../Activity/JobReader/JobReaderFunction.cs | 5 +- .../JobStatusUpdaterFunction.cs | 5 +- .../Activity/Logger/LoggerFunction.cs | 5 +- .../MessageReader/MessageReaderFunction.cs | 5 +- .../TeamsUpdater/TeamsUpdaterFunction.cs | 5 +- .../TelemetryTrackerFunction.cs | 5 +- .../Orchestrator/OrchestratorFunction.cs | 12 +-- .../TeamsChannelUpdater/Function/Program.cs | 99 +++++++++++++++++++ .../QueueMessageOrchestratorFunction.cs | 9 +- .../Function/Starter/StarterFunction.cs | 13 +-- .../TeamsChannelUpdater/Function/Startup.cs | 87 ---------------- .../Function/TeamsChannelUpdater.csproj | 29 ++++-- .../Function/TeamsChannelUpdater.sln | 6 ++ ...msChannelUpdaterSubOrchestratorFunction.cs | 8 +- .../TeamsChannelUpdater/Function/host.json | 3 +- .../Infrastructure/compute/template.bicep | 2 +- .../Services.Tests/OrchestratorTests.cs | 40 ++++---- .../QueueMessageOrchestratorTests.cs | 49 ++++++--- .../Services.Tests/Services.Tests.csproj | 1 + .../Services.Tests/StarterFunctionTests.cs | 15 +-- ...TeamsChannelUpdaterSubOrchestratorTests.cs | 16 +-- 24 files changed, 238 insertions(+), 197 deletions(-) create mode 100644 Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Program.cs delete mode 100644 Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs index 74df3955e..33f58627d 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/EmailSender/EmailSenderFunction.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using Services.Contracts; using System; using System.Threading.Tasks; using Services.TeamsChannelUpdater.Contracts; +using Microsoft.Azure.Functions.Worker; namespace Hosts.TeamsChannelUpdater { @@ -22,7 +21,7 @@ public EmailSenderFunction(ILoggingRepository loggingRepository, ITeamsChannelUp _teamsChannelUpdaterService = teamsChannelUpdaterService ?? throw new ArgumentNullException(nameof(teamsChannelUpdaterService)); ; } - [FunctionName(nameof(EmailSenderFunction))] + [Function(nameof(EmailSenderFunction))] public async Task SendEmailAsync([ActivityTrigger] EmailSenderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(EmailSenderFunction)} function started", RunId = request.SyncJob.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/FileDownloader/FileDownloaderFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/FileDownloader/FileDownloaderFunction.cs index 9776fab23..4683e7df5 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/FileDownloader/FileDownloaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/FileDownloader/FileDownloaderFunction.cs @@ -1,13 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.IO; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.TeamsChannelUpdater { @@ -22,7 +20,7 @@ public FileDownloaderFunction(ILoggingRepository loggingRepository, IBlobStorage _blobStorageRepository = blobStorageRepository ?? throw new ArgumentNullException(nameof(blobStorageRepository)); } - [FunctionName(nameof(FileDownloaderFunction))] + [Function(nameof(FileDownloaderFunction))] public async Task DownloadFileAsync([ActivityTrigger] FileDownloaderRequest request) { var blobResult = new BlobResult { BlobStatus = BlobStatus.NotFound }; diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs index ce9c7e727..d02734a92 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/GroupNameReader/GroupNameReaderFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; using Services.TeamsChannelUpdater.Contracts; +using Microsoft.Azure.Functions.Worker; namespace Hosts.TeamsChannelUpdater { @@ -21,7 +20,7 @@ public GroupNameReaderFunction(ILoggingRepository loggingRepository, ITeamsChann _teamsChannelUpdaterService = teamsChannelUpdaterService ?? throw new ArgumentNullException(nameof(teamsChannelUpdaterService)); } - [FunctionName(nameof(GroupNameReaderFunction))] + [Function(nameof(GroupNameReaderFunction))] public async Task GetGroupNameAsync([ActivityTrigger] GroupNameReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(GroupNameReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/JobReader/JobReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/JobReader/JobReaderFunction.cs index e6b6c75bd..c56bd8833 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/JobReader/JobReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/JobReader/JobReaderFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.TeamsChannelUpdater.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.TeamsChannelUpdater { @@ -21,7 +20,7 @@ public JobReaderFunction(ILoggingRepository loggingRepository, ITeamsChannelUpda _teamsChannelUpdaterService = teamsChannelUpdaterService ?? throw new ArgumentNullException(nameof(teamsChannelUpdaterService)); } - [FunctionName(nameof(JobReaderFunction))] + [Function(nameof(JobReaderFunction))] public async Task GetSyncJobAsync([ActivityTrigger] JobReaderRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobReaderFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs index 580bacfbb..e8399b589 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/JobStatusUpdater/JobStatusUpdaterFunction.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using Services.TeamsChannelUpdater.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.TeamsChannelUpdater { @@ -23,7 +22,7 @@ public JobStatusUpdaterFunction( _teamsChannelUpdaterService = teamsChannelUpdaterService ?? throw new ArgumentNullException(nameof(teamsChannelUpdaterService)); } - [FunctionName(nameof(JobStatusUpdaterFunction))] + [Function(nameof(JobStatusUpdaterFunction))] public async Task UpdateJobStatusAsync([ActivityTrigger] JobStatusUpdaterRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(JobStatusUpdaterFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/Logger/LoggerFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/Logger/LoggerFunction.cs index 83fc7ea21..85af1b1fe 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/Logger/LoggerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/Logger/LoggerFunction.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.TeamsChannelUpdater { @@ -18,7 +17,7 @@ public LoggerFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(LoggerFunction))] + [Function(nameof(LoggerFunction))] public async Task LogMessageAsync([ActivityTrigger] LoggerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = request.Message, RunId = request.RunId },request.Verbosity); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/MessageReader/MessageReaderFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/MessageReader/MessageReaderFunction.cs index 0351c4f3c..844d78d89 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/MessageReader/MessageReaderFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/MessageReader/MessageReaderFunction.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Azure.Messaging.ServiceBus; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Newtonsoft.Json; using Repositories.Contracts; using System; using System.Text; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.TeamsChannelUpdater { @@ -23,7 +22,7 @@ public MessageReaderFunction(ILoggingRepository loggingRepository, ServiceBusRec _serviceBusReceiver = serviceBusReceiver ?? throw new ArgumentNullException(nameof(serviceBusReceiver)); } - [FunctionName(nameof(MessageReaderFunction))] + [Function(nameof(MessageReaderFunction))] public async Task GetSyncJobAsync([ActivityTrigger] object input) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(MessageReaderFunction)} function started" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/TeamsUpdater/TeamsUpdaterFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/TeamsUpdater/TeamsUpdaterFunction.cs index 69937c963..3c9191dc0 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/TeamsUpdater/TeamsUpdaterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/TeamsUpdater/TeamsUpdaterFunction.cs @@ -2,14 +2,13 @@ // Licensed under the MIT license. using Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Threading.Tasks; using Services.TeamsChannelUpdater.Contracts; using System.Collections.Generic; using Models.Entities; +using Microsoft.Azure.Functions.Worker; namespace Hosts.TeamsChannelUpdater { @@ -26,7 +25,7 @@ public TeamsUpdaterFunction( _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(TeamsUpdaterFunction))] + [Function(nameof(TeamsUpdaterFunction))] public async Task RunAsync([ActivityTrigger] TeamsUpdaterRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TeamsUpdaterFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs index 29e38bf73..4df8e9935 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Activity/TelemetryTracker/TelemetryTrackerFunction.cs @@ -2,12 +2,11 @@ // Licensed under the MIT license. using Models; using Microsoft.ApplicationInsights; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; namespace Hosts.TeamsChannelUpdater { @@ -22,7 +21,7 @@ public TelemetryTrackerFunction(ILoggingRepository loggingRepository, TelemetryC _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - [FunctionName(nameof(TelemetryTrackerFunction))] + [Function(nameof(TelemetryTrackerFunction))] public async Task TrackEventAsync([ActivityTrigger] TelemetryTrackerRequest request) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(TelemetryTrackerFunction)} function started", RunId = request.RunId }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Orchestrator/OrchestratorFunction.cs index f70b3d5ff..4b49a9d98 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Orchestrator/OrchestratorFunction.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Repositories.Contracts; using System.Threading.Tasks; using Models; @@ -14,11 +12,13 @@ using Models.ServiceBus; using TeamsChannelUpdater.Helpers; using Repositories.Contracts.InjectConfig; -using ExecutionContext = Microsoft.Azure.WebJobs.ExecutionContext; using Models.Entities; using System.Text.Json; using Services.TeamsChannelUpdater.Contracts; using Models.Notifications; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace Hosts.TeamsChannelUpdater { @@ -46,9 +46,9 @@ public OrchestratorFunction(ILoggingRepository loggingRepository, _gmmResources = gmmResources ?? throw new ArgumentNullException(nameof(gmmResources)); } - [FunctionName(nameof(OrchestratorFunction))] + [Function(nameof(OrchestratorFunction))] public async Task RunOrchestratorAsync( - [OrchestrationTrigger] IDurableOrchestrationContext context, ExecutionContext executionContext) + [OrchestrationTrigger] TaskOrchestrationContext context) { TeamsGroupMembership groupMembership = null; MembershipHttpRequest graphRequest = null; @@ -221,7 +221,7 @@ await context.CallActivityAsync(nameof(JobStatusUpdaterFunction), } } - private void TrackSyncCompleteEvent(IDurableOrchestrationContext context, SyncJob syncJob, SyncCompleteCustomEvent syncCompleteEvent, string successStatus) + private void TrackSyncCompleteEvent(TaskOrchestrationContext context, SyncJob syncJob, SyncCompleteCustomEvent syncCompleteEvent, string successStatus) { var timeElapsedForJob = (context.CurrentUtcDateTime - syncJob.Timestamp.GetValueOrDefault()).TotalSeconds; _telemetryClient.TrackMetric(nameof(Metric.SyncJobTimeElapsedSeconds), timeElapsedForJob); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Program.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Program.cs new file mode 100644 index 000000000..f94c17bde --- /dev/null +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Program.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Azure.Core; +using Azure.Identity; +using Azure.Messaging.ServiceBus; +using Common.DependencyInjection; +using Hosts.FunctionBase; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Graph; +using Repositories.BlobStorage; +using Repositories.Contracts; +using Repositories.ServiceBusQueue; +using Repositories.TeamsChannel; +using Services.TeamsChannelUpdater; +using Services.TeamsChannelUpdater.Contracts; +using System; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration((context, config) => + { + var settings = config.Build(); + var appConfigEndpoint = CommonServices.GetValueOrThrowBase("appConfigurationEndpoint"); + + config.AddAzureAppConfiguration(options => + { + options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) + .UseFeatureFlags(); + }); + }) + .ConfigureServices((context, services) => + { + var configuration = context.Configuration; + var functionName = "TeamsChannelUpdater"; + var dryRunSettingName = "TeamsChannel:IsTeamsChannelDryRunEnabled"; + var rootPath = context.HostingEnvironment.ContentRootPath; + CommonServices.ConfigureCommonServices(services, configuration, functionName, dryRunSettingName, rootPath); + + services.AddSingleton((services) => + { + var configuration = services.GetService(); + var graphCredentials = services.GetService>().Value; + + var channelReadWriteApplicationPermissionGranted = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); + + TokenCredential graphTokenCredential; + if (channelReadWriteApplicationPermissionGranted) + { + graphTokenCredential = FunctionAppDI.CreateAuthProviderFromSecret(graphCredentials); + } + else + { + graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; + graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; + + graphTokenCredential = FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials); + } + return new GraphServiceClient(graphTokenCredential); + }) + .AddSingleton((s) => + { + var configuration = s.GetService(); + var storageAccountName = configuration["membershipStorageAccountName"]; + var containerName = configuration["membershipContainerName"]; + + return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); + }) + .AddTransient() + .AddSingleton(services => + { + var client = services.GetRequiredService(); + var serviceBusMembershipUpdatersTopic = CommonServices.GetValueOrThrowBase("serviceBusMembershipUpdatersTopic"); + var receiver = client.CreateReceiver(serviceBusMembershipUpdatersTopic, "TeamsChannelUpdater"); + return receiver; + }) + .AddSingleton(services => + { + var configuration = services.GetRequiredService(); + var notificationsQueue = configuration["serviceBusNotificationsQueue"]; + var client = services.GetRequiredService(); + var sender = client.CreateSender(notificationsQueue); + return new ServiceBusQueueRepository(sender); + }) + .AddTransient(); + }) + .Build(); + +host.Run(); + +static bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) +{ + var checkParse = bool.TryParse(configuration[settingName], out bool value); + return checkParse ? value : defaultValue; +} + diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/QueueMessageOrchestrator/QueueMessageOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/QueueMessageOrchestrator/QueueMessageOrchestratorFunction.cs index 360d76144..abd52bb7d 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/QueueMessageOrchestrator/QueueMessageOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/QueueMessageOrchestrator/QueueMessageOrchestratorFunction.cs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace Hosts.TeamsChannelUpdater { @@ -18,8 +19,8 @@ public QueueMessageOrchestratorFunction(ILoggingRepository loggingRepository) _loggingRepository = loggingRepository ?? throw new ArgumentNullException(nameof(loggingRepository)); } - [FunctionName(nameof(QueueMessageOrchestratorFunction))] - public async Task RunOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(QueueMessageOrchestratorFunction))] + public async Task RunOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { try { diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Starter/StarterFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Starter/StarterFunction.cs index 838eba1bc..42b1ca6f6 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Starter/StarterFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Starter/StarterFunction.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. using Azure.Messaging.ServiceBus; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Models; using Repositories.Contracts; using System; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask.Client; namespace Hosts.TeamsChannelUpdater { @@ -21,15 +21,16 @@ public StarterFunction(ILoggingRepository loggingRepository, ServiceBusReceiver _serviceBusReceiver = serviceBusReceiver ?? throw new ArgumentNullException(nameof(serviceBusReceiver)); } - [FunctionName(nameof(StarterFunction))] + [Function(nameof(StarterFunction))] public async Task RunAsync( [TimerTrigger("%triggerSchedule%")] TimerInfo myTimer, - [DurableClient] IDurableOrchestrationClient starter) + [DurableClient] DurableTaskClient client) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function started" }, VerbosityLevel.DEBUG); var instanceId = nameof(QueueMessageOrchestratorFunction); - var orchestratorStatus = await starter.GetStatusAsync(instanceId); + // Inside the RunAsync method + var orchestratorStatus = await client.GetInstanceAsync(instanceId); var isRunning = orchestratorStatus != null && orchestratorStatus.RuntimeStatus != OrchestrationRuntimeStatus.Completed && orchestratorStatus.RuntimeStatus != OrchestrationRuntimeStatus.Terminated @@ -38,7 +39,7 @@ public async Task RunAsync( if (!isRunning) { await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"Calling {instanceId}" }, VerbosityLevel.INFO); - await starter.StartNewAsync(instanceId, instanceId, (object)null); + await client.ScheduleNewOrchestrationInstanceAsync(instanceId, instanceId, null); } await _loggingRepository.LogMessageAsync(new LogMessage { Message = $"{nameof(StarterFunction)} function completed" }, VerbosityLevel.DEBUG); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs deleted file mode 100644 index 1e641350a..000000000 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/Startup.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using Azure.Core; -using Azure.Messaging.ServiceBus; -using Common.DependencyInjection; -using Hosts.FunctionBase; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.Graph; -using Repositories.BlobStorage; -using Repositories.Contracts; -using Repositories.ServiceBusQueue; -using Repositories.TeamsChannel; -using Services.TeamsChannelUpdater; -using Services.TeamsChannelUpdater.Contracts; - -// see https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection -[assembly: FunctionsStartup(typeof(Hosts.TeamsChannelUpdater.Startup))] -namespace Hosts.TeamsChannelUpdater -{ - public class Startup : CommonStartup - { - protected override string FunctionName => nameof(TeamsChannelUpdater); - protected override string DryRunSettingName => "TeamsChannel:IsTeamsChannelDryRunEnabled"; - - public override void Configure(IFunctionsHostBuilder builder) - { - base.Configure(builder); - - builder.Services.AddSingleton((services) => - { - var configuration = services.GetService(); - var graphCredentials = services.GetService>().Value; - - var channelReadWriteApplicationPermissionGranted = GetBoolSetting(configuration, "TeamsChannel:IsChannelReadWriteApplicationPermissionGranted", false); - - TokenCredential graphTokenCredential; - if (channelReadWriteApplicationPermissionGranted) - { - graphTokenCredential = FunctionAppDI.CreateAuthProviderFromSecret(graphCredentials); - } - else - { - graphCredentials.ServiceAccountUserName = configuration["teamsChannelServiceAccountUsername"]; - graphCredentials.ServiceAccountPassword = configuration["teamsChannelServiceAccountPassword"]; - - graphTokenCredential = FunctionAppDI.CreateServiceAccountAuthProvider(graphCredentials); - } - return new GraphServiceClient(graphTokenCredential); - }) - .AddSingleton((s) => - { - var configuration = s.GetService(); - var storageAccountName = configuration["membershipStorageAccountName"]; - var containerName = configuration["membershipContainerName"]; - - return new BlobStorageRepository($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); - }) - .AddTransient() - .AddSingleton(services => - { - var client = services.GetRequiredService(); - var serviceBusMembershipUpdatersTopic = GetValueOrThrow("serviceBusMembershipUpdatersTopic"); - var receiver = client.CreateReceiver(serviceBusMembershipUpdatersTopic, "TeamsChannelUpdater"); - return receiver; - }) - .AddSingleton(services => - { - var configuration = services.GetRequiredService(); - var notificationsQueue = configuration["serviceBusNotificationsQueue"]; - var client = services.GetRequiredService(); - var sender = client.CreateSender(notificationsQueue); - return new ServiceBusQueueRepository(sender); - }) - .AddTransient(); - } - - private bool GetBoolSetting(IConfiguration configuration, string settingName, bool defaultValue) - { - var checkParse = bool.TryParse(configuration[settingName], out bool value); - return checkParse ? value : defaultValue; - } - } -} diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.csproj b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.csproj index 56d0216ea..ff0bdf300 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.csproj +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.csproj @@ -4,24 +4,30 @@ v4 Hosts.$(MSBuildProjectName) Hosts.$(MSBuildProjectName.Replace(" ", "_")) + Exe + enabled <_FunctionsSkipCleanOutput>true - + - - - - + + + + + + + + - - - - + + + + @@ -32,4 +38,7 @@ Never - + + + + \ No newline at end of file diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln index fef84a671..154fb7251 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdater.sln @@ -45,6 +45,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Localization", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.RetryPolicyProvider", "..\..\..\Repositories.RetryPolicyProvider\Repositories.RetryPolicyProvider.csproj", "{FCF83599-33C3-4C6F-BCA4-0EAC2D7D0ABF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repositories.Mocks", "..\..\..\Repositories.Mocks\Repositories.Mocks.csproj", "{CA537690-35C2-48BC-864F-AA3CDB422D48}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -131,6 +133,10 @@ Global {FCF83599-33C3-4C6F-BCA4-0EAC2D7D0ABF}.Debug|Any CPU.Build.0 = Debug|Any CPU {FCF83599-33C3-4C6F-BCA4-0EAC2D7D0ABF}.Release|Any CPU.ActiveCfg = Release|Any CPU {FCF83599-33C3-4C6F-BCA4-0EAC2D7D0ABF}.Release|Any CPU.Build.0 = Release|Any CPU + {CA537690-35C2-48BC-864F-AA3CDB422D48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA537690-35C2-48BC-864F-AA3CDB422D48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA537690-35C2-48BC-864F-AA3CDB422D48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA537690-35C2-48BC-864F-AA3CDB422D48}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdaterSubOrchrestrator/TeamsChannelUpdaterSubOrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdaterSubOrchrestrator/TeamsChannelUpdaterSubOrchestratorFunction.cs index b917c1e65..836421985 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdaterSubOrchrestrator/TeamsChannelUpdaterSubOrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/TeamsChannelUpdaterSubOrchrestrator/TeamsChannelUpdaterSubOrchestratorFunction.cs @@ -1,7 +1,5 @@ // Copyright(c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; using System.Threading.Tasks; using System.Linq; using System.Collections.Generic; @@ -9,6 +7,8 @@ using Microsoft.ApplicationInsights; using Repositories.Contracts; using Models.Entities; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; namespace Hosts.TeamsChannelUpdater { @@ -22,8 +22,8 @@ public TeamsChannelUpdaterSubOrchestratorFunction(TelemetryClient telemetryClien _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); } - [FunctionName(nameof(TeamsChannelUpdaterSubOrchestratorFunction))] - public async Task RunSubOrchestratorAsync([OrchestrationTrigger] IDurableOrchestrationContext context) + [Function(nameof(TeamsChannelUpdaterSubOrchestratorFunction))] + public async Task RunSubOrchestratorAsync([OrchestrationTrigger] TaskOrchestrationContext context) { var skip = 0; var request = context.GetInput(); diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/host.json b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/host.json index b715b753b..4fd5d15be 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/host.json +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Function/host.json @@ -18,7 +18,8 @@ "applicationInsights": { "samplingExcludedTypes": "Request", "samplingSettings": { - "isEnabled": true + "isEnabled": true, + "excludedTypes": "Request" } } } diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep index 17181846a..f3f004a39 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep @@ -98,7 +98,7 @@ var commonSettings = { WEBSITE_ADD_SITENAME_BINDINGS_IN_APPHOST_CONFIG: 1 WEBSITE_ENABLE_SYNC_UPDATE_SITE: 1 SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 - FUNCTIONS_WORKER_RUNTIME: 'dotnet' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' FUNCTIONS_INPROC_NET8_ENABLED : 1 } diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/OrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/OrchestratorTests.cs index 802c0f2a9..db1193166 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/OrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/OrchestratorTests.cs @@ -6,7 +6,7 @@ using Moq; using Repositories.Contracts; using Services.TeamsChannelUpdater.Contracts; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; using Hosts.TeamsChannelUpdater; @@ -22,7 +22,7 @@ namespace Services.Tests [TestClass] public class OrchestratorTests { - private Mock _mockDurableOrchestrationContext = null!; + private Mock _mockDurableOrchestrationContext = null!; private Mock _mockExecutionContext = null!; private TelemetryClient _mockTelemetryClient = null!; private Mock _mockLoggingRepository = null!; @@ -59,38 +59,36 @@ public void SetUp() SyncJob = _syncJob }; - _mockDurableOrchestrationContext = new Mock(); + _mockDurableOrchestrationContext = new Mock(); _mockDurableOrchestrationContext.Setup(x => x.GetInput()) .Returns(input); - _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(JobReaderFunction), It.IsAny())) + _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(JobReaderFunction), It.IsAny(), null)) .ReturnsAsync(_syncJob); - _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(FileDownloaderFunction), It.IsAny())) + _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(FileDownloaderFunction), It.IsAny(), null)) .ReturnsAsync(_groupMembershipJson); - _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(LoggerFunction), It.IsAny())) - .Callback(async (name, request) => + _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(LoggerFunction), It.IsAny(), null)) + .Callback(async (name, request, options) => { await CallLoggerFunctionAsync(request as LoggerRequest); }); - _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny())) - .Callback(async (name, request) => + _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(JobStatusUpdaterFunction), It.IsAny(), null)) + .Callback(async (name, request, options) => { await CallJobStatusUpdaterFunctionAsync(request as JobStatusUpdaterRequest); }); string groupName = ""; - _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(GroupNameReaderFunction), It.IsAny())) - .Callback(async (name, request) => + _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(GroupNameReaderFunction), It.IsAny(), null)) + .Callback(async (name, request, options) => { groupName = await CallGroupNameReaderFunctionAsync(request as GroupNameReaderRequest); }) .ReturnsAsync(() => groupName); - _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(EmailSenderFunction), It.IsAny())) - .Callback(async (name, request) => + _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(EmailSenderFunction), It.IsAny(), null)) + .Callback(async (name, request, options) => { await CallEmailSenderFunctionAsync(request as EmailSenderRequest); }); - - _mockExecutionContext = new Mock(); _mockTelemetryClient = new TelemetryClient(new TelemetryConfiguration()); _mockLoggingRepository = new Mock(); _mockEmailSenderAndRecipients = new Mock(); @@ -108,7 +106,7 @@ public void SetUp() [TestMethod] public async Task TestOrchestratorOngoingSync() { - _mockDurableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(nameof(TeamsChannelUpdaterSubOrchestratorFunction), It.IsAny())) + _mockDurableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(nameof(TeamsChannelUpdaterSubOrchestratorFunction), It.IsAny(), null)) .ReturnsAsync(new TeamsChannelUpdaterSubOrchestratorResponse { Type = RequestType.Add, @@ -122,7 +120,7 @@ public async Task TestOrchestratorOngoingSync() _mockEmailSenderAndRecipients.Object, _mockGMMResources.Object); - await orchestratorFunction.RunOrchestratorAsync(_mockDurableOrchestrationContext.Object, _mockExecutionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_mockDurableOrchestrationContext.Object); _mockLoggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains("OrchestratorFunction function started")), @@ -147,7 +145,7 @@ public async Task TestOrchestratorOngoingSync() public async Task TestOrchestratorInitialSync() { _syncJob.LastRunTime = DateTime.FromFileTimeUtc(0); - _mockDurableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(nameof(TeamsChannelUpdaterSubOrchestratorFunction), It.IsAny())) + _mockDurableOrchestrationContext.Setup(x => x.CallSubOrchestratorAsync(nameof(TeamsChannelUpdaterSubOrchestratorFunction), It.IsAny(), null)) .ReturnsAsync(new TeamsChannelUpdaterSubOrchestratorResponse { Type = RequestType.Add, @@ -161,7 +159,7 @@ public async Task TestOrchestratorInitialSync() _mockEmailSenderAndRecipients.Object, _mockGMMResources.Object); - await orchestratorFunction.RunOrchestratorAsync(_mockDurableOrchestrationContext.Object, _mockExecutionContext.Object); + await orchestratorFunction.RunOrchestratorAsync(_mockDurableOrchestrationContext.Object); _mockLoggingRepository.Verify(x => x.LogMessageAsync( It.Is(m => m.Message.Contains("OrchestratorFunction function started")), @@ -184,7 +182,7 @@ public async Task TestOrchestratorInitialSync() [TestMethod] public async Task TestOrchestratorException() { - _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(FileDownloaderFunction), It.IsAny())) + _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(FileDownloaderFunction), It.IsAny(), null)) .ThrowsAsync(new FileNotFoundException()); var orchestratorFunction = new OrchestratorFunction(_mockLoggingRepository.Object, @@ -192,7 +190,7 @@ public async Task TestOrchestratorException() _mockEmailSenderAndRecipients.Object, _mockGMMResources.Object); - await Assert.ThrowsExceptionAsync(async () => await orchestratorFunction.RunOrchestratorAsync(_mockDurableOrchestrationContext.Object, _mockExecutionContext.Object)); + await Assert.ThrowsExceptionAsync(async () => await orchestratorFunction.RunOrchestratorAsync(_mockDurableOrchestrationContext.Object)); _mockLoggingRepository.Verify(x => x.LogMessageAsync( diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/QueueMessageOrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/QueueMessageOrchestratorTests.cs index eb974928c..6fdda77c2 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/QueueMessageOrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/QueueMessageOrchestratorTests.cs @@ -2,7 +2,8 @@ // Licensed under the MIT license. using Azure.Messaging.ServiceBus; using Hosts.TeamsChannelUpdater; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Moq; @@ -17,7 +18,7 @@ public class QueueMessageOrchestratorTests { private MembershipHttpRequest _request; private Mock _loggerMock; - private Mock _context; + private Mock _context; private Mock _serviceBusReceiverMock; [TestInitialize] @@ -35,11 +36,11 @@ public void SetupTest() }; _loggerMock = new Mock(); - _context = new Mock(); + _context = new Mock(); _serviceBusReceiverMock = new Mock(); MembershipHttpRequest response = null; - _context.Setup(x => x.CallActivityAsync(nameof(MessageReaderFunction), (object)null)) + _context.Setup(x => x.CallActivityAsync(nameof(MessageReaderFunction), (object)null, null)) .Callback(async () => response = await CallMessageReaderFunctionAsync()) .ReturnsAsync(() => response); @@ -60,12 +61,12 @@ public async Task RunOrchestratorWithMessagesInQueueAsync() await orchestrator.RunOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == "There are no more messages to process at this time.")), Times.Never()); + It.Is(r => r.Message == "There are no more messages to process at this time."), null), Times.Never()); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == $"Processing message for group {_request.SyncJob.TargetOfficeGroupId}")), Times.Once()); + It.Is(r => r.Message == $"Processing message for group {_request.SyncJob.TargetOfficeGroupId}"), null), Times.Once()); - _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny()), Times.Once()); + _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(), null), Times.Once()); _context.Verify(x => x.ContinueAsNew((object)null, false), Times.Once()); } @@ -79,30 +80,50 @@ public async Task RunOrchestratorWithNoMessagesInQueueAsync() await orchestrator.RunOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == "There are no more messages to process at this time.")), Times.Once()); + It.Is(r => r.Message == "There are no more messages to process at this time."), null), Times.Once()); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message.StartsWith("Processing message for group"))), Times.Never()); + It.Is(r => r.Message == "Processing message for group"), null), Times.Never()); - _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny()), Times.Never()); + _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), + It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); + _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(), null), Times.Never()); } [TestMethod] public async Task MainOrchestratorFailsAsync() { - _context.Setup(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny())) + _context.Setup(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(), null)) .Throws(new Exception("Main orchestrator failed.")); var orchestrator = new QueueMessageOrchestratorFunction(_loggerMock.Object); await orchestrator.RunOrchestratorAsync(_context.Object); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == "There are no more messages to process at this time.")), Times.Never()); + It.Is(r => r.Message == "There are no more messages to process at this time."), null), Times.Never()); + + _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), + It.Is(r => r.Message == $"Processing message for group {_request.SyncJob.TargetOfficeGroupId}"), null), Times.Once()); _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == $"Processing message for group {_request.SyncJob.TargetOfficeGroupId}")), Times.Once()); + It.Is(r => r.Message == "There are no more messages to process at this time."), null), Times.Once()); - _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny()), Times.Once()); + _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), + It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); + _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), + It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); + _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), + It.Is(r => r.Message == "There are no more messages to process at this time."), null), Times.Once()); + + _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), + It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); + _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), + It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); + _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), + It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); + _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), + It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); + _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(), null), Times.Once()); } private async Task CallMessageReaderFunctionAsync() diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/Services.Tests.csproj b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/Services.Tests.csproj index 1fdf68b08..b908f4587 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/Services.Tests.csproj +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/Services.Tests.csproj @@ -25,6 +25,7 @@ + diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/StarterFunctionTests.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/StarterFunctionTests.cs index 24f37d47b..8a28e557d 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/StarterFunctionTests.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/StarterFunctionTests.cs @@ -2,12 +2,13 @@ // Licensed under the MIT license. using Azure.Messaging.ServiceBus; using Hosts.TeamsChannelUpdater; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; using Microsoft.VisualStudio.TestTools.UnitTesting; using Models; using Moq; using Repositories.Contracts; +using Repositories.Mocks; namespace Services.Tests { @@ -16,7 +17,7 @@ public class StarterFunctionTests { private string _instanceId; private Mock _loggerMock; - private Mock _durableClientMock; + private Mock _durableClientMock; private SyncJob _syncJob; private Mock _serviceBusReceiverMock; @@ -24,7 +25,7 @@ public class StarterFunctionTests public void SetupTest() { _instanceId = "1234567890"; - _durableClientMock = new Mock(); + _durableClientMock = new Mock(); _loggerMock = new Mock(); _serviceBusReceiverMock = new Mock(); _syncJob = new SyncJob @@ -44,12 +45,12 @@ public void SetupTest() public async Task ProcessValidRequestTest() { _durableClientMock - .Setup(x => x.StartNewAsync(It.IsAny(), It.IsAny(), (object)null)) + .Setup(x => x.ScheduleNewOrchestrationInstanceAsync(It.IsAny(), It.IsAny(), null, It.IsAny())) .ReturnsAsync(_instanceId); var instanceId = nameof(QueueMessageOrchestratorFunction); var starterFunction = new StarterFunction(_loggerMock.Object, _serviceBusReceiverMock.Object); - var timer = new TimerInfo(null, null); + var timer = new TimerInfo(); await starterFunction.RunAsync(timer, _durableClientMock.Object); @@ -60,7 +61,7 @@ public async Task ProcessValidRequestTest() It.IsAny() ), Times.Once()); - _durableClientMock.Verify(x => x.StartNewAsync(instanceId, instanceId, (object)null), Times.Once()); + _durableClientMock.Verify(x => x.ScheduleNewOrchestrationInstanceAsync(instanceId, instanceId, null, It.IsAny()), Times.Once()); _loggerMock.Verify(x => x.LogMessageAsync( It.Is(m => m.Message == $"Calling {instanceId}"), diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/TeamsChannelUpdaterSubOrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/TeamsChannelUpdaterSubOrchestratorTests.cs index 1b5f2b1a4..34dd79412 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/TeamsChannelUpdaterSubOrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/TeamsChannelUpdaterSubOrchestratorTests.cs @@ -6,7 +6,7 @@ using Moq; using Repositories.Contracts; using Services.TeamsChannelUpdater.Contracts; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; using Hosts.TeamsChannelUpdater; @@ -19,7 +19,7 @@ namespace Services.Tests [TestClass] public class TeamsChannelUpdaterSubOrchestratorTests { - private Mock _mockDurableOrchestrationContext = null!; + private Mock _mockDurableOrchestrationContext = null!; private Mock _mockExecutionContext = null!; private TelemetryClient _mockTelemetryClient = null!; private Mock _mockLoggingRepository = null!; @@ -62,17 +62,17 @@ public void SetUp() TeamsChannelInfo = _teamsChannelInfo }; - _mockDurableOrchestrationContext = new Mock(); + _mockDurableOrchestrationContext = new Mock(); _mockDurableOrchestrationContext.Setup(x => x.GetInput()) .Returns(_input); - _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(LoggerFunction), It.IsAny())) - .Callback(async (name, request) => + _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(LoggerFunction), It.IsAny(), null)) + .Callback(async (name, request, options) => { await CallLoggerFunctionAsync(request as LoggerRequest); }); TeamsUpdaterResponse response = new TeamsUpdaterResponse(); - _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(TeamsUpdaterFunction), It.IsAny())) - .Callback(async (name, request) => + _mockDurableOrchestrationContext.Setup(x => x.CallActivityAsync(nameof(TeamsUpdaterFunction), It.IsAny(), null)) + .Callback(async (name, request, options) => { response = await CallTeamsUpdaterFunctionAsync(request as TeamsUpdaterRequest); }) @@ -143,7 +143,7 @@ public async Task TestAddUsersTransientException() It.IsAny() ), Times.Once); - _mockDurableOrchestrationContext.Verify(x => x.CallActivityAsync(nameof(TeamsUpdaterFunction), It.IsAny()), + _mockDurableOrchestrationContext.Verify(x => x.CallActivityAsync(nameof(TeamsUpdaterFunction), It.IsAny(), null), Times.Exactly(2)); Assert.AreEqual(response.SuccessCount, 1); From 95422804c0a2a270b425c0164e6624f4eb462959 Mon Sep 17 00:00:00 2001 From: Abril Gonzalez Date: Mon, 30 Sep 2024 09:57:02 -0700 Subject: [PATCH 0344/1479] Update tests --- .../QueueMessageOrchestratorTests.cs | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/QueueMessageOrchestratorTests.cs b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/QueueMessageOrchestratorTests.cs index 6fdda77c2..4d84aca95 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/QueueMessageOrchestratorTests.cs +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Services.Tests/QueueMessageOrchestratorTests.cs @@ -40,7 +40,7 @@ public void SetupTest() _serviceBusReceiverMock = new Mock(); MembershipHttpRequest response = null; - _context.Setup(x => x.CallActivityAsync(nameof(MessageReaderFunction), (object)null, null)) + _context.Setup(x => x.CallActivityAsync(nameof(MessageReaderFunction),null)) .Callback(async () => response = await CallMessageReaderFunctionAsync()) .ReturnsAsync(() => response); @@ -68,7 +68,7 @@ public async Task RunOrchestratorWithMessagesInQueueAsync() _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(), null), Times.Once()); - _context.Verify(x => x.ContinueAsNew((object)null, false), Times.Once()); + _context.Verify(x => x.ContinueAsNew((object)null, true), Times.Once()); } [TestMethod] @@ -82,11 +82,9 @@ public async Task RunOrchestratorWithNoMessagesInQueueAsync() _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), It.Is(r => r.Message == "There are no more messages to process at this time."), null), Times.Once()); - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == "Processing message for group"), null), Times.Never()); - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); + _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(), null), Times.Never()); } @@ -105,24 +103,6 @@ public async Task MainOrchestratorFailsAsync() _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), It.Is(r => r.Message == $"Processing message for group {_request.SyncJob.TargetOfficeGroupId}"), null), Times.Once()); - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == "There are no more messages to process at this time."), null), Times.Once()); - - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message == "There are no more messages to process at this time."), null), Times.Once()); - - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); - _context.Verify(x => x.CallActivityAsync(nameof(LoggerFunction), - It.Is(r => r.Message.StartsWith("Processing message for group")), null), Times.Never()); _context.Verify(x => x.CallSubOrchestratorAsync(nameof(OrchestratorFunction), It.IsAny(), null), Times.Once()); } From 3895527bd7d66fda3a011f3b8af4fd58622d80b2 Mon Sep 17 00:00:00 2001 From: Abril Gonzalez Date: Tue, 1 Oct 2024 11:03:40 -0700 Subject: [PATCH 0345/1479] Remove FUNCTIONS_INPROC_NET8_ENABLED setting --- .../TeamsChannelUpdater/Infrastructure/compute/template.bicep | 1 - 1 file changed, 1 deletion(-) diff --git a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep index f3f004a39..c068361d2 100644 --- a/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep +++ b/Service/GroupMembershipManagement/Hosts/TeamsChannelUpdater/Infrastructure/compute/template.bicep @@ -100,7 +100,6 @@ var commonSettings = { SCM_TOUCH_WEBCONFIG_AFTER_DEPLOYMENT: 0 FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_INPROC_NET8_ENABLED : 1 } var appSettings = { From c326fab556ae1d515c8498d3e9c1ce3a57bf26ce Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 7 Oct 2024 11:07:52 -0700 Subject: [PATCH 0346/1479] support in clause (without grouping) --- .../HRQuerySource/HRQuerySource.base.tsx | 98 +++++++++++++------ .../HRQuerySource/QuerySerializer.ts | 4 +- 2 files changed, 70 insertions(+), 32 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 7956d30ee..9b794fb6d 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -78,6 +78,7 @@ export const HRQuerySourceBase: React.FunctionComponent = (p const [filterTextEnabled, setFilterTextEnabled] = useState(false); const [expanded, setExpanded] = useState(true); const [orgLeaderUpdated, setOrgLeaderUpdated] = useState(false); + const [selectedKeys, setSelectedKeys] = React.useState([]); useEffect(() => { if (!groupingEnabled) { @@ -222,32 +223,37 @@ const checkType = (value: string, type: string | undefined): string => { } } - if (props.source.filter && !groupingEnabled && (props.source.filter.includes("(") || props.source.filter.includes(")"))) { - const groups = parseGroup(props.source.filter); - if (groups.length <= 0) { - setFilterTextEnabled(true); - return; + if (props.source.filter && !groupingEnabled) { + setSelectedKeys([]); + const hasParentheses = props.source.filter.includes("(") || props.source.filter.includes(")"); + const hasInClause = props.source.filter.includes(" IN "); + if (hasParentheses && !hasInClause) { + const groups = parseGroup(props.source.filter); + if (groups.length <= 0) { + setFilterTextEnabled(true); + return; + } + const b = setItemsBasedOnGroups(groups); + setGroups(groups); + setGroupingEnabled(true); } - const b = setItemsBasedOnGroups(groups); - setGroups(groups); - setGroupingEnabled(true); - } - else { - const regex = /( And | Or )/gi; - if (props.source.filter != undefined) { - const parts = props.source.filter.split(regex); - let childFilters = []; - let currentFilter = ""; - for (let i = 0; i < parts.length; i += 2) { - currentFilter = parts[i].trim(); - if (i + 1 < parts.length) { - currentFilter += parts[i + 1]; + else { + const regex = /( And | Or )/gi; + if (props.source.filter != undefined) { + const parts = props.source.filter.split(regex); + let childFilters = []; + let currentFilter = ""; + for (let i = 0; i < parts.length; i += 2) { + currentFilter = parts[i].trim(); + if (i + 1 < parts.length) { + currentFilter += parts[i + 1]; + } + childFilters.push(currentFilter); } - childFilters.push(currentFilter); + setChildren(childFilters.map(filter => ({ + filter + }))); } - setChildren(childFilters.map(filter => ({ - filter - }))); } } }, [props.source.filter]); @@ -290,6 +296,15 @@ const checkType = (value: string, type: string | undefined): string => { } }, [orgLeaderDetails]); + const getSelectedKeys = (input: string): string[] => { + const matches = input.match(/'([^']+)'|([^(),\s]+)/g); + if (matches) { + const keys = matches.map(match => match.replace(/'/g, '')); + return keys; + } + return []; + }; + const getPickerSuggestions = async ( filterText: string ): Promise => { @@ -643,7 +658,8 @@ const checkType = (value: string, type: string | undefined): string => { { key: '>', text: '>'}, { key: '>=', text: '>=' }, { key: '<>', text: '<>' }, - { key: 'IS', text: 'IS' } + { key: 'IS', text: 'IS' }, + { key: 'IN', text: 'IN' } ]; interface UpdateParam { @@ -812,10 +828,31 @@ const checkType = (value: string, type: string | undefined): string => { } }; - const handleAttributeValueChange = (attribute: string, event: React.FormEvent, item?: IComboBoxOption, index?: number, operator?: string): void => { + const handleAttributeValueChange = (attribute: string, event: React.FormEvent, existingValues?: string, item?: IComboBoxOption, index?: number, operator?: string): void => { + let selectedValues = ""; + if (operator && operator.toString().toUpperCase() === "IN") { + let selected = item?.selected; + if (item) { + setSelectedKeys(prevSelectedKeys => { + const isNVarChar = attribute && attributeMappings[attribute] && attributeMappings[attribute.toString()].type === "nvarchar"; + if (prevSelectedKeys.length === 0 && existingValues && existingValues.length > 0) { + prevSelectedKeys = getSelectedKeys(existingValues); + } + const newSelectedKeys = selected + ? [...prevSelectedKeys, item!.key as string] + : prevSelectedKeys.filter(k => k !== item!.key); + const quotedKeys = isNVarChar + ? newSelectedKeys.map(key => `'${key}'`) + : newSelectedKeys; + selectedValues = `(${quotedKeys.join(', ')})`; + return newSelectedKeys; + }); + } + } + if (item) { - const selectedValue = item.key.toString(); - const selectedValueAfterConversion = operator && operator.toString().toUpperCase() === "IS" ? selectedValue : (attributeMappings[attribute] ? checkType(selectedValue, attributeMappings[attribute.toString()].type) : selectedValue); + const selectedValue = operator && operator.toString().toUpperCase() === "IN" ? selectedValues : item.key.toString(); + const selectedValueAfterConversion = operator && (operator.toString().toUpperCase() === "IS" || operator.toString().toUpperCase() === "IN") ? selectedValue : (attributeMappings[attribute] ? checkType(selectedValue, attributeMappings[attribute.toString()].type) : selectedValue); const updatedItems = items.map((it, idx) => { if (idx === index) { return { ...it, value: selectedValueAfterConversion || selectedValue }; @@ -1234,7 +1271,7 @@ const checkType = (value: string, type: string | undefined): string => { handleAttributeValueChange(item.attribute, event, option, index, item.equalityOperator)} + onChange={(event, option) => handleAttributeValueChange(item.attribute, event, items[index].value, option, index, item.equalityOperator)} allowFreeInput autoComplete="off" dropdownMaxWidth={500} @@ -1244,13 +1281,14 @@ const checkType = (value: string, type: string | undefined): string => { else { if (attributeMappings && attributeMappings[items[index].attribute] && attributeMappings[items[index].attribute].mappings.length > 0) { return onAttributeValueChange(text, index)} - onChange={(event, option) => handleAttributeValueChange(item.attribute, event, option, index)} + onChange={(event, option) => handleAttributeValueChange(item.attribute, event, items[index].value, option, index, item.equalityOperator)} onRenderOption={onRenderValueComboBoxOptions} onRenderList={onRenderValueComboBoxList} allowFreeInput + multiSelect={item.equalityOperator === 'IN' ? true : false} autoComplete="off" useComboBoxAsMenuWidth={false} dropdownMaxWidth={500} diff --git a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts index b2cb516c4..1986f0d5c 100644 --- a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts +++ b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts @@ -2,7 +2,7 @@ import { Group } from "../../models/Group"; import { IFilterPart } from "../../models/IFilterPart"; export function containsSqlExpression(filter: string): boolean { - const sqlExpressions = [' IN ', ' NOT IN ', ' BETWEEN ', ' LIKE ', ' NOT LIKE ']; + const sqlExpressions = [' NOT IN ', ' BETWEEN ', ' LIKE ', ' NOT LIKE ']; const regex = new RegExp(`\\b(${sqlExpressions.join('|')})\\b`, 'i'); return regex.test(filter); }; @@ -58,7 +58,7 @@ export function stringifyGroups(groups: Group[]): string { } function parseFilterPart(part: string): IFilterPart { - const operators = ["<=", ">=", "<>", "=", ">", "<", "IS"]; + const operators = ["<=", ">=", "<>", "=", ">", "<", "IS", "IN"]; let operatorFound = ''; let operatorIndex = -1; From 43069b1aefa0c7e1a91a1c266e42070076890b03 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Thu, 3 Oct 2024 11:55:16 -0700 Subject: [PATCH 0347/1479] code cleanup --- .../HRQuerySource/HRQuerySource.base.tsx | 28 +--------------- UI/web-app/src/models/Options.ts | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 UI/web-app/src/models/Options.ts diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 9b794fb6d..6897ef9db 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -27,6 +27,7 @@ import { SqlMembershipAttribute, SqlMembershipAttributeMapping } from '../../mod import { IFilterPart } from '../../models/IFilterPart'; import { Group } from '../../models/Group'; import { containsSqlExpression, parseGroup, stringifyGroups } from './QuerySerializer'; +import { equalityOperatorOptions, nullOptions, orAndOperatorOptions, yesNoOptions } from '../../models/Options'; export const getClassNames = classNamesFunction(); @@ -552,22 +553,6 @@ const checkType = (value: string, type: string | undefined): string => { } }; - const nullOptions: IComboBoxOption[] = [ - { key: 'NULL', text: 'NULL' }, - { key: 'NOT NULL', text: 'NOT NULL' } - ]; - - const yesNoOptions: IChoiceGroupOption[] = [ - { key: 'Yes', text: strings.yes }, - { key: 'No', text: strings.no } - ]; - - const orAndOperatorOptions: IDropdownOption[] = [ - { key: '', text: '' }, - { key: 'Or', text: strings.or }, - { key: 'And', text: strings.and } - ]; - const handleIncludeOrgChange = (ev?: React.FormEvent, option?: IChoiceGroupOption) => { if (option?.key === "No") { setIncludeOrg(false); @@ -651,17 +636,6 @@ const checkType = (value: string, type: string | undefined): string => { }); } - const equalityOperatorOptions: IDropdownOption[] = [ - { key: '=', text: '=' }, - { key: '<', text: '<' }, - { key: '<=', text: '<=' }, - { key: '>', text: '>'}, - { key: '>=', text: '>=' }, - { key: '<>', text: '<>' }, - { key: 'IS', text: 'IS' }, - { key: 'IN', text: 'IN' } - ]; - interface UpdateParam { property: "attribute" | "value" | "andOr" | "equalityOperator"; newValue: string; diff --git a/UI/web-app/src/models/Options.ts b/UI/web-app/src/models/Options.ts new file mode 100644 index 000000000..37b412f5d --- /dev/null +++ b/UI/web-app/src/models/Options.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { IChoiceGroupOption, IComboBoxOption, IDropdownOption } from "@fluentui/react"; +import { strings } from "../services/localization/i18n/locales/en/translations"; + +export const nullOptions: IComboBoxOption[] = [ + { key: 'NULL', text: 'NULL' }, + { key: 'NOT NULL', text: 'NOT NULL' } +]; + +export const yesNoOptions: IChoiceGroupOption[] = [ + { key: 'Yes', text: strings.yes }, + { key: 'No', text: strings.no } +]; + +export const orAndOperatorOptions: IDropdownOption[] = [ + { key: '', text: '' }, + { key: 'Or', text: strings.or }, + { key: 'And', text: strings.and } +]; + +export const equalityOperatorOptions: IDropdownOption[] = [ + { key: '=', text: '=' }, + { key: '<', text: '<' }, + { key: '<=', text: '<=' }, + { key: '>', text: '>'}, + { key: '>=', text: '>=' }, + { key: '<>', text: '<>' }, + { key: 'IS', text: 'IS' }, + { key: 'IN', text: 'IN' } +]; \ No newline at end of file From d506a6fab181477ec2b7aa6fb954c0cbfcfef25a Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 7 Oct 2024 13:13:48 -0700 Subject: [PATCH 0348/1479] consider edge cases - grouping and IN clause --- .../HRQuerySource/HRQuerySource.base.tsx | 32 +++++++++++++++++-- .../HRQuerySource/QuerySerializer.ts | 10 ++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 6897ef9db..86806a8bd 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -26,7 +26,7 @@ import { selectIsJobWriter } from '../../store/roles.slice'; import { SqlMembershipAttribute, SqlMembershipAttributeMapping } from '../../models'; import { IFilterPart } from '../../models/IFilterPart'; import { Group } from '../../models/Group'; -import { containsSqlExpression, parseGroup, stringifyGroups } from './QuerySerializer'; +import { containsSqlExpression, countOccurrences, parseGroup, stringifyGroups } from './QuerySerializer'; import { equalityOperatorOptions, nullOptions, orAndOperatorOptions, yesNoOptions } from '../../models/Options'; export const getClassNames = classNamesFunction(); @@ -225,10 +225,32 @@ const checkType = (value: string, type: string | undefined): string => { } if (props.source.filter && !groupingEnabled) { + + let parseFilter = false; + let numberOfOpenParenthesis = countOccurrences(props.source.filter, "("); + var numberOfCloseParenthesis = countOccurrences(props.source.filter, ")"); + var numberOfInClause = countOccurrences(props.source.filter, " IN "); + setSelectedKeys([]); const hasParentheses = props.source.filter.includes("(") || props.source.filter.includes(")"); const hasInClause = props.source.filter.includes(" IN "); - if (hasParentheses && !hasInClause) { + if (hasParentheses && hasInClause) { + if (numberOfOpenParenthesis > numberOfInClause && numberOfCloseParenthesis > numberOfInClause) { + // grouping + IN clause + console.log("grouping + IN clause"); + setFilterTextEnabled(true); + return; + } + + else if (numberOfOpenParenthesis === numberOfInClause && numberOfCloseParenthesis === numberOfInClause) { + // no grouping, only IN clause + console.log("no grouping, only IN clause"); + parseFilter = true; + } + } + else if (hasParentheses && !hasInClause) { + // only grouping, no IN clause + console.log("only grouping, no IN clause"); const groups = parseGroup(props.source.filter); if (groups.length <= 0) { setFilterTextEnabled(true); @@ -239,6 +261,12 @@ const checkType = (value: string, type: string | undefined): string => { setGroupingEnabled(true); } else { + // no grouping, no IN clause + console.log("no grouping, no IN clause"); + parseFilter = true; + } + + if (parseFilter) { const regex = /( And | Or )/gi; if (props.source.filter != undefined) { const parts = props.source.filter.split(regex); diff --git a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts index 1986f0d5c..ed3dc8709 100644 --- a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts +++ b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts @@ -7,6 +7,16 @@ export function containsSqlExpression(filter: string): boolean { return regex.test(filter); }; +export function countOccurrences(str: string, subStr: string): number { + let count = 0; + let pos = str.indexOf(subStr); + while (pos !== -1) { + count++; + pos = str.indexOf(subStr, pos + subStr.length); + } + return count; +} + export function stringifyGroup(group: Group, isChild?: boolean, childIndex?: number, childrenLength?: number): string { let result = '('; From cb52d5278b387b84b8e4f76fe01dcc9baa0c4bec Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 7 Oct 2024 15:48:56 -0700 Subject: [PATCH 0349/1479] support in clause (with grouping) --- .../HRQuerySource/HRQuerySource.base.tsx | 41 ++++++++++--------- .../HRQuerySource/QuerySerializer.ts | 15 ++++++- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 86806a8bd..0290cb256 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -225,8 +225,8 @@ const checkType = (value: string, type: string | undefined): string => { } if (props.source.filter && !groupingEnabled) { - - let parseFilter = false; + let isParsingFilter = false; + let isParsingGroup = false; let numberOfOpenParenthesis = countOccurrences(props.source.filter, "("); var numberOfCloseParenthesis = countOccurrences(props.source.filter, ")"); var numberOfInClause = countOccurrences(props.source.filter, " IN "); @@ -236,22 +236,22 @@ const checkType = (value: string, type: string | undefined): string => { const hasInClause = props.source.filter.includes(" IN "); if (hasParentheses && hasInClause) { if (numberOfOpenParenthesis > numberOfInClause && numberOfCloseParenthesis > numberOfInClause) { - // grouping + IN clause - console.log("grouping + IN clause"); - setFilterTextEnabled(true); - return; + isParsingGroup = true; // grouping, IN clause } else if (numberOfOpenParenthesis === numberOfInClause && numberOfCloseParenthesis === numberOfInClause) { - // no grouping, only IN clause - console.log("no grouping, only IN clause"); - parseFilter = true; + isParsingFilter = true; // no grouping, only IN clause } } else if (hasParentheses && !hasInClause) { - // only grouping, no IN clause - console.log("only grouping, no IN clause"); - const groups = parseGroup(props.source.filter); + isParsingGroup = true; // only grouping, no IN clause + } + else { + isParsingFilter = true; // no grouping, no IN clause + } + + if (isParsingGroup) { + const groups = parseGroup(props.source.filter, hasInClause); if (groups.length <= 0) { setFilterTextEnabled(true); return; @@ -260,13 +260,8 @@ const checkType = (value: string, type: string | undefined): string => { setGroups(groups); setGroupingEnabled(true); } - else { - // no grouping, no IN clause - console.log("no grouping, no IN clause"); - parseFilter = true; - } - if (parseFilter) { + if (isParsingFilter) { const regex = /( And | Or )/gi; if (props.source.filter != undefined) { const parts = props.source.filter.split(regex); @@ -326,9 +321,15 @@ const checkType = (value: string, type: string | undefined): string => { }, [orgLeaderDetails]); const getSelectedKeys = (input: string): string[] => { - const matches = input.match(/'([^']+)'|([^(),\s]+)/g); + const matches = input.match(/'([^']+)'|([^(),\s\[\]]+)|\[(.+?)\]|\((.+?)\)/g); if (matches) { - const keys = matches.map(match => match.replace(/'/g, '')); + const keys = matches.flatMap(match => { + const cleaned = match.replace(/'/g, '').trim(); + if (cleaned.startsWith('[') || cleaned.startsWith('(')) { + return cleaned.slice(1, -1).split(',').map(v => v.trim()); + } + return [cleaned]; + }); return keys; } return []; diff --git a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts index ed3dc8709..e8f98edd0 100644 --- a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts +++ b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts @@ -51,9 +51,21 @@ export function stringifyGroup(group: Group, isChild?: boolean, childIndex?: num result += ` ${group.children[group.children.length-1].andOr} `; } + result = result.includes(" IN ") ? replaceBracketsWithParentheses(result) : result; return result; } +function replaceBracketsWithParentheses(input: string): string { + return input.replace(/\[/g, '(').replace(/\]/g, ')'); +}; + +function replaceInClause(input: string): string { + const regex = /IN\s*\(\s*('([^']+)')(?:,\s*('([^']+)'))*\s*\)/g; + return input.replace(regex, (match) => { + return match.replace('(', '[').replace(')', ']'); + }); +}; + export function stringifyGroups(groups: Group[]): string { let result = ''; @@ -178,7 +190,8 @@ function appendAndOr(allParts: { currentSegment: string; start: number; end: num return allParts; } -export function parseGroup(input: string): Group[] { +export function parseGroup(input: string, hasInClause: boolean): Group[] { + input = hasInClause ? replaceInClause(input) : input; const groups: Group[] = []; let subStrings: { currentSegment: string, start: number; end: number}[] = []; let depth = 0; From 7b526ffc425394b220c9d63cfbf65c6475939741 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Mon, 7 Oct 2024 16:37:07 -0700 Subject: [PATCH 0350/1479] not supporting 'not in' clause for now --- UI/web-app/src/components/HRQuerySource/QuerySerializer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts index e8f98edd0..86e8b7ad1 100644 --- a/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts +++ b/UI/web-app/src/components/HRQuerySource/QuerySerializer.ts @@ -3,7 +3,7 @@ import { IFilterPart } from "../../models/IFilterPart"; export function containsSqlExpression(filter: string): boolean { const sqlExpressions = [' NOT IN ', ' BETWEEN ', ' LIKE ', ' NOT LIKE ']; - const regex = new RegExp(`\\b(${sqlExpressions.join('|')})\\b`, 'i'); + const regex = new RegExp(`(${sqlExpressions.join('|').trim()})`, 'i'); return regex.test(filter); }; From 07bcc6a63ee33b7ade7e65108fa2e0acee4d8923 Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Tue, 8 Oct 2024 12:54:52 -0700 Subject: [PATCH 0351/1479] display selected values on top --- .../HRQuerySource/HRQuerySource.base.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx index 0290cb256..f665ec0d2 100644 --- a/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx +++ b/UI/web-app/src/components/HRQuerySource/HRQuerySource.base.tsx @@ -207,13 +207,15 @@ const checkType = (value: string, type: string | undefined): string => { return options; }; - const getValueOptions = (attributeMappings?: SqlMembershipAttributeMapping[]): IComboBoxOption[] => { - let valueOptions = attributeMappings?.map((attributeMapping, index) => ({ + const getValueOptions = (attributeMappings?: SqlMembershipAttributeMapping[], selectedKeys?: string[]): IComboBoxOption[] => { + const valueOptions = attributeMappings?.map(attributeMapping => ({ key: attributeMapping.code, text: attributeMapping.description ? attributeMapping.description : attributeMapping.code })) || []; - valueOptions.sort((a, b) => a.text.localeCompare(b.text)); - return valueOptions; + const selectedOptions = valueOptions.filter(option => selectedKeys?.includes(option.key)); + const unselectedOptions = valueOptions.filter(option => !selectedKeys?.includes(option.key)); + unselectedOptions.sort((a, b) => a.text.localeCompare(b.text)); + return [...selectedOptions, ...unselectedOptions]; }; useEffect(() => { @@ -1285,12 +1287,12 @@ const checkType = (value: string, type: string | undefined): string => { if (attributeMappings && attributeMappings[items[index].attribute] && attributeMappings[items[index].attribute].mappings.length > 0) { return onAttributeValueChange(text, index)} onChange={(event, option) => handleAttributeValueChange(item.attribute, event, items[index].value, option, index, item.equalityOperator)} onRenderOption={onRenderValueComboBoxOptions} onRenderList={onRenderValueComboBoxList} - allowFreeInput + allowFreeInput={item.equalityOperator === 'IN' ? false : true} multiSelect={item.equalityOperator === 'IN' ? true : false} autoComplete="off" useComboBoxAsMenuWidth={false} From 902604b128c740ed0e72e127ad1f192155fb55a5 Mon Sep 17 00:00:00 2001 From: Daniel Luo Date: Wed, 2 Oct 2024 18:35:51 -0700 Subject: [PATCH 0352/1479] Updated UI to validate Sql query during the onValidateQuery dispatch for Advanced view --- .../ISqlMembershipSourcesApi.ts | 2 + .../SqlMembershipSourcesApi.ts | 7 +++ .../AdvancedQuery/AdvancedQuery.base.tsx | 54 ++++++++++++++++--- .../src/models/ValidateSqlFiltersResponse.ts | 7 +++ .../src/services/localization/IStrings.ts | 1 + .../i18n/locales/en/translations.ts | 1 + .../i18n/locales/es/translations.ts | 1 + .../src/store/sqlMembershipSources.api.tsx | 24 +++++++-- 8 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 UI/web-app/src/models/ValidateSqlFiltersResponse.ts diff --git a/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts b/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts index a878040f0..0fa9caf3a 100644 --- a/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts +++ b/UI/web-app/src/apis/sqlMembershipSources/ISqlMembershipSourcesApi.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { SqlMembershipAttribute, SqlMembershipSource, SqlMembershipAttributeMapping } from '../../models'; +import { ValidateSqlFiltersResponse } from '../../models/ValidateSqlFiltersResponse'; export interface ISqlMembershipSourcesApi { fetchDefaultSqlMembershipSource(): Promise; @@ -10,4 +11,5 @@ export interface ISqlMembershipSourcesApi { fetchDefaultSqlMembershipSourceAttributeValues(attribute: SqlMembershipAttribute): Promise; patchDefaultSqlMembershipSourceCustomLabel(customLabel: string): Promise; patchDefaultSqlMembershipSourceAttributes(attributes: SqlMembershipAttribute[]): Promise; + validateSqlFilters(filters: Map): Promise; } diff --git a/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts b/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts index 700c308d9..3f6f400d7 100644 --- a/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts +++ b/UI/web-app/src/apis/sqlMembershipSources/SqlMembershipSourcesApi.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { SqlMembershipAttribute, SqlMembershipAttributeMapping, SqlMembershipSource } from '../../models'; +import { ValidateSqlFiltersResponse } from '../../models/ValidateSqlFiltersResponse'; import { ApiBase } from '../ApiBase'; import { ISqlMembershipSourcesApi } from './ISqlMembershipSourcesApi'; @@ -46,4 +47,10 @@ export class SqlMembershipSourcesApi extends ApiBase implements ISqlMembershipSo this.ensureSuccessStatusCode(response); } + public async validateSqlFilters(filters: Map): Promise { + const response = await this.httpClient.post('/validateFilters', Object.fromEntries(filters)); + this.ensureSuccessStatusCode(response); + return response.data; + } + } diff --git a/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx b/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx index 35c06bb46..80b9c19ce 100644 --- a/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx +++ b/UI/web-app/src/components/AdvancedQuery/AdvancedQuery.base.tsx @@ -28,6 +28,7 @@ import { SourcePartType } from '../../models/SourcePartType'; import { SourcePartQuery } from '../../models/SourcePartQuery'; import { validateGroup } from '../../store/groups.api'; import { selectIsJobWriter } from '../../store/roles.slice'; +import { validateSqlFilters } from '../../store/sqlMembershipSources.api'; const getClassNames = classNamesFunction< IAdvancedQueryStyleProps, @@ -55,8 +56,8 @@ export const AdvancedQueryBase: React.FunctionComponent = ( "manager": { "id": 0, "depth": 0 - }, - "filter": "" + }, + "filter": "" }, }, { @@ -114,6 +115,27 @@ export const AdvancedQueryBase: React.FunctionComponent = ( ); }; + const formatSqlErrors = (errors: Map) => { + if (!errors || errors.size === 0) return null; + + return ( +
+
+ {strings.ManageMembership.labels.invalidSqlFilters} +
+ {Array.from(errors.entries()).map((error) => { + const message = `For Source Part ${error[0] + 1}: ${error[1]}..`; + + return ( +
+ {message} +
+ ); + })} +
+ ); + }; + const handleQueryChange = (event: React.FormEvent, newValue?: string) => { setLocalQuery(newValue || ''); onQueryChange(event, newValue); @@ -123,10 +145,10 @@ export const AdvancedQueryBase: React.FunctionComponent = ( try { const parsedQuery = JSON.parse(localQuery || '[]'); const validate = ajv.compile(schema); - const isValid = validate(parsedQuery); + var isValid = validate(parsedQuery); if (isValid) { - const validationResults = await Promise.all( + const groupValidationResults = await Promise.all( (parsedQuery as Array).map(async (part) => { if (part.type === SourcePartType.GroupMembership && part.source) { const result = await dispatch(validateGroup(part.source)).unwrap(); @@ -136,12 +158,32 @@ export const AdvancedQueryBase: React.FunctionComponent = ( }) ); - const invalidGroups = validationResults.filter(result => !result.isValid); + const invalidGroups = groupValidationResults.filter(result => !result.isValid); if (invalidGroups.length > 0) { const invalidGroupIds = invalidGroups.map(result => result.groupId).join(', '); setValidationMessage(`${strings.ManageMembership.labels.invalidGroups} ${invalidGroupIds}`); } else { - setValidationMessage(strings.ManageMembership.labels.validQuery); + const sqlFiltersToValidate = new Map(); + (parsedQuery as Array).forEach((part, index) => { + if (part.type === SourcePartType.HR && part.source && part.source.filter) { + sqlFiltersToValidate.set(index, part.source.filter); + } + }) + + if(sqlFiltersToValidate.size > 0) { + const sqlValidationResponse = await dispatch(validateSqlFilters(sqlFiltersToValidate)).unwrap(); + + if(!sqlValidationResponse.isValid) { + isValid = false; + + const errorsMap = new Map(Object.entries(sqlValidationResponse.errors).map(([key, value]) => [Number(key), value])) + setValidationMessage(formatSqlErrors(errorsMap)); + } + } + + if (isValid) { + setValidationMessage(strings.ManageMembership.labels.validQuery); + } } dispatch(setAdvancedViewQuery(localQuery || '[]')); diff --git a/UI/web-app/src/models/ValidateSqlFiltersResponse.ts b/UI/web-app/src/models/ValidateSqlFiltersResponse.ts new file mode 100644 index 000000000..6dc6ffd29 --- /dev/null +++ b/UI/web-app/src/models/ValidateSqlFiltersResponse.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export interface ValidateSqlFiltersResponse { + isValid: boolean; + errors: Map; +} \ No newline at end of file diff --git a/UI/web-app/src/services/localization/IStrings.ts b/UI/web-app/src/services/localization/IStrings.ts index f3edf9087..0eb668640 100644 --- a/UI/web-app/src/services/localization/IStrings.ts +++ b/UI/web-app/src/services/localization/IStrings.ts @@ -320,6 +320,7 @@ export type IStrings = { validQuery: string; invalidQuery: string; invalidGroups: string; + invalidSqlFilters: string; step3title: string; step3description: string; selectStartDate: string; diff --git a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts index 1546c6a67..2ebaa0f62 100644 --- a/UI/web-app/src/services/localization/i18n/locales/en/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/en/translations.ts @@ -325,6 +325,7 @@ export const strings: IStrings = { validQuery: 'Query is valid.', invalidQuery: 'Failed to parse query. Ensure it is valid JSON.', invalidGroups: 'Invalid group IDs:', + invalidSqlFilters: "Invalid Sql filter. Ensure each filter is valid Sql.", step3title: 'Step 3: Membership Configuration', step3description: 'Define the source membership for the destination.', selectStartDate: 'Select an option to start managing the membership', diff --git a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts index c87da3972..8ed8f776f 100644 --- a/UI/web-app/src/services/localization/i18n/locales/es/translations.ts +++ b/UI/web-app/src/services/localization/i18n/locales/es/translations.ts @@ -328,6 +328,7 @@ export const strings: IStrings = { validQuery: 'Consulta válida.', invalidQuery: 'Error al analizar la consulta. Asegúrese de que sea JSON válido.', invalidGroups: 'Grupos inválido:', + invalidSqlFilters: "Filtro de SQL inválido. Asegure que cada filtro contiene SQL válido.", step3title: 'Paso 3: Configuración de Membresía', step3description: 'Defina la fuente de la membresía para el destino.', selectStartDate: 'Selecciona una opción para comenzar a administrar la membresía', diff --git a/UI/web-app/src/store/sqlMembershipSources.api.tsx b/UI/web-app/src/store/sqlMembershipSources.api.tsx index 8b50d4344..8f269a0b1 100644 --- a/UI/web-app/src/store/sqlMembershipSources.api.tsx +++ b/UI/web-app/src/store/sqlMembershipSources.api.tsx @@ -7,12 +7,13 @@ import { SqlMembershipAttribute, SqlMembershipSource } from '../models'; import { GetAttributeMappingsResponse } from '../models/GetAttributeMappingsResponse'; import { GetAttributeMappingsRequest } from '../models/GetAttributeMappingsRequest'; import { GetAttributeValuesResponse } from '../models/GetAttributeValuesResponse'; +import { ValidateSqlFiltersResponse } from '../models/ValidateSqlFiltersResponse'; export const fetchDefaultSqlMembershipSource = createAsyncThunk( 'sqlMembershipSources/fetchDefaultSqlMembershipSource', async (_, { extra }) => { const { gmmApi } = extra.apis; - + try { return await gmmApi.sqlMembershipSources.fetchDefaultSqlMembershipSource(); } catch (error) { @@ -25,7 +26,7 @@ export const fetchDefaultSqlMembershipSourceAttributes = createAsyncThunk { const { gmmApi } = extra.apis; - + try { return await gmmApi.sqlMembershipSources.fetchDefaultSqlMembershipSourceAttributes(); } catch (error) { @@ -60,7 +61,7 @@ export const fetchAttributeMappings = createAsyncThunk( 'fetchSqlFilterAttributeValues', - async (attribute, { extra }) => { + async (attribute, { extra }) => { const { gmmApi } = extra.apis; let payload: GetAttributeValuesResponse; try { @@ -78,7 +79,7 @@ export const patchDefaultSqlMembershipSourceCustomLabel = createAsyncThunk { const { gmmApi } = extra.apis; - + try { return await gmmApi.sqlMembershipSources.patchDefaultSqlMembershipSourceCustomLabel(customLabel); } catch (error) { @@ -91,11 +92,24 @@ export const patchDefaultSqlMembershipSourceAttributes = createAsyncThunk { const { gmmApi } = extra.apis; - + try { return await gmmApi.sqlMembershipSources.patchDefaultSqlMembershipSourceAttributes(attributes); } catch (error) { throw new Error('Failed to update default SQL membership source attributes!'); } } +); + +export const validateSqlFilters = createAsyncThunk, ThunkConfig>( + 'sqlMembershipSources/validateSqlFilters', + async (filtersMap, { extra }) => { + const { gmmApi } = extra.apis; + + try { + return await gmmApi.sqlMembershipSources.validateSqlFilters(filtersMap); + } catch (error) { + throw new Error('Failed to check Sql filter validation!'); + } + } ); \ No newline at end of file From 83c08d982d05c2e96793a7498566591072ebd8fc Mon Sep 17 00:00:00 2001 From: Lisa Palathingal Date: Wed, 16 Oct 2024 13:11:51 -0700 Subject: [PATCH 0353/1479] update subject/title of onboarding emails --- .../SendNotification/SendNotification.cs | 2 +- .../Orchestrator/OrchestratorFunction.cs | 8 +++++--- .../Orchestrator/OrchestratorRequest.cs | 1 + .../INotifierService.cs | 2 +- .../NotifierServiceTests.cs | 7 ++++--- .../Services.Notifier/NotifierService.cs | 3 ++- .../AdaptiveCards/DefaultCardTemplate.cs | 1 + .../Models/EmailMessage.cs | 1 + .../LocalizationRepository.en-US.resx | 19 ++++++++++++++----- .../Repositories.Mail/MailRepository.cs | 5 ++++- .../NotificationConstants.cs | 5 ++++- 11 files changed, 38 insertions(+), 16 deletions(-) diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNotification/SendNotification.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNotification/SendNotification.cs index 7ebdb84ba..3ccf46dcd 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNotification/SendNotification.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Activity/SendNotification/SendNotification.cs @@ -25,7 +25,7 @@ public SendNotification(ILoggingRepository loggingRepository, INotifierService n public async Task SendNotificationAsync([ActivityTrigger] OrchestratorRequest message) { await _loggingRepository.LogMessageAsync(new LogMessage { RunId = message.RunId, Message = $"{nameof(SendNotification)} function started at: {DateTime.UtcNow}" }); - await _notifierService.SendEmailAsync(message.MessageType, message.MessageBody, message.SubjectTemplate, message.ContentTemplate); + await _notifierService.SendEmailAsync(message.MessageType, message.MessageBody, message.MessageTitle, message.SubjectTemplate, message.ContentTemplate); await _loggingRepository.LogMessageAsync(new LogMessage { RunId = message.RunId, Message = $"{nameof(SendNotification)} function completed at: {DateTime.UtcNow}" }); } } diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs index 0601d3370..b29269913 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorFunction.cs @@ -53,7 +53,8 @@ await context.CallActivityAsync(nameof(LoggerFunction), break; case nameof(NotificationMessageType.SyncStartedNotification): - message.SubjectTemplate = NotificationConstants.OnboardingSubject; + message.MessageTitle = NotificationConstants.OnboardingStartedEmailTitle; + message.SubjectTemplate = NotificationConstants.OnboardingStartedEmailSubject; message.ContentTemplate = NotificationConstants.SyncStartedContent; await context.CallActivityAsync(nameof(SendNotification), message); break; @@ -71,13 +72,14 @@ await context.CallActivityAsync(nameof(LoggerFunction), break; case nameof(NotificationMessageType.SyncCompletedNotification): - message.SubjectTemplate = NotificationConstants.OnboardingSubject; + message.MessageTitle = NotificationConstants.OnboardingCompleteEmailTitle; + message.SubjectTemplate = NotificationConstants.OnboardingCompleteEmailSubject; message.ContentTemplate = NotificationConstants.SyncCompletedContent; await context.CallActivityAsync(nameof(SendNotification), message); break; case nameof(NotificationMessageType.NotValidSourceNotification): - message.SubjectTemplate = NotificationConstants.OnboardingSubject; + message.SubjectTemplate = NotificationConstants.DisabledNotificationSubject; message.ContentTemplate = NotificationConstants.NoValidGroupIdsContent; await context.CallActivityAsync(nameof(SendNotification), message); break; diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorRequest.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorRequest.cs index 930fa0413..d9ebd4759 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorRequest.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Function/Orchestrator/OrchestratorRequest.cs @@ -7,6 +7,7 @@ namespace Hosts.Notifier { public class OrchestratorRequest { + public string MessageTitle { get; set; } public string MessageBody { get; set; } public string MessageType { get; set; } public string SubjectTemplate { get; set; } diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs index 609250dde..66976a78f 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Contracts/INotifierService.cs @@ -16,7 +16,7 @@ public interface INotifierService public Task> RetrieveQueuedNotificationsAsync(); public Task UpdateNotificationStatusAsync(ThresholdNotification notification, ThresholdNotificationStatus status); public Task CreateActionableNotificationFromContentAsync(string messageBody); - public Task SendEmailAsync(string messageType, string messageBody, string subjectTemplate, string contentTemplate); + public Task SendEmailAsync(string messageType, string messageBody, string messageTitle, string subjectTemplate, string contentTemplate); public Task SendNormalThresholdEmailAsync(string messageBody); } diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs index 653bb1426..1334ee826 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier.Tests/NotifierServiceTests.cs @@ -223,7 +223,7 @@ public async Task TestSendEmailAsync() ContentTemplate = contentTemplate }; - await _notifierService.SendEmailAsync(request.MessageType, request.MessageBody, request.SubjectTemplate, request.ContentTemplate); + await _notifierService.SendEmailAsync(request.MessageType, request.MessageBody, request.MessageTitle, request.SubjectTemplate, request.ContentTemplate); _mailRepository.Verify(x => x.SendMailAsync(It.IsAny(), It.IsAny()), Times.Once()); } [TestMethod] @@ -354,6 +354,7 @@ public async Task SendEmailAsync_ShouldHandleNonOKResponse() var job = new SyncJob { Id = Guid.NewGuid(), RunId = Guid.NewGuid(), Requestor = "requestor@example.com", TargetOfficeGroupId = Guid.NewGuid() }; var messageBody = JsonSerializer.Serialize(new { SyncJob = job }); var messageType = NotificationMessageType.SyncStartedNotification.ToString(); + var messageTitle = "OnboardingStartedEmailTitle"; var subjectTemplate = "TestSubjectTemplate"; var contentTemplate = "SyncDisabledNoGroupEmailBody"; @@ -364,7 +365,7 @@ public async Task SendEmailAsync_ShouldHandleNonOKResponse() _mailRepository.Setup(x => x.SendMailAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(responseMessage); - await _notifierService.SendEmailAsync(messageType, messageBody, subjectTemplate, contentTemplate); + await _notifierService.SendEmailAsync(messageType, messageBody, messageTitle, subjectTemplate, contentTemplate); _mailRepository.Verify(x => x.SendMailAsync(It.IsAny(), It.IsAny()), Times.Once); @@ -448,7 +449,7 @@ public async Task SendEmailWhenEmailsHaveBeenDisabled() ContentTemplate = contentTemplate }; - await _notifierService.SendEmailAsync(request.MessageType, request.MessageBody, request.SubjectTemplate, request.ContentTemplate); + await _notifierService.SendEmailAsync(request.MessageType, request.MessageBody, request.MessageTitle, request.SubjectTemplate, request.ContentTemplate); _loggerMock.Verify(x => x.LogMessageAsync(It.Is(m => m.Message.Equals("Email notifications are disabled.")), VerbosityLevel.INFO, It.IsAny(), diff --git a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs index b17d8c9d8..7ac492cfd 100644 --- a/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs +++ b/Service/GroupMembershipManagement/Hosts/Notifier/Services.Notifier/NotifierService.cs @@ -210,7 +210,7 @@ private void TrackSentNotificationEvent(Guid groupId) return (job, additionalContentParameters); } - public async Task SendEmailAsync(string messageType, string messageBody, string subjectTemplate, string contentTemplate) + public async Task SendEmailAsync(string messageType, string messageBody, string messageTitle, string subjectTemplate, string contentTemplate) { var (job, additionalContentParameters) = ParseMessageContentAsync(messageBody); @@ -246,6 +246,7 @@ await _loggingRepository.LogMessageAsync(new LogMessage var message = new EmailMessage { + Title = messageTitle, Subject = subjectTemplate, Content = contentTemplate, SenderAddress = _emailSenderAndRecipients.SenderAddress, diff --git a/Service/GroupMembershipManagement/Models/AdaptiveCards/DefaultCardTemplate.cs b/Service/GroupMembershipManagement/Models/AdaptiveCards/DefaultCardTemplate.cs index beaf1cb38..1191080cf 100644 --- a/Service/GroupMembershipManagement/Models/AdaptiveCards/DefaultCardTemplate.cs +++ b/Service/GroupMembershipManagement/Models/AdaptiveCards/DefaultCardTemplate.cs @@ -9,6 +9,7 @@ public class DefaultCardTemplate { public string GroupId { get; set; } public string ProviderId { get; set; } + public string TitleContent { get; set; } public string SubjectContent { get; set; } public string MessageContent { get; set; } public DateTime CardCreatedTime { get; set; } diff --git a/Service/GroupMembershipManagement/Models/EmailMessage.cs b/Service/GroupMembershipManagement/Models/EmailMessage.cs index 273992edf..6d02ffc76 100644 --- a/Service/GroupMembershipManagement/Models/EmailMessage.cs +++ b/Service/GroupMembershipManagement/Models/EmailMessage.cs @@ -7,6 +7,7 @@ namespace Models [ExcludeFromCodeCoverage] public class EmailMessage { + public string Title { get; set; } public string Subject { get; set; } public string Content { get; set; } public string SenderAddress { get; set; } diff --git a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx index 5c67d11ad..e8fba77e7 100644 --- a/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx +++ b/Service/GroupMembershipManagement/Repositories.Localization/Resources/LocalizationRepository.en-US.resx @@ -1,4 +1,4 @@ - + - + Membership Management diff --git a/UI/web-app/public/staticwebapp.config.json b/UI/web-app/public/staticwebapp.config.json new file mode 100644 index 000000000..697ae7aa3 --- /dev/null +++ b/UI/web-app/public/staticwebapp.config.json @@ -0,0 +1,102 @@ +{ + "routes": [ + { + "route": "/api/*", + "allowedRoles": ["authenticated"] + }, + { + "route": "/static/js/*", + "headers": { + "cache-control": "must-revalidate, max-age=15770000" + } + }, + { + "route": "/static/css/*", + "headers": { + "cache-control": "must-revalidate, max-age=15770000" + } + }, + { + "route": "/static/media/*", + "headers": { + "cache-control": "must-revalidate, max-age=15770000" + } + }, + { + "route": "/Admin", + "rewrite": "/index.html" + }, + { + "route": "/JobDetails", + "rewrite": "/index.html" + }, + { + "route": "/JobDetails/*", + "rewrite": "/index.html" + }, + { + "route": "/OwnerPage", + "rewrite": "/index.html" + }, + { + "route": "/ManageMembership", + "rewrite": "/index.html" + }, + { + "route": "/NotFound", + "rewrite": "/index.html" + }, + { + "route": "/manifest.json", + "headers": { + "cache-control": "no-cache" + } + }, + { + "route": "/*.png", + "headers": { + "cache-control": "must-revalidate, max-age=15770000" + } + }, + { + "route": "/*.ico", + "headers": { + "cache-control": "must-revalidate, max-age=15770000" + } + }, + { + "route": "/*.svg", + "headers": { + "cache-control": "must-revalidate, max-age=15770000" + } + }, + { + "route": "/*", + "rewrite": "/index.html", + "headers": { + "cache-control": "no-cache" + } + } + ], + "navigationFallback": { + "rewrite": "/index.html", + "exclude": [ + "/static/*", + "/manifest.json", + "/*.png", + "/*.ico", + "/*.svg", + "/*.js", + "/*.css" + ] + }, + "mimeTypes": { + ".json": "text/json", + ".js": "text/javascript", + ".css": "text/css", + ".html": "text/html", + ".ico": "image/x-icon", + ".png": "image/png", + ".svg": "image/svg+xml" + } + } \ No newline at end of file diff --git a/UI/web-app/src/apis/entities/JobEntity.ts b/UI/web-app/src/apis/entities/JobEntity.ts index 521d15c20..7e5e1769f 100644 --- a/UI/web-app/src/apis/entities/JobEntity.ts +++ b/UI/web-app/src/apis/entities/JobEntity.ts @@ -19,4 +19,5 @@ export type JobEntity = { thresholdPercentageForAdditions: number; thresholdPercentageForRemovals: number; endpoints: string[]; + requestor: string; }; \ No newline at end of file diff --git a/UI/web-app/src/apis/jobs/JobsApi.ts b/UI/web-app/src/apis/jobs/JobsApi.ts index 04e8dcef3..0b040c4c1 100644 --- a/UI/web-app/src/apis/jobs/JobsApi.ts +++ b/UI/web-app/src/apis/jobs/JobsApi.ts @@ -73,6 +73,7 @@ export class JobsApi extends ApiBase implements IJobsApi { thresholdPercentageForAdditions: entity.thresholdPercentageForAdditions, thresholdPercentageForRemovals: entity.thresholdPercentageForRemovals, endpoints: entity.endpoints, + requestor: entity.requestor, }; } } diff --git a/UI/web-app/src/components/JobsList/JobsList.base.tsx b/UI/web-app/src/components/JobsList/JobsList.base.tsx index ca28216bc..7619851ae 100644 --- a/UI/web-app/src/components/JobsList/JobsList.base.tsx +++ b/UI/web-app/src/components/JobsList/JobsList.base.tsx @@ -241,8 +241,9 @@ export const JobsListBase: React.FunctionComponent = ( ): void => { if(item.targetGroupName === null){ navigate('/NotFound', { replace: true, state: { item: item} }); - } else { - navigate('/JobDetails', { replace: false, state: { item: item } }); + } + if (item && item.syncJobId) { + navigate(`/JobDetails/${item.syncJobId}`); } }; diff --git a/UI/web-app/src/index.tsx b/UI/web-app/src/index.tsx index 37ac18953..5824a0bc8 100644 --- a/UI/web-app/src/index.tsx +++ b/UI/web-app/src/index.tsx @@ -47,11 +47,11 @@ ReactDOM.render( - + }> } /> - } /> + } /> } /> } /> } /> diff --git a/UI/web-app/src/models/Job.ts b/UI/web-app/src/models/Job.ts index 83eac9831..3f6611032 100644 --- a/UI/web-app/src/models/Job.ts +++ b/UI/web-app/src/models/Job.ts @@ -19,4 +19,5 @@ export type Job = { thresholdPercentageForAdditions: number; thresholdPercentageForRemovals: number; endpoints: string[]; + requestor: string; }; diff --git a/UI/web-app/src/models/JobDetails.ts b/UI/web-app/src/models/JobDetails.ts deleted file mode 100644 index 818e3ac79..000000000 --- a/UI/web-app/src/models/JobDetails.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -export interface JobDetails { - startDate: string; - lastSuccessfulStartTime: string; - source: string; - requestor: string; - thresholdViolations: number; - thresholdPercentageForAdditions: number; - thresholdPercentageForRemovals: number; - endpoints: string[]; - period: number; -} diff --git a/UI/web-app/src/models/index.ts b/UI/web-app/src/models/index.ts index 8815d3638..3fd8aa674 100644 --- a/UI/web-app/src/models/index.ts +++ b/UI/web-app/src/models/index.ts @@ -5,7 +5,6 @@ export * from './Destination'; export * from './GetJobDetailsRequest'; export * from './GroupOnboardingStatus'; export * from './Job'; -export * from './JobDetails'; export * from './NewJob'; export * from './ODataQueryOptions'; export * from './Page'; diff --git a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx index 55e764194..dd708196d 100644 --- a/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx +++ b/UI/web-app/src/pages/JobDetails/JobDetails.base.tsx @@ -25,7 +25,7 @@ import { } from '@fluentui/react/lib/Stack'; import React, { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { useNavigate, useLocation } from 'react-router-dom'; +import { useNavigate, useLocation, useParams } from 'react-router-dom'; import { InfoLabel } from '../../components/InfoLabel'; import { PageHeader } from '../../components/PageHeader'; import { type Job } from '../../models/Job'; @@ -39,7 +39,7 @@ import { selectPatchJobDetailsError, selectRemoveGMMError, selectRemoveGMMLoading, - selectJobsLoading + selectSelectedJobLoading } from '../../store/jobs.slice'; import { ContentContainer } from '../../components/ContentContainer/ContentContainer' @@ -80,22 +80,23 @@ export const JobDetailsBase: React.FunctionComponent = ( } ); const location = useLocation(); - const job: Job = location.state.item; + const job: Job = useSelector(selectSelectedJobDetails) ?? location.state?.item ?? {}; const navigate = useNavigate(); + const { jobId } = useParams<{ jobId: string }>(); const dispatch = useDispatch(); - const jobDetails = useSelector(selectSelectedJobDetails); const error = useSelector(selectGetJobDetailsError); + const selectedJob = useSelector(selectSelectedJobDetails); const [showRemoveGMMDialog, setShowRemoveGMMDialog] = useState(false); const [showRemoveGMMError, setShowRemoveGMMError] = useState(false); const removeGMMError = useSelector(selectRemoveGMMError); - const jobsLoading = useSelector(selectJobsLoading); + const jobLoading = useSelector(selectSelectedJobLoading); const removeGMMPending = useSelector(selectRemoveGMMLoading); const isJobWriter = useSelector(selectIsJobWriter); const isJobOwnerDeleter: boolean = useSelector(selectIsJobOwnerDeleter); const canDeleteJob: boolean = isJobWriter || isJobOwnerDeleter; - const [canEditJob, setCanEditJob] = useState(isJobWriter && job.status !== SyncStatus.PendingReview); - const showLoader: boolean = jobsLoading || removeGMMPending; + const [canEditJob, setCanEditJob] = useState(isJobWriter && job?.status !== SyncStatus.PendingReview); + const showLoader: boolean = jobLoading || removeGMMPending; const OpenInNewWindowIcon: IIconProps = { iconName: 'OpenInNewWindow' }; @@ -108,18 +109,18 @@ export const JobDetailsBase: React.FunctionComponent = ( }; const openInAzure = (): void => { - var url = `https://ms.portal.azure.com/#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/Overview/groupId/${job.targetGroupId}`; + var url = `https://ms.portal.azure.com/#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/Overview/groupId/${job?.targetGroupId}`; window.open(url, '_blank', 'noopener,noreferrer'); }; const openRunConfiguration = (): void => { dispatch(setIsEditingExistingJob(true)); - navigate('/ManageMembership', { state: { currentStep: OnboardingSteps.RunConfiguration, jobId: job.syncJobId } }); + navigate('/ManageMembership', { state: { currentStep: OnboardingSteps.RunConfiguration, jobId: job?.syncJobId } }); }; const openMembershipConfiguration = (): void => { dispatch(setIsEditingExistingJob(true)); - navigate('/ManageMembership', { state: { currentStep: OnboardingSteps.MembershipConfiguration, jobId: job.syncJobId } }); + navigate('/ManageMembership', { state: { currentStep: OnboardingSteps.MembershipConfiguration, jobId: job?.syncJobId } }); }; const onRemoveGMMButtonClick = (): void => { @@ -132,10 +133,10 @@ export const JobDetailsBase: React.FunctionComponent = ( const onConfirmRemove = async () => { try { - await dispatch(removeGMM({ syncJobId: job.syncJobId })); + await dispatch(removeGMM({ syncJobId: job?.syncJobId })); await dispatch(fetchJobs()); setShowRemoveGMMDialog(false); - var url = `https://portal.azure.com/#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/Owners/${job.targetGroupId}/menuId/`; + var url = `https://portal.azure.com/#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/Owners/${job?.targetGroupId}/menuId/`; window.open(url, '_blank', 'noopener,noreferrer'); navigate('/'); } catch (error) { @@ -146,12 +147,8 @@ export const JobDetailsBase: React.FunctionComponent = ( useEffect(() => { dispatch(setPagingBarVisible(false)); - dispatch( - fetchJobDetails({ - syncJobId: job.syncJobId - }) - ); - }, [dispatch]); + dispatch(fetchJobDetails({ syncJobId: jobId ?? '' })); + }, [dispatch, jobId]); return ( @@ -185,50 +182,52 @@ export const JobDetailsBase: React.FunctionComponent = ( )} + { selectedJob && (
- - } - removeButton={true} - /> - } - /> - } - actionButtons={ - canEditJob - ? [{ text: strings.JobDetails.editButton, icon: { iconName: 'Edit' }, onClick: openRunConfiguration }] - : [] - } - /> - {jobDetails?.source}} - actionButtons={ - canEditJob - ? [{ text: strings.JobDetails.editButton, icon: { iconName: 'Edit' }, onClick: openMembershipConfiguration }, - { text: strings.JobDetails.viewDetails, icon: { iconName: 'View' }, onClick: openMembershipConfiguration }] - : [] - } - /> -
- {canDeleteJob && - - {strings.JobDetails.labels.removeGMM} - } + + } + removeButton={true} + /> + } + /> + } + actionButtons={ + canEditJob + ? [{ text: strings.JobDetails.editButton, icon: { iconName: 'Edit' }, onClick: openRunConfiguration }] + : [] + } + /> + {job?.query}} + actionButtons={ + canEditJob + ? [{ text: strings.JobDetails.editButton, icon: { iconName: 'Edit' }, onClick: openMembershipConfiguration }, + { text: strings.JobDetails.viewDetails, icon: { iconName: 'View' }, onClick: openMembershipConfiguration }] + : [] + } + /> +
+ {canDeleteJob && + + {strings.JobDetails.labels.removeGMM} + } +
-
+ )}