-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement mgmt resource #47944
base: main
Are you sure you want to change the base?
Implement mgmt resource #47944
Changes from 29 commits
f041f24
badf1a5
69fa28a
615fec1
d136be2
a040627
0296a71
fda752a
580508a
ba12225
ef83882
120e164
e4bbde0
b2842c4
5f5cee9
41cf53c
7bba967
5d3a879
2fe508f
d9fd368
17b2a0f
45a10a7
b06fb59
c98e231
848e6d7
0db68cb
2b15650
51bd7f3
a8b2b8f
dc2b83d
adbcc1c
bb92673
af9bd9c
c5ca437
3ed339d
d800d35
8d78f7f
8fbc8aa
081696d
f9e8afd
8d7b831
4dcccc7
d00ee3a
0086b11
e34db7b
417af63
965e951
579dd9b
682e704
22e08c6
541193c
20cdaaf
78b5cb5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,26 +5,56 @@ | |
using Azure.Generator.Providers; | ||
using Azure.Generator.Utilities; | ||
using Microsoft.TypeSpec.Generator.ClientModel; | ||
using Microsoft.TypeSpec.Generator.Input; | ||
using Microsoft.TypeSpec.Generator.Providers; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace Azure.Generator | ||
{ | ||
/// <inheritdoc/> | ||
public class AzureOutputLibrary : ScmOutputLibrary | ||
{ | ||
// TODO: categorize clients into operationSets, which contains operations sharing the same Path | ||
private Dictionary<string, OperationSet> _pathToOperationSetMap; | ||
private Dictionary<string, HashSet<OperationSet>> _resourceDataBySpecNameMap; | ||
//TODO: Move these to InputLibrary instead | ||
private Dictionary<RequestPath, OperationSet> _pathToOperationSetMap; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a lot of code in here that is mgmt specific and it starts to get difficult to tell which is which have we considered a mgmt plugin? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most likely. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #48481 to track the split, it will happen soon after this PR. |
||
private Dictionary<string, HashSet<OperationSet>> _specNameToOperationSetsMap; | ||
private Dictionary<string, InputModelType> _inputTypeMap; | ||
|
||
/// <inheritdoc/> | ||
public AzureOutputLibrary() | ||
{ | ||
_pathToOperationSetMap = CategorizeClients(); | ||
_resourceDataBySpecNameMap = EnsureResourceDataMap(); | ||
_specNameToOperationSetsMap = EnsureOperationsetMap(); | ||
_inputTypeMap = AzureClientPlugin.Instance.InputLibrary.InputNamespace.Models.OfType<InputModelType>().ToDictionary(model => model.Name); | ||
} | ||
|
||
private Dictionary<string, HashSet<OperationSet>> EnsureResourceDataMap() | ||
private MgmtLongRunningOperationProvider? _armOperation; | ||
internal MgmtLongRunningOperationProvider ArmOperation => _armOperation ??= new MgmtLongRunningOperationProvider(false); | ||
|
||
private MgmtLongRunningOperationProvider? _genericArmOperation; | ||
internal MgmtLongRunningOperationProvider GenericArmOperation => _genericArmOperation ??= new MgmtLongRunningOperationProvider(true); | ||
|
||
private IReadOnlyList<ResourceProvider> BuildResources() | ||
{ | ||
var result = new List<ResourceProvider>(); | ||
foreach ((var schemaName, var operationSets) in _specNameToOperationSetsMap) | ||
{ | ||
var model = _inputTypeMap[schemaName]; | ||
var resourceData = (ResourceDataProvider)AzureClientPlugin.Instance.TypeFactory.CreateModel(model)!; | ||
foreach (var operationSet in operationSets) | ||
{ | ||
var requestPath = operationSet.RequestPath; | ||
var resourceType = ResourceDetection.GetResourceTypeFromPath(requestPath); | ||
var resource = new ResourceProvider(operationSet, schemaName, resourceData, resourceType); | ||
AzureClientPlugin.Instance.AddTypeToKeep(resource.Name); | ||
result.Add(resource); | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
private Dictionary<string, HashSet<OperationSet>> EnsureOperationsetMap() | ||
{ | ||
var result = new Dictionary<string, HashSet<OperationSet>>(); | ||
foreach (var operationSet in _pathToOperationSetMap.Values) | ||
|
@@ -44,16 +74,14 @@ private Dictionary<string, HashSet<OperationSet>> EnsureResourceDataMap() | |
return result; | ||
} | ||
|
||
private Dictionary<string, OperationSet> CategorizeClients() | ||
private Dictionary<RequestPath, OperationSet> CategorizeClients() | ||
live1206 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var result = new Dictionary<string, OperationSet>(); | ||
var result = new Dictionary<RequestPath, OperationSet>(); | ||
foreach (var inputClient in AzureClientPlugin.Instance.InputLibrary.InputNamespace.Clients) | ||
{ | ||
var requestPathList = new HashSet<string>(); | ||
foreach (var operation in inputClient.Operations) | ||
{ | ||
var path = operation.GetHttpPath(); | ||
requestPathList.Add(path); | ||
if (result.TryGetValue(path, out var operationSet)) | ||
{ | ||
operationSet.Add(operation); | ||
|
@@ -75,22 +103,28 @@ private Dictionary<string, OperationSet> CategorizeClients() | |
} | ||
|
||
/// <inheritdoc/> | ||
// TODO: generate resources and collections | ||
// TODO: generate collections | ||
protected override TypeProvider[] BuildTypeProviders() | ||
{ | ||
var baseProviders = base.BuildTypeProviders(); | ||
if (AzureClientPlugin.Instance.IsAzureArm.Value == true) | ||
{ | ||
var armOperation = new MgmtLongRunningOperationProvider(false); | ||
var genericArmOperation = new MgmtLongRunningOperationProvider(true); | ||
|
||
// TODO: remove them once they are referenced in Resource operation implementation | ||
AzureClientPlugin.Instance.AddTypeToKeep(armOperation.Name); | ||
AzureClientPlugin.Instance.AddTypeToKeep(genericArmOperation.Name); | ||
return [.. base.BuildTypeProviders(), new RequestContextExtensionsDefinition(), armOperation, genericArmOperation]; | ||
//BuildLROProviders(); | ||
var resources = BuildResources(); | ||
return [.. base.BuildTypeProviders(), new RequestContextExtensionsDefinition(), ArmOperation, GenericArmOperation, .. resources, .. resources.Select(r => r.Source)]; | ||
} | ||
return [.. base.BuildTypeProviders(), new RequestContextExtensionsDefinition()]; | ||
} | ||
|
||
internal bool IsResource(string name) => _resourceDataBySpecNameMap.ContainsKey(name); | ||
private void BuildLROProviders() | ||
{ | ||
// TODO: remove them once they are referenced in Resource operation implementation | ||
AzureClientPlugin.Instance.AddTypeToKeep(ArmOperation.Name); | ||
AzureClientPlugin.Instance.AddTypeToKeep(GenericArmOperation.Name); | ||
} | ||
|
||
internal bool IsResource(string name) => _specNameToOperationSetsMap.ContainsKey(name); | ||
|
||
internal Lazy<IEnumerable<OperationSet>> ResourceOperationSets => new(() => _specNameToOperationSetsMap.Values.SelectMany(x => x)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
|
||
namespace Azure.Generator.Mgmt.Models | ||
{ | ||
internal class RequestPath : IEquatable<RequestPath>, IReadOnlyList<string> | ||
{ | ||
private const string ProviderPath = "/subscriptions/{subscriptionId}/providers/{resourceProviderNamespace}"; | ||
private const string FeaturePath = "/subscriptions/{subscriptionId}/providers/Microsoft.Features/providers/{resourceProviderNamespace}/features"; | ||
|
||
public const string ManagementGroupScopePrefix = "/providers/Microsoft.Management/managementGroups"; | ||
public const string ResourceGroupScopePrefix = "/subscriptions/{subscriptionId}/resourceGroups"; | ||
public const string SubscriptionScopePrefix = "/subscriptions"; | ||
public const string TenantScopePrefix = "/tenants"; | ||
public const string Providers = "/providers"; | ||
|
||
public static readonly RequestPath ManagementGroup = new("/providers/Microsoft.Management/managementGroups/{managementGroupId}"); | ||
public static readonly RequestPath ResourceGroup = new("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}"); | ||
public static readonly RequestPath Subscription = new("/subscriptions/{subscriptionId}"); | ||
public static readonly RequestPath Tenant = new(string.Empty); | ||
|
||
private string _path; | ||
private IReadOnlyList<string> _segments; | ||
|
||
public RequestPath(string path) | ||
{ | ||
_path = path; | ||
_segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); | ||
IndexOfLastProviders = _path.LastIndexOf(Providers); | ||
} | ||
|
||
public RequestPath(IEnumerable<string> segments) | ||
{ | ||
_segments = segments.ToArray(); | ||
_path = string.Join("", _segments); | ||
live1206 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
public int Count => _segments.Count; | ||
|
||
public string SerializedPath => _path; | ||
|
||
public int IndexOfLastProviders { get; } | ||
|
||
public string this[int index] => _segments[index]; | ||
|
||
/// <summary> | ||
/// Check if this <see cref="RequestPath"/> is a prefix path of the other request path. | ||
/// Note that this.IsAncestorOf(this) will return false which indicates that this method is testing the "proper ancestor" like a proper subset. | ||
/// </summary> | ||
/// <param name="other"></param> | ||
/// <returns></returns> | ||
public bool IsAncestorOf(RequestPath other) | ||
{ | ||
// To be the parent of other, you must at least be shorter than other. | ||
if (other.Count <= Count) | ||
return false; | ||
for (int i = 0; i < Count; i++) | ||
{ | ||
// we need the segment to be identical when strict is true (which is the default value) | ||
// when strict is false, we also need the segment to be identical if it is constant. | ||
// but if it is a reference, we only require they have the same type, do not require they have the same variable name. | ||
// This case happens a lot during the management group parent detection - different RP calls this different things | ||
if (!this[i].Equals(other[i])) | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
/// <summary> | ||
/// Trim this from the other and return the <see cref="RequestPath"/>that remain. | ||
/// The result is "other - this" by removing this as a prefix of other. | ||
/// If this == other, return empty request path | ||
/// </summary> | ||
/// <param name="other"></param> | ||
/// <returns></returns> | ||
/// <exception cref="InvalidOperationException">if this.IsAncestorOf(other) is false</exception> | ||
public RequestPath TrimAncestorFrom(RequestPath other) | ||
{ | ||
if (TryTrimAncestorFrom(other, out var diff)) | ||
return diff; | ||
|
||
throw new InvalidOperationException($"Request path {this} is not parent of {other}"); | ||
} | ||
|
||
private bool TryTrimAncestorFrom(RequestPath other, [MaybeNullWhen(false)] out RequestPath diff) | ||
{ | ||
diff = default; | ||
if (this == other) | ||
{ | ||
diff = Tenant; | ||
return true; | ||
} | ||
if (IsAncestorOf(other)) | ||
{ | ||
diff = new RequestPath(string.Join("", other._segments.Skip(Count))); | ||
return true; | ||
} | ||
// Handle the special case of trim provider from feature | ||
else if (_path == ProviderPath && other._path.StartsWith(FeaturePath)) | ||
{ | ||
diff = new RequestPath(string.Join("", other._segments.Skip(Count + 2))); | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
public bool Equals(RequestPath? other) | ||
{ | ||
if (Count != other?.Count) | ||
return false; | ||
for (int i = 0; i < Count; i++) | ||
{ | ||
if (!this[i].Equals(other[i])) | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
public override bool Equals(object? obj) => obj is RequestPath other && Equals(other); | ||
|
||
public override int GetHashCode() => _path.GetHashCode(); | ||
|
||
public override string ToString() => _path; | ||
|
||
public IEnumerator<String> GetEnumerator() => _segments.GetEnumerator(); | ||
|
||
IEnumerator IEnumerable.GetEnumerator() => _segments.GetEnumerator(); | ||
|
||
public static bool operator ==(RequestPath left, RequestPath right) | ||
{ | ||
return left.Equals(right); | ||
} | ||
|
||
public static bool operator !=(RequestPath left, RequestPath right) | ||
{ | ||
return !(left == right); | ||
} | ||
|
||
public static implicit operator string(RequestPath requestPath) | ||
{ | ||
return requestPath._path; | ||
} | ||
|
||
public static bool IsSegmentConstant(string segment) | ||
{ | ||
var trimmed = segment.TrimStart('{').TrimEnd('}'); | ||
var isScope = trimmed == "scope"; | ||
return !isScope && !segment.StartsWith('{'); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System.Collections.Generic; | ||
|
||
namespace Azure.Generator.Mgmt.Models | ||
{ | ||
internal class SingletonResourceSuffix | ||
{ | ||
public static SingletonResourceSuffix Parse(IReadOnlyList<string> segments) | ||
{ | ||
// put the segments in pairs | ||
var pairs = new List<(string Key, string Value)>(); | ||
for (int i = 0; i < segments.Count; i += 2) | ||
{ | ||
pairs.Add((segments[i], segments[i + 1])); | ||
} | ||
|
||
return new SingletonResourceSuffix(pairs); | ||
} | ||
|
||
private IReadOnlyList<(string Key, string Value)> _pairs; | ||
|
||
private SingletonResourceSuffix(IReadOnlyList<(string Key, string Value)> pairs) | ||
{ | ||
_pairs = pairs; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using Microsoft.TypeSpec.Generator.Primitives; | ||
using Microsoft.TypeSpec.Generator.Providers; | ||
using System.Threading; | ||
|
||
namespace Azure.Generator.Primitives | ||
{ | ||
internal class KnownAzureParameters | ||
{ | ||
public static readonly ParameterProvider Response = new("response", $"The response from the service.", new CSharpType(typeof(Response))); | ||
|
||
public static readonly ParameterProvider WaitUntil = new("waitUntil", $"<see cref=\"WaitUntil.Completed\"/> if the method should wait to return until the long-running operation has completed on the service; <see cref=\"WaitUntil.Started\"/> if it should return after starting the operation. For more information on long-running operations, please see <see href=\"https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/LongRunningOperations.md\"> Azure.Core Long-Running Operation samples</see>.", new CSharpType(typeof(WaitUntil))); | ||
|
||
public static readonly ParameterProvider CancellationTokenWithoutDefault = new("cancellationToken", $"The cancellation token to use.", new CSharpType(typeof(CancellationToken))); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't your parent just a property on the client from typespec?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For TypeSpec, we should be able to get the resource hierarchy directly via #48281.
The resource detection logic is mainly for swagger input.
For now, given
getArmResources
API is not ready for integration. We use it as a temporary workaround.Eventually, when we have the native way ready, we should get rid of them. And the resource detection should be part of the M4 output conversion to tspCodeModel.json in autorest.csharp.
Added TODO to remove them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As offline discussed, we need to clarify the contract of input types containing the resource hierarchy. And it should be an extensible way of holding it in Azure plugin only.
Then, we can put the temp workaround of resource detection close to the input instead of output.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isolated Resource related logic in ResourceBuilder, so we have separated the temp workaround to a central place.