diff --git a/.docs/terminology.md b/.docs/terminology.md new file mode 100644 index 0000000..8bbc6a6 --- /dev/null +++ b/.docs/terminology.md @@ -0,0 +1,8 @@ +# Terminology +>[!WARNING] +>ROUGH DRAFT! + +* Workload Template = Bicep template defining multiple resources that make up a workload/app, maximally leaning on platform templates. +* Platform Template = Bicep template defining multiple resources that are to be used by multiple workloads and other deployments. Configurations should be focused on providing centralized services. +* Resource Template = Bicep template defining an individual resource and how to deploy it. These will typically be included in Mordos, but able to be modified to the MSPs desires. Configurations should be focused on defining best practice and business restrictions for wide-scale (multi-tenant) deployment. +* Module = Functionality add, typically included by Mordos, but able to be expanded and modified. These are code functions, like name generators and helpers. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8ca517d..907da0e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,4 @@ updates: directory: "/" # Location of package manifests schedule: interval: "weekly" + target-branch: "dev" diff --git a/Mordos.API/Functions/BicepTemplatesFunctions.cs b/Mordos.API/Functions/BicepTemplatesFunctions.cs index a2e1b94..ca35726 100644 --- a/Mordos.API/Functions/BicepTemplatesFunctions.cs +++ b/Mordos.API/Functions/BicepTemplatesFunctions.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; @@ -14,22 +15,14 @@ namespace Mordos.API.Functions; /// /// Azure Functions for managing Bicep templates /// -public class BicepTemplatesFunctions +public class BicepTemplatesFunctions(IBicepTemplateService bicepTemplateService, ILogger logger) { - private readonly IBicepTemplateService _bicepTemplateService; - private readonly ILogger _logger; - - public BicepTemplatesFunctions(IBicepTemplateService bicepTemplateService, ILogger logger) - { - _bicepTemplateService = bicepTemplateService; - _logger = logger; - } /// /// Get all Bicep templates /// [Function("GetBicepTemplates")] - [OpenApiOperation(operationId: "GetBicepTemplates", tags: new[] { "BicepTemplates" }, Summary = "Get all Bicep templates", Description = "Retrieves a list of all Bicep templates with optional filtering", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "GetBicepTemplates", tags: ["BicepTemplates"], Summary = "Get all Bicep templates", Description = "Retrieves a list of all Bicep templates with optional filtering", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "nameFilter", In = ParameterLocation.Query, Required = false, Type = typeof(string), Summary = "Name filter", Description = "Filter templates by name (case-insensitive partial match)", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "tagFilter", In = ParameterLocation.Query, Required = false, Type = typeof(string), Summary = "Tag filter", Description = "Filter templates by tag (case-insensitive partial match)", Visibility = OpenApiVisibilityType.Important)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(IEnumerable), Summary = "Success", Description = "List of Bicep templates")] @@ -41,14 +34,14 @@ public async Task GetBicepTemplates( var nameFilter = req.Query["nameFilter"].FirstOrDefault(); var tagFilter = req.Query["tagFilter"].FirstOrDefault(); - _logger.LogInformation("Getting Bicep templates with filters - Name: {NameFilter}, Tag: {TagFilter}", nameFilter, tagFilter); + logger.LogInformation("Getting Bicep templates with filters - Name: {NameFilter}, Tag: {TagFilter}", nameFilter, tagFilter); - var templates = await _bicepTemplateService.GetAllTemplatesAsync(nameFilter, tagFilter); + var templates = await bicepTemplateService.GetAllTemplatesAsync(nameFilter, tagFilter); return new OkObjectResult(templates); } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving Bicep templates"); + logger.LogError(ex, "Error retrieving Bicep templates"); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -57,7 +50,7 @@ public async Task GetBicepTemplates( /// Get a specific Bicep template by ID /// [Function("GetBicepTemplate")] - [OpenApiOperation(operationId: "GetBicepTemplate", tags: new[] { "BicepTemplates" }, Summary = "Get Bicep template by ID", Description = "Retrieves a specific Bicep template by its unique identifier", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "GetBicepTemplate", tags: ["BicepTemplates"], Summary = "Get Bicep template by ID", Description = "Retrieves a specific Bicep template by its unique identifier", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = "Template ID", Description = "The unique identifier of the template", Visibility = OpenApiVisibilityType.Important)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(BicepTemplateResponse), Summary = "Success", Description = "The requested Bicep template")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.NotFound, contentType: "application/json", bodyType: typeof(string), Summary = "Not Found", Description = "Template not found")] @@ -67,9 +60,9 @@ public async Task GetBicepTemplate( { try { - _logger.LogInformation("Getting Bicep template with ID: {Id}", id); + logger.LogInformation("Getting Bicep template with ID: {Id}", id); - var template = await _bicepTemplateService.GetTemplateByIdAsync(id); + var template = await bicepTemplateService.GetTemplateByIdAsync(id); if (template == null) { return new NotFoundObjectResult($"Template with ID '{id}' not found"); @@ -79,7 +72,7 @@ public async Task GetBicepTemplate( } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving Bicep template with ID: {Id}", id); + logger.LogError(ex, "Error retrieving Bicep template with ID: {Id}", id); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -88,7 +81,7 @@ public async Task GetBicepTemplate( /// Create a new Bicep template /// [Function("CreateBicepTemplate")] - [OpenApiOperation(operationId: "CreateBicepTemplate", tags: new[] { "BicepTemplates" }, Summary = "Create new Bicep template", Description = "Creates a new Bicep template", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "CreateBicepTemplate", tags: ["BicepTemplates"], Summary = "Create new Bicep template", Description = "Creates a new Bicep template", Visibility = OpenApiVisibilityType.Important)] [OpenApiRequestBody(contentType: "application/json", bodyType: typeof(CreateBicepTemplateRequest), Required = true, Description = "The template data to create")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.Created, contentType: "application/json", bodyType: typeof(BicepTemplateResponse), Summary = "Created", Description = "The created Bicep template")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.BadRequest, contentType: "application/json", bodyType: typeof(string), Summary = "Bad Request", Description = "Invalid request data")] @@ -97,7 +90,7 @@ public async Task CreateBicepTemplate( { try { - _logger.LogInformation("Creating new Bicep template"); + logger.LogInformation("Creating new Bicep template"); var request = await req.ReadFromJsonAsync(); if (request == null) @@ -116,12 +109,12 @@ public async Task CreateBicepTemplate( return new BadRequestObjectResult("Template content is required"); } - var template = await _bicepTemplateService.CreateTemplateAsync(request); + var template = await bicepTemplateService.CreateTemplateAsync(request); return new ObjectResult(template) { StatusCode = 201 }; } catch (Exception ex) { - _logger.LogError(ex, "Error creating Bicep template"); + logger.LogError(ex, "Error creating Bicep template"); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -130,7 +123,7 @@ public async Task CreateBicepTemplate( /// Update an existing Bicep template /// [Function("UpdateBicepTemplate")] - [OpenApiOperation(operationId: "UpdateBicepTemplate", tags: new[] { "BicepTemplates" }, Summary = "Update Bicep template", Description = "Updates an existing Bicep template", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "UpdateBicepTemplate", tags: ["BicepTemplates"], Summary = "Update Bicep template", Description = "Updates an existing Bicep template", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = "Template ID", Description = "The unique identifier of the template", Visibility = OpenApiVisibilityType.Important)] [OpenApiRequestBody(contentType: "application/json", bodyType: typeof(UpdateBicepTemplateRequest), Required = true, Description = "The template data to update")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(BicepTemplateResponse), Summary = "Success", Description = "The updated Bicep template")] @@ -142,7 +135,7 @@ public async Task UpdateBicepTemplate( { try { - _logger.LogInformation("Updating Bicep template with ID: {Id}", id); + logger.LogInformation("Updating Bicep template with ID: {Id}", id); var request = await req.ReadFromJsonAsync(); if (request == null) @@ -150,7 +143,7 @@ public async Task UpdateBicepTemplate( return new BadRequestObjectResult("Invalid request body"); } - var template = await _bicepTemplateService.UpdateTemplateAsync(id, request); + var template = await bicepTemplateService.UpdateTemplateAsync(id, request); if (template == null) { return new NotFoundObjectResult($"Template with ID '{id}' not found"); @@ -160,7 +153,7 @@ public async Task UpdateBicepTemplate( } catch (Exception ex) { - _logger.LogError(ex, "Error updating Bicep template with ID: {Id}", id); + logger.LogError(ex, "Error updating Bicep template with ID: {Id}", id); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -169,7 +162,7 @@ public async Task UpdateBicepTemplate( /// Delete a Bicep template /// [Function("DeleteBicepTemplate")] - [OpenApiOperation(operationId: "DeleteBicepTemplate", tags: new[] { "BicepTemplates" }, Summary = "Delete Bicep template", Description = "Deletes a Bicep template", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "DeleteBicepTemplate", tags: ["BicepTemplates"], Summary = "Delete Bicep template", Description = "Deletes a Bicep template", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = "Template ID", Description = "The unique identifier of the template", Visibility = OpenApiVisibilityType.Important)] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.NoContent, Summary = "No Content", Description = "Template deleted successfully")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.NotFound, contentType: "application/json", bodyType: typeof(string), Summary = "Not Found", Description = "Template not found")] @@ -179,16 +172,16 @@ public async Task DeleteBicepTemplate( { try { - _logger.LogInformation("Deleting Bicep template with ID: {Id}", id); + logger.LogInformation("Deleting Bicep template with ID: {Id}", id); // Check if template exists first - var existingTemplate = await _bicepTemplateService.GetTemplateByIdAsync(id); + var existingTemplate = await bicepTemplateService.GetTemplateByIdAsync(id); if (existingTemplate == null) { return new NotFoundObjectResult($"Template with ID '{id}' not found"); } - var success = await _bicepTemplateService.DeleteTemplateAsync(id); + var success = await bicepTemplateService.DeleteTemplateAsync(id); if (success) { return new NoContentResult(); @@ -198,7 +191,7 @@ public async Task DeleteBicepTemplate( } catch (Exception ex) { - _logger.LogError(ex, "Error deleting Bicep template with ID: {Id}", id); + logger.LogError(ex, "Error deleting Bicep template with ID: {Id}", id); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -207,7 +200,7 @@ public async Task DeleteBicepTemplate( /// Validate a Bicep template /// [Function("ValidateBicepTemplate")] - [OpenApiOperation(operationId: "ValidateBicepTemplate", tags: new[] { "BicepTemplates" }, Summary = "Validate Bicep template", Description = "Validates a Bicep template", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "ValidateBicepTemplate", tags: ["BicepTemplates"], Summary = "Validate Bicep template", Description = "Validates a Bicep template", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = "Template ID", Description = "The unique identifier of the template", Visibility = OpenApiVisibilityType.Important)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(object), Summary = "Success", Description = "Validation result")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.NotFound, contentType: "application/json", bodyType: typeof(string), Summary = "Not Found", Description = "Template not found")] @@ -217,21 +210,21 @@ public async Task ValidateBicepTemplate( { try { - _logger.LogInformation("Validating Bicep template with ID: {Id}", id); + logger.LogInformation("Validating Bicep template with ID: {Id}", id); // Check if template exists first - var existingTemplate = await _bicepTemplateService.GetTemplateByIdAsync(id); + var existingTemplate = await bicepTemplateService.GetTemplateByIdAsync(id); if (existingTemplate == null) { return new NotFoundObjectResult($"Template with ID '{id}' not found"); } - var success = await _bicepTemplateService.ValidateTemplateAsync(id); + var success = await bicepTemplateService.ValidateTemplateAsync(id); return new OkObjectResult(new { Success = success, Message = success ? "Template validation passed" : "Template validation failed" }); } catch (Exception ex) { - _logger.LogError(ex, "Error validating Bicep template with ID: {Id}", id); + logger.LogError(ex, "Error validating Bicep template with ID: {Id}", id); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } diff --git a/Mordos.API/Functions/DeploymentsFunctions.cs b/Mordos.API/Functions/DeploymentsFunctions.cs index 465e256..1eb8202 100644 --- a/Mordos.API/Functions/DeploymentsFunctions.cs +++ b/Mordos.API/Functions/DeploymentsFunctions.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; @@ -15,22 +16,14 @@ namespace Mordos.API.Functions; /// /// Azure Functions for managing deployments /// -public class DeploymentsFunctions +public class DeploymentsFunctions(IDeploymentService deploymentService, ILogger logger) { - private readonly IDeploymentService _deploymentService; - private readonly ILogger _logger; - - public DeploymentsFunctions(IDeploymentService deploymentService, ILogger logger) - { - _deploymentService = deploymentService; - _logger = logger; - } /// /// Get all deployments /// [Function("GetDeployments")] - [OpenApiOperation(operationId: "GetDeployments", tags: new[] { "Deployments" }, Summary = "Get all deployments", Description = "Retrieves a list of all deployments with optional filtering", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "GetDeployments", tags: ["Deployments"], Summary = "Get all deployments", Description = "Retrieves a list of all deployments with optional filtering", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "templateId", In = ParameterLocation.Query, Required = false, Type = typeof(string), Summary = "Template ID filter", Description = "Filter deployments by template ID", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "status", In = ParameterLocation.Query, Required = false, Type = typeof(DeploymentStatus), Summary = "Status filter", Description = "Filter deployments by status", Visibility = OpenApiVisibilityType.Important)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(IEnumerable), Summary = "Success", Description = "List of deployments")] @@ -48,14 +41,14 @@ public async Task GetDeployments( status = parsedStatus; } - _logger.LogInformation("Getting deployments with filters - TemplateId: {TemplateId}, Status: {Status}", templateIdFilter, status); + logger.LogInformation("Getting deployments with filters - TemplateId: {TemplateId}, Status: {Status}", templateIdFilter, status); - var deployments = await _deploymentService.GetAllDeploymentsAsync(templateIdFilter, status); + var deployments = await deploymentService.GetAllDeploymentsAsync(templateIdFilter, status); return new OkObjectResult(deployments); } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving deployments"); + logger.LogError(ex, "Error retrieving deployments"); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -64,7 +57,7 @@ public async Task GetDeployments( /// Get a specific deployment by ID /// [Function("GetDeployment")] - [OpenApiOperation(operationId: "GetDeployment", tags: new[] { "Deployments" }, Summary = "Get deployment by ID", Description = "Retrieves a specific deployment by its unique identifier", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "GetDeployment", tags: ["Deployments"], Summary = "Get deployment by ID", Description = "Retrieves a specific deployment by its unique identifier", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = "Deployment ID", Description = "The unique identifier of the deployment", Visibility = OpenApiVisibilityType.Important)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(DeploymentResponse), Summary = "Success", Description = "The requested deployment")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.NotFound, contentType: "application/json", bodyType: typeof(string), Summary = "Not Found", Description = "Deployment not found")] @@ -74,9 +67,9 @@ public async Task GetDeployment( { try { - _logger.LogInformation("Getting deployment with ID: {Id}", id); + logger.LogInformation("Getting deployment with ID: {Id}", id); - var deployment = await _deploymentService.GetDeploymentByIdAsync(id); + var deployment = await deploymentService.GetDeploymentByIdAsync(id); if (deployment == null) { return new NotFoundObjectResult($"Deployment with ID '{id}' not found"); @@ -86,7 +79,7 @@ public async Task GetDeployment( } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving deployment with ID: {Id}", id); + logger.LogError(ex, "Error retrieving deployment with ID: {Id}", id); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -95,7 +88,7 @@ public async Task GetDeployment( /// Create a new deployment /// [Function("CreateDeployment")] - [OpenApiOperation(operationId: "CreateDeployment", tags: new[] { "Deployments" }, Summary = "Create new deployment", Description = "Creates a new deployment", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "CreateDeployment", tags: ["Deployments"], Summary = "Create new deployment", Description = "Creates a new deployment", Visibility = OpenApiVisibilityType.Important)] [OpenApiRequestBody(contentType: "application/json", bodyType: typeof(CreateDeploymentRequest), Required = true, Description = "The deployment data to create")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.Created, contentType: "application/json", bodyType: typeof(DeploymentResponse), Summary = "Created", Description = "The created deployment")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.BadRequest, contentType: "application/json", bodyType: typeof(string), Summary = "Bad Request", Description = "Invalid request data")] @@ -104,7 +97,7 @@ public async Task CreateDeployment( { try { - _logger.LogInformation("Creating new deployment"); + logger.LogInformation("Creating new deployment"); var request = await req.ReadFromJsonAsync(); if (request == null) @@ -138,17 +131,17 @@ public async Task CreateDeployment( return new BadRequestObjectResult("Target region is required"); } - var deployment = await _deploymentService.CreateDeploymentAsync(request); + var deployment = await deploymentService.CreateDeploymentAsync(request); return new ObjectResult(deployment) { StatusCode = 201 }; } catch (ArgumentException ex) { - _logger.LogWarning(ex, "Invalid argument when creating deployment"); + logger.LogWarning(ex, "Invalid argument when creating deployment"); return new BadRequestObjectResult(ex.Message); } catch (Exception ex) { - _logger.LogError(ex, "Error creating deployment"); + logger.LogError(ex, "Error creating deployment"); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -157,7 +150,7 @@ public async Task CreateDeployment( /// Update an existing deployment /// [Function("UpdateDeployment")] - [OpenApiOperation(operationId: "UpdateDeployment", tags: new[] { "Deployments" }, Summary = "Update deployment", Description = "Updates an existing deployment", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "UpdateDeployment", tags: ["Deployments"], Summary = "Update deployment", Description = "Updates an existing deployment", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = "Deployment ID", Description = "The unique identifier of the deployment", Visibility = OpenApiVisibilityType.Important)] [OpenApiRequestBody(contentType: "application/json", bodyType: typeof(UpdateDeploymentRequest), Required = true, Description = "The deployment data to update")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(DeploymentResponse), Summary = "Success", Description = "The updated deployment")] @@ -169,7 +162,7 @@ public async Task UpdateDeployment( { try { - _logger.LogInformation("Updating deployment with ID: {Id}", id); + logger.LogInformation("Updating deployment with ID: {Id}", id); var request = await req.ReadFromJsonAsync(); if (request == null) @@ -177,7 +170,7 @@ public async Task UpdateDeployment( return new BadRequestObjectResult("Invalid request body"); } - var deployment = await _deploymentService.UpdateDeploymentAsync(id, request); + var deployment = await deploymentService.UpdateDeploymentAsync(id, request); if (deployment == null) { return new NotFoundObjectResult($"Deployment with ID '{id}' not found"); @@ -187,7 +180,7 @@ public async Task UpdateDeployment( } catch (Exception ex) { - _logger.LogError(ex, "Error updating deployment with ID: {Id}", id); + logger.LogError(ex, "Error updating deployment with ID: {Id}", id); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -196,7 +189,7 @@ public async Task UpdateDeployment( /// Delete a deployment /// [Function("DeleteDeployment")] - [OpenApiOperation(operationId: "DeleteDeployment", tags: new[] { "Deployments" }, Summary = "Delete deployment", Description = "Deletes a deployment", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "DeleteDeployment", tags: ["Deployments"], Summary = "Delete deployment", Description = "Deletes a deployment", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = "Deployment ID", Description = "The unique identifier of the deployment", Visibility = OpenApiVisibilityType.Important)] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.NoContent, Summary = "No Content", Description = "Deployment deleted successfully")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.NotFound, contentType: "application/json", bodyType: typeof(string), Summary = "Not Found", Description = "Deployment not found")] @@ -206,16 +199,16 @@ public async Task DeleteDeployment( { try { - _logger.LogInformation("Deleting deployment with ID: {Id}", id); + logger.LogInformation("Deleting deployment with ID: {Id}", id); // Check if deployment exists first - var existingDeployment = await _deploymentService.GetDeploymentByIdAsync(id); + var existingDeployment = await deploymentService.GetDeploymentByIdAsync(id); if (existingDeployment == null) { return new NotFoundObjectResult($"Deployment with ID '{id}' not found"); } - var success = await _deploymentService.DeleteDeploymentAsync(id); + var success = await deploymentService.DeleteDeploymentAsync(id); if (success) { return new NoContentResult(); @@ -225,7 +218,7 @@ public async Task DeleteDeployment( } catch (Exception ex) { - _logger.LogError(ex, "Error deleting deployment with ID: {Id}", id); + logger.LogError(ex, "Error deleting deployment with ID: {Id}", id); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -234,7 +227,7 @@ public async Task DeleteDeployment( /// Start a deployment /// [Function("StartDeployment")] - [OpenApiOperation(operationId: "StartDeployment", tags: new[] { "Deployments" }, Summary = "Start deployment", Description = "Starts a deployment (changes status to InProgress)", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "StartDeployment", tags: ["Deployments"], Summary = "Start deployment", Description = "Starts a deployment (changes status to InProgress)", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = "Deployment ID", Description = "The unique identifier of the deployment", Visibility = OpenApiVisibilityType.Important)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(object), Summary = "Success", Description = "Deployment started successfully")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.NotFound, contentType: "application/json", bodyType: typeof(string), Summary = "Not Found", Description = "Deployment not found")] @@ -244,16 +237,16 @@ public async Task StartDeployment( { try { - _logger.LogInformation("Starting deployment with ID: {Id}", id); + logger.LogInformation("Starting deployment with ID: {Id}", id); // Check if deployment exists first - var existingDeployment = await _deploymentService.GetDeploymentByIdAsync(id); + var existingDeployment = await deploymentService.GetDeploymentByIdAsync(id); if (existingDeployment == null) { return new NotFoundObjectResult($"Deployment with ID '{id}' not found"); } - var success = await _deploymentService.StartDeploymentAsync(id); + var success = await deploymentService.StartDeploymentAsync(id); if (success) { return new OkObjectResult(new { Success = true, Message = "Deployment started successfully" }); @@ -263,7 +256,7 @@ public async Task StartDeployment( } catch (Exception ex) { - _logger.LogError(ex, "Error starting deployment with ID: {Id}", id); + logger.LogError(ex, "Error starting deployment with ID: {Id}", id); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } @@ -272,7 +265,7 @@ public async Task StartDeployment( /// Get deployments by template ID /// [Function("GetDeploymentsByTemplate")] - [OpenApiOperation(operationId: "GetDeploymentsByTemplate", tags: new[] { "Deployments" }, Summary = "Get deployments by template", Description = "Retrieves all deployments for a specific template", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "GetDeploymentsByTemplate", tags: ["Deployments"], Summary = "Get deployments by template", Description = "Retrieves all deployments for a specific template", Visibility = OpenApiVisibilityType.Important)] [OpenApiParameter(name: "templateId", In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = "Template ID", Description = "The unique identifier of the template", Visibility = OpenApiVisibilityType.Important)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(IEnumerable), Summary = "Success", Description = "List of deployments for the template")] public async Task GetDeploymentsByTemplate( @@ -281,14 +274,14 @@ public async Task GetDeploymentsByTemplate( { try { - _logger.LogInformation("Getting deployments for template ID: {TemplateId}", templateId); + logger.LogInformation("Getting deployments for template ID: {TemplateId}", templateId); - var deployments = await _deploymentService.GetDeploymentsByTemplateIdAsync(templateId); + var deployments = await deploymentService.GetDeploymentsByTemplateIdAsync(templateId); return new OkObjectResult(deployments); } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving deployments for template ID: {TemplateId}", templateId); + logger.LogError(ex, "Error retrieving deployments for template ID: {TemplateId}", templateId); return new ObjectResult("Internal server error") { StatusCode = 500 }; } } diff --git a/Mordos.API/Functions/StatusFunctions.cs b/Mordos.API/Functions/StatusFunctions.cs index a2f67a2..1a64a7d 100644 --- a/Mordos.API/Functions/StatusFunctions.cs +++ b/Mordos.API/Functions/StatusFunctions.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using System.Net; using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; @@ -11,25 +12,18 @@ namespace Mordos.API.Functions; /// /// API status and health check functions /// -public class StatusFunctions +public class StatusFunctions(ILogger logger) { - private readonly ILogger _logger; - - public StatusFunctions(ILogger logger) - { - _logger = logger; - } - /// /// Get API status and health /// [Function("GetStatus")] - [OpenApiOperation(operationId: "GetStatus", tags: new[] { "Status" }, Summary = "Get API status", Description = "Returns the current status and health of the Mordos API", Visibility = OpenApiVisibilityType.Important)] + [OpenApiOperation(operationId: "GetStatus", tags: ["Status"], Summary = "Get API status", Description = "Returns the current status and health of the Mordos API", Visibility = OpenApiVisibilityType.Important)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(object), Summary = "Success", Description = "API status information")] public IActionResult GetStatus( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "status")] HttpRequest req) { - _logger.LogInformation("Status endpoint called"); + logger.LogInformation("Status endpoint called"); var status = new { diff --git a/Mordos.API/Mordos.API.csproj b/Mordos.API/Mordos.API.csproj index 576a947..b3b094c 100644 --- a/Mordos.API/Mordos.API.csproj +++ b/Mordos.API/Mordos.API.csproj @@ -11,8 +11,9 @@ - + + @@ -22,6 +23,8 @@ + + diff --git a/Mordos.API/Program.cs b/Mordos.API/Program.cs index c8b49d9..cdd795a 100644 --- a/Mordos.API/Program.cs +++ b/Mordos.API/Program.cs @@ -1,39 +1,21 @@ +using Azure.Core.Serialization; using Azure.Data.Tables; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Configuration; using Mordos.API.Services; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; var builder = FunctionsApplication.CreateBuilder(args); -builder.AddServiceDefaults(); - builder.ConfigureFunctionsWebApplication(); -// Application Insights isn't enabled by default. See https://aka.ms/AAt8mw4. -builder.Services - .AddApplicationInsightsTelemetryWorkerService() - .ConfigureFunctionsApplicationInsights(); - -// Configure Azure Table Storage -builder.Services.AddSingleton(serviceProvider => -{ - var configuration = serviceProvider.GetRequiredService(); - - // Get the connection string from configuration - // In local development, this will use the storage emulator - // In production, this will use the actual Azure Storage account - var connectionString = configuration.GetConnectionString("tables") - ?? configuration["AzureWebJobsStorage"] - ?? "UseDevelopmentStorage=true"; - - return new TableServiceClient(connectionString); -}); - // Register business services -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddHttpClient() + .AddScoped() + .AddScoped(); -builder.Build().Run(); +builder.Build().Run(); \ No newline at end of file diff --git a/Mordos.API/Services/BicepTemplateService.cs b/Mordos.API/Services/BicepTemplateService.cs index 135eb94..cac123c 100644 --- a/Mordos.API/Services/BicepTemplateService.cs +++ b/Mordos.API/Services/BicepTemplateService.cs @@ -8,23 +8,15 @@ namespace Mordos.API.Services; /// /// Implementation of IBicepTemplateService using Azure Table Storage /// -public class BicepTemplateService : IBicepTemplateService +public class BicepTemplateService(TableServiceClient tableServiceClient, ILogger logger) : IBicepTemplateService { - private readonly TableServiceClient _tableServiceClient; - private readonly ILogger _logger; private const string TableName = "BicepTemplates"; - public BicepTemplateService(TableServiceClient tableServiceClient, ILogger logger) - { - _tableServiceClient = tableServiceClient; - _logger = logger; - } - public async Task> GetAllTemplatesAsync(string? nameFilter = null, string? tagFilter = null) { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var templates = new List(); @@ -41,12 +33,12 @@ public async Task> GetAllTemplatesAsync(strin templates.Add(BicepTemplateResponse.FromEntity(entity)); } - _logger.LogInformation("Retrieved {Count} Bicep templates", templates.Count); + logger.LogInformation("Retrieved {Count} Bicep templates", templates.Count); return templates.OrderByDescending(t => t.UpdatedAt); } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving Bicep templates"); + logger.LogError(ex, "Error retrieving Bicep templates"); throw; } } @@ -55,23 +47,23 @@ public async Task> GetAllTemplatesAsync(strin { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var response = await tableClient.GetEntityIfExistsAsync("BicepTemplate", id); if (!response.HasValue) { - _logger.LogInformation("Bicep template with ID {Id} not found", id); + logger.LogInformation("Bicep template with ID {Id} not found", id); return null; } - _logger.LogInformation("Retrieved Bicep template with ID {Id}", id); + logger.LogInformation("Retrieved Bicep template with ID {Id}", id); return BicepTemplateResponse.FromEntity(response.Value); } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving Bicep template with ID {Id}", id); + logger.LogError(ex, "Error retrieving Bicep template with ID {Id}", id); throw; } } @@ -80,7 +72,7 @@ public async Task CreateTemplateAsync(CreateBicepTemplate { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var template = new BicepTemplate @@ -97,12 +89,12 @@ public async Task CreateTemplateAsync(CreateBicepTemplate await tableClient.AddEntityAsync(template); - _logger.LogInformation("Created new Bicep template with ID {Id}", template.Id); + logger.LogInformation("Created new Bicep template with ID {Id}", template.Id); return BicepTemplateResponse.FromEntity(template); } catch (Exception ex) { - _logger.LogError(ex, "Error creating Bicep template"); + logger.LogError(ex, "Error creating Bicep template"); throw; } } @@ -111,14 +103,14 @@ public async Task CreateTemplateAsync(CreateBicepTemplate { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var existingResponse = await tableClient.GetEntityIfExistsAsync("BicepTemplate", id); if (!existingResponse.HasValue) { - _logger.LogInformation("Bicep template with ID {Id} not found for update", id); + logger.LogInformation("Bicep template with ID {Id} not found for update", id); return null; } @@ -143,12 +135,12 @@ public async Task CreateTemplateAsync(CreateBicepTemplate await tableClient.UpdateEntityAsync(template, template.ETag); - _logger.LogInformation("Updated Bicep template with ID {Id}", id); + logger.LogInformation("Updated Bicep template with ID {Id}", id); return BicepTemplateResponse.FromEntity(template); } catch (Exception ex) { - _logger.LogError(ex, "Error updating Bicep template with ID {Id}", id); + logger.LogError(ex, "Error updating Bicep template with ID {Id}", id); throw; } } @@ -157,17 +149,17 @@ public async Task DeleteTemplateAsync(string id) { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var response = await tableClient.DeleteEntityAsync("BicepTemplate", id); - _logger.LogInformation("Deleted Bicep template with ID {Id}", id); + logger.LogInformation("Deleted Bicep template with ID {Id}", id); return true; } catch (Exception ex) { - _logger.LogError(ex, "Error deleting Bicep template with ID {Id}", id); + logger.LogError(ex, "Error deleting Bicep template with ID {Id}", id); return false; } } @@ -176,14 +168,14 @@ public async Task ValidateTemplateAsync(string id) { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var existingResponse = await tableClient.GetEntityIfExistsAsync("BicepTemplate", id); if (!existingResponse.HasValue) { - _logger.LogInformation("Bicep template with ID {Id} not found for validation", id); + logger.LogInformation("Bicep template with ID {Id} not found for validation", id); return false; } @@ -197,12 +189,12 @@ public async Task ValidateTemplateAsync(string id) await tableClient.UpdateEntityAsync(template, template.ETag); - _logger.LogInformation("Validated Bicep template with ID {Id}", id); + logger.LogInformation("Validated Bicep template with ID {Id}", id); return true; } catch (Exception ex) { - _logger.LogError(ex, "Error validating Bicep template with ID {Id}", id); + logger.LogError(ex, "Error validating Bicep template with ID {Id}", id); return false; } } diff --git a/Mordos.API/Services/DeploymentService.cs b/Mordos.API/Services/DeploymentService.cs index ca04a7b..7d90a54 100644 --- a/Mordos.API/Services/DeploymentService.cs +++ b/Mordos.API/Services/DeploymentService.cs @@ -8,28 +8,18 @@ namespace Mordos.API.Services; /// /// Implementation of IDeploymentService using Azure Table Storage /// -public class DeploymentService : IDeploymentService +public class DeploymentService( + TableServiceClient tableServiceClient, + IBicepTemplateService bicepTemplateService, + ILogger logger) : IDeploymentService { - private readonly TableServiceClient _tableServiceClient; - private readonly IBicepTemplateService _bicepTemplateService; - private readonly ILogger _logger; private const string TableName = "Deployments"; - public DeploymentService( - TableServiceClient tableServiceClient, - IBicepTemplateService bicepTemplateService, - ILogger logger) - { - _tableServiceClient = tableServiceClient; - _bicepTemplateService = bicepTemplateService; - _logger = logger; - } - public async Task> GetAllDeploymentsAsync(string? templateIdFilter = null, DeploymentStatus? statusFilter = null) { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var deployments = new List(); @@ -46,12 +36,12 @@ public async Task> GetAllDeploymentsAsync(string deployments.Add(DeploymentResponse.FromEntity(entity)); } - _logger.LogInformation("Retrieved {Count} deployments", deployments.Count); + logger.LogInformation("Retrieved {Count} deployments", deployments.Count); return deployments.OrderByDescending(d => d.CreatedAt); } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving deployments"); + logger.LogError(ex, "Error retrieving deployments"); throw; } } @@ -60,23 +50,23 @@ public async Task> GetAllDeploymentsAsync(string { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var response = await tableClient.GetEntityIfExistsAsync("Deployment", id); if (!response.HasValue) { - _logger.LogInformation("Deployment with ID {Id} not found", id); + logger.LogInformation("Deployment with ID {Id} not found", id); return null; } - _logger.LogInformation("Retrieved deployment with ID {Id}", id); + logger.LogInformation("Retrieved deployment with ID {Id}", id); return DeploymentResponse.FromEntity(response.Value); } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving deployment with ID {Id}", id); + logger.LogError(ex, "Error retrieving deployment with ID {Id}", id); throw; } } @@ -86,13 +76,8 @@ public async Task CreateDeploymentAsync(CreateDeploymentRequ try { // Validate that the template exists - var template = await _bicepTemplateService.GetTemplateByIdAsync(request.TemplateId); - if (template == null) - { - throw new ArgumentException($"Template with ID {request.TemplateId} not found", nameof(request.TemplateId)); - } - - var tableClient = _tableServiceClient.GetTableClient(TableName); + var template = await bicepTemplateService.GetTemplateByIdAsync(request.TemplateId) ?? throw new ArgumentException($"Template with ID {request.TemplateId} not found", nameof(request.TemplateId)); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var deployment = new Deployment @@ -111,12 +96,12 @@ public async Task CreateDeploymentAsync(CreateDeploymentRequ await tableClient.AddEntityAsync(deployment); - _logger.LogInformation("Created new deployment with ID {Id} for template {TemplateId}", deployment.Id, request.TemplateId); + logger.LogInformation("Created new deployment with ID {Id} for template {TemplateId}", deployment.Id, request.TemplateId); return DeploymentResponse.FromEntity(deployment); } catch (Exception ex) { - _logger.LogError(ex, "Error creating deployment for template {TemplateId}", request.TemplateId); + logger.LogError(ex, "Error creating deployment for template {TemplateId}", request.TemplateId); throw; } } @@ -125,14 +110,14 @@ public async Task CreateDeploymentAsync(CreateDeploymentRequ { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var existingResponse = await tableClient.GetEntityIfExistsAsync("Deployment", id); if (!existingResponse.HasValue) { - _logger.LogInformation("Deployment with ID {Id} not found for update", id); + logger.LogInformation("Deployment with ID {Id} not found for update", id); return null; } @@ -172,12 +157,12 @@ public async Task CreateDeploymentAsync(CreateDeploymentRequ await tableClient.UpdateEntityAsync(deployment, deployment.ETag); - _logger.LogInformation("Updated deployment with ID {Id}. Status changed: {StatusChanged}", id, statusChanged); + logger.LogInformation("Updated deployment with ID {Id}. Status changed: {StatusChanged}", id, statusChanged); return DeploymentResponse.FromEntity(deployment); } catch (Exception ex) { - _logger.LogError(ex, "Error updating deployment with ID {Id}", id); + logger.LogError(ex, "Error updating deployment with ID {Id}", id); throw; } } @@ -186,17 +171,17 @@ public async Task DeleteDeploymentAsync(string id) { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var response = await tableClient.DeleteEntityAsync("Deployment", id); - _logger.LogInformation("Deleted deployment with ID {Id}", id); + logger.LogInformation("Deleted deployment with ID {Id}", id); return true; } catch (Exception ex) { - _logger.LogError(ex, "Error deleting deployment with ID {Id}", id); + logger.LogError(ex, "Error deleting deployment with ID {Id}", id); return false; } } @@ -215,7 +200,7 @@ public async Task StartDeploymentAsync(string id) } catch (Exception ex) { - _logger.LogError(ex, "Error starting deployment with ID {Id}", id); + logger.LogError(ex, "Error starting deployment with ID {Id}", id); return false; } } @@ -224,7 +209,7 @@ public async Task> GetDeploymentsByTemplateIdAsy { try { - var tableClient = _tableServiceClient.GetTableClient(TableName); + var tableClient = tableServiceClient.GetTableClient(TableName); await tableClient.CreateIfNotExistsAsync(); var deployments = new List(); @@ -234,12 +219,12 @@ public async Task> GetDeploymentsByTemplateIdAsy deployments.Add(DeploymentResponse.FromEntity(entity)); } - _logger.LogInformation("Retrieved {Count} deployments for template {TemplateId}", deployments.Count, templateId); + logger.LogInformation("Retrieved {Count} deployments for template {TemplateId}", deployments.Count, templateId); return deployments.OrderByDescending(d => d.CreatedAt); } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving deployments for template {TemplateId}", templateId); + logger.LogError(ex, "Error retrieving deployments for template {TemplateId}", templateId); throw; } } diff --git a/Mordos.AppHost/AppHost.cs b/Mordos.AppHost/AppHost.cs index fa5efbf..e240729 100644 --- a/Mordos.AppHost/AppHost.cs +++ b/Mordos.AppHost/AppHost.cs @@ -1,3 +1,5 @@ +using Google.Protobuf.WellKnownTypes; + var builder = DistributedApplication.CreateBuilder(args); var storage = builder.AddAzureStorage("storage") @@ -8,4 +10,6 @@ .WithHostStorage(storage) .WithReference(tables); +builder.AddProject("mordos-web"); + builder.Build().Run(); diff --git a/Mordos.AppHost/Mordos.AppHost.csproj b/Mordos.AppHost/Mordos.AppHost.csproj index 5350e23..c17ad56 100644 --- a/Mordos.AppHost/Mordos.AppHost.csproj +++ b/Mordos.AppHost/Mordos.AppHost.csproj @@ -11,12 +11,13 @@ - + + diff --git a/Mordos.ServiceDefaults/Extensions.cs b/Mordos.ServiceDefaults/Extensions.cs index 112c128..8c00b8e 100644 --- a/Mordos.ServiceDefaults/Extensions.cs +++ b/Mordos.ServiceDefaults/Extensions.cs @@ -124,4 +124,4 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) return app; } -} +} \ No newline at end of file diff --git a/Mordos.ServiceDefaults/Mordos.ServiceDefaults.csproj b/Mordos.ServiceDefaults/Mordos.ServiceDefaults.csproj index 7f40da6..bd69aee 100644 --- a/Mordos.ServiceDefaults/Mordos.ServiceDefaults.csproj +++ b/Mordos.ServiceDefaults/Mordos.ServiceDefaults.csproj @@ -9,9 +9,8 @@ - - - + + diff --git a/Mordos.Web/.config/dotnet-tools.json b/Mordos.Web/.config/dotnet-tools.json new file mode 100644 index 0000000..8bed88d --- /dev/null +++ b/Mordos.Web/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "microsoft.dotnet-msidentity": { + "version": "2.0.8", + "commands": [ + "dotnet-msidentity" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/Mordos.Web/App.razor b/Mordos.Web/App.razor new file mode 100644 index 0000000..d07039a --- /dev/null +++ b/Mordos.Web/App.razor @@ -0,0 +1,25 @@ + + + + + + @if (context.User.Identity?.IsAuthenticated != true) + { + + } + else + { +

