mirror of
https://dev.azure.com/effectory/Survey%20Software/_git/Cloud%20Engineering
synced 2026-02-27 18:52:18 +01:00
Merged PR 63702: Add Sonar Client to update permissions and tags in Sonar Projects to new team structure
Add Sonar Client to update permissions and tags in Sonar Projects to new team structure Related work items: #125680
This commit is contained in:
16
ConsoleApps/SonarClient/SonarClient.sln
Normal file
16
ConsoleApps/SonarClient/SonarClient.sln
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SonarClient", "SonarClient\SonarClient.csproj", "{1DF21902-A886-4C02-AB59-B19CB291498D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{1DF21902-A886-4C02-AB59-B19CB291498D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1DF21902-A886-4C02-AB59-B19CB291498D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1DF21902-A886-4C02-AB59-B19CB291498D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1DF21902-A886-4C02-AB59-B19CB291498D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,93 @@
|
||||
using SonarClient.Repositories.Interfaces;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace SonarClient.Actions;
|
||||
|
||||
public class UpdatePermissionsAndTags(
|
||||
IProjectRepository projectRepository,
|
||||
IPermissionRepository permissionRepository,
|
||||
IProjectTagRepository projectTagRepository)
|
||||
{
|
||||
private Dictionary<string, string> PermissionTemplateMapping { get; set; } = new();
|
||||
|
||||
public async Task Execute(string organizationId, string organizationKey)
|
||||
{
|
||||
var projects = await projectRepository.GetProjects(organizationId);
|
||||
var permissionTemplates = await permissionRepository.GetPermissionTemplates(organizationKey);
|
||||
PermissionTemplateMapping = permissionTemplates.ToDictionary(x => x.Name, x => x.Id);
|
||||
|
||||
var projectTeams = new List<ProjectTeam>();
|
||||
|
||||
foreach (var project in projects)
|
||||
{
|
||||
if (project.Tags.Length == 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Project {project.Name} has zero tags[/]");
|
||||
continue;
|
||||
}
|
||||
|
||||
var oldTeam = project.Tags[0];
|
||||
|
||||
var newTeam = GetNewTeam(oldTeam);
|
||||
|
||||
if (newTeam == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
projectTeams.Add(new ProjectTeam(project.Key, oldTeam, newTeam));
|
||||
}
|
||||
|
||||
foreach (var teamProjects in projectTeams.GroupBy(p => p.OldTeam).ToDictionary(g => g.Key, g => g.ToList()))
|
||||
{
|
||||
AnsiConsole.MarkupLine($"Do you want to update permissions and tags for team: [yellow]{teamProjects.Key}[/] to [yellow]{GetNewTeam(teamProjects.Key)}[/]? It will be updated for the following projects:");
|
||||
foreach (var project in teamProjects.Value)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[green]{Markup.Escape($"- {project.ProjectKey}")}[/]");
|
||||
}
|
||||
|
||||
if (!await AnsiConsole.ConfirmAsync("Confirm"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var project in teamProjects.Value)
|
||||
{
|
||||
await permissionRepository.ApplyTemplateToProject(project.ProjectKey, GetPermissionTemplateName(project.NewTeam));
|
||||
await projectTagRepository.SetTags(project.ProjectKey, [project.NewTeam]);
|
||||
AnsiConsole.MarkupLine($"[green]Updated permissions and tags for project: [yellow]{project.ProjectKey}[/][/]");
|
||||
}
|
||||
AnsiConsole.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetNewTeam(string oldTeam)
|
||||
{
|
||||
return oldTeam switch
|
||||
{
|
||||
"lime" => "platform",
|
||||
"yellow" => "reporting",
|
||||
"orange" => "reporting",
|
||||
"red" => "surveying",
|
||||
"pink" => "surveying",
|
||||
"gray" => "surveying",
|
||||
"blue" => "data-and-ai",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private string GetPermissionTemplateName(string newTeam)
|
||||
{
|
||||
return newTeam switch
|
||||
{
|
||||
"platform" => PermissionTemplateMapping["Team Platform template"],
|
||||
"reporting" => PermissionTemplateMapping["Team Reporting template"],
|
||||
"surveying" => PermissionTemplateMapping["Team Surveying template"],
|
||||
"data-and-ai" => PermissionTemplateMapping["Team Data and AI template"],
|
||||
"managers" => PermissionTemplateMapping["Team Managers template"],
|
||||
_ => throw new IndexOutOfRangeException($"Unknown team: {newTeam}")
|
||||
};
|
||||
}
|
||||
|
||||
private record ProjectTeam(string ProjectKey, string OldTeam, string NewTeam);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace SonarClient.Constants;
|
||||
|
||||
public static class SonarConstants
|
||||
{
|
||||
public const string SonarApiV1ClientName = "SonarApiV1Client";
|
||||
public const string SonarApiV1BaseAddress = "https://sonarcloud.io/";
|
||||
|
||||
public const string SonarApiV2ClientName = "SonarApiV2Client";
|
||||
public const string SonarApiV2BaseAddress = "https://api.sonarcloud.io/";
|
||||
}
|
||||
8
ConsoleApps/SonarClient/SonarClient/Models/Link.cs
Normal file
8
ConsoleApps/SonarClient/SonarClient/Models/Link.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SonarClient.Models;
|
||||
|
||||
public class Link
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
9
ConsoleApps/SonarClient/SonarClient/Models/Paging.cs
Normal file
9
ConsoleApps/SonarClient/SonarClient/Models/Paging.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SonarClient.Models;
|
||||
|
||||
public class Paging
|
||||
{
|
||||
public int PageIndex { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int Total { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SonarClient.Models;
|
||||
|
||||
public class PermissionTemplate
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace SonarClient.Models;
|
||||
|
||||
public class PermissionTemplateResponse
|
||||
{
|
||||
public List<PermissionTemplate> PermissionTemplates { get; set; }
|
||||
}
|
||||
16
ConsoleApps/SonarClient/SonarClient/Models/Project.cs
Normal file
16
ConsoleApps/SonarClient/SonarClient/Models/Project.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace SonarClient.Models;
|
||||
|
||||
public class Project
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string LegacyId { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Visibility { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string[] Tags { get; set; }
|
||||
public Link[] Links { get; set; }
|
||||
public string CreatedAt { get; set; }
|
||||
public string UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SonarClient.Models;
|
||||
|
||||
public class ProjectsSearchResponse
|
||||
{
|
||||
public Paging Page { get; set; }
|
||||
public List<Project> Projects { get; set; }
|
||||
}
|
||||
59
ConsoleApps/SonarClient/SonarClient/Program.cs
Normal file
59
ConsoleApps/SonarClient/SonarClient/Program.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SonarClient.Actions;
|
||||
using SonarClient.Constants;
|
||||
using SonarClient.Repositories;
|
||||
using SonarClient.Repositories.Interfaces;
|
||||
using Spectre.Console;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
var builder = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json")
|
||||
.AddUserSecrets<Program>()
|
||||
.AddEnvironmentVariables();
|
||||
var configurationRoot = builder.Build();
|
||||
|
||||
var sonarConfiguration = configurationRoot.GetSection("sonar");
|
||||
|
||||
var token = sonarConfiguration["api_key"];
|
||||
var organizationId = sonarConfiguration["organization_id"];
|
||||
var organizationKey = sonarConfiguration["organization_key"];
|
||||
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddLogging(options =>
|
||||
{
|
||||
options.SetMinimumLevel(LogLevel.Warning);
|
||||
options.AddConsole();
|
||||
});
|
||||
|
||||
services.AddHttpClient(SonarConstants.SonarApiV1ClientName, c =>
|
||||
{
|
||||
c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{token}:")));
|
||||
c.BaseAddress = new Uri(SonarConstants.SonarApiV1BaseAddress);
|
||||
});
|
||||
services.AddHttpClient(SonarConstants.SonarApiV2ClientName, c =>
|
||||
{
|
||||
c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
c.BaseAddress = new Uri(SonarConstants.SonarApiV2BaseAddress);
|
||||
});
|
||||
|
||||
// Repositories
|
||||
services.AddTransient<IProjectRepository, ProjectRepository>();
|
||||
services.AddTransient<IPermissionRepository, PermissionRepository>();
|
||||
services.AddTransient<IProjectTagRepository, ProjectTagRepository>();
|
||||
|
||||
// Actions
|
||||
services.AddTransient<UpdatePermissionsAndTags>();
|
||||
|
||||
await using var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
var updatePermissionsAndTags = serviceProvider.GetRequiredService<UpdatePermissionsAndTags>();
|
||||
|
||||
AnsiConsole.Confirm("Are you sure you want to update permissions?", false);
|
||||
await updatePermissionsAndTags.Execute(organizationId, organizationKey);
|
||||
|
||||
Console.WriteLine("Press any key to exit...");
|
||||
Console.ReadKey();
|
||||
@@ -0,0 +1,9 @@
|
||||
using SonarClient.Models;
|
||||
|
||||
namespace SonarClient.Repositories.Interfaces;
|
||||
|
||||
public interface IPermissionRepository
|
||||
{
|
||||
Task<List<PermissionTemplate>> GetPermissionTemplates(string organizationKey);
|
||||
Task ApplyTemplateToProject(string projectKey, string templateId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using SonarClient.Models;
|
||||
|
||||
namespace SonarClient.Repositories.Interfaces;
|
||||
|
||||
public interface IProjectRepository
|
||||
{
|
||||
Task<List<Project>> GetProjects(string organizationId);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace SonarClient.Repositories.Interfaces;
|
||||
|
||||
public interface IProjectTagRepository
|
||||
{
|
||||
Task SetTags(string projectKey, string[] tags);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using SonarClient.Constants;
|
||||
using SonarClient.Models;
|
||||
using SonarClient.Repositories.Interfaces;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace SonarClient.Repositories;
|
||||
|
||||
public class PermissionRepository(IHttpClientFactory httpClientFactory) : IPermissionRepository
|
||||
{
|
||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(SonarConstants.SonarApiV1ClientName);
|
||||
|
||||
public async Task<List<PermissionTemplate>> GetPermissionTemplates(string organizationKey)
|
||||
{
|
||||
var permissionTemplateResponse = await _httpClient.GetFromJsonAsync<PermissionTemplateResponse>($"/api/permissions/search_templates?organization={organizationKey}");
|
||||
return permissionTemplateResponse.PermissionTemplates;
|
||||
}
|
||||
|
||||
public async Task ApplyTemplateToProject(string projectKey, string templateId)
|
||||
{
|
||||
var content = new FormUrlEncodedContent([
|
||||
new KeyValuePair<string, string>("projectKey", projectKey),
|
||||
new KeyValuePair<string, string>("templateId", templateId)
|
||||
]);
|
||||
|
||||
var response = await _httpClient.PostAsync("api/permissions/apply_template", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using SonarClient.Constants;
|
||||
using SonarClient.Models;
|
||||
using SonarClient.Repositories.Interfaces;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace SonarClient.Repositories;
|
||||
|
||||
public class ProjectRepository(IHttpClientFactory httpClientFactory) : IProjectRepository
|
||||
{
|
||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(SonarConstants.SonarApiV2ClientName);
|
||||
|
||||
public async Task<List<Project>> GetProjects(string organizationId)
|
||||
{
|
||||
var projects = new List<Project>();
|
||||
var currentPage = 1;
|
||||
ProjectsSearchResponse projectResponse;
|
||||
|
||||
do
|
||||
{
|
||||
projectResponse = await GetProjects(organizationId, currentPage);
|
||||
projects.AddRange(projectResponse.Projects);
|
||||
currentPage++;
|
||||
} while (projects.Count < projectResponse.Page.Total);
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
private async Task<ProjectsSearchResponse> GetProjects(string organizationId, int page)
|
||||
{
|
||||
return await _httpClient.GetFromJsonAsync<ProjectsSearchResponse>(
|
||||
$"/projects/projects?organizationIds={organizationId}&pageIndex={page}"
|
||||
) ?? throw new InvalidOperationException("Failed to retrieve projects from SonarCloud");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using SonarClient.Constants;
|
||||
using SonarClient.Repositories.Interfaces;
|
||||
|
||||
namespace SonarClient.Repositories;
|
||||
|
||||
public class ProjectTagRepository(IHttpClientFactory httpClientFactory) : IProjectTagRepository
|
||||
{
|
||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(SonarConstants.SonarApiV1ClientName);
|
||||
|
||||
public async Task SetTags(string projectKey, string[] tags)
|
||||
{
|
||||
var tagsCommaSeperated = string.Join(",", tags);
|
||||
|
||||
var content = new FormUrlEncodedContent([
|
||||
new KeyValuePair<string, string>("project", projectKey),
|
||||
new KeyValuePair<string, string>("tags", tagsCommaSeperated)
|
||||
]);
|
||||
|
||||
var response = await _httpClient.PostAsync("api/project_tags/set", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
24
ConsoleApps/SonarClient/SonarClient/SonarClient.csproj
Normal file
24
ConsoleApps/SonarClient/SonarClient/SonarClient.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<UserSecretsId>e28ed066-de2c-46f8-865f-4b0eda67be3c</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.53.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
7
ConsoleApps/SonarClient/SonarClient/appsettings.json
Normal file
7
ConsoleApps/SonarClient/SonarClient/appsettings.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"sonar": {
|
||||
"api_key": "",
|
||||
"organization_id": "aa1160b9-e4ab-4d2a-9ce5-bac8d9b5fb06",
|
||||
"organization_key": "effectory"
|
||||
}
|
||||
}
|
||||
44
ConsoleApps/SonarClient/SonarClient/sonar-client.md
Normal file
44
ConsoleApps/SonarClient/SonarClient/sonar-client.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# SonarClient Console Application
|
||||
|
||||
## Overview
|
||||
|
||||
SonarClient is a console application designed to interact with SonarQube/SonarCloud REST APIs. This tool
|
||||
provides functionality to manage, query, and analyze code quality metrics and security findings from SonarQube
|
||||
instances.
|
||||
|
||||
Depending on the operations, the application will reach out to v1 or v2 of the SonarQube API.
|
||||
|
||||
Documentation for the v1 API can be found here: https://sonarcloud.io/web_api
|
||||
Documentation for the v2 API can be found here: https://api-docs.sonarsource.com/
|
||||
|
||||
## Features
|
||||
|
||||
- **Update Permissions And Tags**: Update permissions and tags for all projects. Added for migration to new team structure.
|
||||
|
||||
## Operations
|
||||
|
||||
- Retrieve all projects
|
||||
- Set tags for a project
|
||||
- Apply permission template on a project
|
||||
|
||||
## Usage
|
||||
|
||||
1. Retrieve a personal token from SonarCloud:
|
||||
- Go to: https://sonarcloud.io/account/security
|
||||
- Enter token name
|
||||
- Click `Generate Token`
|
||||
- Copy token
|
||||
2. In Rider
|
||||
- Right-click on the `SonarClient` project
|
||||
- Select `Tools > .NET User Secrets`
|
||||
- This will open `secrets.json`
|
||||
- Add the following:
|
||||
```json
|
||||
{
|
||||
"sonar": {
|
||||
"api_key": "[add token here]"
|
||||
}
|
||||
}`
|
||||
3. Run the application
|
||||
4. When done revoke the token
|
||||
|
||||
Reference in New Issue
Block a user