Skip to content

Commit

Permalink
Support multiple projects on the server (#812)
Browse files Browse the repository at this point in the history
  • Loading branch information
mburumaxwell authored Sep 21, 2023
1 parent 026437e commit 11bbf10
Show file tree
Hide file tree
Showing 50 changed files with 1,577 additions and 644 deletions.
21 changes: 1 addition & 20 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,10 @@ The deployment exposes the following parameters that can be tuned to suit the se

|Parameter Name|Remarks|Required|Default|
|--|--|--|--|
|projectUrl|The URL of the Azure DevOps project or collection. For example `https://dev.azure.com/fabrikam/DefaultCollection`. This URL must be accessible from the network that the deployment is done in. You can modify the deployment to be done in an private network but you are on your own there.|Yes|**none**|
|projectToken|Personal Access Token (PAT) for accessing the Azure DevOps project. The required permissions are: <br/>-&nbsp;Code (Full)<br/>-&nbsp;Pull Requests Threads (Read & Write).<br/>-&nbsp;Notifications (Read, Write & Manage).<br/>See the [documentation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#create-a-pat) to know more about creating a Personal Access Token|Yes|**none**|
|location|Location to deploy the resources.|No|&lt;resource-group-location&gt;|
|name|The name of all resources.|No|`dependabot`|
|synchronizeOnStartup|Whether to synchronize repositories on startup. This option is useful for initial deployments since the server synchronizes every 6 hours. Leaving it on has no harm, it actually helps you find out if the token works based on the logs.|No|false|
|createOrUpdateWebhooksOnStartup|Whether to create or update Azure DevOps subscriptions on startup. This is required if you want configuration files to be picked up automatically and other event driven functionality.<br/>When this is set to `true`, ensure the value provided for `projectToken` has permissions for service hooks and the owner is a Project Administrator. Leaving this on has no harm because the server will only create new subscription if there are no existing ones based on the URL.|No|false|
|projectSetups|A JSON array string representing the projects to be setup on startup. This is useful when running your own setup. Example: `[{\"url\":\"https://dev.azure.com/tingle/dependabot\",\"token\":\"dummy\",\"AutoComplete\":true}]`|
|githubToken|Access token for authenticating requests to GitHub. Required for vulnerability checks and to avoid rate limiting on free requests|No|&lt;empty&gt;|
|autoComplete|Whether to set auto complete on created pull requests.|No|true|
|autoCompleteIgnoreConfigs|Identifiers of configs to be ignored in auto complete. E.g 3,4,10|No|&lt;empty&gt;|
|autoCompleteMergeStrategy|Merge strategy to use when setting auto complete on created pull requests. Allowed values: `NoFastForward`, `Rebase`, `RebaseMerge`, or `Squash`|No|`Squash`|
|autoApprove|Whether to automatically approve created pull requests.|No|false|
|notificationsPassword|The password used to authenticate incoming requests from Azure DevOps|No|&lt;auto-generated&gt;|
|imageTag|The image tag to use when pulling the docker containers. A tag also defines the version. You should avoid using `latest`. Example: `1.1.0`|No|&lt;version-downloaded&gt;|
|minReplicas|The minimum number of replicas to required for the deployment. Given that scheduling runs in process, this value cannot be less than `1`. This may change in the future.|No|1|
|maxReplicas|The maximum number of replicas when automatic scaling engages. In most cases, you do not need more than 1.|No|1|
Expand All @@ -78,8 +70,6 @@ For a one time deployment, it is similar to how you deploy other resources on Az
```bash
az deployment group create --resource-group DEPENDABOT \
--template-file main.bicep \
--parameters projectUrl=<your-project-url> \
--parameters projectToken=<your-pat> \
--parameters githubToken=<your-github-classic-pat> \
--confirm-with-what-if
```
Expand All @@ -104,15 +94,6 @@ The parameters file (`dependabot.parameters.json`):
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"projectUrl": {
"value": "#{System_TeamFoundationCollectionUri}##{System_TeamProject}#"
},
"projectToken": {
"value": "#{DependabotProjectToken}#"
},
"autoComplete": {
"value": true
},
"githubToken": {
"value": "#{DependabotGithubToken}#"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Tingle.Dependabot.Tests.PeriodicTasks;

public class MissedTriggerCheckerTaskTests
{
private const string ProjectId = "prj_1234567890";
private const string RepositoryId = "repo_1234567890";
private const int UpdateId1 = 1;

Expand Down Expand Up @@ -103,9 +104,20 @@ private async Task TestAsync(DateTimeOffset? lastUpdate0, DateTimeOffset? lastUp
var context = provider.GetRequiredService<MainDbContext>();
await context.Database.EnsureCreatedAsync();

await context.Projects.AddAsync(new Project
{
Id = ProjectId,
Url = "https://dev.azure.com/dependabot/dependabot",
Token = "token",
Name = "dependabot",
ProviderId = "6ce954b1-ce1f-45d1-b94d-e6bf2464ba2c",
Password = "burp-bump",
});
await context.Repositories.AddAsync(new Repository
{
Id = RepositoryId,
ProjectId = ProjectId,
ProviderId = Guid.NewGuid().ToString(),
Name = "test-repo",
ConfigFileContents = "",
Updates = new List<RepositoryUpdate>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Tingle.Dependabot.Events;
using Tingle.Dependabot.Models;
using Tingle.Dependabot.Models.Management;
using Tingle.Dependabot.Workflow;
using Tingle.EventBus;
using Tingle.EventBus.Transports.InMemory;
Expand All @@ -12,6 +15,8 @@ namespace Tingle.Dependabot.Tests.PeriodicTasks;

public class SynchronizationTaskTests
{
private const string ProjectId = "prj_1234567890";

private readonly ITestOutputHelper outputHelper;

public SynchronizationTaskTests(ITestOutputHelper outputHelper)
Expand Down Expand Up @@ -42,13 +47,33 @@ private async Task TestAsync(Func<InMemoryTestHarness, SynchronizationTask, Task
.ConfigureLogging(builder => builder.AddXUnit(outputHelper))
.ConfigureServices((context, services) =>
{
var dbName = Guid.NewGuid().ToString();
services.AddDbContext<MainDbContext>(options =>
{
options.UseInMemoryDatabase(dbName, o => o.EnableNullChecks());
options.EnableDetailedErrors();
});
services.AddEventBus(builder => builder.AddInMemoryTransport().AddInMemoryTestHarness());
})
.Build();

using var scope = host.Services.CreateScope();
var provider = scope.ServiceProvider;

var context = provider.GetRequiredService<MainDbContext>();
await context.Database.EnsureCreatedAsync();

await context.Projects.AddAsync(new Project
{
Id = ProjectId,
Url = "https://dev.azure.com/dependabot/dependabot",
Token = "token",
Name = "dependabot",
ProviderId = "6ce954b1-ce1f-45d1-b94d-e6bf2464ba2c",
Password = "burp-bump",
});
await context.SaveChangesAsync();

var harness = provider.GetRequiredService<InMemoryTestHarness>();
await harness.StartAsync();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Tingle.Dependabot.Tests.PeriodicTasks;

public class UpdateJobsCleanerTaskTests
{
private const string ProjectId = "prj_1234567890";
private const string RepositoryId = "repo_1234567890";

private readonly ITestOutputHelper outputHelper;
Expand All @@ -34,6 +35,7 @@ await TestAsync(async (harness, context, pt) =>
await context.UpdateJobs.AddAsync(new UpdateJob
{
Id = Guid.NewGuid().ToString(),
ProjectId = ProjectId,
RepositoryId = RepositoryId,
RepositorySlug = "test-repo",
Created = DateTimeOffset.UtcNow.AddMinutes(-19),
Expand All @@ -46,6 +48,7 @@ await context.UpdateJobs.AddAsync(new UpdateJob
await context.UpdateJobs.AddAsync(new UpdateJob
{
Id = Guid.NewGuid().ToString(),
ProjectId = ProjectId,
RepositoryId = RepositoryId,
RepositorySlug = "test-repo",
Created = DateTimeOffset.UtcNow.AddHours(-100),
Expand All @@ -58,6 +61,7 @@ await context.UpdateJobs.AddAsync(new UpdateJob
await context.UpdateJobs.AddAsync(new UpdateJob
{
Id = targetId,
ProjectId = ProjectId,
RepositoryId = RepositoryId,
RepositorySlug = "test-repo",
Created = DateTimeOffset.UtcNow.AddMinutes(-30),
Expand Down Expand Up @@ -87,6 +91,7 @@ await TestAsync(async (harness, context, pt) =>
await context.UpdateJobs.AddAsync(new UpdateJob
{
Id = Guid.NewGuid().ToString(),
ProjectId = ProjectId,
RepositoryId = RepositoryId,
RepositorySlug = "test-repo",
Created = DateTimeOffset.UtcNow.AddDays(-80),
Expand All @@ -98,6 +103,7 @@ await context.UpdateJobs.AddAsync(new UpdateJob
await context.UpdateJobs.AddAsync(new UpdateJob
{
Id = Guid.NewGuid().ToString(),
ProjectId = ProjectId,
RepositoryId = RepositoryId,
RepositorySlug = "test-repo",
Created = DateTimeOffset.UtcNow.AddDays(-100),
Expand All @@ -109,6 +115,7 @@ await context.UpdateJobs.AddAsync(new UpdateJob
await context.UpdateJobs.AddAsync(new UpdateJob
{
Id = Guid.NewGuid().ToString(),
ProjectId = ProjectId,
RepositoryId = RepositoryId,
RepositorySlug = "test-repo",
Created = DateTimeOffset.UtcNow.AddDays(-120),
Expand Down Expand Up @@ -146,9 +153,20 @@ private async Task TestAsync(Func<InMemoryTestHarness, MainDbContext, UpdateJobs
var context = provider.GetRequiredService<MainDbContext>();
await context.Database.EnsureCreatedAsync();

await context.Projects.AddAsync(new Project
{
Id = ProjectId,
Url = "https://dev.azure.com/dependabot/dependabot",
Token = "token",
Name = "dependabot",
ProviderId = "6ce954b1-ce1f-45d1-b94d-e6bf2464ba2c",
Password = "burp-bump",
});
await context.Repositories.AddAsync(new Repository
{
Id = RepositoryId,
ProjectId = ProjectId,
ProviderId = Guid.NewGuid().ToString(),
Name = "test-repo",
ConfigFileContents = "",
Updates = new List<RepositoryUpdate>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Net;
Expand All @@ -20,6 +19,8 @@ namespace Tingle.Dependabot.Tests;

public class WebhooksControllerIntegrationTests
{
private const string ProjectId = "prj_1234567890";

private readonly ITestOutputHelper outputHelper;

public WebhooksControllerIntegrationTests(ITestOutputHelper outputHelper)
Expand All @@ -41,7 +42,7 @@ await TestAsync(async (harness, client) =>

// password does not match what is on record
request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump5")));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{ProjectId}:burp-bump5")));
response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Assert.Empty(await response.Content.ReadAsStringAsync());
Expand All @@ -55,7 +56,7 @@ public async Task Returns_BadRequest_NoBody()
await TestAsync(async (harness, client) =>
{
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump")));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{ProjectId}:burp-bump")));
request.Content = new StringContent("", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Expand All @@ -74,7 +75,7 @@ public async Task Returns_BadRequest_MissingValues()
await TestAsync(async (harness, client) =>
{
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump")));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{ProjectId}:burp-bump")));
request.Content = new StringContent("{}", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Expand All @@ -96,7 +97,7 @@ await TestAsync(async (harness, client) =>
{
var stream = TestSamples.GetAzureDevOpsPullRequestUpdated1();
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump")));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{ProjectId}:burp-bump")));
request.Content = new StreamContent(stream);
var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
Expand All @@ -115,7 +116,7 @@ await TestAsync(async (harness, client) =>
{
var stream = TestSamples.GetAzureDevOpsGitPush1();
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump")));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{ProjectId}:burp-bump")));
request.Content = new StreamContent(stream);
request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json", "utf-8");
var response = await client.SendAsync(request);
Expand All @@ -139,7 +140,7 @@ await TestAsync(async (harness, client) =>
{
var stream = TestSamples.GetAzureDevOpsPullRequestUpdated1();
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump")));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{ProjectId}:burp-bump")));
request.Content = new StreamContent(stream);
request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json", "utf-8");
var response = await client.SendAsync(request);
Expand All @@ -156,7 +157,7 @@ await TestAsync(async (harness, client) =>
{
var stream = TestSamples.GetAzureDevOpsPullRequestMerged1();
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump")));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{ProjectId}:burp-bump")));
request.Content = new StreamContent(stream);
request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json", "utf-8");
var response = await client.SendAsync(request);
Expand All @@ -173,7 +174,7 @@ await TestAsync(async (harness, client) =>
{
var stream = TestSamples.GetAzureDevOpsPullRequestCommentEvent1();
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump")));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{ProjectId}:burp-bump")));
request.Content = new StreamContent(stream);
request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json", "utf-8");
var response = await client.SendAsync(request);
Expand All @@ -188,13 +189,6 @@ private async Task TestAsync(Func<InMemoryTestHarness, HttpClient, Task> execute
// Arrange
var builder = new WebHostBuilder()
.ConfigureLogging(builder => builder.AddXUnit(outputHelper))
.ConfigureAppConfiguration(builder =>
{
builder.AddInMemoryCollection(new Dictionary<string, string?>
{
["Authentication:Schemes:ServiceHooks:Credentials:vsts"] = "burp-bump",
});
})
.ConfigureServices((context, services) =>
{
services.AddControllers()
Expand Down Expand Up @@ -247,6 +241,17 @@ private async Task TestAsync(Func<InMemoryTestHarness, HttpClient, Task> execute
var context = provider.GetRequiredService<MainDbContext>();
await context.Database.EnsureCreatedAsync();

await context.Projects.AddAsync(new Dependabot.Models.Management.Project
{
Id = ProjectId,
Url = "https://dev.azure.com/dependabot/dependabot",
Token = "token",
Name = "dependabot",
ProviderId = "6ce954b1-ce1f-45d1-b94d-e6bf2464ba2c",
Password = "burp-bump",
});
await context.SaveChangesAsync();

var harness = provider.GetRequiredService<InMemoryTestHarness>();
await harness.StartAsync();

Expand Down
Loading

0 comments on commit 11bbf10

Please sign in to comment.