diff --git a/src/Balsam/.openapi-generator/VERSION b/src/Balsam/.openapi-generator/VERSION
index 40e3636..ecb2186 100644
--- a/src/Balsam/.openapi-generator/VERSION
+++ b/src/Balsam/.openapi-generator/VERSION
@@ -1 +1 @@
-7.1.0-SNAPSHOT
\ No newline at end of file
+7.6.0-SNAPSHOT
diff --git a/src/Balsam/Balsam.sln b/src/Balsam/Balsam.sln
index 824ba16..6a41dd8 100644
--- a/src/Balsam/Balsam.sln
+++ b/src/Balsam/Balsam.sln
@@ -23,6 +23,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RocketChatChatProviderApiCl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BalsamApi.Server", "src\BalsamApi.Server\BalsamApi.Server.csproj", "{CCE90D11-C8D4-4F76-9730-506B23B1CD9E}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Balsam.Application", "src\Balsam.Application\Balsam.Application.csproj", "{AAAA20E8-E584-401D-A7B4-6741E1316301}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Balsam.Domain", "src\Balsam.Domain\Balsam.Domain.csproj", "{AE263A3F-7988-4491-8339-89899DF6E08F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Balsam.Infrastructure", "src\Balsam.Infrastructure\Balsam.Infrastructure.csproj", "{98CB6D20-3EDC-4AA5-A477-83835FCD3EC7}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Balsam.Tests", "src\Balsam.Tests\Balsam.Tests.csproj", "{FEE2F533-5550-4F4C-9FCB-A3695018307E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -69,6 +77,22 @@ Global
{CCE90D11-C8D4-4F76-9730-506B23B1CD9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCE90D11-C8D4-4F76-9730-506B23B1CD9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCE90D11-C8D4-4F76-9730-506B23B1CD9E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AAAA20E8-E584-401D-A7B4-6741E1316301}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AAAA20E8-E584-401D-A7B4-6741E1316301}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AAAA20E8-E584-401D-A7B4-6741E1316301}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AAAA20E8-E584-401D-A7B4-6741E1316301}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE263A3F-7988-4491-8339-89899DF6E08F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE263A3F-7988-4491-8339-89899DF6E08F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE263A3F-7988-4491-8339-89899DF6E08F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE263A3F-7988-4491-8339-89899DF6E08F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {98CB6D20-3EDC-4AA5-A477-83835FCD3EC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {98CB6D20-3EDC-4AA5-A477-83835FCD3EC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {98CB6D20-3EDC-4AA5-A477-83835FCD3EC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {98CB6D20-3EDC-4AA5-A477-83835FCD3EC7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FEE2F533-5550-4F4C-9FCB-A3695018307E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FEE2F533-5550-4F4C-9FCB-A3695018307E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FEE2F533-5550-4F4C-9FCB-A3695018307E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FEE2F533-5550-4F4C-9FCB-A3695018307E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Balsam/BalsamApi.Server.sln b/src/Balsam/BalsamApi.Server.sln
index 0791abd..e516930 100644
--- a/src/Balsam/BalsamApi.Server.sln
+++ b/src/Balsam/BalsamApi.Server.sln
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27428.2043
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BalsamApi.Server", "src\BalsamApi.Server\BalsamApi.Server.csproj", "{C7291B74-4521-4C9F-B7B4-92A1B686E169}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BalsamApi.Server", "src\BalsamApi.Server\BalsamApi.Server.csproj", "{5F5771EB-9749-4B0E-AB6C-028564A70FD6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,10 +11,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {C7291B74-4521-4C9F-B7B4-92A1B686E169}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C7291B74-4521-4C9F-B7B4-92A1B686E169}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C7291B74-4521-4C9F-B7B4-92A1B686E169}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C7291B74-4521-4C9F-B7B4-92A1B686E169}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5F5771EB-9749-4B0E-AB6C-028564A70FD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5F5771EB-9749-4B0E-AB6C-028564A70FD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5F5771EB-9749-4B0E-AB6C-028564A70FD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5F5771EB-9749-4B0E-AB6C-028564A70FD6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Balsam/README.md b/src/Balsam/README.md
index bf4f7b2..8a29fd4 100644
--- a/src/Balsam/README.md
+++ b/src/Balsam/README.md
@@ -6,6 +6,7 @@ This C# SDK is automatically generated by the [OpenAPI Generator](https://openap
- API version: 2.0
- SDK version: 1.0.0
+- Generator version: 7.6.0-SNAPSHOT
- Build package: org.openapitools.codegen.languages.CSharpClientCodegen
diff --git a/src/Balsam/docs/GroupApi.md b/src/Balsam/docs/GroupApi.md
index 3f000e1..9585a3e 100644
--- a/src/Balsam/docs/GroupApi.md
+++ b/src/Balsam/docs/GroupApi.md
@@ -6,6 +6,7 @@ All URIs are relative to *http://oidc-provider.balsam-system.svc.cluster.local/a
|--------|--------------|-------------|
| [**AddUserToGroup**](GroupApi.md#addusertogroup) | **POST** /groups/{groupId}/users | |
| [**CreateGroup**](GroupApi.md#creategroup) | **POST** /groups | |
+| [**DeleteGroup**](GroupApi.md#deletegroup) | **DELETE** /groups/{groupId} | |
# **AddUserToGroup**
@@ -185,3 +186,89 @@ No authorization required
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
+# **DeleteGroup**
+> void DeleteGroup (string groupId)
+
+
+
+Deletes a user group
+
+### Example
+```csharp
+using System.Collections.Generic;
+using System.Diagnostics;
+using OidcProviderApiClient.Api;
+using OidcProviderApiClient.Client;
+using OidcProviderApiClient.Model;
+
+namespace Example
+{
+ public class DeleteGroupExample
+ {
+ public static void Main()
+ {
+ Configuration config = new Configuration();
+ config.BasePath = "http://oidc-provider.balsam-system.svc.cluster.local/api/v1";
+ var apiInstance = new GroupApi(config);
+ var groupId = "groupId_example"; // string | The id for the group
+
+ try
+ {
+ apiInstance.DeleteGroup(groupId);
+ }
+ catch (ApiException e)
+ {
+ Debug.Print("Exception when calling GroupApi.DeleteGroup: " + e.Message);
+ Debug.Print("Status Code: " + e.ErrorCode);
+ Debug.Print(e.StackTrace);
+ }
+ }
+ }
+}
+```
+
+#### Using the DeleteGroupWithHttpInfo variant
+This returns an ApiResponse object which contains the response data, status code and headers.
+
+```csharp
+try
+{
+ apiInstance.DeleteGroupWithHttpInfo(groupId);
+}
+catch (ApiException e)
+{
+ Debug.Print("Exception when calling GroupApi.DeleteGroupWithHttpInfo: " + e.Message);
+ Debug.Print("Status Code: " + e.ErrorCode);
+ Debug.Print(e.StackTrace);
+}
+```
+
+### Parameters
+
+| Name | Type | Description | Notes |
+|------|------|-------------|-------|
+| **groupId** | **string** | The id for the group | |
+
+### Return type
+
+void (empty response body)
+
+### Authorization
+
+No authorization required
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/problem+json
+
+
+### HTTP response details
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+| **200** | Success | - |
+| **400** | Error respsone for 400 | - |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
diff --git a/src/Balsam/src/Balsam.Api/Balsam.Api.csproj b/src/Balsam/src/Balsam.Api/Balsam.Api.csproj
index 7c0227b..2165108 100644
--- a/src/Balsam/src/Balsam.Api/Balsam.Api.csproj
+++ b/src/Balsam/src/Balsam.Api/Balsam.Api.csproj
@@ -1,4 +1,4 @@
-
+
net6.0
@@ -9,22 +9,17 @@
-
-
-
-
+
+
+
-
-
-
-
diff --git a/src/Balsam/src/Balsam.Api/Models/CapabilityOptions.cs b/src/Balsam/src/Balsam.Api/Configuration/ConfigureCapabilityOptions.cs
similarity index 55%
rename from src/Balsam/src/Balsam.Api/Models/CapabilityOptions.cs
rename to src/Balsam/src/Balsam.Api/Configuration/ConfigureCapabilityOptions.cs
index 69081f3..ce10757 100644
--- a/src/Balsam/src/Balsam.Api/Models/CapabilityOptions.cs
+++ b/src/Balsam/src/Balsam.Api/Configuration/ConfigureCapabilityOptions.cs
@@ -1,39 +1,24 @@
-using Microsoft.Extensions.Options;
-
-namespace Balsam.Api.Models
-{
- public class CapabilityOptions
- {
- public bool Enabled { get; set; }
- public string? ServiceLocation { get; set; }
- public string[]? Actions { get; set; }
- }
-
- public class Capabilities
- {
- public const string Git = "Git";
- public const string S3 = "S3";
- public const string Authentication = "Authentication";
- public const string Chat = "Chat";
- }
-
- public class ConfigureCapabilityOptions : IConfigureNamedOptions
- {
-
- public ConfigureCapabilityOptions()
- {
-
- }
-
- public void Configure(string name, CapabilityOptions options)
- {
- //options.ClientId = this.decrypt.Decrypt(options.ClientId);
- //options.ClientSecret = this.decrypt.Decrypt(options.ClientSecret);
- }
-
- public void Configure(CapabilityOptions options)
- {
- Configure(Options.DefaultName, options);
- }
- }
-}
+using Microsoft.Extensions.Options;
+
+namespace Balsam.Model
+{
+ public class ConfigureCapabilityOptions : IConfigureNamedOptions
+ {
+
+ public ConfigureCapabilityOptions()
+ {
+
+ }
+
+ public void Configure(string name, CapabilityOptions options)
+ {
+ //options.ClientId = this.decrypt.Decrypt(options.ClientId);
+ //options.ClientSecret = this.decrypt.Decrypt(options.ClientSecret);
+ }
+
+ public void Configure(CapabilityOptions options)
+ {
+ Configure(Options.DefaultName, options);
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Controllers/KnowledgeLibraryController.cs b/src/Balsam/src/Balsam.Api/Controllers/KnowledgeLibraryController.cs
index 6003c19..5e9058c 100644
--- a/src/Balsam/src/Balsam.Api/Controllers/KnowledgeLibraryController.cs
+++ b/src/Balsam/src/Balsam.Api/Controllers/KnowledgeLibraryController.cs
@@ -1,6 +1,8 @@
-using BalsamApi.Server.Models;
+using Balsam.Api.Extensions;
+using Balsam.Interfaces;
+using Balsam.Model;
+using BalsamApi.Server.Models;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Options;
using System.ComponentModel.DataAnnotations;
namespace Balsam.Api.Controllers
@@ -9,25 +11,25 @@ namespace Balsam.Api.Controllers
[ApiController]
public class KnowledgeLibraryController : BalsamApi.Server.Controllers.KnowledgeLibraryApiController
{
- private readonly HubClient _hubClient;
private readonly ILogger _logger;
- private readonly KnowledgeLibraryClient _knowledgeLibraryClient;
+ private readonly IKnowledgeLibraryService _knowledgeLibraryService;
- public KnowledgeLibraryController(ILogger logger, HubClient hubClient, KnowledgeLibraryClient knowledgeLibraryClient)
+ public KnowledgeLibraryController(ILogger logger, IKnowledgeLibraryService knowledgeLibraryService)
{
- _hubClient = hubClient;
_logger = logger;
- _knowledgeLibraryClient = knowledgeLibraryClient;
+ _knowledgeLibraryService = knowledgeLibraryService;
}
public async override Task ListKnowledgeLibaries() //A. Implementera
{
_logger.LogInformation("calling endpoint: Listing Knowledgelibraries");
try
{
- var knowledgeLibraries = await _hubClient.ListKnowledgeLibraries();
+ var balsamKnowledgeLibraries = await _knowledgeLibraryService.ListKnowledgeLibraries();
+
+ var knowledgeLibraries = balsamKnowledgeLibraries.Select(bkl => bkl.ToKnowledgeLibrary());
return Ok(knowledgeLibraries);
}
- catch(Exception ex)
+ catch (Exception ex)
{
_logger.LogError(ex, "Error listing knowledgelibraries");
return BadRequest(ex);
@@ -38,13 +40,16 @@ public async override Task GetKnowledgeLibraryFileContent([FromRo
{
try
{
+ //TODO: Pull knowledge library repo
+
+ //TODO: Move stream to knowledgeLibraryService?
string filePath = string.Empty;
- filePath = _knowledgeLibraryClient.GetRepositoryFilePath(libraryId, fileId);
+ filePath = _knowledgeLibraryService.GetRepositoryFilePath(libraryId, fileId);
// Open the file
var stream = System.IO.File.OpenRead(filePath);
-
+
// Determine the content type
var provider = new Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider();
if (!provider.TryGetContentType(fileId, out var contentType))
@@ -69,16 +74,12 @@ public async override Task ListKnowledgeLibraryFiles([FromRoute(N
{
try
{
- var knowledgeLibrary = (await _hubClient.ListKnowledgeLibraries()).FirstOrDefault(kb => kb.Id == libraryId);
+ var balsamRepofiles = await _knowledgeLibraryService.GetRepositoryFileTree(libraryId);
+ var repoFiles = balsamRepofiles.Select(rf => rf.ToRepoFile());
- if (knowledgeLibrary is null)
- {
- return BadRequest(new Problem() { Status = 404, Title = "Knowledge library not found", Detail = "Knowledge library not found" });
- }
-
- var contents = _knowledgeLibraryClient.GetRepositoryContent(libraryId, knowledgeLibrary.RepositoryUrl);
- return Ok(contents.ToArray());
- } catch (Exception ex)
+ return Ok(repoFiles.ToArray());
+ }
+ catch (Exception ex)
{
_logger.LogError(ex, "Error listing knowledge library files");
return BadRequest(new Problem() { Status = 404, Title = "Error fetching knowledge libraries", Detail = "Error fetching knowledge libraries" });
diff --git a/src/Balsam/src/Balsam.Api/Controllers/ProjectController.cs b/src/Balsam/src/Balsam.Api/Controllers/ProjectController.cs
index db9a846..156e609 100644
--- a/src/Balsam/src/Balsam.Api/Controllers/ProjectController.cs
+++ b/src/Balsam/src/Balsam.Api/Controllers/ProjectController.cs
@@ -1,11 +1,14 @@
-using Balsam.Api.Models;
-using BalsamApi.Server.Models;
+using BalsamApi.Server.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
-using LibGit2Sharp;
using GitProviderApiClient.Api;
+using Balsam.Model;
+using Balsam.Interfaces;
+using Balsam.Application.Authorization;
+using System.Net;
+using Balsam.Api.Extensions;
namespace Balsam.Api.Controllers
{
@@ -15,84 +18,99 @@ namespace Balsam.Api.Controllers
[Authorize]
public class ProjectController : BalsamApi.Server.Controllers.ProjectApiController
{
- private readonly HubClient _hubClient;
+ private readonly IProjectService _projectService;
private readonly ILogger _logger;
- private readonly KnowledgeLibraryClient _knowledgeLibraryClient;
+ private readonly IKnowledgeLibraryService _knowledgeLibraryService;
private readonly IRepositoryApi _repositoryApi;
+ private readonly ProjectAuthorization _projectAuthorization;
-
- public ProjectController(IOptionsSnapshot capabilityOptions, ILogger logger, HubClient hubClient, KnowledgeLibraryClient knowledgeLibraryClient, IRepositoryApi reposiotryApi)
+ public ProjectController(IOptionsSnapshot capabilityOptions,
+ ILogger logger,
+ IProjectService projectService,
+ IKnowledgeLibraryService knowledgeLibraryService,
+ IRepositoryApi reposiotryApi)
{
- _hubClient = hubClient;
+ _projectService = projectService;
_logger = logger;
capabilityOptions.Get(Capabilities.Git);
capabilityOptions.Get(Capabilities.Authentication);
- _knowledgeLibraryClient = knowledgeLibraryClient;
+ _knowledgeLibraryService = knowledgeLibraryService;
_repositoryApi = reposiotryApi;
+ _projectAuthorization = new ProjectAuthorization(); //TODO: Use interface
}
public async override Task CreateBranch([FromRoute(Name = "projectId"), Required] string projectId, [FromBody] CreateBranchRequest? createBranchRequest)
{
if (createBranchRequest is null)
{
- return BadRequest(new Problem() { Status = 400, Title = "Parameter error", Detail = "Missing parameters" });
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Title = "Parameter error", Detail = "Missing parameters" });
}
- BranchCreatedResponse branchCreatedResponse;
try
{
- var branch = await _hubClient.CreateBranch(projectId, createBranchRequest.FromBranch, createBranchRequest.Name, createBranchRequest.Description);
+ var project = await _projectService.GetProject(projectId);
+
+ if (!_projectAuthorization.CanUserCreateBranch(this.User, project))
+ {
+ var username = this.User.GetUserName();
+ _logger.LogInformation($"User {username} not authorized to create branch on project {project.Id}");
+ return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Unauthorized", Title = "User cannot create branch" });
+ }
+
+ var branch = await _projectService.CreateBranch(projectId, createBranchRequest.FromBranch, createBranchRequest.Name, createBranchRequest.Description ?? "");
if (branch == null)
{
- return BadRequest(new Problem() { Status = 400, Title = "Could not create branch", Detail = "Branch could not be created" });
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Title = "Could not create branch", Detail = "Branch could not be created" });
}
- branchCreatedResponse = new BranchCreatedResponse
+ var branchCreatedResponse = new BranchCreatedResponse
{
Id = branch.Id,
Name = branch.Name,
ProjectId = projectId
};
+
+ return Ok(branchCreatedResponse);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not create branch");
- return BadRequest(new Problem() { Status = 400, Title = "Could not create branch", Detail = "Branch could not be created" });
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Title = "Could not create branch", Detail = "Branch could not be created" });
}
-
- return Ok(branchCreatedResponse);
}
-
public async override Task CreateProject([FromBody] CreateProjectRequest? createProjectRequest)
{
if (createProjectRequest is null)
{
- return BadRequest(new Problem() { Title = "Parameters missing", Status = 400, Type = "Missing parameters" });
+ return BadRequest(new Problem() { Title = "Parameters missing", Status = (int)HttpStatusCode.BadRequest, Type = "Missing parameters" });
}
- _logger.LogInformation("Reading user information");
- var username = this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value;
- _logger.LogInformation($"The user is {username}");
+
+ bool nameNotUnique = await _projectService.ProjectExists(createProjectRequest.Name);
+
+ if (nameNotUnique)
+ {
+ return BadRequest(new Problem() { Title = "Project with that name already exists", Status = (int)HttpStatusCode.BadRequest, Type = "Project duplication" });
+ }
+
try
{
- BalsamProject? project = await _hubClient.CreateProject(createProjectRequest.Name, createProjectRequest.Description, createProjectRequest.BranchName, username, createProjectRequest.SourceLocation);
+ var username = this.User.GetUserName();
- if (project == null)
- {
- return BadRequest(new Problem() { Title = "Project with that name already exists", Status = 400, Type = "Project duplication" });
- }
+ BalsamProject? project = await _projectService.CreateProject(createProjectRequest.Name, createProjectRequest.Description, createProjectRequest.BranchName, username, createProjectRequest.SourceLocation);
- var evt = new ProjectCreatedResponse();
- evt.Id = project.Id;
- evt.Name = project.Name;
+ var response = new ProjectCreatedResponse();
+ response.Id = project.Id;
+ response.Name = project.Name;
+ //TODO: Set AuthGroup?
- return Ok(evt);
+ return Ok(response);
}
catch (Exception ex)
{
- _logger.LogError("Could not create project", ex);
- return BadRequest(new Problem() { Title = "Internal error", Status = 400, Type = "Internal error" });
+ _logger.LogError(ex, "Could not create project");
+ return BadRequest(new Problem() { Title = "Internal error", Status = (int)HttpStatusCode.InternalServerError, Type = "Internal error" });
}
}
@@ -100,140 +118,121 @@ public async override Task GetFiles([FromRoute(Name = "projectId"
{
try
{
- var files = await _hubClient.GetGitBranchFiles(projectId, branchId);
- if (files is null)
- {
- return BadRequest(new Problem() { Status = 400, Type = "Fetch problem", Title = "Could not fetch files for repository branch" });
- }
- return Ok(files.Select(f => new BalsamApi.Server.Models.RepoFile()
- {
- Name = f.Name,
- Path = f.Path,
- Type = f.Type == GitProviderApiClient.Model.RepoFile.TypeEnum.File ? BalsamApi.Server.Models.RepoFile.TypeEnum.FileEnum : BalsamApi.Server.Models.RepoFile.TypeEnum.FolderEnum,
- ContentUrl = f.ContentUrl,
- Id = f.Id
- }).ToArray());
+ var files = await _projectService.GetGitBranchFiles(projectId, branchId);
+
+ return Ok(files.Select(f => f.ToRepoFile()).ToArray());
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not fetch files");
}
- return BadRequest(new Problem() { Status = 400, Type = "Fetch problem", Title = "Could not fetch files for repository branch" });
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Type = "Fetch problem", Title = "Could not fetch files for repository branch" });
}
-
public async override Task GetProject([FromRoute(Name = "projectId"), Required] string projectId)
{
try
{
- var balsamProject = await _hubClient.GetProject(projectId);
-
- if (balsamProject is null)
- {
- return BadRequest(new Problem() { Title = "Project with given id can not be found", Status = 400, Type = "Can not find project" });
- }
+ var project = await _projectService.GetProject(projectId);
- var evt = new ProjectResponse();
- evt.Id = balsamProject.Id;
- evt.Name = balsamProject.Name;
- evt.Description = balsamProject.Description;
- evt.GitUrl = balsamProject.Git is null ? "" : balsamProject.Git.Path;
- evt.Branches = balsamProject.Branches.Select(b => new BalsamApi.Server.Models.Branch() { Id = b.Id, Description = b.Description, Name = b.Name, IsDefault = b.IsDefault }).ToList();
- evt.AuthGroup = balsamProject.Oidc.GroupName;
+ ProjectResponse response = project.ToProjectResponse();
- return Ok(evt);
+ return Ok(response);
}
catch (Exception ex)
{
- _logger.LogError("", ex);
- return BadRequest(new Problem() { Title = "Project can not be loaded", Status = 400, Type = "Can not load project" });
+ _logger.LogError(ex, "Could not get project");
+ return BadRequest(new Problem() { Title = "Project can not be loaded", Status = (int)HttpStatusCode.BadRequest, Type = "Can not load project" });
}
}
-
public override async Task ListProjects([FromQuery(Name = "all")] bool? all)
{
var listAll = all ?? true;
_logger.LogDebug($"Hit ListProject viewAll={listAll}", listAll);
- var projects = await _hubClient.GetProjects();
+ var projects = await _projectService.GetProjects();
if (!listAll)
{
- var userGroups = User.Claims.Where(x => x.Type == "groups");
+ var userGroups = User.GetGroups();
projects = projects.Where(x => userGroups.Any(o => string.Equals(o.Value, x.Oidc?.GroupName, StringComparison.OrdinalIgnoreCase))).ToList();
}
var projectListResponse = new ProjectListResponse
{
- Projects = MapProject(projects)
+ Projects = projects.Select(project => project.ToProject())
+ .OrderBy(p => p.Name)
+ .ToList()
};
return Ok(projectListResponse);
}
- private List MapProject(List projects)
- {
- return projects.Select(project => new Project()
- {
- Id = project.Id,
- Name = project.Name,
- Description = project.Description,
- Branches = MapBranches(project.Branches),
- AuthGroup = project.Oidc.GroupName,
- GitUrl = project.Git?.Path
- }).OrderBy(p => p.Name).ToList();
- }
- private List MapBranches(List branches)
+ public async override Task GetFile([FromRoute(Name = "projectId"), Required] string projectId, [FromRoute(Name = "branchId"), Required] string branchId, [FromRoute(Name = "fileId"), Required] string fileId)
{
- return branches.Select(branch => new BalsamApi.Server.Models.Branch()
+ try
{
- Id = branch.Id,
- Description = branch.Description,
- Name = branch.Name,
- IsDefault = branch.IsDefault
- }).ToList();
- }
+ //TODO: Authorize get content?
+
+ var fileContent = await _projectService.GetFile(projectId, branchId, fileId);
- public async override Task GetFile([FromRoute(Name = "projectId"), Required] string projectId, [FromRoute(Name = "branchId"), Required] string branchId, [FromRoute(Name = "fileId"), Required] string fileId)
- {
- var file = await _hubClient.GetFile(projectId, branchId, fileId);
+ if (fileContent.Content != null)
+ {
+ Response.Headers.Add("content-disposition", "inline");
- if (file != null)
- {
- Response.Headers.Add("content-disposition", "inline");
- return file;
+ var fileContentResult = new FileContentResult(fileContent.Content, fileContent.Mediatype);
+ return fileContentResult;
+ }
+ else
+ {
+ var errorMessage = "Could not read file content";
+ _logger.LogError(errorMessage);
+ return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Type = "File read error", Detail = errorMessage });
+ }
}
+ catch (Exception ex)
+ {
+ var errorMessage = "Could not get file";
+ _logger.LogError(ex, errorMessage);
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Type = "Get file error", Detail = errorMessage });
- return BadRequest(new Problem() { Status = 404, Type = "file not found", Detail = "Can not find the file" });
-
+ }
}
public async override Task DeleteBranch([FromRoute(Name = "projectId"), Required] string projectId, [FromRoute(Name = "branchId"), Required] string branchId)
{
try
{
- var project = await _hubClient.GetProject(projectId);
- var branch = await _hubClient.GetBranch(projectId, branchId);
-
- if (project is null || branch is null)
+ var project = await _projectService.GetProject(projectId);
+
+ if (project is null)
{
- return BadRequest(new Problem() { Status = 404, Type = "Project/branch not found", Detail = "Can not find the project/branch" });
+ return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Type = "Project not found", Detail = "Can not find the project" });
+ }
+ var branch = project.Branches.First(b => b.Id == branchId);
+
+ if (project is null)
+ {
+ return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Type = "Branch not found", Detail = "Can not find the branch" });
}
- else if (User.Claims.FirstOrDefault(x => x.Type == "groups" && x.Value == project.Oidc?.GroupName) is null)
+
+ if (!_projectAuthorization.CanUserDeleteBranch(this.User, project))
{
- return Unauthorized(new Problem() { Status = 401, Type = "Unauthorized", Detail = "User is not authorized to delete the branch" });
+ var username = this.User.GetUserName();
+ _logger.LogInformation($"User {username} not authorized to delete branch {branch.Id} on project {project.Id}");
+ return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Unauthorized", Detail = "User is not authorized to delete the branch" });
}
- await _hubClient.DeleteBranch(projectId, branchId);
+ await _projectService.DeleteBranch(projectId, branchId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not delete branch");
- return BadRequest(new Problem() { Status = 400, Type = "Could not delete branch", Detail = "Could not delete branch, internal error" });
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Type = "Could not delete branch", Detail = "Could not delete branch, internal error" });
}
return Ok();
@@ -243,23 +242,26 @@ public async override Task DeleteProject([FromRoute(Name = "proje
{
try
{
- var project = await _hubClient.GetProject(projectId);
+ var project = await _projectService.GetProject(projectId);
- if (project is null )
+ if (project is null)
{
- return BadRequest(new Problem() { Status = 404, Type = "Project not found", Detail = "Can not find the project" });
-
- } else if (User.Claims.FirstOrDefault(x => x.Type == "groups" && x.Value == project.Oidc?.GroupName) is null)
+ return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Type = "Project not found", Detail = "Can not find the project" });
+
+ }
+ else if (!_projectAuthorization.CanUserDeleteProject(this.User, project))
{
- return Unauthorized(new Problem() { Status = 401, Type = "Unauthorized", Detail = "User is not authorized to delete the project" });
+ var username = this.User.GetUserName();
+ _logger.LogInformation($"User {username} not authorized to delete project {project.Id}");
+ return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Unauthorized", Detail = "User is not authorized to delete the project" });
}
- await _hubClient.DeleteProject(projectId);
+ await _projectService.DeleteProject(projectId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not delete project");
- return BadRequest(new Problem() { Status = 400, Type = "Could not delete project", Detail = "Could not delete project, internal error" });
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Type = "Could not delete project", Detail = "Could not delete project, internal error" });
}
return Ok();
@@ -269,34 +271,37 @@ public async override Task CopyFromKnowleadgeLibrary([FromRoute(N
{
try
{
- var project = await _hubClient.GetProject(projectId);
- var branch = await _hubClient.GetBranch(projectId, branchId);
- if (project is null || branch is null)
+ var project = await _projectService.GetProject(projectId);
+
+ if (project is null)
+ {
+ return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Type = "Project not found", Detail = "Can not find the project" });
+ }
+
+ if (!_projectAuthorization.CanUserEditBranch(this.User, project))
{
- return BadRequest(new Problem() { Status = 404, Type = "Project/branch not found", Detail = "Can not find the project/branch" });
+ var username = this.User.GetUserName();
+ _logger.LogInformation($"User {username} not authorized to edit branch {branchId} on project {project.Id}");
+ return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Unauthorized", Detail = "User is not authorized to edit project" });
}
- var knowledgeLibrary = (await _hubClient.ListKnowledgeLibraries()).FirstOrDefault(kb => kb.Id == libraryId);
+ var knowledgeLibrary = _knowledgeLibraryService.GetKnowledgeLibrary(libraryId);
if (knowledgeLibrary is null)
{
- return BadRequest(new Problem() { Status = 404, Title = "Knowledge library not found", Detail = "Knowledge library not found" });
+ return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Title = "Knowledge library not found", Detail = "Knowledge library not found" });
}
- var zipFile = _knowledgeLibraryClient.GetZippedResource(libraryId, knowledgeLibrary.RepositoryUrl, fileId);
-
- var stream = System.IO.File.OpenRead(zipFile);
- await _repositoryApi.AddResourceFilesAsync(project.Git?.Id??"", branch.GitBranch, stream);
- stream.Close();
- System.IO.File.Delete(zipFile);
-
+ await _projectService.CopyFromKnowledgeLibrary(project.Id, branchId, libraryId, fileId);
+
_logger.LogInformation("File/directory copied from knowledge library");
+
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not copy file/directory");
- return BadRequest(new Problem() { Status = 400, Title = "Could not copy file/directory", Detail = "Could not copy file/directory" });
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Title = "Could not copy file/directory", Detail = "Could not copy file/directory" });
}
}
}
diff --git a/src/Balsam/src/Balsam.Api/Controllers/WorkspaceController.cs b/src/Balsam/src/Balsam.Api/Controllers/WorkspaceController.cs
index 6fccde8..6b811d9 100644
--- a/src/Balsam/src/Balsam.Api/Controllers/WorkspaceController.cs
+++ b/src/Balsam/src/Balsam.Api/Controllers/WorkspaceController.cs
@@ -1,9 +1,12 @@
-using Balsam.Api.Models;
+using Balsam.Api.Extensions;
+using Balsam.Application.Authorization;
+using Balsam.Interfaces;
+using Balsam.Model;
using BalsamApi.Server.Models;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
+using System.Net;
namespace Balsam.Api.Controllers
{
@@ -13,48 +16,54 @@ namespace Balsam.Api.Controllers
public class WorkspaceController : BalsamApi.Server.Controllers.WorkspaceApiController
{
private readonly ILogger _logger;
- private readonly HubClient _hubClient;
+ private readonly IProjectService _projectService;
+ private readonly IWorkspaceService _workspaceService;
+ private readonly WorkspaceAuthorization _workspaceAuthorization;
- public WorkspaceController(ILogger logger, HubClient hubClient)
+
+ public WorkspaceController(ILogger logger, IProjectService projectService, IWorkspaceService workspaceService)
{
_logger = logger;
- _hubClient = hubClient;
+ _projectService = projectService;
+ _workspaceService = workspaceService;
+ _workspaceAuthorization = new WorkspaceAuthorization(); //TODO: Use interface
}
public async override Task CreateWorkspace([FromBody] CreateWorkspaceRequest? createWorkspaceRequest)
{
if (createWorkspaceRequest is null)
{
- return BadRequest(new Problem() { Status = 400, Title = "Missing parameters", Detail = "Missing input parameter(s)" });
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Title = "Missing parameters", Detail = "Missing input parameter(s)" });
}
- var username = this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value;
- var mail = this.User.Claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value;
- var project = await _hubClient.GetProject(createWorkspaceRequest.ProjectId);
- if (this.User.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName))
+ try
{
- try
- {
- var workspace = await _hubClient.CreateWorkspace(createWorkspaceRequest.ProjectId, createWorkspaceRequest.BranchId, createWorkspaceRequest.Name, createWorkspaceRequest.TemplateId, username, mail);
+ var project = await _projectService.GetProject(createWorkspaceRequest.ProjectId);
- if (workspace == null)
- {
- return BadRequest(new Problem() { Status = 400, Type = "Could not create workspace", Title = "Could not create workspace due to error" });
- }
+ var username = this.User.GetUserName();
+ var mail = this.User.GetEmail();
- return Ok(new WorkspaceCreatedResponse() { Id = workspace.Id, Name = workspace.Name, ProjectId = createWorkspaceRequest.ProjectId, BranchId = createWorkspaceRequest.BranchId, Url = workspace.Url });
+ if (!_workspaceAuthorization.CanUserCreateWorkspace(this.User, project))
+ {
+ _logger.LogInformation($"User {username} not authorized to crate workspace in project {project.Id}.");
+ return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Unauthorized", Title = "User cannot create workspace" });
}
- catch (Exception ex)
+
+ var workspace = await _workspaceService.CreateWorkspace(createWorkspaceRequest.ProjectId, createWorkspaceRequest.BranchId, createWorkspaceRequest.Name, createWorkspaceRequest.TemplateId, username, mail);
+
+ if (workspace == null)
{
- _logger.LogError(ex, "Could not create workspace");
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Type = "Could not create workspace", Title = "Could not create workspace due to error" });
}
+
+ return Ok(new WorkspaceCreatedResponse() { Id = workspace.Id, Name = workspace.Name, ProjectId = createWorkspaceRequest.ProjectId, BranchId = createWorkspaceRequest.BranchId, Url = workspace.Url });
}
- else
+ catch (Exception ex)
{
- return BadRequest(new Problem() { Status = 403, Type = "User not in projectgroup", Title = "Internal error when created error" });
+ _logger.LogError(ex, "Could not create workspace");
}
- return BadRequest(new Problem() { Status = 400, Type = "Could not create workspace", Title = "Internal error when created error" });
-
+
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Type = "Could not create workspace", Title = "Internal error when created error" });
}
@@ -62,83 +71,95 @@ public async override Task DeleteWorkspace([FromRoute(Name = "wor
{
if (workspaceId is null)
{
- return BadRequest(new Problem() { Status = 400, Title = "Missing parameters", Detail = "Missing input parameter(s)" });
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Title = "Missing parameters", Detail = "Missing input parameter(s)" });
}
- var userName = this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value;
+
try
{
- var workspace = await _hubClient.DeleteWorkspace(projectId, branchId, workspaceId, userName);
+ var userName = this.User.GetUserName();
+ var project = await _projectService.GetProject(projectId);
+ var workspace = await _workspaceService.GetWorkspace(projectId, userName, workspaceId);
- if (workspace == null)
+ if (!_workspaceAuthorization.CanUserDeleteWorkspace(this.User, project, workspace))
{
- return BadRequest(new Problem() { Status = 400, Type = "Could not delete workspace", Title = "Could not create workspace due to error" });
+ _logger.LogInformation($"User {userName} not authorized to delete workspace {workspace.Id}.");
+ return Unauthorized(new Problem() { Status = ((int)HttpStatusCode.Unauthorized), Type = "Authentication error", Title = "User cannot delete workspace" });
}
+ await _workspaceService.DeleteWorkspace(projectId, branchId, workspaceId, userName);
+
return Ok($"workspace {workspaceId} deleted.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not delete workspace");
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Type = "Could not delete workspace", Title = "Could not delete workspace due to error" });
}
-
- return BadRequest(new Problem() { Status = 400, Type = "Could not delete workspace", Title = "Internal error when created error" });
}
- public override Task ListTemplates()
+ public async override Task ListTemplates()
{
- var workspaceTemplates = _hubClient.ListWorkspaceTemplates();
+ var workspaceTemplates = await _workspaceService.ListWorkspaceTemplates();
var templates = workspaceTemplates
- .Select(x => new Template { Id = x.Id, Name = x.Name, Description = x.Description });
+ .Select(x => x.ToTemplate());
+
if (templates.Any())
{
- return Task.FromResult(Ok(templates));
+ return await Task.FromResult(Ok(templates));
}
- return Task.FromResult(BadRequest(new Problem
+ return BadRequest(new Problem
{
Detail = "No workspace templates found in hub repository",
- Status = 417,
- Title = "No workspace template found"
- }));
+ Status = (int)HttpStatusCode.ExpectationFailed,
+ Title = "No workspace templates found"
+ });
}
public async override Task ListWorkspaces([FromQuery(Name = "projectId")] string? projectId, [FromQuery(Name = "branchId")] string? branchId, [FromQuery(Name = "all")] bool? all)
{
+ //TODO: Rename and inverse "all" to "ownedByUser"
+
if (branchId != null && projectId is null)
{
- return BadRequest(new Problem() { Status = 400, Type = "Parameter error", Title = "If BranchId is specified a ProjectId must also be specified"});
+ return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Type = "Parameter error", Title = "If BranchId is specified a ProjectId must also be specified" });
}
+
List workspaces;
if (projectId != null)
{
- if (all??true)
+ var username = this.User.GetUserName();
+ var project = await _projectService.GetProject(projectId);
+
+ //Removed authorization so that you can see workspaces for other project, but workspaces are still authorized elsewhere
+ //if (!_workspaceAuthorization.CanUserGetWorkspaces(this.User, project))
+ //{
+ // //TODO: Return empty list?
+ // _logger.LogInformation($"User {username} not authorized to get workspaces for project {project.Id}.");
+ // return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Not authorized", Title = "You are not a member of the project" });
+ //}
+
+ if (all ?? true)
{
if (branchId is null)
{
- workspaces = await _hubClient.GetWorkspacesByProject(projectId);
+ workspaces = await _workspaceService.GetWorkspacesByProject(projectId);
}
else
{
- workspaces = await _hubClient.GetWorkspacesByProjectAndBranch(projectId, branchId);
+ workspaces = await _workspaceService.GetWorkspacesByProjectAndBranch(projectId, branchId);
}
}
else
{
- var project = await _hubClient.GetProject(projectId);
-
- if (!this.User.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName) )
- {
- return BadRequest(new Problem() { Status = 400, Type = "Not authorized", Title = "You are not a member of the project" });
- }
-
if (branchId is null)
{
- workspaces = await _hubClient.GetWorkspacesByProjectAndUser(projectId, this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value);
+ workspaces = await _workspaceService.GetWorkspacesByProjectAndUser(projectId, username);
}
else
{
- workspaces = await _hubClient.GetWorkspacesByProjectBranchAndUser(projectId, branchId, this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value);
+ workspaces = await _workspaceService.GetWorkspacesByProjectBranchAndUser(projectId, branchId, username);
}
}
}
@@ -146,17 +167,21 @@ public async override Task ListWorkspaces([FromQuery(Name = "proj
{
if (all ?? true)
{
- workspaces = await _hubClient.GetWorkspaces();
+ workspaces = await _workspaceService.GetWorkspaces();
}
else
{
- var projects = await _hubClient.GetProjects(false);
- var groups = this.User.Claims.Where(c => c.Type == "groups").Select(c => c.Value).ToList();
+ var projects = await _projectService.GetProjects(false);
+ var groups = this.User.GetGroups().Select(g => g.Value);
+ var userId = this.User.GetUserName();
var projectIds = projects.Where(p => groups.Contains(p.Oidc.GroupName)).Select(p => p.Id).ToList();
- workspaces = await _hubClient.GetWorkspacesByUser(this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value, projectIds);
+ workspaces = await _workspaceService.GetWorkspacesByUser(userId, projectIds);
}
}
- return Ok(workspaces.Select(w => new Workspace { Id = w.Id, Name = w.Name, ProjectId = w.ProjectId, BranchId = w.BranchId, TemplateId = w.TemplateId, Url = w.Url, Owner = w.Owner}).OrderBy(w => w.Name).ToArray());
+
+ return base.Ok(workspaces.Select(w => w.ToWorkspace())
+ .OrderBy(w => w.Name).ToArray());
}
+
}
}
diff --git a/src/Balsam/src/Balsam.Api/Dockerfile b/src/Balsam/src/Balsam.Api/Dockerfile
index 6d6c226..58e7d14 100644
--- a/src/Balsam/src/Balsam.Api/Dockerfile
+++ b/src/Balsam/src/Balsam.Api/Dockerfile
@@ -24,10 +24,14 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0.400-1-alpine3.16-amd64 AS build
WORKDIR /src
COPY . .
+RUN dotnet restore "Balsam.Domain/Balsam.Domain.csproj"
+RUN dotnet restore "Balsam.Infrastructure/Balsam.Infrastructure.csproj"
+RUN dotnet restore "Balsam.Application/Balsam.Application.csproj"
RUN dotnet restore "Balsam.Api/Balsam.Api.csproj"
WORKDIR "/src/Balsam.Api"
-RUN dotnet build "Balsam.Api.csproj" -c Release -o /app/build
+#TODO: Remove build because publish builds?
+#RUN dotnet build "Balsam.Api.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Balsam.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
diff --git a/src/Balsam/src/Balsam.Api/Extensions/BalsamBranchExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/BalsamBranchExtension.cs
new file mode 100644
index 0000000..bd2aaa6
--- /dev/null
+++ b/src/Balsam/src/Balsam.Api/Extensions/BalsamBranchExtension.cs
@@ -0,0 +1,19 @@
+using Balsam.Model;
+using BalsamApi.Server.Models;
+
+namespace Balsam.Api.Extensions
+{
+ public static class BalsamBranchExtension
+ {
+ public static Branch ToBranch(this BalsamBranch b)
+ {
+ return new Branch()
+ {
+ Id = b.Id,
+ Description = b.Description,
+ Name = b.Name,
+ IsDefault = b.IsDefault
+ };
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Extensions/BalsamKnowledgeLibraryExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/BalsamKnowledgeLibraryExtension.cs
new file mode 100644
index 0000000..389fcd4
--- /dev/null
+++ b/src/Balsam/src/Balsam.Api/Extensions/BalsamKnowledgeLibraryExtension.cs
@@ -0,0 +1,20 @@
+using Balsam.Model;
+using BalsamApi.Server.Models;
+
+namespace Balsam.Api.Extensions
+{
+ public static class BalsamKnowledgeLibraryExtension
+ {
+ public static KnowledgeLibrary ToKnowledgeLibrary(this BalsamKnowledgeLibrary bkl)
+ {
+ return new KnowledgeLibrary
+ {
+ Name = bkl.Name,
+ RepositoryUrl = bkl.RepositoryUrl,
+ Description = bkl.Description,
+ Id = bkl.Id,
+ RepositoryFriendlyUrl = bkl.RepositoryFriendlyUrl,
+ };
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Extensions/BalsamProjectExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/BalsamProjectExtension.cs
new file mode 100644
index 0000000..805fc6e
--- /dev/null
+++ b/src/Balsam/src/Balsam.Api/Extensions/BalsamProjectExtension.cs
@@ -0,0 +1,37 @@
+using Balsam.Model;
+using BalsamApi.Server.Models;
+using System.Xml.Linq;
+
+namespace Balsam.Api.Extensions
+{
+ public static class BalsamProjectExtension
+ {
+ public static Project ToProject(this BalsamProject project)
+ {
+ return new Project()
+ {
+ Id = project.Id,
+ Name = project.Name,
+ Description = project.Description,
+ Branches = project.Branches.Select(b => b.ToBranch()).ToList(),
+ AuthGroup = project.Oidc?.GroupName,
+ GitUrl = project.Git?.Path
+ };
+ }
+
+
+ //TODO: Do we need both Project and ProjectResponse?
+ public static ProjectResponse ToProjectResponse(this BalsamProject project)
+ {
+ return new ProjectResponse()
+ {
+ Id = project.Id,
+ Name = project.Name,
+ Description = project.Description,
+ Branches = project.Branches.Select(b => b.ToBranch()).ToList(),
+ AuthGroup = project.Oidc?.GroupName,
+ GitUrl = project.Git?.Path
+ };
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Extensions/BalsamRepoFileExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/BalsamRepoFileExtension.cs
new file mode 100644
index 0000000..ff17c38
--- /dev/null
+++ b/src/Balsam/src/Balsam.Api/Extensions/BalsamRepoFileExtension.cs
@@ -0,0 +1,33 @@
+using Balsam.Model;
+using BalsamApi.Server.Models;
+
+namespace Balsam.Api.Extensions
+{
+ public static class BalsamRepoFileExtension
+ {
+ public static RepoFile ToRepoFile(this BalsamRepoFile repoFile)
+ {
+ var file = new RepoFile
+ {
+ Id = repoFile.Id,
+ Name = repoFile.Name,
+ Path = repoFile.Path,
+ ContentUrl = repoFile.ContentUrl,
+ };
+
+ switch (repoFile.Type)
+ {
+ case BalsamRepoFile.TypeEnum.FileEnum:
+ file.Type = RepoFile.TypeEnum.FileEnum;
+ break;
+ case BalsamRepoFile.TypeEnum.FolderEnum:
+ file.Type = RepoFile.TypeEnum.FolderEnum;
+ break;
+ default:
+ throw new ApplicationException("Could not convert type " + repoFile.Type.ToString());
+ }
+
+ return file;
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Extensions/BalsamWorkspaceExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/BalsamWorkspaceExtension.cs
new file mode 100644
index 0000000..a14a1d5
--- /dev/null
+++ b/src/Balsam/src/Balsam.Api/Extensions/BalsamWorkspaceExtension.cs
@@ -0,0 +1,22 @@
+using Balsam.Model;
+using BalsamApi.Server.Models;
+
+namespace Balsam.Api.Extensions
+{
+ public static class BalsamWorkspaceExtension
+ {
+ public static Workspace ToWorkspace(this BalsamWorkspace w)
+ {
+ return new Workspace
+ {
+ Id = w.Id,
+ Name = w.Name,
+ ProjectId = w.ProjectId,
+ BranchId = w.BranchId,
+ TemplateId = w.TemplateId,
+ Url = w.Url,
+ Owner = w.Owner
+ };
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Extensions/ClaimsExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/ClaimsExtension.cs
new file mode 100644
index 0000000..100b549
--- /dev/null
+++ b/src/Balsam/src/Balsam.Api/Extensions/ClaimsExtension.cs
@@ -0,0 +1,21 @@
+using System.Security.Claims;
+
+namespace Balsam.Api.Extensions
+{
+ public static class ClaimsExtension
+ {
+ public static string GetUserName(this ClaimsPrincipal claimsPrincipal)
+ {
+ return claimsPrincipal.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value;
+ }
+ public static List GetGroups(this ClaimsPrincipal claimsPrincipal)
+ {
+ return claimsPrincipal.Claims.Where(x => x.Type == "groups").ToList();
+ }
+
+ public static string GetEmail(this ClaimsPrincipal claimsPrincipal)
+ {
+ return claimsPrincipal.Claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value;
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Extensions/RepoFileExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/RepoFileExtension.cs
new file mode 100644
index 0000000..e812f3e
--- /dev/null
+++ b/src/Balsam/src/Balsam.Api/Extensions/RepoFileExtension.cs
@@ -0,0 +1,35 @@
+using Balsam.Model;
+using GitProviderApiClient.Model;
+using System.Runtime.CompilerServices;
+
+namespace Balsam.Api.Extensions
+{
+ public static class RepoFileExtension
+ {
+ public static BalsamRepoFile ToBalsamRepoFile(this RepoFile repoFile)
+ {
+ var balsamRepoFile = new BalsamRepoFile
+ {
+ Id = repoFile.Id,
+ ContentUrl = repoFile.ContentUrl,
+ Name = repoFile.Name,
+ Path = repoFile.Path,
+
+ };
+
+ switch (repoFile.Type)
+ {
+ case RepoFile.TypeEnum.File:
+ balsamRepoFile.Type = BalsamRepoFile.TypeEnum.FileEnum;
+ break;
+ case RepoFile.TypeEnum.Folder:
+ balsamRepoFile.Type = BalsamRepoFile.TypeEnum.FolderEnum;
+ break;
+ default:
+ throw new NotImplementedException();
+ }
+
+ return balsamRepoFile;
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Extensions/WorkspaceTemplateExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/WorkspaceTemplateExtension.cs
new file mode 100644
index 0000000..5dd6bde
--- /dev/null
+++ b/src/Balsam/src/Balsam.Api/Extensions/WorkspaceTemplateExtension.cs
@@ -0,0 +1,18 @@
+using Balsam.Model;
+using BalsamApi.Server.Models;
+
+namespace Balsam.Api.Extensions
+{
+ public static class WorkspaceTemplateExtension
+ {
+ public static Template ToTemplate(this WorkspaceTemplate workspaceTemplate)
+ {
+ return new Template
+ {
+ Id = workspaceTemplate.Id,
+ Name = workspaceTemplate.Name,
+ Description = workspaceTemplate.Description
+ };
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/GitClient.cs b/src/Balsam/src/Balsam.Api/GitClient.cs
deleted file mode 100644
index c96ff10..0000000
--- a/src/Balsam/src/Balsam.Api/GitClient.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-//using Balsam.Api.Models;
-//using Microsoft.Extensions.Options;
-//using Newtonsoft.Json;
-//using System.Net.Http.Headers;
-
-//namespace Balsam.Api
-//{
-// public class GitClient
-// {
-// private HttpClient _httpClient;
-
-// //private string _baseUrl = "http://git-provider.balsam-system.svc.cluster.local/api/v1";
-// private string _baseUrl = "http://localhost:8081/api/v1";
-// public GitClient(IOptionsSnapshot capabilityOptions, HttpClient httpClient)
-// {
-// var gitOptions = capabilityOptions.Get(Capabilities.Git);
-// _baseUrl = gitOptions.ServiceLocation;
-// _httpClient = httpClient;
-
-// httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
-// httpClient.Timeout = TimeSpan.FromSeconds(30);
-// }
-
-
-// public async Task CreateRepository(string repositoryName)
-// {
-// HttpResponseMessage response = await _httpClient.PostAsync($"{_baseUrl}/repos?preferredName={repositoryName}", null);
-// response.EnsureSuccessStatusCode();
-
-// GitData data = new GitData();
-
-// if (response.Content is object && response.Content.Headers.ContentType != null && response.Content.Headers.ContentType.MediaType == "application/json")
-// {
-// string content = await response.Content.ReadAsStringAsync();
-// if (content == null)
-// {
-// return null;
-// }
-// dynamic createResponse = JsonConvert.DeserializeObject(content);
-// if (createResponse != null)
-// {
-// data.Name = createResponse.name;
-// data.Path = createResponse.path;
-// }
-// }
-// return data;
-// }
-// }
-//}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Api/HubClient.cs b/src/Balsam/src/Balsam.Api/HubClient.cs
deleted file mode 100644
index 55a7c2d..0000000
--- a/src/Balsam/src/Balsam.Api/HubClient.cs
+++ /dev/null
@@ -1,836 +0,0 @@
-using Balsam.Api.Models;
-using GitProviderApiClient.Api;
-using GitProviderApiClient.Model;
-using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.Options;
-using Newtonsoft.Json;
-using S3ProviderApiClient.Api;
-using S3ProviderApiClient.Model;
-using OidcProviderApiClient.Api;
-using OidcProviderApiClient.Model;
-using System.Text.RegularExpressions;
-using System.IO.Hashing;
-using RocketChatChatProviderApiClient.Api;
-using HandlebarsDotNet;
-using File = System.IO.File;
-using Microsoft.AspNetCore.Mvc;
-using BalsamApi.Server.Models;
-using LibGit2Sharp;
-
-namespace Balsam.Api
-{
- public class HubClient
- {
- private readonly CapabilityOptions _git;
- private readonly CapabilityOptions _s3;
- private readonly CapabilityOptions _authentication;
- private readonly CapabilityOptions _chat;
- private readonly IBucketApi _s3Client;
- private readonly IGroupApi _oidcClient;
- private readonly HubRepositoryClient _hubRepositoryClient;
- private readonly IMemoryCache _memoryCache;
- private readonly IRepositoryApi _repositoryApi;
- private readonly ILogger _logger;
- private readonly IAreaApi _chatClient;
- private readonly IUserApi _gitUserClient;
-
-
- public HubClient(ILogger logger, IOptionsSnapshot capabilityOptions, IMemoryCache memoryCache, HubRepositoryClient hubRepoClient, IBucketApi s3Client, IRepositoryApi reposiotryApi, IGroupApi oidcClient, IAreaApi chatClient, IUserApi gitUserClient)
- {
- _logger = logger;
- _memoryCache = memoryCache;
- _s3Client = s3Client;
- _oidcClient = oidcClient;
- _chatClient = chatClient;
- _gitUserClient = gitUserClient;
-
- _hubRepositoryClient = hubRepoClient;
-
- _git = capabilityOptions.Get(Capabilities.Git);
- _s3 = capabilityOptions.Get(Capabilities.S3);
- _authentication = capabilityOptions.Get(Capabilities.Authentication);
- _chat = capabilityOptions.Get(Capabilities.Chat);
- _repositoryApi = reposiotryApi;
-
- }
-
- static HubClient()
- {
- Handlebars.RegisterHelper("curlies", (writer, context, parameters) =>
- {
- if (parameters.Length == 1 && parameters.At(0) == true)
- {
- writer.Write("{{");
- }
- else
- {
- writer.Write("}}");
- }
-
- });
- }
-
- public async Task> GetProjects(bool includeBranches = true)
- {
- var projects = new List();
- var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub");
-
- foreach (var projectPath in Directory.GetDirectories(hubPath))
- {
- var propsFile = Path.Combine(projectPath, "properties.json");
- var project = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
- if (project != null)
- {
- if (includeBranches)
- {
- project.Branches = await ReadBranches(projectPath);
- }
- projects.Add(project);
- }
- else
- {
- _logger.LogWarning($"Could not parse properties file {propsFile}");
- }
- }
- return projects;
- }
-
- public async Task GetProject(string projectId, bool includeBranches = true)
- {
- var projectPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId);
- var propsFile = Path.Combine(projectPath, "properties.json");
-
- if (!File.Exists(propsFile))
- {
- return null;
- }
- var project = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
- if (project != null && includeBranches)
- {
- project.Branches = await ReadBranches(projectPath);
- }
- return project;
- }
-
- public async Task GetProject(string projectId)
- {
- var projectPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId);
-
- if (!Directory.Exists(projectPath))
- {
- return null;
- }
-
- var propsFile = Path.Combine(projectPath, "properties.json");
- var project = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
- if (project != null)
- {
- project.Branches = await ReadBranches(projectPath);
- }
-
- return project;
- }
-
- private async Task> ReadBranches(string projectPath)
- {
- var branches = new List();
-
- if (!Directory.Exists(projectPath))
- {
- return branches;
- }
-
- foreach (var branchPath in Directory.GetDirectories(projectPath))
- {
- var propsFile = Path.Combine(branchPath, "properties.json");
- var branch = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
- if (branch != null)
- {
- branches.Add(branch);
- }
- else
- {
- _logger.LogWarning($"Could not parse properties file {propsFile}");
- }
- }
- return branches;
- }
-
- public async Task GetBranch(string projectId, string branchId)
- {
- var propsFile = Path.Combine(_hubRepositoryClient.Path, "hub", projectId, branchId, "properties.json");
-
- if (!File.Exists(propsFile))
- {
- return null;
- }
-
- var branch = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
- return branch;
- }
-
- private async Task ProjectExists(string projectName)
- {
- var projects = await GetProjects(false);
- if (projects.FirstOrDefault(p => p.Name == projectName) == null)
- {
- return false;
- }
- return true;
- }
-
- public async Task CreateProject(string preferredName, string description, string defaultBranchName, string username, string? sourceLocation)
- {
- //Check if there is a program with the same name.
- _logger.LogDebug("Check for duplicate names");
- if (await ProjectExists(preferredName))
- {
- _logger.LogInformation($"Could not create project {preferredName}, due to name duplication");
- return null;
- }
-
- _hubRepositoryClient.PullChanges();
-
- _logger.LogDebug($"create project information");
- var project = new BalsamProject(SanitizeName(preferredName), preferredName, description);
- string projectPath = Path.Combine(_hubRepositoryClient.Path, "hub", project.Id);
-
- _logger.LogDebug($"Assure path exists {projectPath}");
-
- DirectoryUtil.AssureDirectoryExists(projectPath);
-
- _logger.LogDebug($"Begin call service providers");
-
- if (_authentication.Enabled)
- {
- _logger.LogDebug($"Begin call OpenIdConnect");
- var oidcData = await _oidcClient.CreateGroupAsync(new CreateGroupRequest(project.Id));
- await _oidcClient.AddUserToGroupAsync(oidcData.Id, new AddUserToGroupRequest(username));
- project.Oidc = new OidcData(oidcData.Id, oidcData.Name);
- _logger.LogInformation($"Group {project.Oidc.GroupName}({project.Oidc.GroupId}) created");
- }
-
- if (project.Oidc == null)
- {
- throw new Exception("Could not parse oidc data");
- }
-
- if (_git.Enabled)
- {
- _logger.LogDebug($"Begin call Git");
- var gitData = await _repositoryApi.CreateRepositoryAsync(new CreateRepositoryRequest(preferredName, description, defaultBranchName));
- defaultBranchName = gitData.DefaultBranchName;
- project.Git = new GitData() { Id = gitData.Id, Name = gitData.Name, Path = gitData.Path, SourceLocation = sourceLocation };
- _logger.LogInformation($"Git repository {project.Git.Name} created");
- }
-
- if (_s3.Enabled)
- {
- _logger.LogDebug($"Begin call S3");
- var s3Data = await _s3Client.CreateBucketAsync(new CreateBucketRequest(preferredName, project.Oidc.GroupName));
- project.S3 = new S3Data() { BucketName = s3Data.Name };
- _logger.LogInformation($"Bucket {project.S3.BucketName} created");
- }
-
- if (_chat.Enabled)
- {
- _logger.LogDebug("Begin the call to chatprovider");
- var chatData = await _chatClient.CreateAreaAsync(new RocketChatChatProviderApiClient.Model.CreateAreaRequest(preferredName));
- project.Chat = new ChatData(chatData.Id, chatData.Name);
- _logger.LogInformation($"Channel created named {chatData.Name}");
- }
-
- string propPath = Path.Combine(projectPath, "properties.json");
-
- if (await CreateBranch(project, defaultBranchName, description, true) != null)
- {
- _logger.LogInformation($"Default Balsam branch {defaultBranchName} created");
- }
-
- // serialize JSON to a string and then write string to a file
- await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(project));
-
- await CreateProjectManifests(project, projectPath);
- _hubRepositoryClient.PersistChanges($"New program with id {project.Id}");
- _logger.LogInformation($"Project {project.Name}({project.Id}) created");
- return project;
- }
-
- public async Task CreateWorkspace(string projectId, string branchId, string name, string templateId, string userName, string userMail)
- {
- var branchPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId, branchId);
-
- if (!Directory.Exists(branchPath))
- {
- return null;
- }
-
- var workspace = new BalsamWorkspace(CreateWorkspaceId(name), name, templateId, projectId, branchId, userName);
-
- var workspacePath = Path.Combine(branchPath, userName, workspace.Id);
-
- DirectoryUtil.AssureDirectoryExists(workspacePath);
-
-
- var project = await GetProject(projectId);
- var branch = project.Branches.FirstOrDefault(b => b.Id == branchId);
- var gitPAT = string.Empty;
-
- if (_git.Enabled)
- {
- var patResponse = await _gitUserClient.CreatePATAsync(userName);
- gitPAT = patResponse.Token;
- _logger.LogInformation($"Git PAT created");
- }
-
- var user = new UserInfo(userName, userMail, gitPAT);
- var propPath = Path.Combine(workspacePath, "properties.json");
-
- _logger.LogDebug("Pulling changes");
- _hubRepositoryClient.PullChanges();
- // serialize JSON to a string and then write string to a file
- await CreateWorkspaceManifests(project, branch, workspace, user, workspacePath, templateId);
-
- var template = await GetWorkspaceTemplate(templateId);
- workspace.Url = ManifestUtil.GetWorkspaceUrl(Path.Combine(workspacePath, template.UrlConfig));
- await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(workspace));
-
- _hubRepositoryClient.PersistChanges($"New workspace with id {project.Id}");
- _logger.LogInformation("Workspace created");
- return workspace;
- }
-
- public async Task DeleteWorkspace(string projectId, string branchId, string workspaceId, string userName)
- {
- _hubRepositoryClient.PullChanges();
- var workspacePath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId, branchId, userName, workspaceId);
-
- if (!Directory.Exists(workspacePath))
- {
- return null;
- }
-
- if (Directory.Exists(workspacePath))
- {
- EmptyDirectory(workspacePath);
- Directory.Delete(workspacePath, true);
- }
-
- _hubRepositoryClient.PersistChanges($"Deleted workspace with id {workspaceId}");
- return "workspace deleted";
- }
-
- private async Task CreateWorkspaceManifests(BalsamProject project, BalsamBranch branch, BalsamWorkspace workspace, UserInfo user, string workspacePath, string templateId)
- {
- var token = new AccessKeyCreatedResponse("", "");
- if (_s3.Enabled)
- {
- token = await _s3Client.CreateAccessKeyAsync(project.S3.BucketName);
- }
-
- var s3Token = new S3Token(token.AccessKey, token.SecretKey);
- user.S3 = s3Token;
- var context = new WorkspaceContext(project, branch, workspace, user);
- await CreateManifests(context, workspacePath, "workspaces" + Path.DirectorySeparatorChar + templateId);
- }
-
- private async Task CreateProjectManifests(BalsamProject project, string projectPath)
- {
- var context = new ProjectContext() { Project = project };
-
- await CreateManifests(context, projectPath, "projects");
- }
-
- private async Task CreateManifests(BalsamContext context, string destinationPath, string templateName)
- {
- var templatePath = Path.Combine(_hubRepositoryClient.Path, "templates", templateName);
-
- foreach (var file in Directory.GetFiles(templatePath, "*.yaml"))
- {
- var source = await File.ReadAllTextAsync(file);
-
- var template = Handlebars.Compile(source);
-
- var result = template(context);
-
- var destinationFilePath = Path.Combine(destinationPath, Path.GetFileName(file));
-
- await File.WriteAllTextAsync(destinationFilePath, result);
- }
-
- }
-
- private async Task GetWorkspaceTemplate(string templateId)
- {
- var templatePath = Path.Combine(_hubRepositoryClient.Path, "templates", "workspaces", templateId, "properties.json");
-
- if (!File.Exists(templatePath)) return null;
-
- return JsonConvert.DeserializeObject(await File.ReadAllTextAsync(templatePath));
- }
-
- public async Task CreateBranch(string projectId, string fromBranch, string branchName, string description)
- {
- var project = await GetProject(projectId, false);
- var branch = await GetBranch(projectId, fromBranch);
-
- if (project is null || project.Git is null || branch is null || branch.GitBranch is null)
- {
- return null;
- }
-
- var response = await _repositoryApi.CreateBranchAsync(project.Git.Id, new GitProviderApiClient.Model.CreateBranchRequest(branchName, branch.GitBranch));
- branchName = response.Name;
-
- _hubRepositoryClient.PullChanges();
-
- var createdBranch = await CreateBranch(project, branchName, description, false);
- _hubRepositoryClient.PersistChanges($"Branch {branchName} created for project {project.Name}");
-
- return createdBranch;
- }
-
- private async Task CreateBranch(BalsamProject project, string branchName, string description, bool isDefault = false)
- {
- var branchId = SanitizeName(branchName);
- var branchPath = Path.Combine(_hubRepositoryClient.Path, "hub", project.Id, branchId);
-
- DirectoryUtil.AssureDirectoryExists(branchPath);
-
- if (project.S3 is null || string.IsNullOrEmpty(project.S3.BucketName))
- {
- return null;
- }
-
- if (_s3.Enabled)
- {
- await _s3Client.CreateFolderAsync(project.S3.BucketName, new CreateFolderRequest(branchName));
- _logger.LogInformation($"Folder {branchName} created in bucket {project.S3.BucketName}.");
- }
-
- var branch = new BalsamBranch()
- {
- Id = branchId,
- Name = branchName,
- Description = description,
- IsDefault = isDefault,
- GitBranch = branchName
- };
-
- var propPath = Path.Combine(branchPath, "properties.json");
- await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(branch));
-
- return branch;
- }
-
- public async Task?> GetGitBranchFiles(string projectId, string branchId)
- {
- var project = await GetProject(projectId);
- var branch = await GetBranch(projectId, branchId);
- if (project is null || branch is null || project.Git is null)
- {
- return null;
- }
- return _repositoryApi.GetFilesInBranch(project.Git.Id, branch.Name);
- }
-
- public async Task GetFile(string projectId, string branchId, string fileId)
- {
- var project = await GetProject(projectId);
- var branch = await GetBranch(projectId, branchId);
- if (project is null || branch is null || project.Git is null)
- {
- return null;
- }
-
- try
- {
- var request = new HttpRequestMessage(HttpMethod.Get, $"{_git.ServiceLocation}/repos/{project.Git.Id}/branches/{branch.GitBranch}/files/{fileId}");
-
- var httpClient = new HttpClient();
-
- var response = await httpClient.SendAsync(request);
- if (response.IsSuccessStatusCode)
- {
- var data = await response.Content.ReadAsByteArrayAsync();
- return new FileContentResult(data, response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream");
- }
-
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not read files from repository");
- }
-
- return null;
- }
-
-
- private string SanitizeName(string name)
- {
- var crc32 = new Crc32();
-
- crc32.Append(System.Text.Encoding.ASCII.GetBytes(name));
- var hash = crc32.GetCurrentHash();
- var crcHash = string.Join("", hash.Select(b => b.ToString("x2").ToLower()).Reverse());
-
- name = name.ToLower(); //Only lower charachters allowed
- name = name.Replace(" ", "-"); //replaces spaches with hypen
- name = Regex.Replace(name, @"[^a-z0-9\-]", ""); // make sure that only a-z or digit or hypen removes all other characters
- name = name.Substring(0, Math.Min(50 - crcHash.Length, name.Length)) + "-" + crcHash; //Assures max size of 50 characters
-
- return name;
-
- }
-
- private static void EmptyDirectory(string directory)
- {
- var di = new DirectoryInfo(directory);
- foreach (var file in di.GetFiles())
- {
- file.Delete();
- }
- foreach (var dir in di.GetDirectories())
- {
- dir.Delete(true);
- }
- }
-
- private static string CreateWorkspaceId(string name)
- {
- var crc32 = new Crc32();
-
- crc32.Append(System.Text.Encoding.ASCII.GetBytes(name + Guid.NewGuid().ToString()));
- var hash = crc32.GetCurrentHash();
- var crcHash = string.Join("", hash.Select(b => b.ToString("x2").ToLower()).Reverse());
-
- name = name.ToLower(); //Only lower charachters allowed
- name = name.Replace(" ", "-"); //replaces spaches with hypen
- name = Regex.Replace(name, @"[^a-z0-9\-]", ""); // make sure that only a-z or digit or hypen removes all other characters
- name = name.Substring(0, Math.Min(50 - crcHash.Length, name.Length)) + "-" + crcHash; //Assures max size of 50 characters
-
- return name;
- }
-
- public IEnumerable ListWorkspaceTemplates()
- {
- _logger.LogDebug("Start ListWorkspaceTemplates");
- var workspaceTemplatePath = Path.Combine(_hubRepositoryClient.Path, "templates", "workspaces");
-
- var workspaceTemplates = new List();
-
- if (!Directory.Exists(workspaceTemplatePath))
- {
- _logger.LogInformation("No workspace template folder found!");
- return workspaceTemplates;
- }
-
- foreach (var directory in Directory.GetDirectories(workspaceTemplatePath))
- {
- var id = new DirectoryInfo(directory).Name;
- var fileName = Path.Combine(directory, "properties.json");
- var jsonString = File.ReadAllText(fileName);
- var template = JsonConvert.DeserializeObject(jsonString);
-
- if (template == null) continue;
-
- template.Id = id;
- workspaceTemplates.Add(template);
- }
-
- _logger.LogDebug("End ListWorkspaceTemplates");
- return workspaceTemplates;
- }
-
- public async Task> GetWorkspaces()
- {
- var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub");
-
- var workspaces = new List();
-
- foreach (var projectPath in Directory.GetDirectories(hubPath))
- {
- foreach (var branchPath in Directory.GetDirectories(projectPath))
- {
- foreach (var userPath in Directory.GetDirectories(branchPath))
- {
- foreach (var workspacePath in Directory.GetDirectories(userPath))
- {
- var propsFile = Path.Combine(workspacePath, "properties.json");
- var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
-
- if (workspace == null) continue;
- workspaces.Add(workspace);
- }
- }
- }
- }
- return workspaces;
- }
-
- public async Task> GetWorkspacesByUser(string userId, List projectIds)
- {
- var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub");
-
- var workspaces = new List();
-
- foreach (var projectId in projectIds)
- {
- var projectPath = Path.Combine(hubPath, projectId);
- if (Directory.Exists(projectPath))
- {
- foreach (var branchPath in Directory.GetDirectories(projectPath))
- {
- var userPath = Path.Combine(branchPath, userId);
-
- if (Directory.Exists(userPath))
- {
- foreach (var workspacePath in Directory.GetDirectories(userPath))
- {
- var propsFile = Path.Combine(workspacePath, "properties.json");
- var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
-
- if (workspace == null) continue;
- workspaces.Add(workspace);
- }
- }
- }
- }
- }
- return workspaces;
- }
-
- public async Task> GetWorkspacesByProject(string projectId)
- {
- var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub");
-
- var workspaces = new List();
-
- var projectPath = Path.Combine(hubPath, projectId);
-
- if (Directory.Exists(projectPath))
- {
- foreach (var branchPath in Directory.GetDirectories(projectPath))
- {
- foreach (var userPath in Directory.GetDirectories(branchPath))
- {
- foreach (var workspacePath in Directory.GetDirectories(userPath))
- {
- var propsFile = Path.Combine(workspacePath, "properties.json");
- var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
-
- if (workspace == null) continue;
- workspaces.Add(workspace);
- }
- }
- }
- }
- return workspaces;
- }
-
- public async Task> GetWorkspacesByProjectAndBranch(string projectId, string branchId)
- {
- var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub");
-
- var workspaces = new List();
-
- var branchPath = Path.Combine(hubPath, projectId, branchId);
-
- if (Directory.Exists(branchPath))
- {
- foreach (var userPath in Directory.GetDirectories(branchPath))
- {
- foreach (var workspacePath in Directory.GetDirectories(userPath))
- {
- var propsFile = Path.Combine(workspacePath, "properties.json");
- var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
-
- if (workspace == null) continue;
- workspaces.Add(workspace);
- }
- }
- }
- return workspaces;
- }
-
- public async Task> GetWorkspacesByProjectAndUser(string projectId, string userId)
- {
- var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub");
-
- var workspaces = new List();
-
- var projectPath = Path.Combine(hubPath, projectId);
-
- if (Directory.Exists(projectPath))
- {
- foreach (var branchPath in Directory.GetDirectories(projectPath))
- {
- var userPath = Path.Combine(branchPath, userId);
- if (Directory.Exists(userPath))
- {
- foreach (var workspacePath in Directory.GetDirectories(userPath))
- {
- var propsFile = Path.Combine(workspacePath, "properties.json");
- var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
-
- if (workspace == null) continue;
- workspaces.Add(workspace);
- }
- }
- }
- }
- return workspaces;
- }
-
- public async Task> GetWorkspacesByProjectBranchAndUser(string projectId, string branchId, string userId)
- {
- var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub");
-
- var workspaces = new List();
-
- var userPath = Path.Combine(hubPath, projectId, branchId, userId);
-
- if (Directory.Exists(userPath))
- {
- foreach (var workspacePath in Directory.GetDirectories(userPath))
- {
- var propsFile = Path.Combine(workspacePath, "properties.json");
- var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
-
- if (workspace == null) continue;
- workspaces.Add(workspace);
- }
- }
- return workspaces;
- }
-
- public async Task DeleteBranch(string projectId, string branchId)
- {
- var branchPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId, branchId);
-
- var project = await GetProject(projectId);
- var branch = await GetBranch(projectId, branchId);
-
- //Asure that the id are correct
- if (project == null || branch == null) return;
-
-
- if (_s3.Enabled)
- {
- try
- {
- await _s3Client.DeleteFolderAsync(project.S3?.BucketName??"", branch.Name);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not delete s3 folder");
- }
- }
-
- if (_git.Enabled)
- {
- try
- {
- await _repositoryApi.DeleteRepositoryBranchAsync(project.Git?.Id ?? "", branch.GitBranch);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not delete git branch");
- }
- }
-
-
- _hubRepositoryClient.PullChanges();
-
- if (Directory.Exists(branchPath))
- {
- //EmptyDirectory(branchPath);
- Directory.Delete(branchPath, true);
- }
-
- _hubRepositoryClient.PersistChanges($"Branch {branch.Name} deleted for project {project.Name}");
-
- }
-
- public async Task> ListKnowledgeLibraries()
- {
- var knowledgeLibraries = new List();
- var kbPath = Path.Combine(_hubRepositoryClient.Path, "kb");
- foreach (var knowledgelibraryFile in Directory.GetFiles(kbPath))
- {
- try
- {
- var knowledgeLibrary = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(knowledgelibraryFile));
- if (knowledgeLibrary != null)
- {
- knowledgeLibraries.Add(knowledgeLibrary);
- }
- else
- {
- _logger.LogWarning($"Could not parse properties file for knowledgelibrary {knowledgelibraryFile}");
- }
- }
- catch(Exception ex)
- {
- _logger.LogError(ex, "Error with deserialization of file");
- }
- }
- return knowledgeLibraries;
- }
- internal async Task DeleteProject(string projectId)
- {
- var project = await GetProject(projectId);
-
- if (project == null)
- {
- return;
- }
-
- if (_authentication.Enabled)
- {
- try
- {
- await _oidcClient.DeleteGroupAsync(project.Oidc?.GroupId ?? "");
- } catch (Exception ex)
- {
- _logger.LogError(ex, "Could not delete oidc group");
- }
- }
-
- if (_git.Enabled)
- {
- try
- {
- await _repositoryApi.DeleteRepositoryAsync(project.Git?.Id ?? "");
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not delete git repository");
- }
- }
-
- if (_s3.Enabled)
- {
- try
- {
- await _s3Client.DeleteBucketAsync(project.S3?.BucketName ?? "");
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not delete s3 bucket");
- }
- }
-
- var branchPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId);
-
- _hubRepositoryClient.PullChanges();
-
- if (Directory.Exists(branchPath))
- {
- Directory.Delete(branchPath, true);
- }
-
- _hubRepositoryClient.PersistChanges($"Project {project.Name} deleted");
- }
- }
-}
diff --git a/src/Balsam/src/Balsam.Api/KnowledgeLibraryClient.cs b/src/Balsam/src/Balsam.Api/KnowledgeLibraryClient.cs
deleted file mode 100644
index 45b4775..0000000
--- a/src/Balsam/src/Balsam.Api/KnowledgeLibraryClient.cs
+++ /dev/null
@@ -1,175 +0,0 @@
-using BalsamApi.Server.Models;
-using LibGit2Sharp;
-using System.IO.Compression;
-using System.Text;
-using static System.Net.Mime.MediaTypeNames;
-
-namespace Balsam.Api
-{
- public class KnowledgeLibraryClient
- {
- public List GetRepositoryContent(string repositoryId, string repositoryUrl)
- {
- string localRepositoryPath = Path.Combine(Path.GetTempPath(), "kb", repositoryId);
-
- Repository repository = GetOrCreateRepository(localRepositoryPath, repositoryUrl);
-
- var repositoryContents = new List();
-
- AddContents(localRepositoryPath, localRepositoryPath.Length + 1, repositoryContents);
-
- return repositoryContents;
- }
-
- public string GetRepositoryFilePath(string repositoryId, string fileId)
- {
- string relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(fileId));
-
- if (relativePath.Contains(".."))
- {
- throw new ArgumentException("Invalid file id");
- }
-
- string filePath = Path.Combine(Path.GetTempPath(), "kb", repositoryId, relativePath);
-
- if (!File.Exists(filePath))
- {
- throw new ArgumentException("File not found");
- }
-
- return filePath;
-
- }
-
- public string GetZippedResource(string repositoryId, string repositoryUrl, string fileId)
- {
-
- //Make sure that the repository is cloned and up to date
- string localRepositoryPath = Path.Combine(Path.GetTempPath(), "kb", repositoryId);
- Repository repository = GetOrCreateRepository(localRepositoryPath, repositoryUrl);
-
- //Make sure that the file/directory exists in the repository
- string relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(fileId));
- if (relativePath.Contains(".."))
- {
- throw new ArgumentException("Invalid file id");
- }
-
- string filePath = Path.Combine(Path.GetTempPath(), "kb", repositoryId, relativePath);
-
- string workPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
-
- Directory.CreateDirectory(workPath);
-
-
- if (File.Exists(filePath))
- {
- File.Copy(filePath, Path.Combine(workPath, Path.GetFileName(filePath)));
- } else if (Directory.Exists(filePath))
- {
- CopyDirectory(filePath, workPath);
- }
- else
- {
- throw new ArgumentException("File/Directory not found");
- }
-
- string zipPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip");
- ZipFile.CreateFromDirectory(workPath, zipPath);
-
- Directory.Delete(workPath, true);
-
- return zipPath;
-
- }
-
- private static void CopyDirectory(string sourceDirName, string destDirName)
- {
- DirectoryInfo dir = new DirectoryInfo(sourceDirName);
-
- if (!dir.Exists)
- {
- throw new DirectoryNotFoundException(
- "Source directory does not exist or could not be found: "
- + sourceDirName);
- }
-
- DirectoryInfo[] dirs = dir.GetDirectories();
-
- // Append the name of the source directory to the destination directory
- destDirName = Path.Combine(destDirName, dir.Name);
-
- // If the destination directory doesn't exist, create it.
- Directory.CreateDirectory(destDirName);
-
- FileInfo[] files = dir.GetFiles();
- foreach (FileInfo file in files)
- {
- string tempPath = Path.Combine(destDirName, file.Name);
- file.CopyTo(tempPath, false);
- }
-
- foreach (DirectoryInfo subdir in dirs)
- {
- string tempPath = Path.Combine(destDirName, subdir.Name);
- CopyDirectory(subdir.FullName, tempPath);
- }
- }
-
-
- private Repository GetOrCreateRepository(string localRepositoryPath, string repositoryUrl)
- {
- if (Directory.Exists(localRepositoryPath))
- {
- //Pull repository changes
- var pullOptions = new PullOptions();
- pullOptions.FetchOptions = new FetchOptions();
- var repository = new Repository(localRepositoryPath);
- Commands.Pull(repository, new Signature("x", "x@x.com", DateTimeOffset.Now), pullOptions);
- return repository;
- }
- else
- {
- //Clone repository
- Repository.Clone(repositoryUrl, localRepositoryPath);
- return new Repository(localRepositoryPath);
- }
- }
-
- void AddContents(string path, int relativePathPosition, List contents)
- {
- foreach (var directory in Directory.GetDirectories(path))
- {
- //Ignore .git folder
- if (directory.Substring(relativePathPosition).StartsWith(".git") ) { continue; }
-
- contents.Add(CreateRepoFileFromDirectory(directory, relativePathPosition));
- AddContents(directory, relativePathPosition, contents);
- }
- foreach (var file in Directory.GetFiles(path))
- {
- contents.Add(CreateRepoFileFromFile(file, relativePathPosition));
- }
- }
-
- RepoFile CreateRepoFileFromFile(string path, int relativePathPosition)
- {
- var repoFile = new RepoFile();
- repoFile.Path = path.Substring(relativePathPosition);
- repoFile.Name = Path.GetFileName(path);
- repoFile.Id = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(repoFile.Path));
- repoFile.Type = RepoFile.TypeEnum.FileEnum;
- return repoFile;
- }
-
- RepoFile CreateRepoFileFromDirectory(string path, int relativePathPosition)
- {
- var repoFile = new RepoFile();
- repoFile.Path = path.Substring(relativePathPosition);
- repoFile.Name = Path.GetFileName(path);
- repoFile.Id = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(repoFile.Path));
- repoFile.Type = RepoFile.TypeEnum.FolderEnum;
- return repoFile;
- }
- }
-}
diff --git a/src/Balsam/src/Balsam.Api/Models/WorkspaceContext.cs b/src/Balsam/src/Balsam.Api/Models/WorkspaceContext.cs
deleted file mode 100644
index 501dad6..0000000
--- a/src/Balsam/src/Balsam.Api/Models/WorkspaceContext.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Balsam.Api.Models
-{
- public class WorkspaceContext : BalsamContext
- {
- public BalsamProject Project { get; set; }
- public BalsamBranch Branch { get; set; }
- public BalsamWorkspace Workspace { get; set; }
- public UserInfo User { get; set; }
-
- public WorkspaceContext(BalsamProject project, BalsamBranch branch, BalsamWorkspace workspace, UserInfo user)
- {
- Project = project;
- Branch = branch;
- Workspace = workspace;
- User = user;
- }
- }
-}
diff --git a/src/Balsam/src/Balsam.Api/Program.cs b/src/Balsam/src/Balsam.Api/Program.cs
index 8a89067..ef2ba11 100644
--- a/src/Balsam/src/Balsam.Api/Program.cs
+++ b/src/Balsam/src/Balsam.Api/Program.cs
@@ -1,8 +1,9 @@
using Balsam.Api;
-using Balsam.Api.Models;
+using Balsam.Interfaces;
+using Balsam.Model;
+using Balsam.Repositories;
+using Balsam.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
-using Microsoft.AspNetCore.HttpLogging;
-using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
@@ -16,9 +17,17 @@
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton();
-builder.Services.AddTransient();
-builder.Services.AddSingleton();
-builder.Services.AddSingleton();
+builder.Services.AddTransient(); //TODO: Use AddScoped ?
+builder.Services.AddTransient(); //TODO: Use AddScoped ?
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+builder.Services.AddTransient();
+builder.Services.AddTransient();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddTransient();
+builder.Services.AddTransient();
builder.Services.AddMemoryCache();
builder.Services.AddSingleton(
diff --git a/src/Balsam/src/Balsam.Api/README.md b/src/Balsam/src/Balsam.Api/README.md
new file mode 100644
index 0000000..61b8d59
--- /dev/null
+++ b/src/Balsam/src/Balsam.Api/README.md
@@ -0,0 +1,3 @@
+#To build a local image with docker
+
+ docker build -t balsamapi -f Dockerfile ..\
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Api/S3Client.cs b/src/Balsam/src/Balsam.Api/S3Client.cs
deleted file mode 100644
index 5b93b4c..0000000
--- a/src/Balsam/src/Balsam.Api/S3Client.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-//using Balsam.Api.Models;
-//using Microsoft.Extensions.Options;
-//using Newtonsoft.Json;
-//using System.Net.Http.Headers;
-//using System.Runtime.CompilerServices;
-//using System.Security.Cryptography;
-//using static System.Net.WebRequestMethods;
-
-//namespace Balsam.Api
-//{
-// public class S3Client
-// {
-// private HttpClient _httpClient;
-
-// //private string _baseUrl = "http://s3-provider.balsam-system.svc.cluster.local/api/v1/buckets";
-// private string _baseUrl = "http://localhost:8080/api/v1";
-// public S3Client(IOptionsSnapshot capabilityOptions, HttpClient httpClient) {
-// var s3Options = capabilityOptions.Get(Capabilities.S3);
-// _baseUrl = s3Options.ServiceLocation;
-// _httpClient = httpClient;
-
-// httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
-// httpClient.Timeout = TimeSpan.FromSeconds(30);
-
-// }
-
-
-// public async Task CreateBucket(string bucketName)
-// {
-// HttpResponseMessage response = await _httpClient.PostAsync($"{_baseUrl}/buckets?preferredName={bucketName}", null);
-// response.EnsureSuccessStatusCode();
-
-// S3Data data = new S3Data();
-
-// if (response.Content is object && response.Content.Headers.ContentType != null && response.Content.Headers.ContentType.MediaType == "application/json")
-// {
-// string content = await response.Content.ReadAsStringAsync();
-// if (content == null)
-// {
-// return null;
-// }
-// dynamic createResponse = JsonConvert.DeserializeObject(content);
-// if (createResponse != null)
-// {
-
-
-// data.BucketName = createResponse.name;
-// }
-// }
-// return data;
-// }
-// }
-//}
diff --git a/src/Balsam/src/Balsam.Application/Authorization/ProjectAuthorization.cs b/src/Balsam/src/Balsam.Application/Authorization/ProjectAuthorization.cs
new file mode 100644
index 0000000..5aa0d10
--- /dev/null
+++ b/src/Balsam/src/Balsam.Application/Authorization/ProjectAuthorization.cs
@@ -0,0 +1,33 @@
+using Balsam.Model;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Balsam.Application.Authorization
+{
+ public class ProjectAuthorization
+ {
+ public bool CanUserDeleteProject(ClaimsPrincipal user, BalsamProject project)
+ {
+ return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName);
+ }
+
+ public bool CanUserCreateBranch(ClaimsPrincipal user, BalsamProject project)
+ {
+ return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName);
+ }
+
+ public bool CanUserEditBranch(ClaimsPrincipal user, BalsamProject project)
+ {
+ return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName);
+ }
+
+ public bool CanUserDeleteBranch(ClaimsPrincipal user, BalsamProject project)
+ {
+ return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName);
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Application/Authorization/WorkspaceAuthorization.cs b/src/Balsam/src/Balsam.Application/Authorization/WorkspaceAuthorization.cs
new file mode 100644
index 0000000..182af0a
--- /dev/null
+++ b/src/Balsam/src/Balsam.Application/Authorization/WorkspaceAuthorization.cs
@@ -0,0 +1,31 @@
+using Balsam.Model;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Balsam.Application.Authorization
+{
+ public class WorkspaceAuthorization
+ {
+ public bool CanUserCreateWorkspace(ClaimsPrincipal user, BalsamProject project)
+ {
+ return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName);
+ }
+
+ public bool CanUserDeleteWorkspace(ClaimsPrincipal user, BalsamProject project, BalsamWorkspace workspace)
+ {
+ var userName = user.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value;
+
+ return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName)
+ && userName == workspace.Owner;
+ }
+
+ public bool CanUserGetWorkspaces(ClaimsPrincipal user, BalsamProject project)
+ {
+ return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName);
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Application/Balsam.Application.csproj b/src/Balsam/src/Balsam.Application/Balsam.Application.csproj
new file mode 100644
index 0000000..0900a23
--- /dev/null
+++ b/src/Balsam/src/Balsam.Application/Balsam.Application.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Balsam/src/Balsam.Application/Extensions/RepoFileExtension.cs b/src/Balsam/src/Balsam.Application/Extensions/RepoFileExtension.cs
new file mode 100644
index 0000000..4eeb2a5
--- /dev/null
+++ b/src/Balsam/src/Balsam.Application/Extensions/RepoFileExtension.cs
@@ -0,0 +1,34 @@
+using Balsam.Model;
+using GitProviderApiClient.Model;
+
+namespace Balsam.Application.Extensions
+{
+ public static class RepoFileExtension
+ {
+ public static BalsamRepoFile ToBalsamRepoFile(this RepoFile repoFile)
+ {
+ var balsamRepoFile = new BalsamRepoFile
+ {
+ Id = repoFile.Id,
+ ContentUrl = repoFile.ContentUrl,
+ Name = repoFile.Name,
+ Path = repoFile.Path,
+
+ };
+
+ switch (repoFile.Type)
+ {
+ case RepoFile.TypeEnum.File:
+ balsamRepoFile.Type = BalsamRepoFile.TypeEnum.FileEnum;
+ break;
+ case RepoFile.TypeEnum.Folder:
+ balsamRepoFile.Type = BalsamRepoFile.TypeEnum.FolderEnum;
+ break;
+ default:
+ throw new NotImplementedException();
+ }
+
+ return balsamRepoFile;
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Application/Interfaces/IKnowledgeLibraryService.cs b/src/Balsam/src/Balsam.Application/Interfaces/IKnowledgeLibraryService.cs
new file mode 100644
index 0000000..e1cba5a
--- /dev/null
+++ b/src/Balsam/src/Balsam.Application/Interfaces/IKnowledgeLibraryService.cs
@@ -0,0 +1,13 @@
+using Balsam.Model;
+
+namespace Balsam.Interfaces
+{
+ public interface IKnowledgeLibraryService
+ {
+ Task GetKnowledgeLibrary(string knowledgeLibraryId);
+ Task> GetRepositoryFileTree(string knowledgeLibraryId);
+ string GetRepositoryFilePath(string repositoryId, string fileId);
+ Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId);
+ Task> ListKnowledgeLibraries();
+ }
+}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Application/Interfaces/IProjectService.cs b/src/Balsam/src/Balsam.Application/Interfaces/IProjectService.cs
new file mode 100644
index 0000000..c1eafd9
--- /dev/null
+++ b/src/Balsam/src/Balsam.Application/Interfaces/IProjectService.cs
@@ -0,0 +1,20 @@
+using Balsam.Model;
+
+namespace Balsam.Interfaces
+{
+ public interface IProjectService
+ {
+ Task CopyFromKnowledgeLibrary(string projectId, string branchId, string knowledgeLibraryId, string fileId);
+ Task CreateBranch(string projectId, string fromBranch, string branchName, string description);
+ Task CreateProject(string preferredName, string description, string defaultBranchName, string username, string? sourceLocation = null);
+ Task DeleteBranch(string projectId, string branchId);
+ Task DeleteProject(string projectId);
+ Task GetBranch(string projectId, string branchId);
+ Task GetFile(string projectId, string branchId, string fileId);
+ Task?> GetGitBranchFiles(string projectId, string branchId);
+ Task GetProject(string projectId, bool includeBranches = true);
+ Task> GetProjects(bool includeBranches = true);
+
+ Task ProjectExists(string preferredName);
+ }
+}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Application/Interfaces/IWorkspaceService.cs b/src/Balsam/src/Balsam.Application/Interfaces/IWorkspaceService.cs
new file mode 100644
index 0000000..8b80e09
--- /dev/null
+++ b/src/Balsam/src/Balsam.Application/Interfaces/IWorkspaceService.cs
@@ -0,0 +1,19 @@
+
+using Balsam.Model;
+
+namespace Balsam.Interfaces
+{
+ public interface IWorkspaceService
+ {
+ Task CreateWorkspace(string projectId, string branchId, string name, string templateId, string userName, string userMail);
+ Task DeleteWorkspace(string projectId, string branchId, string workspaceId, string userName);
+ Task GetWorkspace(string projectId, string userId, string workspaceId);
+ Task> GetWorkspaces();
+ Task> GetWorkspacesByProject(string projectId);
+ Task> GetWorkspacesByProjectAndBranch(string projectId, string branchId);
+ Task> GetWorkspacesByProjectAndUser(string projectId, string userId);
+ Task> GetWorkspacesByProjectBranchAndUser(string projectId, string branchId, string userId);
+ Task> GetWorkspacesByUser(string userId, List projectIds);
+ Task> ListWorkspaceTemplates();
+ }
+}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Application/Services/KnowledgeLibraryService.cs b/src/Balsam/src/Balsam.Application/Services/KnowledgeLibraryService.cs
new file mode 100644
index 0000000..0999466
--- /dev/null
+++ b/src/Balsam/src/Balsam.Application/Services/KnowledgeLibraryService.cs
@@ -0,0 +1,43 @@
+using Balsam.Interfaces;
+using Balsam.Model;
+using Balsam.Repositories;
+
+namespace Balsam.Services
+{
+
+ public class KnowledgeLibraryService : IKnowledgeLibraryService
+ {
+ private readonly IKnowledgeLibraryRepository _knowledgeLibraryRepository;
+
+
+ public KnowledgeLibraryService(IKnowledgeLibraryRepository knowledgeLibraryRepository)
+ {
+ _knowledgeLibraryRepository = knowledgeLibraryRepository;
+ }
+
+ public async Task> ListKnowledgeLibraries()
+ {
+ return await _knowledgeLibraryRepository.ListKnowledgeLibraries();
+ }
+
+ public async Task GetKnowledgeLibrary(string knowledgeLibraryId)
+ {
+ return await _knowledgeLibraryRepository.GetKnowledgeLibrary(knowledgeLibraryId);
+ }
+
+ public async Task> GetRepositoryFileTree(string knowledgeLibraryId)
+ {
+ return await _knowledgeLibraryRepository.GetRepositoryFileTree(knowledgeLibraryId);
+ }
+
+ public string GetRepositoryFilePath(string repositoryId, string fileId)
+ {
+ return _knowledgeLibraryRepository.GetRepositoryFilePath(repositoryId, fileId);
+ }
+
+ public async Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId)
+ {
+ return await _knowledgeLibraryRepository.GetZippedResource(repositoryId, repositoryUrl, fileId);
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Application/Services/ProjectService.cs b/src/Balsam/src/Balsam.Application/Services/ProjectService.cs
new file mode 100644
index 0000000..ab4b5c4
--- /dev/null
+++ b/src/Balsam/src/Balsam.Application/Services/ProjectService.cs
@@ -0,0 +1,352 @@
+using Balsam.Repositories;
+using Balsam.Model;
+using Microsoft.Extensions.Logging;
+using S3ProviderApiClient.Api; //TODO: Split up generation of model and implementation and reference model.
+using OidcProviderApiClient.Api;
+using GitProviderApiClient.Api;
+using RocketChatChatProviderApiClient.Api;
+using Balsam.Interfaces;
+using GitProviderApiClient.Model;
+using Microsoft.Extensions.Options;
+using OidcProviderApiClient.Model;
+using S3ProviderApiClient.Model;
+using System.ComponentModel.DataAnnotations;
+using System.IO.Compression;
+using System.Runtime.InteropServices;
+using Balsam.Application.Extensions;
+
+namespace Balsam.Services
+{
+ public class ProjectService : IProjectService
+ {
+ private readonly CapabilityOptions _git;
+ private readonly CapabilityOptions _s3;
+ private readonly CapabilityOptions _authentication;
+ private readonly CapabilityOptions _chat;
+ private readonly IBucketApi _s3Client;
+ private readonly IGroupApi _oidcClient; //TODO: Rename IGroupAPI
+ private readonly IRepositoryApi _repositoryApi;
+ private readonly ILogger _logger;
+ private readonly IAreaApi _chatClient; //TODO: Rename IAreaApi
+ private readonly IProjectRepository _projectRepository;
+ private readonly IProjectGitOpsRepository _projectGitOpsRepository;
+ private readonly IKnowledgeLibraryService _knowledgeLibraryService;
+
+ public ProjectService(ILogger logger,
+ IOptionsSnapshot capabilityOptions,
+ IBucketApi s3Client,
+ IRepositoryApi reposiotryApi,
+ IGroupApi oidcClient,
+ IAreaApi chatClient,
+ IProjectRepository projectRepository,
+ IProjectGitOpsRepository projectGitOpsRepository,
+ IKnowledgeLibraryService knowledgeLibraryService)
+ {
+ _logger = logger;
+ _s3Client = s3Client;
+ _oidcClient = oidcClient;
+ _chatClient = chatClient;
+ _projectRepository = projectRepository;
+ _projectGitOpsRepository = projectGitOpsRepository;
+ _knowledgeLibraryService = knowledgeLibraryService;
+
+ _git = capabilityOptions.Get(Capabilities.Git);
+ _s3 = capabilityOptions.Get(Capabilities.S3);
+ _authentication = capabilityOptions.Get(Capabilities.Authentication);
+ _chat = capabilityOptions.Get(Capabilities.Chat);
+ _repositoryApi = reposiotryApi;
+
+ }
+
+ static ProjectService()
+ {
+
+ }
+
+ public async Task> GetProjects(bool includeBranches = true)
+ {
+ return await _projectRepository.GetProjects(includeBranches);
+ }
+
+ public async Task GetProject(string projectId, bool includeBranches = true)
+ {
+ return await _projectRepository.GetProject(projectId, includeBranches);
+ }
+
+ public async Task GetBranch(string projectId, string branchId)
+ {
+ return await _projectRepository.GetBranch(projectId, branchId);
+ }
+
+ public async Task ProjectExists(string preferredName)
+ {
+ return await _projectRepository.ProjectExists(preferredName);
+ }
+
+ public async Task CreateProject(string preferredName, string description, string defaultBranchName, string username, string? sourceLocation = null)
+ {
+ //Check if there is a program with the same name.
+
+ _logger.LogDebug("Check for duplicate names");
+ if (await _projectRepository.ProjectExists(preferredName))
+ {
+ string errorMessage = $"Could not create project {preferredName}, due to name duplication";
+ _logger.LogInformation(errorMessage);
+ throw new ApplicationException(errorMessage);
+ }
+
+ _logger.LogDebug($"create project information");
+ var project = new BalsamProject(preferredName, description);
+
+ project = await _projectRepository.CreateProject(project, defaultBranchName);
+
+ _logger.LogDebug($"Begin call service providers");
+
+ if (_authentication.Enabled)
+ {
+ _logger.LogDebug($"Begin call OpenIdConnect");
+ var oidcData = await _oidcClient.CreateGroupAsync(new CreateGroupRequest(project.Id));
+ await _oidcClient.AddUserToGroupAsync(oidcData.Id, new AddUserToGroupRequest(username));
+ project.Oidc = new OidcData(oidcData.Id, oidcData.Name);
+ _logger.LogInformation($"Group {project.Oidc.GroupName}({project.Oidc.GroupId}) created");
+
+ if (project.Oidc == null)
+ {
+ throw new Exception("Could not parse oidc data");
+ }
+ }
+
+ if (_git.Enabled)
+ {
+ _logger.LogDebug($"Begin call Git");
+ var gitData = await _repositoryApi.CreateRepositoryAsync(new CreateRepositoryRequest(preferredName, description, defaultBranchName));
+ //defaultBranchName = gitData.DefaultBranchName;
+ project.Git = new GitData() { Id = gitData.Id, Name = gitData.Name, Path = gitData.Path, SourceLocation = sourceLocation };
+ _logger.LogInformation($"Git repository {project.Git.Name} created");
+ }
+
+ if (_s3.Enabled)
+ {
+ _logger.LogDebug($"Begin call S3");
+ var s3Data = await _s3Client.CreateBucketAsync(new CreateBucketRequest(preferredName, project.Oidc.GroupName));
+ project.S3 = new S3Data() { BucketName = s3Data.Name };
+ _logger.LogInformation($"Bucket {project.S3.BucketName} created");
+ }
+
+ if (_chat.Enabled)
+ {
+ _logger.LogDebug("Begin the call to chatprovider");
+ var chatData = await _chatClient.CreateAreaAsync(new RocketChatChatProviderApiClient.Model.CreateAreaRequest(preferredName));
+ project.Chat = new ChatData(chatData.Id, chatData.Name);
+ _logger.LogInformation($"Channel created named {chatData.Name}");
+ }
+
+ await _projectRepository.UpdateProject(project);
+
+ await _projectGitOpsRepository.CreateProjectManifests(project);
+
+ _logger.LogInformation($"Project {project.Name}({project.Id}) created");
+ return project;
+ }
+
+ public async Task CreateBranch(string projectId, string fromBranchId, string branchName, string description)
+ {
+ var project = await GetProject(projectId);
+ var fromBranch = await GetBranch(projectId, fromBranchId);
+
+ if (fromBranch is null)
+ {
+ var errorMessage = $"Project with id {projectId} does not have a branch with id {fromBranchId}";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage, fromBranchId);
+ }
+
+ if (project is null)
+ {
+ var errorMessage = $"Project with id {projectId} does not exist";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage, projectId);
+ }
+
+ if (project.Git is null)
+ {
+ var errorMessage = $"Git-information not set for project with id {projectId}";
+ _logger.LogError(errorMessage);
+ throw new ApplicationException(errorMessage);
+ }
+
+ if (_git.Enabled)
+ {
+ var response = await _repositoryApi.CreateBranchAsync(project.Git.Id, new CreateBranchRequest(branchName, fromBranch.GitBranch));
+ branchName = response.Name;
+ }
+
+ return await _projectRepository.CreateBranch(projectId, branchName, description);
+ }
+
+ public async Task?> GetGitBranchFiles(string projectId, string branchId)
+ {
+ var project = await GetProject(projectId);
+
+ if(!_git.Enabled)
+ {
+ return new List();
+ }
+
+ if (project == null)
+ {
+ var errorMessage = $"Project with project id {projectId} does not exist";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage);
+ }
+
+ var branch = await GetBranch(projectId, branchId);
+
+ if (branch == null)
+ {
+ var errorMessage = $"Branch with branch id {branchId} in project {projectId} does not exist";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage);
+ }
+
+ if (project.Git is null)
+ {
+ var errorMessage = $"Git definition missing for project {projectId}";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage);
+ }
+
+
+ return (await _repositoryApi.GetFilesInBranchAsync(project.Git.Id, branch.GitBranch))
+ .Select(f => f.ToBalsamRepoFile())
+ .ToList();
+ }
+
+ public async Task GetFile(string projectId, string branchId, string fileId)
+ {
+ //TODO: Use GitProvider instead...
+ return await _projectRepository.GetFile(projectId, branchId, fileId);
+ }
+
+ public async Task DeleteBranch(string projectId, string branchId)
+ {
+ //var branchPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId, branchId);
+ _logger.LogDebug($"Deleting branch {branchId} for project {projectId}");
+
+
+ if (_git.Enabled)
+ {
+ var project = await GetProject(projectId);
+ var branch = await GetBranch(projectId, branchId);
+
+ //Asure that the id are correct
+ if (project == null || branch == null) return;
+
+ try
+ {
+ _logger.LogDebug($"Deleting branch in git");
+ await _repositoryApi.DeleteRepositoryBranchAsync(project.Git?.Id ?? "", branch.GitBranch);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not delete git branch");
+ }
+ }
+
+ _logger.LogDebug($"Deleting branch information");
+ await _projectRepository.DeleteBranch(projectId, branchId);
+ _logger.LogDebug($"Deleting branch manifests");
+
+ await _projectGitOpsRepository.DeleteBranchManifests(projectId, branchId);
+
+ _logger.LogInformation($"Deleted branch {branchId} for project {projectId}");
+ }
+
+ public async Task DeleteProject(string projectId)
+ {
+ var project = await GetProject(projectId);
+
+ if (project == null)
+ {
+ return;
+ }
+
+ //TODO: Delete oidc group?
+
+ if (_authentication.Enabled)
+ {
+ try
+ {
+ await _oidcClient.DeleteGroupAsync(project.Oidc?.GroupId ?? "");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not delete oidc group");
+ }
+ }
+
+ if (_git.Enabled)
+ {
+ try
+ {
+ await _repositoryApi.DeleteRepositoryAsync(project.Git?.Id ?? "");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not delete git repository");
+ }
+ }
+
+ if (_s3.Enabled)
+ {
+ try
+ {
+ await _s3Client.DeleteBucketAsync(project.S3?.BucketName ?? "");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not delete s3 bucket");
+ }
+ }
+
+ await _projectRepository.DeleteProject(projectId);
+ await _projectGitOpsRepository.DeleteProjectManifests(projectId);
+
+ _logger.LogInformation($"Project {projectId} deleted");
+ }
+
+ public async Task CopyFromKnowledgeLibrary(string projectId, string branchId, string knowledgeLibraryId, string fileId)
+ {
+ var project = await GetProject(projectId);
+
+ if (project is null)
+ {
+ var errorMessage = $"Project with id {projectId} does not exist";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage, projectId);
+ }
+
+ if (project.Git is null)
+ {
+ var errorMessage = $"Git-information not set for project with id {projectId}";
+ _logger.LogError(errorMessage);
+ throw new ApplicationException(errorMessage);
+ }
+
+ var branch = project.Branches.First(b => b.Id == branchId);
+ var knowledgeLibrary = await _knowledgeLibraryService.GetKnowledgeLibrary(knowledgeLibraryId);
+ var zipFile = await _knowledgeLibraryService.GetZippedResource(knowledgeLibraryId, knowledgeLibrary.RepositoryUrl, fileId);
+
+ try
+ {
+ using var stream = System.IO.File.OpenRead(zipFile);
+ await _repositoryApi.AddResourceFilesAsync(project.Git.Id, branch.GitBranch, stream);
+ }
+ finally
+ {
+ System.IO.File.Delete(zipFile);
+ }
+
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Application/Services/WorkspaceService.cs b/src/Balsam/src/Balsam.Application/Services/WorkspaceService.cs
new file mode 100644
index 0000000..58360c8
--- /dev/null
+++ b/src/Balsam/src/Balsam.Application/Services/WorkspaceService.cs
@@ -0,0 +1,176 @@
+using Balsam.Interfaces;
+using Balsam.Model;
+using Balsam.Repositories;
+using GitProviderApiClient.Api;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using S3ProviderApiClient.Api;
+using S3ProviderApiClient.Model;
+
+namespace Balsam.Services
+{
+ public class WorkspaceService : IWorkspaceService
+ {
+ private readonly CapabilityOptions _git;
+ private readonly CapabilityOptions _s3;
+ private readonly IWorkspaceRepository _workspaceRepository;
+ private readonly ILogger _logger;
+ private readonly IBucketApi _s3Client;
+ private readonly IUserApi _gitUserClient;
+ private readonly IProjectRepository _projectRepository; //TODO: User ProjectService instead?
+ private readonly IWorkspaceGitOpsRepository _workspaceGitOpsService; //TODO: Make and use interface
+
+ public WorkspaceService(IOptionsSnapshot capabilityOptions,
+ IWorkspaceRepository workspaceRepository,
+ ILogger logger,
+ IBucketApi s3Client,
+ IWorkspaceGitOpsRepository workspaceGitOpsService,
+ IUserApi gitUserClient,
+ IProjectRepository projectRepository)
+ {
+ _git = capabilityOptions.Get(Capabilities.Git);
+ _s3 = capabilityOptions.Get(Capabilities.S3);
+ _workspaceRepository = workspaceRepository;
+ _logger = logger;
+ _s3Client = s3Client;
+ _workspaceGitOpsService = workspaceGitOpsService;
+ _gitUserClient = gitUserClient;
+ _projectRepository = projectRepository;
+ }
+
+
+ public async Task CreateWorkspace(string projectId, string branchId, string name, string templateId, string userName, string userMail)
+ {
+ var workspaceId = _workspaceRepository.GenerateWorkspaceId(name);
+ var workspace = new BalsamWorkspace(workspaceId, name, templateId, projectId, branchId, userName);
+
+ await CreateWorkspaceManifests(projectId, branchId, templateId, userName, userMail, workspace);
+
+ var template = await _workspaceRepository.GetWorkspaceTemplate(workspace.TemplateId);
+
+ if (template != null)
+ {
+ workspace.Url = await _workspaceGitOpsService.GetWorkspaceUrl(projectId, branchId, userName, workspaceId, template.UrlConfig);
+ }
+ else
+ {
+ var errorMessage = $"Template with id {templateId} does not exist";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage);
+ }
+
+ await _workspaceRepository.CreateWorkspace(workspace);
+
+ _logger.LogInformation("Workspace created");
+ return workspace;
+ }
+
+ private async Task CreateWorkspaceManifests(string projectId, string branchId, string templateId, string userName, string userMail, BalsamWorkspace workspace)
+ {
+ var project = await _projectRepository.GetProject(projectId);
+
+ if (project == null)
+ {
+ var errorMessage = $"Project with project id {projectId} does not exist";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage);
+ }
+
+ var gitPAT = string.Empty;
+
+ if (_git.Enabled)
+ {
+ var patResponse = await _gitUserClient.CreatePATAsync(userName);
+ gitPAT = patResponse.Token;
+ _logger.LogInformation($"Git PAT created");
+ }
+
+ var user = new UserInfo(userName, userMail, gitPAT);
+
+ var token = new AccessKeyCreatedResponse("", "");
+ if (_s3.Enabled && project.S3 != null)
+ {
+ token = await _s3Client.CreateAccessKeyAsync(project.S3.BucketName);
+
+ var s3Token = new S3Token(token.AccessKey, token.SecretKey);
+ user.S3 = s3Token;
+ }
+
+ _logger.LogDebug($"Copying manifests");
+
+ var branch = project.Branches.FirstOrDefault(b => b.Id == branchId);
+
+ if (branch != null)
+ {
+ await _workspaceGitOpsService.CreateWorkspaceManifests(project, branch, workspace, user, templateId);
+ }
+ else
+ {
+ var errorMessage = $"Branch with branch id {branchId} in project {projectId} does not exist";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage);
+ }
+
+ _logger.LogInformation("Manifests persisted");
+ }
+
+ public async Task DeleteWorkspace(string projectId, string branchId, string workspaceId, string userName)
+ {
+ await _workspaceRepository.DeleteWorkspace(projectId, branchId, workspaceId, userName);
+
+ await _workspaceGitOpsService.DeleteWorkspaceManifests(projectId, branchId, workspaceId, userName);
+
+ }
+
+ public async Task> ListWorkspaceTemplates()
+ {
+ _logger.LogDebug("Start ListWorkspaceTemplates");
+
+ var workspaceTemplates = await _workspaceRepository.ListWorkspaceTemplates();
+
+ if (workspaceTemplates.Count() == 0)
+ {
+ _logger.LogInformation("No workspace template folder found!");
+
+ }
+ _logger.LogDebug("End ListWorkspaceTemplates");
+
+ return workspaceTemplates;
+ }
+
+ public async Task> GetWorkspaces()
+ {
+ return await _workspaceRepository.GetWorkspaces();
+ }
+
+ public async Task> GetWorkspacesByUser(string userId, List projectIds)
+ {
+ return await _workspaceRepository.GetWorkspacesByUser(userId, projectIds);
+ }
+
+ public async Task> GetWorkspacesByProject(string projectId)
+ {
+ return await _workspaceRepository.GetWorkspacesByProject(projectId);
+ }
+
+ public async Task> GetWorkspacesByProjectAndBranch(string projectId, string branchId)
+ {
+ return await _workspaceRepository.GetWorkspacesByProjectAndBranch(projectId, branchId);
+ }
+
+ public async Task> GetWorkspacesByProjectAndUser(string projectId, string userId)
+ {
+ return await _workspaceRepository.GetWorkspacesByProjectAndUser(projectId, userId);
+ }
+
+ public async Task> GetWorkspacesByProjectBranchAndUser(string projectId, string branchId, string userId)
+ {
+ return await _workspaceRepository.GetWorkspacesByProjectBranchAndUser(projectId, branchId, userId);
+ }
+
+ public async Task GetWorkspace(string projectId, string userId, string workspaceId)
+ {
+ return await _workspaceRepository.GetWorkspace(projectId, userId, workspaceId);
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Domain/Balsam.Domain.csproj b/src/Balsam/src/Balsam.Domain/Balsam.Domain.csproj
new file mode 100644
index 0000000..bf34ca9
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Balsam.Domain.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/src/Balsam/src/Balsam.Api/Models/BalsamBranch.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamBranch.cs
similarity index 89%
rename from src/Balsam/src/Balsam.Api/Models/BalsamBranch.cs
rename to src/Balsam/src/Balsam.Domain/Model/BalsamBranch.cs
index 0a936de..3b56b04 100644
--- a/src/Balsam/src/Balsam.Api/Models/BalsamBranch.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/BalsamBranch.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class BalsamBranch
{
diff --git a/src/Balsam/src/Balsam.Api/Models/BalsamContext.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamContext.cs
similarity index 60%
rename from src/Balsam/src/Balsam.Api/Models/BalsamContext.cs
rename to src/Balsam/src/Balsam.Domain/Model/BalsamContext.cs
index 9971c29..273b5fb 100644
--- a/src/Balsam/src/Balsam.Api/Models/BalsamContext.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/BalsamContext.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class BalsamContext
{
diff --git a/src/Balsam/src/Balsam.Domain/Model/BalsamKnowledgeLibrary.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamKnowledgeLibrary.cs
new file mode 100644
index 0000000..d39f219
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Model/BalsamKnowledgeLibrary.cs
@@ -0,0 +1,15 @@
+namespace Balsam.Model
+{
+ public class BalsamKnowledgeLibrary
+ {
+ public string Id { get; set; }
+
+ public string Name { get; set; }
+
+ public string? Description { get; set; }
+
+ public string RepositoryUrl { get; set; }
+
+ public string? RepositoryFriendlyUrl { get; set; }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Models/BalsamProject.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamProject.cs
similarity index 81%
rename from src/Balsam/src/Balsam.Api/Models/BalsamProject.cs
rename to src/Balsam/src/Balsam.Domain/Model/BalsamProject.cs
index fed35ee..acf6330 100644
--- a/src/Balsam/src/Balsam.Api/Models/BalsamProject.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/BalsamProject.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class BalsamProject
{
@@ -12,9 +12,8 @@ public class BalsamProject
public List Branches { get; set; }
- public BalsamProject(string id, string name, string description)
+ public BalsamProject(string name, string description)
{
- Id = id;
Name = name;
Description = description;
Branches = new List();
diff --git a/src/Balsam/src/Balsam.Domain/Model/BalsamRepoFile.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamRepoFile.cs
new file mode 100644
index 0000000..c06783b
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Model/BalsamRepoFile.cs
@@ -0,0 +1,34 @@
+using System.Runtime.Serialization;
+
+namespace Balsam.Model
+{
+ //TODO: Rename to RepoFileTreeNode or something
+ public class BalsamRepoFile
+ {
+ public string Id { get; set; }
+
+ public string Path { get; set; }
+
+ public string Name { get; set; }
+
+ public enum TypeEnum
+ {
+
+ ///
+ /// Enum FileEnum for File
+ ///
+ [EnumMember(Value = "File")]
+ FileEnum = 1,
+
+ ///
+ /// Enum FolderEnum for Folder
+ ///
+ [EnumMember(Value = "Folder")]
+ FolderEnum = 2
+ }
+
+ public TypeEnum Type { get; set; }
+
+ public string? ContentUrl { get; set; }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Models/BalsamWorkspace.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamWorkspace.cs
similarity index 95%
rename from src/Balsam/src/Balsam.Api/Models/BalsamWorkspace.cs
rename to src/Balsam/src/Balsam.Domain/Model/BalsamWorkspace.cs
index 36817d9..adeed67 100644
--- a/src/Balsam/src/Balsam.Api/Models/BalsamWorkspace.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/BalsamWorkspace.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class BalsamWorkspace
{
diff --git a/src/Balsam/src/Balsam.Domain/Model/CapabilityOptions.cs b/src/Balsam/src/Balsam.Domain/Model/CapabilityOptions.cs
new file mode 100644
index 0000000..e2f152b
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Model/CapabilityOptions.cs
@@ -0,0 +1,17 @@
+namespace Balsam.Model
+{
+ public class CapabilityOptions
+ {
+ public bool Enabled { get; set; }
+ public string? ServiceLocation { get; set; }
+ public string[]? Actions { get; set; }
+ }
+
+ public class Capabilities
+ {
+ public const string Git = "Git";
+ public const string S3 = "S3";
+ public const string Authentication = "Authentication";
+ public const string Chat = "Chat";
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Models/ChatData.cs b/src/Balsam/src/Balsam.Domain/Model/ChatData.cs
similarity index 88%
rename from src/Balsam/src/Balsam.Api/Models/ChatData.cs
rename to src/Balsam/src/Balsam.Domain/Model/ChatData.cs
index 9a37875..a0cddf3 100644
--- a/src/Balsam/src/Balsam.Api/Models/ChatData.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/ChatData.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class ChatData
{
diff --git a/src/Balsam/src/Balsam.Domain/Model/FileContent.cs b/src/Balsam/src/Balsam.Domain/Model/FileContent.cs
new file mode 100644
index 0000000..9a99e41
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Model/FileContent.cs
@@ -0,0 +1,20 @@
+namespace Balsam.Model
+{
+ public class FileContent
+ {
+
+ public byte[]? Content { get; set; }
+ public string Mediatype { get; set; } = string.Empty;
+
+ public FileContent(byte[]? content, string mediatype)
+ {
+ Content = content;
+ Mediatype = mediatype;
+ }
+
+ public FileContent()
+ {
+
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Models/GitData.cs b/src/Balsam/src/Balsam.Domain/Model/GitData.cs
similarity index 61%
rename from src/Balsam/src/Balsam.Api/Models/GitData.cs
rename to src/Balsam/src/Balsam.Domain/Model/GitData.cs
index 7b9d71a..c639864 100644
--- a/src/Balsam/src/Balsam.Api/Models/GitData.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/GitData.cs
@@ -1,13 +1,13 @@
-using System.Security.Principal;
-
-namespace Balsam.Api.Models
-{
- public class GitData
- {
- public string Id { get; set; }
- public string Name { get; set; }
- public string Path { get; set; }
- public string? SourceLocation { get; set; }
- public bool HasTemplate { get { return !string.IsNullOrEmpty(SourceLocation); } }
- }
-}
+using System.Security.Principal;
+
+namespace Balsam.Model
+{
+ public class GitData
+ {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public string Path { get; set; }
+ public string? SourceLocation { get; set; } //TODO: What is this used for?
+ public bool HasTemplate { get { return !string.IsNullOrEmpty(SourceLocation); } } //TODO: What is this used for?
+ }
+}
diff --git a/src/Balsam/src/Balsam.Domain/Model/HubPaths.cs b/src/Balsam/src/Balsam.Domain/Model/HubPaths.cs
new file mode 100644
index 0000000..4b31539
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Model/HubPaths.cs
@@ -0,0 +1,67 @@
+
+namespace Balsam.Model
+{
+ public class HubPaths
+ {
+ public string BasePath { get; }
+
+ public HubPaths(string basePath)
+ {
+ BasePath = basePath;
+ }
+
+ public string GetTemplatesPath()
+ {
+ return Path.Combine(BasePath, "templates");
+ }
+
+ public string GetHubPath()
+ {
+ return Path.Combine(BasePath, "hub");
+ }
+
+ public string GetProjectPath(string projectId)
+ {
+ return Path.Combine(GetHubPath(), projectId);
+ }
+
+ public string GetBranchPath(string projectId, string branchId)
+ {
+ return Path.Combine(GetProjectPath(projectId), branchId);
+ }
+ public string GetUserPath(string projectId, string branchId, string userName)
+ {
+ return Path.Combine(GetBranchPath(projectId, branchId), userName);
+ }
+
+ public string GetWorkspacePath(string projectId, string branchId, string userName, string workspaceId)
+ {
+ return Path.Combine(GetUserPath(projectId, branchId, userName), workspaceId);
+ }
+
+ public string GetWorkspaceTemplatesPath(string templateId)
+ {
+ return Path.Combine(GetWorkspacesTemplatesPath(), templateId);
+ }
+
+ public string GetProjectsTemplatesPath()
+ {
+ return Path.Combine(GetTemplatesPath(), "projects");
+ }
+
+ public string GetWorkspacesTemplatesPath()
+ {
+ return Path.Combine(GetTemplatesPath(), "workspaces");
+ }
+
+ public string GetKnowledgeLibrariesPath()
+ {
+ return Path.Combine(BasePath, "kb");
+ }
+
+ public string GetKnowledgeLibraryFilePath(string knowledgeLibraryFileId)
+ {
+ return Path.Combine(GetKnowledgeLibrariesPath(), $"{knowledgeLibraryFileId}.json");
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Models/HubRepositoryOptions.cs b/src/Balsam/src/Balsam.Domain/Model/HubRepositoryOptions.cs
similarity index 90%
rename from src/Balsam/src/Balsam.Api/Models/HubRepositoryOptions.cs
rename to src/Balsam/src/Balsam.Domain/Model/HubRepositoryOptions.cs
index 56a9467..d41552d 100644
--- a/src/Balsam/src/Balsam.Api/Models/HubRepositoryOptions.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/HubRepositoryOptions.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class HubRepositoryOptions
{
diff --git a/src/Balsam/src/Balsam.Api/Models/OidcData.cs b/src/Balsam/src/Balsam.Domain/Model/OidcData.cs
similarity index 89%
rename from src/Balsam/src/Balsam.Api/Models/OidcData.cs
rename to src/Balsam/src/Balsam.Domain/Model/OidcData.cs
index 2f0ff78..890d799 100644
--- a/src/Balsam/src/Balsam.Api/Models/OidcData.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/OidcData.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class OidcData
{
diff --git a/src/Balsam/src/Balsam.Api/Models/ProjectContext.cs b/src/Balsam/src/Balsam.Domain/Model/ProjectContext.cs
similarity index 78%
rename from src/Balsam/src/Balsam.Api/Models/ProjectContext.cs
rename to src/Balsam/src/Balsam.Domain/Model/ProjectContext.cs
index f978b2f..8f843b1 100644
--- a/src/Balsam/src/Balsam.Api/Models/ProjectContext.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/ProjectContext.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class ProjectContext : BalsamContext
{
diff --git a/src/Balsam/src/Balsam.Api/Models/S3Data.cs b/src/Balsam/src/Balsam.Domain/Model/S3Data.cs
similarity index 73%
rename from src/Balsam/src/Balsam.Api/Models/S3Data.cs
rename to src/Balsam/src/Balsam.Domain/Model/S3Data.cs
index bcb86ab..f7bc95e 100644
--- a/src/Balsam/src/Balsam.Api/Models/S3Data.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/S3Data.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class S3Data
{
diff --git a/src/Balsam/src/Balsam.Api/Models/S3Token.cs b/src/Balsam/src/Balsam.Domain/Model/S3Token.cs
similarity index 90%
rename from src/Balsam/src/Balsam.Api/Models/S3Token.cs
rename to src/Balsam/src/Balsam.Domain/Model/S3Token.cs
index 9e90fc7..1935731 100644
--- a/src/Balsam/src/Balsam.Api/Models/S3Token.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/S3Token.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class S3Token
{
diff --git a/src/Balsam/src/Balsam.Api/Models/UserInfo.cs b/src/Balsam/src/Balsam.Domain/Model/UserInfo.cs
similarity index 92%
rename from src/Balsam/src/Balsam.Api/Models/UserInfo.cs
rename to src/Balsam/src/Balsam.Domain/Model/UserInfo.cs
index f6a0958..8c1760c 100644
--- a/src/Balsam/src/Balsam.Api/Models/UserInfo.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/UserInfo.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models
+namespace Balsam.Model
{
public class UserInfo
{
diff --git a/src/Balsam/src/Balsam.Domain/Model/WorkspaceContext.cs b/src/Balsam/src/Balsam.Domain/Model/WorkspaceContext.cs
new file mode 100644
index 0000000..2cf4633
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Model/WorkspaceContext.cs
@@ -0,0 +1,17 @@
+namespace Balsam.Model;
+
+public class WorkspaceContext : BalsamContext
+{
+ public BalsamProject Project { get; set; }
+ public BalsamBranch Branch { get; set; }
+ public BalsamWorkspace Workspace { get; set; }
+ public UserInfo User { get; set; }
+
+ public WorkspaceContext(BalsamProject project, BalsamBranch branch, BalsamWorkspace workspace, UserInfo user)
+ {
+ Project = project;
+ Branch = branch;
+ Workspace = workspace;
+ User = user;
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/Models/WorkspaceTemplate.cs b/src/Balsam/src/Balsam.Domain/Model/WorkspaceTemplate.cs
similarity index 85%
rename from src/Balsam/src/Balsam.Api/Models/WorkspaceTemplate.cs
rename to src/Balsam/src/Balsam.Domain/Model/WorkspaceTemplate.cs
index a514510..16dee59 100644
--- a/src/Balsam/src/Balsam.Api/Models/WorkspaceTemplate.cs
+++ b/src/Balsam/src/Balsam.Domain/Model/WorkspaceTemplate.cs
@@ -1,4 +1,4 @@
-namespace Balsam.Api.Models;
+namespace Balsam.Model;
public class WorkspaceTemplate
{
diff --git a/src/Balsam/src/Balsam.Domain/Repository/IHubRepositoryClient.cs b/src/Balsam/src/Balsam.Domain/Repository/IHubRepositoryClient.cs
new file mode 100644
index 0000000..e7a7c13
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Repository/IHubRepositoryClient.cs
@@ -0,0 +1,9 @@
+namespace Balsam.Repositories
+{
+ public interface IHubRepositoryClient
+ {
+ string Path { get; }
+ void PersistChanges(string commitMessage);
+ void PullChanges();
+ }
+}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryContentRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryContentRepository.cs
new file mode 100644
index 0000000..28a378c
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryContentRepository.cs
@@ -0,0 +1,11 @@
+using Balsam.Model;
+
+namespace Balsam.Repositories
+{
+ public interface IKnowledgeLibraryContentRepository
+ {
+ Task> GetFileTree(string repositoryId, string repositoryUrl);
+ string GetFilePath(string repositoryId, string fileId);
+ Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId);
+ }
+}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryRepository.cs
new file mode 100644
index 0000000..a2be31b
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryRepository.cs
@@ -0,0 +1,13 @@
+using Balsam.Model;
+
+namespace Balsam.Repositories
+{
+ public interface IKnowledgeLibraryRepository
+ {
+ Task GetKnowledgeLibrary(string knowledgeLibraryId);
+ Task> GetRepositoryFileTree(string knowledgeLibraryId);
+ string GetRepositoryFilePath(string repositoryId, string fileId);
+ Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId);
+ Task> ListKnowledgeLibraries();
+ }
+}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Domain/Repository/IProjectGitOpsRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IProjectGitOpsRepository.cs
new file mode 100644
index 0000000..8fcab3b
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Repository/IProjectGitOpsRepository.cs
@@ -0,0 +1,11 @@
+using Balsam.Model;
+
+namespace Balsam.Repositories
+{
+ public interface IProjectGitOpsRepository
+ {
+ Task CreateProjectManifests(BalsamProject project);
+ Task DeleteBranchManifests(string projectId, string branchId);
+ Task DeleteProjectManifests(string projectId);
+ }
+}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Domain/Repository/IProjectRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IProjectRepository.cs
new file mode 100644
index 0000000..73ac35d
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Repository/IProjectRepository.cs
@@ -0,0 +1,21 @@
+
+using Balsam.Model;
+
+namespace Balsam.Repositories
+{
+ public interface IProjectRepository
+ {
+ Task CreateBalsamBranchFromGitBranch(string projectId, string gitBranchName);
+ Task CreateBranch(string projectId, string branchName, string description);
+ Task CreateProject(BalsamProject project, string defaultBranchName);
+ Task DeleteBranch(string projectId, string branchId);
+ Task DeleteProject(string projectId);
+ Task GetBranch(string projectId, string branchId);
+ //Task GetProject(string projectId);
+ Task GetProject(string projectId, bool includeBranches = true);
+ Task> GetProjects(bool includeBranches = true);
+ Task ProjectExists(string projectName);
+ Task GetFile(string projectId, string branchId, string fileId);
+ Task UpdateProject(BalsamProject project);
+ }
+}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceGitOpsRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceGitOpsRepository.cs
new file mode 100644
index 0000000..986623e
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceGitOpsRepository.cs
@@ -0,0 +1,12 @@
+
+using Balsam.Model;
+
+namespace Balsam.Repositories
+{
+ public interface IWorkspaceGitOpsRepository
+ {
+ Task CreateWorkspaceManifests(BalsamProject project, BalsamBranch branch, BalsamWorkspace workspace, UserInfo user, string templateId);
+ Task DeleteWorkspaceManifests(string projectId, string branchId, string workspaceId, string userName);
+ Task GetWorkspaceUrl(string projectId, string branchId, string userName, string workspaceId, string urlConfigFile);
+ }
+}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceRepository.cs
new file mode 100644
index 0000000..c1081e4
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceRepository.cs
@@ -0,0 +1,21 @@
+
+using Balsam.Model;
+
+namespace Balsam.Repositories
+{
+ public interface IWorkspaceRepository
+ {
+ Task CreateWorkspace(BalsamWorkspace workspace);
+ Task DeleteWorkspace(string projectId, string branchId, string workspaceId, string userName);
+ string GenerateWorkspaceId(string name);
+ Task GetWorkspace(string projectId, string userId, string workspaceId);
+ Task> GetWorkspaces();
+ Task> GetWorkspacesByProject(string projectId);
+ Task> GetWorkspacesByProjectAndBranch(string projectId, string branchId);
+ Task> GetWorkspacesByProjectAndUser(string projectId, string userId);
+ Task> GetWorkspacesByProjectBranchAndUser(string projectId, string branchId, string userId);
+ Task> GetWorkspacesByUser(string userId, List projectIds);
+ Task GetWorkspaceTemplate(string templateId);
+ Task> ListWorkspaceTemplates();
+ }
+}
\ No newline at end of file
diff --git a/src/Balsam/src/Balsam.Domain/Utility/NameUtil.cs b/src/Balsam/src/Balsam.Domain/Utility/NameUtil.cs
new file mode 100644
index 0000000..3a3fd73
--- /dev/null
+++ b/src/Balsam/src/Balsam.Domain/Utility/NameUtil.cs
@@ -0,0 +1,25 @@
+using System.Text.RegularExpressions;
+
+namespace Balsam.Utility
+{
+ public static class NameUtil
+ {
+ public static string SanitizeName(string name)
+ {
+ //TODO: Combine with GenerateWorkspaceId
+ var crc32 = new System.IO.Hashing.Crc32();
+
+ crc32.Append(System.Text.Encoding.ASCII.GetBytes(name));
+ var hash = crc32.GetCurrentHash();
+ var crcHash = string.Join("", hash.Select(b => b.ToString("x2").ToLower()).Reverse());
+
+ name = name.ToLower(); //Only lower charachters allowed
+ name = name.Replace(" ", "-"); //replaces spaches with hypen
+ name = Regex.Replace(name, @"[^a-z0-9\-]", ""); // make sure that only a-z or digit or hypen removes all other characters
+ name = name.Substring(0, Math.Min(50 - crcHash.Length, name.Length)) + "-" + crcHash; //Assures max size of 50 characters
+
+ return name;
+
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Infrastructure/Balsam.Infrastructure.csproj b/src/Balsam/src/Balsam.Infrastructure/Balsam.Infrastructure.csproj
new file mode 100644
index 0000000..3d9c28e
--- /dev/null
+++ b/src/Balsam/src/Balsam.Infrastructure/Balsam.Infrastructure.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/GitOpsRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/GitOpsRepository.cs
new file mode 100644
index 0000000..a491a63
--- /dev/null
+++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/GitOpsRepository.cs
@@ -0,0 +1,69 @@
+using Balsam.Model;
+using Balsam.Utility;
+using HandlebarsDotNet;
+
+namespace Balsam.Repositories
+{
+ public class GitOpsRepository
+ {
+ protected readonly IHubRepositoryClient HubRepositoryClient;
+ protected HubPaths HubPaths { get; set; }
+
+
+ public GitOpsRepository(IHubRepositoryClient hubRepositoryClient)
+ {
+ HubRepositoryClient = hubRepositoryClient;
+ HubPaths = new HubPaths(HubRepositoryClient.Path);
+
+ Handlebars.RegisterHelper("curlies", (writer, context, parameters) =>
+ {
+ if (parameters.Length == 1 && parameters.At(0) == true)
+ {
+ writer.Write("{{");
+ }
+ else
+ {
+ writer.Write("}}");
+ }
+
+ });
+ }
+
+ ///
+ /// Copies manifest-templates and replaces templated variables like {{variable}} with context value
+ ///
+ /// The Context for the template
+ /// The destination path for the manifest
+ /// The path to the template relative to the path of all templates
+ ///
+ protected async Task CreateManifests(BalsamContext context, string destinationPath, string templatePath)
+ {
+
+ //var templatesPath = HubPaths.GetTemplatesPath();
+ //var templateFullPath = Path.Combine(templatesPath, templateRelativePath);
+
+ DirectoryUtil.AssureDirectoryExists(destinationPath);
+
+ foreach (var file in Directory.GetFiles(templatePath, "*.yaml"))
+ {
+ var source = await File.ReadAllTextAsync(file);
+
+ var template = Handlebars.Compile(source);
+
+ var result = template(context);
+
+ var destinationFilePath = Path.Combine(destinationPath, Path.GetFileName(file));
+
+ await File.WriteAllTextAsync(destinationFilePath, result);
+ }
+ }
+
+ protected async Task DeleteManifests(string path)
+ {
+ if (Directory.Exists(path))
+ {
+ await Task.Run(() => Directory.Delete(path, true));
+ }
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Api/HubRepositoryClient.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/HubRepositoryClient.cs
similarity index 73%
rename from src/Balsam/src/Balsam.Api/HubRepositoryClient.cs
rename to src/Balsam/src/Balsam.Infrastructure/Repositories/HubRepositoryClient.cs
index 777a3c6..69e15ed 100644
--- a/src/Balsam/src/Balsam.Api/HubRepositoryClient.cs
+++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/HubRepositoryClient.cs
@@ -1,10 +1,12 @@
-using Balsam.Api.Models;
+using Balsam.Model;
+using Balsam.Utility;
using LibGit2Sharp;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-namespace Balsam.Api
+namespace Balsam.Repositories
{
- public class HubRepositoryClient
+ public class HubRepositoryClient : IHubRepositoryClient
{
private readonly string _repositoryPath;
private readonly Repository _repository;
@@ -15,15 +17,17 @@ public class HubRepositoryClient
public string Path { get { return _repositoryPath; } }
- public HubRepositoryClient(IOptions options, ILogger logger) {
+ public HubRepositoryClient(IOptions options, ILogger logger)
+ {
_logger = logger;
_userId = options.Value.User;
_password = options.Value.Password;
- _mail = options.Value.Mail??"noreply@balsam.local";
+ _mail = options.Value.Mail ?? "noreply@balsam.local";
//Assure credentials
- if (string.IsNullOrEmpty(_userId) || string.IsNullOrEmpty(_password)) {
+ if (string.IsNullOrEmpty(_userId) || string.IsNullOrEmpty(_password))
+ {
_logger.LogWarning("Missing hub repository credentials");
}
@@ -37,17 +41,22 @@ public HubRepositoryClient(IOptions options, ILogger new UsernamePasswordCredentials { Username = _userId, Password = _password }
+ //}
CredentialsProvider = (_url, _user, _cred) => new UsernamePasswordCredentials { Username = _userId, Password = _password }
};
_logger.LogInformation("Begin clone Hub repo");
+
_repositoryPath = Repository.Clone(options.Value.RemoteUrl, basePath, cloneOptions);
_logger.LogInformation("End clone Hub repo");
_repository = new Repository(_repositoryPath);
_repositoryPath = basePath;
}
-
+
public void PersistChanges(string commitMessage)
{
var pushOptions = new PushOptions();
@@ -58,11 +67,15 @@ public void PersistChanges(string commitMessage)
Commands.Stage(_repository, "*");
- _repository.Commit(commitMessage, namesig, namesig);
+ RepositoryStatus status = _repository.RetrieveStatus();
+
+ if (status.IsDirty) {
+ _repository.Commit(commitMessage, namesig, namesig);
- var remote = _repository.Network.Remotes["origin"];
+ var remote = _repository.Network.Remotes["origin"];
- _repository.Network.Push(remote, _repository.Head.CanonicalName, pushOptions);
+ _repository.Network.Push(remote, _repository.Head.CanonicalName, pushOptions);
+ }
}
public void PullChanges()
diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryContentRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryContentRepository.cs
new file mode 100644
index 0000000..373e9ea
--- /dev/null
+++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryContentRepository.cs
@@ -0,0 +1,196 @@
+using Balsam.Model;
+using LibGit2Sharp;
+using System.IO.Compression;
+using System.Text;
+
+namespace Balsam.Repositories
+{
+ public class KnowledgeLibraryContentRepository : IKnowledgeLibraryContentRepository
+ {
+ public async Task> GetFileTree(string repositoryId, string repositoryUrl)
+ {
+ string localRepositoryPath = GetLocalRepositoryPath(repositoryId);
+
+ await CloneOrPull(localRepositoryPath, repositoryUrl);
+
+ var repositoryContents = new List();
+
+ await GetFileTree(localRepositoryPath, localRepositoryPath.Length + 1, repositoryContents);
+
+ return repositoryContents;
+ }
+
+ private static string GetLocalRepositoryPath(string repositoryId)
+ {
+ return Path.Combine(Path.GetTempPath(), "kb", repositoryId);
+ }
+
+ public string GetFilePath(string repositoryId, string fileId)
+ {
+ string relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(fileId));
+
+ if (relativePath.Contains(".."))
+ {
+ throw new ArgumentException("Invalid file id");
+ }
+
+ string localRepositoryPath = GetLocalRepositoryPath(repositoryId);
+
+ string filePath = Path.Combine(localRepositoryPath, relativePath);
+
+ if (!File.Exists(filePath))
+ {
+ throw new ArgumentException("File not found");
+ }
+
+ return filePath;
+
+ }
+
+ ///
+ /// Zips a resoruce (file or directory) and returns a path to the ziped file
+ ///
+ public async Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId)
+ {
+ //Make sure that the repository is cloned and up to date
+ string localRepositoryPath = GetLocalRepositoryPath(repositoryId);
+
+ await CloneOrPull(localRepositoryPath, repositoryUrl);
+
+ //Make sure that the file/directory exists in the repository
+ string relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(fileId));
+ if (relativePath.Contains(".."))
+ {
+ throw new ArgumentException("Invalid file id");
+ }
+
+
+ string filePath = Path.Combine(localRepositoryPath, relativePath);
+
+ string workPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+
+ Directory.CreateDirectory(workPath);
+
+
+ if (File.Exists(filePath))
+ {
+ File.Copy(filePath, Path.Combine(workPath, Path.GetFileName(filePath)));
+ }
+ else if (Directory.Exists(filePath))
+ {
+ await CopyDirectory(filePath, workPath);
+ }
+ else
+ {
+ throw new ArgumentException("File/Directory not found");
+ }
+
+ string zipPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip");
+
+ await Task.Run(() =>
+ {
+ ZipFile.CreateFromDirectory(workPath, zipPath, CompressionLevel.SmallestSize, false);
+ });
+
+ Directory.Delete(workPath, true);
+
+ return zipPath;
+ }
+
+ private static async Task CopyDirectory(string sourceDirName, string destDirName)
+ {
+ DirectoryInfo dir = new DirectoryInfo(sourceDirName);
+
+ if (!dir.Exists)
+ {
+ throw new DirectoryNotFoundException(
+ "Source directory does not exist or could not be found: "
+ + sourceDirName);
+ }
+
+ DirectoryInfo[] dirs = dir.GetDirectories();
+
+ // Append the name of the source directory to the destination directory
+ destDirName = Path.Combine(destDirName, dir.Name);
+
+ await Task.Run(() =>
+ {
+ // If the destination directory doesn't exist, create it.
+ Directory.CreateDirectory(destDirName);
+
+ FileInfo[] files = dir.GetFiles();
+ foreach (FileInfo file in files)
+ {
+ string tempPath = Path.Combine(destDirName, file.Name);
+ file.CopyTo(tempPath, false);
+ }
+ });
+
+ foreach (DirectoryInfo subdir in dirs)
+ {
+ string tempPath = Path.Combine(destDirName, subdir.Name);
+ await CopyDirectory(subdir.FullName, tempPath);
+ }
+ }
+
+
+ private async Task CloneOrPull(string localRepositoryPath, string repositoryUrl)
+ {
+ if (Directory.Exists(localRepositoryPath))
+ {
+ //Pull repository changes
+ var pullOptions = new PullOptions();
+ pullOptions.FetchOptions = new FetchOptions();
+
+ await Task.Run(() =>
+ {
+ using (var repository = new Repository(localRepositoryPath))
+ {
+ Commands.Pull(repository, new Signature("x", "x@x.com", DateTimeOffset.Now), pullOptions);
+ }
+ });
+
+ }
+ else
+ {
+ //Clone repository
+ await Task.Run(() =>
+ {
+ Repository.Clone(repositoryUrl, localRepositoryPath);
+ });
+ }
+ }
+
+ private async Task GetFileTree(string path, int relativePathPosition, List contents)
+ {
+
+ foreach (var directory in Directory.GetDirectories(path))
+ {
+ //Ignore .git folder
+ if (directory.Substring(relativePathPosition).StartsWith(".git")) { continue; }
+
+ contents.Add(CreateBalsamRepoFile(directory, relativePathPosition, BalsamRepoFile.TypeEnum.FolderEnum));
+ await GetFileTree(directory, relativePathPosition, contents);
+ }
+
+ await Task.Run(() =>
+ {
+ foreach (var file in Directory.GetFiles(path))
+ {
+ contents.Add(CreateBalsamRepoFile(file, relativePathPosition, BalsamRepoFile.TypeEnum.FileEnum));
+ }
+ });
+ }
+
+ private static BalsamRepoFile CreateBalsamRepoFile(string path, int relativePathPosition, BalsamRepoFile.TypeEnum type)
+ {
+ var repoFile = new BalsamRepoFile();
+ repoFile.Path = path.Substring(relativePathPosition);
+ repoFile.Name = Path.GetFileName(path);
+ repoFile.Id = Convert.ToBase64String(Encoding.UTF8.GetBytes(repoFile.Path));
+ repoFile.Type = type;
+
+ return repoFile;
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryRepository.cs
new file mode 100644
index 0000000..0ceb53c
--- /dev/null
+++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryRepository.cs
@@ -0,0 +1,73 @@
+using Balsam.Model;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+
+
+namespace Balsam.Repositories
+{
+ public class KnowledgeLibraryRepository : IKnowledgeLibraryRepository
+ {
+ private readonly ILogger _logger;
+ private readonly IHubRepositoryClient _hubRepositoryClient;
+ private readonly IKnowledgeLibraryContentRepository _knowledgeLibraryContentRepository;
+ protected HubPaths HubPaths { get; set; }
+
+ public KnowledgeLibraryRepository(ILogger logger, IHubRepositoryClient hubRepositoryClient, IKnowledgeLibraryContentRepository knowledgeLibraryContentRepository)
+ {
+ _logger = logger;
+ _hubRepositoryClient = hubRepositoryClient;
+ _knowledgeLibraryContentRepository = knowledgeLibraryContentRepository;
+ HubPaths = new HubPaths(_hubRepositoryClient.Path);
+ }
+
+ public async Task> ListKnowledgeLibraries()
+ {
+ var knowledgeLibraries = new List();
+
+ var kbPath = HubPaths.GetKnowledgeLibrariesPath();
+ _logger.LogDebug("kbPath: " + kbPath);
+ foreach (var knowledgelibraryFile in Directory.GetFiles(kbPath))
+ {
+ try
+ {
+ var knowledgeLibrary = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(knowledgelibraryFile));
+ if (knowledgeLibrary != null)
+ {
+ knowledgeLibraries.Add(knowledgeLibrary);
+ }
+ else
+ {
+ _logger.LogWarning($"Could not parse properties file for knowledgelibrary {knowledgelibraryFile}");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error with deserialization of file");
+ }
+ }
+ return knowledgeLibraries;
+ }
+
+ public async Task GetKnowledgeLibrary(string knowledgeLibraryId)
+ {
+ return (await ListKnowledgeLibraries()).First(kb => kb.Id == knowledgeLibraryId);
+ }
+
+ public async Task> GetRepositoryFileTree(string knowledgeLibraryId)
+ {
+ var knowledgeLibrary = await GetKnowledgeLibrary(knowledgeLibraryId);
+
+ return await _knowledgeLibraryContentRepository.GetFileTree(knowledgeLibraryId, knowledgeLibrary.RepositoryUrl);
+ }
+
+ public string GetRepositoryFilePath(string repositoryId, string fileId)
+ {
+ return _knowledgeLibraryContentRepository.GetFilePath(repositoryId, fileId);
+ }
+
+ public async Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId)
+ {
+ return await _knowledgeLibraryContentRepository.GetZippedResource(repositoryId, repositoryUrl, fileId);
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectGitOpsRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectGitOpsRepository.cs
new file mode 100644
index 0000000..2e6f044
--- /dev/null
+++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectGitOpsRepository.cs
@@ -0,0 +1,47 @@
+using Balsam.Model;
+
+namespace Balsam.Repositories
+{
+ public class ProjectGitOpsRepository : GitOpsRepository, IProjectGitOpsRepository
+ {
+
+ public ProjectGitOpsRepository(IHubRepositoryClient hubRepoClient)
+ : base(hubRepoClient)
+ {
+ }
+
+ public async Task CreateProjectManifests(BalsamProject project)
+ {
+ string projectPath = HubPaths.GetProjectPath(project.Id);
+ var context = new ProjectContext() { Project = project };
+
+ HubRepositoryClient.PullChanges();
+ string projectsTemplatePath = HubPaths.GetProjectsTemplatesPath();
+ await CreateManifests(context, projectPath, projectsTemplatePath);
+
+ HubRepositoryClient.PersistChanges($"Manifests for project {project.Id} persisted");
+ }
+
+ public async Task DeleteBranchManifests(string projectId, string branchId)
+ {
+ var branchPath = HubPaths.GetBranchPath(projectId, branchId);
+
+ HubRepositoryClient.PullChanges();
+
+ await DeleteManifests(branchPath);
+
+ HubRepositoryClient.PersistChanges($"Branch {branchId} deleted for project {projectId}");
+ }
+
+ public async Task DeleteProjectManifests(string projectId)
+ {
+ var projectPath = HubPaths.GetProjectPath(projectId);
+
+ HubRepositoryClient.PullChanges();
+
+ await DeleteManifests(projectPath);
+
+ HubRepositoryClient.PersistChanges($"Project {projectId} deleted");
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectRepository.cs
new file mode 100644
index 0000000..f7bf3e5
--- /dev/null
+++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectRepository.cs
@@ -0,0 +1,327 @@
+using Balsam.Model;
+using Balsam.Utility;
+using Newtonsoft.Json;
+using File = System.IO.File;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Balsam.Repositories
+{
+ public class ProjectRepository : IProjectRepository
+ {
+ private readonly IHubRepositoryClient _hubRepositoryClient;
+ private readonly ILogger _logger;
+ private readonly CapabilityOptions _git;
+
+ protected HubPaths HubPaths { get; set; }
+
+ public ProjectRepository(IOptionsSnapshot capabilityOptions, ILogger logger, IHubRepositoryClient hubRepositoryClient)
+ {
+ _git = capabilityOptions.Get(Capabilities.Git);
+ _logger = logger;
+ _hubRepositoryClient = hubRepositoryClient;
+ HubPaths = new HubPaths(_hubRepositoryClient.Path);
+ }
+
+ public async Task> GetProjects(bool includeBranches = true)
+ {
+ var projects = new List();
+ //var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub");
+ var hubPath = HubPaths.GetHubPath();
+
+ foreach (var projectPath in Directory.GetDirectories(hubPath))
+ {
+ var propsFile = Path.Combine(projectPath, "properties.json");
+ var project = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
+ if (project != null)
+ {
+ if (includeBranches)
+ {
+ project.Branches = await GetBranches(project.Id);
+ }
+ projects.Add(project);
+ }
+ else
+ {
+ _logger.LogWarning($"Could not parse properties file {propsFile}");
+ }
+ }
+ return projects;
+ }
+
+ //TODO:Move to other repository
+ public async Task GetFile(string projectId, string branchId, string fileId)
+ {
+ var project = await GetProject(projectId);
+ var branch = await GetBranch(projectId, branchId);
+ if (project is null || branch is null || project.Git is null)
+ {
+ throw new ArgumentException("File not found");
+ }
+
+ try
+ {
+
+ //TODO:Use Git provider API...
+ var request = new HttpRequestMessage(HttpMethod.Get, $"{_git.ServiceLocation}/repos/{project.Git.Id}/branches/{branch.GitBranch}/files/{fileId}");
+
+ var httpClient = new HttpClient();
+
+ var response = await httpClient.SendAsync(request);
+ if (response.IsSuccessStatusCode)
+ {
+ var data = await response.Content.ReadAsByteArrayAsync();
+ return new FileContent(data, response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream");
+ }
+ }
+ catch (Exception ex)
+ {
+ string errorMessage = "Could not read files from repository";
+ _logger.LogError(ex, errorMessage);
+ throw new ApplicationException(errorMessage);
+ }
+
+ return null;
+ }
+
+ public async Task GetProject(string projectId, bool includeBranches = true)
+ {
+ //var projectPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId);
+ var projectPath = HubPaths.GetProjectPath(projectId);
+
+ var propsFile = Path.Combine(projectPath, "properties.json");
+
+ if (!File.Exists(propsFile))
+ {
+ var errorMessage = $"Project with project id {projectId} does not exist";
+ _logger.LogError(errorMessage);
+ return null;
+ }
+
+ var project = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
+
+ if (project == null)
+ {
+ var errorMessage = $"Project with project id {projectId} does not exist";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage);
+ }
+
+ if (includeBranches)
+ {
+ project.Branches = await GetBranches(project.Id);
+ }
+
+ return project!;
+ }
+
+ private async Task> GetBranches(string projectId)
+ {
+ var branches = new List();
+
+ var projectPath = HubPaths.GetProjectPath(projectId);
+ if (!Directory.Exists(projectPath))
+ {
+ var errorMessage = $"Project with project id {projectId} does not exist";
+ _logger.LogError(errorMessage);
+ throw new ArgumentException(errorMessage);
+ }
+
+ foreach (var branchPath in Directory.GetDirectories(projectPath))
+ {
+ var propsFile = Path.Combine(branchPath, "properties.json");
+ var branch = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
+ if (branch != null)
+ {
+ branches.Add(branch);
+ }
+ else
+ {
+ _logger.LogWarning($"Could not parse properties file {propsFile}");
+ }
+ }
+
+ //var gitBranches = await _repositoryApi.GetBranchesAsync(repositoryId);
+
+ //var gitBranchesWithoutBalsamBranch = gitBranches.Where(gitBranch => !branches.Any(b => b.GitBranch == gitBranch));
+
+ //foreach (var gitBranch in gitBranchesWithoutBalsamBranch)
+ //{
+ // var newBalsamBranch = await CreateBalsamBranchFromGitBranch(projectId, gitBranch);
+ // if (newBalsamBranch != null)
+ // {
+ // branches.Add(newBalsamBranch);
+ // }
+ // else
+ // {
+ // _logger.LogWarning($"Could not create BalsamBranch from git-branch {gitBranch} for project {projectId}");
+ // }
+ //}
+
+ return branches;
+ }
+
+
+ public async Task GetBranch(string projectId, string branchId)
+ {
+ var propsFile = Path.Combine(HubPaths.GetBranchPath(projectId, branchId), "properties.json");
+
+ if (!File.Exists(propsFile))
+ {
+ return null;
+ }
+
+ var branch = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
+ return branch;
+ }
+
+
+ public async Task ProjectExists(string projectName)
+ {
+ var projects = await GetProjects(false); //TODO: Optimize
+ if (projects.FirstOrDefault(p => p.Name == projectName) == null)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public async Task CreateProject(BalsamProject project, string defaultBranchName)
+ {
+ _logger.LogDebug($"create project information");
+
+ var id = NameUtil.SanitizeName(project.Name);
+
+ project.Id = id;
+
+ await AddOrUpdate(project);
+
+ var branch = await CreateBranch(project, defaultBranchName, project.Description, true);
+
+ if (branch != null)
+ {
+ _logger.LogInformation($"Default Balsam branch {defaultBranchName} created");
+ project.Branches.Add(branch);
+ }
+ else
+ {
+ throw new ApplicationException($"Could not create branch for project {project.Id}.");
+ }
+
+ return project;
+ }
+
+ private async Task AddOrUpdate(BalsamProject project)
+ {
+ string projectPath = HubPaths.GetProjectPath(project.Id);
+
+ _hubRepositoryClient.PullChanges();
+ _logger.LogDebug($"Assure path exists {projectPath}");
+
+ DirectoryUtil.AssureDirectoryExists(projectPath);
+
+ string propPath = Path.Combine(projectPath, "properties.json");
+ // serialize JSON to a string and then write string to a file
+ await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(project));
+ _hubRepositoryClient.PersistChanges($"New program with id {project.Id}");
+ }
+
+ public async Task UpdateProject(BalsamProject project)
+ {
+ await AddOrUpdate(project);
+ }
+
+
+ public async Task CreateBranch(string projectId, string branchName, string description)
+ {
+ var project = await GetProject(projectId, false);
+
+ if (project is null)
+ {
+ throw new ArgumentException($"Project with id {projectId} does not exist", projectId);
+ }
+
+ var createdBranch = await CreateBranch(project, branchName, description, false);
+ return createdBranch;
+ }
+
+ private async Task CreateBranch(BalsamProject project, string branchName, string description, bool isDefault = false)
+ {
+ var branchId = NameUtil.SanitizeName(branchName);
+ var branchPath = HubPaths.GetBranchPath(project.Id, branchId);
+
+ DirectoryUtil.AssureDirectoryExists(branchPath);
+
+ var branch = new BalsamBranch()
+ {
+ Id = branchId,
+ Name = branchName,
+ Description = description,
+ IsDefault = isDefault,
+ GitBranch = branchName
+ };
+
+ var propPath = Path.Combine(branchPath, "properties.json");
+
+ _hubRepositoryClient.PullChanges();
+ await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(branch));
+ _hubRepositoryClient.PersistChanges($"Branch {branchName} created for project {project.Name}");
+
+ return branch;
+ }
+
+ public async Task CreateBalsamBranchFromGitBranch(string projectId, string gitBranchName)
+ {
+ var project = await GetProject(projectId, false);
+
+ if (project is null || project.Git is null)
+ {
+ return null;
+ }
+
+ var createdBranch = await CreateBranch(project, gitBranchName, "", false);
+
+ return createdBranch;
+ }
+
+ public async Task DeleteBranch(string projectId, string branchId)
+ {
+ var branchPath = HubPaths.GetBranchPath(projectId, branchId);
+
+ var propsFile = Path.Combine(branchPath, "properties.json");
+
+ _hubRepositoryClient.PullChanges();
+
+ if (File.Exists(propsFile))
+ {
+ await Task.Run(() => File.Delete(propsFile));
+ }
+
+ _hubRepositoryClient.PersistChanges($"Branch {branchId} deleted for project {projectId}");
+
+ }
+
+ public async Task DeleteProject(string projectId)
+ {
+ var projectPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId);
+
+ _hubRepositoryClient.PullChanges();
+
+ var propPath = Path.Combine(projectPath, "properties.json");
+
+ if (!Directory.Exists(projectPath))
+ {
+ return;
+ }
+
+ if (File.Exists(propPath))
+ {
+ await Task.Run(() => File.Delete(propPath));
+ }
+
+ _hubRepositoryClient.PersistChanges($"Project {projectId} deleted");
+ }
+
+
+ }
+}
diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceGitOpsRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceGitOpsRepository.cs
new file mode 100644
index 0000000..eed997a
--- /dev/null
+++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceGitOpsRepository.cs
@@ -0,0 +1,57 @@
+using Balsam.Model;
+using Balsam.Utility;
+
+namespace Balsam.Repositories
+{
+ public class WorkspaceGitOpsRepository : GitOpsRepository, IWorkspaceGitOpsRepository
+ {
+ public static string WorkspaceUrlAnnotationKey = "balsam-workspace-url";
+
+ public WorkspaceGitOpsRepository(IHubRepositoryClient hubRepoClient)
+ : base(hubRepoClient)
+ {
+ }
+
+ public async Task CreateWorkspaceManifests(BalsamProject project, BalsamBranch branch, BalsamWorkspace workspace, UserInfo user, string templateId)
+ {
+ var workspacePath = HubPaths.GetWorkspacePath(project.Id, branch.Id, user.UserName, workspace.Id);
+ var context = new WorkspaceContext(project, branch, workspace, user);
+
+ HubRepositoryClient.PullChanges();
+
+ string workspaceTemplatesPath = HubPaths.GetWorkspaceTemplatesPath(templateId);
+ await CreateManifests(context, workspacePath, workspaceTemplatesPath);
+
+ HubRepositoryClient.PersistChanges($"Manifests for workspace {workspace.Id} persisted");
+
+ }
+
+ public async Task DeleteWorkspaceManifests(string projectId, string branchId, string workspaceId, string userName)
+ {
+ var workspacePath = HubPaths.GetWorkspacePath(projectId, branchId, userName, workspaceId);
+
+ HubRepositoryClient.PullChanges();
+
+ await DeleteManifests(workspacePath);
+
+ HubRepositoryClient.PersistChanges($"Deleted workspace manifests with id {workspaceId}");
+ }
+
+ public async Task GetWorkspaceUrl(string projectId, string branchId, string userName, string workspaceId, string urlConfigFile)
+ {
+ //TODO: Pull?
+
+ return await Task.Run(() =>
+ {
+ string manifestFilePath = GetWorkspaceManifestFilePath(projectId, branchId, userName, workspaceId, urlConfigFile);
+
+ return ManifestUtil.GetAnnotation(manifestFilePath, WorkspaceUrlAnnotationKey);
+ });
+ }
+
+ private string GetWorkspaceManifestFilePath(string projectId, string branchId, string userName, string workspaceId, string manifestFileName)
+ {
+ return Path.Combine(HubPaths.GetWorkspacePath(projectId, branchId, userName, workspaceId), manifestFileName);
+ }
+ }
+}
diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceRepository.cs
new file mode 100644
index 0000000..23f5f7d
--- /dev/null
+++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceRepository.cs
@@ -0,0 +1,302 @@
+using Balsam.Utility;
+using Newtonsoft.Json;
+using Balsam.Model;
+using Microsoft.Extensions.Logging;
+using System.ComponentModel.Design;
+
+namespace Balsam.Repositories
+{
+
+ public class WorkspaceRepository : IWorkspaceRepository
+ {
+ private readonly IHubRepositoryClient _hubRepositoryClient;
+ private readonly ILogger _logger;
+ protected HubPaths HubPaths { get; set; }
+
+ public WorkspaceRepository(IHubRepositoryClient hubRepositoryClient, ILogger logger)
+ {
+ _hubRepositoryClient = hubRepositoryClient;
+ _logger = logger;
+ HubPaths = new HubPaths(_hubRepositoryClient.Path);
+ }
+
+ public async Task CreateWorkspace(BalsamWorkspace workspace)
+ {
+ var projectPath = HubPaths.GetProjectPath(workspace.ProjectId);
+
+ if (!Directory.Exists(projectPath))
+ {
+ DirectoryUtil.AssureDirectoryExists(projectPath);
+ }
+
+ var workspacePath = HubPaths.GetWorkspacePath(workspace.ProjectId, workspace.BranchId, workspace.Owner, workspace.Id);
+ var propPath = Path.Combine(workspacePath, "properties.json");
+
+ _logger.LogDebug("Pulling changes");
+ _hubRepositoryClient.PullChanges();
+ DirectoryUtil.AssureDirectoryExists(workspacePath);
+ // serialize JSON to a string and then write string to a file
+ await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(workspace));
+ _hubRepositoryClient.PersistChanges($"New workspace with id {workspace.Id}");
+
+ _logger.LogInformation("Workspace created");
+ return workspace;
+ }
+
+ public async Task GetWorkspaceTemplate(string templateId)
+ {
+ //var templatesPaths = HubPaths.GetTemplatesPath();
+ var workspaceTemplatePath = HubPaths.GetWorkspaceTemplatesPath(templateId);
+
+ var templateFilePath = Path.Combine(workspaceTemplatePath, "properties.json");
+
+ //TODO: Pull?
+
+ if (!File.Exists(templateFilePath))
+ {
+ return null;
+ }
+
+ var content = await File.ReadAllTextAsync(templateFilePath);
+
+ return JsonConvert.DeserializeObject(content);
+ }
+
+ public async Task DeleteWorkspace(string projectId, string branchId, string workspaceId, string userName)
+ {
+ _hubRepositoryClient.PullChanges();
+ var workspacePath = HubPaths.GetWorkspacePath(projectId, branchId, userName, workspaceId);
+
+ var propPath = Path.Combine(workspacePath, "properties.json");
+
+ if (!Directory.Exists(workspacePath))
+ {
+ return;
+ }
+
+ if (File.Exists(propPath))
+ {
+ await Task.Run(() => File.Delete(propPath));
+ }
+
+ _hubRepositoryClient.PersistChanges($"Deleted workspace with id {workspaceId}");
+ }
+
+ public async Task GetWorkspace(string projectId, string userId, string workspaceId)
+ {
+ var workspaces = await GetWorkspacesByProjectAndUser(projectId, userId);
+ return workspaces.FirstOrDefault(w => w.Id == workspaceId);
+ }
+
+ public string GenerateWorkspaceId(string name)
+ {
+ return NameUtil.SanitizeName(name);
+ }
+
+ public async Task> ListWorkspaceTemplates()
+ {
+ var workspaceTemplatesPath = HubPaths.GetWorkspacesTemplatesPath();
+
+ if (!Directory.Exists(workspaceTemplatesPath))
+ {
+ return new List();
+ }
+
+ var workspaceTemplates = await Task.Run(() =>
+ {
+ var workspaceTemplates = new List();
+ foreach (var directory in Directory.GetDirectories(workspaceTemplatesPath))
+ {
+ var id = new DirectoryInfo(directory).Name;
+ var fileName = Path.Combine(directory, "properties.json");
+ var jsonString = File.ReadAllText(fileName);
+ var template = JsonConvert.DeserializeObject(jsonString);
+
+ if (template == null) continue;
+
+ template.Id = id;
+ workspaceTemplates.Add(template);
+ }
+ return workspaceTemplates;
+ });
+
+
+ return workspaceTemplates;
+ }
+
+ public async Task> GetWorkspaces()
+ {
+ var hubPath = HubPaths.GetHubPath();
+
+ var workspaces = new List();
+
+ //TODO: Gör oberoende av katalogstruktur
+ foreach (var projectPath in Directory.GetDirectories(hubPath))
+ {
+ foreach (var branchPath in Directory.GetDirectories(projectPath))
+ {
+ foreach (var userPath in Directory.GetDirectories(branchPath))
+ {
+ foreach (var workspacePath in Directory.GetDirectories(userPath))
+ {
+ var propsFile = Path.Combine(workspacePath, "properties.json");
+
+ if (File.Exists(propsFile))
+ {
+ var content = await File.ReadAllTextAsync(propsFile);
+ var workspace = JsonConvert.DeserializeObject(content);
+
+ if (workspace != null)
+ {
+ workspaces.Add(workspace);
+ }
+ else
+ {
+ continue;
+ }
+ }
+ else
+ {
+ continue;
+ }
+ }
+ }
+ }
+ }
+ return workspaces;
+ }
+
+ public async Task> GetWorkspacesByUser(string userId, List projectIds)
+ {
+ var workspaces = new List();
+
+ foreach (var projectId in projectIds)
+ {
+ var projectPath = HubPaths.GetProjectPath(projectId);
+
+ //TODO: Gör oberoende av katalogstruktur?
+ if (Directory.Exists(projectPath))
+ {
+ foreach (var branchPath in Directory.GetDirectories(projectPath))
+ {
+ var userPath = Path.Combine(branchPath, userId);
+
+ if (Directory.Exists(userPath))
+ {
+ foreach (var workspacePath in Directory.GetDirectories(userPath))
+ {
+ var propsFile = Path.Combine(workspacePath, "properties.json");
+ var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
+
+ if (workspace == null) continue;
+ workspaces.Add(workspace);
+ }
+ }
+ }
+ }
+ }
+ return workspaces;
+ }
+
+ public async Task> GetWorkspacesByProject(string projectId)
+ {
+
+ var workspaces = new List();
+
+ var projectPath = HubPaths.GetProjectPath(projectId);
+ //TODO: Gör oberoende av katalogstruktur
+ if (Directory.Exists(projectPath))
+ {
+ foreach (var branchPath in Directory.GetDirectories(projectPath))
+ {
+ foreach (var userPath in Directory.GetDirectories(branchPath))
+ {
+ foreach (var workspacePath in Directory.GetDirectories(userPath))
+ {
+ var propsFile = Path.Combine(workspacePath, "properties.json");
+ var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile));
+
+ if (workspace == null) continue;
+ workspaces.Add(workspace);
+ }
+ }
+ }
+ }
+ return workspaces;
+ }
+
+ public async Task