You are not authorized to access this resource.

+ } +
+
+ +
+ + Not found + +

Sorry, there's nothing at this address.

+
+
+
+
diff --git a/Mordos.Web/Layout/LoginDisplay.razor b/Mordos.Web/Layout/LoginDisplay.razor new file mode 100644 index 0000000..7775a52 --- /dev/null +++ b/Mordos.Web/Layout/LoginDisplay.razor @@ -0,0 +1,19 @@ +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@inject NavigationManager Navigation + + + + Hello, @context.User.Identity?.Name! + + + + Log in + + + +@code{ + public void BeginLogOut() + { + Navigation.NavigateToLogout("authentication/logout"); + } +} diff --git a/Mordos.Web/Layout/MainLayout.razor b/Mordos.Web/Layout/MainLayout.razor new file mode 100644 index 0000000..3ac8d06 --- /dev/null +++ b/Mordos.Web/Layout/MainLayout.razor @@ -0,0 +1,4 @@ +@inherits LayoutComponentBase + + +@Body diff --git a/Mordos.Web/Layout/RedirectToLogin.razor b/Mordos.Web/Layout/RedirectToLogin.razor new file mode 100644 index 0000000..a1cf400 --- /dev/null +++ b/Mordos.Web/Layout/RedirectToLogin.razor @@ -0,0 +1,9 @@ +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@inject NavigationManager Navigation + +@code { + protected override void OnInitialized() + { + Navigation.NavigateToLogin("authentication/login"); + } +} diff --git a/Mordos.Web/Mordos.Web.csproj b/Mordos.Web/Mordos.Web.csproj new file mode 100644 index 0000000..b92925e --- /dev/null +++ b/Mordos.Web/Mordos.Web.csproj @@ -0,0 +1,20 @@ + + + + net9.0 + enable + enable + service-worker-assets.js + + + + + + + + + + + + + diff --git a/Mordos.Web/Pages/Authentication.razor b/Mordos.Web/Pages/Authentication.razor new file mode 100644 index 0000000..6c74356 --- /dev/null +++ b/Mordos.Web/Pages/Authentication.razor @@ -0,0 +1,7 @@ +@page "/authentication/{action}" +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + + +@code{ + [Parameter] public string? Action { get; set; } +} diff --git a/Mordos.Web/Pages/Home.razor b/Mordos.Web/Pages/Home.razor new file mode 100644 index 0000000..9001e0b --- /dev/null +++ b/Mordos.Web/Pages/Home.razor @@ -0,0 +1,7 @@ +@page "/" + +Home + +

