diff --git a/CaddyManager.Contracts/Caddy/ICaddyConfigurationParsingService.cs b/CaddyManager.Contracts/Caddy/ICaddyConfigurationParsingService.cs
index 901b0f4..14f1390 100644
--- a/CaddyManager.Contracts/Caddy/ICaddyConfigurationParsingService.cs
+++ b/CaddyManager.Contracts/Caddy/ICaddyConfigurationParsingService.cs
@@ -35,4 +35,11 @@ public interface ICaddyConfigurationParsingService
///
///
List GetReverseProxyPortsFromCaddyfileContent(string caddyfileContent);
+
+ ///
+ /// Extracts tags from a Caddyfile content using the format: # Tags: [tag1;tag2;tag3]
+ ///
+ ///
+ ///
+ List GetTagsFromCaddyfileContent(string caddyfileContent);
}
\ No newline at end of file
diff --git a/CaddyManager.Contracts/Models/Caddy/CaddyConfigurationInfo.cs b/CaddyManager.Contracts/Models/Caddy/CaddyConfigurationInfo.cs
index fc91a85..7178e51 100644
--- a/CaddyManager.Contracts/Models/Caddy/CaddyConfigurationInfo.cs
+++ b/CaddyManager.Contracts/Models/Caddy/CaddyConfigurationInfo.cs
@@ -30,6 +30,11 @@ public class CaddyConfigurationInfo
///
public List AggregatedReverseProxyPorts { get; set; } = [];
+ ///
+ /// Tags extracted from the configuration content using the format: # Tags: [tag1;tag2;tag3]
+ ///
+ public List Tags { get; set; } = [];
+
public override bool Equals(object? obj)
{
if (obj is not CaddyConfigurationInfo other)
diff --git a/CaddyManager.Services/Caddy/CaddyConfigurationParsingService.cs b/CaddyManager.Services/Caddy/CaddyConfigurationParsingService.cs
index a4cf8fc..27ef822 100644
--- a/CaddyManager.Services/Caddy/CaddyConfigurationParsingService.cs
+++ b/CaddyManager.Services/Caddy/CaddyConfigurationParsingService.cs
@@ -39,7 +39,7 @@ public partial class CaddyConfigurationParsingService: ICaddyConfigurationParsin
hostnames.AddRange(splitHostnames);
}
// Remove duplicates and return the list
- return hostnames.Distinct().ToList();
+ return [.. hostnames.Distinct()];
}
///
@@ -80,6 +80,39 @@ public partial class CaddyConfigurationParsingService: ICaddyConfigurationParsin
}
}
- return results.Distinct().ToList();
+ return [.. results.Distinct()];
+ }
+
+ ///
+ public List GetTagsFromCaddyfileContent(string caddyfileContent)
+ {
+ // Split the content into lines and look for the tags line
+ var lines = caddyfileContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+
+ foreach (var line in lines)
+ {
+ var trimmedLine = line.Trim();
+ if (trimmedLine.StartsWith("#"))
+ {
+ // Remove the # and any leading whitespace, then check if it starts with "tags:"
+ var afterHash = trimmedLine.Substring(1).TrimStart();
+ if (afterHash.StartsWith("tags:", StringComparison.OrdinalIgnoreCase))
+ {
+ // Extract the part after "tags:"
+ var tagsString = afterHash.Substring(5).Trim(); // 5 = length of "tags:"
+ if (string.IsNullOrWhiteSpace(tagsString))
+ return [];
+
+ // Split by semicolon and clean up each tag
+ return [.. tagsString.Split(';')
+ .Select(tag => tag.Trim())
+ .Where(tag => !string.IsNullOrWhiteSpace(tag))
+ .Distinct()];
+ }
+ }
+ }
+
+ // No tags line found
+ return [];
}
}
\ No newline at end of file
diff --git a/CaddyManager.Services/Caddy/CaddyService.cs b/CaddyManager.Services/Caddy/CaddyService.cs
index fe5c768..b2e637f 100644
--- a/CaddyManager.Services/Caddy/CaddyService.cs
+++ b/CaddyManager.Services/Caddy/CaddyService.cs
@@ -146,7 +146,11 @@ public class CaddyService(
///
public CaddyConfigurationInfo GetCaddyConfigurationInfo(string configurationName)
{
- var result = new CaddyConfigurationInfo();
+ var result = new CaddyConfigurationInfo
+ {
+ FileName = configurationName
+ };
+
var content = GetCaddyConfigurationContent(configurationName);
if (string.IsNullOrWhiteSpace(content))
{
@@ -156,6 +160,7 @@ public class CaddyService(
result.Hostnames = parsingService.GetHostnamesFromCaddyfileContent(content);
result.ReverseProxyHostname = parsingService.GetReverseProxyTargetFromCaddyfileContent(content);
result.ReverseProxyPorts = parsingService.GetReverseProxyPortsFromCaddyfileContent(content);
+ result.Tags = parsingService.GetTagsFromCaddyfileContent(content);
return result;
}
diff --git a/CaddyManager.Tests/Models/Caddy/CaddyConfigurationInfoTests.cs b/CaddyManager.Tests/Models/Caddy/CaddyConfigurationInfoTests.cs
index d4e5d0a..cb98a94 100644
--- a/CaddyManager.Tests/Models/Caddy/CaddyConfigurationInfoTests.cs
+++ b/CaddyManager.Tests/Models/Caddy/CaddyConfigurationInfoTests.cs
@@ -27,6 +27,8 @@ public class CaddyConfigurationInfoTests
info.FileName.Should().Be(string.Empty);
info.AggregatedReverseProxyPorts.Should().NotBeNull();
info.AggregatedReverseProxyPorts.Should().BeEmpty();
+ info.Tags.Should().NotBeNull();
+ info.Tags.Should().BeEmpty();
}
///
@@ -43,6 +45,7 @@ public class CaddyConfigurationInfoTests
var reverseProxyPorts = new List { 8080, 9090 };
var fileName = "test-config";
var aggregatedPorts = new List { 8080, 9090, 3000 };
+ var tags = new List { "web", "production", "ssl" };
// Act
var info = new CaddyConfigurationInfo
@@ -51,7 +54,8 @@ public class CaddyConfigurationInfoTests
ReverseProxyHostname = reverseProxyHostname,
ReverseProxyPorts = reverseProxyPorts,
FileName = fileName,
- AggregatedReverseProxyPorts = aggregatedPorts
+ AggregatedReverseProxyPorts = aggregatedPorts,
+ Tags = tags
};
// Assert
@@ -60,6 +64,7 @@ public class CaddyConfigurationInfoTests
info.ReverseProxyPorts.Should().BeEquivalentTo(reverseProxyPorts);
info.FileName.Should().Be(fileName);
info.AggregatedReverseProxyPorts.Should().BeEquivalentTo(aggregatedPorts);
+ info.Tags.Should().BeEquivalentTo(tags);
}
///
diff --git a/CaddyManager.Tests/Services/Caddy/CaddyConfigurationParsingServiceTests.cs b/CaddyManager.Tests/Services/Caddy/CaddyConfigurationParsingServiceTests.cs
index 218488c..f1882a6 100644
--- a/CaddyManager.Tests/Services/Caddy/CaddyConfigurationParsingServiceTests.cs
+++ b/CaddyManager.Tests/Services/Caddy/CaddyConfigurationParsingServiceTests.cs
@@ -665,4 +665,289 @@ api.test {
}
#endregion
+
+ #region GetTagsFromCaddyfileContent Tests
+
+ ///
+ /// Tests that the parsing service correctly extracts tags from a basic tags comment line.
+ /// Setup: Provides a Caddyfile content string with a simple tags comment in the correct format.
+ /// Expectation: The service should return a list containing all tags separated by semicolons, enabling proper tag-based organization of Caddy configurations.
+ ///
+ [Fact]
+ public void GetTagsFromCaddyfileContent_WithBasicTags_ReturnsCorrectTags()
+ {
+ // Arrange
+ var caddyfileContent = @"
+# Tags: web;production;ssl
+example.com {
+ reverse_proxy localhost:8080
+}";
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().HaveCount(3);
+ result.Should().Contain("web");
+ result.Should().Contain("production");
+ result.Should().Contain("ssl");
+ }
+
+ ///
+ /// Tests that the parsing service correctly extracts tags with whitespace variations.
+ /// Setup: Provides a Caddyfile content string with tags containing various whitespace patterns.
+ /// Expectation: The service should trim whitespace and return clean tag names, ensuring consistent tag handling regardless of formatting.
+ ///
+ [Fact]
+ public void GetTagsFromCaddyfileContent_WithWhitespaceVariations_ReturnsTrimmedTags()
+ {
+ // Arrange
+ var caddyfileContent = @"
+# Tags: web ; production ; ssl
+example.com {
+ reverse_proxy localhost:8080
+}";
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().HaveCount(3);
+ result.Should().Contain("web");
+ result.Should().Contain("production");
+ result.Should().Contain("ssl");
+ }
+
+ ///
+ /// Tests that the parsing service correctly extracts a single tag.
+ /// Setup: Provides a Caddyfile content string with only one tag in the tags comment.
+ /// Expectation: The service should return a list containing exactly one tag, ensuring proper handling of minimal tag configurations.
+ ///
+ [Fact]
+ public void GetTagsFromCaddyfileContent_WithSingleTag_ReturnsOneTag()
+ {
+ // Arrange
+ var caddyfileContent = @"
+# Tags: production
+example.com {
+ reverse_proxy localhost:8080
+}";
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().HaveCount(1);
+ result.Should().Contain("production");
+ }
+
+ ///
+ /// Tests that the parsing service handles case-insensitive tags comment detection.
+ /// Setup: Provides a Caddyfile content string with various case combinations for the tags comment.
+ /// Expectation: The service should detect tags comments regardless of case, ensuring robust tag parsing across different writing styles.
+ ///
+ [Theory]
+ [InlineData("# Tags: web;production")]
+ [InlineData("# tags: web;production")]
+ [InlineData("# TAGS: web;production")]
+ [InlineData("#Tags: web;production")]
+ [InlineData("# Tags: web;production")]
+ public void GetTagsFromCaddyfileContent_WithCaseVariations_ReturnsCorrectTags(string tagsLine)
+ {
+ // Arrange
+ var caddyfileContent = $@"
+{tagsLine}
+example.com {{
+ reverse_proxy localhost:8080
+}}";
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().HaveCount(2);
+ result.Should().Contain("web");
+ result.Should().Contain("production");
+ }
+
+ ///
+ /// Tests that the parsing service handles empty tags list gracefully.
+ /// Setup: Provides a Caddyfile content string with an empty tags comment.
+ /// Expectation: The service should return an empty list when tags are empty, ensuring proper handling of configurations without tags.
+ ///
+ [Fact]
+ public void GetTagsFromCaddyfileContent_WithEmptyTagsList_ReturnsEmptyList()
+ {
+ // Arrange
+ var caddyfileContent = @"
+# Tags:
+example.com {
+ reverse_proxy localhost:8080
+}";
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().BeEmpty();
+ }
+
+ ///
+ /// Tests that the parsing service handles duplicate tags by removing them.
+ /// Setup: Provides a Caddyfile content string with duplicate tags in the tags comment.
+ /// Expectation: The service should return a list with unique tags only, ensuring clean tag lists without duplicates.
+ ///
+ [Fact]
+ public void GetTagsFromCaddyfileContent_WithDuplicateTags_ReturnsUniqueTags()
+ {
+ // Arrange
+ var caddyfileContent = @"
+# Tags: web;production;web;ssl;production
+example.com {
+ reverse_proxy localhost:8080
+}";
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().HaveCount(3);
+ result.Should().Contain("web");
+ result.Should().Contain("production");
+ result.Should().Contain("ssl");
+ }
+
+ ///
+ /// Tests that the parsing service handles content without tags comment gracefully.
+ /// Setup: Provides a Caddyfile content string without any tags comment.
+ /// Expectation: The service should return an empty list when no tags comment is found, ensuring proper handling of non-tagged configurations.
+ ///
+ [Fact]
+ public void GetTagsFromCaddyfileContent_WithoutTagsComment_ReturnsEmptyList()
+ {
+ // Arrange
+ var caddyfileContent = @"
+example.com {
+ reverse_proxy localhost:8080
+}";
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().BeEmpty();
+ }
+
+ ///
+ /// Tests that the parsing service handles malformed tags comment gracefully.
+ /// Setup: Provides a Caddyfile content string with malformed tags comment that doesn't match the expected format.
+ /// Expectation: The service should return an empty list when tags comment is malformed, ensuring robust error handling for invalid tag formats.
+ ///
+ [Theory]
+ [InlineData("# Tags web;production")] // Missing colon
+ [InlineData("Tags: web;production")] // Missing hash
+ public void GetTagsFromCaddyfileContent_WithMalformedTagsComment_ReturnsEmptyList(string tagsLine)
+ {
+ // Arrange
+ var caddyfileContent = $@"
+{tagsLine}
+example.com {{
+ reverse_proxy localhost:8080
+}}";
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().BeEmpty();
+ }
+
+ ///
+ /// Tests that the parsing service handles empty string content gracefully.
+ /// Setup: Provides an empty string as Caddyfile content.
+ /// Expectation: The service should return an empty list when content is empty, ensuring robust error handling for missing configurations.
+ ///
+ [Fact]
+ public void GetTagsFromCaddyfileContent_WithEmptyContent_ReturnsEmptyList()
+ {
+ // Arrange
+ var caddyfileContent = string.Empty;
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().BeEmpty();
+ }
+
+ ///
+ /// Tests that the parsing service handles complex tag names with special characters.
+ /// Setup: Provides a Caddyfile content string with tags containing special characters and complex names.
+ /// Expectation: The service should correctly extract tags with special characters, ensuring support for comprehensive tag naming schemes.
+ ///
+ [Fact]
+ public void GetTagsFromCaddyfileContent_WithComplexTagNames_ReturnsCorrectTags()
+ {
+ // Arrange
+ var caddyfileContent = @"
+# Tags: web-frontend;backend_api;v2.0;production-env;team:alpha
+example.com {
+ reverse_proxy localhost:8080
+}";
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().HaveCount(5);
+ result.Should().Contain("web-frontend");
+ result.Should().Contain("backend_api");
+ result.Should().Contain("v2.0");
+ result.Should().Contain("production-env");
+ result.Should().Contain("team:alpha");
+ }
+
+ ///
+ /// Tests that the parsing service only extracts the first tags comment when multiple are present.
+ /// Setup: Provides a Caddyfile content string with multiple tags comments.
+ /// Expectation: The service should only extract tags from the first valid tags comment, ensuring consistent behavior when multiple tag definitions exist.
+ ///
+ [Fact]
+ public void GetTagsFromCaddyfileContent_WithMultipleTagsComments_ReturnsFirstMatch()
+ {
+ // Arrange
+ var caddyfileContent = @"
+# Tags: web;production
+example.com {
+ reverse_proxy localhost:8080
+}
+
+# Tags: api;development
+api.example.com {
+ reverse_proxy localhost:8081
+}";
+
+ // Act
+ var result = _service.GetTagsFromCaddyfileContent(caddyfileContent);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().HaveCount(2);
+ result.Should().Contain("web");
+ result.Should().Contain("production");
+ result.Should().NotContain("api");
+ result.Should().NotContain("development");
+ }
+
+ #endregion
}
\ No newline at end of file
diff --git a/CaddyManager.Tests/Services/Caddy/CaddyServiceTests.cs b/CaddyManager.Tests/Services/Caddy/CaddyServiceTests.cs
index 2b7f22d..0c1c416 100644
--- a/CaddyManager.Tests/Services/Caddy/CaddyServiceTests.cs
+++ b/CaddyManager.Tests/Services/Caddy/CaddyServiceTests.cs
@@ -594,6 +594,7 @@ public class CaddyServiceTests : IDisposable
var expectedHostnames = new List { "example.com" };
var expectedTarget = "localhost";
var expectedPorts = new List { 8080 };
+ var expectedTags = new List();
_mockParsingService
.Setup(x => x.GetHostnamesFromCaddyfileContent(testContent))
@@ -604,6 +605,9 @@ public class CaddyServiceTests : IDisposable
_mockParsingService
.Setup(x => x.GetReverseProxyPortsFromCaddyfileContent(testContent))
.Returns(expectedPorts);
+ _mockParsingService
+ .Setup(x => x.GetTagsFromCaddyfileContent(testContent))
+ .Returns(expectedTags);
// Act
var result = _service.GetCaddyConfigurationInfo("test");
@@ -613,6 +617,57 @@ public class CaddyServiceTests : IDisposable
result.Hostnames.Should().BeEquivalentTo(expectedHostnames);
result.ReverseProxyHostname.Should().Be(expectedTarget);
result.ReverseProxyPorts.Should().BeEquivalentTo(expectedPorts);
+ result.Tags.Should().BeEquivalentTo(expectedTags);
+ }
+
+ ///
+ /// Tests that the Caddy service correctly populates the Tags property when configuration content contains tags.
+ /// Setup: Creates a configuration file with tags comment and mocks the parsing service to return expected tags.
+ /// Expectation: The service should correctly populate the Tags property using the parsing service, ensuring tag information is available for configuration management.
+ ///
+ [Fact]
+ public void GetCaddyConfigurationInfo_WithTags_PopulatesTagsCorrectly()
+ {
+ // Arrange
+ var testContent = @"
+# Tags: [web;production;ssl]
+example.com {
+ reverse_proxy localhost:8080
+}";
+ var filePath = Path.Combine(_tempConfigDir, "test-with-tags.caddy");
+ File.WriteAllText(filePath, testContent);
+
+ var expectedHostnames = new List { "example.com" };
+ var expectedTarget = "localhost";
+ var expectedPorts = new List { 8080 };
+ var expectedTags = new List { "web", "production", "ssl" };
+
+ _mockParsingService
+ .Setup(x => x.GetHostnamesFromCaddyfileContent(testContent))
+ .Returns(expectedHostnames);
+ _mockParsingService
+ .Setup(x => x.GetReverseProxyTargetFromCaddyfileContent(testContent))
+ .Returns(expectedTarget);
+ _mockParsingService
+ .Setup(x => x.GetReverseProxyPortsFromCaddyfileContent(testContent))
+ .Returns(expectedPorts);
+ _mockParsingService
+ .Setup(x => x.GetTagsFromCaddyfileContent(testContent))
+ .Returns(expectedTags);
+
+ // Act
+ var result = _service.GetCaddyConfigurationInfo("test-with-tags");
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Hostnames.Should().BeEquivalentTo(expectedHostnames);
+ result.ReverseProxyHostname.Should().Be(expectedTarget);
+ result.ReverseProxyPorts.Should().BeEquivalentTo(expectedPorts);
+ result.Tags.Should().BeEquivalentTo(expectedTags);
+ result.Tags.Should().HaveCount(3);
+ result.Tags.Should().Contain("web");
+ result.Tags.Should().Contain("production");
+ result.Tags.Should().Contain("ssl");
}
///
diff --git a/CaddyManager/Components/Pages/Caddy/CaddyReverseProxies/CaddyReverseProxyItem.razor b/CaddyManager/Components/Pages/Caddy/CaddyReverseProxies/CaddyReverseProxyItem.razor
index e22a837..92a696d 100644
--- a/CaddyManager/Components/Pages/Caddy/CaddyReverseProxies/CaddyReverseProxyItem.razor
+++ b/CaddyManager/Components/Pages/Caddy/CaddyReverseProxies/CaddyReverseProxyItem.razor
@@ -7,6 +7,21 @@
@ConfigurationInfo.FileName
+ @if (ConfigurationInfo.Tags.Count > 0)
+ {
+
+
+ @("tag".ToQuantity(ConfigurationInfo.Tags.Count))
+
+
+ @foreach (var tag in ConfigurationInfo.Tags)
+ {
+ ⏵ @tag
+ }
+
+
+ }
@ConfigurationInfo.ReverseProxyHostname
diff --git a/CaddyManager/Components/Pages/Caddy/CaddyReverseProxies/CaddyReverseProxyItem.razor.cs b/CaddyManager/Components/Pages/Caddy/CaddyReverseProxies/CaddyReverseProxyItem.razor.cs
index ada5e94..5d558ae 100644
--- a/CaddyManager/Components/Pages/Caddy/CaddyReverseProxies/CaddyReverseProxyItem.razor.cs
+++ b/CaddyManager/Components/Pages/Caddy/CaddyReverseProxies/CaddyReverseProxyItem.razor.cs
@@ -1,3 +1,4 @@
+using CaddyManager.Contracts.Caddy;
using CaddyManager.Contracts.Models.Caddy;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -28,6 +29,11 @@ public partial class CaddyReverseProxyItem : ComponentBase
[Inject]
private IDialogService DialogService { get; set; } = null!;
+ ///
+ /// Caddy service for ops on the Caddy configuration
+ ///
+ [Inject] private ICaddyService CaddyService { get; set; } = null!;
+
///
/// Show the Caddy file editor dialog
///
@@ -45,6 +51,9 @@ public partial class CaddyReverseProxyItem : ComponentBase
});
var result = await dialog.Result;
+
+ ConfigurationInfo = CaddyService.GetCaddyConfigurationInfo(ConfigurationInfo.FileName);
+ await InvokeAsync(StateHasChanged);
if (result is { Data: bool, Canceled: false } && (bool)result.Data)
{
diff --git a/CaddyManager/Components/Pages/Caddy/CaddyfileEditor/CaddyfileEditor.razor.cs b/CaddyManager/Components/Pages/Caddy/CaddyfileEditor/CaddyfileEditor.razor.cs
index 086b5ad..181cbfb 100644
--- a/CaddyManager/Components/Pages/Caddy/CaddyfileEditor/CaddyfileEditor.razor.cs
+++ b/CaddyManager/Components/Pages/Caddy/CaddyfileEditor/CaddyfileEditor.razor.cs
@@ -95,7 +95,7 @@ public partial class CaddyfileEditor : ComponentBase
else
{
Snackbar.Add(response.Message, Severity.Error);
- MudDialog.Close(DialogResult.Ok(false)); // Indicate failed save
+ // MudDialog.Close(DialogResult.Ok(false)); // Indicate failed save
}
}
@@ -129,7 +129,7 @@ public partial class CaddyfileEditor : ComponentBase
{
Snackbar.Add(submitResponse.Message, Severity.Error);
// Indicate failed save, no restart needed
- MudDialog.Close(DialogResult.Ok(false));
+ // MudDialog.Close(DialogResult.Ok(false));
}
}
@@ -139,9 +139,7 @@ public partial class CaddyfileEditor : ComponentBase
private async Task Duplicate()
{
var content = await _codeEditor.GetValue();
-
- await OnDuplicate.InvokeAsync(content);
-
MudDialog.Close(DialogResult.Ok(false));
+ await OnDuplicate.InvokeAsync(content);
}
}
\ No newline at end of file