Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2407049
Implement configuration ingestor
adamzip Dec 5, 2025
04c3c99
Update src/Maestro/Maestro.DataProviders/ConfigurationIngestor/Config…
adamzip Dec 10, 2025
a5c29f3
Update src/Maestro/Maestro.DataProviders/ISqlBarClient.cs
adamzip Dec 10, 2025
58d7ae3
Update src/Maestro/Maestro.DataProviders/SqlBarClient.cs
adamzip Dec 10, 2025
dd64430
Update src/Maestro/Maestro.DataProviders/SqlBarClient.cs
adamzip Dec 10, 2025
32e4cb7
Update src/Microsoft.DotNet.Darc/DarcLib/Models/Yaml/IExternallySynce…
adamzip Dec 10, 2025
27603e0
Update src/Maestro/Maestro.DataProviders/SqlBarClient.cs
adamzip Dec 10, 2025
99ecb06
apply copilot suggestion
adamzip Dec 10, 2025
fdf9d75
ignore UniqueId field on yaml models
adamzip Dec 10, 2025
8a91cf5
Fix deletions, rename namespace, add tests
adamzip Dec 11, 2025
7067d8f
Rename method because it's not actually a hash
adamzip Dec 11, 2025
f9ca460
Create wrappers for uniqueId member
adamzip Dec 12, 2025
2afadc0
PR improvements
adamzip Dec 15, 2025
800ae0b
Remove wrong usage of IEnumerable
adamzip Dec 15, 2025
43a32e2
Add tests for excluded assets
adamzip Dec 15, 2025
960beaf
More CR changes
adamzip Dec 15, 2025
f9f79ef
Merge branch 'main' into configuration-ingestion-implementation
adamzip Dec 15, 2025
fea9080
force immutable name, one more test, and fix build
adamzip Dec 15, 2025
1909d6f
CR changes: use namespace include, add target repo ...
adamzip Dec 16, 2025
7934644
Add comment about subscriptions string
adamzip Dec 16, 2025
26e6d3b
Use hash sets instead of dictionaries in method
adamzip Dec 16, 2025
b05a405
make not async method not async
adamzip Dec 16, 2025
6fe0036
Change internalsVisibleTo
adamzip Dec 16, 2025
45dbeef
Use IReadOnlyCollection instead of List
adamzip Dec 16, 2025
edd6b9e
improve dictionary usages
adamzip Dec 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Maestro.DataProviders.ConfigurationIngestion.Helpers;
using System.Collections.Generic;

#nullable enable
namespace Maestro.DataProviders.ConfigurationIngestion;

public record ConfigurationData(
IReadOnlyCollection<IngestedSubscription> Subscriptions,
IReadOnlyCollection<IngestedChannel> Channels,
IReadOnlyCollection<IngestedDefaultChannel> DefaultChannels,
IReadOnlyCollection<IngestedBranchMergePolicies> BranchMergePolicies);
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Licensed to the .NET Foundation under one or more agreements.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this go to the helpers folder?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Helper might not be the best name for this class. It does quite a lot of the core logic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be fair, all the methods here are internal and static, so it's kind of a helper in that sense (there's zero side effects in that class).

It's hard to find another name, but maybe if i separate the converters into one class and the entity diff computations into another, we can have a converters class and a .. ingestion diff.. computer.. class

// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using Maestro.Data.Models;
using Maestro.DataProviders.ConfigurationIngestion.Helpers;
using Microsoft.DotNet.MaestroConfiguration.Client.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

#nullable enable
namespace Maestro.DataProviders.ConfigurationIngestion;

