Skip to content

Commit 526106d

Browse files
committed
Use skiptoken to retrieve complete list of resources
Currently all calls to ARM APIs ignore skiptoken link. This meant only partial list of resources would be retrieved in some cases. This change adds support to walk the skiptoken url when retrieving GET /subscriptions/{subscriptionId}/resources. Note: Other API calls still have this limitation. Resource Search uses the same API but has not been updated to make use of this new functionality yet (only looks at top 100 resources in a subscription)
1 parent ddf19b2 commit 526106d

File tree

5 files changed

+123
-18
lines changed

5 files changed

+123
-18
lines changed

ARMExplorer.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,8 @@
376376
<Compile Include="Controllers\OperationController.cs" />
377377
<Compile Include="Controllers\OperationInfo.cs" />
378378
<Compile Include="Controllers\ArmRepository.cs" />
379+
<Compile Include="Model\ArmResource.cs" />
380+
<Compile Include="Model\ArmResourceListResult.cs" />
379381
<Compile Include="Modules\Extensions.cs" />
380382
<Compile Include="SwaggerParser\Model\CodeGenerationSettings.cs" />
381383
<Compile Include="SwaggerParser\Model\ComparisonContext.cs" />

Controllers/ArmRepository.cs

+63-17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Net.Http.Headers;
77
using System.Text.RegularExpressions;
88
using System.Threading.Tasks;
9+
using ARMExplorer.Model;
910
using Newtonsoft.Json;
1011
using Newtonsoft.Json.Linq;
1112

