diff --git a/src/Aspire.Hosting.Docker/EnvFile.cs b/src/Aspire.Hosting.Docker/EnvFile.cs index a95d668bf8e..5ac74226213 100644 --- a/src/Aspire.Hosting.Docker/EnvFile.cs +++ b/src/Aspire.Hosting.Docker/EnvFile.cs @@ -19,15 +19,9 @@ public static EnvFile Load(string path) foreach (var line in File.ReadAllLines(path)) { envFile._lines.Add(line); - var trimmed = line.TrimStart(); - if (!trimmed.StartsWith('#') && trimmed.Contains('=')) + if (TryParseKey(line, out var key)) { - var eqIndex = trimmed.IndexOf('='); - if (eqIndex > 0) - { - var key = trimmed[..eqIndex].Trim(); - envFile._keys.Add(key); - } + envFile._keys.Add(key); } } return envFile; @@ -35,10 +29,24 @@ public static EnvFile Load(string path) public void Add(string key, string? value, string? comment, bool onlyIfMissing = true) { - if (onlyIfMissing && _keys.Contains(key)) + if (_keys.Contains(key)) { - return; + if (onlyIfMissing) + { + return; + } + + // Update the existing key's value. + for (int i = 0; i < _lines.Count; i++) + { + if (TryParseKey(_lines[i], out var lineKey) && lineKey == key) + { + _lines[i] = value is not null ? $"{key}={value}" : $"{key}="; + return; + } + } } + if (!string.IsNullOrWhiteSpace(comment)) { _lines.Add($"# {comment}"); @@ -48,6 +56,22 @@ public void Add(string key, string? value, string? comment, bool onlyIfMissing = _keys.Add(key); } + private static bool TryParseKey(string line, out string key) + { + key = string.Empty; + var trimmed = line.TrimStart(); + if (!trimmed.StartsWith('#') && trimmed.Contains('=')) + { + var eqIndex = trimmed.IndexOf('='); + if (eqIndex > 0) + { + key = trimmed[..eqIndex].Trim(); + return true; + } + } + return false; + } + public void Save(string path) { File.WriteAllLines(path, _lines); diff --git a/tests/Aspire.Hosting.Docker.Tests/EnvFileTests.cs b/tests/Aspire.Hosting.Docker.Tests/EnvFileTests.cs new file mode 100644 index 00000000000..58d23741b61 --- /dev/null +++ b/tests/Aspire.Hosting.Docker.Tests/EnvFileTests.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.Docker.Tests; + +public class EnvFileTests +{ + [Fact] + public void Add_WithOnlyIfMissingTrue_DoesNotAddDuplicate() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Create initial .env file + File.WriteAllLines(envFilePath, [ + "# Comment for KEY1", + "KEY1=value1", + "" + ]); + + // Load and try to add the same key with onlyIfMissing=true + var envFile = EnvFile.Load(envFilePath); + envFile.Add("KEY1", "value2", "New comment", onlyIfMissing: true); + envFile.Save(envFilePath); + + var lines = File.ReadAllLines(envFilePath); + var keyLines = lines.Where(l => l.StartsWith("KEY1=")).ToArray(); + + // Should still have only one KEY1 line with original value + Assert.Single(keyLines); + Assert.Equal("KEY1=value1", keyLines[0]); + } + + [Fact] + public void Add_WithOnlyIfMissingFalse_UpdatesExistingKey() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Create initial .env file + File.WriteAllLines(envFilePath, [ + "# Comment for KEY1", + "KEY1=value1", + "" + ]); + + // Load and try to add the same key with onlyIfMissing=false + var envFile = EnvFile.Load(envFilePath); + envFile.Add("KEY1", "value2", "New comment", onlyIfMissing: false); + envFile.Save(envFilePath); + + var lines = File.ReadAllLines(envFilePath); + var keyLines = lines.Where(l => l.StartsWith("KEY1=")).ToArray(); + + // Should still have only one KEY1 line, but with updated value + Assert.Single(keyLines); + Assert.Equal("KEY1=value2", keyLines[0]); + } + + [Fact] + public void Add_WithOnlyIfMissingFalse_UpdatesImageNameWithoutDuplication() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Create initial .env file simulating a project resource + File.WriteAllLines(envFilePath, [ + "# Default container port for project1", + "PROJECT1_PORT=8080", + "", + "# Container image name for project1", + "PROJECT1_IMAGE=project1:latest", + "" + ]); + + // Load the file + var envFile = EnvFile.Load(envFilePath); + + // Add PORT with onlyIfMissing=true (should be skipped since it exists) + envFile.Add("PROJECT1_PORT", "8080", "Default container port for project1", onlyIfMissing: true); + + // Add IMAGE with onlyIfMissing=false (should update the existing value) + envFile.Add("PROJECT1_IMAGE", "project1:1.0.0", "Container image name for project1", onlyIfMissing: false); + + envFile.Save(envFilePath); + + var lines = File.ReadAllLines(envFilePath); + var imageLines = lines.Where(l => l.StartsWith("PROJECT1_IMAGE=")).ToArray(); + + // Should have exactly one IMAGE line with the new value + Assert.Single(imageLines); + Assert.Equal("PROJECT1_IMAGE=project1:1.0.0", imageLines[0]); + + // PORT should also still be present once + var portLines = lines.Where(l => l.StartsWith("PROJECT1_PORT=")).ToArray(); + Assert.Single(portLines); + Assert.Equal("PROJECT1_PORT=8080", portLines[0]); + } + + [Fact] + public void Add_NewKey_AddsToFile() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Create initial .env file + File.WriteAllLines(envFilePath, [ + "# Comment for KEY1", + "KEY1=value1", + "" + ]); + + // Load and add a new key + var envFile = EnvFile.Load(envFilePath); + envFile.Add("KEY2", "value2", "Comment for KEY2", onlyIfMissing: true); + envFile.Save(envFilePath); + + var lines = File.ReadAllLines(envFilePath); + + // Should have both keys + Assert.Contains("KEY1=value1", lines); + Assert.Contains("KEY2=value2", lines); + } + + [Fact] + public void Load_EmptyFile_ReturnsEmptyEnvFile() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Create empty file + File.WriteAllText(envFilePath, string.Empty); + + var envFile = EnvFile.Load(envFilePath); + envFile.Add("KEY1", "value1", "Comment"); + envFile.Save(envFilePath); + + var lines = File.ReadAllLines(envFilePath); + Assert.Contains("KEY1=value1", lines); + } + + [Fact] + public void Load_NonExistentFile_ReturnsEmptyEnvFile() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Don't create the file + var envFile = EnvFile.Load(envFilePath); + envFile.Add("KEY1", "value1", "Comment"); + envFile.Save(envFilePath); + + Assert.True(File.Exists(envFilePath)); + var lines = File.ReadAllLines(envFilePath); + Assert.Contains("KEY1=value1", lines); + } +}