internal class ConfigurationDataHelper
{
internal static ConfigurationData CreateConfigurationDataObject(Namespace namespaceEntity)
{
var convertedSubscriptions = namespaceEntity.Subscriptions
.Select(sub => SqlBarClient.ToClientModelSubscription(sub))
.Select(SubscriptionYaml.FromClientModel)
.Select(yamlSub => new IngestedSubscription(yamlSub))
.ToList();

var convertedChannels = namespaceEntity.Channels
.Select(channel => SqlBarClient.ToClientModelChannel(channel))
.Select(ChannelYaml.FromClientModel)
.Select(yamlChannel => new IngestedChannel(yamlChannel))
.ToList();

var convertedDefaultChannels = namespaceEntity.DefaultChannels
.Select(dc => SqlBarClient.ToClientModelDefaultChannel(dc))
.Select(DefaultChannelYaml.FromClientModel)
.Select(yamlDc => new IngestedDefaultChannel(yamlDc))
.ToList();

var convertedBranchMergePolicies = namespaceEntity.RepositoryBranches
.Select(rb => SqlBarClient.ToClientModelRepositoryBranch(rb))
.Select(BranchMergePoliciesYaml.FromClientModel)
.Select(rbYaml => new IngestedBranchMergePolicies(rbYaml))
.ToList();

return new ConfigurationData(
convertedSubscriptions,
convertedChannels,
convertedDefaultChannels,
convertedBranchMergePolicies);
}

internal static ConfigurationDataUpdate ComputeEntityUpdates(
ConfigurationData configurationData,
ConfigurationData existingConfigurationData)
{
EntityChanges<IngestedSubscription> subscriptionChanges =
ComputeUpdatesForEntity<IngestedSubscription, Guid>(
existingConfigurationData.Subscriptions,
configurationData.Subscriptions);

EntityChanges<IngestedChannel> channelChanges =
ComputeUpdatesForEntity<IngestedChannel, string>(
existingConfigurationData.Channels,
configurationData.Channels);

EntityChanges<IngestedDefaultChannel> defaultChannelChanges =
ComputeUpdatesForEntity<IngestedDefaultChannel, (string, string, string)>(
existingConfigurationData.DefaultChannels,
configurationData.DefaultChannels);

EntityChanges<IngestedBranchMergePolicies> branchMergePolicyChanges =
ComputeUpdatesForEntity<IngestedBranchMergePolicies, (string, string)>(
existingConfigurationData.BranchMergePolicies,
configurationData.BranchMergePolicies);

return new ConfigurationDataUpdate(
subscriptionChanges,
channelChanges,
defaultChannelChanges,
branchMergePolicyChanges);
}

internal static EntityChanges<T> ComputeUpdatesForEntity<T, TId>(
IReadOnlyCollection<T> dbEntities,
IReadOnlyCollection<T> externalEntities)
where T : class, IExternallySyncedEntity<TId>
where TId : notnull
{
var dbIds = dbEntities.Select(e => e.UniqueId).ToHashSet();
var externalIds = externalEntities.Select(e => e.UniqueId).ToHashSet();

IReadOnlyCollection<T> creations = [.. externalEntities
.Where(e => !dbIds.Contains(e.UniqueId))];

IReadOnlyCollection<T> removals = [.. dbEntities
.Where(e => !externalIds.Contains(e.UniqueId))];

IReadOnlyCollection<T> updates = [.. externalEntities
.Where(e => dbIds.Contains(e.UniqueId))];

return new EntityChanges<T>(creations, updates, removals);
}

internal static Subscription ConvertIngestedSubscriptionToDao(
IngestedSubscription subscription,
Namespace namespaceEntity,
Dictionary<string, Channel> existingChannelsByName)
{
if (!existingChannelsByName.TryGetValue(subscription.Values.Channel, out Channel? existingChannel))
{
throw new InvalidOperationException(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

darc exception?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think we often use those when it's an internal logic exception

$"Channel '{subscription.Values.Channel}' not found for subscription creation.");
}

return new Subscription
{
Id = subscription.Values.Id,
ChannelId = existingChannel.Id,
Channel = existingChannel,
SourceRepository = subscription.Values.SourceRepository,
TargetRepository = subscription.Values.TargetRepository,
TargetBranch = subscription.Values.TargetBranch,
PolicyObject = new SubscriptionPolicy
{
UpdateFrequency = (UpdateFrequency)(int)subscription.Values.UpdateFrequency,
Batchable = subscription.Values.Batchable,
MergePolicies = [.. subscription.Values.MergePolicies.Select(ConvertMergePolicyYamlToDao)],
},
Enabled = subscription.Values.Enabled,
SourceEnabled = subscription.Values.SourceEnabled,
SourceDirectory = subscription.Values.SourceDirectory,
TargetDirectory = subscription.Values.TargetDirectory,
PullRequestFailureNotificationTags = subscription.Values.FailureNotificationTags,
ExcludedAssets = subscription.Values.ExcludedAssets == null ? [] : [.. subscription.Values.ExcludedAssets.Select(asset => new AssetFilter() { Filter = asset })],
Namespace = namespaceEntity,
};
}

internal static Channel ConvertIngestedChannelToDao(
IngestedChannel channel,
Namespace namespaceEntity)
=> new()
{
Name = channel.Values.Name,
Classification = channel.Values.Classification,
Namespace = namespaceEntity,
};

internal static DefaultChannel ConvertIngestedDefaultChannelToDao(
IngestedDefaultChannel defaultChannel,
Namespace namespaceEntity,
Dictionary<string, Channel> existingChannelsByName)
{
if (existingChannelsByName.TryGetValue(defaultChannel.Values.Channel, out Channel? existingChannel))
{
return new DefaultChannel
{
ChannelId = existingChannel.Id,
Channel = existingChannel,
Repository = defaultChannel.Values.Repository,
Namespace = namespaceEntity,
Branch = defaultChannel.Values.Branch,
Enabled = defaultChannel.Values.Enabled,
};
}
else
{
throw new InvalidOperationException(
$"Channel '{defaultChannel.Values.Channel}' not found for default channel creation.");
}
}

internal static RepositoryBranch ConvertIngestedBranchMergePoliciesToDao(
IngestedBranchMergePolicies branchMergePolicies,
Namespace namespaceEntity)
{
var policyObject = new RepositoryBranch.Policy
{
MergePolicies = [.. branchMergePolicies.Values.MergePolicies.Select(ConvertMergePolicyYamlToDao)],
};

var branchMergePolicyDao = new RepositoryBranch
{
RepositoryName = branchMergePolicies.Values.Repository,
BranchName = branchMergePolicies.Values.Branch,
PolicyString = JsonConvert.SerializeObject(policyObject),
Namespace = namespaceEntity,
};

return branchMergePolicyDao;
}

private static MergePolicyDefinition ConvertMergePolicyYamlToDao(MergePolicyYaml mergePolicy)
=> new()
{
Name = mergePolicy.Name,
Properties = mergePolicy.Properties?.ToDictionary(
p => p.Key,
p => JToken.FromObject(p.Value)), // todo: this seems fragile. Can we change MergePolicyYaml to be <string, JToken> like the DAO & DTO?
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using Maestro.DataProviders.ConfigurationIngestion.Helpers;

#nullable enable
namespace Maestro.DataProviders.ConfigurationIngestion;

public record ConfigurationDataUpdate(
EntityChanges<IngestedSubscription> Subscriptions,
EntityChanges<IngestedChannel> Channels,
EntityChanges<IngestedDefaultChannel> DefaultChannels,
EntityChanges<IngestedBranchMergePolicies> RepositoryBranches);

public record EntityChanges<T>(
IReadOnlyCollection<T> Creations,
IReadOnlyCollection<T> Updates,
IReadOnlyCollection<T> Removals) where T: class;
Loading