Skip to content

Commit 96f0ea3

Browse files
committed
Refactor AzureRMDocsService and related classes for improved markdown parsing
- Moved markdown parsing logic from AzureRMDocsService to a new AzureRMDocsParser class for better separation of concerns. - Introduced RemarksJson and TerraformSampleEntry models for structured data handling. - Updated AzApiExamplesService to utilize new models and parsing methods. - Added clamping for parallelism in AztfexportService methods to ensure valid input. - Refactored ConftestService to handle policy set names in a case-insensitive manner. - Adjusted unit tests to reflect changes in method calls and expected outputs.
1 parent 10273cf commit 96f0ea3

13 files changed

Lines changed: 630 additions & 569 deletions

File tree

servers/Azure.Mcp.Server/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,7 +1149,7 @@ Example prompts that generate Azure CLI commands:
11491149
* "Turn off DDoS protection in my Platform Landing Zone"
11501150
* "Turn off Bastion host in my Platform Landing Zone"
11511151

1152-
### �️ Azure Terraform
1152+
### Azure Terraform
11531153

11541154
* "Get the documentation for azurerm_virtual_network"
11551155
* "Show me the arguments for azurerm_storage_account"
@@ -1163,7 +1163,7 @@ Example prompts that generate Azure CLI commands:
11631163
* "Validate Terraform files in ./my-terraform-folder against Azure security policies"
11641164
* "Validate my Terraform plan file against Azure-Proactive-Resiliency-Library-v2 policies"
11651165

1166-
### 🏛️ Azure Well-Architected Framework
1166+
### 🏛️ Azure Well-Architected Framework
11671167

11681168
* "List all services with Well-Architected Framework guidance"
11691169
* "What services have architectural guidance?"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Azure.Mcp.Tools.AzureTerraform.Services;
7+
8+
internal sealed class RemarksJson
9+
{
10+
[JsonPropertyName("TerraformSamples")]
11+
public List<TerraformSampleEntry>? TerraformSamples { get; set; }
12+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Azure.Mcp.Tools.AzureTerraform.Services;
7+
8+
internal sealed class TerraformSampleEntry
9+
{
10+
[JsonPropertyName("ResourceType")]
11+
public string ResourceType { get; set; } = string.Empty;
12+
13+
[JsonPropertyName("Path")]
14+
public string Path { get; set; } = string.Empty;
15+
16+
[JsonPropertyName("Description")]
17+
public string Description { get; set; } = string.Empty;
18+
}

tools/Azure.Mcp.Tools.AzureTerraform/src/Services/AvmDocsService.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public async Task<List<AvmVersion>> GetVersionsAsync(string moduleName, Cancella
4141
ConfigureGitHubHeaders(client);
4242

4343
var response = await client.GetAsync(new Uri(apiUrl), cancellationToken).ConfigureAwait(false);
44+
CheckForRateLimit(response);
4445
response.EnsureSuccessStatusCode();
4546

4647
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
@@ -248,4 +249,30 @@ private static void ConfigureGitHubHeaders(HttpClient client)
248249
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", githubToken);
249250
}
250251
}
252+
253+
private static void CheckForRateLimit(HttpResponseMessage response)
254+
{
255+
if ((int)response.StatusCode == 403
256+
&& response.Headers.TryGetValues("X-RateLimit-Remaining", out var values))
257+
{
258+
var remaining = values.FirstOrDefault();
259+
if (remaining == "0")
260+
{
261+
string resetMessage = "";
262+
if (response.Headers.TryGetValues("X-RateLimit-Reset", out var resetValues))
263+
{
264+
var resetUnix = resetValues.FirstOrDefault();
265+
if (long.TryParse(resetUnix, out var epoch))
266+
{
267+
var resetTime = DateTimeOffset.FromUnixTimeSeconds(epoch);
268+
resetMessage = $" Rate limit resets at {resetTime:u}.";
269+
}
270+
}
271+
272+
throw new InvalidOperationException(
273+
$"GitHub API rate limit exceeded.{resetMessage} " +
274+
"Set a GITHUB_TOKEN environment variable to increase the rate limit.");
275+
}
276+
}
277+
}
251278
}

tools/Azure.Mcp.Tools.AzureTerraform/src/Services/AzAPIDocsService.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public AzApiDocsResult GetDocumentation(string resourceTypeName, string? apiVers
4141
};
4242
}
4343

