diff --git a/examples/ConversionWebhookOperator/Program.cs b/examples/ConversionWebhookOperator/Program.cs index 44a63cea..d317706a 100644 --- a/examples/ConversionWebhookOperator/Program.cs +++ b/examples/ConversionWebhookOperator/Program.cs @@ -1,6 +1,8 @@ using KubeOps.Operator; using KubeOps.Operator.Web.Builder; +#pragma warning disable CS0618 // Type or member is obsolete + var builder = WebApplication.CreateBuilder(args); builder.Services .AddKubernetesOperator() diff --git a/examples/WebhookOperator/Program.cs b/examples/WebhookOperator/Program.cs index 44a63cea..d317706a 100644 --- a/examples/WebhookOperator/Program.cs +++ b/examples/WebhookOperator/Program.cs @@ -1,6 +1,8 @@ using KubeOps.Operator; using KubeOps.Operator.Web.Builder; +#pragma warning disable CS0618 // Type or member is obsolete + var builder = WebApplication.CreateBuilder(args); builder.Services .AddKubernetesOperator() diff --git a/src/Directory.Build.props b/src/Directory.Build.props index edd3521c..20097967 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -36,15 +36,15 @@ - + diff --git a/src/KubeOps.Abstractions/Certificates/CertificatePair.cs b/src/KubeOps.Abstractions/Certificates/CertificatePair.cs index 122fba70..b06eac24 100644 --- a/src/KubeOps.Abstractions/Certificates/CertificatePair.cs +++ b/src/KubeOps.Abstractions/Certificates/CertificatePair.cs @@ -1,7 +1,6 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -namespace KubeOps.Abstractions.Certificates -{ - public record CertificatePair(X509Certificate2 Certificate, AsymmetricAlgorithm Key); -} +namespace KubeOps.Abstractions.Certificates; + +public record CertificatePair(X509Certificate2 Certificate, AsymmetricAlgorithm Key); diff --git a/src/KubeOps.Abstractions/Certificates/ICertificateProvider.cs b/src/KubeOps.Abstractions/Certificates/ICertificateProvider.cs index fd9d5cb1..5e74c1b4 100644 --- a/src/KubeOps.Abstractions/Certificates/ICertificateProvider.cs +++ b/src/KubeOps.Abstractions/Certificates/ICertificateProvider.cs @@ -1,22 +1,18 @@ -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; +namespace KubeOps.Abstractions.Certificates; -namespace KubeOps.Abstractions.Certificates +/// +/// Defines properties for certificate/key pair so a custom certificate/key provider may be implemented. +/// The provider is used by the CertificateWebhookService to provide a caBundle to the webhooks. +/// +public interface ICertificateProvider : IDisposable { /// - /// Defines properties for certificate/key pair so a custom certificate/key provider may be implemented. - /// The provider is used by the CertificateWebhookService to provide a caBundle to the webhooks. + /// The server certificate and key. /// - public interface ICertificateProvider : IDisposable - { - /// - /// The server certificate and key. - /// - CertificatePair Server { get; } + CertificatePair Server { get; } - /// - /// The root certificate and key. - /// - CertificatePair Root { get; } - } + /// + /// The root certificate and key. + /// + CertificatePair Root { get; } } diff --git a/src/KubeOps.Abstractions/KubeOps.Abstractions.csproj b/src/KubeOps.Abstractions/KubeOps.Abstractions.csproj index 9f603584..5655fef2 100644 --- a/src/KubeOps.Abstractions/KubeOps.Abstractions.csproj +++ b/src/KubeOps.Abstractions/KubeOps.Abstractions.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/KubeOps.Cli/Generators/CertificateGenerator.cs b/src/KubeOps.Cli/Generators/CertificateGenerator.cs index 3eb720e1..a8f895c9 100644 --- a/src/KubeOps.Cli/Generators/CertificateGenerator.cs +++ b/src/KubeOps.Cli/Generators/CertificateGenerator.cs @@ -7,7 +7,7 @@ internal class CertificateGenerator(string serverName, string namespaceName) : I { public void Generate(ResultOutput output) { - using Operator.Web.CertificateGenerator generator = new(serverName, namespaceName); + using Operator.Web.Certificates.CertificateGenerator generator = new(serverName, namespaceName); output.Add("ca.pem", generator.Root.Certificate.EncodeToPem(), OutputFormat.Plain); output.Add("ca-key.pem", generator.Root.Key.EncodeToPem(), OutputFormat.Plain); diff --git a/src/KubeOps.Cli/Generators/RbacGenerator.cs b/src/KubeOps.Cli/Generators/RbacGenerator.cs index 30e74633..ffad903d 100644 --- a/src/KubeOps.Cli/Generators/RbacGenerator.cs +++ b/src/KubeOps.Cli/Generators/RbacGenerator.cs @@ -26,7 +26,7 @@ public void Generate(ResultOutput output) var roleBinding = new V1ClusterRoleBinding( roleRef: new V1RoleRef(V1ClusterRole.KubeGroup, V1ClusterRole.KubeKind, "operator-role"), - subjects: new List + subjects: new List { new(V1ServiceAccount.KubeKind, "default", namespaceProperty: "system"), }) diff --git a/src/KubeOps.Cli/KubeOps.Cli.csproj b/src/KubeOps.Cli/KubeOps.Cli.csproj index 6f731eae..466315d1 100644 --- a/src/KubeOps.Cli/KubeOps.Cli.csproj +++ b/src/KubeOps.Cli/KubeOps.Cli.csproj @@ -18,14 +18,14 @@ - - - - - - - - + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/KubeOps.Cli/Transpilation/BaseWebhook.cs b/src/KubeOps.Cli/Transpilation/BaseWebhook.cs index 10155f61..eb0850c9 100644 --- a/src/KubeOps.Cli/Transpilation/BaseWebhook.cs +++ b/src/KubeOps.Cli/Transpilation/BaseWebhook.cs @@ -6,14 +6,14 @@ namespace KubeOps.Cli.Transpilation; internal abstract record BaseWebhook(TypeInfo Webhook, EntityMetadata Metadata) { + public abstract string WebhookPath { get; } + private bool HasCreate => Webhook.DeclaredMembers.Any(m => m.Name.StartsWith("Create")); private bool HasUpdate => Webhook.DeclaredMembers.Any(m => m.Name.StartsWith("Update")); private bool HasDelete => Webhook.DeclaredMembers.Any(m => m.Name.StartsWith("Delete")); - public abstract string WebhookPath { get; } - public string[] GetOperations() => new[] { HasCreate ? "CREATE" : null, HasUpdate ? "UPDATE" : null, HasDelete ? "DELETE" : null, } .Where(o => o is not null).ToArray()!; diff --git a/src/KubeOps.Generator/KubeOps.Generator.csproj b/src/KubeOps.Generator/KubeOps.Generator.csproj index 39531ed7..0612269e 100644 --- a/src/KubeOps.Generator/KubeOps.Generator.csproj +++ b/src/KubeOps.Generator/KubeOps.Generator.csproj @@ -18,11 +18,16 @@ - + - + diff --git a/src/KubeOps.Operator.Web/Certificates/CertificateExtensions.cs b/src/KubeOps.Operator.Web/Certificates/CertificateExtensions.cs index 734e2d89..7310e6c2 100644 --- a/src/KubeOps.Operator.Web/Certificates/CertificateExtensions.cs +++ b/src/KubeOps.Operator.Web/Certificates/CertificateExtensions.cs @@ -4,55 +4,54 @@ using KubeOps.Abstractions.Certificates; -namespace KubeOps.Operator.Web.Certificates +namespace KubeOps.Operator.Web.Certificates; + +public static class CertificateExtensions { - public static class CertificateExtensions - { - /// - /// Encodes the certificate in PEM format for use in Kubernetes. - /// - /// The certificate to encode. - /// The byte representation of the PEM-encoded certificate. - public static byte[] EncodeToPemBytes(this X509Certificate2 certificate) => Encoding.UTF8.GetBytes(certificate.EncodeToPem()); + /// + /// Encodes the certificate in PEM format for use in Kubernetes. + /// + /// The certificate to encode. + /// The byte representation of the PEM-encoded certificate. + public static byte[] EncodeToPemBytes(this X509Certificate2 certificate) => Encoding.UTF8.GetBytes(certificate.EncodeToPem()); - /// - /// Encodes the certificate in PEM format. - /// - /// The certificate to encode. - /// The string representation of the PEM-encoded certificate. - public static string EncodeToPem(this X509Certificate2 certificate) => new(PemEncoding.Write("CERTIFICATE", certificate.RawData)); + /// + /// Encodes the certificate in PEM format. + /// + /// The certificate to encode. + /// The string representation of the PEM-encoded certificate. + public static string EncodeToPem(this X509Certificate2 certificate) => new(PemEncoding.Write("CERTIFICATE", certificate.RawData)); - /// - /// Encodes the key in PEM format. - /// - /// The key to encode. - /// The string representation of the PEM-encoded key. - public static string EncodeToPem(this AsymmetricAlgorithm key) => new(PemEncoding.Write("PRIVATE KEY", key.ExportPkcs8PrivateKey())); + /// + /// Encodes the key in PEM format. + /// + /// The key to encode. + /// The string representation of the PEM-encoded key. + public static string EncodeToPem(this AsymmetricAlgorithm key) => new(PemEncoding.Write("PRIVATE KEY", key.ExportPkcs8PrivateKey())); - /// - /// Generates a new server certificate with its private key attached, and sets . - /// For example, this certificate can be used in development environments to configure . - /// - /// The cert/key tuple to attach. - /// An with the private key attached. - /// The not have a CopyWithPrivateKey method, or the - /// method has not been implemented in this extension. - public static X509Certificate2 CopyServerCertWithPrivateKey(this CertificatePair serverPair) + /// + /// Generates a new server certificate with its private key attached, and sets . + /// For example, this certificate can be used in development environments to configure . + /// + /// The cert/key tuple to attach. + /// An with the private key attached. + /// The does not have a CopyWithPrivateKey method, or the + /// method has not been implemented in this extension. + public static X509Certificate2 CopyServerCertWithPrivateKey(this CertificatePair serverPair) + { + const string? password = null; + using X509Certificate2 temp = serverPair.Key switch { - const string? password = null; - using X509Certificate2 temp = serverPair.Key switch - { - ECDsa ecdsa => serverPair.Certificate.CopyWithPrivateKey(ecdsa), - RSA rsa => serverPair.Certificate.CopyWithPrivateKey(rsa), - ECDiffieHellman ecdh => serverPair.Certificate.CopyWithPrivateKey(ecdh), - DSA dsa => serverPair.Certificate.CopyWithPrivateKey(dsa), - _ => throw new NotImplementedException($"{serverPair.Key} is not implemented for {nameof(CopyServerCertWithPrivateKey)}"), - }; + ECDsa ecdsa => serverPair.Certificate.CopyWithPrivateKey(ecdsa), + RSA rsa => serverPair.Certificate.CopyWithPrivateKey(rsa), + ECDiffieHellman ecdh => serverPair.Certificate.CopyWithPrivateKey(ecdh), + DSA dsa => serverPair.Certificate.CopyWithPrivateKey(dsa), + _ => throw new NotImplementedException($"{serverPair.Key} is not implemented for {nameof(CopyServerCertWithPrivateKey)}"), + }; - return new X509Certificate2( - temp.Export(X509ContentType.Pfx, password), - password, - X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet); - } + return new X509Certificate2( + temp.Export(X509ContentType.Pfx, password), + password, + X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet); } } diff --git a/src/KubeOps.Operator.Web/Certificates/CertificateGenerator.cs b/src/KubeOps.Operator.Web/Certificates/CertificateGenerator.cs index 28e272d8..27a69c2d 100644 --- a/src/KubeOps.Operator.Web/Certificates/CertificateGenerator.cs +++ b/src/KubeOps.Operator.Web/Certificates/CertificateGenerator.cs @@ -5,213 +5,212 @@ using KubeOps.Abstractions.Certificates; -namespace KubeOps.Operator.Web +namespace KubeOps.Operator.Web.Certificates; + +/// +/// Generates a self-signed CA certificate and server certificate using ECDsa that can be used for operator webhooks. +/// +public class CertificateGenerator : ICertificateProvider { + private readonly string _serverName; + private readonly string? _serverNamespace; + private readonly DateTime _startDate; + private readonly DateTime _endDate; + private readonly Lazy _root; + private readonly Lazy _server; + /// - /// Generates a self-signed CA certificate and server certificate using ECDsa that can be used for operator webhooks. + /// Initializes a new instance of the class. /// - public class CertificateGenerator : ICertificateProvider + /// The hostname, IP, or FQDN of the machine running the operator. + public CertificateGenerator(string serverName) { - private readonly string _serverName; - private readonly string? _serverNamespace; - private readonly DateTime _startDate; - private readonly DateTime _endDate; - private readonly Lazy _root; - private readonly Lazy _server; - - /// - /// Initializes a new instance of the class. - /// - /// The hostname, IP, or FQDN of the machine running the operator. - public CertificateGenerator(string serverName) - { - _serverName = serverName; - _serverNamespace = null; - _startDate = DateTime.UtcNow.Date; - _endDate = _startDate.AddYears(5); - _root = new(GenerateRootCertificate); - _server = new(GenerateServerCertificate); - } + _serverName = serverName; + _serverNamespace = null; + _startDate = DateTime.UtcNow.Date; + _endDate = _startDate.AddYears(5); + _root = new(GenerateRootCertificate); + _server = new(GenerateServerCertificate); + } - /// - /// - /// - /// - /// The Kubernetes namespace the server will run in. - public CertificateGenerator(string serverName, string serverNamespace) - : this(serverName) - { - _serverNamespace = serverNamespace; - } + /// + /// + /// + /// + /// The Kubernetes namespace the server will run in. + public CertificateGenerator(string serverName, string serverNamespace) + : this(serverName) + { + _serverNamespace = serverNamespace; + } + + public CertificatePair Root => _root.Value; - public CertificatePair Root => _root.Value; + public CertificatePair Server => _server.Value; - public CertificatePair Server => _server.Value; + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - public void Dispose() + protected virtual void Dispose(bool disposing) + { + if (_root.IsValueCreated) { - Dispose(true); - GC.SuppressFinalize(this); + _root.Value.Certificate.Dispose(); + _root.Value.Key.Dispose(); } - protected virtual void Dispose(bool disposing) + if (_server.IsValueCreated) { - if (_root.IsValueCreated) - { - _root.Value.Certificate.Dispose(); - _root.Value.Key.Dispose(); - } + _server.Value.Certificate.Dispose(); + _server.Value.Key.Dispose(); + } + } - if (_server.IsValueCreated) - { - _server.Value.Certificate.Dispose(); - _server.Value.Key.Dispose(); - } + private CertificatePair GenerateRootCertificate() + { + ECDsa? key = null; + X509Certificate2? cert = null; + try + { + // Create an ECDsa key and a certificate request + key = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var request = new CertificateRequest( + "CN=Operator Root CA, C=DEV, L=Kubernetes", + key, + HashAlgorithmName.SHA512); + + // Specify certain details of how the certificate can be used + request.CertificateExtensions.Add( + new X509BasicConstraintsExtension(true, false, 0, true)); + request.CertificateExtensions.Add( + new X509KeyUsageExtension( + X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyEncipherment, + true)); + + // Create the self-signed cert + cert = request.CreateSelfSigned(_startDate, _endDate); + return new(cert, key); } + catch + { + key?.Dispose(); + cert?.Dispose(); + throw; + } + } - private CertificatePair GenerateRootCertificate() + private CertificatePair GenerateServerCertificate() + { + ECDsa? key = null; + X509Certificate2? cert = null; + try { - ECDsa? key = null; - X509Certificate2? cert = null; - try + key = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var request = new CertificateRequest( + "CN=Operator Service, C=DEV, L=Kubernetes", + key, + HashAlgorithmName.SHA512); + + request.CertificateExtensions.Add( + new X509BasicConstraintsExtension(false, false, 0, false)); + request.CertificateExtensions.Add( + new X509KeyUsageExtension( + X509KeyUsageFlags.NonRepudiation | X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, + true)); + + // Key purpose: clientAuth and serverAuth + request.CertificateExtensions.Add( + new X509EnhancedKeyUsageExtension( + [new Oid("1.3.6.1.5.5.7.3.1"), new Oid("1.3.6.1.5.5.7.3.2")], + false)); + request.CertificateExtensions.Add( + new X509SubjectKeyIdentifierExtension( + Root.Certificate.PublicKey, + false)); + request.CertificateExtensions.Add( + new CustomX509AuthorityKeyIdentifierExtension( + Root.Certificate, + false)); + + // If a server namespace is provided, it's safe to assume that the operator will be running in the Kubernetes cluster + // Otherwise, just try to parse whatever is there (i.e. for development) + var sanBuilder = new SubjectAlternativeNameBuilder(); + if (_serverNamespace != null) { - // Create an ECDsa key and a certificate request - key = ECDsa.Create(ECCurve.NamedCurves.nistP256); - var request = new CertificateRequest( - "CN=Operator Root CA, C=DEV, L=Kubernetes", - key, - HashAlgorithmName.SHA512); - - // Specify certain details of how the certificate can be used - request.CertificateExtensions.Add( - new X509BasicConstraintsExtension(true, false, 0, true)); - request.CertificateExtensions.Add( - new X509KeyUsageExtension( - X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyEncipherment, - true)); - - // Create the self-signed cert - cert = request.CreateSelfSigned(_startDate, _endDate); - return new(cert, key); + sanBuilder.AddDnsName($"{_serverName}.{_serverNamespace}.svc"); + sanBuilder.AddDnsName($"*.{_serverNamespace}.svc"); + sanBuilder.AddDnsName("*.svc"); } - catch + else if (IPAddress.TryParse(_serverName, out IPAddress? ipAddress)) { - key?.Dispose(); - cert?.Dispose(); - throw; + sanBuilder.AddIpAddress(ipAddress); } - } - - private CertificatePair GenerateServerCertificate() - { - ECDsa? key = null; - X509Certificate2? cert = null; - try + else { - key = ECDsa.Create(ECCurve.NamedCurves.nistP256); - var request = new CertificateRequest( - "CN=Operator Service, C=DEV, L=Kubernetes", - key, - HashAlgorithmName.SHA512); - - request.CertificateExtensions.Add( - new X509BasicConstraintsExtension(false, false, 0, false)); - request.CertificateExtensions.Add( - new X509KeyUsageExtension( - X509KeyUsageFlags.NonRepudiation | X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, - true)); - - // Key purpose: clientAuth and serverAuth - request.CertificateExtensions.Add( - new X509EnhancedKeyUsageExtension( - [new Oid("1.3.6.1.5.5.7.3.1"), new Oid("1.3.6.1.5.5.7.3.2")], - false)); - request.CertificateExtensions.Add( - new X509SubjectKeyIdentifierExtension( - Root.Certificate.PublicKey, - false)); - request.CertificateExtensions.Add( - new CustomX509AuthorityKeyIdentifierExtension( - Root.Certificate, - false)); - - // If a server namespace is provided, it's safe to assume that the operator will be running in the Kubernetes cluster - // Otherwise, just try to parse whatever is there (i.e. for development) - var sanBuilder = new SubjectAlternativeNameBuilder(); - if (_serverNamespace != null) - { - sanBuilder.AddDnsName($"{_serverName}.{_serverNamespace}.svc"); - sanBuilder.AddDnsName($"*.{_serverNamespace}.svc"); - sanBuilder.AddDnsName("*.svc"); - } - else if (IPAddress.TryParse(_serverName, out IPAddress? ipAddress)) - { - sanBuilder.AddIpAddress(ipAddress); - } - else - { - sanBuilder.AddDnsName(_serverName); - } + sanBuilder.AddDnsName(_serverName); + } - request.CertificateExtensions.Add(sanBuilder.Build()); + request.CertificateExtensions.Add(sanBuilder.Build()); - // Generate using the root certificate - X509SignatureGenerator generator = X509SignatureGenerator - .CreateForECDsa(Root.Certificate.GetECDsaPrivateKey()!); + // Generate using the root certificate + X509SignatureGenerator generator = X509SignatureGenerator + .CreateForECDsa(Root.Certificate.GetECDsaPrivateKey()!); - // Generate - cert = request.Create( - Root.Certificate.SubjectName, - generator, - _startDate, - _endDate, - Guid.NewGuid().ToByteArray()); + // Generate + cert = request.Create( + Root.Certificate.SubjectName, + generator, + _startDate, + _endDate, + Guid.NewGuid().ToByteArray()); - return new(cert, key); - } - catch - { - key?.Dispose(); - cert?.Dispose(); - throw; - } + return new(cert, key); } - - /// - /// Custom class for implementing a slim version of the .NET7/8 X509AuthorityKeyIdentifierExtension class. - /// - private sealed class CustomX509AuthorityKeyIdentifierExtension(X509Certificate2 certificate, bool critical) - : X509Extension(new Oid("2.5.29.35"), CreateFromCertificate(certificate), critical) + catch { - // https://source.dot.net/#System.Security.Cryptography/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs - // This .NET code is shipped with .NET 7/8, but is not in .NET 6, which is still supported by operator - // The method below uses portions of the static methods CreateFromCertificate() and Create() - private static byte[] CreateFromCertificate(X509Certificate2 certificate) - { - X509SubjectKeyIdentifierExtension skid = - (X509SubjectKeyIdentifierExtension?)certificate.Extensions["2.5.29.14"] ?? - new X509SubjectKeyIdentifierExtension(certificate.PublicKey, false); + key?.Dispose(); + cert?.Dispose(); + throw; + } + } - byte[] skidBytes = Convert.FromHexString(skid.SubjectKeyIdentifier!); + /// + /// Custom class for implementing a slim version of the .NET7/8 X509AuthorityKeyIdentifierExtension class. + /// + private sealed class CustomX509AuthorityKeyIdentifierExtension(X509Certificate2 certificate, bool critical) + : X509Extension(new Oid("2.5.29.35"), CreateFromCertificate(certificate), critical) + { + // https://source.dot.net/#System.Security.Cryptography/System/Security/Cryptography/X509Certificates/X509AuthorityKeyIdentifierExtension.cs + // This .NET code is shipped with .NET 7/8, but is not in .NET 6, which is still supported by operator + // The method below uses portions of the static methods CreateFromCertificate() and Create() + private static byte[] CreateFromCertificate(X509Certificate2 certificate) + { + X509SubjectKeyIdentifierExtension skid = + (X509SubjectKeyIdentifierExtension?)certificate.Extensions["2.5.29.14"] ?? + new X509SubjectKeyIdentifierExtension(certificate.PublicKey, false); - AsnWriter writer = new(AsnEncodingRules.DER); + byte[] skidBytes = Convert.FromHexString(skid.SubjectKeyIdentifier!); - using (writer.PushSequence()) - { - writer.WriteOctetString(skidBytes, new Asn1Tag(TagClass.ContextSpecific, 0)); + AsnWriter writer = new(AsnEncodingRules.DER); - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 1))) - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 4))) - { - writer.WriteEncodedValue(certificate.IssuerName.RawData); - } + using (writer.PushSequence()) + { + writer.WriteOctetString(skidBytes, new Asn1Tag(TagClass.ContextSpecific, 0)); - byte[] serialBytes = Convert.FromHexString(certificate.SerialNumber); - writer.WriteInteger(serialBytes, new Asn1Tag(TagClass.ContextSpecific, 2)); + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 1))) + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 4))) + { + writer.WriteEncodedValue(certificate.IssuerName.RawData); } - return writer.Encode(); + byte[] serialBytes = Convert.FromHexString(certificate.SerialNumber); + writer.WriteInteger(serialBytes, new Asn1Tag(TagClass.ContextSpecific, 2)); } + + return writer.Encode(); } } } diff --git a/src/KubeOps.Operator.Web/Certificates/CertificateWebhookService.cs b/src/KubeOps.Operator.Web/Certificates/CertificateWebhookService.cs index e6acf069..6eb96b59 100644 --- a/src/KubeOps.Operator.Web/Certificates/CertificateWebhookService.cs +++ b/src/KubeOps.Operator.Web/Certificates/CertificateWebhookService.cs @@ -5,26 +5,25 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace KubeOps.Operator.Web.Certificates +namespace KubeOps.Operator.Web.Certificates; + +internal class CertificateWebhookService(ILogger logger, IKubernetesClient client, WebhookLoader loader, WebhookConfig config, ICertificateProvider provider) + : WebhookServiceBase(client, loader, config), IHostedService { - internal class CertificateWebhookService(ILogger logger, IKubernetesClient client, WebhookLoader loader, WebhookConfig config, ICertificateProvider provider) - : WebhookServiceBase(client, loader, config), IHostedService - { - private readonly ILogger _logger = logger; - private readonly ICertificateProvider _provider = provider; + private readonly ILogger _logger = logger; + private readonly ICertificateProvider _provider = provider; - public async Task StartAsync(CancellationToken cancellationToken) - { - CaBundle = _provider.Server.Certificate.EncodeToPemBytes(); + public async Task StartAsync(CancellationToken cancellationToken) + { + CaBundle = _provider.Server.Certificate.EncodeToPemBytes(); - _logger.LogDebug("Registering webhooks"); - await RegisterAll(); - } + _logger.LogDebug("Registering webhooks"); + await RegisterAll(); + } - public Task StopAsync(CancellationToken cancellationToken) - { - _provider.Dispose(); - return Task.CompletedTask; - } + public Task StopAsync(CancellationToken cancellationToken) + { + _provider.Dispose(); + return Task.CompletedTask; } } diff --git a/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj b/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj index 018ee0cc..1a628439 100644 --- a/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj +++ b/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/KubeOps.Operator.Web/LocalTunnel/TunnelWebhookService.cs b/src/KubeOps.Operator.Web/LocalTunnel/TunnelWebhookService.cs index b5a7ebb3..a7e40842 100644 --- a/src/KubeOps.Operator.Web/LocalTunnel/TunnelWebhookService.cs +++ b/src/KubeOps.Operator.Web/LocalTunnel/TunnelWebhookService.cs @@ -5,26 +5,27 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace KubeOps.Operator.Web.LocalTunnel +namespace KubeOps.Operator.Web.LocalTunnel; + +internal class TunnelWebhookService( + ILogger logger, + IKubernetesClient client, + WebhookLoader loader, + WebhookConfig config, + DevelopmentTunnel developmentTunnel) + : WebhookServiceBase(client, loader, config), IHostedService { - internal class TunnelWebhookService(ILogger logger, IKubernetesClient client, WebhookLoader loader, WebhookConfig config, DevelopmentTunnel developmentTunnel) - : WebhookServiceBase(client, loader, config), IHostedService + public async Task StartAsync(CancellationToken cancellationToken) { - private readonly ILogger _logger = logger; - private readonly DevelopmentTunnel _developmentTunnel = developmentTunnel; - - public async Task StartAsync(CancellationToken cancellationToken) - { - Uri = await _developmentTunnel.StartAsync(cancellationToken); + Uri = await developmentTunnel.StartAsync(cancellationToken); - _logger.LogDebug("Registering webhooks"); - await RegisterAll(); - } + logger.LogDebug("Registering webhooks"); + await RegisterAll(); + } - public Task StopAsync(CancellationToken cancellationToken) - { - _developmentTunnel.Dispose(); - return Task.CompletedTask; - } + public Task StopAsync(CancellationToken cancellationToken) + { + developmentTunnel.Dispose(); + return Task.CompletedTask; } } diff --git a/src/KubeOps.Operator.Web/Webhooks/Admission/AdmissionRequest.cs b/src/KubeOps.Operator.Web/Webhooks/Admission/AdmissionRequest.cs index e43ae9c1..26bbe515 100644 --- a/src/KubeOps.Operator.Web/Webhooks/Admission/AdmissionRequest.cs +++ b/src/KubeOps.Operator.Web/Webhooks/Admission/AdmissionRequest.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using k8s; using k8s.Models; @@ -18,6 +19,7 @@ public sealed class AdmissionRequest : AdmissionReview /// Admission request data. /// [JsonPropertyName("request")] + [Required] public AdmissionRequestData Request { get; init; } = new(); /// @@ -31,6 +33,7 @@ public sealed class AdmissionRequestData /// The unique ID of the admission request. /// [JsonPropertyName("uid")] + [Required] public string Uid { get; init; } = string.Empty; /// @@ -39,6 +42,7 @@ public sealed class AdmissionRequestData /// "CONNECT" does exist, but is not supported by the operator-sdk. /// [JsonPropertyName("operation")] + [Required] public string Operation { get; init; } = string.Empty; /// @@ -59,6 +63,7 @@ public sealed class AdmissionRequestData /// A flag to indicate if the API was called with the "dryRun" flag. /// [JsonPropertyName("dryRun")] + [Required] public bool DryRun { get; init; } } } diff --git a/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationResult.cs b/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationResult.cs index d37f65fb..eb48944d 100644 --- a/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationResult.cs +++ b/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationResult.cs @@ -18,10 +18,6 @@ public record MutationResult(TEntity? ModifiedObject = default) : IActi { private const string JsonPatch = "JSONPatch"; - internal string Uid { get; init; } = string.Empty; - - internal JsonNode? OriginalObject { get; init; } - public bool Valid { get; init; } = true; /// @@ -42,6 +38,10 @@ public record MutationResult(TEntity? ModifiedObject = default) : IActi /// public IList Warnings { get; init; } = new List(); + internal string Uid { get; init; } = string.Empty; + + internal JsonNode? OriginalObject { get; init; } + /// public async Task ExecuteResultAsync(ActionContext context) { diff --git a/src/KubeOps.Operator.Web/Webhooks/Admission/Validation/ValidationResult.cs b/src/KubeOps.Operator.Web/Webhooks/Admission/Validation/ValidationResult.cs index 65e8f738..33fcea43 100644 --- a/src/KubeOps.Operator.Web/Webhooks/Admission/Validation/ValidationResult.cs +++ b/src/KubeOps.Operator.Web/Webhooks/Admission/Validation/ValidationResult.cs @@ -9,8 +9,6 @@ namespace KubeOps.Operator.Web.Webhooks.Admission.Validation; /// 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. @@ -29,6 +27,8 @@ public record ValidationResult(bool Valid = true) : IActionResult /// public IList Warnings { get; init; } = new List(); + internal string Uid { get; init; } = string.Empty; + /// public async Task ExecuteResultAsync(ActionContext context) { diff --git a/src/KubeOps.Operator.Web/Webhooks/Conversion/ConversionRequest.cs b/src/KubeOps.Operator.Web/Webhooks/Conversion/ConversionRequest.cs index f239319e..bb1df845 100644 --- a/src/KubeOps.Operator.Web/Webhooks/Conversion/ConversionRequest.cs +++ b/src/KubeOps.Operator.Web/Webhooks/Conversion/ConversionRequest.cs @@ -1,10 +1,8 @@ -using System.Diagnostics.CodeAnalysis; +using System.ComponentModel.DataAnnotations; using System.Runtime.Versioning; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using KubeOps.Operator.Web.Webhooks.Admission; - namespace KubeOps.Operator.Web.Webhooks.Conversion; /// @@ -19,6 +17,7 @@ public sealed class ConversionRequest : ConversionReview /// Conversion request data. /// [JsonPropertyName("request")] + [Required] public ConversionRequestData Request { get; init; } = new(); /// @@ -30,18 +29,20 @@ public sealed class ConversionRequestData /// The unique ID of the conversion request. /// [JsonPropertyName("uid")] + [Required] public string Uid { get; init; } = string.Empty; /// /// Target group/api version of the conversion. /// [JsonPropertyName("desiredAPIVersion")] + [Required] public string DesiredApiVersion { get; init; } = string.Empty; /// /// List of objects that need to be converted. /// [JsonPropertyName("objects")] - public JsonNode[] Objects { get; init; } = Array.Empty(); + public JsonNode[] Objects { get; init; } = []; } } diff --git a/src/KubeOps.Operator.Web/Webhooks/WebhookServiceBase.cs b/src/KubeOps.Operator.Web/Webhooks/WebhookServiceBase.cs index e6d7b793..1d416661 100644 --- a/src/KubeOps.Operator.Web/Webhooks/WebhookServiceBase.cs +++ b/src/KubeOps.Operator.Web/Webhooks/WebhookServiceBase.cs @@ -4,79 +4,79 @@ using KubeOps.KubernetesClient; using KubeOps.Transpiler; -namespace KubeOps.Operator.Web.Webhooks +namespace KubeOps.Operator.Web.Webhooks; + +internal abstract class WebhookServiceBase(IKubernetesClient client, WebhookLoader loader, WebhookConfig config) { - internal abstract class WebhookServiceBase(IKubernetesClient client, WebhookLoader loader, WebhookConfig config) - { - /// - /// The URI the webhooks will use to connect to the operator. - /// - private protected virtual Uri Uri { get; set; } = new($"https://{config.Hostname}:{config.Port}"); + /// + /// The URI the webhooks will use to connect to the operator. + /// + private protected virtual Uri Uri { get; set; } = new($"https://{config.Hostname}:{config.Port}"); + + private protected IKubernetesClient Client { get; } = client; - private protected IKubernetesClient Client { get; } = client; + /// + /// The PEM-encoded CA bundle for validating the webhook's certificate. + /// + private protected byte[]? CaBundle { get; set; } - /// - /// The PEM-encoded CA bundle for validating the webhook's certificate. - /// - private protected byte[]? CaBundle { get; set; } + internal async Task RegisterAll() + { + await RegisterValidators(); + await RegisterMutators(); + await RegisterConverters(); + } - internal async Task RegisterAll() + internal async Task RegisterConverters() + { + var conversionWebhooks = loader.ConversionWebhooks.ToList(); + if (conversionWebhooks.Count == 0) { - await RegisterValidators(); - await RegisterMutators(); - await RegisterConverters(); + return; } - internal async Task RegisterConverters() + foreach (var wh in conversionWebhooks) { - var conversionWebhooks = loader.ConversionWebhooks.ToList(); - if (conversionWebhooks.Count == 0) + var metadata = Entities.ToEntityMetadata(wh.BaseType!.GenericTypeArguments[0]).Metadata; + var crdName = $"{metadata.PluralName}.{metadata.Group}"; + + if (await Client.GetAsync(crdName) is not { } crd) { - return; + continue; } - foreach (var wh in conversionWebhooks) + var whUrl = $"{Uri}convert/{metadata.Group}/{metadata.PluralName}"; + crd.Spec.Conversion = new V1CustomResourceConversion("Webhook") { - var metadata = Entities.ToEntityMetadata(wh.BaseType!.GenericTypeArguments[0]).Metadata; - var crdName = $"{metadata.PluralName}.{metadata.Group}"; - - if (await Client.GetAsync(crdName) is not { } crd) - { - continue; - } - - var whUrl = $"{Uri}convert/{metadata.Group}/{metadata.PluralName}"; - crd.Spec.Conversion = new V1CustomResourceConversion("Webhook") + Webhook = new V1WebhookConversion { - Webhook = new V1WebhookConversion + ConversionReviewVersions = new[] { "v1" }, + ClientConfig = new Apiextensionsv1WebhookClientConfig { - ConversionReviewVersions = new[] { "v1" }, - ClientConfig = new Apiextensionsv1WebhookClientConfig - { - Url = whUrl, - CaBundle = CaBundle, - }, + Url = whUrl, + CaBundle = CaBundle, }, - }; + }, + }; - await Client.UpdateAsync(crd); - } + await Client.UpdateAsync(crd); } + } - internal async Task RegisterMutators() - { - var mutationWebhooks = loader - .MutationWebhooks - .Select(t => (HookTypeName: t.BaseType!.GenericTypeArguments[0].Name.ToLowerInvariant(), - Entities.ToEntityMetadata(t.BaseType!.GenericTypeArguments[0]).Metadata)) - .Select(hook => new V1MutatingWebhook + internal async Task RegisterMutators() + { + var mutationWebhooks = loader + .MutationWebhooks + .Select(t => (HookTypeName: t.BaseType!.GenericTypeArguments[0].Name.ToLowerInvariant(), + Entities.ToEntityMetadata(t.BaseType!.GenericTypeArguments[0]).Metadata)) + .Select(hook => new V1MutatingWebhook + { + Name = $"mutate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}", + MatchPolicy = "Exact", + AdmissionReviewVersions = new[] { "v1" }, + SideEffects = "None", + Rules = new[] { - Name = $"mutate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}", - MatchPolicy = "Exact", - AdmissionReviewVersions = new[] { "v1" }, - SideEffects = "None", - Rules = new[] - { new V1RuleWithOperations { Operations = new[] { "*" }, @@ -84,38 +84,38 @@ internal async Task RegisterMutators() ApiGroups = new[] { hook.Metadata.Group }, ApiVersions = new[] { hook.Metadata.Version }, }, - }, - ClientConfig = new Admissionregistrationv1WebhookClientConfig - { - Url = $"{Uri}mutate/{hook.HookTypeName}", - CaBundle = CaBundle, - }, - }); + }, + ClientConfig = new Admissionregistrationv1WebhookClientConfig + { + Url = $"{Uri}mutate/{hook.HookTypeName}", + CaBundle = CaBundle, + }, + }); - var mutatorConfig = new V1MutatingWebhookConfiguration( - metadata: new V1ObjectMeta(name: "dev-mutators"), - webhooks: mutationWebhooks.ToList()).Initialize(); + var mutatorConfig = new V1MutatingWebhookConfiguration( + metadata: new V1ObjectMeta(name: "dev-mutators"), + webhooks: mutationWebhooks.ToList()).Initialize(); - if (mutatorConfig.Webhooks.Any()) - { - await Client.SaveAsync(mutatorConfig); - } + if (mutatorConfig.Webhooks.Any()) + { + await Client.SaveAsync(mutatorConfig); } + } - internal async Task RegisterValidators() - { - var validationWebhooks = loader - .ValidationWebhooks - .Select(t => (HookTypeName: t.BaseType!.GenericTypeArguments[0].Name.ToLowerInvariant(), - Entities.ToEntityMetadata(t.BaseType!.GenericTypeArguments[0]).Metadata)) - .Select(hook => new V1ValidatingWebhook + internal async Task RegisterValidators() + { + var validationWebhooks = loader + .ValidationWebhooks + .Select(t => (HookTypeName: t.BaseType!.GenericTypeArguments[0].Name.ToLowerInvariant(), + Entities.ToEntityMetadata(t.BaseType!.GenericTypeArguments[0]).Metadata)) + .Select(hook => new V1ValidatingWebhook + { + Name = $"validate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}", + MatchPolicy = "Exact", + AdmissionReviewVersions = new[] { "v1" }, + SideEffects = "None", + Rules = new[] { - Name = $"validate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}", - MatchPolicy = "Exact", - AdmissionReviewVersions = new[] { "v1" }, - SideEffects = "None", - Rules = new[] - { new V1RuleWithOperations { Operations = new[] { "*" }, @@ -123,22 +123,21 @@ internal async Task RegisterValidators() ApiGroups = new[] { hook.Metadata.Group }, ApiVersions = new[] { hook.Metadata.Version }, }, - }, - ClientConfig = new Admissionregistrationv1WebhookClientConfig - { - Url = $"{Uri}validate/{hook.HookTypeName}", - CaBundle = CaBundle, - }, - }); + }, + ClientConfig = new Admissionregistrationv1WebhookClientConfig + { + Url = $"{Uri}validate/{hook.HookTypeName}", + CaBundle = CaBundle, + }, + }); - var validatorConfig = new V1ValidatingWebhookConfiguration( - metadata: new V1ObjectMeta(name: "dev-validators"), - webhooks: validationWebhooks.ToList()).Initialize(); + var validatorConfig = new V1ValidatingWebhookConfiguration( + metadata: new V1ObjectMeta(name: "dev-validators"), + webhooks: validationWebhooks.ToList()).Initialize(); - if (validatorConfig.Webhooks.Any()) - { - await Client.SaveAsync(validatorConfig); - } + if (validatorConfig.Webhooks.Any()) + { + await Client.SaveAsync(validatorConfig); } } } diff --git a/src/KubeOps.Operator/Events/KubeOpsEventPublisherFactory.cs b/src/KubeOps.Operator/Events/KubeOpsEventPublisherFactory.cs index 20ef9278..0988ae1d 100644 --- a/src/KubeOps.Operator/Events/KubeOpsEventPublisherFactory.cs +++ b/src/KubeOps.Operator/Events/KubeOpsEventPublisherFactory.cs @@ -22,7 +22,7 @@ public EventPublisher Create() => async (entity, reason, message, type, token) = { var @namespace = entity.Namespace() ?? "default"; logger.LogTrace( - "Encoding event name with: {resourceName}.{resourceNamespace}.{reason}.{message}.{type}.", + "Encoding event name with: {ResourceName}.{ResourceNamespace}.{Reason}.{Message}.{Type}.", entity.Name(), @namespace, reason, @@ -35,7 +35,7 @@ public EventPublisher Create() => async (entity, reason, message, type, token) = SHA512.HashData( Encoding.UTF8.GetBytes(eventName))); - logger.LogTrace("""Search or create event with name "{name}".""", encodedEventName); + logger.LogTrace("""Search or create event with name "{Name}".""", encodedEventName); Corev1Event? @event; try @@ -46,7 +46,7 @@ public EventPublisher Create() => async (entity, reason, message, type, token) = { logger.LogError( e, - """Could not receive event with name "{name}" on entity "{kind}/{name}".""", + """Could not receive event with name "{EventName}" on entity "{Kind}/{Name}".""", eventName, entity.Kind, entity.Name()); @@ -80,7 +80,7 @@ public EventPublisher Create() => async (entity, reason, message, type, token) = @event.Count++; @event.LastTimestamp = DateTime.UtcNow; logger.LogTrace( - "Save event with new count {count} and last timestamp {timestamp}", + "Save event with new count {Count} and last timestamp {Timestamp}", @event.Count, @event.LastTimestamp); @@ -88,7 +88,7 @@ public EventPublisher Create() => async (entity, reason, message, type, token) = { await client.SaveAsync(@event, token); logger.LogInformation( - """Created or updated event with name "{name}" to new count {count} on entity "{kind}/{name}".""", + """Created or updated event with name "{EventName}" to new count {Count} on entity "{Kind}/{Name}".""", eventName, @event.Count, entity.Kind, @@ -98,7 +98,7 @@ public EventPublisher Create() => async (entity, reason, message, type, token) = { logger.LogError( e, - """Could not publish event with name "{name}" on entity "{kind}/{name}".""", + """Could not publish event with name "{EventName}" on entity "{Kind}/{Name}".""", eventName, entity.Kind, entity.Name()); diff --git a/src/KubeOps.Operator/Finalizer/KubeOpsEventFinalizerAttacherFactory.cs b/src/KubeOps.Operator/Finalizer/KubeOpsEventFinalizerAttacherFactory.cs index a79d070d..7946536d 100644 --- a/src/KubeOps.Operator/Finalizer/KubeOpsEventFinalizerAttacherFactory.cs +++ b/src/KubeOps.Operator/Finalizer/KubeOpsEventFinalizerAttacherFactory.cs @@ -19,7 +19,7 @@ public EntityFinalizerAttacher Create { logger.LogTrace( - """Try to add finalizer "{finalizer}" on entity "{kind}/{name}".""", + """Try to add finalizer "{Finalizer}" on entity "{Kind}/{Name}".""", identifier, entity.Kind, entity.Name()); @@ -30,7 +30,7 @@ public EntityFinalizerAttacher Create elector.RunAsync(_cts.Token), CancellationToken.None); + _ = Task.Run(() => elector.RunUntilLeadershipLostAsync(_cts.Token), CancellationToken.None); return Task.CompletedTask; } diff --git a/src/KubeOps.Operator/Queue/EntityRequeueBackgroundService.cs b/src/KubeOps.Operator/Queue/EntityRequeueBackgroundService.cs index 6b95f568..71a7e0cc 100644 --- a/src/KubeOps.Operator/Queue/EntityRequeueBackgroundService.cs +++ b/src/KubeOps.Operator/Queue/EntityRequeueBackgroundService.cs @@ -113,13 +113,13 @@ private async Task WatchAsync(CancellationToken cancellationToken) private async Task ReconcileSingleAsync(TEntity queued, CancellationToken cancellationToken) { - logger.LogTrace("""Execute requested requeued reconciliation for "{name}".""", queued.Name()); + logger.LogTrace("""Execute requested requeued reconciliation for "{Name}".""", queued.Name()); if (await client.GetAsync(queued.Name(), queued.Namespace(), cancellationToken) is not { } entity) { logger.LogWarning( - """Requeued entity "{name}" was not found. Skipping reconciliation.""", queued.Name()); + """Requeued entity "{Name}" was not found. Skipping reconciliation.""", queued.Name()); return; } diff --git a/src/KubeOps.Operator/Queue/KubeOpsEntityRequeueFactory.cs b/src/KubeOps.Operator/Queue/KubeOpsEntityRequeueFactory.cs index 31000164..59a832f6 100644 --- a/src/KubeOps.Operator/Queue/KubeOpsEntityRequeueFactory.cs +++ b/src/KubeOps.Operator/Queue/KubeOpsEntityRequeueFactory.cs @@ -19,7 +19,7 @@ public EntityRequeue Create() var queue = services.GetRequiredService>(); logger?.LogTrace( - """Requeue entity "{kind}/{name}" in {milliseconds}ms.""", + """Requeue entity "{Kind}/{Name}" in {Milliseconds}ms.""", entity.Kind, entity.Name(), timeSpan.TotalMilliseconds); diff --git a/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs b/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs index 1566e83d..58ba7b1a 100644 --- a/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs +++ b/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs @@ -161,7 +161,7 @@ private async Task WatchClientEventsAsync(CancellationToken stoppingToken) { if (e.Status.Code == (int)HttpStatusCode.Gone) { - logger.LogDebug("Watch restarting due to 410 HTTP Gone"); + logger.LogDebug(e, "Watch restarting due to 410 HTTP Gone"); break; } @@ -213,7 +213,7 @@ private async Task OnEventAsync(WatchEventType type, TEntity? entity, Cancellati if (entity.Generation() <= cachedGeneration) { logger.LogDebug( - """Entity "{kind}/{name}" modification did not modify generation. Skip event.""", + """Entity "{Kind}/{Name}" modification did not modify generation. Skip event.""", entity.Kind, entity.Name()); return; @@ -234,7 +234,7 @@ private async Task OnEventAsync(WatchEventType type, TEntity? entity, Cancellati break; default: logger.LogWarning( - """Received unsupported event "{eventType}" for "{kind}/{name}".""", + """Received unsupported event "{EventType}" for "{Kind}/{Name}".""", type, entity?.Kind, entity.Name()); @@ -250,7 +250,7 @@ private async Task OnWatchErrorAsync(Exception e) e.InnerException is JsonException && e.InnerException.Message.Contains("The input does not contain any JSON tokens"): logger.LogDebug( - """The watcher received an empty response for resource "{resource}".""", + """The watcher received an empty response for resource "{Resource}".""", typeof(TEntity)); return; @@ -258,19 +258,19 @@ e.InnerException is JsonException && e.InnerException is EndOfStreamException && e.InnerException.Message.Contains("Attempted to read past the end of the stream."): logger.LogDebug( - """The watcher received a known error from the watched resource "{resource}". This indicates that there are no instances of this resource.""", + """The watcher received a known error from the watched resource "{Resource}". This indicates that there are no instances of this resource.""", typeof(TEntity)); return; } - logger.LogError(e, """There was an error while watching the resource "{resource}".""", typeof(TEntity)); + logger.LogError(e, """There was an error while watching the resource "{Resource}".""", typeof(TEntity)); _watcherReconnectRetries++; var delay = TimeSpan .FromSeconds(Math.Pow(2, Math.Clamp(_watcherReconnectRetries, 0, 5))) .Add(TimeSpan.FromMilliseconds(new Random().Next(0, 1000))); logger.LogWarning( - "There were {retries} errors / retries in the watcher. Wait {seconds}s before next attempt to connect.", + "There were {Retries} errors / retries in the watcher. Wait {Seconds}s before next attempt to connect.", _watcherReconnectRetries, delay.TotalSeconds); await Task.Delay(delay); @@ -301,7 +301,7 @@ private async Task ReconcileFinalizersSequentialAsync(TEntity entity, Cancellati { } finalizer) { logger.LogDebug( - """Entity "{kind}/{name}" is finalizing but this operator has no registered finalizers for the identifier {finalizerIdentifier}.""", + """Entity "{Kind}/{Name}" is finalizing but this operator has no registered finalizers for the identifier {FinalizerIdentifier}.""", entity.Kind, entity.Name(), identifier); @@ -312,7 +312,7 @@ private async Task ReconcileFinalizersSequentialAsync(TEntity entity, Cancellati entity.RemoveFinalizer(identifier); await client.UpdateAsync(entity, cancellationToken); logger.LogInformation( - """Entity "{kind}/{name}" finalized with "{finalizer}".""", + """Entity "{Kind}/{Name}" finalized with "{Finalizer}".""", entity.Kind, entity.Name(), identifier); diff --git a/test/KubeOps.Cli.Test/KubeOps.Cli.Test.csproj b/test/KubeOps.Cli.Test/KubeOps.Cli.Test.csproj index ce1ef0e3..0af15b70 100644 --- a/test/KubeOps.Cli.Test/KubeOps.Cli.Test.csproj +++ b/test/KubeOps.Cli.Test/KubeOps.Cli.Test.csproj @@ -6,11 +6,11 @@ - - - - - + + + + + diff --git a/test/KubeOps.Generator.Test/KubeOps.Generator.Test.csproj b/test/KubeOps.Generator.Test/KubeOps.Generator.Test.csproj index ca277cff..06faf69e 100644 --- a/test/KubeOps.Generator.Test/KubeOps.Generator.Test.csproj +++ b/test/KubeOps.Generator.Test/KubeOps.Generator.Test.csproj @@ -3,10 +3,10 @@ - - - + + + - + diff --git a/test/KubeOps.Operator.Test/KubeOps.Operator.Test.csproj b/test/KubeOps.Operator.Test/KubeOps.Operator.Test.csproj index e1176b36..974c6f29 100644 --- a/test/KubeOps.Operator.Test/KubeOps.Operator.Test.csproj +++ b/test/KubeOps.Operator.Test/KubeOps.Operator.Test.csproj @@ -1,12 +1,12 @@ - + - - - - - + + + + + diff --git a/test/KubeOps.Operator.Web.Test/Builder/OperatorBuilderExtensions.Test.cs b/test/KubeOps.Operator.Web.Test/Builder/OperatorBuilderExtensions.Test.cs index a72dc3d7..9dc495df 100644 --- a/test/KubeOps.Operator.Web.Test/Builder/OperatorBuilderExtensions.Test.cs +++ b/test/KubeOps.Operator.Web.Test/Builder/OperatorBuilderExtensions.Test.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -using FluentAssertions; +using FluentAssertions; using KubeOps.Abstractions.Builder; using KubeOps.Abstractions.Certificates; diff --git a/test/KubeOps.Operator.Web.Test/Certificates/CertificateGenerator.Test.cs b/test/KubeOps.Operator.Web.Test/Certificates/CertificateGenerator.Test.cs index 6d3cb78b..61fce27f 100644 --- a/test/KubeOps.Operator.Web.Test/Certificates/CertificateGenerator.Test.cs +++ b/test/KubeOps.Operator.Web.Test/Certificates/CertificateGenerator.Test.cs @@ -2,42 +2,43 @@ using FluentAssertions; -namespace KubeOps.Operator.Web.Test.Certificates +using KubeOps.Operator.Web.Certificates; + +namespace KubeOps.Operator.Web.Test.Certificates; + +public class CertificateGeneratorTest : IDisposable { - public class CertificateGeneratorTest : IDisposable + private readonly CertificateGenerator _certificateGenerator = new(Environment.MachineName); + + [Fact] + public void Root_Should_Be_Valid() + { + var (certificate, key) = _certificateGenerator.Root; + + certificate.Should().NotBeNull(); + DateTime.Parse(certificate.GetEffectiveDateString()).Should().BeOnOrBefore(DateTime.UtcNow); + certificate.Extensions.Any(e => e is X509BasicConstraintsExtension basic && basic.CertificateAuthority).Should().BeTrue(); + certificate.HasPrivateKey.Should().BeTrue(); + + key.Should().NotBeNull(); + } + + [Fact] + public void Server_Should_Be_Valid() + { + var (certificate, key) = _certificateGenerator.Server; + + certificate.Should().NotBeNull(); + DateTime.Parse(certificate.GetEffectiveDateString()).Should().BeOnOrBefore(DateTime.UtcNow); + certificate.Extensions.Any(e => e is X509BasicConstraintsExtension basic && basic.CertificateAuthority).Should().BeFalse(); + certificate.HasPrivateKey.Should().BeFalse(); + + key.Should().NotBeNull(); + } + + public void Dispose() { - private readonly CertificateGenerator _certificateGenerator = new(Environment.MachineName); - - [Fact] - public void Root_Should_Be_Valid() - { - var (certificate, key) = _certificateGenerator.Root; - - certificate.Should().NotBeNull(); - DateTime.Parse(certificate.GetEffectiveDateString()).Should().BeOnOrBefore(DateTime.UtcNow); - certificate.Extensions.Any(e => e is X509BasicConstraintsExtension basic && basic.CertificateAuthority).Should().BeTrue(); - certificate.HasPrivateKey.Should().BeTrue(); - - key.Should().NotBeNull(); - } - - [Fact] - public void Server_Should_Be_Valid() - { - var (certificate, key) = _certificateGenerator.Server; - - certificate.Should().NotBeNull(); - DateTime.Parse(certificate.GetEffectiveDateString()).Should().BeOnOrBefore(DateTime.UtcNow); - certificate.Extensions.Any(e => e is X509BasicConstraintsExtension basic && basic.CertificateAuthority).Should().BeFalse(); - certificate.HasPrivateKey.Should().BeFalse(); - - key.Should().NotBeNull(); - } - - public void Dispose() - { - _certificateGenerator.Dispose(); - GC.SuppressFinalize(this); - } + _certificateGenerator.Dispose(); + GC.SuppressFinalize(this); } } diff --git a/test/KubeOps.Operator.Web.Test/KubeOps.Operator.Web.Test.csproj b/test/KubeOps.Operator.Web.Test/KubeOps.Operator.Web.Test.csproj index 6ec920f9..2b8bc919 100644 --- a/test/KubeOps.Operator.Web.Test/KubeOps.Operator.Web.Test.csproj +++ b/test/KubeOps.Operator.Web.Test/KubeOps.Operator.Web.Test.csproj @@ -1,13 +1,13 @@ - + - - - - - + + + + + @@ -16,4 +16,8 @@ + + + CS0618 + diff --git a/test/KubeOps.Transpiler.Test/KubeOps.Transpiler.Test.csproj b/test/KubeOps.Transpiler.Test/KubeOps.Transpiler.Test.csproj index e3e7eaf0..238e6337 100644 --- a/test/KubeOps.Transpiler.Test/KubeOps.Transpiler.Test.csproj +++ b/test/KubeOps.Transpiler.Test/KubeOps.Transpiler.Test.csproj @@ -1,13 +1,13 @@ - + - - - - - + + + + +