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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This project follows semantic versioning once stable releases begin.

- Clarified across English and Simplified Chinese documentation that 1.1.1 is the latest published version while M2 APIs and examples reflect unreleased `master` work planned for 1.2.0 (#186).
- Closed the M2 repository and pull review gaps around custom-CA chains, credential scoping, and configured-index request credentials (#184).
- Completed dependency parity follow-ups for list status, descendant globals, vendored charts, alias staging, and exact lock-file builds (#185).
- Normalized packaged root and nested `Chart.yaml` metadata to Helm-compatible LF payloads on every platform while preserving source chart files (#180).
- Hardened repository state with secure atomic writes, Helm-compatible cross-platform cache paths, and collision-safe cache identities (#181).
- Documented complete packaging, repository, pull, and dependency workflows in English and Simplified Chinese, with compile-checked examples and explicit OCI/provenance boundaries (#141).
Expand Down
96 changes: 84 additions & 12 deletions src/HelmSharp.Action/HelmClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,10 @@ public async Task<CommandResult> DependencyUpdateAsync(
var output = new StringBuilder();
var resolvedDependencies = new List<HelmResolvedDependency>(chart.Dependencies.Count);
var stagedArchives = new List<string>(chart.Dependencies.Count);
var localDependencyNames = chart.Dependencies
.Where(dependency => string.IsNullOrWhiteSpace(dependency.Repository))
.Select(dependency => dependency.Name)
.ToHashSet(StringComparer.Ordinal);
var errors = new List<string>();

try
Expand All @@ -1079,14 +1083,25 @@ public async Task<CommandResult> DependencyUpdateAsync(
foreach (var dependency in chart.Dependencies)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(dependency.Repository))
{
errors.Add($"Dependency '{dependency.Name}' does not specify a repository.");
continue;
}

try
{
if (string.IsNullOrWhiteSpace(dependency.Repository))
{
var local = await ResolveVendoredDependencyAsync(
chartPath,
dependency.Name,
dependency.Version,
exactVersion: false,
cancellationToken);
output.AppendLine(
$"Resolved local dependency: {dependency.Name} ({local.Version}) from charts/{dependency.Name}");
resolvedDependencies.Add(new HelmResolvedDependency(
dependency.Name,
dependency.Version ?? local.Version,
string.Empty));
continue;
Comment thread
GaTTGeng marked this conversation as resolved.
}

var staged = await HelmDependencySource.StageAsync(
repo,
configuredRepositories,
Expand All @@ -1099,6 +1114,7 @@ public async Task<CommandResult> DependencyUpdateAsync(
verifyDigest: true,
refreshConfiguredRepository: !request.SkipRepositoryRefresh,
requireConfiguredCache: request.SkipRepositoryRefresh,
exactVersion: false,
cancellationToken);
output.AppendLine(
$"Resolved dependency: {dependency.Name} ({staged.Version}) from {dependency.Repository}");
Expand Down Expand Up @@ -1129,6 +1145,7 @@ public async Task<CommandResult> DependencyUpdateAsync(
await InstallStagedDependencyArchivesAsync(
chartsDir,
stagedArchives,
localDependencyNames,
output,
cancellationToken);

Expand Down Expand Up @@ -1169,6 +1186,7 @@ private static async Task<bool> FilesHaveSameDigestAsync(
private static async Task InstallStagedDependencyArchivesAsync(
string chartsDirectory,
IReadOnlyList<string> stagedArchives,
IReadOnlySet<string> localDependencyNames,
StringBuilder output,
CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -1202,6 +1220,10 @@ private static async Task InstallStagedDependencyArchivesAsync(
{
if (!desiredArchiveNames.Contains(Path.GetFileName(existingArchive)))
{
var existingChart = await HelmChartLoader.LoadAsync(existingArchive, cancellationToken);
if (localDependencyNames.Contains(existingChart.Name))
continue;

File.Delete(existingArchive);
output.AppendLine($"Deleted outdated dependency: {existingArchive}");
}
Expand Down Expand Up @@ -1575,6 +1597,10 @@ public async Task<CommandResult> DependencyBuildAsync(
Directory.CreateDirectory(stagingDirectory);
var output = new StringBuilder();
var stagedArchives = new List<string>(lockFile.Dependencies.Count);
var localDependencyNames = lockFile.Dependencies
.Where(dependency => string.IsNullOrWhiteSpace(dependency.Repository))
.Select(dependency => dependency.Name)
.ToHashSet(StringComparer.Ordinal);
var errors = new List<string>();

try
Expand All @@ -1592,14 +1618,22 @@ public async Task<CommandResult> DependencyBuildAsync(
foreach (var dependency in lockFile.Dependencies)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(dependency.Repository))
{
errors.Add($"Dependency '{dependency.Name}' has no repository in Chart.lock.");
continue;
}

try
{
if (string.IsNullOrWhiteSpace(dependency.Repository))
{
await ResolveVendoredDependencyAsync(
chartPath,
dependency.Name,
dependency.Version,
exactVersion: false,
cancellationToken);
output.AppendLine(
$"Using local locked dependency: {dependency.Name} ({dependency.Version}) " +
$"from charts/{dependency.Name}");
continue;
}

output.AppendLine(
$"Downloading locked dependency: {dependency.Name} ({dependency.Version}) " +
$"from {dependency.Repository}");
Expand All @@ -1615,6 +1649,7 @@ public async Task<CommandResult> DependencyBuildAsync(
request.VerifyDigests,
refreshConfiguredRepository: false,
requireConfiguredCache: true,
exactVersion: true,
cancellationToken);
var archivePath = staged.ArchivePath;
if (!File.Exists(archivePath))
Expand Down Expand Up @@ -1646,6 +1681,7 @@ await VerifyDependencyArchiveDigestAsync(
await InstallStagedDependencyArchivesAsync(
chartsDirectory,
stagedArchives,
localDependencyNames,
output,
cancellationToken);
output.AppendLine("Dependencies rebuilt from Chart.lock.");
Expand Down Expand Up @@ -1687,6 +1723,42 @@ private static async Task VerifyDependencyArchiveDigestAsync(
}
}

private static async Task<HelmChart> ResolveVendoredDependencyAsync(
string parentChartPath,
string dependencyName,
string? requestedVersion,
bool exactVersion,
CancellationToken cancellationToken)
{
var dependencyPath = Path.Combine(parentChartPath, "charts", dependencyName);
Comment thread
GaTTGeng marked this conversation as resolved.
if (!Directory.Exists(dependencyPath))
{
throw new DirectoryNotFoundException(
$"Local dependency directory was not found: {dependencyPath}");
}

var chart = await HelmChartLoader.LoadAsync(dependencyPath, cancellationToken);
if (!string.Equals(chart.Name, dependencyName, StringComparison.Ordinal))
{
throw new InvalidDataException(
$"Local dependency chart '{chart.Name}' does not match dependency '{dependencyName}'.");
}

var versionMatches = exactVersion
? string.Equals(chart.Version, requestedVersion?.Trim(), StringComparison.Ordinal)
: HelmChartVersionResolver.Satisfies(chart.Version, requestedVersion);
if (!versionMatches)
{
var expectation = exactVersion
? $"locked version '{requestedVersion}'"
: $"constraint '{requestedVersion}'";
throw new InvalidDataException(
$"Local dependency '{dependencyName}' version '{chart.Version}' does not match {expectation}.");
}

return chart;
}

public async Task<CommandResult> DependencyListAsync(
string chartPath,
CancellationToken cancellationToken = default)
Expand Down
4 changes: 3 additions & 1 deletion src/HelmSharp.Action/HelmDependencyLockFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,5 +182,7 @@ private static void AddListIfNotEmpty(
};

private static string SerializeScalar(string value)
=> HelmYaml.Serialize(value).TrimEnd('\r', '\n');
=> value.Length == 0
? "\"\""
: HelmYaml.Serialize(value).TrimEnd('\r', '\n');
}
19 changes: 14 additions & 5 deletions src/HelmSharp.Action/HelmDependencySource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static async Task<HelmStagedDependency> StageAsync(
bool verifyDigest,
bool refreshConfiguredRepository,
bool requireConfiguredCache,
bool exactVersion,
CancellationToken cancellationToken)
{
if (repositoryReference.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
Expand All @@ -29,6 +30,7 @@ public static async Task<HelmStagedDependency> StageAsync(
versionConstraint,
repositoryReference,
destination,
exactVersion,
cancellationToken);
}

Expand All @@ -52,7 +54,8 @@ public static async Task<HelmStagedDependency> StageAsync(
ChartReference = $"{configured.Name}/{dependencyName}",
Version = versionConstraint,
Destination = destination,
VerifyDigest = verifyDigest
VerifyDigest = verifyDigest,
ExactVersion = exactVersion
};
}
else
Expand All @@ -72,7 +75,8 @@ public static async Task<HelmStagedDependency> StageAsync(
RepositoryUrl = repositoryReference,
Version = versionConstraint,
Destination = destination,
VerifyDigest = verifyDigest
VerifyDigest = verifyDigest,
ExactVersion = exactVersion
};
}

Expand All @@ -93,6 +97,7 @@ private static async Task<HelmStagedDependency> StageFileDependencyAsync(
string? versionConstraint,
string repositoryReference,
string destination,
bool exactVersion,
CancellationToken cancellationToken)
{
var fileReference = Uri.UnescapeDataString(repositoryReference["file://".Length..]);
Expand All @@ -110,11 +115,15 @@ private static async Task<HelmStagedDependency> StageFileDependencyAsync(
throw new InvalidDataException(
$"File dependency chart '{chart.Name}' does not match dependency '{dependencyName}'.");
}
if (!HelmChartVersionResolver.Satisfies(chart.Version, versionConstraint))
if (exactVersion
? !string.Equals(chart.Version, versionConstraint?.Trim(), StringComparison.Ordinal)
: !HelmChartVersionResolver.Satisfies(chart.Version, versionConstraint))
{
var expectation = exactVersion
? $"locked version '{versionConstraint}'"
: $"constraint '{versionConstraint}'";
throw new InvalidDataException(
$"File dependency '{dependencyName}' version '{chart.Version}' does not satisfy " +
$"constraint '{versionConstraint}'.");
$"File dependency '{dependencyName}' version '{chart.Version}' does not match {expectation}.");
}

var archivePath = await HelmChartPackager.PackageAsync(
Expand Down
62 changes: 1 addition & 61 deletions src/HelmSharp.Action/HelmDependencyStatusInspector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ public static async Task<string> InspectAsync(
HelmChartDependency dependency,
CancellationToken cancellationToken)
{
if (IsDisabled(parent, dependency))
return "disabled";

var expectedVersion = GetExpectedVersion(parent, dependency);
var expectedVersion = dependency.Version;
if (Directory.Exists(chartPath))
{
var chartsDirectory = Path.Combine(chartPath, "charts");
Expand Down Expand Up @@ -190,61 +187,4 @@ private static string InspectChart(
: "wrong version";
}

private static string? GetExpectedVersion(HelmChart parent, HelmChartDependency dependency)
{
var lockEntry = parent.LockEntries.FirstOrDefault(entry =>
string.Equals(entry.Name, dependency.Name, StringComparison.OrdinalIgnoreCase) &&
(string.IsNullOrWhiteSpace(entry.Repository) ||
string.IsNullOrWhiteSpace(dependency.Repository) ||
string.Equals(entry.Repository, dependency.Repository, StringComparison.Ordinal)));
return string.IsNullOrWhiteSpace(lockEntry?.Version)
? dependency.Version
: lockEntry.Version;
}

private static bool IsDisabled(HelmChart parent, HelmChartDependency dependency)
{
if (!dependency.Enabled)
return true;

if (string.IsNullOrWhiteSpace(dependency.Condition))
return false;

var values = HelmYaml.DeserializeDictionary(parent.ValuesYaml);
foreach (var condition in dependency.Condition.Split(
',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (TryGetBoolean(values, condition, out var enabled))
return !enabled;
}

return false;
}

private static bool TryGetBoolean(
IReadOnlyDictionary<string, object?> values,
string path,
out bool value)
{
object? current = values;
foreach (var segment in path.Split('.', StringSplitOptions.RemoveEmptyEntries))
{
if (current is not IReadOnlyDictionary<string, object?> map ||
!map.TryGetValue(segment, out current))
{
value = default;
return false;
}
}

if (current is bool boolean)
{
value = boolean;
return true;
}

value = default;
return false;
}
}
7 changes: 4 additions & 3 deletions src/HelmSharp.Chart/HelmValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@ public static class HelmValues

foreach (var child in node.Children)
{
var childOverrides = GetMap(result, child.Identity)
?? new Dictionary<string, object?>(StringComparer.Ordinal);
var childOverrides = GetMap(result, child.Identity) is { } configured
? CloneDictionary(configured)
: new Dictionary<string, object?>(StringComparer.Ordinal);
MergeGlobalValues(childOverrides, GetMap(result, "global"));
var childValues = BuildChartValues(child.Chart, childOverrides, child);
MergeGlobalValues(childValues, GetMap(result, "global"));
result[child.Identity] = childValues;
}

Expand Down
Loading