Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions src/Aspire.Hosting.Docker/EnvFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,34 @@ 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;
}

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}");
Expand All @@ -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);
Expand Down
157 changes: 157 additions & 0 deletions tests/Aspire.Hosting.Docker.Tests/EnvFileTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading