Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
68 changes: 68 additions & 0 deletions source/Calamari.Tests/ArgoCD/ContainerImageReplacerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,74 @@ public void UpdateImages_WithQuotedReference_PreservesQuotes()
result.UpdatedImageReferences.Should().ContainSingle(r => r == "nginx:1.25");
}

[Test]
public void UpdateImages_WithImageOnNonDefaultRegistry_UpdatesTagAndReportsImage()
{
// Images on a non-default registry (e.g. GAR/GCR/ECR) must be matched and updated. The
// first path segment (us-docker.pkg.dev) is recognised as a registry, and the rest is the
// image name; only the tag should change.
const string inputYaml = @"apiVersion: v1
kind: Pod
spec:
containers:
- name: helloworld
image: us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v1";
const string expectedYaml = @"apiVersion: v1
kind: Pod
spec:
containers:
- name: helloworld
image: us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2";

var imageReplacer = new ContainerImageReplacer(inputYaml, DefaultContainerRegistry);

var updatedImage = new List<ContainerImageReferenceAndHelmReference>
{
new(ContainerImageReference.FromReferenceString(
"us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2",
DefaultContainerRegistry))
};

var result = imageReplacer.UpdateImages(updatedImage);

result.UpdatedContents.Should().Be(expectedYaml);
result.UpdatedImageReferences.Should().ContainSingle()
.Which.Should().Be("us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2");
}

[Theory]
[TestCase("docker.io/nginx:1.27.1", "docker.io/nginx:1.28.0")]
[TestCase("nginx:1.27.1", "nginx:1.28.0")]
[TestCase("us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v1",
"us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2")]
public void ReturnsSameImageBaseAsInYaml(string originalImage, string expectedImage)
{
var inputYaml = $@"apiVersion: v1
kind: Pod
spec:
containers:
- name: my-container
image: {originalImage}";
var expectedYaml = $@"apiVersion: v1
kind: Pod
spec:
containers:
- name: my-container
image: {expectedImage}";

var imageReplacer = new ContainerImageReplacer(inputYaml, DefaultContainerRegistry);

var update = new List<ContainerImageReferenceAndHelmReference>
{
new(ContainerImageReference.FromReferenceString(expectedImage, DefaultContainerRegistry))
};

var result = imageReplacer.UpdateImages(update);

result.UpdatedContents.Should().Be(expectedYaml);
result.UpdatedImageReferences.Should().ContainSingle().Which.Should().Be(expectedImage);
}