Hello, world!

+ +Welcome to your new app. diff --git a/Mordos.Web/Program.cs b/Mordos.Web/Program.cs new file mode 100644 index 0000000..e07487d --- /dev/null +++ b/Mordos.Web/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Mordos.Web; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + +builder.Services.AddMsalAuthentication(options => +{ + builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); +}); + +await builder.Build().RunAsync(); diff --git a/Mordos.Web/Properties/launchSettings.json b/Mordos.Web/Properties/launchSettings.json new file mode 100644 index 0000000..299ade9 --- /dev/null +++ b/Mordos.Web/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7045;http://localhost:5285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Mordos.Web/_Imports.razor b/Mordos.Web/_Imports.razor new file mode 100644 index 0000000..4261002 --- /dev/null +++ b/Mordos.Web/_Imports.razor @@ -0,0 +1,11 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Mordos.Web +@using Mordos.Web.Layout diff --git a/Mordos.Web/design-requirements.md b/Mordos.Web/design-requirements.md new file mode 100644 index 0000000..3a8a8f1 --- /dev/null +++ b/Mordos.Web/design-requirements.md @@ -0,0 +1,25 @@ +# Design Requirements + +## Overview + +This document outlines the design requirements for the Mordos Web application. It serves as a guide for developers and designers to ensure that the application meets the necessary standards and provides a consistent user experience. + +## General Requirements + +1. **User Authentication**: The application must support user registration and login with Entra ID. Users should be able to authenticate using their organizational accounts. +1. **Dashboard**: Users should have access to a personalized dashboard that displays relevant information and quick access to features. +1. **Content Management**: The application must allow users to create, edit, and delete content. This includes resource templates, workload templates, deployments, and more. +1. **Workload Designer**: Users should be able to design workloads using a visual interface that allows for drag-and-drop functionality. Workflow.js or nuget equivalent is recommended for this purpose. +1. **Bicep and ARM focus**: The application should support Bicep and ARM templates for resource deployment. Users should be able to create and manage these templates within the application. +1. **API Focus**: This application should rely fully on the `Mordos.API` project for backend functionality. +1. **Desired State Configuration (DSC)**: Implement DSC to ensure that the application can maintain the desired state of resources and configurations across deployments. +1. **User Roles and Permissions**: Implement a role-based access control system to manage user permissions and access to different features of the application. +1. **Multi-Tenant Support**: The application should support multi-tenant scenarios, allowing users to manage resources across different tenants. + + +## Feature Ideas + +1. **Resource Templates**: Users should be able to create and manage resource templates that can be reused across different workloads. +1. **Workload Templates**: The application should support the creation of workload templates that can be applied to multiple tenants or environments. +1. **Deployment Management**: Users should be able to manage deployments, including viewing deployment history, status, and logs. +1. **Notifications**: Users should receive notifications for important events, such as deployment status changes, errors, or updates to resources. diff --git a/Mordos.Web/wwwroot/appsettings.json b/Mordos.Web/wwwroot/appsettings.json new file mode 100644 index 0000000..c28c508 --- /dev/null +++ b/Mordos.Web/wwwroot/appsettings.json @@ -0,0 +1,12 @@ +{ + /* + The following identity settings need to be configured + before the project can be successfully executed. + For more info see https://aka.ms/dotnet-template-ms-identity-platform + */ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/22222222-2222-2222-2222-222222222222", + "ClientId": "33333333-3333-3333-33333333333333333", + "ValidateAuthority": true + } +} diff --git a/Mordos.Web/wwwroot/css/app.css b/Mordos.Web/wwwroot/css/app.css new file mode 100644 index 0000000..7bc88b2 --- /dev/null +++ b/Mordos.Web/wwwroot/css/app.css @@ -0,0 +1,88 @@ +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +code { + color: #c02d76; +} + +.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { + color: var(--bs-secondary-color); + text-align: end; +} + +.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { + text-align: start; +} \ No newline at end of file diff --git a/Mordos.Web/wwwroot/icon-192.png b/Mordos.Web/wwwroot/icon-192.png new file mode 100644 index 0000000..166f56d Binary files /dev/null and b/Mordos.Web/wwwroot/icon-192.png differ diff --git a/Mordos.Web/wwwroot/icon-512.png b/Mordos.Web/wwwroot/icon-512.png new file mode 100644 index 0000000..c2dd484 Binary files /dev/null and b/Mordos.Web/wwwroot/icon-512.png differ diff --git a/Mordos.Web/wwwroot/index.html b/Mordos.Web/wwwroot/index.html new file mode 100644 index 0000000..f2b67a6 --- /dev/null +++ b/Mordos.Web/wwwroot/index.html @@ -0,0 +1,36 @@ + + + + + + + Mordos.Web + + + + + + + + + +
+ + + + +
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + diff --git a/Mordos.Web/wwwroot/manifest.webmanifest b/Mordos.Web/wwwroot/manifest.webmanifest new file mode 100644 index 0000000..ee62bbb --- /dev/null +++ b/Mordos.Web/wwwroot/manifest.webmanifest @@ -0,0 +1,22 @@ +{ + "name": "Mordos.Web", + "short_name": "Mordos.Web", + "id": "./", + "start_url": "./", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#03173d", + "prefer_related_applications": false, + "icons": [ + { + "src": "icon-512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "icon-192.png", + "type": "image/png", + "sizes": "192x192" + } + ] +} diff --git a/Mordos.Web/wwwroot/service-worker.js b/Mordos.Web/wwwroot/service-worker.js new file mode 100644 index 0000000..fe614da --- /dev/null +++ b/Mordos.Web/wwwroot/service-worker.js @@ -0,0 +1,4 @@ +// In development, always fetch from the network and do not enable offline support. +// This is because caching would make development more difficult (changes would not +// be reflected on the first load after each change). +self.addEventListener('fetch', () => { }); diff --git a/Mordos.Web/wwwroot/service-worker.published.js b/Mordos.Web/wwwroot/service-worker.published.js new file mode 100644 index 0000000..1f7f543 --- /dev/null +++ b/Mordos.Web/wwwroot/service-worker.published.js @@ -0,0 +1,55 @@ +// Caution! Be sure you understand the caveats before publishing an application with +// offline support. See https://aka.ms/blazor-offline-considerations + +self.importScripts('./service-worker-assets.js'); +self.addEventListener('install', event => event.waitUntil(onInstall(event))); +self.addEventListener('activate', event => event.waitUntil(onActivate(event))); +self.addEventListener('fetch', event => event.respondWith(onFetch(event))); + +const cacheNamePrefix = 'offline-cache-'; +const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; +const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; +const offlineAssetsExclude = [ /^service-worker\.js$/ ]; + +// Replace with your base path if you are hosting on a subfolder. Ensure there is a trailing '/'. +const base = "/"; +const baseUrl = new URL(base, self.origin); +const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.url, baseUrl).href); + +async function onInstall(event) { + console.info('Service worker: Install'); + + // Fetch and cache all matching items from the assets manifest + const assetsRequests = self.assetsManifest.assets + .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) + .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) + .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); + await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); +} + +async function onActivate(event) { + console.info('Service worker: Activate'); + + // Delete unused caches + const cacheKeys = await caches.keys(); + await Promise.all(cacheKeys + .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) + .map(key => caches.delete(key))); +} + +async function onFetch(event) { + let cachedResponse = null; + if (event.request.method === 'GET') { + // For all navigation requests, try to serve index.html from cache, + // unless that request is for an offline resource. + // If you need some URLs to be server-rendered, edit the following check to exclude those URLs + const shouldServeIndexHtml = event.request.mode === 'navigate' + && !manifestUrlList.some(url => url === event.request.url); + + const request = shouldServeIndexHtml ? 'index.html' : event.request; + const cache = await caches.open(cacheName); + cachedResponse = await cache.match(request); + } + + return cachedResponse || fetch(event.request); +} diff --git a/Mordos.sln b/Mordos.sln index 57ce603..b1174da 100644 --- a/Mordos.sln +++ b/Mordos.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36401.2 d17.14 +VisualStudioVersion = 17.14.36401.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mordos.API", "Mordos.API\Mordos.API.csproj", "{09494C88-573A-7C71-DC03-749DEBCF8716}" EndProject @@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{ .mcp.json = .mcp.json EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mordos.Web", "Mordos.Web\Mordos.Web.csproj", "{9A9ED2D5-9139-0C2D-CB7E-4D251C5E92F5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +34,10 @@ Global {EF99234D-3FE5-6B80-8EFE-0E07656385D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF99234D-3FE5-6B80-8EFE-0E07656385D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF99234D-3FE5-6B80-8EFE-0E07656385D0}.Release|Any CPU.Build.0 = Release|Any CPU + {9A9ED2D5-9139-0C2D-CB7E-4D251C5E92F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A9ED2D5-9139-0C2D-CB7E-4D251C5E92F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A9ED2D5-9139-0C2D-CB7E-4D251C5E92F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A9ED2D5-9139-0C2D-CB7E-4D251C5E92F5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE