diff --git a/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/ResourceUpdateReporterTests.cs b/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/ResourceUpdateReporterTests.cs index bf106c3b56..b1d9ffa75c 100644 --- a/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/ResourceUpdateReporterTests.cs +++ b/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/ResourceUpdateReporterTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Calamari.Common.Plumbing.ServiceMessages; using Calamari.Common.Plumbing.Variables; using Calamari.Kubernetes; using Calamari.Kubernetes.ResourceStatus; @@ -155,5 +156,52 @@ public void ClusterScopedResourcesAreIncluded() new KeyValuePair("namespace", null), }).Count() == 6); } + + [Test] + public void ReportsTheUidOfTheControllingOwnerReference() + { + var serviceMessage = ReportSingleResourceFrom("one-replicaset-with-controlling-owner.json"); + + serviceMessage.Properties.Should().Contain("ownerUuid", "42c67fbc-486c-4091-a4df-3827d32b23c7"); + } + + [Test] + public void OmitsOwnerUuidWhenNoOwnerReferenceIsTheController() + { + var serviceMessage = ReportSingleResourceFrom("one-replicaset-with-non-controlling-owners.json"); + + serviceMessage.Properties.Should().NotContainKey("ownerUuid"); + } + + [Test] + public void OmitsOwnerUuidWhenThereAreNoOwnerReferences() + { + var serviceMessage = ReportSingleResourceFrom("one-deployment.json"); + + serviceMessage.Properties.Should().NotContainKey("ownerUuid"); + } + + [Test] + public void ReportsTheFirstControllingOwnerWhenMoreThanOneIsMarkedAsController() + { + var serviceMessage = ReportSingleResourceFrom("one-replicaset-with-multiple-controlling-owners.json"); + + serviceMessage.Properties.Should().Contain("ownerUuid", "42c67fbc-486c-4091-a4df-3827d32b23c7"); + } + + static ServiceMessage ReportSingleResourceFrom(string assetFileName) + { + var log = new InMemoryLog(); + var reporter = new ResourceUpdateReporter(new CalamariVariables(), log); + + var newStatuses = ResourceFactory + .FromListJson(TestFileLoader.Load(assetFileName), new Options()) + .ToDictionary(resource => resource.Uid, resource => resource); + + reporter.ReportUpdatedResources(new Dictionary(), newStatuses, 1); + + return log.ServiceMessages + .Single(message => message.Name == SpecialVariables.ServiceMessages.ResourceStatus.Name); + } } } \ No newline at end of file diff --git a/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/assets/one-replicaset-with-controlling-owner.json b/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/assets/one-replicaset-with-controlling-owner.json new file mode 100644 index 0000000000..f0c0372d15 --- /dev/null +++ b/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/assets/one-replicaset-with-controlling-owner.json @@ -0,0 +1,43 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "apps/v1", + "kind": "ReplicaSet", + "metadata": { + "creationTimestamp": "2023-03-06T06:23:16Z", + "generation": 1, + "labels": { + "app": "nginx" + }, + "name": "nginx-748c667d99", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "nginx", + "uid": "42c67fbc-486c-4091-a4df-3827d32b23c7", + "controller": true, + "blockOwnerDeletion": true + } + ], + "resourceVersion": "1052117", + "uid": "8dd1bd54-2c4e-4f1a-9b7f-6e63f0d0a1c2" + }, + "spec": { + "replicas": 1 + }, + "status": { + "availableReplicas": 1, + "observedGeneration": 1, + "readyReplicas": 1, + "replicas": 1 + } + } + ], + "kind": "List", + "metadata": { + "resourceVersion": "" + } +} diff --git a/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/assets/one-replicaset-with-multiple-controlling-owners.json b/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/assets/one-replicaset-with-multiple-controlling-owners.json new file mode 100644 index 0000000000..45b66d3dd0 --- /dev/null +++ b/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/assets/one-replicaset-with-multiple-controlling-owners.json @@ -0,0 +1,51 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "apps/v1", + "kind": "ReplicaSet", + "metadata": { + "creationTimestamp": "2023-03-06T06:23:16Z", + "generation": 1, + "labels": { + "app": "nginx" + }, + "name": "nginx-748c667d99", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "nginx", + "uid": "42c67fbc-486c-4091-a4df-3827d32b23c7", + "controller": true, + "blockOwnerDeletion": true + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "nginx-other", + "uid": "badc0de0-486c-4091-a4df-3827d32b23c7", + "controller": true, + "blockOwnerDeletion": true + } + ], + "resourceVersion": "1052117", + "uid": "8dd1bd54-2c4e-4f1a-9b7f-6e63f0d0a1c2" + }, + "spec": { + "replicas": 1 + }, + "status": { + "availableReplicas": 1, + "observedGeneration": 1, + "readyReplicas": 1, + "replicas": 1 + } + } + ], + "kind": "List", + "metadata": { + "resourceVersion": "" + } +} diff --git a/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/assets/one-replicaset-with-non-controlling-owners.json b/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/assets/one-replicaset-with-non-controlling-owners.json new file mode 100644 index 0000000000..8c0edd871b --- /dev/null +++ b/source/Calamari.Tests/KubernetesFixtures/ResourceStatus/assets/one-replicaset-with-non-controlling-owners.json @@ -0,0 +1,49 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "apps/v1", + "kind": "ReplicaSet", + "metadata": { + "creationTimestamp": "2023-03-06T06:23:16Z", + "generation": 1, + "labels": { + "app": "nginx" + }, + "name": "nginx-748c667d99", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "nginx", + "uid": "42c67fbc-486c-4091-a4df-3827d32b23c7", + "controller": false, + "blockOwnerDeletion": true + }, + { + "apiVersion": "v1", + "kind": "ConfigMap", + "name": "nginx-config", + "uid": "c0ffee00-486c-4091-a4df-3827d32b23c7" + } + ], + "resourceVersion": "1052117", + "uid": "8dd1bd54-2c4e-4f1a-9b7f-6e63f0d0a1c2" + }, + "spec": { + "replicas": 1 + }, + "status": { + "availableReplicas": 1, + "observedGeneration": 1, + "readyReplicas": 1, + "replicas": 1 + } + } + ], + "kind": "List", + "metadata": { + "resourceVersion": "" + } +} diff --git a/source/Calamari/Kubernetes/ResourceStatus/ResourceUpdateReporter.cs b/source/Calamari/Kubernetes/ResourceStatus/ResourceUpdateReporter.cs index 4f51147fda..f8936a10b5 100644 --- a/source/Calamari/Kubernetes/ResourceStatus/ResourceUpdateReporter.cs +++ b/source/Calamari/Kubernetes/ResourceStatus/ResourceUpdateReporter.cs @@ -97,6 +97,12 @@ private void SendServiceMessage(Resource resource, bool removed, int checkCount) {ResourceStatusAttributes.CheckCount, checkCount.ToString()} }; + // Absent rather than empty: the server reads this optionally, and a blank value would read as an owner with no uid. + if (!string.IsNullOrEmpty(resource.ControllerOwnerUid)) + { + parameters.Add(ResourceStatusAttributes.OwnerUuid, resource.ControllerOwnerUid); + } + var message = new ServiceMessage(SpecialVariables.ServiceMessages.ResourceStatus.Name, parameters); log.WriteServiceMessage(message); } diff --git a/source/Calamari/Kubernetes/ResourceStatus/Resources/Resource.cs b/source/Calamari/Kubernetes/ResourceStatus/Resources/Resource.cs index 1f2cc8fd3a..8e91b34d13 100644 --- a/source/Calamari/Kubernetes/ResourceStatus/Resources/Resource.cs +++ b/source/Calamari/Kubernetes/ResourceStatus/Resources/Resource.cs @@ -13,6 +13,10 @@ public class Resource : IResourceIdentity { [JsonIgnore] public IEnumerable OwnerUids { get; } + // Kubernetes permits at most one owner reference with controller: true, so this is the single parent + // an object hangs off in an ownership tree. Null for objects Octopus applies directly, which are roots. + [JsonIgnore] public string ControllerOwnerUid { get; } + [JsonIgnore] public string Uid { get; protected set; } [JsonIgnore] public ResourceGroupVersionKind GroupVersionKind { get; protected set; } @@ -34,6 +38,9 @@ public Resource(JObject json, Options options) { // force enumeration to prevent memory growth OwnerUids = json.SelectTokens("$.metadata.ownerReferences[*].uid").Values().ToList(); + ControllerOwnerUid = json.SelectTokens("$.metadata.ownerReferences[*]") + .FirstOrDefault(owner => owner["controller"]?.Value() == true) + ?["uid"]?.Value(); Uid = Field(json, "$.metadata.uid"); GroupVersionKind = json.ToResourceGroupVersionKind(); Name = Field(json, "$.metadata.name"); diff --git a/source/Calamari/Kubernetes/SpecialVariables.cs b/source/Calamari/Kubernetes/SpecialVariables.cs index e130cbcd74..f3f263fed2 100644 --- a/source/Calamari/Kubernetes/SpecialVariables.cs +++ b/source/Calamari/Kubernetes/SpecialVariables.cs @@ -158,6 +158,7 @@ public static class Attributes public const string TargetName = "targetName"; public const string SpaceId = "spaceId"; public const string Uuid = "uuid"; + public const string OwnerUuid = "ownerUuid"; public const string Group = "group"; public const string Version = "version"; public const string Kind = "kind";