Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter out invalid name information to generate valid hook name #714

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d52e5fb
fix(deps): update dependency sonaranalyzer.csharp to v9.17.0.82934
renovate[bot] Jan 17, 2024
f9ccb1b
chore: run release on maintenance branches
buehler Jan 18, 2024
342e12d
fix(deps): update dependency roslynator.analyzers to v4.10.0
renovate[bot] Jan 25, 2024
592061c
fix(deps): update dependency sonaranalyzer.csharp to v9.18.0.83559
renovate[bot] Jan 26, 2024
53a2f49
chore(deps): update dependency docfx to v2.75.2
renovate[bot] Jan 27, 2024
cc4434c
fix(deps): update dependency sonaranalyzer.csharp to v9.19.0.84025
renovate[bot] Jan 31, 2024
1c88add
fix(deps): update dependency bouncycastle.cryptography to v2.3.0
renovate[bot] Feb 5, 2024
2d08179
chore(deps): update dependency microsoft.build.locator to v1.7.1
renovate[bot] Feb 6, 2024
8059daf
chore(deps): update dependency microsoft.net.test.sdk to v17.9.0
renovate[bot] Feb 7, 2024
bfc9f7b
fix(deps): update dependency microsoft.build.locator to v1.7.1
renovate[bot] Feb 7, 2024
ea51344
fix hook name
stevefan1999-personal Feb 10, 2024
baaef29
fix linter
stevefan1999-personal Feb 10, 2024
9e0a218
add test
stevefan1999-personal Feb 10, 2024
4f4a061
fix line endings
stevefan1999-personal Feb 10, 2024
26d394a
collapse parameters into variables
stevefan1999-personal Feb 10, 2024
3f86458
add TestServiceValidationWebhook
stevefan1999-personal Feb 10, 2024
6ca4469
chore(deps): update helm/kind-action action to v1.9.0
renovate[bot] Feb 11, 2024
4df87e8
fix(watcher): queues not polling when status updated (#712)
slacki123 Feb 12, 2024
21dc2da
Merge branch 'main' into patch-hook
buehler Feb 13, 2024
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
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"docfx": {
"version": "2.75.1",
"version": "2.75.2",
"commands": ["docfx"]
}
}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/dotnet-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches:
- main
- release
- "maintenance/**"

jobs:
semantic-release:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dotnet-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
run: dotnet format --verify-no-changes

- name: Create Kubernetes Cluster
uses: helm/kind-action@v1.8.0
uses: helm/kind-action@v1.9.0

- name: Execute Tests
run: dotnet test --configuration ${{ runner.debug == '1' && 'Debug' || 'Release' }}
4 changes: 2 additions & 2 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
Condition="$(MSBuildProjectExtension) == '.csproj'" />
<PackageReference
Include="SonarAnalyzer.CSharp"
Version="9.16.0.82469"
Version="9.19.0.84025"
PrivateAssets="all"
Condition="$(MSBuildProjectExtension) == '.csproj'" />
<PackageReference Include="Roslynator.Analyzers" Version="4.9.0" PrivateAssets="All" />
<PackageReference Include="Roslynator.Analyzers" Version="4.10.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 3 additions & 1 deletion src/KubeOps.Cli/Generators/MutationWebhookGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ public void Generate(ResultOutput output)

