Skip to content

Commit

Permalink
Merge pull request #151 from Dynatrace/fix-jira-auth
Browse files Browse the repository at this point in the history
JIRA rest api session handling and bugfixes
  • Loading branch information
discostu105 authored Dec 18, 2018
2 parents b555d24 + 88e701b commit a9cbb8a
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 28 deletions.
2 changes: 1 addition & 1 deletion src/SuperDumpSelector/SuperDumpSelector.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
<Version>2.3.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Diagnostics.Runtime">
<Version>1.0.0</Version>
<Version>1.0.2</Version>
</PackageReference>
<PackageReference Include="System.Console">
<Version>4.3.1</Version>
Expand Down
2 changes: 2 additions & 0 deletions src/SuperDumpService.Test.Fakes/FakeDumpStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class FakeDumpStorage : IDumpStorage, IBundleStorage {

public bool DelaysEnabled { get; set; }

public FakeDumpStorage() : this(Enumerable.Empty<FakeDump>()) { }

public FakeDumpStorage(IEnumerable<FakeDump> fakeDumps) {
this.fakeDumpsDict = new Dictionary<DumpIdentifier, FakeDump>();
this.fakeBundlesDict = new Dictionary<string, FakeBundle>();
Expand Down
6 changes: 5 additions & 1 deletion src/SuperDumpService.Test.Fakes/FakeJiraApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ public class FakeJiraApiService : IJiraApiService {
private readonly object sync = new object();

public void SetFakeJiraIssues(string bundleId, IEnumerable<JiraIssueModel> jiraIssueModels) {
jiraIssueStore[bundleId] = jiraIssueModels;
if (jiraIssueModels == null) {
jiraIssueStore.Remove(bundleId, out var x);
} else {
jiraIssueStore[bundleId] = jiraIssueModels;
}
}

public Task<IEnumerable<JiraIssueModel>> GetBulkIssues(IEnumerable<string> issueKeys) {
Expand Down
22 changes: 15 additions & 7 deletions src/SuperDumpService.Test/JiraIssueRepositoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public async Task TestJiraIssueRepository() {
// population
await jiraIssueStorage.Store("bundle1", new List<JiraIssueModel> { new JiraIssueModel("JRA-1111") });
await jiraIssueStorage.Store("bundle2", new List<JiraIssueModel> { new JiraIssueModel("JRA-2222"), new JiraIssueModel("JRA-3333") });
await jiraIssueStorage.Store("bundle9", new List<JiraIssueModel> { new JiraIssueModel("JRA-9999") });

await bundleRepo.Populate();
await jiraIssueRepository.Populate();
Expand All @@ -81,39 +82,46 @@ public async Task TestJiraIssueRepository() {
item => Assert.Equal("JRA-2222", item.Key),
item => Assert.Equal("JRA-3333", item.Key));

Assert.Collection(jiraIssueRepository.GetIssues("bundle9"),
item => Assert.Equal("JRA-9999", item.Key));

Assert.Empty(jiraIssueRepository.GetIssues("bundle3"));

// fake, that in jira some bundles have been referenced in new issues
jiraApiService.SetFakeJiraIssues("bundle1", new JiraIssueModel[] { new JiraIssueModel("JRA-1111") });
jiraApiService.SetFakeJiraIssues("bundle2", new JiraIssueModel[] { new JiraIssueModel("JRA-2222"), new JiraIssueModel("JRA-3333"), new JiraIssueModel("JRA-4444") });
jiraApiService.SetFakeJiraIssues("bundle3", new JiraIssueModel[] { new JiraIssueModel("JRA-1111"), new JiraIssueModel("JRA-5555") });
jiraApiService.SetFakeJiraIssues("bundle1", new JiraIssueModel[] { new JiraIssueModel("JRA-1111") }); // same
jiraApiService.SetFakeJiraIssues("bundle2", new JiraIssueModel[] { new JiraIssueModel("JRA-2222"), new JiraIssueModel("JRA-4444") }); // one added, one removed
jiraApiService.SetFakeJiraIssues("bundle3", new JiraIssueModel[] { new JiraIssueModel("JRA-1111"), new JiraIssueModel("JRA-5555") }); // new
jiraApiService.SetFakeJiraIssues("bundle9", null ); // removed jira issues

// trigger update of repository
await jiraIssueRepository.SearchBundleIssuesAsync(bundleRepo.GetAll(), true);

Assert.Collection(jiraIssueRepository.GetIssues("bundle1"),
item => Assert.Equal("JRA-1111", item.Key));

Assert.Collection(jiraIssueRepository.GetIssues("bundle2"),
item => Assert.Equal("JRA-2222", item.Key),
item => Assert.Equal("JRA-3333", item.Key),
item => Assert.Equal("JRA-4444", item.Key));

Assert.Collection(jiraIssueRepository.GetIssues("bundle3"),
item => Assert.Equal("JRA-1111", item.Key),
item => Assert.Equal("JRA-5555", item.Key));

Assert.Empty(jiraIssueRepository.GetIssues("bundle9"));

var res = await jiraIssueRepository.GetAllIssuesByBundleIdsWithoutWait(new string[] { "bundle1", "bundle2", "bundle7", "bundle666" });
var res = await jiraIssueRepository.GetAllIssuesByBundleIdsWithoutWait(new string[] { "bundle1", "bundle2", "bundle7", "bundle666", "bundle9" });
Assert.Equal(2, res.Count());

Assert.Collection(res["bundle1"],
item => Assert.Equal("JRA-1111", item.Key));

Assert.Collection(res["bundle2"],
item => Assert.Equal("JRA-2222", item.Key),
item => Assert.Equal("JRA-3333", item.Key),
item => Assert.Equal("JRA-4444", item.Key));


Assert.Empty(jiraIssueRepository.GetIssues("bundle7"));
Assert.Empty(jiraIssueRepository.GetIssues("bundle666"));
Assert.Empty(jiraIssueRepository.GetIssues("bundle9"));
}

private IEnumerable<FakeDump> CreateFakeDumps() {
Expand Down
1 change: 1 addition & 0 deletions src/SuperDumpService/JiraIntegrationSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class JiraIntegrationSettings {
public int JiraBundleSearchLimit { get; set; }
public double JiraBundleSearchDelay { get; set; }
public TimeSpan JiraBundleSearchTimeSpan { get; set; }
public string JiraApiAuthUrl { get; set; }
public string JiraApiSearchUrl { get; set; }
public string JiraApiUsername { get; set; }
public string JiraApiPassword { get; set; }
Expand Down
1 change: 0 additions & 1 deletion src/SuperDumpService/Models/DumpIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ private sealed class DumpIdentifierPool {

public DumpIdentifier Allocate(string bundleId, string dumpId) {
int hash = $"{bundleId}:{dumpId}".GetHashCode();
if (pool.TryGetValue(hash, out DumpIdentifier id)) return id; // fast path
lock (sync) {
if (pool.TryGetValue(hash, out DumpIdentifier id2)) return id2;
var newId = new DumpIdentifier(bundleId, dumpId);
Expand Down
67 changes: 62 additions & 5 deletions src/SuperDumpService/Services/JiraApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
Expand All @@ -13,6 +14,26 @@
using SuperDumpService.Models;

namespace SuperDumpService.Services {
/// <summary>
/// Datastructure that Jira returns on authentication
/// </summary>
public class SessionInfo {
public Session session;
public LoginInfo loginInfo;

public class Session {
public string name;
public string value;
}

public class LoginInfo {
public int failedLoginCount;
public int loginCount;
public DateTime lastFailedLoginTime;
public DateTime previousLoginTime;
}
}

public class JiraApiService : IJiraApiService {
private const string JsonMediaType = "application/json";
private const string JiraIssueFields = "status,resolution";
Expand All @@ -21,13 +42,49 @@ public class JiraApiService : IJiraApiService {
private readonly JiraIntegrationSettings settings;
private readonly HttpClient client;

public CookieContainer Cookies {
get { return HttpClientHandler.CookieContainer; }
set { HttpClientHandler.CookieContainer = value; }
}

public HttpClientHandler HttpClientHandler { get; set; }

public SessionInfo Session { get; set; }

public JiraApiService(IOptions<SuperDumpSettings> settings) {
this.settings = settings.Value.JiraIntegrationSettings;
if (this.settings == null) return;
client = new HttpClient();

HttpClientHandler = new HttpClientHandler {
AllowAutoRedirect = true,
UseCookies = true,
CookieContainer = new CookieContainer()
};

client = new HttpClient(HttpClientHandler);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(JsonMediaType));
client.DefaultRequestHeaders.Authorization = GetBasicAuthenticationHeader(this.settings.JiraApiUsername, this.settings.JiraApiPassword);
}

private async Task Authenticate() {
using (var authClient = new HttpClient()) {
var uriBuilder = new UriBuilder(settings.JiraApiAuthUrl);
var response = await authClient.PostAsJsonAsync(settings.JiraApiAuthUrl, new {
username = this.settings.JiraApiUsername,
password = this.settings.JiraApiPassword
});
var sessionInfo = await response.Content.ReadAsAsync<SessionInfo>();
this.Session = sessionInfo;
var cookieDomain = new Uri(new Uri(settings.JiraApiAuthUrl).GetLeftPart(UriPartial.Authority));
this.Cookies.Add(cookieDomain, new Cookie(sessionInfo.session.name, sessionInfo.session.value));
}
}

private async Task EnsureAuthentication() {
// reauthenticate every 10 minutes
if (Session == null || (DateTime.Now - Session.loginInfo.previousLoginTime).Minutes > 10) {
await Authenticate();
}
}

public async Task<IEnumerable<JiraIssueModel>> GetJiraIssues(string bundleId) {
Expand All @@ -49,10 +106,12 @@ private async Task<IEnumerable<JiraIssueModel>> JiraSearch(string queryString) {
query["fields"] = JiraIssueFields;
uriBuilder.Query = query.ToString();

await EnsureAuthentication();
return await HandleResponse(await client.GetAsync(uriBuilder.ToString()));
}

private async Task<IEnumerable<JiraIssueModel>> JiraPostSearch(string queryString) {
private async Task<IEnumerable<JiraIssueModel>> JiraPostSearch(string queryString, int retry = 3) {
await EnsureAuthentication();
return await HandleResponse(await client.PostAsJsonAsync(settings.JiraApiSearchUrl, new {
jql = queryString,
fields = JiraIssueFieldsArray
Expand All @@ -64,11 +123,9 @@ private async Task<IEnumerable<JiraIssueModel>> HandleResponse(HttpResponseMessa
throw new HttpRequestException($"Jira api call {response.RequestMessage.RequestUri} returned status code {response.StatusCode}");
}
IEnumerable<JiraIssueModel> issues = (await response.Content.ReadAsAsync<JiraSearchResultModel>()).Issues;

foreach (JiraIssueModel issue in issues) {
issue.Url = settings.JiraIssueUrl + issue.Key;
}

return issues;
}

Expand Down
32 changes: 19 additions & 13 deletions src/SuperDumpService/Services/JiraIssueRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class JiraIssueRepository {
private readonly IdenticalDumpRepository identicalDumpRepository;
private readonly JiraIntegrationSettings settings;
private readonly ILogger<JiraIssueRepository> logger;
private readonly ConcurrentDictionary<string, IEnumerable<JiraIssueModel>> bundleIssues = new ConcurrentDictionary<string, IEnumerable<JiraIssueModel>>();
private readonly ConcurrentDictionary<string, IList<JiraIssueModel>> bundleIssues = new ConcurrentDictionary<string, IList<JiraIssueModel>>();
public bool IsPopulated { get; private set; } = false;

public JiraIssueRepository(IOptions<SuperDumpSettings> settings,
Expand All @@ -47,7 +47,7 @@ public async Task Populate() {
try {
IEnumerable<JiraIssueModel> jiraIssues = await jiraIssueStorage.Read(bundle.BundleId);
if (jiraIssues != null) {
bundleIssues[bundle.BundleId] = jiraIssues;
bundleIssues[bundle.BundleId] = jiraIssues.ToList();
}
} catch (Exception e) {
logger.LogError("error reading jira-issue file: " + e.ToString());
Expand All @@ -61,7 +61,7 @@ public async Task Populate() {
}

public IEnumerable<JiraIssueModel> GetIssues(string bundleId) {
return bundleIssues.GetValueOrDefault(bundleId, Enumerable.Empty<JiraIssueModel>())
return bundleIssues.GetValueOrDefault(bundleId, Enumerable.Empty<JiraIssueModel>().ToList())
.ToList();
}

Expand All @@ -85,7 +85,7 @@ public async Task<IDictionary<string, IEnumerable<JiraIssueModel>>> GetAllIssues
public async Task WipeJiraIssueCache() {
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try {
foreach (KeyValuePair<string, IEnumerable<JiraIssueModel>> item in bundleIssues) {
foreach (KeyValuePair<string, IList<JiraIssueModel>> item in bundleIssues) {
jiraIssueStorage.Wipe(item.Key);
}
bundleIssues.Clear();
Expand All @@ -105,7 +105,7 @@ public async Task RefreshAllIssuesAsync() {
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try {
//Only update bundles with unresolved issues
IEnumerable<KeyValuePair<string, IEnumerable<JiraIssueModel>>> bundlesToRefresh =
IEnumerable<KeyValuePair<string, IList<JiraIssueModel>>> bundlesToRefresh =
bundleIssues.Where(bundle => bundle.Value.Any(issue => issue.GetStatusName() != "Resolved"));

if (!bundlesToRefresh.Any()) {
Expand Down Expand Up @@ -171,7 +171,7 @@ public async Task SearchBundleIssuesAsync(IEnumerable<BundleMetainfo> bundles, b
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try {
IEnumerable<BundleMetainfo> bundlesToSearch = force ? bundles :
bundles.Where(bundle => !bundleIssues.TryGetValue(bundle.BundleId, out IEnumerable<JiraIssueModel> issues) || !issues.Any()); //All bundles without issues
bundles.Where(bundle => !bundleIssues.TryGetValue(bundle.BundleId, out IList<JiraIssueModel> issues) || !issues.Any()); //All bundles without issues

await Task.WhenAll(bundlesToSearch.Select(bundle => SearchBundleAsync(bundle, force)));
} finally {
Expand All @@ -180,26 +180,32 @@ public async Task SearchBundleIssuesAsync(IEnumerable<BundleMetainfo> bundles, b
}

private async Task SearchBundleAsync(BundleMetainfo bundle, bool force) {
IEnumerable<JiraIssueModel> jiraIssues;
IList<JiraIssueModel> jiraIssues;
if (!force && bundle.CustomProperties.TryGetValue(settings.CustomPropertyJiraIssueKey, out string jiraIssue)) {
jiraIssues = new List<JiraIssueModel>() { new JiraIssueModel { Key = jiraIssue } };
} else {
jiraIssues = await apiService.GetJiraIssues(bundle.BundleId);
jiraIssues = (await apiService.GetJiraIssues(bundle.BundleId)).ToList();
}
if (jiraIssues.Any()) {
await jiraIssueStorage.Store(bundle.BundleId, bundleIssues[bundle.BundleId] = jiraIssues);
bundleIssues[bundle.BundleId] = jiraIssues;
await jiraIssueStorage.Store(bundle.BundleId, jiraIssues);
} else {
bundleIssues.Remove(bundle.BundleId, out var val);
await jiraIssueStorage.Store(bundle.BundleId, Enumerable.Empty<JiraIssueModel>());
}
}

private async Task SetBundleIssues(IEnumerable<KeyValuePair<string, IEnumerable<JiraIssueModel>>> bundlesToUpdate, IEnumerable<JiraIssueModel> refreshedIssues) {
private async Task SetBundleIssues(IEnumerable<KeyValuePair<string, IList<JiraIssueModel>>> bundlesToUpdate, IEnumerable<JiraIssueModel> refreshedIssues) {
var issueDictionary = refreshedIssues.ToDictionary(issue => issue.Key, issue => issue);

//Select the issues for each bundle and store them in the bundleIssues Dictionary
//I am not sure if this is the best way to do this
var fileStorageTasks = new List<Task>();
foreach (KeyValuePair<string, IEnumerable<JiraIssueModel>> bundle in bundlesToUpdate) {
IEnumerable<JiraIssueModel> issues = bundle.Value.Select(issue => issueDictionary[issue.Key]);
fileStorageTasks.Add(jiraIssueStorage.Store(bundle.Key, bundleIssues[bundle.Key] = issues)); //update the issue file for the bundle
foreach (KeyValuePair<string, IList<JiraIssueModel>> bundle in bundlesToUpdate) {
if (issueDictionary.ContainsKey(bundle.Key)) {
IList<JiraIssueModel> issues = bundle.Value.Select(issue => issueDictionary[issue.Key]).ToList();
fileStorageTasks.Add(jiraIssueStorage.Store(bundle.Key, bundleIssues[bundle.Key] = issues)); //update the issue file for the bundle
}
}

await Task.WhenAll(fileStorageTasks);
Expand Down

0 comments on commit a9cbb8a

Please sign in to comment.