Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0269ee0
its looking pretty good
rain-on Mar 11, 2026
765eb80
sort of?
rain-on Mar 11, 2026
86fb93e
DirectoryUpdater looks about right
rain-on Mar 11, 2026
c70942b
added empty updaters
rain-on Mar 11, 2026
35e6d26
half way with ref updating
rain-on Mar 11, 2026
b07dfd9
Kustomize is in - now for ref
rain-on Mar 11, 2026
1f67816
created updaterHelpers
rain-on Mar 11, 2026
51df92d
ref is compiling
rain-on Mar 11, 2026
8e74fa2
created an intermediate helmupdater its nasty
rain-on Mar 11, 2026
b0ef6a3
compiling - wonder what happens with the main convention
rain-on Mar 11, 2026
fbe1a5d
moved everything
rain-on Mar 11, 2026
d07ff17
added a repositoryAdapter
rain-on Mar 11, 2026
fe5dc4a
split out the repo and outputvariables stuff
rain-on Mar 12, 2026
bdaaff8
sort out compilation
rain-on Mar 12, 2026
ca8752f
fix the null whoopsie
rain-on Mar 12, 2026
690e254
yay
rain-on Mar 12, 2026
e10f532
clean up
rain-on Mar 12, 2026
4ed8def
catch the lost convention
rain-on Mar 12, 2026
023f9a8
slow tidy up after review
rain-on Mar 12, 2026
2aa02c3
tidy it up even more
rain-on Mar 12, 2026
535baed
and more
rain-on Mar 12, 2026
54558bd
cleaning
rain-on Mar 12, 2026
233d297
removed updatdfiles and rely on patches
rain-on Mar 12, 2026
766ab76
make file list distinct
rain-on Mar 12, 2026
f449e19
added authenticatingrepositoryfactor
rain-on Mar 12, 2026
20cd4c8
moved things around a bit
rain-on Mar 12, 2026
ae52c8e
clean up imports
rain-on Mar 12, 2026
96bdae1
move files into a subdirectory under Conventions
rain-on Mar 12, 2026
e75926c
fix namespacing
rain-on Mar 12, 2026
a55c7f9
its mostly good
rain-on Mar 12, 2026
79a7a6d
Refactored the templating step - yay?
rain-on Mar 12, 2026
f2b4d65
Merge remote-tracking branch 'origin/main' into tmm/refactor_other_step
rain-on Mar 13, 2026
638be1f
fix up the whoopsie
rain-on Mar 13, 2026
7e44ec2
yay
rain-on Mar 13, 2026
52065d9
trying to make this nicer
rain-on Mar 13, 2026
aa2e5c2
passing
rain-on Mar 13, 2026
4ffe8c5
made the new steop way better
rain-on Mar 13, 2026
3c10736
And the Templating is way nicer
rain-on Mar 13, 2026
dbf87e8
add comment
rain-on Mar 13, 2026
5046d60
looks ok
rain-on Mar 16, 2026
1966820
minor tidy up
rain-on Mar 16, 2026
bd7df91
fix compilation issues
rain-on Mar 16, 2026
f55822d
sorted
rain-on Mar 16, 2026
7a817e3
back it out
rain-on Mar 16, 2026
8cfe54f
remove the slash which wasn't going to work
rain-on Mar 16, 2026
d082bfe
I hate windows
rain-on Mar 16, 2026
8103b86
fix compilation
rain-on Mar 16, 2026
d5a0e37
tidy up
rain-on Mar 16, 2026
799533e
fixed the pathing issues, wild.
rain-on Mar 16, 2026
fb68452
create a test for pathing
rain-on Mar 17, 2026
53b5c3d
make everything posix-ee
rain-on Mar 17, 2026
15f4ecf
normalize to posix deliberately
rain-on Mar 17, 2026
57322e9
force removed files
rain-on Mar 17, 2026
c05bc45
fix up the relative path error
rain-on Mar 17, 2026
c6b9a72
revert the normalizePath in repositorywrapper
rain-on Mar 18, 2026
92a193b
always force repositoryWrapper to use forward slash
rain-on Mar 18, 2026
526c6e2
fix normalize yet again
rain-on Mar 19, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ public void EnsureOutputDirectoryIsPurgedWhenVariableIsSetRecursiveDeletion()
};