@@ -14,6 +15,7 @@ namespace ARMExplorer.Controllers
1415
public class ArmRepository : IArmRepository
1516
{
1617
private readonly IHttpClientWrapper _clientWrapper;
18+
private readonly int _maxNextLinkDepth = 5;
1719

1820
public ArmRepository(IHttpClientWrapper clientWrapper)
1921
{
@@ -40,17 +42,63 @@ public async Task<IList<string>> GetSubscriptionIdsAsync(HttpRequestMessage requ
4042
return subscriptionIds;
4143
}
4244

45+
private static bool AddResourceToList(IEnumerable<ArmResource> resources, ISet<ArmResource> allResources)
46+
{
47+
var initalCount = allResources.Count;
48+
49+
foreach (var resource in resources)
50+
{
51+
allResources.Add(resource);
52+
}
53+
54+
var updatedCount = allResources.Count;
55+
56+
return updatedCount > initalCount;
57+
}
58+
59+
private async Task<HashSet<ArmResource>> GetResources(HttpRequestMessage requestMessage, string getResourcesUrl)
60+
{
61+
var allResources = new HashSet<ArmResource>();
62+
var currentNextLinkDepth = 0;
63+
64+
while (!string.IsNullOrEmpty(getResourcesUrl))
65+
{
66+
var response = await GetAsync(requestMessage, getResourcesUrl);
67+
response.EnsureSuccessStatusCode();
68+
var armResourceListResult = await response.Content.ReadAsAsync<ArmResourceListResult>();
69+
70+
var newResourceFound = AddResourceToList(armResourceListResult.Value, allResources);
71+
72+
// ARM API returns the same skiptoken and resources repeatedly when there are no more resources. To avoid infinite cycle break when
73+
// 1. No new resource was found in the current response or
74+
// 2. Limit the max number of links to follow to _maxNextLinkDepth or
75+
// 3. When nextLink is empty
76+
77+
if (!newResourceFound)
78+
{
79+
break;
80+
}
81+
82+
if (currentNextLinkDepth++ > _maxNextLinkDepth)
83+
{
84+
break;
85+
}
86+
87+
getResourcesUrl = armResourceListResult.NextLink;
88+
}
89+
90+
return allResources;
91+
}
92+
4393
public async Task<HashSet<string>> GetProviderNamesFor(HttpRequestMessage requestMessage, string subscriptionId)
4494
{
45-
var response = await GetResourcesForAsync(requestMessage, subscriptionId);
46-
response.EnsureSuccessStatusCode();
47-
dynamic resources = await response.Content.ReadAsAsync<JObject>();
48-
JArray values = resources.value;
95+
var initialGetResourcesUrl = string.Format(Utils.ResourcesTemplate, HyakUtils.CSMUrl, subscriptionId, Utils.CSMApiVersion);
96+
var resources = await GetResources(requestMessage, initialGetResourcesUrl);
4997
var uniqueProviders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
50-
foreach (dynamic value in values)
98+
99+
foreach (var resource in resources)
51100
{
52-
string id = value.id;
53-
var match = Regex.Match(id, "/subscriptions/.*?/resourceGroups/(.*?)/providers/(.*?)/(.*?)/");
101+
var match = Regex.Match(resource.Id, "/subscriptions/.*?/resourceGroups/(.*?)/providers/(.*?)/(.*?)/");
54102
if (match.Success)
55103
{
56104
var provider = match.Groups[2].Value.ToUpperInvariant();
@@ -63,15 +111,13 @@ public async Task<HashSet<string>> GetProviderNamesFor(HttpRequestMessage reques
63111

64112
public async Task<Dictionary<string, Dictionary<string, HashSet<string>>>> GetProvidersFor(HttpRequestMessage requestMessage, string subscriptionId)
65113
{
66-
var response = await GetResourcesForAsync(requestMessage, subscriptionId);
67-
response.EnsureSuccessStatusCode();
68-
69-
dynamic resources = await response.Content.ReadAsAsync<JObject>();
70-
JArray values = resources.value;
114+
var initialGetResourcesUrl = string.Format(Utils.ResourcesTemplate, HyakUtils.CSMUrl, subscriptionId, Utils.CSMApiVersion);
115+
var resources = await GetResources(requestMessage, initialGetResourcesUrl);
71116
var result = new Dictionary<string, Dictionary<string, HashSet<string>>>();
72-
foreach (dynamic value in values)
117+
118+
foreach (var resource in resources)
73119
{
74-
string id = value.id;
120+
string id = resource.Id;
75121
var match = Regex.Match(id, "/subscriptions/.*?/resourceGroups/(.*?)/providers/(.*?)/(.*?)/");
76122
if (match.Success)
77123
{
@@ -116,10 +162,10 @@ private async Task<HttpResponseMessage> GetSubscriptionsAsync(HttpRequestMessage
116162
return await _clientWrapper.SendAsync(requestMessage, sendRequest);
117163
}
118164

119-
private async Task<HttpResponseMessage> GetResourcesForAsync(HttpRequestMessage requestMessage, string subscriptionId)
165+
private async Task<HttpResponseMessage> GetAsync(HttpRequestMessage requestMessage, string url)
120166
{
121-
var sendRequest = new HttpRequestMessage(HttpMethod.Get, string.Format(Utils.ResourcesTemplate, HyakUtils.CSMUrl, subscriptionId, Utils.CSMApiVersion));
122-
return await _clientWrapper.SendAsync(requestMessage, sendRequest);
167+
var sendRequest = new HttpRequestMessage(HttpMethod.Get, url);
168+
return await _clientWrapper.ExecuteAsync(requestMessage, sendRequest);
123169
}
124170
}
125171
}

Model/ArmResource.cs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
3+
namespace ARMExplorer.Model
4+
{
5+
public class ArmResource : IEquatable<ArmResource>
6+
{
7+
[Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
8+
public string Id { get; set; }
9+
// other fields ignored
10+
11+
public bool Equals(ArmResource other)
12+
{
13+
if (ReferenceEquals(null, other)) return false;
14+
if (ReferenceEquals(this, other)) return true;
15+
return string.Equals(Id, other.Id, StringComparison.OrdinalIgnoreCase);
16+
}
17+
18+
public override bool Equals(object obj)
19+
{
20+
if (ReferenceEquals(null, obj)) return false;
21+
if (ReferenceEquals(this, obj)) return true;
22+
if (obj.GetType() != this.GetType()) return false;
23+
return Equals((ArmResource) obj);
24+
}
25+
26+
public override int GetHashCode()
27+
{
28+
return Id != null ? Id.GetHashCode() : 0;
29+
}
30+
}
31+
}

Model/ArmResourceListResult.cs

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Collections.ObjectModel;
2+
3+
namespace ARMExplorer.Model
4+
{
5+
public class ArmResourceListResult
6+
{
7+
[Newtonsoft.Json.JsonProperty("value", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
8+
public Collection<ArmResource> Value { get; set; }
9+
10+
[Newtonsoft.Json.JsonProperty("nextLink", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
11+
public string NextLink { get; set; }
12+
}
13+
}

Tests/WebApiTests/MockHttpClientWrapper.cs

+14-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,20 @@ public Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage, Ht
3030

3131
public Task<HttpResponseMessage> ExecuteAsync(HttpRequestMessage requestMessage, HttpRequestMessage executeRequest)
3232
{
33-
throw new NotImplementedException();
33+
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
34+
string filePath;
35+
if (executeRequest.RequestUri.ToString().Contains("resources"))
36+
{
37+
filePath = Path.Combine(new DirectoryInfo(Directory.GetCurrentDirectory()).FullName, Path.Combine("WebApiTests", "data", "resourcesForsubscription.json"));
38+
}
39+
else
40+
{
41+
filePath = Path.Combine(new DirectoryInfo(Directory.GetCurrentDirectory()).FullName, Path.Combine("WebApiTests", "data", "subscriptions.json"));
42+
}
43+
responseMessage.Content = new StringContent(File.ReadAllText(filePath), Encoding.UTF8, "application/json");
44+
var response = new TaskCompletionSource<HttpResponseMessage>();
45+
response.SetResult(responseMessage);
46+
return response.Task;
3447
}
3548
}
3649
}

0 commit comments

Comments
 (0)