foreach (var hook in webhooks)
{
var names = new List<string> { hook.Metadata.SingularName, hook.Metadata.Group ?? string.Empty, hook.Metadata.Version };
var hookName = string.Join(".", names.Where(name => !string.IsNullOrWhiteSpace(name)));
mutatorConfig.Webhooks.Add(new V1MutatingWebhook
{
Name = $"mutate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}",
Name = $"mutate.{hookName}",
Copy link
Owner

Choose a reason for hiding this comment

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

As far as I know, if no "group" is present, Kubernetes internal stuff defaults to "Core".

Shouldn't we default to "Core" if there is no group present? Either in the metadata class or in this logic

Choose a reason for hiding this comment

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

It's a shame to let this fix languish, I got a way into building a validating webhook before finding it doesn't work with pods. Is there a workaround?

Copy link
Owner

Choose a reason for hiding this comment

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

I don't know. Never had the issue and never got a serious answer 🤷🏻

Copy link
Owner

Choose a reason for hiding this comment

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

Addition: If you have a good take on it, or an opinion, feel free to share.
I thought using "default" for the group is a promising idea since it is the default behavior of Kubernetes.

MatchPolicy = "Exact",
AdmissionReviewVersions = new[] { "v1" },
SideEffects = "None",
Expand Down
4 changes: 3 additions & 1 deletion src/KubeOps.Cli/Generators/ValidationWebhookGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ public void Generate(ResultOutput output)

foreach (var hook in webhooks)
{
var names = new List<string> { hook.Metadata.SingularName, hook.Metadata.Group ?? string.Empty, hook.Metadata.Version };
var hookName = string.Join(".", names.Where(name => !string.IsNullOrWhiteSpace(name)));
validatorConfig.Webhooks.Add(new V1ValidatingWebhook
{
Name = $"validate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}",
Name = $"validate.{hookName}",
MatchPolicy = "Exact",
AdmissionReviewVersions = new[] { "v1" },
SideEffects = "None",
Expand Down
4 changes: 2 additions & 2 deletions src/KubeOps.Cli/KubeOps.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.2.1" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.6.10" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.3.0" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.7.1" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
Expand Down
74 changes: 42 additions & 32 deletions src/KubeOps.Operator.Web/LocalTunnel/DevelopmentTunnelService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,31 @@ private async Task RegisterValidators(Uri uri)
.ValidationWebhooks
.Select(t => (HookTypeName: t.BaseType!.GenericTypeArguments[0].Name.ToLowerInvariant(),
Entities.ToEntityMetadata(t.BaseType!.GenericTypeArguments[0]).Metadata))
.Select(hook => new V1ValidatingWebhook
.Select(hook =>
{
Name = $"validate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}",
MatchPolicy = "Exact",
AdmissionReviewVersions = new[] { "v1" },
SideEffects = "None",
Rules = new[]
var names = new List<string> { hook.Metadata.SingularName, hook.Metadata.Group ?? string.Empty, hook.Metadata.Version };
var hookName = string.Join(".", names.Where(name => !string.IsNullOrWhiteSpace(name)));
return new V1ValidatingWebhook
{
new V1RuleWithOperations
Name = $"validate.{hookName}",
MatchPolicy = "Exact",
AdmissionReviewVersions = new[] { "v1" },
SideEffects = "None",
Rules = new[]
{
Operations = new[] { "*" },
Resources = new[] { hook.Metadata.PluralName },
ApiGroups = new[] { hook.Metadata.Group },
ApiVersions = new[] { hook.Metadata.Version },
new V1RuleWithOperations
{
Operations = new[] { "*" },
Resources = new[] { hook.Metadata.PluralName },
ApiGroups = new[] { hook.Metadata.Group },
ApiVersions = new[] { hook.Metadata.Version },
},
},
},
ClientConfig = new Admissionregistrationv1WebhookClientConfig
{
Url = $"{uri}validate/{hook.HookTypeName}",
},
ClientConfig = new Admissionregistrationv1WebhookClientConfig
{
Url = $"{uri}validate/{hook.HookTypeName}",
},
};
});

var validatorConfig = new V1ValidatingWebhookConfiguration(
Expand All @@ -85,26 +90,31 @@ private async Task RegisterMutators(Uri uri)
.MutationWebhooks
.Select(t => (HookTypeName: t.BaseType!.GenericTypeArguments[0].Name.ToLowerInvariant(),
Entities.ToEntityMetadata(t.BaseType!.GenericTypeArguments[0]).Metadata))
.Select(hook => new V1MutatingWebhook
.Select(hook =>
{
Name = $"mutate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}",
MatchPolicy = "Exact",
AdmissionReviewVersions = new[] { "v1" },
SideEffects = "None",
Rules = new[]
var names = new List<string> { hook.Metadata.SingularName, hook.Metadata.Group ?? string.Empty, hook.Metadata.Version };
var hookName = string.Join(".", names.Where(name => !string.IsNullOrWhiteSpace(name)));
return new V1MutatingWebhook
{
new V1RuleWithOperations
Name = $"mutate.{hookName}",
MatchPolicy = "Exact",
AdmissionReviewVersions = new[] { "v1" },
SideEffects = "None",
Rules = new[]
{
Operations = new[] { "*" },
Resources = new[] { hook.Metadata.PluralName },
ApiGroups = new[] { hook.Metadata.Group },
ApiVersions = new[] { hook.Metadata.Version },
new V1RuleWithOperations
{
Operations = new[] { "*" },
Resources = new[] { hook.Metadata.PluralName },
ApiGroups = new[] { hook.Metadata.Group },
ApiVersions = new[] { hook.Metadata.Version },
},
},
},
ClientConfig = new Admissionregistrationv1WebhookClientConfig
{
Url = $"{uri}mutate/{hook.HookTypeName}",
},
ClientConfig = new Admissionregistrationv1WebhookClientConfig
{
Url = $"{uri}mutate/{hook.HookTypeName}",
},
};
});

var mutatorConfig = new V1MutatingWebhookConfiguration(
Expand Down
39 changes: 24 additions & 15 deletions src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ private async void OnEntityRequeue(object? sender, (string Name, string? Namespa
return;
}

_entityCache.TryRemove(entity.Uid(), out _);
await ReconcileModification(entity);
}

Expand Down Expand Up @@ -173,16 +172,32 @@ private async void OnEvent(WatchEventType type, TEntity entity)
entity.Name(),
_lastResourceVersion);

_queue.RemoveIfQueued(entity);

try
{
switch (type)
{
case WatchEventType.Added or WatchEventType.Modified:
case WatchEventType.Added:
_entityCache.TryAdd(entity.Uid(), entity.Generation() ?? 0);
await ReconcileModification(entity);
break;
case WatchEventType.Modified:
switch (entity)
{
case { Metadata.DeletionTimestamp: null }:
_entityCache.TryGetValue(entity.Uid(), out var cachedGeneration);

// Check if entity spec has changed through "Generation" value increment. Skip reconcile if not changed.
if (entity.Generation() <= cachedGeneration)
{
_logger.LogDebug(
"""Entity "{kind}/{name}" modification did not modify generation. Skip event.""",
entity.Kind,
entity.Name());
return;
}

// update cached generation since generation now changed
_entityCache.TryUpdate(entity.Uid(), entity.Generation() ?? 1, cachedGeneration);
await ReconcileModification(entity);
break;
case { Metadata: { DeletionTimestamp: not null, Finalizers.Count: > 0 } }:
Expand Down Expand Up @@ -216,31 +231,25 @@ private async void OnEvent(WatchEventType type, TEntity entity)

private async Task ReconcileModification(TEntity entity)
{
var latestGeneration = _entityCache.GetOrAdd(entity.Uid(), 0);
if (entity.Generation() <= latestGeneration)
{
_logger.LogDebug(
"""Entity "{kind}/{name}" modification did not modify generation. Skip event.""",
entity.Kind,
entity.Name());
return;
}

_entityCache.TryUpdate(entity.Uid(), entity.Generation() ?? 1, latestGeneration);
// Re-queue should requested in the controller reconcile method. Invalidate any existing queues.
_queue.RemoveIfQueued(entity);
await using var scope = _provider.CreateAsyncScope();
var controller = scope.ServiceProvider.GetRequiredService<IEntityController<TEntity>>();
await controller.ReconcileAsync(entity);
}

private async Task ReconcileDeletion(TEntity entity)
{
_queue.RemoveIfQueued(entity);
_entityCache.TryRemove(entity.Uid(), out _);
await using var scope = _provider.CreateAsyncScope();
var controller = scope.ServiceProvider.GetRequiredService<IEntityController<TEntity>>();
await controller.DeletedAsync(entity);
}

private async Task ReconcileFinalizer(TEntity entity)
{
_queue.RemoveIfQueued(entity);
var pendingFinalizer = entity.Finalizers();
if (_finalizers.Value.Find(reg =>
reg.EntityType == entity.GetType() && pendingFinalizer.Contains(reg.Identifier)) is not
Expand Down
2 changes: 1 addition & 1 deletion test/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ public async Task Should_Cancel_Requeue_If_New_Event_Fires()
Services.GetRequiredService<TimedEntityQueue<V1OperatorIntegrationTestEntity>>().Count.Should().Be(0);
}

[Fact]
public async Task Should_Not_Affect_Queues_If_Only_Status_Updated()
{
_mock.TargetInvocationCount = 1;
var result = await _client.CreateAsync(new V1OperatorIntegrationTestEntity("test-entity", "username", _ns.Namespace));
result.Status.Status = "changed";
await _client.UpdateStatusAsync(result);
await _mock.WaitForInvocations;

_mock.Invocations.Count.Should().Be(1);
Services.GetRequiredService<TimedEntityQueue<V1OperatorIntegrationTestEntity>>().Count.Should().Be(1);

}

public override async Task InitializeAsync()
{
await base.InitializeAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,27 @@ void Check(int idx, string username)
}
}

[Fact]
public async Task Should_Not_Call_Reconcile_When_Only_Entity_Status_Changed()
{
_mock.TargetInvocationCount = 1;

var result =
await _client.CreateAsync(new V1OperatorIntegrationTestEntity("test-entity", "username", _ns.Namespace));
result.Status.Status = "changed";
// Update or UpdateStatus do not call Reconcile
await _client.UpdateAsync(result);
await _client.UpdateStatusAsync(result);
await _mock.WaitForInvocations;

_mock.Invocations.Count.Should().Be(1);

(string method, V1OperatorIntegrationTestEntity entity) = _mock.Invocations.Single();
method.Should().Be("ReconcileAsync");
entity.Should().BeOfType<V1OperatorIntegrationTestEntity>();
entity.Spec.Username.Should().Be("username");
}

[Fact]
public async Task Should_Call_Delete_For_Deleted_Entity()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@ public async Task Should_Requeue_Entity_And_Reconcile()
_mock.Invocations.Count.Should().Be(5);
}

[Fact]
public async Task Should_Separately_And_Reliably_Requeue_And_Reconcile_Multiple_Entities_In_Parallel()
{
_mock.TargetInvocationCount = 100;
await _client.CreateAsync(new V1OperatorIntegrationTestEntity("test-entity1", "username", _ns.Namespace));
await _client.CreateAsync(new V1OperatorIntegrationTestEntity("test-entity2", "username", _ns.Namespace));
await _client.CreateAsync(new V1OperatorIntegrationTestEntity("test-entity3", "username", _ns.Namespace));
await _client.CreateAsync(new V1OperatorIntegrationTestEntity("test-entity4", "username", _ns.Namespace));
await _mock.WaitForInvocations;

// Expecting invocations, but since in parallel, there is a possibility to for target hit while other are in flight.
_mock.Invocations.Count.Should().BeGreaterOrEqualTo(100).And.BeLessThan(105);
var invocationsGroupedById = _mock.Invocations.GroupBy(item => item.Entity.Metadata.Uid).ToList();
invocationsGroupedById.Count.Should().Be(4);
var invocationDistributions = invocationsGroupedById
.Select(g => (double)g.Count() / _mock.Invocations.Count * 100)
.ToList();
invocationDistributions
.All(p => p is >= 15 and <= 35) // Check that invocations are reasonably distributed
.Should()
.BeTrue($"each entity invocation proportion should be within the specified range of total invocations, " +
$"but instead the distributions were: '{string.Join(", ", invocationDistributions)}'");
}

public override async Task InitializeAsync()
{
await base.InitializeAsync();
Expand Down
2 changes: 1 addition & 1 deletion test/KubeOps.Operator.Test/KubeOps.Operator.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="KubernetesClient" Version="12.1.1"/>
<PackageReference Include="Microsoft.Build.Locator" Version="1.6.10"/>
<PackageReference Include="Microsoft.Build.Locator" Version="1.7.1"/>
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.8.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<ItemGroup>
<PackageReference Include="KubernetesClient" Version="12.1.1"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1"/>
<PackageReference Include="Microsoft.Build.Locator" Version="1.6.10"/>
<PackageReference Include="Microsoft.Build.Locator" Version="1.7.1"/>
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.8.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using k8s.Models;

using KubeOps.Operator.Web.Webhooks.Admission.Mutation;

namespace KubeOps.Operator.Web.Test.TestApp;

[MutationWebhook(typeof(V1Service))]
public class TestServiceMutationWebhook : MutationWebhook<V1Service>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using k8s.Models;

using KubeOps.Operator.Web.Webhooks.Admission.Validation;

namespace KubeOps.Operator.Web.Test.TestApp;

[ValidationWebhook(typeof(V1Service))]
public class TestServiceValidationWebhook : ValidationWebhook<V1Service>;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<ItemGroup>
<PackageReference Include="KubernetesClient" Version="12.1.1"/>
<PackageReference Include="Microsoft.Build.Locator" Version="1.6.10" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.7.1" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
Expand Down
Loading