[Test]
public void DoesNotUpdateComments()
{
Expand Down
87 changes: 87 additions & 0 deletions source/Calamari.Tests/ArgoCD/DirectoryUpdaterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.IO;
using Calamari.ArgoCD;
using Calamari.ArgoCD.Conventions;
using Calamari.ArgoCD.Conventions.UpdateImageTag;
using Calamari.ArgoCD.Domain;
using Calamari.ArgoCD.Models;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;
using Calamari.Testing.Helpers;
using Calamari.Tests.Fixtures.Integration.FileSystem;
using FluentAssertions;
using NUnit.Framework;

namespace Calamari.Tests.ArgoCD
{
/// <summary>
/// Integration tests for DirectoryUpdater.Process() workflow.
/// Mirrors KustomizeUpdaterTests, focusing on non-default container registries.
/// </summary>
[TestFixture]
public class DirectoryUpdaterTests
{
ILog log;
ICalamariFileSystem fileSystem;
string tempDir;

[SetUp]
public void SetUp()
{
log = new InMemoryLog();
fileSystem = TestCalamariPhysicalFileSystem.GetPhysicalFileSystem();
tempDir = fileSystem.CreateTemporaryDirectory();
}

[TearDown]
public void TearDown()
{
fileSystem?.DeleteDirectory(tempDir);
}

[Test]
public void Process_WithImageOnNonDefaultRegistry_ProducesPatchSoChangesAreCommitted()
{
// Regression: an image on a non-default registry (e.g. GAR/GCR/ECR) must update the
// manifest AND produce a JSON patch, so HasChanges() is true and the commit/push isn't
// silently skipped.
const string deployment = @"apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld
spec:
template:
spec:
containers:
- name: helloworld
image: us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v1
";
fileSystem.OverwriteFile(Path.Combine(tempDir, "deployment.yaml"), deployment);

var garImages = new List<ContainerImageReferenceAndHelmReference>
{
new(ContainerImageReference.FromReferenceString(
"us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2",
ArgoCDConstants.DefaultContainerRegistry)),
};

var sourceWithMetadata = new ApplicationSourceWithMetadata(
new ApplicationSource { Path = "." },
SourceType.Directory,
0);

var updater = new DirectoryUpdater(garImages, ArgoCDConstants.DefaultContainerRegistry, log, fileSystem);

var result = updater.Process(sourceWithMetadata, tempDir);

result.UpdatedImages.Should().NotBeEmpty();
result.HasChanges().Should().BeTrue("a JSON patch must be produced so the commit/push is not skipped");
result.PatchedFiles.Should().NotBeEmpty();

var updatedContent = fileSystem.ReadFile(Path.Combine(tempDir, "deployment.yaml"));
updatedContent.Should().Contain("us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2");
updatedContent.Should().NotContain("helloworld:v1");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using Calamari.ArgoCD.Conventions;
using Calamari.ArgoCD.Helm;
using Calamari.ArgoCD.Models;
using Calamari.Testing.Helpers;
using FluentAssertions;
using NUnit.Framework;

namespace Calamari.Tests.ArgoCD.Helm;

[TestFixture]
public class HelmContainerImageReplacerTests
{
const string DefaultRegistry = "docker.io";
readonly InMemoryLog log = new();

[Theory]
[TestCase("docker.io/nginx:1.27.1", "docker.io/nginx:1.28.0")]
[TestCase("nginx:1.27.1", "nginx:1.28.0")]
[TestCase("us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v1",
"us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2")]
public void ReturnsSameImageBaseAsInYaml(string originalImage, string expectedImage)
{
var yaml = $@"image: {originalImage}
";
var annotations = new[] { "{{ .Values.image }}" };
var replacer = new HelmContainerImageReplacer(yaml, DefaultRegistry, annotations, log);

var images = new List<ContainerImageReferenceAndHelmReference>
{
new(ContainerImageReference.FromReferenceString(expectedImage, DefaultRegistry))
};

var result = replacer.UpdateImages(images);

result.UpdatedImageReferences.Should().ContainSingle().Which.Should().Be(expectedImage);
result.UpdatedContents.Should().Contain($"image: {expectedImage}");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,30 @@ public void StructuredValue_AlreadyAtTarget_TracksWithFriendlyName()
result.AlreadyUpToDateImages.Should().BeEquivalentTo(["nginx:1.27.1"]);
}

[Test]
public void StructuredValue_ImageOnNonDefaultRegistry_UpdatesFullRefAndTracksWithRegistry()
{
// Helm/Ref report updates using the registry-qualified FriendlyName, so an image on a
// non-default registry (e.g. GAR/GCR/ECR) round-trips with its registry intact.
const string yaml = @"
image:
name: us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v1
";
var replacer = new HelmValuesImageReplaceStepVariables(yaml, DefaultRegistry, log);
var images = new List<ContainerImageReferenceAndHelmReference>
{
new(ContainerImageReference.FromReferenceString(
"us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2",
DefaultRegistry), "image.name")
};

var result = replacer.UpdateImages(images);

using var scope = new AssertionScope();
result.UpdatedImageReferences.Should().BeEquivalentTo(["us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2"]);
result.UpdatedContents.Should().Contain("name: us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2");
}

[Test]
public void TwoImagesWithSameTag_OnlyUpdatesConfiguredPath()
{
Expand Down Expand Up @@ -159,6 +183,30 @@ public void PathNotFoundInYaml_SkipsImage()
result.UpdatedContents.Should().Be(yaml);
}

[Theory]
[TestCase("docker.io/nginx:1.27.1", "docker.io/nginx:1.28.0")]
[TestCase("nginx:1.27.1", "nginx:1.28.0")]
[TestCase("us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v1",
"us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2")]
public void ReturnsSameImageBaseAsInYaml(string originalImage, string expectedImage)
{
var yaml = $@"
image:
name: {originalImage}
";
var replacer = new HelmValuesImageReplaceStepVariables(yaml, DefaultRegistry, log);
var images = new List<ContainerImageReferenceAndHelmReference>
{
new(ContainerImageReference.FromReferenceString(expectedImage, DefaultRegistry), "image.name")
};

var result = replacer.UpdateImages(images);

using var scope = new AssertionScope();
result.UpdatedImageReferences.Should().BeEquivalentTo(new[] { expectedImage });
result.UpdatedContents.Should().Contain($"name: {expectedImage}");
}

[Test]
public void StructuredValue_MismatchedImageName_DoesNotUpdate()
{
Expand Down
30 changes: 30 additions & 0 deletions source/Calamari.Tests/ArgoCD/InlineJson6902ImageReplacerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@ public void ProcessInlineJson6902Patches_WithInlinePatches_UpdatesImages()
result.UpdatedContents.Should().Contain("nginx:1.25");
}

[Theory]
[TestCase("docker.io/nginx:1.27.1", "docker.io/nginx:1.28.0")]
[TestCase("nginx:1.27.1", "nginx:1.28.0")]
[TestCase("us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v1",
"us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2")]
public void ReturnsSameImageBaseAsInYaml(string originalImage, string expectedImage)
{
var content = $@"apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
patchesJson6902:
- target:
kind: Deployment
name: my-deployment
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: {originalImage}";

var imagesToUpdate = new List<ContainerImageReferenceAndHelmReference>
{
new(ContainerImageReference.FromReferenceString(expectedImage, ArgoCDConstants.DefaultContainerRegistry))
};

var replacer = new InlineJson6902ImageReplacer(content, ArgoCDConstants.DefaultContainerRegistry, log);
var result = replacer.UpdateImages(imagesToUpdate);

result.UpdatedImageReferences.Should().ContainSingle().Which.Should().Be(expectedImage);
result.UpdatedContents.Should().Contain(expectedImage);
}

[Test]
public void ProcessInlineJson6902Patches_WithNoMatches_ReturnsOriginalContent()
{
Expand Down
46 changes: 42 additions & 4 deletions source/Calamari.Tests/ArgoCD/InlineJsonPatchReplacerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public void UpdateImages_WithMultiplePatchesAndImages_UpdatesAllMatchingImages()
result.UpdatedContents.Should().Contain("my-registry.com/busybox:stable");
result.UpdatedImageReferences.Count.Should().Be(2);
result.UpdatedImageReferences.Should().Contain("nginx:1.25");
result.UpdatedImageReferences.Should().Contain("busybox:stable");
result.UpdatedImageReferences.Should().Contain("my-registry.com/busybox:stable");
}

[Test]
Expand Down Expand Up @@ -146,7 +146,7 @@ public void UpdateImages_WithNestedContainerStructures_UpdatesNestedImages()
result.UpdatedContents.Should().NotBeNull();
result.UpdatedImageReferences.Count.Should().Be(2);
result.UpdatedImageReferences.Should().Contain("nginx:1.25");
result.UpdatedImageReferences.Should().Contain("busybox:stable");
result.UpdatedImageReferences.Should().Contain("my-registry.com/busybox:stable");
}

[Test]
Expand Down Expand Up @@ -388,7 +388,7 @@ public void UpdateImages_WithJson6902PatchAddOperation_UpdatesImageReferences()
result.UpdatedContents.Should().Contain("my-registry.com/busybox:stable");
result.UpdatedImageReferences.Count.Should().Be(2);
result.UpdatedImageReferences.Should().Contain("nginx:1.25");
result.UpdatedImageReferences.Should().Contain("busybox:stable");
result.UpdatedImageReferences.Should().Contain("my-registry.com/busybox:stable");
}

[Test]
Expand Down Expand Up @@ -428,7 +428,7 @@ public void UpdateImages_WithMixedStrategicMergeAndJson6902Patches_UpdatesBothTy
result.UpdatedContents.Should().Contain("my-registry.com/busybox:stable");
result.UpdatedImageReferences.Count.Should().Be(2);
result.UpdatedImageReferences.Should().Contain("nginx:1.25");
result.UpdatedImageReferences.Should().Contain("busybox:stable");
result.UpdatedImageReferences.Should().Contain("my-registry.com/busybox:stable");
}

[Test]
Expand Down Expand Up @@ -457,6 +457,44 @@ public void UpdateImages_WithJson6902PatchInitContainerOperation_UpdatesInitCont
result.UpdatedImageReferences.Should().ContainSingle(r => r == "nginx:1.25");
}

[Theory]
[TestCase("docker.io/nginx:1.27.1", "docker.io/nginx:1.28.0")]
[TestCase("nginx:1.27.1", "nginx:1.28.0")]
[TestCase("us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v1",
"us-docker.pkg.dev/shared-gke-dev-gqtrxy/argo-test/helloworld:v2")]
public void ReturnsSameImageBaseAsInYaml(string originalImage, string expectedImage)
{
var inputYaml = $@"
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
patches:
- target:
kind: Deployment
name: my-deployment
patch: |-
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: my-container
image: {originalImage}
";

var imageReplacer = new InlineJsonPatchReplacer(inputYaml, ArgoCDConstants.DefaultContainerRegistry, log);

var update = new List<ContainerImageReferenceAndHelmReference>
{
new(ContainerImageReference.FromReferenceString(expectedImage, ArgoCDConstants.DefaultContainerRegistry))
};

var result = imageReplacer.UpdateImages(update);

result.UpdatedImageReferences.Should().ContainSingle().Which.Should().Be(expectedImage);
result.UpdatedContents.Should().Contain(expectedImage);
}

[Test]
public void UpdateImages_WithJson6902PatchNonImageOperation_DoesNotUpdate()
{
Expand Down
Loading