//add arbitrary file to the origin repo
var fileToPurge = "subDirectory/removeThis.yaml";
//Note: the filename should use "/" as it goes into the repos Treedefinition, rather than the filesystem
var fileToPurge = "subDirectory1/subdirectory2/removeThis.yaml";
originRepo.AddFilesToBranch(argoCDBranchName, (fileToPurge, "This file to be removed"));

var allVariables = new CalamariVariables();
Expand All @@ -198,6 +199,7 @@ public void EnsureOutputDirectoryIsPurgedWhenVariableIsSetRecursiveDeletion()
var resultPath = RepositoryHelpers.CloneOrigin(tempDirectory, OriginPath, argoCDBranchName);
File.Exists(Path.Combine(resultPath, firstFilename)).Should().BeTrue();
File.Exists(Path.Combine(resultPath, fileToPurge)).Should().BeFalse();
Directory.Exists(Path.Combine(resultPath, "subDirectory1")).Should().BeFalse();

AssertOutputVariables();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.IO;
using System.Text;
using Calamari.ArgoCD.Git;
using LibGit2Sharp;
Expand All @@ -14,7 +15,8 @@ public static class RepositoryExtensionMethods
{
public static string ReadFileFromBranch(this Repository repo, GitBranchName branchName, string filename)
{
var fileTreeEntry = repo.Branches[branchName.Value].Tip[filename];
//filename will always be posix-compliant
var fileTreeEntry = repo.Branches[branchName.Value].Tip[filename.Replace(Path.DirectorySeparatorChar, '/')];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if this filename also needs to be stripped of the leading "./"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, the way we're using it in the tests - the answer is "no" - but that's luck.
The reason we have a problem in production code is when the argo app's path is "./" :(


var fileBlob = (Blob)fileTreeEntry.Target;
return fileBlob.GetContentText();
Expand Down
73 changes: 32 additions & 41 deletions source/Calamari.Tests/ArgoCD/Git/RepositoryWrapperTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void Cleanup()
[Test]
public void StagingANonExistentFileThrowsException()
{
Action act = () => repository.StageFiles(new[] { "nonexistent.txt" });
Action act = () => repository.AddFiles(new[] { "nonexistent.txt" });
act.Should().Throw<LibGit2SharpException>().And.Message.Should().Contain("could not find ");
}

Expand All @@ -80,12 +80,12 @@ public void EmptyCommitReturnsFalse()
}

[Test]
public void AttemptingToAddFilestartingWithDotSlashSucceeds()
public void AttemptingToAddFileStartingWithDotSlashSucceeds()
{
//This is to highlight a behaviour of libGit2Sharp which we may run into
string filename = "newFile.txt";
File.WriteAllText(Path.Combine(RepositoryRootPath, filename), "");
repository.StageFiles(new[] { $"./{filename}" });
repository.AddFiles(new[] { $"./{filename}" });
}

[Test]
Expand All @@ -94,7 +94,7 @@ public async Task StagingARealFileSucceedsAndCanBeCommittedAndPushed()
string filename = "newFile.txt";
string fileContents = "Lorem ipsum dolor sit amet";
File.WriteAllText(Path.Combine(RepositoryRootPath, filename), fileContents);
repository.StageFiles(new[] { filename });
repository.AddFiles(new[] { filename });
repository.CommitChanges("Summary Message", "A file has changed").Should().BeTrue();
await repository.PushChanges(false,
"Summary Message",
Expand All @@ -112,7 +112,7 @@ public async Task CanPushTheHeadToAnyBranchNameOnRemote()
{
string filename = "newFile.txt";
File.WriteAllText(Path.Combine(RepositoryRootPath, filename), "");
repository.StageFiles(new[] { filename });
repository.AddFiles(new[] { filename });
repository.CommitChanges("Summary Message", "There is no data to comm it").Should().BeTrue();
await repository.PushChanges(false,
"Summary Message",
Expand All @@ -131,7 +131,7 @@ public async Task WhenCreatingAPrThePrTitleAndBodyMatchTheCommitMessageFields()
{
string filename = "newFile.txt";
await File.WriteAllTextAsync(Path.Combine(RepositoryRootPath, filename), "");
repository.StageFiles(new[] { filename });
repository.AddFiles(new[] { filename });
var commitSummary = "Summary Message";
var commitDescription = "A commit description";
repository.CommitChanges(commitSummary, commitDescription).Should().BeTrue();
Expand All @@ -158,7 +158,7 @@ public async Task WhenDisposingOfARepository_TheCheckoutDirectoryIsRemoved()
const string fileContents = "Lorem ipsum dolor sit amet";
await File.WriteAllTextAsync(Path.Combine(RepositoryRootPath, filename), fileContents);

repository.StageFiles(new[] { filename });
repository.AddFiles(new[] { filename });
repository.CommitChanges("Summary Message", "A file has changed").Should().BeTrue();

// Act
Expand All @@ -170,38 +170,6 @@ public async Task WhenDisposingOfARepository_TheCheckoutDirectoryIsRemoved()
.BeFalse();
}

[Test]
[TestCase("", 0)]
[TestCase("./", 0)]
[TestCase("nested_1", 2)]
[TestCase("nested_1/nested_2", 3)]
[TestCase("nested", 3)]
[TestCase("nest", 4)]
public void RemoveFiles(string subPath, int totalFilesRemaining)
{
//Arrange
bareOrigin.AddFilesToBranch(branchName,
("file.yaml", ""),
("nested/file.txt", ""),
("nested_1/file.yaml", ""),
("nested_1/nested_2/file.yaml", ""));

var repositoryFactory = new RepositoryFactory(log, fileSystem, tempDirectory, gitVendorAgnosticApiAdapterFactory, new SystemClock());
gitConnection = new GitConnection(null, null, new Uri(OriginPath), branchName);

// Act
var sut = repositoryFactory.CloneRepository($"{repositoryPath}/sut", gitConnection);
sut.RecursivelyStageFilesForRemoval(subPath);
sut.CommitChanges("Deleted files", string.Empty);
sut.PushChanges(branchName);

//Assert
var result = CloneOrigin();
var files = fileSystem.EnumerateFilesWithGlob(result, "**/*");
var notGitFiles = files.Where(file => !file.Contains(".git")).ToList();
notGitFiles.Count.Should().Be(totalFilesRemaining);
}

[Test]
public void CloningAReferenceOtherThanABranchFails()
{
Expand All @@ -225,7 +193,7 @@ public async Task WhenRemoteHasNewCommitBeforePush_RetrySucceedsAfterFetchAndReb
const string filename = "ourFile.txt";
const string fileContents = "our content";
await File.WriteAllTextAsync(Path.Combine(RepositoryRootPath, filename), fileContents);
repository.StageFiles([filename]);
repository.AddFiles([filename]);
repository.CommitChanges("Our commit", "").Should().BeTrue();

// Simulate a concurrent push to origin on a different file (causes non-fast-forward failure)
Expand All @@ -249,7 +217,7 @@ public async Task WhenRebaseConflictDuringRetry_ThrowsCommandException()
// Arrange: commit a change to a file in our clone
const string conflictFile = "conflict.txt";
File.WriteAllText(Path.Combine(RepositoryRootPath, conflictFile), "our content");
repository.StageFiles(new[] { conflictFile });
repository.AddFiles(new[] { conflictFile });
repository.CommitChanges("Our commit", "").Should().BeTrue();

// Simulate a concurrent conflicting change to the same file in origin
Expand All @@ -262,6 +230,29 @@ await act.Should()
.WithMessage("*Rebase conflict*");
}

[Test]
public async Task CanPushWithPathSeparators()
{
var subDirName = "subDir";
string filename = Path.Combine(subDirName, "newFile.txt");
string fileContents = "Lorem ipsum dolor sit amet";
var subDirPath = Path.Combine(RepositoryRootPath, subDirName);
Directory.CreateDirectory(subDirPath);
File.WriteAllText(Path.Combine(RepositoryRootPath, filename), fileContents);

repository.AddFiles(new[] { filename });
repository.CommitChanges("Summary Message", "A file has changed").Should().BeTrue();
await repository.PushChanges(false,
"Summary Message",
"A file has changed",
branchName,
CancellationToken.None);

//ensure the remote contains the file
var originFileContent = bareOrigin.ReadFileFromBranch(branchName, filename);
originFileContent.Should().Be(fileContents);
}

string CloneOrigin()
{
var subPath = Guid.NewGuid().ToString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Calamari.ArgoCD.Domain;
using Calamari.ArgoCD.Dtos;
using Calamari.ArgoCD.Git;
using Calamari.ArgoCD.Models;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;

namespace Calamari.ArgoCD.Conventions.ManifestTemplating;

public class ApplicationSourceUpdater
{
readonly Application applicationFromYaml;
readonly DeploymentScope deploymentScope;
readonly RepositoryAdapter repositoryAdapter;
readonly ArgoCommitToGitConfig deploymentConfig;
readonly IPackageRelativeFile[] packageFiles;
readonly ArgoCDGatewayDto gateway;
readonly ILog log;
readonly ICalamariFileSystem fileSystem;
readonly ArgoCDOutputVariablesWriter outputVariablesWriter;

public ApplicationSourceUpdater(Application applicationFromYaml,
ArgoCDGatewayDto gateway,
DeploymentScope deploymentScope,
ArgoCommitToGitConfig deploymentConfig,
IPackageRelativeFile[] packageFiles,
ILog log,
ICalamariFileSystem fileSystem,
ArgoCDOutputVariablesWriter outputVariablesWriter,
RepositoryAdapter repositoryAdapter)
{
this.applicationFromYaml = applicationFromYaml;
this.deploymentScope = deploymentScope;
this.deploymentConfig = deploymentConfig;
this.packageFiles = packageFiles;
this.gateway = gateway;
this.log = log;
this.fileSystem = fileSystem;
this.outputVariablesWriter = outputVariablesWriter;
this.repositoryAdapter = repositoryAdapter;
}

public ManifestUpdateResult ProcessSource(ApplicationSourceWithMetadata sourceWithMetadata)
{
var applicationSource = sourceWithMetadata.Source;
var annotatedScope = ScopingAnnotationReader.GetScopeForApplicationSource(applicationSource.Name.ToApplicationSourceName(), applicationFromYaml.Metadata.Annotations, applicationFromYaml.Spec.Sources.Count > 1);

log.LogApplicationSourceScopeStatus(annotatedScope, applicationSource.Name.ToApplicationSourceName(), deploymentScope);

if (!deploymentScope.Matches(annotatedScope))
return new ManifestUpdateResult(false, string.Empty, []);

log.Info($"Writing files to repository '{applicationSource.OriginalRepoUrl}' for '{applicationFromYaml.Metadata.Name}'");

var sourceUpdater = new CopyTemplatesSourceUpdater(packageFiles, log, fileSystem, deploymentConfig.PurgeOutputDirectory);

var sourceUpdateResult = repositoryAdapter.Process(sourceWithMetadata, sourceUpdater);

if (sourceUpdateResult.PushResult is not null)
{
outputVariablesWriter.WritePushResultOutput(gateway.Name,
applicationFromYaml.Metadata.Name,
sourceWithMetadata.Index,
sourceUpdateResult.PushResult);

return new ManifestUpdateResult(true, sourceUpdateResult.PushResult.CommitSha, sourceUpdateResult.PatchedFiles);
}

log.Info("No changes were commited");
return new ManifestUpdateResult(false, string.Empty, []);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.Collections.Generic;
using System.Linq;
using Calamari.ArgoCD.Domain;
using Calamari.ArgoCD.Dtos;
using Calamari.ArgoCD.Git;
using Calamari.ArgoCD.Models;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;

namespace Calamari.ArgoCD.Conventions.ManifestTemplating;

public class ApplicationUpdater
{
readonly AuthenticatingRepositoryFactory repositoryFactory;
readonly DeploymentScope deploymentScope;
readonly ArgoCommitToGitConfig deploymentConfig;
readonly ILog log;
readonly ICalamariFileSystem fileSystem;
readonly IArgoCDApplicationManifestParser argoCdApplicationManifestParser;
readonly ArgoCDOutputVariablesWriter outputVariablesWriter;
readonly IPackageRelativeFile[] packageFiles;


public ApplicationUpdater(AuthenticatingRepositoryFactory repositoryFactory, DeploymentScope deploymentScope, ArgoCommitToGitConfig deploymentConfig, ILog log,
ICalamariFileSystem fileSystem,
IArgoCDApplicationManifestParser argoCdApplicationManifestParser,
ArgoCDOutputVariablesWriter outputVariablesWriter,
IPackageRelativeFile[] packageFiles)
{
this.repositoryFactory = repositoryFactory;
this.deploymentScope = deploymentScope;
this.deploymentConfig = deploymentConfig;
this.log = log;
this.fileSystem = fileSystem;
this.argoCdApplicationManifestParser = argoCdApplicationManifestParser;
this.outputVariablesWriter = outputVariablesWriter;
this.packageFiles = packageFiles;
}

public ProcessApplicationResult ProcessApplication(
ArgoCDApplicationDto application,
ArgoCDGatewayDto gateway)
{
log.InfoFormat("Processing application {0}", application.Name);
var applicationFromYaml = argoCdApplicationManifestParser.ParseManifest(application.Manifest);
var containsMultipleSources = applicationFromYaml.Spec.Sources.Count > 1;
var applicationName = applicationFromYaml.Metadata.Name;

LogWarningIfUpdatingMultipleSources(applicationFromYaml.Spec.Sources,
applicationFromYaml.Metadata.Annotations,
containsMultipleSources);

ValidateApplication(applicationFromYaml);

var repositoryAdapter = new RepositoryAdapter(repositoryFactory, deploymentConfig.CommitParameters, log, new CommitMessageGenerator());
var sourceUpdater = new ApplicationSourceUpdater(applicationFromYaml,
gateway,
deploymentScope,
deploymentConfig,
packageFiles,
log,
fileSystem,
outputVariablesWriter,
repositoryAdapter);

var updatedSourcesResults = applicationFromYaml
.GetSourcesWithMetadata()
.Select(applicationSource => new
{
UpdateResult = sourceUpdater.ProcessSource(applicationSource),
applicationSource
})
.Where(u => u.UpdateResult.Updated)
.ToList();

//if we have links, use that to generate a link, otherwise just put the name there
var instanceLinks = application.InstanceWebUiUrl != null ? new ArgoCDInstanceLinks(application.InstanceWebUiUrl) : null;
var linkifiedAppName = instanceLinks != null
? log.FormatLink(instanceLinks.ApplicationDetails(applicationName, applicationFromYaml.Metadata.Namespace), applicationName)
: applicationName;

var message = updatedSourcesResults.Any()
? "Updated Application {0}"
: "Nothing to update for Application {0}";

log.InfoFormat(message, linkifiedAppName);

return new ProcessApplicationResult(
application.GatewayId,
applicationName.ToApplicationName(),
applicationFromYaml.Spec.Sources.Count,
applicationFromYaml.Spec.Sources.Count(s => deploymentScope.Matches(ScopingAnnotationReader.GetScopeForApplicationSource(s.Name.ToApplicationSourceName(), applicationFromYaml.Metadata.Annotations, containsMultipleSources))),
updatedSourcesResults.Select(r => new UpdatedSourceDetail(r.UpdateResult.CommitSha, r.applicationSource.Index, r.UpdateResult.ReplacedFiles, [])).ToList(),
[],
updatedSourcesResults.Select(r => r.applicationSource.Source.OriginalRepoUrl).ToHashSet());
}

void LogWarningIfUpdatingMultipleSources(
List<ApplicationSource> sourcesToInspect,
Dictionary<string, string> applicationAnnotations,
bool containsMultipleSources)
{
if (sourcesToInspect.Count > 1)
{
var sourcesWithScopes = sourcesToInspect.Select(s => (s, ScopingAnnotationReader.GetScopeForApplicationSource(s.Name.ToApplicationSourceName(), applicationAnnotations, containsMultipleSources))).ToList();
var sourcesWithMatchingScopes = sourcesWithScopes.Where(s => deploymentScope.Matches(s.Item2)).ToList();

if (sourcesWithMatchingScopes.Count > 1)
{
log.Warn($"Multiple sources are associated with this deployment, they will all be updated with the same contents: {string.Join(", ", sourcesWithMatchingScopes.Select(s => s.s.Name))}");
}
}
}

void ValidateApplication(Application applicationFromYaml)
{
var validationResult = ValidationResult.Merge(
ApplicationValidator.ValidateSourceNames(applicationFromYaml),
ApplicationValidator.ValidateUnnamedAnnotationsInMultiSourceApplication(applicationFromYaml)
);
validationResult.Action(log);
}
}
Loading