diff --git a/_old/src/KubeOps/Operator/Webhooks/IValidationWebhook{TEntity}.cs b/_old/src/KubeOps/Operator/Webhooks/IValidationWebhook{TEntity}.cs
deleted file mode 100644
index fb8b1909..00000000
--- a/_old/src/KubeOps/Operator/Webhooks/IValidationWebhook{TEntity}.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-namespace KubeOps.Operator.Webhooks;
-
-///
-///
-/// Validation webhook for kubernetes.
-/// This is used by the https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/.
-/// Implement a class with this interface, overwrite the needed functions.
-///
-///
-/// If there are _any_ webhooks registered in the system, the build process
-/// will create the needed certificates for the CA and the service for the
-/// application to run.
-///
-///
-/// All methods have a default implementation. They *MUST* return in 10s, otherwise kubernetes will fail the call.
-/// The async methods do call the sync ones and the sync ones
-/// return a "not implemented" result by default.
-///
-///
-/// Overwrite the needed ones as defined in .
-/// The async implementations take precedence over the synchronous ones.
-///
-///
-/// Note that the operator must use HTTPS (in some form) to use validators.
-/// For local development, this could be done with ngrok (https://ngrok.com/).
-///
-///
-/// The operator generator (if enabled during build) will generate the CA certificate
-/// and the operator will generate the server certificate during pod startup.
-///
-///
-/// The type of the entity that should be validated.
-public interface IValidationWebhook : IAdmissionWebhook
-{
- ///
- string IAdmissionWebhook.Endpoint
- {
- get => WebhookEndpointFactory.Create(GetType(), "/validate");
- }
-
- ///
- ValidationResult IAdmissionWebhook.Create(TEntity newEntity, bool dryRun)
- => AdmissionResult.NotImplemented();
-
- ///
- ValidationResult IAdmissionWebhook.Update(
- TEntity oldEntity,
- TEntity newEntity,
- bool dryRun)
- => AdmissionResult.NotImplemented();
-
- ///
- ValidationResult IAdmissionWebhook.Delete(TEntity oldEntity, bool dryRun)
- => AdmissionResult.NotImplemented();
-
- AdmissionResponse IAdmissionWebhook.TransformResult(
- ValidationResult result,
- AdmissionRequest request)
- => new()
- {
- Allowed = result.Valid,
- Status = result.StatusMessage == null
- ? null
- : new AdmissionResponse.Reason { Code = result.StatusCode ?? 0, Message = result.StatusMessage, },
- Warnings = result.Warnings.ToArray(),
- };
-}
diff --git a/_old/src/KubeOps/Operator/Webhooks/MutatingWebhookBuilder.cs b/_old/src/KubeOps/Operator/Webhooks/MutatingWebhookBuilder.cs
deleted file mode 100644
index 0c17fc75..00000000
--- a/_old/src/KubeOps/Operator/Webhooks/MutatingWebhookBuilder.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using System.Reflection;
-
-using k8s.Models;
-
-using KubeOps.KubernetesClient.Entities;
-using KubeOps.Operator.Builder;
-using KubeOps.Operator.Entities.Extensions;
-using KubeOps.Operator.Util;
-
-namespace KubeOps.Operator.Webhooks;
-
-internal class MutatingWebhookBuilder
-{
- private readonly IComponentRegistrar _componentRegistrar;
- private readonly IWebhookMetadataBuilder _webhookMetadataBuilder;
- private readonly IServiceProvider _services;
-
- public MutatingWebhookBuilder(
- IComponentRegistrar componentRegistrar,
- IWebhookMetadataBuilder webhookMetadataBuilder,
- IServiceProvider services)
- {
- _componentRegistrar = componentRegistrar;
- _webhookMetadataBuilder = webhookMetadataBuilder;
- _services = services;
- }
-
- public List BuildWebhooks(WebhookConfig webhookConfig)
- {
- using var scope = _services.CreateScope();
- return _componentRegistrar.MutatorRegistrations
- .Select(
- wh =>
- {
- (Type mutatorType, Type entityType) = wh;
-
- var instance = scope.ServiceProvider.GetRequiredService(mutatorType);
-
- var (name, endpoint) =
- _webhookMetadataBuilder.GetMetadata(instance, entityType);
-
- var clientConfig = new Admissionregistrationv1WebhookClientConfig();
- if (!string.IsNullOrWhiteSpace(webhookConfig.BaseUrl))
- {
- clientConfig.Url = webhookConfig.BaseUrl.FormatWebhookUrl(endpoint);
- }
- else
- {
- clientConfig.Service = webhookConfig.Service?.DeepClone();
- if (clientConfig.Service != null)
- {
- clientConfig.Service.Path = endpoint;
- }
-
- clientConfig.CaBundle = webhookConfig.CaBundle;
- }
-
- var operationsProperty = typeof(IAdmissionWebhook<,>)
- .MakeGenericType(entityType, typeof(MutationResult))
- .GetProperties(BindingFlags.Instance | BindingFlags.NonPublic)
- .First(m => m.Name == "SupportedOperations");
-
- var crd = entityType.ToEntityDefinition();
-
- var webhook = new V1MutatingWebhook
- {
- Name = name.TrimWebhookName(),
- AdmissionReviewVersions = new[] { "v1" },
- SideEffects = "None",
- MatchPolicy = "Exact",
- Rules = new List
- {
- new()
- {
- Operations = operationsProperty.GetValue(instance) as IList,
- Resources = new[] { crd.Plural },
- Scope = "*",
- ApiGroups = new[] { crd.Group },
- ApiVersions = new[] { crd.Version },
- },
- },
- ClientConfig = clientConfig,
- };
-
- if (instance is IConfigurableMutationWebhook configurable)
- {
- configurable.Configure(webhook);
- }
-
- return webhook;
- })
- .ToList();
- }
-}
diff --git a/_old/src/KubeOps/Operator/Webhooks/MutatingWebhookConfigurationBuilder.cs b/_old/src/KubeOps/Operator/Webhooks/MutatingWebhookConfigurationBuilder.cs
deleted file mode 100644
index 5c7e8d89..00000000
--- a/_old/src/KubeOps/Operator/Webhooks/MutatingWebhookConfigurationBuilder.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using k8s.Models;
-
-using KubeOps.Operator.Util;
-
-namespace KubeOps.Operator.Webhooks;
-
-internal class MutatingWebhookConfigurationBuilder
-{
- private readonly MutatingWebhookBuilder _webhookBuilder;
-
- public MutatingWebhookConfigurationBuilder(MutatingWebhookBuilder webhookBuilder)
- {
- _webhookBuilder = webhookBuilder;
- }
-
- public V1MutatingWebhookConfiguration BuildWebhookConfiguration(WebhookConfig webhookConfig) =>
- new()
- {
- Kind = V1MutatingWebhookConfiguration.KubeKind,
- ApiVersion =
- $"{V1MutatingWebhookConfiguration.KubeGroup}/{V1MutatingWebhookConfiguration.KubeApiVersion}",
- Metadata = new V1ObjectMeta { Name = webhookConfig.OperatorName.TrimWebhookName("mutators."), },
- Webhooks = _webhookBuilder.BuildWebhooks(webhookConfig),
- };
-}
diff --git a/_old/src/KubeOps/Operator/Webhooks/ValidatingWebhookBuilder.cs b/_old/src/KubeOps/Operator/Webhooks/ValidatingWebhookBuilder.cs
deleted file mode 100644
index 66414cea..00000000
--- a/_old/src/KubeOps/Operator/Webhooks/ValidatingWebhookBuilder.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using System.Reflection;
-
-using k8s.Models;
-
-using KubeOps.KubernetesClient.Entities;
-using KubeOps.Operator.Builder;
-using KubeOps.Operator.Entities.Extensions;
-using KubeOps.Operator.Util;
-
-namespace KubeOps.Operator.Webhooks;
-
-internal class ValidatingWebhookBuilder
-{
- private readonly IComponentRegistrar _componentRegistrar;
- private readonly IWebhookMetadataBuilder _webhookMetadataBuilder;
- private readonly IServiceProvider _services;
-
- public ValidatingWebhookBuilder(
- IComponentRegistrar componentRegistrar,
- IWebhookMetadataBuilder webhookMetadataBuilder,
- IServiceProvider services)
- {
- _componentRegistrar = componentRegistrar;
- _webhookMetadataBuilder = webhookMetadataBuilder;
- _services = services;
- }
-
- public List BuildWebhooks(WebhookConfig webhookConfig)
- {
- using var scope = _services.CreateScope();
- return _componentRegistrar.ValidatorRegistrations
- .Select(
- wh =>
- {
- (Type validatorType, Type entityType) = wh;
-
- var instance = scope.ServiceProvider.GetRequiredService(validatorType);
-
- var (name, endpoint) =
- _webhookMetadataBuilder.GetMetadata(instance, entityType);
-
- var clientConfig = new Admissionregistrationv1WebhookClientConfig();
- if (!string.IsNullOrWhiteSpace(webhookConfig.BaseUrl))
- {
- clientConfig.Url = webhookConfig.BaseUrl.FormatWebhookUrl(endpoint);
- }
- else
- {
- clientConfig.Service = webhookConfig.Service?.DeepClone();
- if (clientConfig.Service != null)
- {
- clientConfig.Service.Path = endpoint;
- }
-
- clientConfig.CaBundle = webhookConfig.CaBundle;
- }
-
- var operationsProperty = typeof(IAdmissionWebhook<,>)
- .MakeGenericType(entityType, typeof(ValidationResult))
- .GetProperties(BindingFlags.Instance | BindingFlags.NonPublic)
- .First(m => m.Name == "SupportedOperations");
-
- var crd = entityType.ToEntityDefinition();
-
- var webhook = new V1ValidatingWebhook
- {
- Name = name.TrimWebhookName(),
- AdmissionReviewVersions = new[] { "v1" },
- SideEffects = "None",
- MatchPolicy = "Exact",
- Rules = new List
- {
- new()
- {
- Operations = operationsProperty.GetValue(instance) as IList,
- Resources = new[] { crd.Plural },
- Scope = "*",
- ApiGroups = new[] { crd.Group },
- ApiVersions = new[] { crd.Version },
- },
- },
- ClientConfig = clientConfig,
- };
-
- if (instance is IConfigurableValidationWebhook configurable)
- {
- configurable.Configure(webhook);
- }
-
- return webhook;
- })
- .ToList();
- }
-}
diff --git a/_old/src/KubeOps/Operator/Webhooks/ValidatingWebhookConfigurationBuilder.cs b/_old/src/KubeOps/Operator/Webhooks/ValidatingWebhookConfigurationBuilder.cs
deleted file mode 100644
index 5b824010..00000000
--- a/_old/src/KubeOps/Operator/Webhooks/ValidatingWebhookConfigurationBuilder.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using k8s.Models;
-
-using KubeOps.Operator.Util;
-
-namespace KubeOps.Operator.Webhooks;
-
-internal class ValidatingWebhookConfigurationBuilder
-{
- private readonly ValidatingWebhookBuilder _webhookBuilder;
-
- public ValidatingWebhookConfigurationBuilder(ValidatingWebhookBuilder webhookBuilder)
- {
- _webhookBuilder = webhookBuilder;
- }
-
- public V1ValidatingWebhookConfiguration BuildWebhookConfiguration(WebhookConfig webhookConfig) =>
- new()
- {
- Kind = V1ValidatingWebhookConfiguration.KubeKind,
- ApiVersion =
- $"{V1ValidatingWebhookConfiguration.KubeGroup}/{V1ValidatingWebhookConfiguration.KubeApiVersion}",
- Metadata = new() { Name = webhookConfig.OperatorName.TrimWebhookName("validators."), },
- Webhooks = _webhookBuilder.BuildWebhooks(webhookConfig),
- };
-}
diff --git a/_old/src/KubeOps/Operator/Webhooks/ValidationResult.cs b/_old/src/KubeOps/Operator/Webhooks/ValidationResult.cs
deleted file mode 100644
index f775cf4f..00000000
--- a/_old/src/KubeOps/Operator/Webhooks/ValidationResult.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-namespace KubeOps.Operator.Webhooks;
-
-///
-/// Result that should be returned to kubernetes.
-/// This result gets translated to a result object that is then transmitted to kubernetes.
-/// Describes results of an .
-///
-public sealed class ValidationResult : AdmissionResult
-{
- ///
- /// Utility method that creates a successful (valid) result with an optional
- /// list of warnings.
- ///
- /// An optional list of warnings to append.
- /// A valid .
- public static ValidationResult Success(params string[] warnings) => new() { Warnings = warnings };
-
- ///
- /// Utility method that creates a fail result without any further information.
- ///
- /// An invalid .
- public static ValidationResult Fail() => new() { Valid = false };
-
- ///
- /// Utility method that creates a fail result with a customized http status code
- /// and status message.
- ///
- /// The custom http-status-code.
- /// The custom status-message.
- /// An invalid with a custom status-code and status-message.
- public static ValidationResult Fail(int statusCode, string statusMessage) => new()
- {
- Valid = false,
- StatusCode = statusCode,
- StatusMessage = statusMessage,
- };
-}
diff --git a/_old/src/KubeOps/Operator/Webhooks/WebhookConfig.cs b/_old/src/KubeOps/Operator/Webhooks/WebhookConfig.cs
deleted file mode 100644
index 552d3298..00000000
--- a/_old/src/KubeOps/Operator/Webhooks/WebhookConfig.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using k8s.Models;
-
-namespace KubeOps.Operator.Webhooks;
-
-internal record WebhookConfig(
- string OperatorName,
- string? BaseUrl,
- byte[]? CaBundle,
- Admissionregistrationv1ServiceReference? Service);
diff --git a/examples/WebhookOperator/Controller/V1TestEntityController.cs b/examples/WebhookOperator/Controller/V1TestEntityController.cs
index 53b6de2d..96c19795 100644
--- a/examples/WebhookOperator/Controller/V1TestEntityController.cs
+++ b/examples/WebhookOperator/Controller/V1TestEntityController.cs
@@ -1,30 +1,30 @@
-using KubeOps.Abstractions.Controller;
-using KubeOps.Abstractions.Rbac;
-
-using WebhookOperator.Entities;
-
-namespace WebhookOperator.Controller;
-
-[EntityRbac(typeof(V1TestEntity), Verbs = RbacVerb.All)]
-public class V1TestEntityController : IEntityController
-{
- private readonly ILogger _logger;
-
- public V1TestEntityController(
- ILogger logger)
- {
- _logger = logger;
- }
-
- public Task ReconcileAsync(V1TestEntity entity)
- {
- _logger.LogInformation("Reconciling entity {Entity}.", entity);
- return Task.CompletedTask;
- }
-
- public Task DeletedAsync(V1TestEntity entity)
- {
- _logger.LogInformation("Deleted entity {Entity}.", entity);
- return Task.CompletedTask;
- }
-}
+using KubeOps.Abstractions.Controller;
+using KubeOps.Abstractions.Rbac;
+
+using WebhookOperator.Entities;
+
+namespace WebhookOperator.Controller;
+
+[EntityRbac(typeof(V1TestEntity), Verbs = RbacVerb.All)]
+public class V1TestEntityController : IEntityController
+{
+ private readonly ILogger _logger;
+
+ public V1TestEntityController(
+ ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public Task ReconcileAsync(V1TestEntity entity)
+ {
+ _logger.LogInformation("Reconciling entity {Entity}.", entity);
+ return Task.CompletedTask;
+ }
+
+ public Task DeletedAsync(V1TestEntity entity)
+ {
+ _logger.LogInformation("Deleted entity {Entity}.", entity);
+ return Task.CompletedTask;
+ }
+}
diff --git a/examples/WebhookOperator/Entities/V1TestEntity.cs b/examples/WebhookOperator/Entities/V1TestEntity.cs
index 8610df26..7855d56b 100644
--- a/examples/WebhookOperator/Entities/V1TestEntity.cs
+++ b/examples/WebhookOperator/Entities/V1TestEntity.cs
@@ -1,16 +1,16 @@
-using k8s.Models;
-
-using KubeOps.Abstractions.Entities;
-
-namespace WebhookOperator.Entities;
-
-[KubernetesEntity(Group = "webhook.dev", ApiVersion = "v1", Kind = "TestEntity")]
-public partial class V1TestEntity : CustomKubernetesEntity
-{
- public override string ToString() => $"Test Entity ({Metadata.Name}): {Spec.Username}";
-
- public class EntitySpec
- {
- public string Username { get; set; } = string.Empty;
- }
-}
+using k8s.Models;
+
+using KubeOps.Abstractions.Entities;
+
+namespace WebhookOperator.Entities;
+
+[KubernetesEntity(Group = "webhook.dev", ApiVersion = "v1", Kind = "TestEntity")]
+public partial class V1TestEntity : CustomKubernetesEntity
+{
+ public override string ToString() => $"Test Entity ({Metadata.Name}): {Spec.Username}";
+
+ public class EntitySpec
+ {
+ public string Username { get; set; } = string.Empty;
+ }
+}
diff --git a/examples/WebhookOperator/Program.cs b/examples/WebhookOperator/Program.cs
index 2395f682..aa264da5 100644
--- a/examples/WebhookOperator/Program.cs
+++ b/examples/WebhookOperator/Program.cs
@@ -1,17 +1,17 @@
-using KubeOps.Operator;
-
-var builder = WebApplication.CreateBuilder(args);
-builder.Services
- .AddKubernetesOperator()
- .RegisterComponents();
-
-builder.Services
- .AddControllers();
-
-var app = builder.Build();
-
-app.UseRouting();
-app.UseDeveloperExceptionPage();
-app.MapControllers();
-
-await app.RunAsync();
+using KubeOps.Operator;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services
+ .AddKubernetesOperator()
+ .RegisterComponents();
+
+builder.Services
+ .AddControllers();
+
+var app = builder.Build();
+
+app.UseRouting();
+app.UseDeveloperExceptionPage();
+app.MapControllers();
+
+await app.RunAsync();
diff --git a/examples/WebhookOperator/Webhooks/TestMutationWebhook.cs b/examples/WebhookOperator/Webhooks/TestMutationWebhook.cs
new file mode 100644
index 00000000..27b274d5
--- /dev/null
+++ b/examples/WebhookOperator/Webhooks/TestMutationWebhook.cs
@@ -0,0 +1,20 @@
+using KubeOps.Operator.Web.Webhooks.Mutation;
+
+using WebhookOperator.Entities;
+
+namespace WebhookOperator.Webhooks;
+
+[MutationWebhook(typeof(V1TestEntity))]
+public class TestMutationWebhook : MutationWebhook
+{
+ public override MutationResult Create(V1TestEntity entity, bool dryRun)
+ {
+ if (entity.Spec.Username == "overwrite")
+ {
+ entity.Spec.Username = "random overwritten";
+ return Modified(entity);
+ }
+
+ return NoChanges();
+ }
+}
diff --git a/examples/WebhookOperator/Webhooks/TestValidationWebhook.cs b/examples/WebhookOperator/Webhooks/TestValidationWebhook.cs
index 061a1e0a..e2904325 100644
--- a/examples/WebhookOperator/Webhooks/TestValidationWebhook.cs
+++ b/examples/WebhookOperator/Webhooks/TestValidationWebhook.cs
@@ -1,29 +1,29 @@
-using KubeOps.Operator.Web.Webhooks.Validation;
-
-using WebhookOperator.Entities;
-
-namespace WebhookOperator.Webhooks;
-
-[ValidationWebhook(typeof(V1TestEntity))]
-public class TestValidationWebhook : ValidationWebhook
-{
- public override ValidationResult Create(V1TestEntity entity, bool dryRun)
- {
- if (entity.Spec.Username == "forbidden")
- {
- return Fail("name may not be 'forbidden'.", 422);
- }
-
- return Success();
- }
-
- public override ValidationResult Update(V1TestEntity oldEntity, V1TestEntity newEntity, bool dryRun)
- {
- if (newEntity.Spec.Username == "forbidden")
- {
- return Fail("name may not be 'forbidden'.");
- }
-
- return Success();
- }
-}
+using KubeOps.Operator.Web.Webhooks.Validation;
+
+using WebhookOperator.Entities;
+
+namespace WebhookOperator.Webhooks;
+
+[ValidationWebhook(typeof(V1TestEntity))]
+public class TestValidationWebhook : ValidationWebhook
+{
+ public override ValidationResult Create(V1TestEntity entity, bool dryRun)
+ {
+ if (entity.Spec.Username == "forbidden")
+ {
+ return Fail("name may not be 'forbidden'.", 422);
+ }
+
+ return Success();
+ }
+
+ public override ValidationResult Update(V1TestEntity oldEntity, V1TestEntity newEntity, bool dryRun)
+ {
+ if (newEntity.Spec.Username == "forbidden")
+ {
+ return Fail("name may not be 'forbidden'.");
+ }
+
+ return Success();
+ }
+}
diff --git a/examples/WebhookOperator/test_entity.yaml b/examples/WebhookOperator/test_entity.yaml
index 9fa577d7..6f1975cf 100644
--- a/examples/WebhookOperator/test_entity.yaml
+++ b/examples/WebhookOperator/test_entity.yaml
@@ -3,4 +3,4 @@ kind: TestEntity
metadata:
name: test-webhook-entity
spec:
- username: my-username
+ username: overwrite
diff --git a/examples/WebhookOperator/validation_webhook_config.yaml b/examples/WebhookOperator/validation_webhook_config.yaml
deleted file mode 100644
index d5dc67e2..00000000
--- a/examples/WebhookOperator/validation_webhook_config.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-apiVersion: admissionregistration.k8s.io/v1
-kind: ValidatingWebhookConfiguration
-metadata:
- name: webhook-operator-dev
-webhooks:
- - admissionReviewVersions:
- - v1
- clientConfig:
- url: https://290f-2a02-169-601-0-ad64-cd7-b178-7a5b.ngrok-free.app/validate/v1testentity
- failurePolicy: Fail
- matchPolicy: Exact
- name: test.web.hook.com
- rules:
- - apiGroups:
- - webhook.dev
- apiVersions:
- - '*'
- operations:
- - '*'
- resources:
- - testentitys
- scope: '*'
- sideEffects: None
- timeoutSeconds: 10
diff --git a/examples/WebhookOperator/webhook_configs.yaml b/examples/WebhookOperator/webhook_configs.yaml
new file mode 100644
index 00000000..3f1821d1
--- /dev/null
+++ b/examples/WebhookOperator/webhook_configs.yaml
@@ -0,0 +1,49 @@
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+ name: validation-webhooks
+webhooks:
+ - admissionReviewVersions:
+ - v1
+ clientConfig:
+ url: https://e9f2-85-195-221-58.ngrok-free.app/validate/v1testentity
+ matchPolicy: Exact
+ name: test.web.hook.com
+ rules:
+ - apiGroups:
+ - webhook.dev
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ - DELETE
+ resources:
+ - testentitys
+ sideEffects: None
+ timeoutSeconds: 10
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+ name: mutation-webhooks
+webhooks:
+ - admissionReviewVersions:
+ - v1
+ clientConfig:
+ url: https://e9f2-85-195-221-58.ngrok-free.app/mutate/v1testentity
+ matchPolicy: Exact
+ name: test.web.hook.com
+ rules:
+ - apiGroups:
+ - webhook.dev
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ - DELETE
+ resources:
+ - testentitys
+ sideEffects: None
+ timeoutSeconds: 10
diff --git a/src/KubeOps.Abstractions/Kustomize/KustomizationSecretGenerator.cs b/src/KubeOps.Abstractions/Kustomize/KustomizationSecretGenerator.cs
index e8cc080e..856dce6e 100644
--- a/src/KubeOps.Abstractions/Kustomize/KustomizationSecretGenerator.cs
+++ b/src/KubeOps.Abstractions/Kustomize/KustomizationSecretGenerator.cs
@@ -1,23 +1,23 @@
-namespace KubeOps.Abstractions.Kustomize;
-
-///
-/// Entitiy for config map generators in a kustomization.yaml file.
-///
-public class KustomizationSecretGenerator
-{
- ///
- /// The name of the config map.
- ///
- public string Name { get; set; } = string.Empty;
-
- ///
- /// List of files that should be added to the generated config map.
- ///
- public IList? Files { get; set; }
-
- ///
- /// Config literals to add to the config map in the form of:
- /// - NAME=value.
- ///
- public IList? Literals { get; set; }
-}
+namespace KubeOps.Abstractions.Kustomize;
+
+///
+/// Entitiy for config map generators in a kustomization.yaml file.
+///
+public class KustomizationSecretGenerator
+{
+ ///
+ /// The name of the config map.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// List of files that should be added to the generated config map.
+ ///
+ public IList? Files { get; set; }
+
+ ///
+ /// Config literals to add to the config map in the form of:
+ /// - NAME=value.
+ ///
+ public IList? Literals { get; set; }
+}
diff --git a/src/KubeOps.Cli/Certificates/CertificateGenerator.cs b/src/KubeOps.Cli/Certificates/CertificateGenerator.cs
index dba5654a..6e3ccf57 100644
--- a/src/KubeOps.Cli/Certificates/CertificateGenerator.cs
+++ b/src/KubeOps.Cli/Certificates/CertificateGenerator.cs
@@ -1,130 +1,130 @@
-using Org.BouncyCastle.Asn1.X509;
-using Org.BouncyCastle.Crypto;
-using Org.BouncyCastle.Crypto.Generators;
-using Org.BouncyCastle.Crypto.Operators;
-using Org.BouncyCastle.Crypto.Prng;
-using Org.BouncyCastle.Math;
-using Org.BouncyCastle.Security;
-using Org.BouncyCastle.Utilities;
-using Org.BouncyCastle.X509;
-using Org.BouncyCastle.X509.Extension;
-
-namespace KubeOps.Cli.Certificates;
-
-internal static class CertificateGenerator
-{
- public static (X509Certificate Certificate, AsymmetricCipherKeyPair Key) CreateCaCertificate()
- {
- var randomGenerator = new CryptoApiRandomGenerator();
- var random = new SecureRandom(randomGenerator);
-
- // The Certificate Generator
- var certificateGenerator = new X509V3CertificateGenerator();
-
- // Serial Number
- var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
- certificateGenerator.SetSerialNumber(serialNumber);
-
- // Issuer and Subject Name
- var name = new X509Name("CN=Operator Root CA, C=DEV, L=Kubernetes");
- certificateGenerator.SetIssuerDN(name);
- certificateGenerator.SetSubjectDN(name);
-
- // Valid For
- var notBefore = DateTime.UtcNow.Date;
- var notAfter = notBefore.AddYears(5);
- certificateGenerator.SetNotBefore(notBefore);
- certificateGenerator.SetNotAfter(notAfter);
-
- // Cert Extensions
- certificateGenerator.AddExtension(
- X509Extensions.BasicConstraints,
- true,
- new BasicConstraints(true));
- certificateGenerator.AddExtension(
- X509Extensions.KeyUsage,
- true,
- new KeyUsage(KeyUsage.KeyCertSign | KeyUsage.CrlSign | KeyUsage.KeyEncipherment));
-
- // Subject Public Key
- const int keyStrength = 256;
- var keyGenerator = new ECKeyPairGenerator("ECDSA");
- keyGenerator.Init(new KeyGenerationParameters(random, keyStrength));
- var key = keyGenerator.GenerateKeyPair();
-
- certificateGenerator.SetPublicKey(key.Public);
-
- var signatureFactory = new Asn1SignatureFactory("SHA512WITHECDSA", key.Private, random);
- var certificate = certificateGenerator.Generate(signatureFactory);
-
- return (certificate, key);
- }
-
- public static (X509Certificate Certificate, AsymmetricCipherKeyPair Key) CreateServerCertificate(
- (X509Certificate Certificate, AsymmetricCipherKeyPair Key) ca, string serverName, string serverNamespace)
- {
- var randomGenerator = new CryptoApiRandomGenerator();
- var random = new SecureRandom(randomGenerator);
-
- // The Certificate Generator
- var certificateGenerator = new X509V3CertificateGenerator();
-
- // Serial Number
- var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
- certificateGenerator.SetSerialNumber(serialNumber);
-
- // Issuer and Subject Name
- certificateGenerator.SetIssuerDN(ca.Certificate.SubjectDN);
- certificateGenerator.SetSubjectDN(new X509Name("CN=Operator Service, C=DEV, L=Kubernetes"));
-
- // Valid For
- var notBefore = DateTime.UtcNow.Date;
- var notAfter = notBefore.AddYears(5);
- certificateGenerator.SetNotBefore(notBefore);
- certificateGenerator.SetNotAfter(notAfter);
-
- // Cert Extensions
- certificateGenerator.AddExtension(
- X509Extensions.BasicConstraints,
- false,
- new BasicConstraints(false));
- certificateGenerator.AddExtension(
- X509Extensions.KeyUsage,
- true,
- new KeyUsage(KeyUsage.NonRepudiation | KeyUsage.KeyEncipherment | KeyUsage.DigitalSignature));
- certificateGenerator.AddExtension(
- X509Extensions.ExtendedKeyUsage,
- false,
- new ExtendedKeyUsage(KeyPurposeID.id_kp_clientAuth, KeyPurposeID.id_kp_serverAuth));
- certificateGenerator.AddExtension(
- X509Extensions.SubjectKeyIdentifier,
- false,
- new SubjectKeyIdentifierStructure(ca.Key.Public));
- certificateGenerator.AddExtension(
- X509Extensions.AuthorityKeyIdentifier,
- false,
- new AuthorityKeyIdentifierStructure(ca.Certificate));
- certificateGenerator.AddExtension(
- X509Extensions.SubjectAlternativeName,
- false,
- new GeneralNames(new[]
- {
- new GeneralName(GeneralName.DnsName, $"{serverName}.{serverNamespace}.svc"),
- new GeneralName(GeneralName.DnsName, $"*.{serverNamespace}.svc"),
- new GeneralName(GeneralName.DnsName, "*.svc"),
- }));
-
- // Subject Public Key
- const int keyStrength = 256;
- var keyGenerator = new ECKeyPairGenerator("ECDSA");
- keyGenerator.Init(new KeyGenerationParameters(random, keyStrength));
- var key = keyGenerator.GenerateKeyPair();
-
- certificateGenerator.SetPublicKey(key.Public);
-
- var signatureFactory = new Asn1SignatureFactory("SHA512WITHECDSA", ca.Key.Private, random);
- var certificate = certificateGenerator.Generate(signatureFactory);
-
- return (certificate, key);
- }
-}
+using Org.BouncyCastle.Asn1.X509;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Operators;
+using Org.BouncyCastle.Crypto.Prng;
+using Org.BouncyCastle.Math;
+using Org.BouncyCastle.Security;
+using Org.BouncyCastle.Utilities;
+using Org.BouncyCastle.X509;
+using Org.BouncyCastle.X509.Extension;
+
+namespace KubeOps.Cli.Certificates;
+
+internal static class CertificateGenerator
+{
+ public static (X509Certificate Certificate, AsymmetricCipherKeyPair Key) CreateCaCertificate()
+ {
+ var randomGenerator = new CryptoApiRandomGenerator();
+ var random = new SecureRandom(randomGenerator);
+
+ // The Certificate Generator
+ var certificateGenerator = new X509V3CertificateGenerator();
+
+ // Serial Number
+ var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
+ certificateGenerator.SetSerialNumber(serialNumber);
+
+ // Issuer and Subject Name
+ var name = new X509Name("CN=Operator Root CA, C=DEV, L=Kubernetes");
+ certificateGenerator.SetIssuerDN(name);
+ certificateGenerator.SetSubjectDN(name);
+
+ // Valid For
+ var notBefore = DateTime.UtcNow.Date;
+ var notAfter = notBefore.AddYears(5);
+ certificateGenerator.SetNotBefore(notBefore);
+ certificateGenerator.SetNotAfter(notAfter);
+
+ // Cert Extensions
+ certificateGenerator.AddExtension(
+ X509Extensions.BasicConstraints,
+ true,
+ new BasicConstraints(true));
+ certificateGenerator.AddExtension(
+ X509Extensions.KeyUsage,
+ true,
+ new KeyUsage(KeyUsage.KeyCertSign | KeyUsage.CrlSign | KeyUsage.KeyEncipherment));
+
+ // Subject Public Key
+ const int keyStrength = 256;
+ var keyGenerator = new ECKeyPairGenerator("ECDSA");
+ keyGenerator.Init(new KeyGenerationParameters(random, keyStrength));
+ var key = keyGenerator.GenerateKeyPair();
+
+ certificateGenerator.SetPublicKey(key.Public);
+
+ var signatureFactory = new Asn1SignatureFactory("SHA512WITHECDSA", key.Private, random);
+ var certificate = certificateGenerator.Generate(signatureFactory);
+
+ return (certificate, key);
+ }
+
+ public static (X509Certificate Certificate, AsymmetricCipherKeyPair Key) CreateServerCertificate(
+ (X509Certificate Certificate, AsymmetricCipherKeyPair Key) ca, string serverName, string serverNamespace)
+ {
+ var randomGenerator = new CryptoApiRandomGenerator();
+ var random = new SecureRandom(randomGenerator);
+
+ // The Certificate Generator
+ var certificateGenerator = new X509V3CertificateGenerator();
+
+ // Serial Number
+ var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
+ certificateGenerator.SetSerialNumber(serialNumber);
+
+ // Issuer and Subject Name
+ certificateGenerator.SetIssuerDN(ca.Certificate.SubjectDN);
+ certificateGenerator.SetSubjectDN(new X509Name("CN=Operator Service, C=DEV, L=Kubernetes"));
+
+ // Valid For
+ var notBefore = DateTime.UtcNow.Date;
+ var notAfter = notBefore.AddYears(5);
+ certificateGenerator.SetNotBefore(notBefore);
+ certificateGenerator.SetNotAfter(notAfter);
+
+ // Cert Extensions
+ certificateGenerator.AddExtension(
+ X509Extensions.BasicConstraints,
+ false,
+ new BasicConstraints(false));
+ certificateGenerator.AddExtension(
+ X509Extensions.KeyUsage,
+ true,
+ new KeyUsage(KeyUsage.NonRepudiation | KeyUsage.KeyEncipherment | KeyUsage.DigitalSignature));
+ certificateGenerator.AddExtension(
+ X509Extensions.ExtendedKeyUsage,
+ false,
+ new ExtendedKeyUsage(KeyPurposeID.id_kp_clientAuth, KeyPurposeID.id_kp_serverAuth));
+ certificateGenerator.AddExtension(
+ X509Extensions.SubjectKeyIdentifier,
+ false,
+ new SubjectKeyIdentifierStructure(ca.Key.Public));
+ certificateGenerator.AddExtension(
+ X509Extensions.AuthorityKeyIdentifier,
+ false,
+ new AuthorityKeyIdentifierStructure(ca.Certificate));
+ certificateGenerator.AddExtension(
+ X509Extensions.SubjectAlternativeName,
+ false,
+ new GeneralNames(new[]
+ {
+ new GeneralName(GeneralName.DnsName, $"{serverName}.{serverNamespace}.svc"),
+ new GeneralName(GeneralName.DnsName, $"*.{serverNamespace}.svc"),
+ new GeneralName(GeneralName.DnsName, "*.svc"),
+ }));
+
+ // Subject Public Key
+ const int keyStrength = 256;
+ var keyGenerator = new ECKeyPairGenerator("ECDSA");
+ keyGenerator.Init(new KeyGenerationParameters(random, keyStrength));
+ var key = keyGenerator.GenerateKeyPair();
+
+ certificateGenerator.SetPublicKey(key.Public);
+
+ var signatureFactory = new Asn1SignatureFactory("SHA512WITHECDSA", ca.Key.Private, random);
+ var certificate = certificateGenerator.Generate(signatureFactory);
+
+ return (certificate, key);
+ }
+}
diff --git a/src/KubeOps.Cli/Certificates/Extensions.cs b/src/KubeOps.Cli/Certificates/Extensions.cs
index dfe53b83..a72bf103 100644
--- a/src/KubeOps.Cli/Certificates/Extensions.cs
+++ b/src/KubeOps.Cli/Certificates/Extensions.cs
@@ -1,22 +1,22 @@
-using System.Text;
-
-using Org.BouncyCastle.Crypto;
-using Org.BouncyCastle.OpenSsl;
-using Org.BouncyCastle.X509;
-
-namespace KubeOps.Cli.Certificates;
-
-internal static class Extensions
-{
- public static string ToPem(this X509Certificate cert) => ObjToPem(cert);
-
- public static string ToPem(this AsymmetricCipherKeyPair key) => ObjToPem(key);
-
- private static string ObjToPem(object obj)
- {
- var sb = new StringBuilder();
- using var writer = new PemWriter(new StringWriter(sb));
- writer.WriteObject(obj);
- return sb.ToString();
- }
-}
+using System.Text;
+
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.OpenSsl;
+using Org.BouncyCastle.X509;
+
+namespace KubeOps.Cli.Certificates;
+
+internal static class Extensions
+{
+ public static string ToPem(this X509Certificate cert) => ObjToPem(cert);
+
+ public static string ToPem(this AsymmetricCipherKeyPair key) => ObjToPem(key);
+
+ private static string ObjToPem(object obj)
+ {
+ var sb = new StringBuilder();
+ using var writer = new PemWriter(new StringWriter(sb));
+ writer.WriteObject(obj);
+ return sb.ToString();
+ }
+}
diff --git a/src/KubeOps.Cli/Commands/Generator/WebhookOperatorGenerator.cs b/src/KubeOps.Cli/Commands/Generator/WebhookOperatorGenerator.cs
index c2e2d313..5d3472c7 100644
--- a/src/KubeOps.Cli/Commands/Generator/WebhookOperatorGenerator.cs
+++ b/src/KubeOps.Cli/Commands/Generator/WebhookOperatorGenerator.cs
@@ -1,264 +1,265 @@
-using System.CommandLine;
-using System.CommandLine.Invocation;
-using System.Text;
-
-using k8s;
-using k8s.Models;
-
-using KubeOps.Abstractions.Kustomize;
-using KubeOps.Cli.Certificates;
-using KubeOps.Cli.Output;
-using KubeOps.Cli.Transpilation;
-using KubeOps.Transpiler;
-
-using Spectre.Console;
-
-namespace KubeOps.Cli.Commands.Generator;
-
-internal static class WebhookOperatorGenerator
-{
- public static Command Command
- {
- get
- {
- var cmd = new Command(
- "webhook-operator",
- "Generates deployments and other resources for an operator with webhooks to run.")
- {
- Options.OutputFormat,
- Options.OutputPath,
- Options.SolutionProjectRegex,
- Options.TargetFramework,
- Arguments.OperatorName,
- Arguments.SolutionOrProjectFile,
- };
- cmd.AddAlias("wh-op");
- cmd.SetHandler(ctx => Handler(AnsiConsole.Console, ctx));
-
- return cmd;
- }
- }
-
- internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
- {
- var name = ctx.ParseResult.GetValueForArgument(Arguments.OperatorName);
- var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile);
- var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath);
- var format = ctx.ParseResult.GetValueForOption(Options.OutputFormat);
-
- var result = new ResultOutput(console, format);
- console.WriteLine("Generate webhook resources.");
-
- console.MarkupLine("Generate [cyan]CA[/] certificate and private key.");
- var (caCert, caKey) = Certificates.CertificateGenerator.CreateCaCertificate();
-
- result.Add("ca.pem", caCert.ToPem(), OutputFormat.Plain);
- result.Add("ca-key.pem", caKey.ToPem(), OutputFormat.Plain);
-
- console.MarkupLine("Generate [cyan]server[/] certificate and private key.");
- var (srvCert, srvKey) = Certificates.CertificateGenerator.CreateServerCertificate(
- (caCert, caKey),
- name,
- $"{name}-system");
-
- result.Add("svc.pem", srvCert.ToPem(), OutputFormat.Plain);
- result.Add("svc-key.pem", srvKey.ToPem(), OutputFormat.Plain);
-
- console.MarkupLine("Generate [cyan]deployment[/].");
- result.Add(
- $"deployment.{format.ToString().ToLowerInvariant()}",
- new V1Deployment(
- metadata: new V1ObjectMeta(
- labels: new Dictionary { { "operator-deployment", "kubernetes-operator" } },
- name: "operator"),
- spec: new V1DeploymentSpec
- {
- Replicas = 1,
- RevisionHistoryLimit = 0,
- Selector = new V1LabelSelector(
- matchLabels:
- new Dictionary { { "operator-deployment", "kubernetes-operator" } }),
- Template = new V1PodTemplateSpec
- {
- Metadata = new V1ObjectMeta(
- labels:
- new Dictionary { { "operator-deployment", "kubernetes-operator" }, }),
- Spec = new V1PodSpec
- {
- TerminationGracePeriodSeconds = 10,
- Volumes = new List
- {
- new() { Name = "certificates", Secret = new() { SecretName = "webhook-cert" }, },
- new() { Name = "ca-certificates", Secret = new() { SecretName = "webhook-ca" }, },
- },
- Containers = new List
- {
- new()
- {
- Image = "operator",
- Name = "operator",
- VolumeMounts = new List
- {
- new()
- {
- Name = "certificates",
- MountPath = "/certs",
- ReadOnlyProperty = true,
- },
- new()
- {
- Name = "ca-certificates",
- MountPath = "/ca",
- ReadOnlyProperty = true,
- },
- },
- Env = new List
- {
- new()
- {
- Name = "POD_NAMESPACE",
- ValueFrom =
- new V1EnvVarSource
- {
- FieldRef = new V1ObjectFieldSelector
- {
- FieldPath = "metadata.namespace",
- },
- },
- },
- },
- EnvFrom =
- new List
- {
- new() { ConfigMapRef = new() { Name = "webhook-config" } },
- },
- Ports = new List { new(5001, name: "https"), },
- Resources = new V1ResourceRequirements
- {
- Requests = new Dictionary
- {
- { "cpu", new ResourceQuantity("100m") },
- { "memory", new ResourceQuantity("64Mi") },
- },
- Limits = new Dictionary
- {
- { "cpu", new ResourceQuantity("100m") },
- { "memory", new ResourceQuantity("128Mi") },
- },
- },
- },
- },
- },
- },
- }).Initialize());
-
- console.MarkupLine("Generate [cyan]service[/].");
- result.Add(
- $"service.{format.ToString().ToLowerInvariant()}",
- new V1Service(
- metadata: new V1ObjectMeta(name: "operator"),
- spec: new V1ServiceSpec
- {
- Ports =
- new List { new() { Name = "https", TargetPort = "https", Port = 443, }, },
- Selector = new Dictionary { { "operator-deployment", "kubernetes-operator" }, },
- }).Initialize());
-
- console.MarkupLine("Generate [cyan]webhook configurations[/].");
- var parser = file switch
- {
- { Extension: ".csproj", Exists: true } => await AssemblyLoader.ForProject(console, file),
- { Extension: ".sln", Exists: true } => await AssemblyLoader.ForSolution(
- console,
- file,
- ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
- ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
- { Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."),
- _ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
- };
- var validatedEntities = parser.GetValidatedEntities().ToList();
- var validatorConfig = new V1ValidatingWebhookConfiguration(
- metadata: new V1ObjectMeta(name: "validators"),
- webhooks: new List()).Initialize();
-
- foreach (var entity in validatedEntities)
- {
- var (metadata, _) = parser.ToEntityMetadata(entity);
- validatorConfig.Webhooks.Add(new V1ValidatingWebhook
- {
- Name = $"validate.{metadata.SingularName}.{metadata.Group}.{metadata.Version}",
- MatchPolicy = "Exact",
- AdmissionReviewVersions = new[] { "v1" },
- SideEffects = "None",
- Rules = new[]
- {
- new V1RuleWithOperations
- {
- Operations = new[] { "CREATE", "UPDATE", "DELETE" },
- Resources = new[] { metadata.PluralName },
- ApiGroups = new[] { metadata.Group },
- ApiVersions = new[] { metadata.Version },
- },
- },
- ClientConfig = new Admissionregistrationv1WebhookClientConfig
- {
- CaBundle = Encoding.ASCII.GetBytes(Convert.ToBase64String(Encoding.ASCII.GetBytes(caCert.ToPem()))),
- Service = new Admissionregistrationv1ServiceReference
- {
- Name = "operator", Path = $"/validate/{entity.Name.ToLowerInvariant()}",
- },
- },
- });
- }
-
- if (validatedEntities.Any())
- {
- result.Add(
- $"validators.{format.ToString().ToLowerInvariant()}", validatorConfig);
- }
-
- result.Add(
- $"kustomization.{format.ToString().ToLowerInvariant()}",
- new KustomizationConfig
- {
- Resources =
- new List
- {
- $"deployment.{format.ToString().ToLowerInvariant()}",
- $"service.{format.ToString().ToLowerInvariant()}",
- validatorConfig.Webhooks.Any()
- ? $"validators.{format.ToString().ToLowerInvariant()}"
- : string.Empty,
- }.Where(s => !string.IsNullOrWhiteSpace(s)).ToList(),
- CommonLabels = new Dictionary { { "operator-element", "operator-instance" }, },
- ConfigMapGenerator = new List
- {
- new()
- {
- Name = "webhook-config",
- Literals = new List
- {
- "KESTREL__ENDPOINTS__HTTP__URL=http://0.0.0.0:5000",
- "KESTREL__ENDPOINTS__HTTPS__URL=https://0.0.0.0:5001",
- "KESTREL__ENDPOINTS__HTTPS__CERTIFICATE__PATH=/certs/svc.pem",
- "KESTREL__ENDPOINTS__HTTPS__CERTIFICATE__KEYPATH=/certs/svc-key.pem",
- },
- },
- },
- SecretGenerator = new List
- {
- new() { Name = "webhook-ca", Files = new List { "ca.pem", "ca-key.pem", }, },
- new() { Name = "webhook-cert", Files = new List { "svc.pem", "svc-key.pem", }, },
- },
- });
-
- if (outPath is not null)
- {
- await result.Write(outPath);
- }
- else
- {
- result.Write();
- }
- }
-}
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.Text;
+
+using k8s;
+using k8s.Models;
+
+using KubeOps.Abstractions.Kustomize;
+using KubeOps.Cli.Certificates;
+using KubeOps.Cli.Output;
+using KubeOps.Cli.Transpilation;
+using KubeOps.Transpiler;
+
+using Spectre.Console;
+
+namespace KubeOps.Cli.Commands.Generator;
+
+internal static class WebhookOperatorGenerator
+{
+ public static Command Command
+ {
+ get
+ {
+ var cmd = new Command(
+ "webhook-operator",
+ "Generates deployments and other resources for an operator with webhooks to run.")
+ {
+ Options.OutputFormat,
+ Options.OutputPath,
+ Options.SolutionProjectRegex,
+ Options.TargetFramework,
+ Arguments.OperatorName,
+ Arguments.SolutionOrProjectFile,
+ };
+ cmd.AddAlias("wh-op");
+ cmd.SetHandler(ctx => Handler(AnsiConsole.Console, ctx));
+
+ return cmd;
+ }
+ }
+
+ internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
+ {
+ var name = ctx.ParseResult.GetValueForArgument(Arguments.OperatorName);
+ var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile);
+ var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath);
+ var format = ctx.ParseResult.GetValueForOption(Options.OutputFormat);
+
+ var result = new ResultOutput(console, format);
+ console.WriteLine("Generate webhook resources.");
+
+ console.MarkupLine("Generate [cyan]CA[/] certificate and private key.");
+ var (caCert, caKey) = Certificates.CertificateGenerator.CreateCaCertificate();
+
+ result.Add("ca.pem", caCert.ToPem(), OutputFormat.Plain);
+ result.Add("ca-key.pem", caKey.ToPem(), OutputFormat.Plain);
+
+ console.MarkupLine("Generate [cyan]server[/] certificate and private key.");
+ var (srvCert, srvKey) = Certificates.CertificateGenerator.CreateServerCertificate(
+ (caCert, caKey),
+ name,
+ $"{name}-system");
+
+ result.Add("svc.pem", srvCert.ToPem(), OutputFormat.Plain);
+ result.Add("svc-key.pem", srvKey.ToPem(), OutputFormat.Plain);
+
+ console.MarkupLine("Generate [cyan]deployment[/].");
+ result.Add(
+ $"deployment.{format.ToString().ToLowerInvariant()}",
+ new V1Deployment(
+ metadata: new V1ObjectMeta(
+ labels: new Dictionary { { "operator-deployment", "kubernetes-operator" } },
+ name: "operator"),
+ spec: new V1DeploymentSpec
+ {
+ Replicas = 1,
+ RevisionHistoryLimit = 0,
+ Selector = new V1LabelSelector(
+ matchLabels:
+ new Dictionary { { "operator-deployment", "kubernetes-operator" } }),
+ Template = new V1PodTemplateSpec
+ {
+ Metadata = new V1ObjectMeta(
+ labels:
+ new Dictionary { { "operator-deployment", "kubernetes-operator" }, }),
+ Spec = new V1PodSpec
+ {
+ TerminationGracePeriodSeconds = 10,
+ Volumes = new List
+ {
+ new() { Name = "certificates", Secret = new() { SecretName = "webhook-cert" }, },
+ new() { Name = "ca-certificates", Secret = new() { SecretName = "webhook-ca" }, },
+ },
+ Containers = new List
+ {
+ new()
+ {
+ Image = "operator",
+ Name = "operator",
+ VolumeMounts = new List
+ {
+ new()
+ {
+ Name = "certificates",
+ MountPath = "/certs",
+ ReadOnlyProperty = true,
+ },
+ new()
+ {
+ Name = "ca-certificates",
+ MountPath = "/ca",
+ ReadOnlyProperty = true,
+ },
+ },
+ Env = new List
+ {
+ new()
+ {
+ Name = "POD_NAMESPACE",
+ ValueFrom =
+ new V1EnvVarSource
+ {
+ FieldRef = new V1ObjectFieldSelector
+ {
+ FieldPath = "metadata.namespace",
+ },
+ },
+ },
+ },
+ EnvFrom =
+ new List
+ {
+ new() { ConfigMapRef = new() { Name = "webhook-config" } },
+ },
+ Ports = new List { new(5001, name: "https"), },
+ Resources = new V1ResourceRequirements
+ {
+ Requests = new Dictionary
+ {
+ { "cpu", new ResourceQuantity("100m") },
+ { "memory", new ResourceQuantity("64Mi") },
+ },
+ Limits = new Dictionary
+ {
+ { "cpu", new ResourceQuantity("100m") },
+ { "memory", new ResourceQuantity("128Mi") },
+ },
+ },
+ },
+ },
+ },
+ },
+ }).Initialize());
+
+ console.MarkupLine("Generate [cyan]service[/].");
+ result.Add(
+ $"service.{format.ToString().ToLowerInvariant()}",
+ new V1Service(
+ metadata: new V1ObjectMeta(name: "operator"),
+ spec: new V1ServiceSpec
+ {
+ Ports =
+ new List { new() { Name = "https", TargetPort = "https", Port = 443, }, },
+ Selector = new Dictionary { { "operator-deployment", "kubernetes-operator" }, },
+ }).Initialize());
+
+ console.MarkupLine("Generate [cyan]webhook configurations[/].");
+ var parser = file switch
+ {
+ { Extension: ".csproj", Exists: true } => await AssemblyLoader.ForProject(console, file),
+ { Extension: ".sln", Exists: true } => await AssemblyLoader.ForSolution(
+ console,
+ file,
+ ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
+ ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
+ { Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."),
+ _ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
+ };
+ var validatedEntities = parser.GetValidatedEntities().ToList();
+ var validatorConfig = new V1ValidatingWebhookConfiguration(
+ metadata: new V1ObjectMeta(name: "validators"),
+ webhooks: new List()).Initialize();
+
+ foreach (var entity in validatedEntities)
+ {
+ var (metadata, _) = parser.ToEntityMetadata(entity);
+ validatorConfig.Webhooks.Add(new V1ValidatingWebhook
+ {
+ Name = $"validate.{metadata.SingularName}.{metadata.Group}.{metadata.Version}",
+ MatchPolicy = "Exact",
+ AdmissionReviewVersions = new[] { "v1" },
+ SideEffects = "None",
+ Rules = new[]
+ {
+ new V1RuleWithOperations
+ {
+ Operations = new[] { "CREATE", "UPDATE", "DELETE" },
+ Resources = new[] { metadata.PluralName },
+ ApiGroups = new[] { metadata.Group },
+ ApiVersions = new[] { metadata.Version },
+ },
+ },
+ ClientConfig = new Admissionregistrationv1WebhookClientConfig
+ {
+ CaBundle = Encoding.ASCII.GetBytes(Convert.ToBase64String(Encoding.ASCII.GetBytes(caCert.ToPem()))),
+ Service = new Admissionregistrationv1ServiceReference
+ {
+ Name = "operator",
+ Path = $"/validate/{entity.Name.ToLowerInvariant()}",
+ },
+ },
+ });
+ }
+
+ if (validatedEntities.Any())
+ {
+ result.Add(
+ $"validators.{format.ToString().ToLowerInvariant()}", validatorConfig);
+ }
+
+ result.Add(
+ $"kustomization.{format.ToString().ToLowerInvariant()}",
+ new KustomizationConfig
+ {
+ Resources =
+ new List
+ {
+ $"deployment.{format.ToString().ToLowerInvariant()}",
+ $"service.{format.ToString().ToLowerInvariant()}",
+ validatorConfig.Webhooks.Any()
+ ? $"validators.{format.ToString().ToLowerInvariant()}"
+ : string.Empty,
+ }.Where(s => !string.IsNullOrWhiteSpace(s)).ToList(),
+ CommonLabels = new Dictionary { { "operator-element", "operator-instance" }, },
+ ConfigMapGenerator = new List
+ {
+ new()
+ {
+ Name = "webhook-config",
+ Literals = new List
+ {
+ "KESTREL__ENDPOINTS__HTTP__URL=http://0.0.0.0:5000",
+ "KESTREL__ENDPOINTS__HTTPS__URL=https://0.0.0.0:5001",
+ "KESTREL__ENDPOINTS__HTTPS__CERTIFICATE__PATH=/certs/svc.pem",
+ "KESTREL__ENDPOINTS__HTTPS__CERTIFICATE__KEYPATH=/certs/svc-key.pem",
+ },
+ },
+ },
+ SecretGenerator = new List
+ {
+ new() { Name = "webhook-ca", Files = new List { "ca.pem", "ca-key.pem", }, },
+ new() { Name = "webhook-cert", Files = new List { "svc.pem", "svc-key.pem", }, },
+ },
+ });
+
+ if (outPath is not null)
+ {
+ await result.Write(outPath);
+ }
+ else
+ {
+ result.Write();
+ }
+ }
+}
diff --git a/src/KubeOps.Cli/Commands/Management/Install.cs b/src/KubeOps.Cli/Commands/Management/Install.cs
index ca94b0b3..ed16f0ba 100644
--- a/src/KubeOps.Cli/Commands/Management/Install.cs
+++ b/src/KubeOps.Cli/Commands/Management/Install.cs
@@ -78,7 +78,7 @@ internal static async Task Handler(IAnsiConsole console, IKubernetes client, Inv
case { Items: [var existing] }:
console.MarkupLineInterpolated(
$"""[yellow]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" already exists.[/]""");
- if (!force && console.Confirm("[yellow]Should the CRD be overwritten?[/]"))
+ if (!force && !console.Confirm("[yellow]Should the CRD be overwritten?[/]"))
{
ctx.ExitCode = ExitCodes.Aborted;
return;
diff --git a/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs b/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs
index 84273738..98cdc3b3 100644
--- a/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs
+++ b/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs
@@ -1,177 +1,177 @@
-using System.Reflection;
-using System.Text;
-using System.Text.RegularExpressions;
-
-using k8s.Models;
-
-using KubeOps.Abstractions.Entities.Attributes;
-using KubeOps.Abstractions.Rbac;
-using KubeOps.Operator.Web.Webhooks.Validation;
-using KubeOps.Transpiler;
-
-using Microsoft.Build.Locator;
-using Microsoft.CodeAnalysis.MSBuild;
-
-using Spectre.Console;
-
-namespace KubeOps.Cli.Transpilation;
-
-///
-/// AssemblyLoader.
-///
-internal static partial class AssemblyLoader
-{
- static AssemblyLoader()
- {
- MSBuildLocator.RegisterDefaults();
- }
-
- public static Task ForProject(
- IAnsiConsole console,
- FileInfo projectFile)
- => console.Status().StartAsync($"Compiling {projectFile.Name}...", async _ =>
- {
- console.MarkupLineInterpolated($"Compile project [aqua]{projectFile.FullName}[/].");
- using var workspace = MSBuildWorkspace.Create();
- workspace.SkipUnrecognizedProjects = true;
- workspace.LoadMetadataForReferencedProjects = true;
- console.WriteLine("Load project.");
- var project = await workspace.OpenProjectAsync(projectFile.FullName);
- console.MarkupLine("[green]Project loaded.[/]");
- console.WriteLine("Load compilation context.");
- var compilation = await project.GetCompilationAsync();
- console.MarkupLine("[green]Compilation context loaded.[/]");
- if (compilation is null)
- {
- throw new AggregateException("Compilation could not be found.");
- }
-
- using var assemblyStream = new MemoryStream();
- console.WriteLine("Start compilation.");
- switch (compilation.Emit(assemblyStream))
- {
- case { Success: false, Diagnostics: var diag }:
- throw new AggregateException(
- $"Compilation failed: {diag.Aggregate(new StringBuilder(), (sb, d) => sb.AppendLine(d.ToString()))}");
- }
-
- console.MarkupLine("[green]Compilation successful.[/]");
- console.WriteLine();
- var mlc = new MetadataLoadContext(
- new PathAssemblyResolver(project.MetadataReferences.Select(m => m.Display ?? string.Empty)
- .Concat(new[] { typeof(object).Assembly.Location })));
- mlc.LoadFromByteArray(assemblyStream.ToArray());
-
- return mlc;
- });
-
- public static Task ForSolution(
- IAnsiConsole console,
- FileInfo slnFile,
- Regex? projectFilter = null,
- string? tfm = null)
- => console.Status().StartAsync($"Compiling {slnFile.Name}...", async _ =>
- {
- projectFilter ??= DefaultRegex();
- tfm ??= "latest";
-
- console.MarkupLineInterpolated($"Compile solution [aqua]{slnFile.FullName}[/].");
-#pragma warning disable RCS1097
- console.MarkupLineInterpolated($"[grey]With project filter:[/] {projectFilter.ToString()}");
-#pragma warning restore RCS1097
- console.MarkupLineInterpolated($"[grey]With Target Platform:[/] {tfm}");
-
- using var workspace = MSBuildWorkspace.Create();
- workspace.SkipUnrecognizedProjects = true;
- workspace.LoadMetadataForReferencedProjects = true;
- console.WriteLine("Load solution.");
- var solution = await workspace.OpenSolutionAsync(slnFile.FullName);
- console.MarkupLine("[green]Solution loaded.[/]");
-
- var assemblies = await Task.WhenAll(solution.Projects
- .Select(p =>
- {
- var name = TfmComparer.TfmRegex().Replace(p.Name, string.Empty);
- var tfm = TfmComparer.TfmRegex().Match(p.Name).Groups["tfm"].Value;
- return (name, tfm, project: p);
- })
- .Where(p => projectFilter.IsMatch(p.name))
- .Where(p => tfm == "latest" || p.tfm.Length == 0 || p.tfm == tfm)
- .OrderByDescending(p => p.tfm, new TfmComparer())
- .GroupBy(p => p.name)
- .Select(p => p.FirstOrDefault())
- .Where(p => p != default)
- .Select(async p =>
- {
- console.MarkupLineInterpolated(
- $"Load compilation context for [aqua]{p.name}[/]{(p.tfm.Length > 0 ? $" [grey]{p.tfm}[/]" : string.Empty)}.");
- var compilation = await p.project.GetCompilationAsync();
- console.MarkupLineInterpolated($"[green]Compilation context loaded for {p.name}.[/]");
- if (compilation is null)
- {
- throw new AggregateException("Compilation could not be found.");
- }
-
- using var assemblyStream = new MemoryStream();
- console.MarkupLineInterpolated(
- $"Start compilation for [aqua]{p.name}[/]{(p.tfm.Length > 0 ? $" [grey]{p.tfm}[/]" : string.Empty)}.");
- switch (compilation.Emit(assemblyStream))
- {
- case { Success: false, Diagnostics: var diag }:
- throw new AggregateException(
- $"Compilation failed: {diag.Aggregate(new StringBuilder(), (sb, d) => sb.AppendLine(d.ToString()))}");
- }
-
- console.MarkupLineInterpolated($"[green]Compilation successful for {p.name}.[/]");
- return (Assembly: assemblyStream.ToArray(),
- Refs: p.project.MetadataReferences.Select(m => m.Display ?? string.Empty));
- }));
-
- console.WriteLine();
- var mlc = new MetadataLoadContext(
- new PathAssemblyResolver(assemblies.SelectMany(a => a.Refs)
- .Concat(new[] { typeof(object).Assembly.Location }).Distinct()));
- foreach (var assembly in assemblies)
- {
- mlc.LoadFromByteArray(assembly.Assembly);
- }
-
- return mlc;
- });
-
- public static IEnumerable GetEntities(this MetadataLoadContext context) => context.GetAssemblies()
- .SelectMany(a => a.DefinedTypes)
- .Select(t => (t, attrs: CustomAttributeData.GetCustomAttributes(t)))
- .Where(e => e.attrs.Any(a => a.AttributeType.Name == nameof(KubernetesEntityAttribute)) &&
- e.attrs.All(a => a.AttributeType.Name != nameof(IgnoreAttribute)))
- .Select(e => e.t);
-
- public static IEnumerable GetRbacAttributes(this MetadataLoadContext context)
- {
- foreach (var type in context.GetAssemblies()
- .SelectMany(a => a.DefinedTypes)
- .SelectMany(t =>
- t.GetCustomAttributesData()))
- {
- yield return type;
- }
-
- foreach (var type in context.GetAssemblies()
- .SelectMany(a => a.DefinedTypes)
- .SelectMany(t =>
- t.GetCustomAttributesData()))
- {
- yield return type;
- }
- }
-
- public static IEnumerable GetValidatedEntities(this MetadataLoadContext context) => context.GetAssemblies()
- .SelectMany(a => a.DefinedTypes)
- .Where(t => t.BaseType?.Name == typeof(ValidationWebhook<>).Name &&
- t.BaseType?.Namespace == typeof(ValidationWebhook<>).Namespace)
- .Select(t => t.BaseType!.GenericTypeArguments[0])
- .Distinct();
-
- [GeneratedRegex(".*")]
- private static partial Regex DefaultRegex();
-}
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+
+using k8s.Models;
+
+using KubeOps.Abstractions.Entities.Attributes;
+using KubeOps.Abstractions.Rbac;
+using KubeOps.Operator.Web.Webhooks.Validation;
+using KubeOps.Transpiler;
+
+using Microsoft.Build.Locator;
+using Microsoft.CodeAnalysis.MSBuild;
+
+using Spectre.Console;
+
+namespace KubeOps.Cli.Transpilation;
+
+///
+/// AssemblyLoader.
+///
+internal static partial class AssemblyLoader
+{
+ static AssemblyLoader()
+ {
+ MSBuildLocator.RegisterDefaults();
+ }
+
+ public static Task ForProject(
+ IAnsiConsole console,
+ FileInfo projectFile)
+ => console.Status().StartAsync($"Compiling {projectFile.Name}...", async _ =>
+ {
+ console.MarkupLineInterpolated($"Compile project [aqua]{projectFile.FullName}[/].");
+ using var workspace = MSBuildWorkspace.Create();
+ workspace.SkipUnrecognizedProjects = true;
+ workspace.LoadMetadataForReferencedProjects = true;
+ console.WriteLine("Load project.");
+ var project = await workspace.OpenProjectAsync(projectFile.FullName);
+ console.MarkupLine("[green]Project loaded.[/]");
+ console.WriteLine("Load compilation context.");
+ var compilation = await project.GetCompilationAsync();
+ console.MarkupLine("[green]Compilation context loaded.[/]");
+ if (compilation is null)
+ {
+ throw new AggregateException("Compilation could not be found.");
+ }
+
+ using var assemblyStream = new MemoryStream();
+ console.WriteLine("Start compilation.");
+ switch (compilation.Emit(assemblyStream))
+ {
+ case { Success: false, Diagnostics: var diag }:
+ throw new AggregateException(
+ $"Compilation failed: {diag.Aggregate(new StringBuilder(), (sb, d) => sb.AppendLine(d.ToString()))}");
+ }
+
+ console.MarkupLine("[green]Compilation successful.[/]");
+ console.WriteLine();
+ var mlc = new MetadataLoadContext(
+ new PathAssemblyResolver(project.MetadataReferences.Select(m => m.Display ?? string.Empty)
+ .Concat(new[] { typeof(object).Assembly.Location })));
+ mlc.LoadFromByteArray(assemblyStream.ToArray());
+
+ return mlc;
+ });
+
+ public static Task ForSolution(
+ IAnsiConsole console,
+ FileInfo slnFile,
+ Regex? projectFilter = null,
+ string? tfm = null)
+ => console.Status().StartAsync($"Compiling {slnFile.Name}...", async _ =>
+ {
+ projectFilter ??= DefaultRegex();
+ tfm ??= "latest";
+
+ console.MarkupLineInterpolated($"Compile solution [aqua]{slnFile.FullName}[/].");
+#pragma warning disable RCS1097
+ console.MarkupLineInterpolated($"[grey]With project filter:[/] {projectFilter.ToString()}");
+#pragma warning restore RCS1097
+ console.MarkupLineInterpolated($"[grey]With Target Platform:[/] {tfm}");
+
+ using var workspace = MSBuildWorkspace.Create();
+ workspace.SkipUnrecognizedProjects = true;
+ workspace.LoadMetadataForReferencedProjects = true;
+ console.WriteLine("Load solution.");
+ var solution = await workspace.OpenSolutionAsync(slnFile.FullName);
+ console.MarkupLine("[green]Solution loaded.[/]");
+
+ var assemblies = await Task.WhenAll(solution.Projects
+ .Select(p =>
+ {
+ var name = TfmComparer.TfmRegex().Replace(p.Name, string.Empty);
+ var tfm = TfmComparer.TfmRegex().Match(p.Name).Groups["tfm"].Value;
+ return (name, tfm, project: p);
+ })
+ .Where(p => projectFilter.IsMatch(p.name))
+ .Where(p => tfm == "latest" || p.tfm.Length == 0 || p.tfm == tfm)
+ .OrderByDescending(p => p.tfm, new TfmComparer())
+ .GroupBy(p => p.name)
+ .Select(p => p.FirstOrDefault())
+ .Where(p => p != default)
+ .Select(async p =>
+ {
+ console.MarkupLineInterpolated(
+ $"Load compilation context for [aqua]{p.name}[/]{(p.tfm.Length > 0 ? $" [grey]{p.tfm}[/]" : string.Empty)}.");
+ var compilation = await p.project.GetCompilationAsync();
+ console.MarkupLineInterpolated($"[green]Compilation context loaded for {p.name}.[/]");
+ if (compilation is null)
+ {
+ throw new AggregateException("Compilation could not be found.");
+ }
+
+ using var assemblyStream = new MemoryStream();
+ console.MarkupLineInterpolated(
+ $"Start compilation for [aqua]{p.name}[/]{(p.tfm.Length > 0 ? $" [grey]{p.tfm}[/]" : string.Empty)}.");
+ switch (compilation.Emit(assemblyStream))
+ {
+ case { Success: false, Diagnostics: var diag }:
+ throw new AggregateException(
+ $"Compilation failed: {diag.Aggregate(new StringBuilder(), (sb, d) => sb.AppendLine(d.ToString()))}");
+ }
+
+ console.MarkupLineInterpolated($"[green]Compilation successful for {p.name}.[/]");
+ return (Assembly: assemblyStream.ToArray(),
+ Refs: p.project.MetadataReferences.Select(m => m.Display ?? string.Empty));
+ }));
+
+ console.WriteLine();
+ var mlc = new MetadataLoadContext(
+ new PathAssemblyResolver(assemblies.SelectMany(a => a.Refs)
+ .Concat(new[] { typeof(object).Assembly.Location }).Distinct()));
+ foreach (var assembly in assemblies)
+ {
+ mlc.LoadFromByteArray(assembly.Assembly);
+ }
+
+ return mlc;
+ });
+
+ public static IEnumerable GetEntities(this MetadataLoadContext context) => context.GetAssemblies()
+ .SelectMany(a => a.DefinedTypes)
+ .Select(t => (t, attrs: CustomAttributeData.GetCustomAttributes(t)))
+ .Where(e => e.attrs.Any(a => a.AttributeType.Name == nameof(KubernetesEntityAttribute)) &&
+ e.attrs.All(a => a.AttributeType.Name != nameof(IgnoreAttribute)))
+ .Select(e => e.t);
+
+ public static IEnumerable GetRbacAttributes(this MetadataLoadContext context)
+ {
+ foreach (var type in context.GetAssemblies()
+ .SelectMany(a => a.DefinedTypes)
+ .SelectMany(t =>
+ t.GetCustomAttributesData()))
+ {
+ yield return type;
+ }
+
+ foreach (var type in context.GetAssemblies()
+ .SelectMany(a => a.DefinedTypes)
+ .SelectMany(t =>
+ t.GetCustomAttributesData()))
+ {
+ yield return type;
+ }
+ }
+
+ public static IEnumerable GetValidatedEntities(this MetadataLoadContext context) => context.GetAssemblies()
+ .SelectMany(a => a.DefinedTypes)
+ .Where(t => t.BaseType?.Name == typeof(ValidationWebhook<>).Name &&
+ t.BaseType?.Namespace == typeof(ValidationWebhook<>).Namespace)
+ .Select(t => t.BaseType!.GenericTypeArguments[0])
+ .Distinct();
+
+ [GeneratedRegex(".*")]
+ private static partial Regex DefaultRegex();
+}
diff --git a/src/KubeOps.Cli/Transpilation/TfmComparer.cs b/src/KubeOps.Cli/Transpilation/TfmComparer.cs
index a965232d..b35fc4c4 100644
--- a/src/KubeOps.Cli/Transpilation/TfmComparer.cs
+++ b/src/KubeOps.Cli/Transpilation/TfmComparer.cs
@@ -1,58 +1,58 @@
-using System.Text.RegularExpressions;
-
-namespace KubeOps.Cli.Transpilation;
-
-///
-/// Tfm Comparer.
-///
-internal sealed partial class TfmComparer : IComparer
-{
- [GeneratedRegex(
- "[(]?(?(?(netcoreapp|net|netstandard){1})(?[0-9]+)[.](?[0-9]+))[)]?",
- RegexOptions.Compiled)]
- public static partial Regex TfmRegex();
-
- public int Compare(string? x, string? y)
- {
- if (x == null || y == null)
- {
- return StringComparer.CurrentCulture.Compare(x, y);
- }
-
- switch (TfmRegex().Match(x), TfmRegex().Match(y))
- {
- case ({ Success: false }, _) or (_, { Success: false }):
- return StringComparer.CurrentCulture.Compare(x, y);
- case ({ } matchX, { } matchY):
- var platformX = matchX.Groups["name"].Value;
- var platformY = matchY.Groups["name"].Value;
- if (platformX != platformY)
- {
- return (platformX, platformY) switch
- {
- ("netstandard", _) or (_, "net") => -1,
- (_, "netstandard") or ("net", _) => 1,
- _ => 0,
- };
- }
-
- var majorX = matchX.Groups["major"].Value;
- var majorY = matchY.Groups["major"].Value;
- if (majorX != majorY)
- {
- return int.Parse(majorX) - int.Parse(majorY);
- }
-
- var minorX = matchX.Groups["minor"].Value;
- var minorY = matchY.Groups["minor"].Value;
- if (minorX != minorY)
- {
- return int.Parse(minorX) - int.Parse(minorY);
- }
-
- return 0;
- default:
- return 0;
- }
- }
-}
+using System.Text.RegularExpressions;
+
+namespace KubeOps.Cli.Transpilation;
+
+///
+/// Tfm Comparer.
+///
+internal sealed partial class TfmComparer : IComparer
+{
+ [GeneratedRegex(
+ "[(]?(?(?(netcoreapp|net|netstandard){1})(?[0-9]+)[.](?[0-9]+))[)]?",
+ RegexOptions.Compiled)]
+ public static partial Regex TfmRegex();
+
+ public int Compare(string? x, string? y)
+ {
+ if (x == null || y == null)
+ {
+ return StringComparer.CurrentCulture.Compare(x, y);
+ }
+
+ switch (TfmRegex().Match(x), TfmRegex().Match(y))
+ {
+ case ({ Success: false }, _) or (_, { Success: false }):
+ return StringComparer.CurrentCulture.Compare(x, y);
+ case ({ } matchX, { } matchY):
+ var platformX = matchX.Groups["name"].Value;
+ var platformY = matchY.Groups["name"].Value;
+ if (platformX != platformY)
+ {
+ return (platformX, platformY) switch
+ {
+ ("netstandard", _) or (_, "net") => -1,
+ (_, "netstandard") or ("net", _) => 1,
+ _ => 0,
+ };
+ }
+
+ var majorX = matchX.Groups["major"].Value;
+ var majorY = matchY.Groups["major"].Value;
+ if (majorX != majorY)
+ {
+ return int.Parse(majorX) - int.Parse(majorY);
+ }
+
+ var minorX = matchX.Groups["minor"].Value;
+ var minorY = matchY.Groups["minor"].Value;
+ if (minorX != minorY)
+ {
+ return int.Parse(minorX) - int.Parse(minorY);
+ }
+
+ return 0;
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj b/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj
index ffc03a4e..be9e3c20 100644
--- a/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj
+++ b/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj
@@ -24,6 +24,10 @@
+
+
+
+
build/
diff --git a/src/KubeOps.Operator.Web/README.md b/src/KubeOps.Operator.Web/README.md
index 0cfc3782..1e6de779 100644
--- a/src/KubeOps.Operator.Web/README.md
+++ b/src/KubeOps.Operator.Web/README.md
@@ -86,7 +86,43 @@ like "normal" `IActionResult` creation methods.
## Mutation Hooks
-TODO.
+To create a mutation webhook, first create a new class
+that implements the `MutationWebhook` base class.
+Then decorate the webhook with the `MutationWebhookAttribute`
+to set the route correctly.
+
+After that setup, you may overwrite any of the following methods:
+
+- Create
+- CreateAsync
+- Update
+- UpdateAsync
+- Delete
+- DeleteAsync
+
+The async methods take precedence over the sync methods.
+
+An example of such a mutation webhook looks like:
+
+```csharp
+[MutationWebhook(typeof(V1TestEntity))]
+public class TestMutationWebhook : MutationWebhook
+{
+ public override MutationResult Create(V1TestEntity entity, bool dryRun)
+ {
+ if (entity.Spec.Username == "overwrite")
+ {
+ entity.Spec.Username = "random overwritten";
+ return Modified(entity);
+ }
+
+ return NoChanges();
+ }
+}
+```
+
+To create the mutation results, use the `protected` methods (`NoChanges`, `Modified`, and `Fail`)
+like "normal" `IActionResult` creation methods.
## Conversion Hooks
diff --git a/src/KubeOps.Operator.Web/Webhooks/AdmissionRequest.cs b/src/KubeOps.Operator.Web/Webhooks/AdmissionRequest.cs
index d4de8d21..eb64862e 100644
--- a/src/KubeOps.Operator.Web/Webhooks/AdmissionRequest.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/AdmissionRequest.cs
@@ -1,20 +1,20 @@
-using System.Text.Json.Serialization;
-
-using k8s;
-using k8s.Models;
-
-namespace KubeOps.Operator.Web.Webhooks;
-
-///
-/// Incoming admission request for a webhook.
-///
-/// The type of the entity.
-public sealed class AdmissionRequest : AdmissionReview
- where TEntity : IKubernetesObject
-{
- ///
- /// Admission request data.
- ///
- [JsonPropertyName("request")]
- public AdmissionRequestData Request { get; init; } = new();
-}
+using System.Text.Json.Serialization;
+
+using k8s;
+using k8s.Models;
+
+namespace KubeOps.Operator.Web.Webhooks;
+
+///
+/// Incoming admission request for a webhook.
+///
+/// The type of the entity.
+public sealed class AdmissionRequest : AdmissionReview
+ where TEntity : IKubernetesObject
+{
+ ///
+ /// Admission request data.
+ ///
+ [JsonPropertyName("request")]
+ public AdmissionRequestData Request { get; init; } = new();
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/AdmissionRequestData.cs b/src/KubeOps.Operator.Web/Webhooks/AdmissionRequestData.cs
index 73c8839f..9aae3919 100644
--- a/src/KubeOps.Operator.Web/Webhooks/AdmissionRequestData.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/AdmissionRequestData.cs
@@ -1,48 +1,48 @@
-using System.Text.Json.Serialization;
-
-using k8s;
-using k8s.Models;
-
-namespace KubeOps.Operator.Web.Webhooks;
-
-///
-/// Data for an incoming admission request.
-///
-/// The type of the entity.
-public sealed class AdmissionRequestData
- where TEntity : IKubernetesObject
-{
- ///
- /// The unique ID of the admission request.
- ///
- [JsonPropertyName("uid")]
- public string Uid { get; init; } = string.Empty;
-
- ///
- /// The operation that is used.
- /// Valid values are: "CREATE", "UPDATE", "DELETE".
- /// "CONNECT" does exist, but is not supported by the operator-sdk.
- ///
- [JsonPropertyName("operation")]
- public string Operation { get; init; } = string.Empty;
-
- ///
- /// If set, the object that is passed to the webhook.
- /// This is set in CREATE and UPDATE operations.
- ///
- [JsonPropertyName("object")]
- public TEntity? Object { get; init; }
-
- ///
- /// If set, the old object that is passed to the webhook.
- /// This is set in UPDATE and DELETE operations.
- ///
- [JsonPropertyName("oldObject")]
- public TEntity? OldObject { get; init; }
-
- ///
- /// A flag to indicate if the API was called with the "dryRun" flag.
- ///
- [JsonPropertyName("dryRun")]
- public bool DryRun { get; init; }
-}
+using System.Text.Json.Serialization;
+
+using k8s;
+using k8s.Models;
+
+namespace KubeOps.Operator.Web.Webhooks;
+
+///
+/// Data for an incoming admission request.
+///
+/// The type of the entity.
+public sealed class AdmissionRequestData
+ where TEntity : IKubernetesObject
+{
+ ///
+ /// The unique ID of the admission request.
+ ///
+ [JsonPropertyName("uid")]
+ public string Uid { get; init; } = string.Empty;
+
+ ///
+ /// The operation that is used.
+ /// Valid values are: "CREATE", "UPDATE", "DELETE".
+ /// "CONNECT" does exist, but is not supported by the operator-sdk.
+ ///
+ [JsonPropertyName("operation")]
+ public string Operation { get; init; } = string.Empty;
+
+ ///
+ /// If set, the object that is passed to the webhook.
+ /// This is set in CREATE and UPDATE operations.
+ ///
+ [JsonPropertyName("object")]
+ public TEntity? Object { get; init; }
+
+ ///
+ /// If set, the old object that is passed to the webhook.
+ /// This is set in UPDATE and DELETE operations.
+ ///
+ [JsonPropertyName("oldObject")]
+ public TEntity? OldObject { get; init; }
+
+ ///
+ /// A flag to indicate if the API was called with the "dryRun" flag.
+ ///
+ [JsonPropertyName("dryRun")]
+ public bool DryRun { get; init; }
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/AdmissionResponse.cs b/src/KubeOps.Operator.Web/Webhooks/AdmissionResponse.cs
index 2e586a98..bc8774a4 100644
--- a/src/KubeOps.Operator.Web/Webhooks/AdmissionResponse.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/AdmissionResponse.cs
@@ -1,20 +1,20 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace KubeOps.Operator.Web.Webhooks;
-
-internal sealed class AdmissionResponse : AdmissionReview
-{
- public static readonly JsonSerializerOptions SerializerOptions = new()
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- ReferenceHandler = ReferenceHandler.IgnoreCycles,
- };
-
- [JsonPropertyName("response")]
-#if NET7_0_OR_GREATER
- public required AdmissionResponseData Response { get; init; }
-#else
- public AdmissionResponseData Response { get; init; } = new();
-#endif
-}
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace KubeOps.Operator.Web.Webhooks;
+
+internal sealed class AdmissionResponse : AdmissionReview
+{
+ public static readonly JsonSerializerOptions SerializerOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ ReferenceHandler = ReferenceHandler.IgnoreCycles,
+ };
+
+ [JsonPropertyName("response")]
+#if NET7_0_OR_GREATER
+ public required AdmissionResponseData Response { get; init; }
+#else
+ public AdmissionResponseData Response { get; init; } = new();
+#endif
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/AdmissionResponseData.cs b/src/KubeOps.Operator.Web/Webhooks/AdmissionResponseData.cs
index 416a09b6..95db5729 100644
--- a/src/KubeOps.Operator.Web/Webhooks/AdmissionResponseData.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/AdmissionResponseData.cs
@@ -1,22 +1,28 @@
-using System.Text.Json.Serialization;
-
-namespace KubeOps.Operator.Web.Webhooks;
-
-internal sealed class AdmissionResponseData
-{
- [JsonPropertyName("uid")]
-#if NET7_0_OR_GREATER
- public required string Uid { get; init; }
-#else
- public string Uid { get; init; } = string.Empty;
-#endif
-
- [JsonPropertyName("allowed")]
- public bool Allowed { get; init; }
-
- [JsonPropertyName("status")]
- public AdmissionStatus? Status { get; init; }
-
- [JsonPropertyName("warnings")]
- public string[]? Warnings { get; init; }
-}
+using System.Text.Json.Serialization;
+
+namespace KubeOps.Operator.Web.Webhooks;
+
+internal sealed class AdmissionResponseData
+{
+ [JsonPropertyName("uid")]
+#if NET7_0_OR_GREATER
+ public required string Uid { get; init; }
+#else
+ public string Uid { get; init; } = string.Empty;
+#endif
+
+ [JsonPropertyName("allowed")]
+ public bool Allowed { get; init; }
+
+ [JsonPropertyName("status")]
+ public AdmissionStatus? Status { get; init; }
+
+ [JsonPropertyName("warnings")]
+ public string[]? Warnings { get; init; }
+
+ [JsonPropertyName("patch")]
+ public string? Patch { get; init; }
+
+ [JsonPropertyName("patchType")]
+ public string? PatchType { get; init; }
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/AdmissionReview.cs b/src/KubeOps.Operator.Web/Webhooks/AdmissionReview.cs
index d04fdbcc..077455ac 100644
--- a/src/KubeOps.Operator.Web/Webhooks/AdmissionReview.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/AdmissionReview.cs
@@ -1,15 +1,15 @@
-using System.Text.Json.Serialization;
-
-namespace KubeOps.Operator.Web.Webhooks;
-
-///
-/// Base class for admission review requests.
-///
-public abstract class AdmissionReview
-{
- [JsonPropertyName("apiVersion")]
- public string ApiVersion => "admission.k8s.io/v1";
-
- [JsonPropertyName("kind")]
- public string Kind => "AdmissionReview";
-}
+using System.Text.Json.Serialization;
+
+namespace KubeOps.Operator.Web.Webhooks;
+
+///
+/// Base class for admission review requests.
+///
+public abstract class AdmissionReview
+{
+ [JsonPropertyName("apiVersion")]
+ public string ApiVersion => "admission.k8s.io/v1";
+
+ [JsonPropertyName("kind")]
+ public string Kind => "AdmissionReview";
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/AdmissionStatus.cs b/src/KubeOps.Operator.Web/Webhooks/AdmissionStatus.cs
index a3b85a15..e543b86b 100644
--- a/src/KubeOps.Operator.Web/Webhooks/AdmissionStatus.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/AdmissionStatus.cs
@@ -1,10 +1,10 @@
-using Microsoft.AspNetCore.Http;
-
-namespace KubeOps.Operator.Web.Webhooks;
-
-///
-/// The admission status for the response to the API.
-///
-/// A message that is passed to the API.
-/// A custom status code to provide more detailed information.
-public record AdmissionStatus(string Message, int? Code = StatusCodes.Status200OK);
+using Microsoft.AspNetCore.Http;
+
+namespace KubeOps.Operator.Web.Webhooks;
+
+///
+/// The admission status for the response to the API.
+///
+/// A message that is passed to the API.
+/// A custom status code to provide more detailed information.
+public record AdmissionStatus(string Message, int? Code = StatusCodes.Status200OK);
diff --git a/src/KubeOps.Operator.Web/Webhooks/Mutation/JsonDiffer.cs b/src/KubeOps.Operator.Web/Webhooks/Mutation/JsonDiffer.cs
new file mode 100644
index 00000000..21d43c59
--- /dev/null
+++ b/src/KubeOps.Operator.Web/Webhooks/Mutation/JsonDiffer.cs
@@ -0,0 +1,27 @@
+using System.Text;
+using System.Text.Json.JsonDiffPatch;
+using System.Text.Json.JsonDiffPatch.Diffs.Formatters;
+using System.Text.Json.Nodes;
+
+using k8s;
+
+namespace KubeOps.Operator.Web.Webhooks.Mutation;
+
+internal static class JsonDiffer
+{
+ private static readonly JsonPatchDeltaFormatter Formatter = new();
+
+ public static string Base64Diff(this JsonNode from, object? to)
+ {
+ var toToken = GetNode(to);
+ var patch = from.Diff(toToken, Formatter)!;
+
+ return Convert.ToBase64String(Encoding.UTF8.GetBytes(patch.ToString()));
+ }
+
+ public static JsonNode? GetNode(object? o)
+ {
+ var json = KubernetesJson.Serialize(o);
+ return JsonNode.Parse(json);
+ }
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/Mutation/MutationResult.cs b/src/KubeOps.Operator.Web/Webhooks/Mutation/MutationResult.cs
new file mode 100644
index 00000000..df363c7c
--- /dev/null
+++ b/src/KubeOps.Operator.Web/Webhooks/Mutation/MutationResult.cs
@@ -0,0 +1,78 @@
+using System.Text.Json.JsonDiffPatch;
+using System.Text.Json.JsonDiffPatch.Diffs.Formatters;
+using System.Text.Json.Nodes;
+
+using k8s;
+using k8s.Models;
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace KubeOps.Operator.Web.Webhooks.Mutation;
+
+///
+/// The mutation result for the mutation (admission) request to a webhook.
+///
+public record MutationResult(TEntity? ModifiedObject = default) : IActionResult
+ where TEntity : IKubernetesObject
+{
+ private const string JsonPatch = "JSONPatch";
+
+ internal string Uid { get; init; } = string.Empty;
+
+ internal JsonNode? OriginalObject { get; init; }
+
+ public bool Valid { get; init; } = true;
+
+ ///
+ /// Provides additional information to the validation result.
+ /// The message is displayed to the user if the validation fails.
+ /// The status code can provide more information about the error.
+ ///
+ /// See "Extensible Admission Controller Response"
+ ///
+ ///
+ public AdmissionStatus? Status { get; init; }
+
+ ///
+ /// Despite being "valid", the validation can add a list of warnings to the user.
+ /// If this is not yet supported by the cluster, the field is ignored.
+ /// Warnings may contain up to 256 characters but they should be limited to 120 characters.
+ /// If more than 4096 characters are submitted, additional messages are ignored.
+ ///
+ public IList Warnings { get; init; } = new List();
+
+ ///
+ public async Task ExecuteResultAsync(ActionContext context)
+ {
+ var response = context.HttpContext.Response;
+ if (string.IsNullOrWhiteSpace(Uid))
+ {
+ response.StatusCode = StatusCodes.Status500InternalServerError;
+ await response.WriteAsync("No request UID was provided.");
+ return;
+ }
+
+ if (ModifiedObject is not null && OriginalObject is null)
+ {
+ response.StatusCode = StatusCodes.Status500InternalServerError;
+ await response.WriteAsync("No original object was provided.");
+ return;
+ }
+
+ await response.WriteAsJsonAsync(
+ new AdmissionResponse
+ {
+ Response = new()
+ {
+ Uid = Uid,
+ Allowed = Valid,
+ Status = Status,
+ Warnings = Warnings.ToArray(),
+ PatchType = ModifiedObject is null ? null : JsonPatch,
+ Patch = ModifiedObject is null ? null : OriginalObject!.Base64Diff(ModifiedObject),
+ },
+ },
+ AdmissionResponse.SerializerOptions);
+ }
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/Mutation/MutationWebhookAttribute.cs b/src/KubeOps.Operator.Web/Webhooks/Mutation/MutationWebhookAttribute.cs
new file mode 100644
index 00000000..74682625
--- /dev/null
+++ b/src/KubeOps.Operator.Web/Webhooks/Mutation/MutationWebhookAttribute.cs
@@ -0,0 +1,17 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace KubeOps.Operator.Web.Webhooks.Mutation;
+
+///
+/// Defines an MVC controller as "mutation webhook". The route is automatically set to
+/// /mutate/[lower-case-name-of-the-type].
+/// This must be used in conjunction with the class.
+///
+[AttributeUsage(AttributeTargets.Class)]
+public class MutationWebhookAttribute : RouteAttribute
+{
+ public MutationWebhookAttribute(Type entityType)
+ : base($"/mutate/{entityType.Name.ToLowerInvariant()}")
+ {
+ }
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/Mutation/MutationWebhook{TEntity}.cs b/src/KubeOps.Operator.Web/Webhooks/Mutation/MutationWebhook{TEntity}.cs
new file mode 100644
index 00000000..4d017660
--- /dev/null
+++ b/src/KubeOps.Operator.Web/Webhooks/Mutation/MutationWebhook{TEntity}.cs
@@ -0,0 +1,167 @@
+using k8s;
+using k8s.Models;
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace KubeOps.Operator.Web.Webhooks.Mutation;
+
+///
+/// The abstract base for any mutation webhook. To use them, attach controllers in the
+/// main program and map controllers as well. A mutating webhook must be decorated
+/// with the and the type must be provided.
+/// There are async and sync methods for each operation. The async will take
+/// precedence if both are implemented (i.e. overridden).
+///
+/// The type of the entity that is mutated.
+///
+/// Simple example of a webhook that sets all usernames to "hidden".
+///
+/// [MutationWebhook(typeof(V1TestEntity))]
+/// public class TestMutationWebhook : MutationWebhook<V1TestEntity>
+/// {
+/// public override MutationResult<V1TestEntity> Create(V1TestEntity entity, bool dryRun)
+/// {
+/// entity.Spec.Username = "hidden";
+/// return Modified(entity);
+/// }
+/// }
+///
+///
+[ApiController]
+public abstract class MutationWebhook : ControllerBase
+ where TEntity : IKubernetesObject
+{
+ private const string CreateOperation = "CREATE";
+ private const string UpdateOperation = "UPDATE";
+ private const string DeleteOperation = "DELETE";
+
+ ///
+ /// Mutation callback for entities that are created.
+ ///
+ /// The (soon to be) new entity.
+ /// Flag that indicates if the webhook was called in a dry-run.
+ /// A .
+ [NonAction]
+ public virtual Task> CreateAsync(TEntity entity, bool dryRun) =>
+ Task.FromResult(Create(entity, dryRun));
+
+ ///
+ [NonAction]
+ public virtual MutationResult Create(TEntity entity, bool dryRun) => NoChanges();
+
+ ///
+ /// Mutation callback for entities that are updated.
+ ///
+ /// The currently stored entity in the Kubernetes API.
+ /// The new entity that should be stored.
+ /// Flag that indicates if the webhook was called in a dry-run.
+ /// A .
+ [NonAction]
+ public virtual Task> UpdateAsync(TEntity oldEntity, TEntity newEntity, bool dryRun) =>
+ Task.FromResult(Update(oldEntity, newEntity, dryRun));
+
+ ///
+ [NonAction]
+ public virtual MutationResult Update(TEntity oldEntity, TEntity newEntity, bool dryRun) => NoChanges();
+
+ ///
+ /// Mutation callback for entities that are to be deleted.
+ ///
+ /// The (soon to be removed) entity.
+ /// Flag that indicates if the webhook was called in a dry-run.
+ /// A .
+ [NonAction]
+ public virtual Task> DeleteAsync(TEntity entity, bool dryRun) =>
+ Task.FromResult(Delete(entity, dryRun));
+
+ ///
+ [NonAction]
+ public virtual MutationResult Delete(TEntity entity, bool dryRun) => NoChanges();
+
+ ///
+ /// Public, non-virtual method that is called by the controller.
+ /// This method will call the correct method based on the operation.
+ ///
+ /// The incoming admission request for an entity.
+ /// The .
+ [HttpPost]
+ public async Task Validate([FromBody] AdmissionRequest request)
+ {
+ var original = JsonDiffer.GetNode(request.Request.Operation switch
+ {
+ CreateOperation or UpdateOperation => request.Request.Object!,
+ _ => request.Request.OldObject!,
+ });
+
+ var result = request.Request.Operation switch
+ {
+ CreateOperation => await CreateAsync(request.Request.Object!, request.Request.DryRun),
+ UpdateOperation => await UpdateAsync(
+ request.Request.OldObject!,
+ request.Request.Object!,
+ request.Request.DryRun),
+ DeleteOperation => await DeleteAsync(request.Request.OldObject!, request.Request.DryRun),
+ _ => Fail(
+ $"Operation {request.Request.Operation} is not supported.",
+ StatusCodes.Status422UnprocessableEntity),
+ };
+
+ return result with { Uid = request.Request.Uid, OriginalObject = original };
+ }
+
+ ///
+ /// Create a with an optional list of warnings.
+ /// The mutation result indicates that no changes are required for the entity.
+ ///
+ /// A list of warnings that is presented to the user.
+ /// A .
+ [NonAction]
+ protected MutationResult NoChanges(params string[] warnings)
+ => new() { Warnings = warnings };
+
+ ///
+ /// Create a with an optional list of warnings.
+ /// The mutation result indicates that the entity needs to be patched before
+ /// it is definitely stored. This creates a JSON Patch between the original object
+ /// and the changed entity. If warnings are provided, they are presented to the user.
+ ///
+ /// The modified entity.
+ /// A list of warnings that is presented to the user.
+ /// A that indicates changes.
+ [NonAction]
+ protected MutationResult Modified(TEntity entity, params string[] warnings)
+ => new(entity) { Warnings = warnings };
+
+ ///
+ /// Create a that will fail the mutation.
+ /// The user will only see that the mutation failed.
+ ///
+ /// A .
+ [NonAction]
+ protected MutationResult Fail()
+ => new() { Valid = false };
+
+ ///
+ /// Create a that will fail the mutation.
+ /// The reason is presented to the user.
+ ///
+ /// A reason for the failure of the mutation.
+ /// A .
+ [NonAction]
+ protected MutationResult Fail(string reason)
+ => Fail() with { Status = new(reason) };
+
+ ///
+ /// Create a that will fail the mutation.
+ /// The reason is presented to the user with the custom status code.
+ /// The custom status code may provide further specific information about the
+ /// failure, but not all Kubernetes clusters support custom status codes.
+ ///
+ /// A reason for the failure of the mutation.
+ /// The custom status code.
+ /// A .
+ [NonAction]
+ protected MutationResult Fail(string reason, int statusCode) =>
+ Fail() with { Status = new(reason, statusCode), };
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationResult.cs b/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationResult.cs
index 0e398e9f..84d43295 100644
--- a/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationResult.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationResult.cs
@@ -1,56 +1,56 @@
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace KubeOps.Operator.Web.Webhooks.Validation;
-
-///
-/// The validation result for the validation (admission) request to a webhook.
-///
-/// Whether the validation / the entity is valid or not.
-public record ValidationResult(bool Valid = true) : IActionResult
-{
- internal string Uid { get; init; } = string.Empty;
-
- ///
- /// Provides additional information to the validation result.
- /// The message is displayed to the user if the validation fails.
- /// The status code can provide more information about the error.
- ///
- /// See "Extensible Admission Controller Response"
- ///
- ///
- public AdmissionStatus? Status { get; init; }
-
- ///
- /// Despite being "valid", the validation can add a list of warnings to the user.
- /// If this is not yet supported by the cluster, the field is ignored.
- /// Warnings may contain up to 256 characters but they should be limited to 120 characters.
- /// If more than 4096 characters are submitted, additional messages are ignored.
- ///
- public IList Warnings { get; init; } = new List();
-
- ///
- public async Task ExecuteResultAsync(ActionContext context)
- {
- var response = context.HttpContext.Response;
- if (string.IsNullOrWhiteSpace(Uid))
- {
- response.StatusCode = StatusCodes.Status500InternalServerError;
- await response.WriteAsync("No request UID was provided.");
- return;
- }
-
- await response.WriteAsJsonAsync(
- new AdmissionResponse
- {
- Response = new()
- {
- Uid = Uid,
- Allowed = Valid,
- Status = Status,
- Warnings = Warnings.ToArray(),
- },
- },
- AdmissionResponse.SerializerOptions);
- }
-}
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace KubeOps.Operator.Web.Webhooks.Validation;
+
+///
+/// The validation result for the validation (admission) request to a webhook.
+///
+/// Whether the validation / the entity is valid or not.
+public record ValidationResult(bool Valid = true) : IActionResult
+{
+ internal string Uid { get; init; } = string.Empty;
+
+ ///
+ /// Provides additional information to the validation result.
+ /// The message is displayed to the user if the validation fails.
+ /// The status code can provide more information about the error.
+ ///
+ /// See "Extensible Admission Controller Response"
+ ///
+ ///
+ public AdmissionStatus? Status { get; init; }
+
+ ///
+ /// Despite being "valid", the validation can add a list of warnings to the user.
+ /// If this is not yet supported by the cluster, the field is ignored.
+ /// Warnings may contain up to 256 characters but they should be limited to 120 characters.
+ /// If more than 4096 characters are submitted, additional messages are ignored.
+ ///
+ public IList Warnings { get; init; } = new List();
+
+ ///
+ public async Task ExecuteResultAsync(ActionContext context)
+ {
+ var response = context.HttpContext.Response;
+ if (string.IsNullOrWhiteSpace(Uid))
+ {
+ response.StatusCode = StatusCodes.Status500InternalServerError;
+ await response.WriteAsync("No request UID was provided.");
+ return;
+ }
+
+ await response.WriteAsJsonAsync(
+ new AdmissionResponse
+ {
+ Response = new()
+ {
+ Uid = Uid,
+ Allowed = Valid,
+ Status = Status,
+ Warnings = Warnings.ToArray(),
+ },
+ },
+ AdmissionResponse.SerializerOptions);
+ }
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationWebhookAttribute.cs b/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationWebhookAttribute.cs
index a0f674c1..17c22b13 100644
--- a/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationWebhookAttribute.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationWebhookAttribute.cs
@@ -1,17 +1,17 @@
-using Microsoft.AspNetCore.Mvc;
-
-namespace KubeOps.Operator.Web.Webhooks.Validation;
-
-///
-/// Defines an MVC controller as "validation webhook". The route is automatically set to
-/// /validate/[lower-case-name-of-the-type].
-/// This must be used in conjunction with the class.
-///
-[AttributeUsage(AttributeTargets.Class)]
-public class ValidationWebhookAttribute : RouteAttribute
-{
- public ValidationWebhookAttribute(Type entityType)
- : base($"/validate/{entityType.Name.ToLowerInvariant()}")
- {
- }
-}
+using Microsoft.AspNetCore.Mvc;
+
+namespace KubeOps.Operator.Web.Webhooks.Validation;
+
+///
+/// Defines an MVC controller as "validation webhook". The route is automatically set to
+/// /validate/[lower-case-name-of-the-type].
+/// This must be used in conjunction with the class.
+///
+[AttributeUsage(AttributeTargets.Class)]
+public class ValidationWebhookAttribute : RouteAttribute
+{
+ public ValidationWebhookAttribute(Type entityType)
+ : base($"/validate/{entityType.Name.ToLowerInvariant()}")
+ {
+ }
+}
diff --git a/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationWebhook{TEntity}.cs b/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationWebhook{TEntity}.cs
index 3cb1914d..5b184a1f 100644
--- a/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationWebhook{TEntity}.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/Validation/ValidationWebhook{TEntity}.cs
@@ -1,146 +1,146 @@
-using k8s;
-using k8s.Models;
-
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace KubeOps.Operator.Web.Webhooks.Validation;
-
-///
-/// The abstract base for any validation webhook. To use them, attach controllers in the
-/// main program and map controllers as well. A validation webhook must be decorated
-/// with the and the type must be provided.
-/// There are async and sync methods for each operation. The async will take
-/// precedence if both are implemented (i.e. overridden).
-///
-/// The type of the entity that is validated.
-///
-/// Simple example of a webhook that checks nothing but is called on every "CREATE" event.
-///
-/// [ValidationWebhook(typeof(V1TestEntity))]
-/// public class TestValidationWebhook : ValidationWebhook<V1TestEntity>
-/// {
-/// public override ValidationResult Create(V1TestEntity entity, bool dryRun)
-/// {
-/// return Success();
-/// }
-/// }
-///
-///
-[ApiController]
-public abstract class ValidationWebhook : ControllerBase
- where TEntity : IKubernetesObject
-{
- private const string CreateOperation = "CREATE";
- private const string UpdateOperation = "UPDATE";
- private const string DeleteOperation = "DELETE";
-
- ///
- /// Validation callback for entities that are created.
- ///
- /// The (soon to be) new entity.
- /// Flag that indicates if the webhook was called in a dry-run.
- /// A .
- [NonAction]
- public virtual Task CreateAsync(TEntity entity, bool dryRun) =>
- Task.FromResult(Create(entity, dryRun));
-
- ///
- [NonAction]
- public virtual ValidationResult Create(TEntity entity, bool dryRun) => Success();
-
- ///
- /// Validation callback for entities that are updated.
- ///
- /// The currently stored entity in the Kubernetes API.
- /// The new entity that should be stored.
- /// Flag that indicates if the webhook was called in a dry-run.
- /// A .
- [NonAction]
- public virtual Task UpdateAsync(TEntity oldEntity, TEntity newEntity, bool dryRun) =>
- Task.FromResult(Update(oldEntity, newEntity, dryRun));
-
- ///
- [NonAction]
- public virtual ValidationResult Update(TEntity oldEntity, TEntity newEntity, bool dryRun) => Success();
-
- ///
- /// Validation callback for entities that are to be deleted.
- ///
- /// The (soon to be removed) entity.
- /// Flag that indicates if the webhook was called in a dry-run.
- /// A .
- [NonAction]
- public virtual Task DeleteAsync(TEntity entity, bool dryRun) =>
- Task.FromResult(Delete(entity, dryRun));
-
- ///
- [NonAction]
- public virtual ValidationResult Delete(TEntity entity, bool dryRun) => Success();
-
- ///
- /// Public, non-virtual method that is called by the controller.
- /// This method will call the correct method based on the operation.
- ///
- /// The incoming admission request for an entity.
- /// The .
- [HttpPost]
- public async Task Validate([FromBody] AdmissionRequest request)
- {
- var result = request.Request.Operation switch
- {
- CreateOperation => await CreateAsync(request.Request.Object!, request.Request.DryRun),
- UpdateOperation => await UpdateAsync(
- request.Request.OldObject!,
- request.Request.Object!,
- request.Request.DryRun),
- DeleteOperation => await DeleteAsync(request.Request.OldObject!, request.Request.DryRun),
- _ => Fail(
- $"Operation {request.Request.Operation} is not supported.",
- StatusCodes.Status422UnprocessableEntity),
- };
-
- return result with { Uid = request.Request.Uid };
- }
-
- ///
- /// Create a with an optional list of warnings.
- /// The validation will succeed, such that the operation will proceed.
- ///
- /// A list of warnings that is presented to the user.
- /// A .
- [NonAction]
- protected ValidationResult Success(params string[] warnings)
- => new() { Warnings = warnings };
-
- ///
- /// Create a that will fail the validation.
- /// The user will only see that the validation failed.
- ///
- /// A .
- [NonAction]
- protected ValidationResult Fail()
- => new(false);
-
- ///
- /// Create a that will fail the validation.
- /// The reason is presented to the user.
- ///
- /// A reason for the failure of the validation.
- /// A .
- [NonAction]
- protected ValidationResult Fail(string reason)
- => new(false) { Status = new(reason) };
-
- ///
- /// Create a that will fail the validation.
- /// The reason is presented to the user with the custom status code.
- /// The custom status code may provide further specific information about the
- /// failure, but not all Kubernetes clusters support custom status codes.
- ///
- /// A reason for the failure of the validation.
- /// The custom status code.
- /// A .
- [NonAction]
- protected ValidationResult Fail(string reason, int statusCode) => new(false) { Status = new(reason, statusCode), };
-}
+using k8s;
+using k8s.Models;
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace KubeOps.Operator.Web.Webhooks.Validation;
+
+///
+/// The abstract base for any validation webhook. To use them, attach controllers in the
+/// main program and map controllers as well. A validation webhook must be decorated
+/// with the and the type must be provided.
+/// There are async and sync methods for each operation. The async will take
+/// precedence if both are implemented (i.e. overridden).
+///
+/// The type of the entity that is validated.
+///
+/// Simple example of a webhook that checks nothing but is called on every "CREATE" event.
+///
+/// [ValidationWebhook(typeof(V1TestEntity))]
+/// public class TestValidationWebhook : ValidationWebhook<V1TestEntity>
+/// {
+/// public override ValidationResult Create(V1TestEntity entity, bool dryRun)
+/// {
+/// return Success();
+/// }
+/// }
+///
+///
+[ApiController]
+public abstract class ValidationWebhook : ControllerBase
+ where TEntity : IKubernetesObject
+{
+ private const string CreateOperation = "CREATE";
+ private const string UpdateOperation = "UPDATE";
+ private const string DeleteOperation = "DELETE";
+
+ ///
+ /// Validation callback for entities that are created.
+ ///
+ /// The (soon to be) new entity.
+ /// Flag that indicates if the webhook was called in a dry-run.
+ /// A .
+ [NonAction]
+ public virtual Task CreateAsync(TEntity entity, bool dryRun) =>
+ Task.FromResult(Create(entity, dryRun));
+
+ ///
+ [NonAction]
+ public virtual ValidationResult Create(TEntity entity, bool dryRun) => Success();
+
+ ///
+ /// Validation callback for entities that are updated.
+ ///
+ /// The currently stored entity in the Kubernetes API.
+ /// The new entity that should be stored.
+ /// Flag that indicates if the webhook was called in a dry-run.
+ /// A .
+ [NonAction]
+ public virtual Task UpdateAsync(TEntity oldEntity, TEntity newEntity, bool dryRun) =>
+ Task.FromResult(Update(oldEntity, newEntity, dryRun));
+
+ ///
+ [NonAction]
+ public virtual ValidationResult Update(TEntity oldEntity, TEntity newEntity, bool dryRun) => Success();
+
+ ///
+ /// Validation callback for entities that are to be deleted.
+ ///
+ /// The (soon to be removed) entity.
+ /// Flag that indicates if the webhook was called in a dry-run.
+ /// A .
+ [NonAction]
+ public virtual Task DeleteAsync(TEntity entity, bool dryRun) =>
+ Task.FromResult(Delete(entity, dryRun));
+
+ ///
+ [NonAction]
+ public virtual ValidationResult Delete(TEntity entity, bool dryRun) => Success();
+
+ ///
+ /// Public, non-virtual method that is called by the controller.
+ /// This method will call the correct method based on the operation.
+ ///
+ /// The incoming admission request for an entity.
+ /// The .
+ [HttpPost]
+ public async Task Validate([FromBody] AdmissionRequest request)
+ {
+ var result = request.Request.Operation switch
+ {
+ CreateOperation => await CreateAsync(request.Request.Object!, request.Request.DryRun),
+ UpdateOperation => await UpdateAsync(
+ request.Request.OldObject!,
+ request.Request.Object!,
+ request.Request.DryRun),
+ DeleteOperation => await DeleteAsync(request.Request.OldObject!, request.Request.DryRun),
+ _ => Fail(
+ $"Operation {request.Request.Operation} is not supported.",
+ StatusCodes.Status422UnprocessableEntity),
+ };
+
+ return result with { Uid = request.Request.Uid };
+ }
+
+ ///
+ /// Create a with an optional list of warnings.
+ /// The validation will succeed, such that the operation will proceed.
+ ///
+ /// A list of warnings that is presented to the user.
+ /// A .
+ [NonAction]
+ protected ValidationResult Success(params string[] warnings)
+ => new() { Warnings = warnings };
+
+ ///
+ /// Create a that will fail the validation.
+ /// The user will only see that the validation failed.
+ ///
+ /// A .
+ [NonAction]
+ protected ValidationResult Fail()
+ => new(false);
+
+ ///
+ /// Create a that will fail the validation.
+ /// The reason is presented to the user.
+ ///
+ /// A reason for the failure of the validation.
+ /// A .
+ [NonAction]
+ protected ValidationResult Fail(string reason)
+ => Fail() with { Status = new(reason) };
+
+ ///
+ /// Create a that will fail the validation.
+ /// The reason is presented to the user with the custom status code.
+ /// The custom status code may provide further specific information about the
+ /// failure, but not all Kubernetes clusters support custom status codes.
+ ///
+ /// A reason for the failure of the validation.
+ /// The custom status code.
+ /// A .
+ [NonAction]
+ protected ValidationResult Fail(string reason, int statusCode) => Fail() with { Status = new(reason, statusCode), };
+}
diff --git a/src/KubeOps.Transpiler/ContextCreator.cs b/src/KubeOps.Transpiler/ContextCreator.cs
index d94f1cdb..850023dd 100644
--- a/src/KubeOps.Transpiler/ContextCreator.cs
+++ b/src/KubeOps.Transpiler/ContextCreator.cs
@@ -1,36 +1,36 @@
-using System.Reflection;
-
-namespace KubeOps.Transpiler;
-
-///
-/// Helper to create s.
-///
-public static class ContextCreator
-{
- ///
- /// Create a new with the given
- /// and directly load an assembly into it.
- ///
- /// A list of paths.
- /// The byte array that contains the assembly to load.
- /// Optional core assembly name.
- /// The configured .
- public static MetadataLoadContext Create(
- IEnumerable assemblyPaths,
- byte[] assembly,
- string? coreAssemblyName = null)
- {
- var mlc = Create(assemblyPaths, coreAssemblyName);
- mlc.LoadFromByteArray(assembly);
- return mlc;
- }
-
- ///
- /// Create a new with the given .
- ///
- /// A list of paths.
- /// Optional core assembly name.
- /// The configured .
- public static MetadataLoadContext Create(IEnumerable assemblyPaths, string? coreAssemblyName = null) =>
- new(new PathAssemblyResolver(assemblyPaths), coreAssemblyName: coreAssemblyName);
-}
+using System.Reflection;
+
+namespace KubeOps.Transpiler;
+
+///
+/// Helper to create s.
+///
+public static class ContextCreator
+{
+ ///
+ /// Create a new with the given
+ /// and directly load an assembly into it.
+ ///
+ /// A list of paths.
+ /// The byte array that contains the assembly to load.
+ /// Optional core assembly name.
+ /// The configured .
+ public static MetadataLoadContext Create(
+ IEnumerable assemblyPaths,
+ byte[] assembly,
+ string? coreAssemblyName = null)
+ {
+ var mlc = Create(assemblyPaths, coreAssemblyName);
+ mlc.LoadFromByteArray(assembly);
+ return mlc;
+ }
+
+ ///
+ /// Create a new with the given .
+ ///
+ /// A list of paths.
+ /// Optional core assembly name.
+ /// The configured .
+ public static MetadataLoadContext Create(IEnumerable assemblyPaths, string? coreAssemblyName = null) =>
+ new(new PathAssemblyResolver(assemblyPaths), coreAssemblyName: coreAssemblyName);
+}
diff --git a/src/KubeOps.Transpiler/Crds.cs b/src/KubeOps.Transpiler/Crds.cs
index 1d47b540..31d9a772 100644
--- a/src/KubeOps.Transpiler/Crds.cs
+++ b/src/KubeOps.Transpiler/Crds.cs
@@ -1,475 +1,475 @@
-using System.Collections;
-using System.Collections.ObjectModel;
-using System.Diagnostics.CodeAnalysis;
-using System.Reflection;
-using System.Text.Json.Serialization;
-
-using k8s;
-using k8s.Models;
-
-using KubeOps.Abstractions.Entities;
-using KubeOps.Abstractions.Entities.Attributes;
-using KubeOps.Transpiler.Kubernetes;
-
-namespace KubeOps.Transpiler;
-
-///
-/// CRD transpiler for Kubernetes entities.
-///
-public static class Crds
-{
- private const string Integer = "integer";
- private const string Number = "number";
- private const string String = "string";
- private const string Boolean = "boolean";
- private const string Object = "object";
- private const string Array = "array";
-
- private const string Int32 = "int32";
- private const string Int64 = "int64";
- private const string Float = "float";
- private const string Double = "double";
- private const string DateTime = "date-time";
-
- private static readonly string[] IgnoredToplevelProperties = { "metadata", "apiversion", "kind" };
-
- ///
- /// Transpile a single type to a CRD.
- ///
- /// The .
- /// The type to convert.
- /// The converted custom resource definition.
- public static V1CustomResourceDefinition Transpile(this MetadataLoadContext context, Type type)
- {
- type = context.GetContextType(type);
- var (meta, scope) = context.ToEntityMetadata(type);
- var crd = new V1CustomResourceDefinition(new()).Initialize();
-
- crd.Metadata.Name = $"{meta.PluralName}.{meta.Group}";
- crd.Spec.Group = meta.Group;
-
- crd.Spec.Names =
- new V1CustomResourceDefinitionNames
- {
- Kind = meta.Kind,
- ListKind = meta.ListKind,
- Singular = meta.SingularName,
- Plural = meta.PluralName,
- };
- crd.Spec.Scope = scope;
- if (type.GetCustomAttributeData()?.ConstructorArguments[0].Value is
- ReadOnlyCollection shortNames)
- {
- crd.Spec.Names.ShortNames = shortNames.Select(a => a.Value?.ToString()).ToList();
- }
-
- var version = new V1CustomResourceDefinitionVersion(meta.Version, true, true);
- if
- (type.GetProperty("Status") != null
- || type.GetProperty("status") != null)
- {
- version.Subresources = new V1CustomResourceSubresources(null, new object());
- }
-
- version.Schema = new V1CustomResourceValidation(new V1JSONSchemaProps
- {
- Type = Object,
- Description =
- type.GetCustomAttributeData()?.GetCustomAttributeCtorArg(context, 0),
- Properties = type.GetProperties()
- .Where(p => !IgnoredToplevelProperties.Contains(p.Name.ToLowerInvariant()))
- .Where(p => p.GetCustomAttributeData() == null)
- .Select(p => (Name: p.GetPropertyName(context), Schema: context.Map(p)))
- .ToDictionary(t => t.Name, t => t.Schema),
- });
-
- version.AdditionalPrinterColumns = context.MapPrinterColumns(type).ToList() switch
- {
- { Count: > 0 } l => l,
- _ => null,
- };
- crd.Spec.Versions = new List { version };
- crd.Validate();
-
- return crd;
- }
-
- ///
- /// Transpile a list of entities to CRDs and group them by version.
- ///
- /// The .
- /// The types to convert.
- /// The converted custom resource definitions.
- public static IEnumerable Transpile(
- this MetadataLoadContext context,
- IEnumerable types)
- => types
- .Select(context.GetContextType)
- .Where(type => type.Assembly != context.GetContextType().Assembly)
- .Where(type => type.GetCustomAttributesData().Any())
- .Where(type => !type.GetCustomAttributesData().Any())
- .Select(type => (Props: context.Transpile(type),
- IsStorage: type.GetCustomAttributesData().Any()))
- .GroupBy(grp => grp.Props.Metadata.Name)
- .Select(
- group =>
- {
- if (group.Count(def => def.IsStorage) > 1)
- {
- throw new ArgumentException("There are multiple stored versions on an entity.");
- }
-
- var crd = group.First().Props;
- crd.Spec.Versions = group
- .SelectMany(
- c => c.Props.Spec.Versions.Select(
- v =>
- {
- v.Served = true;
- v.Storage = c.IsStorage;
- return v;
- }))
- .OrderByDescending(v => v.Name, new KubernetesVersionComparer())
- .ToList();
-
- // when only one version exists, or when no StorageVersion attributes are found
- // the first version in the list is the stored one.
- if (crd.Spec.Versions.Count == 1 || !group.Any(def => def.IsStorage))
- {
- crd.Spec.Versions[0].Storage = true;
- }
-
- return crd;
- });
-
- private static string GetPropertyName(this PropertyInfo prop, MetadataLoadContext context)
- {
- var name = prop.GetCustomAttributeData() switch
- {
- null => prop.Name,
- { } attr => attr.GetCustomAttributeCtorArg(context, 0) ?? prop.Name,
- };
-
- return $"{name[..1].ToLowerInvariant()}{name[1..]}";
- }
-
- private static IEnumerable MapPrinterColumns(
- this MetadataLoadContext context,
- Type type)
- {
- var props = type.GetProperties().Select(p => (Prop: p, Path: string.Empty)).ToList();
- while (props.Count > 0)
- {
- var (prop, path) = props[0];
- props.RemoveAt(0);
-
- if (prop.PropertyType.IsClass)
- {
- props.AddRange(prop.PropertyType.GetProperties()
- .Select(p => (Prop: p, Path: $"{path}.{prop.GetPropertyName(context)}")));
- }
-
- if (prop.GetCustomAttributeData() is not { } attr)
- {
- continue;
- }
-
- var mapped = context.Map(prop);
- yield return new V1CustomResourceColumnDefinition
- {
- Name = attr.GetCustomAttributeCtorArg(context, 1) ?? prop.GetPropertyName(context),
- JsonPath = $"{path}.{prop.GetPropertyName(context)}",
- Type = mapped.Type,
- Description = mapped.Description,
- Format = mapped.Format,
- Priority = attr.GetCustomAttributeCtorArg(context, 0) switch
- {
- PrinterColumnPriority.StandardView => 0,
- _ => 1,
- },
- };
- }
-
- foreach (var attr in type.GetCustomAttributesData())
- {
- yield return new V1CustomResourceColumnDefinition
- {
- Name = attr.GetCustomAttributeCtorArg(context, 1),
- JsonPath = attr.GetCustomAttributeCtorArg(context, 0),
- Type = attr.GetCustomAttributeCtorArg(context, 2),
- Description = attr.GetCustomAttributeNamedArg(context, "Description"),
- Format = attr.GetCustomAttributeNamedArg(context, "Format"),
- Priority = attr.GetCustomAttributeNamedArg(context, "Priority") switch
- {
- PrinterColumnPriority.StandardView => 0,
- _ => 1,
- },
- };
- }
- }
-
- private static V1JSONSchemaProps Map(this MetadataLoadContext context, PropertyInfo prop)
- {
- var props = context.Map(prop.PropertyType);
-
- props.Description ??= prop.GetCustomAttributeData()
- ?.GetCustomAttributeCtorArg(context, 0);
-
- props.Nullable = prop.IsNullable();
-
- if (prop.GetCustomAttributeData() is { } extDocs)
- {
- props.ExternalDocs = new V1ExternalDocumentation(
- extDocs.GetCustomAttributeCtorArg(context, 0),
- extDocs.GetCustomAttributeCtorArg(context, 1));
- }
-
- if (prop.GetCustomAttributeData() is { } items)
- {
- props.MinItems = items.GetCustomAttributeCtorArg(context, 0);
- props.MaxItems = items.GetCustomAttributeCtorArg(context, 1);
- }
-
- if (prop.GetCustomAttributeData() is { } length)
- {
- props.MinLength = length.GetCustomAttributeCtorArg(context, 0);
- props.MaxLength = length.GetCustomAttributeCtorArg(context, 1);
- }
-
- if (prop.GetCustomAttributeData() is { } multi)
- {
- props.MultipleOf = multi.GetCustomAttributeCtorArg(context, 0);
- }
-
- if (prop.GetCustomAttributeData() is { } pattern)
- {
- props.Pattern = pattern.GetCustomAttributeCtorArg(context, 0);
- }
-
- if (prop.GetCustomAttributeData() is { } rangeMax)
- {
- props.Maximum = rangeMax.GetCustomAttributeCtorArg(context, 0);
- props.ExclusiveMaximum =
- rangeMax.GetCustomAttributeCtorArg(context, 1);
- }
-
- if (prop.GetCustomAttributeData() is { } rangeMin)
- {
- props.Minimum = rangeMin.GetCustomAttributeCtorArg(context, 0);
- props.ExclusiveMinimum =
- rangeMin.GetCustomAttributeCtorArg(context, 1);
- }
-
- if (prop.GetCustomAttributeData() is not null)
- {
- props.XKubernetesPreserveUnknownFields = true;
- }
-
- if (prop.GetCustomAttributeData() is not null)
- {
- props.XKubernetesEmbeddedResource = true;
- props.XKubernetesPreserveUnknownFields = true;
- props.Type = Object;
- props.Properties = null;
- }
-
- return props;
- }
-
- private static V1JSONSchemaProps Map(this MetadataLoadContext context, Type type)
- {
- if (type == context.GetContextType())
- {
- return new V1JSONSchemaProps { Type = Object };
- }
-
- if (type.IsArray && type.GetElementType() != null)
- {
- var items = context.Map(type.GetElementType()!);
- items.Nullable = type.GetElementType()!.IsNullable();
- return new V1JSONSchemaProps { Type = Array, Items = items };
- }
-
- if (!IsSimpleType(type)
- && type.IsGenericType
- && type.GetGenericTypeDefinition() == context.GetContextType(typeof(IDictionary<,>))
- && type.GenericTypeArguments.Contains(context.GetContextType(typeof(ResourceQuantity))))
- {
- return new V1JSONSchemaProps { Type = Object, XKubernetesPreserveUnknownFields = true };
- }
-
- if (!IsSimpleType(type) &&
- type.IsGenericType &&
- type.GetGenericTypeDefinition() == context.GetContextType(typeof(IEnumerable<>)) &&
- type.GenericTypeArguments.Length == 1 &&
- type.GenericTypeArguments.Single().IsGenericType &&
- type.GenericTypeArguments.Single().GetGenericTypeDefinition() ==
- context.GetContextType(typeof(KeyValuePair<,>)))
- {
- var props = context.Map(type.GenericTypeArguments.Single().GenericTypeArguments[1]);
- props.Nullable = type.GenericTypeArguments.Single().GenericTypeArguments[1].IsNullable();
- return new V1JSONSchemaProps { Type = Object, AdditionalProperties = props, };
- }
-
- if (!IsSimpleType(type)
- && type.IsGenericType
- && type.GetGenericTypeDefinition() == context.GetContextType(typeof(IDictionary<,>)))
- {
- var props = context.Map(type.GenericTypeArguments[1]);
- props.Nullable = type.GenericTypeArguments[1].IsNullable();
- return new V1JSONSchemaProps { Type = Object, AdditionalProperties = props, };
- }
-
- if (!IsSimpleType(type) &&
- (context.GetContextType().IsAssignableFrom(type) ||
- (type.IsGenericType &&
- type.GetGenericArguments().FirstOrDefault()?.IsGenericType == true &&
- type.GetGenericArguments().FirstOrDefault()?.GetGenericTypeDefinition() ==
- context.GetContextType(typeof(KeyValuePair<,>)))))
- {
- return new V1JSONSchemaProps { Type = Object, XKubernetesPreserveUnknownFields = true };
- }
-
- if (!IsSimpleType(type) && IsGenericEnumerableType(type, out Type? closingType))
- {
- var items = context.Map(closingType);
- items.Nullable = closingType.IsNullable();
- return new V1JSONSchemaProps { Type = Array, Items = items };
- }
-
- if (type == context.GetContextType())
- {
- return new V1JSONSchemaProps { XKubernetesIntOrString = true };
- }
-
- if (context.GetContextType().IsAssignableFrom(type) &&
- type is { IsAbstract: false, IsInterface: false } &&
- type.Assembly == context.GetContextType().Assembly)
- {
- return new V1JSONSchemaProps
- {
- Type = Object,
- Properties = null,
- XKubernetesPreserveUnknownFields = true,
- XKubernetesEmbeddedResource = true,
- };
- }
-
- if (type == context.GetContextType() ||
- type == context.GetContextType())
- {
- return new V1JSONSchemaProps { Type = Integer, Format = Int32 };
- }
-
- if (type == context.GetContextType() ||
- type == context.GetContextType())
- {
- return new V1JSONSchemaProps { Type = Integer, Format = Int64 };
- }
-
- if (type == context.GetContextType() ||
- type == context.GetContextType())
- {
- return new V1JSONSchemaProps { Type = Number, Format = Float };
- }
-
- if (type == context.GetContextType() ||
- type == context.GetContextType())
- {
- return new V1JSONSchemaProps { Type = Number, Format = Double };
- }
-
- if (type == context.GetContextType() ||
- type == context.GetContextType())
- {
- return new V1JSONSchemaProps { Type = String };
- }
-
- if (type == context.GetContextType() ||
- type == context.GetContextType())
- {
- return new V1JSONSchemaProps { Type = Boolean };
- }
-
- if (type == context.GetContextType() ||
- type == context.GetContextType())
- {
- return new V1JSONSchemaProps { Type = String, Format = DateTime };
- }
-
- if (type.IsEnum)
- {
- return new V1JSONSchemaProps { Type = String, EnumProperty = Enum.GetNames(type).Cast