44+
private static string EscapeHcl(string value) =>
45+
value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n");
46+
4447
private static string GetParentResourceType(string resourceTypeName)
4548
{
4649
string[] parts = resourceTypeName.Split('/');
@@ -80,7 +83,7 @@ internal static string FormatAsHcl(
8083
// Get the resource label from the last part of resource type
8184
string[] resourceParts = resourceTypeName.Split('/');
8285
string lastPart = resourceParts[^1];
83-
string label = lastPart.EndsWith('s') ? lastPart[..^1] : lastPart;
86+
string label = lastPart.ToLowerInvariant();
8487

8588
sb.AppendLine("```hcl");
8689
sb.AppendLine($"resource \"azapi_resource\" \"{label}\" {{");
@@ -186,14 +189,14 @@ private static void FormatObjectProperties(
186189
{
187190
if (nestedType is ObjectTypeEntity nestedObj)
188191
{
189-
string desc = string.IsNullOrEmpty(prop.Description) ? "" : $" // {prop.Description}";
192+
string desc = string.IsNullOrEmpty(prop.Description) ? "" : $" // {EscapeHcl(prop.Description)}";
190193
sb.AppendLine($"{pad}{prop.Name} = {{ {required}{desc}");
191194
FormatObjectProperties(sb, nestedObj, typeIndex, indent + 2);
192195
sb.AppendLine($"{pad}}}");
193196
}
194197
else if (nestedType is DiscriminatedObjectTypeEntity discObj)
195198
{
196-
string desc = string.IsNullOrEmpty(prop.Description) ? "" : $" // {prop.Description}";
199+
string desc = string.IsNullOrEmpty(prop.Description) ? "" : $" // {EscapeHcl(prop.Description)}";
197200
sb.AppendLine($"{pad}{prop.Name} = \"{required} Discriminated by '{discObj.Discriminator}'.{desc}\"");
198201
}
199202
else
@@ -207,14 +210,14 @@ private static void FormatObjectProperties(
207210
string elementType = prop.Type[..^2];
208211
if (typeIndex.TryGetValue(elementType, out var elementComplexType) && elementComplexType is ObjectTypeEntity elementObj)
209212
{
210-
string desc = string.IsNullOrEmpty(prop.Description) ? "" : $" // {prop.Description}";
213+
string desc = string.IsNullOrEmpty(prop.Description) ? "" : $" // {EscapeHcl(prop.Description)}";
211214
sb.AppendLine($"{pad}{prop.Name} = [ {{ {required} Array.{desc}");
212215
FormatObjectProperties(sb, elementObj, typeIndex, indent + 2);
213216
sb.AppendLine($"{pad}}} ]");
214217
}
215218
else
216219
{
217-
sb.AppendLine($"{pad}{prop.Name} = \"{required} Array of {elementType}. {prop.Description ?? ""}\"");
220+
sb.AppendLine($"{pad}{prop.Name} = \"{required} Array of {elementType}. {EscapeHcl(prop.Description ?? "")}\"");
218221
}
219222
}
220223
else
@@ -227,8 +230,8 @@ private static void FormatObjectProperties(
227230
private static string FormatPropertyDescriptionWithRequired(string required, PropertyInfo prop)
228231
{
229232
string typeDesc = MapTypeToDescription(prop.Type);
230-
string desc = prop.Description ?? "";
231-
string modifiers = prop.Modifiers != null ? $" [{prop.Modifiers}]" : "";
233+
string desc = EscapeHcl(prop.Description ?? "");
234+
string modifiers = prop.Modifiers != null ? $" [{EscapeHcl(prop.Modifiers)}]" : "";
232235
return $"{required} {typeDesc}. {desc}{modifiers}".Trim();
233236
}
234237

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Azure.Mcp.Tools.AzureTerraform.Services;
7+
8+
[JsonSerializable(typeof(RemarksJson))]
9+
[JsonSerializable(typeof(List<TerraformSampleEntry>))]
10+
internal sealed partial class AzApiExamplesJsonContext : JsonSerializerContext;

tools/Azure.Mcp.Tools.AzureTerraform/src/Services/AzApiExamplesService.cs

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License.
33

44
using System.Text.Json;
5-
using System.Text.Json.Serialization;
65
using Azure.Mcp.Tools.AzureTerraform.Models;
76

87
namespace Azure.Mcp.Tools.AzureTerraform.Services;
@@ -122,25 +121,3 @@ public async Task<List<AzApiExample>> GetExamplesAsync(
122121
}
123122
}
124123
}
125-
126-
internal sealed class RemarksJson
127-
{
128-
[JsonPropertyName("TerraformSamples")]
129-
public List<TerraformSampleEntry>? TerraformSamples { get; set; }
130-
}
131-
132-
internal sealed class TerraformSampleEntry
133-
{
134-
[JsonPropertyName("ResourceType")]
135-
public string ResourceType { get; set; } = string.Empty;
136-
137-
[JsonPropertyName("Path")]
138-
public string Path { get; set; } = string.Empty;
139-
140-
[JsonPropertyName("Description")]
141-
public string Description { get; set; } = string.Empty;
142-
}
143-
144-
[JsonSerializable(typeof(RemarksJson))]
145-
[JsonSerializable(typeof(List<TerraformSampleEntry>))]
146-
internal sealed partial class AzApiExamplesJsonContext : JsonSerializerContext;

tools/Azure.Mcp.Tools.AzureTerraform/src/Services/AztfexportService.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public AztfexportCommandResult GenerateResourceCommand(
4444
int parallelism = 10,
4545
bool continueOnError = true)
4646
{
47+
parallelism = Math.Clamp(parallelism, 1, 50);
4748
var args = new List<string> { "resource", "--non-interactive", "--plain-ui" };
4849

4950
if (string.Equals(provider, "azapi", StringComparison.OrdinalIgnoreCase))
@@ -98,6 +99,7 @@ public AztfexportCommandResult GenerateResourceGroupCommand(
9899
int parallelism = 10,
99100
bool continueOnError = true)
100101
{
102+
parallelism = Math.Clamp(parallelism, 1, 50);
101103
var args = new List<string> { "resource-group", "--non-interactive", "--plain-ui" };
102104

103105
if (string.Equals(provider, "azapi", StringComparison.OrdinalIgnoreCase))
@@ -152,6 +154,7 @@ public AztfexportCommandResult GenerateQueryCommand(
152154
int parallelism = 10,
153155
bool continueOnError = true)
154156
{
157+
parallelism = Math.Clamp(parallelism, 1, 50);
155158
var args = new List<string> { "query", "--non-interactive", "--plain-ui" };
156159

157160
if (string.Equals(provider, "azapi", StringComparison.OrdinalIgnoreCase))

0 commit comments

Comments
 (0)